Repository: mathesar-foundation/mathesar

Issue:
Return RFC 3339 (ish) style date / time values from API
## Problem
<!-- Please provide a clear and concise description of the problem that this feature request is designed to solve.-->

Currently, the style of date/time values returned by the API is whatever the Django default for handling `python` date / time types is.  Further, since the dates and times are made into `python` types during the process, precision is lost, as well as functionality around dates before the common era, and dates with years containing more than 4 digits.

## Proposed solution
<!-- A clear and concise description of your proposed solution or feature. -->

We should parse the dates and times into RFC 3339 compliant strings in the database, and then pass those strings up through the API.  We will need to make the following slight extensions and refinements
- We will append `AD` or `BC` to each date or datetime
- We will allow years with more than 4 digits
- We will always take the 'verbose' version when there's a choice (e.g., a timezone will always be `+08:00`, not `+08`).

## Additional context
<!-- Add any other context or screenshots about the feature request here.-->

There is a spec being written in the wiki in parallel with the implementation of this issue.  The PR is here:  https://github.com/centerofci/mathesar-wiki/pull/35 .  


Discussion:

Resolving patch:
diff --git a/db/types/__init__.py b/db/types/__init__.py
--- a/db/types/__init__.py
+++ b/db/types/__init__.py
@@ -1,5 +1,5 @@
 from sqlalchemy import DECIMAL as sa_decimal
-from db.types import email, money, datetime, uri, interval
+from db.types import email, money, datetime, uri
 from db.types.base import PostgresType
 
 
@@ -7,9 +7,10 @@
     # For some reason, SQLAlchemy doesn't add DECIMAL to the default
     # ischema_names supported by a PostgreSQL engine
     PostgresType.DECIMAL.value: sa_decimal,
-    PostgresType.INTERVAL.value: interval.Interval,
+    PostgresType.INTERVAL.value: datetime.Interval,
     email.DB_TYPE: email.Email,
     money.DB_TYPE: money.MathesarMoney,
+    datetime.DATE_TYPE: datetime.DATE,
     datetime.TIME_ZONE_DB_TYPE: datetime.TIME_WITH_TIME_ZONE,
     datetime.WITHOUT_TIME_ZONE_DB_TYPE: datetime.TIME_WITHOUT_TIME_ZONE,
     datetime.TIMESTAMP_TIME_ZONE_DB_TYPE: datetime.TIMESTAMP_WITH_TIME_ZONE,
diff --git a/db/types/datetime.py b/db/types/datetime.py
--- a/db/types/datetime.py
+++ b/db/types/datetime.py
@@ -1,61 +1,263 @@
-from sqlalchemy.dialects.postgresql.base import TIME as SA_TIME
-from sqlalchemy.dialects.postgresql.base import TIMESTAMP as SA_TIMESTAMP
+from sqlalchemy import case, func, and_
+from sqlalchemy.dialects.postgresql import DATE as SA_DATE
+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.types import TypeDecorator
 
 from db.types import base
+from db.types.exceptions import InvalidTypeParameters
 
 TIME_ZONE_DB_TYPE = base.PostgresType.TIME_WITH_TIME_ZONE.value
 WITHOUT_TIME_ZONE_DB_TYPE = base.PostgresType.TIME_WITHOUT_TIME_ZONE.value
 TIMESTAMP_TIME_ZONE_DB_TYPE = base.PostgresType.TIMESTAMP_WITH_TIME_ZONE.value
 TIMESTAMP_WITHOUT_TIME_ZONE_DB_TYPE = base.PostgresType.TIMESTAMP_WITHOUT_TIME_ZONE.value
+DATE_TYPE = base.PostgresType.DATE.value
 
 
-class TIME_WITHOUT_TIME_ZONE(SA_TIME):
+class DATE(TypeDecorator):
+    impl = SA_DATE
+    cache_ok = True
+
+    def column_expression(self, column):
+        format_str = "YYYY-MM-DD AD"
+
+        return func.to_char(column, format_str)
+
+
+class TIME_WITHOUT_TIME_ZONE(TypeDecorator):
+    impl = SA_TIME
+    cache_ok = True
+
     def __init__(self, *args, precision=None, **kwargs):
-        # On reflection timezone is passed as a kwarg, so we need to make sure it isn't
-        # included in the TIME init call twice
+        # On reflection timezone is passed as a kwarg, so we need to make
+        # sure it isn't included in the TIME init call twice
         if "timezone" in kwargs:
             kwargs.pop("timezone")
-        super().__init__(*args, timezone=False, precision=precision, **kwargs)
+        TypeDecorator.__init__(
+            self, *args, timezone=False, precision=precision, **kwargs
+        )
 
     @classmethod
     def __str__(cls):
         return cls.__name__
 
+    def column_expression(self, column):
+        return case(
+            (column == None, None),  # noqa
+            else_=func.concat(
+                func.to_char(column, "HH24:MI"),
+                ":",
+                func.to_char(
+                    func.date_part("seconds", column), "FM00.0999999999"
+                ),
+            )
+        )
+
+
+class TIME_WITH_TIME_ZONE(TypeDecorator):
+    impl = SA_TIME
+    cache_ok = True
 
-class TIME_WITH_TIME_ZONE(SA_TIME):
     def __init__(self, *args, precision=None, **kwargs):
-        # On reflection timezone is passed as a kwarg, so we need to make sure it isn't
-        # included in the TIME init call twice
+        # On reflection timezone is passed as a kwarg, so we need to make
+        # sure it isn't included in the TIME init call twice
         if "timezone" in kwargs:
             kwargs.pop("timezone")
-        super().__init__(*args, timezone=True, precision=precision, **kwargs)
+        TypeDecorator.__init__(
+            self, *args, timezone=True, precision=precision, **kwargs
+        )
 
     @classmethod
     def __str__(cls):
         return cls.__name__
 
+    def column_expression(self, column):
+        return case(
+            (column == None, None),  # noqa
+            (
+                and_(
+                    func.date_part("timezone_hour", column) == 0,
+                    func.date_part("timezone_minute", column) == 0
+                ),
+                func.concat(
+                    func.to_char(func.date_part("hour", column), "FM00"),
+                    ":",
+                    func.to_char(func.date_part("minute", column), "FM00"),
+                    ":",
+                    func.to_char(
+                        func.date_part("seconds", column), "FM00.0999999999"
+                    ),
+                    "Z")
+            ),
+            else_=func.concat(
+                func.to_char(func.date_part("hour", column), "FM00"),
+                ":",
+                func.to_char(func.date_part("minute", column), "FM00"),
+                ":",
+                func.to_char(
+                    func.date_part("seconds", column), "FM00.0999999999"
+                ),
+                func.to_char(func.date_part("timezone_hour", column), "S00"),
+                ":",
+                func.ltrim(
+                    func.to_char(func.date_part("timezone_minute", column), "00"),
+                    '+- '
+                )
+            )
+        )
+
+
+class TIMESTAMP_WITHOUT_TIME_ZONE(TypeDecorator):
+    impl = SA_TIMESTAMP
+    cache_ok = True
 
-class TIMESTAMP_WITHOUT_TIME_ZONE(SA_TIMESTAMP):
     def __init__(self, *args, precision=None, **kwargs):
-        # On reflection timezone is passed as a kwarg, so we need to make sure it isn't
-        # included in the TIMESTAMP init call twice
+        # On reflection timezone is passed as a kwarg, so we need to make
+        # sure it isn't included in the TIMESTAMP init call twice
         if "timezone" in kwargs:
             kwargs.pop("timezone")
-        super().__init__(*args, timezone=False, precision=precision, **kwargs)
+        TypeDecorator.__init__(
+            self, *args, timezone=False, precision=precision, **kwargs
+        )
 
     @classmethod
     def __str__(cls):
         return cls.__name__
 
