Repository: mathesar-foundation/mathesar

Issue:
Handle `DOUBLE PRECISION` data type in the backend.
This issue is to ensure that Mathesar can handle the Postgres `DOUBLE PRECISION` data type.

As part of this issue, we need to ensure that:
- Users can use the API to change a column to `DOUBLE PRECISION` if it's possible to do so.
- There's nothing to do here on automatic type inference. It should never suggest `DOUBLE PRECISION` since we're aiming to be friendly to non-technical users and floating points can be confusing.

**Additional Context** 
- See PR #368 for code that calculates which types are allowed for a column.


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,10 @@
+from sqlalchemy import DECIMAL as sa_decimal
 from db.types import email
+from db.types.alteration import DECIMAL
 
 CUSTOM_TYPE_DICT = {
+    # For some reason, SQLAlchemy doesn't add DECIMAL to the default
+    # ischema_names supported by a PostgreSQL engine
+    DECIMAL: sa_decimal,
     email.QUALIFIED_EMAIL: email.Email,
 }
diff --git a/db/types/alteration.py b/db/types/alteration.py
--- a/db/types/alteration.py
+++ b/db/types/alteration.py
@@ -5,9 +5,13 @@
 
 BOOLEAN = "boolean"
 EMAIL = "email"
+DECIMAL = "decimal"
+DOUBLE_PRECISION = "double precision"
+FLOAT = "float"
 INTERVAL = "interval"
 NAME = "name"
 NUMERIC = "numeric"
+REAL = "real"
 STRING = "string"
 VARCHAR = "varchar"
 FULL_VARCHAR = "character varying"
