Repository: mathesar-foundation/mathesar

Issue:
Use the same `display_options` for columns present in tables, for explorations
Also ensure the `display_options` persist when aggregating values


Discussion:
Blocked on #1805
This seems to already be done on backend. Removing `work: backend` label. Readd if I misunderstood something.
@dmos62 This is not yet implemented, or there must be a bug because this does not work.

* Set a display option to one of the columns eg,. `Items.acquisition_date`, set `format` as `friendly`. The columns response to the table contains this display option.
* Run a query over the `Items` table by adding `acquisition_date` as an initial column.
* Notice that the query run response contains `null` for display_options in `column_metadata` for the added initial column.
@pavish noted that #2008 is only part of the solution. I did not realise that we want display options to be inherited by new aliases (issue body may have been a clue). I can see how we can do this with some aliases (those generated via grouping), but not those generated via aggregation (without a lot of fuss).

I don't see how it makes sense for an aggregated column to inherit display_options from the column it was aggregated from. What the output of an aggregation will be depends on the aggregation function; it might be a different type (e.g. anything really, but often a number or an array).

Is the goal that frontend could style array items based on the display options for the column that was passed to an aggregation function to generate that array? What about other aggregation functions (i.e. functions other than `aggregate_to_array`)?

So far I've considered any kind of "inheritance" to be broken when you aggregate. Allowing inheritance through aggregation would be quite fiddly. I suggest postponing this feature. It requires an order of magnitude more work than inheritance for grouped columns.
@dmos62 I don't know if we can postpone this because formatting is important for types like dates and number, which will be displayed as arrays when aggregated. This would probably be needed for the demo video.

We do not have to worry about other functions, and the focus is currently only on the `aggregate_to_array` function.

@kgodey We need your thoughts on this.
@pavish gotcha.

I'm in favor of sidestepping this somehow. Installing special treatment for `aggregate_to_array` would be not easy and technically indebting: preferably we'd focus on some more general mechanism at some point down the road.

Edit: Note: I'll be working on having display_options be inherited by grouped aliases and the like. That's much simpler than doing the same for aggregated aliases.
We should not postpone this. I'm sure we can find a quick solution.

Resolving patch:
diff --git a/db/transforms/base.py b/db/transforms/base.py
--- a/db/transforms/base.py
+++ b/db/transforms/base.py
@@ -70,12 +70,17 @@ def __eq__(self, other):
     @property
     def map_of_output_alias_to_input_alias(self):
         """
-        Expected to return a mapping of output aliases to input aliases.
+        Expected to return a mapping of output alias to input alias.
 
-        Useful when looking for parent aliases of a given alias.
+        Useful when looking for ancestor aliases of a given alias.
 
-        Notice that the reverse mapping (from input aliases to output aliases) would be
+        Note, the reverse mapping (from input aliases to output aliases) would be
         significantly different, because a single input alias can map to multiple output aliases.
+
+        Note, this presumes that a single output alias maps to no more than a single input alias,
+        but that's not true at least in the case of multi-column aggregation functions [0].
+
+        [0] http://www.postgresonline.com/journal/archives/105-How-to-create-multi-column-aggregates.html
         """
         return dict()
 
diff --git a/mathesar/models/query.py b/mathesar/models/query.py
--- a/mathesar/models/query.py
+++ b/mathesar/models/query.py
@@ -208,10 +208,6 @@ def initial_columns_described(self):
         )
 
     def _describe_query_column(self, sa_col):
-        """
-        Note, has some conditional fields depending on whether the column is an initial column or
-        is generated mid-query (created via a summarization).
-        """
         alias = sa_col.name
         initial_db_column = self._get_db_initial_column_by_alias(alias)
         is_initial_column = initial_db_column is not None
@@ -341,13 +337,20 @@ def _get_display_name_for_alias(self, alias):
 
     def _get_display_options_for_alias(self, alias):
         display_options = None
+        # Try getting display options from this model's field
         if self.display_options:
             display_options = self.display_options.get(alias)