+    def column_expression(self, column):
+        base_format_str = 'YYYY-MM-DD"T"HH24:MI'
+        return case(
+            (column == None, None),  # noqa
+            else_=func.concat(
+                func.to_char(column, base_format_str),
+                ":",
+                func.to_char(
+                    func.date_part("seconds", column), "FM00.0999999999"
+                ),
+                func.to_char(column, " BC"),
+            )
+        )
+
+
+class TIMESTAMP_WITH_TIME_ZONE(TypeDecorator):
+    impl = SA_TIMESTAMP
+    cache_ok = True
 
-class TIMESTAMP_WITH_TIME_ZONE(SA_TIMESTAMP):
     def __init__(self, *args, precision=None, **kwargs):
-        # On reflection timezone is passed as a kwarg, so we need to make sure it isn't
-        # included in the TIMESTAMP init call twice
+        # On reflection timezone is passed as a kwarg, so we need to make
+        # sure it isn't included in the TIMESTAMP init call twice
         if "timezone" in kwargs:
             kwargs.pop("timezone")
-        super().__init__(*args, timezone=True, precision=precision, **kwargs)
+        TypeDecorator.__init__(
+            self, *args, timezone=True, precision=precision, **kwargs
+        )
 
     @classmethod
     def __str__(cls):
         return cls.__name__
+
+    def column_expression(self, column):
+        base_format_str = 'YYYY-MM-DD"T"HH24:MI'
+        return case(
+            (column == None, None),  # noqa
+            (
+                and_(
+                    func.date_part("timezone_hour", column) == 0,
+                    func.date_part("timezone_minute", column) == 0
+                ),
+                func.concat(
+                    func.to_char(column, base_format_str),
+                    ":",
+                    func.to_char(
+                        func.date_part("seconds", column), "FM00.0999999999"
+                    ),
+                    "Z",
+                    func.to_char(column, " BC"),
+                )
+            ),
+            else_=func.concat(
+                func.to_char(column, base_format_str),
+                ":",
+                func.to_char(
+                    func.date_part("seconds", column), "FM00.0999999999"
+                ),
+                func.to_char(func.date_part("timezone_hour", column), "S00"),
+                ":",
+                func.ltrim(
+                    func.to_char(func.date_part("timezone_minute", column), "00"),
+                    '+- '
+                ),
+                func.to_char(column, " BC"),
+            )
+        )
+
+
+class Interval(TypeDecorator):
+    impl = INTERVAL
+    cache_ok = True
+
+    def __init__(self, *arg, **kwarg):
+        TypeDecorator.__init__(self, *arg, **kwarg)
+        self.validate_arguments()
+
+    def validate_arguments(self):
+        seconds_fields = {
+            'SECOND',
+            'DAY TO SECOND',
+            'HOUR TO SECOND',
+            'MINUTE TO SECOND',
+        }
+        other_fields = {
+            'YEAR',
+            'MONTH',
+            'DAY',
+            'HOUR',
+            'MINUTE',
+            'YEAR TO MONTH',
+            'DAY TO HOUR',
+            'DAY TO MINUTE',
+            'HOUR TO MINUTE',
+        }
+        all_fields = seconds_fields.union(other_fields)
+        if self.impl.precision is not None:
+            try:
+                assert isinstance(self.impl.precision, int)
+            except AssertionError:
+                raise InvalidTypeParameters('precision must be an integer')
+            try:
+                assert (
+                    self.impl.fields is None
+                    or self.impl.fields.upper() in seconds_fields
+                )
+            except AssertionError:
+                raise InvalidTypeParameters(
+                    'If precision and fields are both given,'
+                    ' seconds must be included in fields.'
+                )
+        elif self.impl.fields is not None:
+            try:
+                assert self.impl.fields.upper() in all_fields
+            except AssertionError:
+                raise InvalidTypeParameters(
+                    f'fields "{self.impl.fields}" is not in {all_fields}'
+                )
+
+    def column_expression(self, col):
+        """
+        Given a column, this function constructs a function that writes
+        an SQL expression that formats an interval into an ISO 8601
+        string.
+        """
+        iso_8601_format_str = 'PFMYYYY"Y"FMMM"M"FMDD"D""T"FMHH24"H"FMMI"M"'
+        return case(
+            (col == None, None),  # noqa
+            # For some reason, it's not possible to nicely format
+            # including the seconds, so those are concatenated to the
+            # end.
+            else_=func.concat(
+                func.to_char(col, iso_8601_format_str),
+                func.date_part('seconds', col),
+                'S',
+            )
+        )
diff --git a/db/types/interval.py b/db/types/interval.py
deleted file mode 100644
--- a/db/types/interval.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from sqlalchemy.dialects.postgresql import INTERVAL
-from sqlalchemy import func, case
-from sqlalchemy.types import TypeDecorator
-
-from db.types.exceptions import InvalidTypeParameters
-
-
-class Interval(TypeDecorator):
-    impl = INTERVAL
-    cache_ok = True
-
-    def __init__(self, *arg, **kwarg):
-        TypeDecorator.__init__(self, *arg, **kwarg)
-        self.validate_arguments()
-
-    def validate_arguments(self):
-        seconds_fields = {
-            'SECOND',
-            'DAY TO SECOND',
-            'HOUR TO SECOND',
-            'MINUTE TO SECOND',
-        }
-        other_fields = {
-            'YEAR',
-            'MONTH',
-            'DAY',
-            'HOUR',
-            'MINUTE',
-            'YEAR TO MONTH',
-            'DAY TO HOUR',
-            'DAY TO MINUTE',
-            'HOUR TO MINUTE',
-        }
-        all_fields = seconds_fields.union(other_fields)
-        if self.impl.precision is not None:
-            try:
-                assert isinstance(self.impl.precision, int)
-            except AssertionError:
-                raise InvalidTypeParameters('precision must be an integer')
-            try:
-                assert (
-                    self.impl.fields is None
-                    or self.impl.fields.upper() in seconds_fields
-                )
-            except AssertionError:
-                raise InvalidTypeParameters(
-                    'If precision and fields are both given,'
-                    ' seconds must be included in fields.'
-                )
-        elif self.impl.fields is not None:
-            try:
-                assert self.impl.fields.upper() in all_fields
-            except AssertionError:
-                raise InvalidTypeParameters(
-                    f'fields "{self.impl.fields}" is not in {all_fields}'
-                )
-
-    def column_expression(self, col):
-        """
-        Given a column, this function constructs a function that writes
-        an SQL expression that formats an interval into an ISO 8601
-        string.
-        """
-        iso_8601_format_str = 'PFMYYYY"Y"FMMM"M"FMDD"D""T"FMHH24"H"FMMI"M"'
-        return case(
-            (col == None, None),  # noqa
-            # For some reason, it's not possible to nicely format
-            # including the seconds, so those are concatenated to the
-            # end.
-            else_=func.concat(
-                func.to_char(col, iso_8601_format_str),
-                func.date_part('seconds', col),
-                'S',
-            )
-        )


Test patch:
diff --git a/db/tests/columns/test_base.py b/db/tests/columns/test_base.py
--- a/db/tests/columns/test_base.py
+++ b/db/tests/columns/test_base.py
@@ -9,7 +9,7 @@
 from db.columns.defaults import DEFAULT_COLUMNS
 from db.columns.utils import get_default_mathesar_column_list
 from db.tests.types import fixtures
-from db.types import email, interval
+from db.types import email, datetime
 
 
 engine_with_types = fixtures.engine_with_types
@@ -247,7 +247,7 @@ def test_MC_type_options_str(engine, target_type, type_options):
 
 
 def test_MC_type_options_interval(engine):