@@ -30,8 +34,12 @@ def get_supported_alter_column_types(engine, friendly_names=True):
     friendly_type_map = {
         # Default Postgres types
         BOOLEAN: dialect_types.get(BOOLEAN),
+        DECIMAL: dialect_types.get(DECIMAL),
+        DOUBLE_PRECISION: dialect_types.get(DOUBLE_PRECISION),
+        FLOAT: dialect_types.get(FLOAT),
         INTERVAL: dialect_types.get(INTERVAL),
         NUMERIC: dialect_types.get(NUMERIC),
+        REAL: dialect_types.get(REAL),
         STRING: dialect_types.get(NAME),
         VARCHAR: dialect_types.get(FULL_VARCHAR),
         # Custom Mathesar types
@@ -48,6 +56,15 @@ def get_supported_alter_column_types(engine, friendly_names=True):
     return type_map
 
 
+def get_supported_alter_column_db_types(engine):
+    return set(
+        [
+            type_().compile(dialect=engine.dialect)
+            for type_ in get_supported_alter_column_types(engine).values()
+        ]
+    )
+
+
 def get_robust_supported_alter_column_type_map(engine):
     supported_types = get_supported_alter_column_types(engine, friendly_names=True)
     supported_types.update(get_supported_alter_column_types(engine, friendly_names=False))
@@ -126,8 +143,8 @@ def get_column_cast_expression(column, target_type_str, engine, type_options={})
 def install_all_casts(engine):
     create_boolean_casts(engine)
     create_email_casts(engine)
+    create_decimal_number_casts(engine)
     create_interval_casts(engine)
-    create_numeric_casts(engine)
     create_varchar_casts(engine)
 
 
@@ -146,20 +163,22 @@ def create_interval_casts(engine):
     create_cast_functions(INTERVAL, type_body_map, engine)
 
 
-def create_numeric_casts(engine):
-    type_body_map = _get_numeric_type_body_map()
-    create_cast_functions(NUMERIC, type_body_map, engine)
+def create_decimal_number_casts(engine):
+    decimal_number_types = [DECIMAL, DOUBLE_PRECISION, FLOAT, NUMERIC, REAL]
+    for type_str in decimal_number_types:
+        type_body_map = _get_decimal_number_type_body_map(target_type_str=type_str)
+        create_cast_functions(type_str, type_body_map, engine)
 
 
 def create_varchar_casts(engine):
-    type_body_map = _get_varchar_type_body_map()
+    type_body_map = _get_varchar_type_body_map(engine)
     create_cast_functions(VARCHAR, type_body_map, engine)
 
 
 def get_full_cast_map(engine):
     full_cast_map = {}
     supported_types = get_robust_supported_alter_column_type_map(engine)
-    for source, target in get_defined_source_target_cast_tuples():
+    for source, target in get_defined_source_target_cast_tuples(engine):
         source_python_type = supported_types.get(source)
         target_python_type = supported_types.get(target)
         if source_python_type is not None and target_python_type is not None:
@@ -167,21 +186,27 @@ def get_full_cast_map(engine):
             target_db_type = target_python_type().compile(dialect=engine.dialect)
             full_cast_map.setdefault(source_db_type, []).append(target_db_type)
 
-    return full_cast_map
+    return {
+        key: list(set(val)) for key, val in full_cast_map.items()
+    }
 
 
-def get_defined_source_target_cast_tuples():
+def get_defined_source_target_cast_tuples(engine):
     type_body_map_map = {
-        BOOLEAN: _get_boolean_type_body_map,
-        EMAIL: _get_email_type_body_map,
-        INTERVAL: _get_interval_type_body_map,
-        NUMERIC: _get_numeric_type_body_map,
-        VARCHAR: _get_varchar_type_body_map,
+        BOOLEAN: _get_boolean_type_body_map(),
+        EMAIL: _get_email_type_body_map(),
+        DECIMAL: _get_decimal_number_type_body_map(target_type_str=DECIMAL),
+        DOUBLE_PRECISION: _get_decimal_number_type_body_map(target_type_str=DOUBLE_PRECISION),
+        FLOAT: _get_decimal_number_type_body_map(target_type_str=FLOAT),
+        INTERVAL: _get_interval_type_body_map(),
+        NUMERIC: _get_decimal_number_type_body_map(target_type_str=NUMERIC),
+        REAL: _get_decimal_number_type_body_map(target_type_str=REAL),
+        VARCHAR: _get_varchar_type_body_map(engine),
     }
     return {
         (source_type, target_type)
         for target_type in type_body_map_map
-        for source_type in type_body_map_map[target_type]()
+        for source_type in type_body_map_map[target_type]
     }
 
 
@@ -222,7 +247,8 @@ def assemble_function_creation_sql(argument_type, target_type, function_body):
 def get_cast_function_name(target_type):
     unqualified_type_name = target_type.split('.')[-1].lower()
     bare_type_name = unqualified_type_name.split('(')[0]
-    bare_function_name = f"cast_to_{bare_type_name}"
+    function_type_name = '_'.join(bare_type_name.split())
+    bare_function_name = f"cast_to_{function_type_name}"
     return f"{base.get_qualified_name(bare_function_name)}"
 
 
@@ -231,39 +257,53 @@ def _get_boolean_type_body_map():
     Get SQL strings that create various functions for casting different
     types to booleans.
 
-    boolean -> boolean:  Identity. No remarks
-    varchar -> boolean:     We only cast 't', 'f', 'true', or 'false' all
-                         others raise a custom exception.
-    numeric -> boolean:  We only cast 1 -> true, 0 -> false (this is not
-                         default behavior for PostgreSQL). Others raise a
-                         custom exception.
+    boolean -> boolean:      Identity. No remarks
+    varchar -> boolean:      We only cast 't', 'f', 'true', or 'false'
+                             all others raise a custom exception.
+    number type -> boolean:  We only cast numbers 1 -> true, 0 -> false
+                             (this is not default behavior for
+                             PostgreSQL).  Others raise a custom
+                             exception.
     """
+    source_number_types = [DECIMAL, DOUBLE_PRECISION, FLOAT, NUMERIC, REAL]
+    default_behavior_source_types = [BOOLEAN]
+
     not_bool_exception_str = f"RAISE EXCEPTION '% is not a {BOOLEAN}', $1;"
-    return {
-        BOOLEAN: """
-        BEGIN
-          RETURN $1;
-        END;
-        """,
-        VARCHAR: f"""
-        DECLARE
-        istrue {BOOLEAN};
-        BEGIN
-          SELECT lower($1)='t' OR lower($1)='true' OR $1='1' INTO istrue;
-          IF istrue OR lower($1)='f' OR lower($1)='false' OR $1='0' THEN
-            RETURN istrue;
-          END IF;
-          {not_bool_exception_str}
-        END;
-        """,
-        NUMERIC: f"""
+
+    def _get_number_to_boolean_cast_str():
+        return f"""
         BEGIN
           IF $1<>0 AND $1<>1 THEN
             {not_bool_exception_str} END IF;
           RETURN $1<>0;
         END;
-        """,
-    }
+        """
+    type_body_map = _get_default_type_body_map(
+        default_behavior_source_types, BOOLEAN,
+    )
+    type_body_map.update(
+        {
+            number_type: _get_number_to_boolean_cast_str()
+            for number_type in source_number_types
+        }
+    )
+    type_body_map.update(
+        {
+            VARCHAR: f"""
+            DECLARE
+            istrue {BOOLEAN};
+            BEGIN
+              SELECT lower($1)='t' OR lower($1)='true' OR $1='1' INTO istrue;
+              IF istrue OR lower($1)='f' OR lower($1)='false' OR $1='0' THEN
+                RETURN istrue;
+              END IF;
+              {not_bool_exception_str}
+            END;
+            """,
+        }
+
+    )
+    return type_body_map
 
 
 def _get_email_type_body_map():
@@ -276,18 +316,10 @@ def _get_email_type_body_map():
                      just check that the VARCHAR object satisfies the email
                      DOMAIN).
     """
