Repository: mathesar-foundation/mathesar

Issue:
`Distinct list` results in `NaN:NaN` when applied on Duration columns.
## Description
If `Distinct List` transformation is applied on `Duration` columns, it generates pills of `NaN:NaN`.


## Expected behavior
The format of the pills should adhere to the cell representaion of `Duration` type in table.

# Screenshot

<img width="960" alt="image" src="https://github.com/centerofci/mathesar/assets/64671908/a6e51d3b-8999-4550-8a44-39535e11c856">




Discussion:
This problem seems to be in frontend.

<img width="269" alt="image" src="https://github.com/centerofci/mathesar/assets/64671908/b8c71ab1-4978-4b09-a969-b9c1cc2f6aed">


Resolving patch:
diff --git a/conftest.py b/conftest.py
--- a/conftest.py
+++ b/conftest.py
@@ -262,6 +262,7 @@ def _get_connection_string(username, password, hostname, database):
 LIBRARY_CHECKOUTS_SQL = os.path.join(RESOURCES, "library_add_checkouts.sql")
 FRAUDULENT_PAYMENTS_SQL = os.path.join(RESOURCES, "fraudulent_payments.sql")
 PLAYER_PROFILES_SQL = os.path.join(RESOURCES, "player_profiles.sql")
+MARATHON_ATHLETES_SQL = os.path.join(RESOURCES, "marathon_athletes.sql")
 
 
 @pytest.fixture
@@ -367,3 +368,20 @@ def players_db_table(engine_with_player_profiles):
     metadata = MetaData(bind=engine)
     table = Table("Players", metadata, schema=schema, autoload_with=engine)
     return table
+
+
+@pytest.fixture
+def engine_with_marathon_athletes(engine_with_schema):
+    engine, schema = engine_with_schema
+    with engine.begin() as conn, open(MARATHON_ATHLETES_SQL) as f:
+        conn.execute(text(f"SET search_path={schema}"))
+        conn.execute(text(f.read()))
+    yield engine, schema
+
+
+@pytest.fixture
+def athletes_db_table(engine_with_marathon_athletes):
+    engine, schema = engine_with_marathon_athletes
+    metadata = MetaData(bind=engine)
+    table = Table("Marathon", metadata, schema=schema, autoload_with=engine)
+    return table
diff --git a/db/types/custom/datetime.py b/db/types/custom/datetime.py
--- a/db/types/custom/datetime.py
+++ b/db/types/custom/datetime.py
@@ -3,9 +3,11 @@
 from sqlalchemy.dialects.postgresql import INTERVAL
 from sqlalchemy.dialects.postgresql import TIME as SA_TIME
 from sqlalchemy.dialects.postgresql import TIMESTAMP as SA_TIMESTAMP
+from sqlalchemy.dialects.postgresql import TEXT as SA_TEXT
 from sqlalchemy.types import TypeDecorator
 
 from db.types.exceptions import InvalidTypeParameters
+from db.types.custom.underlying_type import HasUnderlyingType
 
 
 class DATE(TypeDecorator):
@@ -186,9 +188,10 @@ def column_expression(self, column):
         )
 
 
-class Interval(TypeDecorator):
+class Interval(TypeDecorator, HasUnderlyingType):
     impl = INTERVAL
     cache_ok = True
+    underlying_type = SA_TEXT
 
     def __init__(self, *arg, **kwarg):
         TypeDecorator.__init__(self, *arg, **kwarg)