-    target_type = interval.Interval(precision=3, fields='SECOND')
+    target_type = datetime.Interval(precision=3, fields='SECOND')
     mc = MathesarColumn('testable_col', target_type)
     mc.add_engine(engine)
     assert mc.type_options == {'precision': 3, 'fields': 'SECOND'}
diff --git a/db/tests/columns/utils.py b/db/tests/columns/utils.py
--- a/db/tests/columns/utils.py
+++ b/db/tests/columns/utils.py
@@ -1,7 +1,5 @@
-from datetime import date
-
 from sqlalchemy import (
-    CHAR, String, Integer, Boolean, Date, select, Table, MetaData
+    CHAR, String, Integer, Boolean, select, Table, MetaData
 )
 
 
@@ -9,7 +7,6 @@
     Integer: {"start": "0", "set": "5", "expt": 5},
     String: {"start": "default", "set": "test", "expt": "test"},
     Boolean: {"start": "false", "set": "true", "expt": True},
-    Date: {"start": "2019-01-01", "set": "2020-01-01", "expt": date(2020, 1, 1)},
     CHAR: {"start": "a", "set": "b", "expt": "b"}
 }
 
diff --git a/db/tests/conftest.py b/db/tests/conftest.py
--- a/db/tests/conftest.py
+++ b/db/tests/conftest.py
@@ -3,7 +3,7 @@
 import pytest
 from sqlalchemy import MetaData, text
 
-from db import constants
+from db import constants, types
 from db.tables.operations.split import extract_columns_from_table
 
 
@@ -37,6 +37,7 @@ def engine_with_roster(engine_with_schema):
 @pytest.fixture
 def engine_with_filter_sort(engine_with_schema):
     engine, schema = engine_with_schema
+    engine.dialect.ischema_names.update(types.CUSTOM_TYPE_DICT)
     with engine.begin() as conn, open(FILTER_SORT_SQL) as f:
         conn.execute(text(f"SET search_path={schema}"))
         conn.execute(text(f.read()))
diff --git a/db/tests/functions/operations/test_filter.py b/db/tests/functions/operations/test_filter.py
--- a/db/tests/functions/operations/test_filter.py
+++ b/db/tests/functions/operations/test_filter.py
@@ -1,6 +1,5 @@
 import re
 import pytest
-from datetime import datetime
 
 from db.utils import execute_query
 
@@ -65,7 +64,7 @@ def _ilike(x, v):
     # eq
     ("varchar", "eq", "string42", 1),
     ("numeric", "eq", 1, 1),
-    ("date", "eq", "2000-01-01", 1),
+    ("date", "eq", "2000-01-01 AD", 1),
     ("array", "eq", "{0,0}", 1),
     # gt
     ("varchar", "gt", "string0", 100),