-    return {
-        email.QUALIFIED_EMAIL: """
-        BEGIN
-          RETURN $1;
-        END;
-        """,
-        VARCHAR: f"""
-        BEGIN
-          RETURN $1::{email.QUALIFIED_EMAIL};
-        END;
-        """,
-    }
+    default_behavior_source_types = [email.QUALIFIED_EMAIL, VARCHAR]
+    return _get_default_type_body_map(
+        default_behavior_source_types, email.QUALIFIED_EMAIL,
+    )
 
 
 def _get_interval_type_body_map():
@@ -320,72 +352,53 @@ def _get_interval_type_body_map():
     }
 
 
-def _get_numeric_type_body_map():
+def _get_decimal_number_type_body_map(target_type_str=NUMERIC):
     """
     Get SQL strings that create various functions for casting different
-    types to numerics.
+    types to number types including DECIMAL, DOUBLE PRECISION, FLOAT,
+    NUMERIC, and REAL.
 
-    numeric -> numeric:  Identity. No remarks
-    boolean -> numeric:  We cast TRUE -> 1, FALSE -> 0
-    varchar -> numeric:     We use the default PostgreSQL behavior.
+    The only notable non-default cast is from boolean:
+        boolean -> number:  We cast TRUE -> 1, FALSE -> 0
+    """
+
+    default_behavior_source_types = [
+        DECIMAL, DOUBLE_PRECISION, FLOAT, NUMERIC, REAL, VARCHAR
+    ]
+    type_body_map = _get_default_type_body_map(
+        default_behavior_source_types, target_type_str,
+    )
+    type_body_map.update({BOOLEAN: _get_boolean_to_number_cast(target_type_str)})
+    return type_body_map
+
+
+def _get_boolean_to_number_cast(target_type):
+    return f"""
+    BEGIN
+      IF $1 THEN
+        RETURN 1::{target_type};
+      END IF;
+      RETURN 0::{target_type};
+    END;
     """