+        # Try getting display options from Dj column, if this is an initial column
         if display_options is None:
-            # notice that this isn't meant to support non-initial-column aliases
             dj_col = self._map_of_initial_col_alias_to_dj_column.get(alias)
             if dj_col:
                 display_options = dj_col.display_options
+        # Try recursively repeating these steps for its parent alias, if it can be found
+        if display_options is None:
+            parent_alias = \
+                self.db_query.map_of_output_alias_to_input_alias.get(alias)
+            if parent_alias:
+                display_options = self._get_display_options_for_alias(parent_alias)
         return display_options
 
     @property


Test patch:
diff --git a/mathesar/tests/api/query/test_library_demo_reports.py b/mathesar/tests/api/query/test_library_demo_reports.py
--- a/mathesar/tests/api/query/test_library_demo_reports.py
+++ b/mathesar/tests/api/query/test_library_demo_reports.py
@@ -1,6 +1,11 @@
 import pytest
 
 
+# Otherwise meaningless display option key meant to designate on which alias a given display
+# option was defined, so that we can test if display option inheritance works as expected.
+display_option_origin = "display_option_origin"
+
+
 @pytest.fixture
 def create_overdue_books_query(library_ma_tables, get_uid, client):
     checkouts = library_ma_tables["Checkouts"]
@@ -14,6 +19,14 @@ def create_overdue_books_query(library_ma_tables, get_uid, client):
             "email": "Patron Email",
             "Title List": "Titles"
         },
+        "display_options": {
+            "email": {
+                display_option_origin: "email",
+            },
+            "Book Title": {
+                display_option_origin: "Book Title",
+            },
+        },
         "initial_columns": [
             {
                 "id": checkouts.get_column_by_name("id").id,
@@ -93,22 +106,27 @@ def check_overdue_books_columns(create_overdue_books_query, client):
             'display_name': 'Titles',
             'type': '_array',
             'type_options': {'item_type': 'text'},
-            'display_options': None,
+            'display_options': {
+                display_option_origin: 'Book Title',
+            },
             'is_initial_column': False,
             'input_table_name': None,
             'input_column_name': None,
             'input_alias': 'Book Title',
-        }, {
+        },
+        {
             'alias': 'email',
             'display_name': 'Patron Email',
             'type': 'mathesar_types.email',
             'type_options': None,
-            'display_options': None,
+            'display_options': {
+                display_option_origin: 'email',
+            },
             'is_initial_column': True,
             'input_table_name': 'Patrons',
             'input_column_name': 'Email',
             'input_alias': None,
-        }
+        },
     ]
     actual_response_data = client.get(f'/api/db/v0/queries/{query_id}/columns/').json()
     assert sorted(actual_response_data, key=lambda x: x['alias']) == expect_response_data
@@ -171,6 +189,14 @@ def create_monthly_checkouts_query(run_overdue_books_scenario, get_uid, client):
             "Checkout Month": "Month",
             "Count": "Number of Checkouts",
         },
+        "display_options": {
+            "Checkout Time": {
+                display_option_origin: "Checkout Time",
+            },
+            "id": {
+                display_option_origin: "id",
+            },
+        },
         "transformations": [
             {
                 "spec": {
@@ -207,7 +233,9 @@ def check_monthly_checkouts_columns(create_monthly_checkouts_query, client):
             'display_name': 'Month',
             'type': 'text',
             'type_options': None,
-            'display_options': None,
+            'display_options': {
+                display_option_origin: "Checkout Time",
+            },
             'is_initial_column': False,
             'input_table_name': None,
             'input_column_name': None,
@@ -217,7 +245,9 @@ def check_monthly_checkouts_columns(create_monthly_checkouts_query, client):
             'display_name': 'Number of Checkouts',
             'type': 'integer',
             'type_options': None,
-            'display_options': None,
+            'display_options': {
+                display_option_origin: "id",
+            },
             'is_initial_column': False,
             'input_table_name': None,
             'input_column_name': None,