@@ -93,9 +92,7 @@ def test_filter_with_db_functions(
 
     record_list = execute_query(engine, query)
 
-    if column_name == "date" and value is not None:
-        value = datetime.strptime(value, "%Y-%m-%d").date()
-    elif column_name == "array" and value is not None and op not in ["any", "not_any"]:
+    if column_name == "array" and value is not None and op not in ["any", "not_any"]:
         value = [int(c) for c in value[1:-1].split(",")]
 
     assert len(record_list) == res_len
diff --git a/db/tests/tables/operations/test_infer_types.py b/db/tests/tables/operations/test_infer_types.py
--- a/db/tests/tables/operations/test_infer_types.py
+++ b/db/tests/tables/operations/test_infer_types.py
@@ -1,7 +1,7 @@
 import pytest
 from unittest.mock import call, patch
 from sqlalchemy import Column, MetaData, Table, select
-from sqlalchemy import BOOLEAN, Numeric, NUMERIC, String, VARCHAR, DATE
+from sqlalchemy import BOOLEAN, Numeric, NUMERIC, String, VARCHAR
 from sqlalchemy.dialects.postgresql import MONEY
 
 from db.columns.operations.infer_types import infer_column_type
@@ -27,7 +27,11 @@
     (String, ["a", "cat", "mat", "bat"], VARCHAR),
     (String, ["2", "1", "0", "0"], NUMERIC),
     (String, ["$2", "$1", "$0"], MONEY),
-    (String, ["2000-01-12", "6/23/2004", "May-2007-29", "May-2007-29 00:00:00+0", "20200909"], DATE),
+    (
+        String,
+        ["2000-01-12", "6/23/2004", "May-2007-29", "May-2007-29 00:00:00+0", "20200909"],
+        datetime.DATE
+    ),
     (String, ["9:24+01", "23:12", "03:04:05", "3:4:5"], datetime.TIME_WITHOUT_TIME_ZONE),
     (
         String,
diff --git a/db/tests/types/operations/test_cast.py b/db/tests/types/operations/test_cast.py
--- a/db/tests/types/operations/test_cast.py
+++ b/db/tests/types/operations/test_cast.py
@@ -1,9 +1,6 @@
-from datetime import date, time
-from datetime import datetime as py_datetime
 from decimal import Decimal
 
 import pytest
-from psycopg2.tz import FixedOffsetTimezone
 from psycopg2.errors import InvalidParameterValue
 from sqlalchemy import Table, Column, MetaData, select, cast
 from sqlalchemy import String, Numeric
@@ -14,7 +11,7 @@
 from db.columns.operations.alter import alter_column_type
 from db.tables.operations.select import get_oid_from_table
 from db.tests.types import fixtures
-from db.types import money, datetime, interval
+from db.types import money, datetime
 from db.types.operations import cast as cast_operations
 from db.types.base import PostgresType, MathesarCustomType, get_qualified_name, get_available_types
 
@@ -168,22 +165,14 @@
         REFLECTED_NAME: DATE,
         TARGET_DICT: {
             CHAR: {VALID: []},
-            DATE: {VALID: [(date(1999, 1, 18), date(1999, 1, 18))]},
-            TEXT: {VALID: [(date(1999, 1, 18), "1999-01-18")]},
-            VARCHAR: {VALID: [(date(1999, 1, 18), "1999-01-18")]},
+            DATE: {VALID: [("1999-01-18 AD", "1999-01-18 AD")]},
+            TEXT: {VALID: [("1999-01-18 AD", "1999-01-18")]},
+            VARCHAR: {VALID: [("1999-01-18 AD", "1999-01-18")]},
             TIMESTAMP_WITH_TIME_ZONE: {
-                VALID: [(
-                    date(1999, 1, 18),
-                    py_datetime(1999, 1, 18, 0, 0, 0, tzinfo=FixedOffsetTimezone(offset=0))
-                ),
-                ]
+                VALID: [("1999-01-18 AD", "1999-01-18T00:00:00.0Z AD")]
             },
             TIMESTAMP_WITHOUT_TIME_ZONE: {
-                VALID: [(
-                    date(1999, 1, 18),
-                    py_datetime(1999, 1, 18, 0, 0, 0)
-                ),
-                ]
+                VALID: [("1999-01-18 AD", "1999-01-18T00:00:00.0 AD")]
             },
         },
     },
@@ -459,15 +448,10 @@
         REFLECTED_NAME: TIME_WITHOUT_TIME_ZONE,
         TARGET_DICT: {
             CHAR: {VALID: []},
-            TIME_WITHOUT_TIME_ZONE: {VALID: [(time(12, 30, 45), time(12, 30, 45))]},
-            TIME_WITH_TIME_ZONE: {
-                VALID: [
-                    (time(12, 30, 45),
-                     time(12, 30, 45, tzinfo=FixedOffsetTimezone(offset=0))),
-                ]
-            },
-            TEXT: {VALID: [(time(12, 30, 45), "12:30:45")]},
-            VARCHAR: {VALID: [(time(12, 30, 45), "12:30:45")]},
+            TIME_WITHOUT_TIME_ZONE: {VALID: [("12:30:45", "12:30:45.0")]},
+            TIME_WITH_TIME_ZONE: {VALID: [("12:30:45", "12:30:45.0Z")]},
+            TEXT: {VALID: [("12:30:45", "12:30:45")]},
+            VARCHAR: {VALID: [("12:30:45", "12:30:45")]},
         },
     },
     TIME_WITH_TIME_ZONE: {
@@ -476,29 +460,11 @@
         TARGET_DICT: {
             CHAR: {VALID: []},
             TIME_WITH_TIME_ZONE: {
-                VALID: [
-                    (time(12, 30, 45, tzinfo=FixedOffsetTimezone(offset=60)),
-                     time(12, 30, 45, tzinfo=FixedOffsetTimezone(offset=60))),
-                ]
-            },
-            TIME_WITHOUT_TIME_ZONE: {
-                VALID: [
-                    (time(12, 30, 45, tzinfo=FixedOffsetTimezone(offset=1)),
-                     time(12, 30, 45))
-                ]
-            },
-            TEXT: {
-                VALID: [
-                    (time(12, 30, 45, tzinfo=FixedOffsetTimezone(offset=60)),
-                     "12:30:45+01")
-                ]
-            },
-            VARCHAR: {
-                VALID: [
-                    (time(12, 30, 45, tzinfo=FixedOffsetTimezone(offset=60)),
-                     "12:30:45+01")
-                ]
+                VALID: [("12:30:45+01:00", "12:30:45.0+01:00")]
             },
+            TIME_WITHOUT_TIME_ZONE: {VALID: [("12:30:45+01:00", "12:30:45.0")]},
+            TEXT: {VALID: [("12:30:45+01:00", "12:30:45+01")]},
+            VARCHAR: {VALID: [("12:30:45+01:00", "12:30:45+01")]},
         },
     },
     TIMESTAMP_WITH_TIME_ZONE: {
@@ -506,34 +472,37 @@
         REFLECTED_NAME: TIMESTAMP_WITH_TIME_ZONE,
         TARGET_DICT: {
             CHAR: {VALID: []},
-            DATE: {VALID: [(py_datetime(1999, 1, 18, 0, 0, 0), date(1999, 1, 18)),
-                           (
-                           py_datetime(1999, 1, 18, 0, 0, 0, tzinfo=FixedOffsetTimezone(offset=0)), date(1999, 1, 18))],
-                   INVALID: [py_datetime(1999, 1, 18, 12, 30, 45),
-                             py_datetime(1999, 1, 18, 0, 0, 0, tzinfo=FixedOffsetTimezone(offset=60))
-                             ]
-                   },
+            DATE: {
+                VALID: [("1999-01-18T00:00:00.0Z AD", "1999-01-18 AD")],
+                INVALID: [
+                    "1999-01-18T12:30:45.0Z AD",
+                    "1999-01-18T00:00:00.0+01:00 AD",
+                ]
+            },
             TIMESTAMP_WITH_TIME_ZONE: {
                 VALID: [
-                    (py_datetime(1999, 1, 18, 12, 30, 45, tzinfo=FixedOffsetTimezone(offset=60)),
-                     py_datetime(1999, 1, 18, 12, 30, 45, tzinfo=FixedOffsetTimezone(offset=60))),
+                    (
+                        "1999-01-18T12:30:45.0+01:00 AD",
+                        "1999-01-18T11:30:45.0Z AD",
+                    ),
                 ]
             },
             TIMESTAMP_WITHOUT_TIME_ZONE: {
-                VALID: [(py_datetime(1999, 1, 18, 12, 30, 45, tzinfo=FixedOffsetTimezone(offset=0)),
-                         py_datetime(1999, 1, 18, 12, 30, 45)
-                         )],
+                VALID: [
+                    (
+                        "1999-01-18T12:30:45.0+01:00 AD",
+                        "1999-01-18T11:30:45.0 AD",
+                    )
+                ],
             },
             TEXT: {
                 VALID: [
-                    (py_datetime(1999, 1, 18, 12, 30, 45),
-                     "1999-01-18 12:30:45+00")
+                    ("1999-01-18T12:30:45.0+01:00 AD", "1999-01-18 11:30:45+00")
                 ]
             },
             VARCHAR: {
                 VALID: [
-                    (py_datetime(1999, 1, 18, 12, 30, 45),
-                     "1999-01-18 12:30:45+00")
+                    ("1999-01-18T12:30:45.0+01:00 AD", "1999-01-18 11:30:45+00")
                 ]
             },
         },
@@ -543,18 +512,20 @@
         REFLECTED_NAME: TIMESTAMP_WITHOUT_TIME_ZONE,
         TARGET_DICT: {
             CHAR: {VALID: []},
-            DATE: {VALID: [(py_datetime(1999, 1, 18, 0, 0, 0), date(1999, 1, 18))],
-                   INVALID: [(py_datetime(1999, 1, 18, 12, 30, 45), date(1999, 1, 18))]},
+            DATE: {
+                VALID: [("1999-01-18T00:00:00.0 AD", "1999-01-18 AD")],
+                INVALID: ["1999-01-18T00:10:00.0 AD"]
+            },
             TIMESTAMP_WITHOUT_TIME_ZONE: {
-                VALID: [(py_datetime(1999, 1, 18, 12, 30, 45), py_datetime(1999, 1, 18, 12, 30, 45))]
+                VALID: [("1999-01-18T12:30:45", "1999-01-18T12:30:45.0 AD")]
             },
             TIMESTAMP_WITH_TIME_ZONE: {
-                VALID: [(py_datetime(1999, 1, 18, 12, 30, 45), py_datetime(1999, 1, 18, 12, 30, 45,
-                                                                           tzinfo=FixedOffsetTimezone(offset=0)))
-                        ]
+                VALID: [("1999-01-18T12:30:45", "1999-01-18T12:30:45.0Z AD")]
+            },
+            TEXT: {VALID: [("1999-01-18T12:30:45.0 AD", "1999-01-18 12:30:45")]},
+            VARCHAR: {
+                VALID: [("1999-01-18T12:30:45.0 AD", "1999-01-18 12:30:45")]
             },
-            TEXT: {VALID: [(py_datetime(1999, 1, 18, 12, 30, 45), "1999-01-18 12:30:45")]},
-            VARCHAR: {VALID: [(py_datetime(1999, 1, 18, 12, 30, 45), "1999-01-18 12:30:45")]},
         },
     },
     TEXT: {
@@ -628,10 +599,10 @@
             },
             DATE: {
                 VALID: [
-                    ("1999-01-18", date(1999, 1, 18)),
-                    ("1/18/1999", date(1999, 1, 18)),
-                    ("jan-1999-18", date(1999, 1, 18)),
-                    ("19990118", date(1999, 1, 18)),
+                    ("1999-01-18", "1999-01-18 AD"),
+                    ("1/18/1999", "1999-01-18 AD"),
+                    ("jan-1999-18", "1999-01-18 AD"),
+                    ("19990118", "1999-01-18 AD"),
                 ],
                 INVALID: [
                     "18/1/1999",
@@ -651,41 +622,23 @@
             },
             TEXT: {VALID: [("a string", "a string")]},
             TIME_WITHOUT_TIME_ZONE: {
-                VALID: [
-                    ("04:05:06", time(4, 5, 6)),
-                    ("04:05", time(4, 5)),
-                ],
-                INVALID: [
-                    "not a time",
-                ]
+                VALID: [("04:05:06", "04:05:06.0"), ("04:05", "04:05:00.0")],
+                INVALID: ["not a time"]
             },
             TIME_WITH_TIME_ZONE: {
                 VALID: [
-                    ("04:05:06", time(4, 5, 6, tzinfo=FixedOffsetTimezone(offset=0))),
-                    ("04:05+01", time(4, 5, tzinfo=FixedOffsetTimezone(offset=60))),
+                    ("04:05:06", "04:05:06.0Z"),
+                    ("04:05+01", "04:05:00.0+01:00")
                 ],
-                INVALID: [
-                    "not a time",
-                ]
+                INVALID: ["not a time"]
             },
             TIMESTAMP_WITH_TIME_ZONE: {
-                VALID: [
-                    ("1999-01-18 12:30:45+00",
-                     py_datetime(1999, 1, 18, 12, 30, 45, tzinfo=FixedOffsetTimezone(offset=0)),
-                     )
-                ],
-                INVALID: [
-                    "not a timestamp",
-                ]
+                VALID: [("1999-01-18 12:30:45+00", "1999-01-18T12:30:45.0Z AD")],
+                INVALID: ["not a timestamp"]
             },
             TIMESTAMP_WITHOUT_TIME_ZONE: {
-                VALID: [
-                    ("1999-01-18 12:30:45", py_datetime(1999, 1, 18, 12, 30, 45),
-                     )
-                ],
-                INVALID: [
-                    "not a timestamp",
-                ]
+                VALID: [("1999-01-18 12:30:45", "1999-01-18T12:30:45.0 AD")],
+                INVALID: ["not a timestamp"]
             },
             VARCHAR: {VALID: [("a string", "a string")]},
         }