-    return {
-        NUMERIC: """
-        BEGIN
-          RETURN $1;
-        END;
-        """,
-        BOOLEAN: f"""
-        BEGIN
-          IF $1 THEN
-            RETURN 1::{NUMERIC};
-          END IF;
-          RETURN 0;
-        END;
-        """,
-        VARCHAR: f"""
-        BEGIN
-          RETURN $1::{NUMERIC};
-        END;
-        """,
-    }
 
 
-def _get_varchar_type_body_map():
+def _get_varchar_type_body_map(engine):
     """
     Get SQL strings that create various functions for casting different
     types to varchar.
 
-    varchar -> varchar:   Identity. No remarks
-    boolean -> varchar:   We use the default PostgreSQL cast behavior.
-    email -> varchar:     We use the default PostgreSQL cast behavior.
-    interval -> varchar:  We use the default PostgreSQL cast behavior.
-    numeric -> varchar:   We use the default PostgreSQL cast behavior.
+    All casts to varchar use default PostgreSQL behavior.
+    All types in get_supported_alter_column_types are supported.
     """
-    return {
-        VARCHAR: """
-        BEGIN
-          RETURN $1;
-        END;
-        """,
-        BOOLEAN: f"""
-        BEGIN
-          RETURN $1::{VARCHAR};
-        END;
-        """,
-        email.QUALIFIED_EMAIL: f"""
-        BEGIN
-          RETURN $1::{VARCHAR};
-        END;
-        """,
-        INTERVAL: f"""
-        BEGIN
-          RETURN $1::{VARCHAR};
-        END;
-        """,
-        NUMERIC: f"""
+    supported_types = get_supported_alter_column_db_types(engine)
+    return _get_default_type_body_map(supported_types, VARCHAR)
+
+
+def _get_default_type_body_map(source_types, target_type_str):
+    default_cast_str = f"""
         BEGIN
-          RETURN $1::{VARCHAR};
+          RETURN $1::{target_type_str};
         END;
-        """,
-    }
+    """
+    return {type_name: default_cast_str for type_name in source_types}


Test patch:
diff --git a/db/tests/test_columns.py b/db/tests/test_columns.py
--- a/db/tests/test_columns.py
+++ b/db/tests/test_columns.py
@@ -8,10 +8,12 @@
 )
 from sqlalchemy.exc import IntegrityError
 from db import columns, tables, constants
-from db.types import email
+from db.types import email, alteration
 from db.tests.types import fixtures
 
 engine_with_types = fixtures.engine_with_types
+temporary_testing_schema = fixtures.temporary_testing_schema
+engine_email_type = fixtures.engine_email_type
 
 
 def init_column(*args, **kwargs):
@@ -383,10 +385,10 @@ def test_retype_column_correct_column(engine_with_schema):
     )
 
 
-def test_retype_column_adds_options(engine_with_schema):
+@pytest.mark.parametrize('target_type', ['numeric', 'decimal'])
+def test_retype_column_adds_options(engine_with_schema, target_type):
     engine, schema = engine_with_schema
     table_name = "atableone"
-    target_type = "numeric"
     target_column_name = "thecolumntochange"
     nontarget_column_name = "notthecolumntochange"
     table = Table(
@@ -404,19 +406,47 @@ def test_retype_column_adds_options(engine_with_schema):
         schema,
         table_name,
         target_column_name,
-        "numeric",
+        target_type,
         engine,
         friendly_names=False,
         type_options=type_options,
     )
 
 
-def test_create_column(engine_with_schema):
-    engine, schema = engine_with_schema
+type_set = {
+    'BOOLEAN',
+    'DECIMAL',
+    'DOUBLE PRECISION',
+    'FLOAT',
+    'INTERVAL',
+    'NUMERIC',
+    'REAL',
+    'VARCHAR',
+    'mathesar_types.email',
+}
+
+
+def test_type_list_completeness(engine_with_types):
+    """
+    This metatest ensures that tests parameterized on the type_set
+    use the entire set supported.
+    """
+    actual_supported_db_types = alteration.get_supported_alter_column_db_types(
+        engine_with_types
+    )
+    assert type_set == actual_supported_db_types
+
+
+@pytest.mark.parametrize("target_type", type_set)
+def test_create_column(engine_email_type, target_type):
+    engine, schema = engine_email_type
     table_name = "atableone"