Test patch:
diff --git a/db/tests/resources/marathon_athletes.sql b/db/tests/resources/marathon_athletes.sql
new file mode 100644
--- /dev/null
+++ b/db/tests/resources/marathon_athletes.sql
@@ -0,0 +1,50 @@
+CREATE TABLE "Marathon" (
+  id integer NOT NULL,
+  "athlete" VARCHAR(255),
+  "gender" VARCHAR(255),
+  "finish time" Interval,
+  "city" VARCHAR(255)
+);
+
+CREATE SEQUENCE "Marathon_id_seq"
+    AS integer
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE "Marathon_id_seq" OWNED BY "Marathon".id;
+
+ALTER TABLE ONLY "Marathon" 
+  ALTER COLUMN id SET DEFAULT nextval('"Marathon_id_seq"'::regclass);
+
+INSERT INTO "Marathon" ("athlete", "gender", "finish time", "city") VALUES
+('Eliud Kipchoge', 'Male', 'PT2H1M39S', 'Berlin');
+
+INSERT INTO "Marathon" ("athlete", "gender", "finish time", "city") VALUES
+('Brigid Kosgei', 'Female', 'PT2H14M4S', 'Chicago');
+
+INSERT INTO "Marathon" ("athlete", "gender", "finish time", "city") VALUES
+('Dennis Kimetto', 'Male', 'PT2H2M57S', 'Berlin');
+
+INSERT INTO "Marathon" ("athlete", "gender", "finish time", "city") VALUES
+('Paula Radcliffe', 'Female', 'PT2H15M25S', 'London');
+
+INSERT INTO "Marathon" ("athlete", "gender", "finish time", "city") VALUES
+('Haile Gebrselassie', 'Male', 'PT2H3M59S', 'Berlin');
+
+INSERT INTO "Marathon" ("athlete", "gender", "finish time", "city") VALUES
+('Mary Keitany', 'Female', 'PT2H18M35S', 'London');
+
+INSERT INTO "Marathon" ("athlete", "gender", "finish time", "city") VALUES
+('Geoffrey Kamworor', 'Male', 'PT2H4M0S', 'Berlin');
+
+INSERT INTO "Marathon" ("athlete", "gender", "finish time", "city") VALUES
+('Tirunesh Dibaba', 'Female', 'PT2H17M56S', 'London');
+
+INSERT INTO "Marathon" ("athlete", "gender", "finish time", "city") VALUES
+('Wilson Kipsang', 'Male', 'PT2H3M23S', 'Berlin');
+
+INSERT INTO "Marathon" ("athlete", "gender", "finish time", "city") VALUES
+('Florence Kiplagat', 'Female', 'PT2H17M45S', 'Chicago');
diff --git a/mathesar/tests/api/conftest.py b/mathesar/tests/api/conftest.py
--- a/mathesar/tests/api/conftest.py
+++ b/mathesar/tests/api/conftest.py
@@ -309,6 +309,12 @@ def players_ma_table(db_table_to_dj_table, players_db_table):
     return db_table_to_dj_table(players_db_table)
 
 
+@pytest.fixture
+def athletes_ma_table(db_table_to_dj_table, athletes_db_table):
+    reset_reflection()
+    return db_table_to_dj_table(athletes_db_table)
+
+
 @pytest.fixture
 def table_with_unknown_types(create_schema, get_uid, engine):
     prefix = "unknown_types"
diff --git a/mathesar/tests/api/query/test_aggregation_functions.py b/mathesar/tests/api/query/test_aggregation_functions.py
--- a/mathesar/tests/api/query/test_aggregation_functions.py
+++ b/mathesar/tests/api/query/test_aggregation_functions.py
@@ -966,6 +966,88 @@ def test_percentage_true_aggregation(payments_ma_table, get_uid, client):
     assert sorted(actual_records, key=lambda x: x['Payment Mode']) == expect_records
 
 
+def test_list_aggregation_intrval(athletes_ma_table, get_uid, client):
+    _ = athletes_ma_table
+    athletes = {
+        t["name"]: t for t in client.get("/api/db/v0/tables/").json()["results"]
+    }["Marathon"]
+    columns = {
+        c["name"]: c for c in athletes["columns"]
+    }
+    request_data = {
+        "name": get_uid(),
+        "base_table": athletes["id"],
+        "initial_columns": [
+            {"id": columns["city"]["id"], "alias": "city"},
+            {"id": columns["finish time"]["id"], "alias": "finish time"},
+        ],
+        "display_names": {
+            "city": "city",
+            "finish time": "finish time",
+        },
+        "display_options": {
+            "ciy": {
+                display_option_origin: "country",
+            },
+            "finish time": {
+                display_option_origin: "finish time",
+            },
+        },
+        "transformations": [
+            {
+                "spec": {
+                    "grouping_expressions": [
+                        {
+                            "input_alias": "city",
+                            "output_alias": "city",
+                        }
+                    ],
+                    "aggregation_expressions": [
+                        {
+                            "input_alias": "finish time",
+                            "output_alias": "finish time",
+                            "function": "distinct_aggregate_to_array",
+                        }
+                    ]
+                },
+                "type": "summarize",
+            }
+        ]
+    }
+    response = client.post('/api/db/v0/queries/', data=request_data)
+    assert response.status_code == 201
+    query_id = response.json()['id']
+    expect_records = [
+        {
+            "city": "Berlin",
+            "finish time": [
+                "02:01:39",
+                "02:02:57",
+                "02:03:23",
+                "02:03:59",
+                "02:04:00"
+            ]
+        },
+        {
+            "city": "Chicago",
+            "finish time": [
+                "02:14:04",
+                "02:17:45"
+            ]
+        },
+        {
+            "city": "London",
+            "finish time": [
+                "02:15:25",
+                "02:17:56",
+                "02:18:35"
+            ]
+        }
+    ]
+    actual_records = client.get(f'/api/db/v0/queries/{query_id}/records/').json()['results']
+    assert sorted(actual_records, key=lambda x: x['city']) == expect_records
+
+
 def test_list_aggregation_mathesar_json_array(players_ma_table, get_uid, client):
     _ = players_ma_table
     players = {