@@ -721,10 +674,10 @@
             CHAR: {VALID: [("a", "a")]},
             DATE: {
                 VALID: [
-                    ("1999-01-18", date(1999, 1, 18)),
-                    ("1/18/1999", date(1999, 1, 18)),
-                    ("jan-1999-18", date(1999, 1, 18)),
-                    ("19990118", date(1999, 1, 18)),
+                    ("1999-01-18", "1999-01-18 AD"),
+                    ("1/18/1999", "1999-01-18 AD"),
+                    ("jan-1999-18", "1999-01-18 AD"),
+                    ("19990118", "1999-01-18 AD"),
                 ],
                 INVALID: [
                     "18/1/1999",
@@ -783,41 +736,25 @@
             },
             TEXT: {VALID: [("a string", "a string")]},
             TIME_WITHOUT_TIME_ZONE: {
-                VALID: [
-                    ("04:05:06", time(4, 5, 6)),
-                    ("04:05", time(4, 5)),
-                ],
-                INVALID: [
-                    "not a time",
-                ]
+                VALID: [("04:05:06", "04:05:06.0"), ("04:05", "04:05:00.0")],
+                INVALID: ["not a time"]
             },
             TIME_WITH_TIME_ZONE: {
                 VALID: [
-                    ("04:05:06", time(4, 5, 6, tzinfo=FixedOffsetTimezone(offset=0))),
-                    ("04:05+01", time(4, 5, tzinfo=FixedOffsetTimezone(offset=60))),
+                    ("04:05:06", "04:05:06.0Z"),
+                    ("04:05+01", "04:05:00.0+01:00")
                 ],
                 INVALID: [
                     "not a time",
                 ]
             },
             TIMESTAMP_WITH_TIME_ZONE: {
-                VALID: [
-                    ("1999-01-18 12:30:45+00",
-                     py_datetime(1999, 1, 18, 12, 30, 45, tzinfo=FixedOffsetTimezone(offset=0)),
-                     )
-                ],
-                INVALID: [
-                    "not a timestamp",
-                ]
+                VALID: [("1999-01-18 12:30:45+00", "1999-01-18T12:30:45.0Z AD")],
+                INVALID: ["not a timestamp"]
             },
             TIMESTAMP_WITHOUT_TIME_ZONE: {
-                VALID: [
-                    ("1999-01-18 12:30:45", py_datetime(1999, 1, 18, 12, 30, 45),
-                     )
-                ],
-                INVALID: [
-                    "not a timestamp",
-                ]
+                VALID: [("1999-01-18 12:30:45+00", "1999-01-18T12:30:45.0 AD")],
+                INVALID: ["not a timestamp"]
             },
             URI: {
                 VALID: [("https://centerofci.org", "https://centerofci.org")],
@@ -831,6 +768,8 @@
 
 def test_get_alter_column_types_with_custom_engine(engine_with_types):
     type_dict = cast_operations.get_supported_alter_column_types(engine_with_types)
+    for type_ in types.CUSTOM_TYPE_DICT.values():
+        assert type_ in type_dict.values()
     assert all(
         [
             type_ in type_dict.values()
@@ -944,16 +883,33 @@ def test_alter_column_type_alters_column_type(
     # test that rounding is as intended
     (Numeric, "numeric", {"precision": 5, "scale": 2}, 1.235, Decimal("1.24")),
     (String, "numeric", {"precision": 5, "scale": 2}, "500.134", Decimal("500.13")),
-
-    (datetime.TIME_WITHOUT_TIME_ZONE, "time without time zone", {"precision": 0},
-     time(0, 0, 0, 9), time(0, 0, 0)),
-    (datetime.TIME_WITH_TIME_ZONE, "time with time zone", {"precision": 0},
-     time(0, 0, 0, 9, tzinfo=FixedOffsetTimezone(offset=0)),
-     time(0, 0, 0, tzinfo=FixedOffsetTimezone(offset=0))),
-    (datetime.TIMESTAMP_WITH_TIME_ZONE, "timestamp with time zone", {"precision": 0},
-     py_datetime(1999, 1, 1, 0, 0, 0), py_datetime(1999, 1, 1, 0, 0, 0, tzinfo=FixedOffsetTimezone(offset=0))),
-    (datetime.TIMESTAMP_WITHOUT_TIME_ZONE, "timestamp without time zone", {"precision": 0},
-     py_datetime(1999, 1, 1, 0, 0, 0), py_datetime(1999, 1, 1, 0, 0, 0)),
+    (
+        datetime.TIME_WITHOUT_TIME_ZONE,
+        "time without time zone",
+        {"precision": 0},
+        "00:00:00.1234",
+        "00:00:00.0"
+    ),
+    (
+        datetime.TIME_WITH_TIME_ZONE,
+        "time with time zone",
+        {"precision": 0},
+        "00:00:00.1234-04:30", "00:00:00.0-04:30"
+    ),
+    (
+        datetime.TIMESTAMP_WITH_TIME_ZONE,
+        "timestamp with time zone",
+        {"precision": 0},
+        "1999-01-01 00:00:00",
+        "1999-01-01T00:00:00.0Z AD",
+    ),
+    (
+        datetime.TIMESTAMP_WITHOUT_TIME_ZONE,
+        "timestamp without time zone",
+        {"precision": 0},
+        "1999-01-01 00:00:00",
+        "1999-01-01T00:00:00.0 AD",
+    ),
     (String, "char", {"length": 5}, "abcde", "abcde"),
 ]
 
@@ -1205,19 +1161,19 @@ def test_get_column_cast_expression_unsupported(engine_with_types):
         'CAST(mathesar_types.cast_to_numeric(colname) AS NUMERIC(3, 2))'
     ),
     (
-        interval.Interval,
+        datetime.Interval,
         "interval",
         {"fields": "YEAR"},
         "CAST(colname AS INTERVAL YEAR)"
     ),
     (
-        interval.Interval,
+        datetime.Interval,
         "interval",
         {"precision": 2},
         "CAST(colname AS INTERVAL (2))"
     ),
     (
-        interval.Interval,
+        datetime.Interval,
         "interval",
         {"precision": 3, "fields": "SECOND"},
         "CAST(colname AS INTERVAL SECOND (3))"
diff --git a/db/tests/types/test_datetime.py b/db/tests/types/test_datetime.py
new file mode 100644
--- /dev/null
+++ b/db/tests/types/test_datetime.py
@@ -0,0 +1,298 @@
+import pytest
+from sqlalchemy import text, Table, Column, MetaData, select, cast
+from sqlalchemy.dialects.postgresql import DATE as SA_DATE
+from sqlalchemy.dialects.postgresql import INTERVAL as SA_INTERVAL
+from sqlalchemy.dialects.postgresql import TIME as SA_TIME
+from sqlalchemy.dialects.postgresql import TIMESTAMP as SA_TIMESTAMP
+
+from db.engine import _add_custom_types_to_engine
+from db.tests.types import fixtures
+from db.types import datetime, exceptions
+
+
+# We need to set these variables when the file loads, or pytest can't
+# properly detect the fixtures.  Importing them directly results in a
+# flake8 unused import error, and a bunch of flake8 F811 errors.
+engine_with_types = fixtures.engine_with_types
+temporary_testing_schema = fixtures.temporary_testing_schema
+engine_email_type = fixtures.engine_email_type
+
+
+datetime_types = [
+    (
+        datetime.DATE, SA_DATE(),
+        {
+            '2021-01-03 AD': [
+                '2021-01-03', '3 Jan, 2021', 'Jan 3, 2021', '3 Jan, 2021 AD'
+            ],
+            '0023-01-03 BC': ['0023-01-03 BC', '3 Jan, 23 BC', 'Jan 3, 23 BC']
+        },
+    ),
+    (
+        datetime.Interval, SA_INTERVAL(),
+        {
+            # The keys are expected output, the list of strings are
+            # various inputs that should map to the output denoted by the
+            # associated key.  These should be comprehensive enough to
+            # document expected parseable intervals.
+            'P0Y0M1DT0H0M0S': ['1 day', '1 days', '1.0 days', 'P1D', '1D'],
+            'P1Y2M3DT4H5M6S': [
+                '1 year 2 months 3 days 4 hours 5 minutes 6 seconds',
+                '1-2 3 4:05:06', '1 year 2 months 3 days 4:05:06'
+            ],
+            # negative interval
+            'P0Y0M-1DT-3H0M0S': [
+                '1 day 3 hours ago', '-1 day, -3 hours', 'P-1DT-3H', '-1D, -3H',
+                '-1 -3:00:00'
+            ],
+            # mixed positive negative interval
+            'P0Y0M1DT-3H0M0S': [
+                '1 day -3 hours', '-1 day 3 hours ago', 'P1DT-3H', '1D -3H',
+                '1 day, -3H', '1 -3:00:00'
+            ],
+            # checking disaggregation
+            'P0Y11M3DT13H50M30.4S': [
+                '1 year -1 month 3 days 14 hours -10 minutes 30.4 seconds'
+            ],
+            'P0Y-11M-3DT-13H-50M-30.4S': [
+                '1 year -1 month 3 days 14 hours -10 minutes 30.4 seconds ago'
+            ],
+            # Pluralization doesn't matter
+            'P0Y0M2DT0H0M0S': ['2 day', '2 days'],
+            # Some roll overs.  Note that we do not accrue days, since some
+            # are different lengths whenever DST begins or ends.
+            'P0Y0M0DT45H1M10S': [
+                '45 hours, 1 minute, 10 seconds', '45 hours 70 seconds',
+                '44 hours 59 minutes 130 seconds', '162070 sec',
+                '162070S', '45:01:10'
+            ],
+            # months roll into years, but days don't roll into months (since
+            # different months have different numbers of days).
+            'P1Y1M40DT0H0M0S': ['1 year 1 month 40 days', '13 months 40 days'],
+            'P0Y0M0DT0H0M1.234567S': [
+                '1.234567 seconds', '1234.567 milliseconds', '1234.567ms',
+                '1234567 microseconds'
+            ],
+            # some more exotic time intervals
+            'P73Y2M10DT0H0M0S': ['5 decades 22 years 14 months 1 week 3 days'],
+            'P100Y0M0DT0H0M0S': ['1 century', '1C', '1 centuries', '10 decades'],
+            'P2000Y0M0DT0H0M0S': ['2 millennia', '2 millennium', '20 centuries'],
+        }
+    ),
+    (
+        datetime.TIME_WITH_TIME_ZONE, SA_TIME(timezone=True),
+        {
+            '12:30:45.0+05:30': ['12:30:45+05:30'],
+            '12:30:45.0Z': ['12:30:45', '12:30:45 UTC'],
+            '12:30:45.123456-08:00': ['12:30:45.123456-08', '12:30:45.123456000-08'],
+        }
+    ),
+    (
+        datetime.TIME_WITHOUT_TIME_ZONE, SA_TIME(timezone=False),
+        {
+            '12:30:00.0': ['12:30'],
+        }
+    ),
+    (
+        datetime.TIMESTAMP_WITH_TIME_ZONE, SA_TIMESTAMP(timezone=True),
+        {
+            '2000-07-30T19:15:03.65Z AD': [
+                '30 July, 2000 19:15:03.65', '07-30-2000 19:15:03.65+00'
+            ],
+            '10000-01-01T00:00:00.0Z AD': ['10000-01-01 00:00:00'],
+            '0025-03-03T16:30:15.0Z BC': ['3 March, 25 BC, 17:30:15+01'],
+        }
+    ),
+    (
+        datetime.TIMESTAMP_WITHOUT_TIME_ZONE, SA_TIMESTAMP(timezone=False),
+        {
+            '17654-03-02T01:00:00.0 AD': ['17654-03-02 01:00:00']
+        }
+    ),
+]
+
+
+@pytest.mark.parametrize('test_type', [type_[0] for type_ in datetime_types])
+def test_datetime_type_column_creation(engine_email_type, test_type):
+    engine, app_schema = engine_email_type
+    with engine.begin() as conn:
+        conn.execute(text(f'SET search_path={app_schema}'))
+        metadata = MetaData(bind=conn)
+        test_table = Table(
+            'test_table',
+            metadata,
+            Column('time_type', test_type),
+        )
+        test_table.create()
+
+
+@pytest.mark.parametrize(
+    'test_type,sa_type',
+    [(type_[0], type_[1]) for type_ in datetime_types]
+)
+def test_datetime_type_column_reflection(engine_email_type, test_type, sa_type):
+    engine, app_schema = engine_email_type
+    col_name = 'time_type'
+    table_name = 'test_table'
+    with engine.begin() as conn:
+        metadata = MetaData(bind=conn, schema=app_schema)
+        test_table = Table(
+            table_name,
+            metadata,
+            Column(col_name, sa_type),
+        )
+        test_table.create()
+
+    _add_custom_types_to_engine(engine)
+    with engine.begin() as conn:
+        metadata = MetaData(bind=conn, schema=app_schema)
+        reflect_table = Table(table_name, metadata, autoload_with=conn)
+    expect_cls = test_type
+    actual_cls = reflect_table.columns[col_name].type.__class__
+    assert actual_cls == expect_cls
+
+
+datetime_defaults = [
+    (tup[0], key)
+    for tup in datetime_types
+    for key in tup[2]
+]
+
+
+@pytest.mark.parametrize('type_,val', datetime_defaults)
+def test_datetime_type_column_default(engine_email_type, type_, val):
+    engine, app_schema = engine_email_type
+    default_str = val
+    column_name = 'time_type'
+    table_name = 'test_table'
+    with engine.begin() as conn:
+        conn.execute(text(f'SET search_path={app_schema}'))
+        metadata = MetaData(bind=conn)
+        test_table = Table(
+            table_name,
+            metadata,
+            Column(
+                column_name, type_, server_default=default_str,
+            ),
+        )
+        test_table.create()
+
+    _add_custom_types_to_engine(engine)
+    with engine.begin() as conn:
+        metadata = MetaData(bind=conn, schema=app_schema)
+        reflect_table = Table(table_name, metadata, autoload_with=conn)
+    test_col = reflect_table.columns[column_name]
+    default_sql_txt = str(test_col.server_default.arg)
+    default_selectable = select(cast(text(default_sql_txt), test_col.type))
+    with engine.begin() as conn:
+        actual_default = conn.execute(default_selectable).scalar()
+    assert actual_default == default_str
+
+
+def test_interval_type_column_args(engine_email_type):
+    engine, app_schema = engine_email_type
+    with engine.begin() as conn:
+        metadata = MetaData(bind=conn, schema=app_schema)
+        test_table = Table(
+            'test_table',
+            metadata,
+            Column(
+                'time_intervals',
+                datetime.Interval(precision=5, fields='SECOND')
+            )
+        )
+        test_table.create()
+
+    _add_custom_types_to_engine(engine)
+    with engine.begin() as conn:
+        metadata = MetaData(bind=conn, schema=app_schema)
+        reflect_table = Table('test_table', metadata, autoload_with=conn)
+    expect_cls = datetime.Interval
+    actual_cls = reflect_table.columns['time_intervals'].type.__class__
+    assert actual_cls == expect_cls
+    actual_interval = reflect_table.columns['time_intervals'].type
+    assert actual_interval.precision == 5
+    assert actual_interval.fields.upper() == 'SECOND'
+
+
+invalid_args_list = [(None, 'SECONDS'), (1.34, None), (5, 'HOURS')]
+
+
+@pytest.mark.parametrize('precision,fields', invalid_args_list)
+def test_interval_type_column_invalid_args(precision, fields):
+    with pytest.raises(exceptions.InvalidTypeParameters):
+        datetime.Interval(precision=precision, fields=fields)
+
+
+types_self_map = [
+    (tup[0], key, key)
+    for tup in datetime_types
+    for key in tup[2]
+]
+
+types_exploded = types_self_map + [
+    (tup[0], val, key)
+    for tup in datetime_types
+    for key in tup[2]
+    for val in tup[2][key]
+]
+
+
+@pytest.mark.parametrize('type_,val_in,val_out', types_exploded)
+def test_type_transformations(engine_with_types, type_, val_in, val_out):
+    # First we make sure the date/time input strings cast properly
+    with engine_with_types.begin() as conn:
+        res = conn.execute(select(cast(val_in, type_))).scalar()
+    assert res == val_out
+
+
+@pytest.mark.parametrize(
+    'type_,out_in_map', [(tup[0], tup[2]) for tup in datetime_types]
+)
+def test_interval_insert_select(engine_email_type, type_, out_in_map):
+    # Now we bulk test inserting and selecting
+    fixed_type_self_map = [(key, key) for key in out_in_map]
+    fixed_type_exploded = fixed_type_self_map + [
+        (val, key) for key in out_in_map for val in out_in_map[key]
+    ]
+    engine, app_schema = engine_email_type
+    column_name = 'time_type'
+    insert_dicts = [{column_name: tup[0]} for tup in fixed_type_exploded]
+    output_values = [tup[1] for tup in fixed_type_exploded]
+    with engine.begin() as conn:
+        metadata = MetaData(bind=conn, schema=app_schema)
+        test_table = Table(
+            'test_table',
+            metadata,
+            Column(column_name, type_),
+        )
+        test_table.create()
+        conn.execute(test_table.insert().values(insert_dicts))
+        res = conn.execute(select(test_table)).fetchall()
+    assert len(res) == len(output_values)
+    assert all([res[i][0] == output_values[i] for i in range(len(res))])
+
+
+def test_interval_datetime_addition(engine_with_types):
+    three_days_interval = cast('3 days 4 hours 30 minutes', datetime.Interval)
+    the_date = cast('2020-01-01', datetime.TIMESTAMP_WITHOUT_TIME_ZONE)
+    with engine_with_types.begin() as conn:
+        res = conn.execute(
+            select(
+                cast(
+                    the_date + three_days_interval,
+                    datetime.TIMESTAMP_WITHOUT_TIME_ZONE
+                )
+            )
+        ).scalar()
+    assert res == '2020-01-04T04:30:00.0 AD'
+
+
+def test_interval_interval_addition(engine_with_types):
+    three_days_interval = cast('3 days 4 hours 30 minutes', datetime.Interval)
+    five_days_interval = cast('5 days', datetime.Interval)
+    with engine_with_types.begin() as conn:
+        res = conn.execute(
+            select(cast(three_days_interval + five_days_interval, datetime.Interval))
+        ).scalar()
+    assert res == 'P0Y0M8DT4H30M0S'
diff --git a/db/tests/types/test_interval.py b/db/tests/types/test_interval.py
deleted file mode 100644
--- a/db/tests/types/test_interval.py
+++ /dev/null
@@ -1,206 +0,0 @@
-from datetime import datetime as dt
-import pytest
-from sqlalchemy import text, Table, Column, MetaData, select, cast
-from sqlalchemy.dialects.postgresql import INTERVAL as SAInterval
-from db.engine import _add_custom_types_to_engine
-from db.tests.types import fixtures
-from db.types import interval, datetime, exceptions
-
-
-# We need to set these variables when the file loads, or pytest can't
-# properly detect the fixtures.  Importing them directly results in a
-# flake8 unused import error, and a bunch of flake8 F811 errors.
-engine_with_types = fixtures.engine_with_types
-temporary_testing_schema = fixtures.temporary_testing_schema
-engine_email_type = fixtures.engine_email_type
-
-
-def test_interval_type_column_creation(engine_email_type):
-    engine, app_schema = engine_email_type
-    with engine.begin() as conn:
-        conn.execute(text(f"SET search_path={app_schema}"))
-        metadata = MetaData(bind=conn)
-        test_table = Table(
-            "test_table",
-            metadata,
-            Column("time_intervals", interval.Interval),
-        )
-        test_table.create()
-
-
-def test_interval_type_column_reflection(engine_email_type):
-    engine, app_schema = engine_email_type
-    with engine.begin() as conn:
-        metadata = MetaData(bind=conn, schema=app_schema)
-        test_table = Table(
-            "test_table",
-            metadata,
-            Column("time_intervals", SAInterval),
-        )
-        test_table.create()
-
-    _add_custom_types_to_engine(engine)
-    with engine.begin() as conn:
-        metadata = MetaData(bind=conn, schema=app_schema)
-        reflect_table = Table("test_table", metadata, autoload_with=conn)
-    expect_cls = interval.Interval
-    actual_cls = reflect_table.columns["time_intervals"].type.__class__
-    assert actual_cls == expect_cls
-
-
-def test_interval_type_column_default(engine_email_type):
-    engine, app_schema = engine_email_type
-    default_str = 'P1Y1M1DT1H1M1.1S'
-    with engine.begin() as conn:
-        conn.execute(text(f"SET search_path={app_schema}"))
-        metadata = MetaData(bind=conn)
-        test_table = Table(
-            "test_table",
-            metadata,
-            Column(
-                "time_intervals", interval.Interval, server_default=default_str,
-            ),
-        )
-        test_table.create()
-
-    _add_custom_types_to_engine(engine)
-    with engine.begin() as conn:
-        metadata = MetaData(bind=conn, schema=app_schema)
-        reflect_table = Table("test_table", metadata, autoload_with=conn)
-    intervals_col = reflect_table.columns["time_intervals"]
-    default_sql_txt = str(intervals_col.server_default.arg)
-    default_selectable = select(cast(text(default_sql_txt), intervals_col.type))
-    with engine.begin() as conn:
-        actual_default = conn.execute(default_selectable).scalar()
-    assert actual_default == default_str
-
-
-def test_interval_type_column_args(engine_email_type):
-    engine, app_schema = engine_email_type
-    with engine.begin() as conn:
-        metadata = MetaData(bind=conn, schema=app_schema)
-        test_table = Table(
-            "test_table",
-            metadata,
-            Column(
-                "time_intervals",
-                interval.Interval(precision=5, fields="SECOND")
-            )
-        )
-        test_table.create()
-
-    _add_custom_types_to_engine(engine)
-    with engine.begin() as conn:
-        metadata = MetaData(bind=conn, schema=app_schema)
-        reflect_table = Table("test_table", metadata, autoload_with=conn)
-    expect_cls = interval.Interval
-    actual_cls = reflect_table.columns["time_intervals"].type.__class__
-    assert actual_cls == expect_cls
-    actual_interval = reflect_table.columns["time_intervals"].type
-    assert actual_interval.precision == 5
-    assert actual_interval.fields.upper() == "SECOND"
-
-
-invalid_args_list = [(None, "SECONDS"), (1.34, None), (5, "HOURS")]
-
-
-@pytest.mark.parametrize("precision,fields", invalid_args_list)
-def test_interval_type_column_invalid_args(precision, fields):
-    with pytest.raises(exceptions.InvalidTypeParameters):
-        interval.Interval(precision=precision, fields=fields)
-
-
-intervals_out_to_ins = {
-    # The keys are expected output, the list of strings are various inputs
-    # that should map to the output denoted by the associated key.  These
-    # should be comprehensive enough to document expected parseable
-    # intervals.
-    'P0Y0M1DT0H0M0S': ['1 day', '1 days', '1.0 days', 'P1D', '1D'],
-    'P1Y2M3DT4H5M6S': [
-        '1 year 2 months 3 days 4 hours 5 minutes 6 seconds',
-        '1-2 3 4:05:06', '1 year 2 months 3 days 4:05:06'
-    ],
-    # negative interval
-    'P0Y0M-1DT-3H0M0S': [
-        '1 day 3 hours ago', '-1 day, -3 hours', 'P-1DT-3H', '-1D, -3H',
-        '-1 -3:00:00'
-    ],
-    # mixed positive negative interval
-    'P0Y0M1DT-3H0M0S': [
-        '1 day -3 hours', '-1 day 3 hours ago', 'P1DT-3H', '1D -3H',
-        '1 day, -3H', '1 -3:00:00'
-    ],
-    # Pluralization doesn't matter
-    'P0Y0M2DT0H0M0S': ['2 day', '2 days'],
-    # Some roll overs.  Note that we do not accrue days, since some
-    # are different lengths whenever DST begins or ends.
-    'P0Y0M0DT45H1M10S': [
-        '45 hours, 1 minute, 10 seconds', '45 hours 70 seconds',
-        '44 hours 59 minutes 130 seconds', '162070 sec',
-        '162070S', '45:01:10'
-    ],
-    # months roll into years, but days don't roll into months (since
-    # different months have different numbers of days).
-    'P1Y1M40DT0H0M0S': ['1 year 1 month 40 days', '13 months 40 days'],
-    'P0Y0M0DT0H0M1.234567S': [
-        '1.234567 seconds', '1234.567 milliseconds', '1234.567ms',
-        '1234567 microseconds'
-    ],
-    # some more exotic time intervals
-    'P73Y2M10DT0H0M0S': ['5 decades 22 years 14 months 1 week 3 days'],
-    'P100Y0M0DT0H0M0S': ['1 century', '1C', '1 centuries', '10 decades'],
-    'P2000Y0M0DT0H0M0S': ['2 millennia', '2 millennium', '20 centuries'],
-}
-
-intervals_self_map = [(key, key) for key in intervals_out_to_ins]
-intervals_exploded = intervals_self_map + [
-    (val, key) for key in intervals_out_to_ins for val in intervals_out_to_ins[key]
-]
-
-
-@pytest.mark.parametrize('interval_in,interval_out', intervals_exploded)
-def test_interval_transformations(engine_with_types, interval_in, interval_out):
-    # First we make sure the interval input strings cast properly
-    with engine_with_types.begin() as conn:
-        res = conn.execute(select(cast(interval_in, interval.Interval))).scalar()
-    assert res == interval_out
-
-
-def test_interval_insert_select(engine_email_type):
-    # Now we bulk test inserting and selecting
-    engine, app_schema = engine_email_type
-    column_name = 'time_interval'
-    insert_dicts = [{column_name: tup[0]} for tup in intervals_exploded]
-    output_values = [tup[1] for tup in intervals_exploded]
-    with engine.begin() as conn:
-        metadata = MetaData(bind=conn, schema=app_schema)
-        test_table = Table(
-            'test_table',
-            metadata,
-            Column(column_name, interval.Interval),
-        )
-        test_table.create()
-        conn.execute(test_table.insert().values(insert_dicts))
-        res = conn.execute(select(test_table)).fetchall()
-    assert len(res) == len(output_values)
-    assert all([res[i][0] == output_values[i] for i in range(len(res))])
-
-
-def test_interval_datetime_addition(engine_with_types):
-    three_days_interval = cast('3 days 4 hours 30 minutes', interval.Interval)
-    the_date = cast(dt(2020, 1, 1), datetime.TIMESTAMP_WITHOUT_TIME_ZONE)
-    with engine_with_types.begin() as conn:
-        res = conn.execute(
-            select(the_date + three_days_interval)
-        ).scalar()
-    assert res == dt(2020, 1, 4, 4, 30)
-
-
-def test_interval_interval_addition(engine_with_types):
-    three_days_interval = cast('3 days 4 hours 30 minutes', interval.Interval)
-    five_days_interval = cast('5 days', interval.Interval)
-    with engine_with_types.begin() as conn:
-        res = conn.execute(
-            select(cast(three_days_interval + five_days_interval, interval.Interval))
-        ).scalar()
-    assert res == 'P0Y0M8DT4H30M0S'
diff --git a/mathesar/tests/api/test_column_api.py b/mathesar/tests/api/test_column_api.py
--- a/mathesar/tests/api/test_column_api.py
+++ b/mathesar/tests/api/test_column_api.py
@@ -1,5 +1,4 @@
 import json
-from datetime import date
 
 import pytest
 from unittest.mock import patch
@@ -183,7 +182,7 @@ def test_column_create(column_test_table, client):
     ("NUMERIC", 42, 42, 42),
     ("STRING", "test_string", "test_string", "test_string"),
     ("VARCHAR", "test_string", "test_string", "test_string"),
-    ("DATE", "2020-1-1", date(2020, 1, 1), "2020-01-01"),
+    ("DATE", "2020-1-1", "2020-01-01 AD", "2020-01-01 AD"),
     ("EMAIL", "test@test.com", "test@test.com", "test@test.com"),
 ]
 
diff --git a/mathesar/tests/api/test_record_api.py b/mathesar/tests/api/test_record_api.py
--- a/mathesar/tests/api/test_record_api.py
+++ b/mathesar/tests/api/test_record_api.py
@@ -59,8 +59,8 @@ def test_record_list(create_table, client):
 
 
 serialization_test_list = [
-    ("TIME WITH TIME ZONE", "12:30:10+01:00"),
-    ("TIMESTAMP WITHOUT TIME ZONE", "2000-05-23T12:30:10"),
+    ("TIME WITH TIME ZONE", "12:30:10.0+01:00"),
+    ("TIMESTAMP WITHOUT TIME ZONE", "2000-05-23T12:30:10.0 AD"),
     ("MONEY", "$5.00"),
 ]