-    target_type = "BOOLEAN"
     initial_column_name = "original_column"
     new_column_name = "added_column"
+    input_output_type_map = {type_: type_ for type_ in type_set}
+    # update the map with types that reflect differently than they're
+    # set when creating a column
+    input_output_type_map.update({'FLOAT': 'DOUBLE PRECISION', 'DECIMAL': 'NUMERIC'})
     table = Table(
         table_name,
         MetaData(bind=engine, schema=schema),
@@ -429,13 +459,13 @@ def test_create_column(engine_with_schema):
     altered_table = tables.reflect_table_from_oid(table_oid, engine)
     assert len(altered_table.columns) == 2
     assert created_col.name == new_column_name
-    assert created_col.type.compile(engine.dialect) == "BOOLEAN"
+    assert created_col.type.compile(engine.dialect) == input_output_type_map[target_type]
 
 
-def test_create_column_options(engine_with_schema):
-    engine, schema = engine_with_schema
+@pytest.mark.parametrize("target_type", ["NUMERIC", "DECIMAL"])
+def test_create_column_options(engine_email_type, target_type):
+    engine, schema = engine_email_type
     table_name = "atableone"
-    target_type = "NUMERIC"
     initial_column_name = "original_column"
     new_column_name = "added_column"
     table = Table(
diff --git a/db/tests/types/test_alteration.py b/db/tests/types/test_alteration.py
--- a/db/tests/types/test_alteration.py
+++ b/db/tests/types/test_alteration.py
@@ -18,10 +18,85 @@
 temporary_testing_schema = fixtures.temporary_testing_schema
 
 
-def test_get_alter_column_types_with_standard_engine(engine):
-    type_dict = alteration.get_supported_alter_column_types(engine)
-    assert len(type_dict) > 0
-    assert all([type_ not in type_dict for type_ in types.CUSTOM_TYPE_DICT])
+BOOLEAN = "BOOLEAN"
+DECIMAL = "DECIMAL"
+DOUBLE = "DOUBLE PRECISION"
+EMAIL = "mathesar_types.email"
+FLOAT = "FLOAT"
+INTERVAL = "INTERVAL"
+NUMERIC = "NUMERIC"
+REAL = "REAL"
+VARCHAR = "VARCHAR"
+
+
+ISCHEMA_NAME = "ischema_name"
+TARGET_LIST = "target_list"
+REFLECTED_NAME = "reflected_name"
+SUPPORTED_MAP_NAME = "supported_map_name"
+
+MASTER_DB_TYPE_MAP_SPEC = {
+    # This dict specifies the full map of what types can be cast to what
+    # target types in Mathesar.  Format of each key, val pair is:
+    # <db_set_type_name>: {
+    #     ISCHEMA_NAME: <name for looking up in engine.dialect.ischema_names>,
+    #     REFLECTED_NAME: <name for reflection of db type>,
+    #     SUPPORTED_MAP_NAME: <optional; key in supported type map dictionaries>
+    #     TARGET_LIST: <list of target db types for alteration>,
+    # }
+    # The tuples in TARGET_LIST should be (set, reflect), where 'set' is
+    # a valid DB type for setting a column type, and 'reflect' is the
+    # type actually reflected afterwards
+    BOOLEAN: {
+        ISCHEMA_NAME: "boolean",
+        REFLECTED_NAME: BOOLEAN,
+        TARGET_LIST: [BOOLEAN, DECIMAL, DOUBLE, FLOAT, NUMERIC, REAL, VARCHAR]
+    },
+    DECIMAL: {
+        ISCHEMA_NAME: "decimal",
+        REFLECTED_NAME: NUMERIC,
+        TARGET_LIST: [BOOLEAN, DECIMAL, DOUBLE, FLOAT, NUMERIC, REAL, VARCHAR]
+    },
+    DOUBLE: {
+        ISCHEMA_NAME: "double precision",
+        REFLECTED_NAME: DOUBLE,
+        TARGET_LIST: [BOOLEAN, DECIMAL, DOUBLE, FLOAT, NUMERIC, REAL, VARCHAR]
+    },
+    FLOAT: {
+        ISCHEMA_NAME: "float",
+        REFLECTED_NAME: DOUBLE,
+        TARGET_LIST: [BOOLEAN, DECIMAL, DOUBLE, FLOAT, NUMERIC, REAL, VARCHAR]
+    },
+    INTERVAL: {
+        ISCHEMA_NAME: "interval",
+        REFLECTED_NAME: INTERVAL,
+        TARGET_LIST: [INTERVAL, VARCHAR]
+    },
+    EMAIL: {
+        ISCHEMA_NAME: "mathesar_types.email",
+        SUPPORTED_MAP_NAME: "email",
+        REFLECTED_NAME: EMAIL,
+        TARGET_LIST: [EMAIL, VARCHAR]
+    },
+    NUMERIC: {
+        ISCHEMA_NAME: "numeric",
+        REFLECTED_NAME: NUMERIC,
+        TARGET_LIST: [BOOLEAN, DECIMAL, DOUBLE, FLOAT, NUMERIC, REAL, VARCHAR]
+    },
+    REAL: {
+        ISCHEMA_NAME: "real",
+        REFLECTED_NAME: REAL,
+        TARGET_LIST: [BOOLEAN, DECIMAL, DOUBLE, FLOAT, NUMERIC, REAL, VARCHAR]
+    },
+    VARCHAR: {
+        ISCHEMA_NAME: "character varying",
+        SUPPORTED_MAP_NAME: "varchar",
+        REFLECTED_NAME: VARCHAR,
+        TARGET_LIST: [
+            BOOLEAN, DECIMAL, DOUBLE, EMAIL, FLOAT, INTERVAL, NUMERIC, REAL,
+            VARCHAR,
+        ]
+    }
+}
 
 
 def test_get_alter_column_types_with_custom_engine(engine_with_types):
@@ -47,13 +122,28 @@ def test_get_alter_column_types_with_unfriendly_names(engine_with_types):
 
 
 type_test_list = [
-    (String, "boolean", {}, "BOOLEAN"),
-    (String, "interval", {}, "INTERVAL"),
-    (String, "numeric", {}, "NUMERIC"),
-    (String, "numeric", {"precision": 5}, "NUMERIC(5, 0)"),
-    (String, "numeric", {"precision": 5, "scale": 3}, "NUMERIC(5, 3)"),
-    (String, "string", {}, "VARCHAR"),
-    (String, "email", {}, "mathesar_types.email"),
+    (
+        val[ISCHEMA_NAME],
+        MASTER_DB_TYPE_MAP_SPEC[target].get(
+            SUPPORTED_MAP_NAME, MASTER_DB_TYPE_MAP_SPEC[target][ISCHEMA_NAME]
+        ),
+        {},
+        MASTER_DB_TYPE_MAP_SPEC[target][REFLECTED_NAME]
+    )
+    for val in MASTER_DB_TYPE_MAP_SPEC.values()
+    for target in val[TARGET_LIST]
+] + [
+    (val[ISCHEMA_NAME], "numeric", {"precision": 5}, "NUMERIC(5, 0)")
+    for val in MASTER_DB_TYPE_MAP_SPEC.values() if NUMERIC in val[TARGET_LIST]
+] + [
+    (val[ISCHEMA_NAME], "numeric", {"precision": 5, "scale": 3}, "NUMERIC(5, 3)")
+    for val in MASTER_DB_TYPE_MAP_SPEC.values() if NUMERIC in val[TARGET_LIST]
+] + [
+    (val[ISCHEMA_NAME], "decimal", {"precision": 5}, "NUMERIC(5, 0)")
+    for val in MASTER_DB_TYPE_MAP_SPEC.values() if DECIMAL in val[TARGET_LIST]
+] + [
+    (val[ISCHEMA_NAME], "decimal", {"precision": 5, "scale": 3}, "NUMERIC(5, 3)")
+    for val in MASTER_DB_TYPE_MAP_SPEC.values() if DECIMAL in val[TARGET_LIST]
 ]
 
 
@@ -63,6 +153,11 @@ def test_get_alter_column_types_with_unfriendly_names(engine_with_types):
 def test_alter_column_type_alters_column_type(
         engine_email_type, type_, target_type, options, expect_type
 ):
+    """
+    The massive number of cases make sure all type casting functions at
+    least pass a smoke test for each type mapping defined in
+    MASTER_DB_TYPE_MAP_SPEC above.
+    """
     engine, schema = engine_email_type
     TABLE_NAME = "testtable"
     COLUMN_NAME = "testcol"
@@ -70,7 +165,7 @@ def test_alter_column_type_alters_column_type(
     input_table = Table(
         TABLE_NAME,
         metadata,
-        Column(COLUMN_NAME, type_),
+        Column(COLUMN_NAME, engine.dialect.ischema_names[type_]),
         schema=schema
     )
     input_table.create()
@@ -277,24 +372,15 @@ def test_get_column_cast_expression_numeric_options(
     assert str(cast_expr) == expect_cast_expr
 
 
-def test_get_full_cast_map(engine_with_types):
-    """
-    This test specifies the full map of what types can be cast to what
-    target types in Mathesar.  When the map is modified, this test
-    should be updated accordingly.
-    """
-    expect_cast_map = {
-        'NUMERIC': ['BOOLEAN', 'NUMERIC', 'VARCHAR'],
-        'VARCHAR': ['NUMERIC', 'VARCHAR', 'INTERVAL', 'mathesar_types.email', 'BOOLEAN'],
-        'mathesar_types.email': ['mathesar_types.email', 'VARCHAR'],
-        'INTERVAL': ['INTERVAL', 'VARCHAR'],
-        'BOOLEAN': ['NUMERIC', 'BOOLEAN', 'VARCHAR']
-    }
+expect_cast_tuples = [
+    (key, [target for target in val[TARGET_LIST]])
+    for key, val in MASTER_DB_TYPE_MAP_SPEC.items()
+]
+
+
+@pytest.mark.parametrize("source_type,expect_target_types", expect_cast_tuples)
+def test_get_full_cast_map(engine_with_types, source_type, expect_target_types):
     actual_cast_map = alteration.get_full_cast_map(engine_with_types)
-    assert len(actual_cast_map) == len(expect_cast_map)
-    assert all(
-        [
-            sorted(actual_cast_map[type_]) == sorted(expect_target_list)
-            for type_, expect_target_list in expect_cast_map.items()
-        ]
-    )
+    actual_target_types = actual_cast_map[source_type]
+    assert len(actual_target_types) == len(expect_target_types)
+    assert sorted(actual_target_types) == sorted(expect_target_types)
diff --git a/mathesar/tests/views/api/test_column_api.py b/mathesar/tests/views/api/test_column_api.py
--- a/mathesar/tests/views/api/test_column_api.py
+++ b/mathesar/tests/views/api/test_column_api.py
@@ -71,8 +71,12 @@ def test_column_list(column_test_table, client):
             'primary_key': False,
             'valid_target_types': [
                 'BOOLEAN',
+                'DECIMAL',
+                'DOUBLE PRECISION',
+                'FLOAT',
                 'INTERVAL',
                 'NUMERIC',
+                'REAL',
                 'VARCHAR',
                 'mathesar_types.email',
             ],