Repository: mathesar-foundation/mathesar

Issue:
Type Inference 4:  Set best types for columns of table
**Problem**
<!-- Please provide a clear and concise description of the problem that this feature request is designed to solve.-->

Different types in Mathesar will enable different operations; for example, strings could be aggregated by concatenating, but numeric types could be aggregated by summing or multiplying. So far, while we can reflect different types, we have no way to determine the type most appropriate for a table.

**Proposed solution**
<!-- A clear and concise description of your proposed solution or feature. -->

Using the functions from #91 , #92 , and #93 , we should make a function that takes a `schema`, `original_table_name`, and `new_table_name`, creates a new table with typed columns of the same names as the old table, and inserts all data there.  The old table can be optionally dropped.


Discussion:
This should not directly take the output of #93, since types need to be verified manually by the user.

Resolving patch:
diff --git a/db/tables.py b/db/tables.py
--- a/db/tables.py
+++ b/db/tables.py
@@ -3,6 +3,7 @@
 )
 
 from db import columns, constants, schemas
+from db.types import inference
 
 
 def create_string_column_table(name, schema, column_names, engine):
@@ -335,3 +336,25 @@ def get_count(table, engine):
     query = select([func.count()]).select_from(table)
     with engine.begin() as conn:
         return conn.execute(query).scalar()
+
+
+def infer_table_column_types(
+        schema,
+        table_name,
+        engine,
+):
+    table = reflect_table(table_name, schema, engine)
+    # we only want to infer (modify) the type of non-default columns
+    inferable_column_names = (
+        col.name for col in table.columns
+        if not columns.MathesarColumn.from_column(col).is_default
+        and not col.primary_key
+        and not col.foreign_keys
+    )
+    for column_name in inferable_column_names:
+        inference.infer_column_type(
+            schema,
+            table_name,
+            column_name,
+            engine,
+        )
diff --git a/db/types/inference.py b/db/types/inference.py
--- a/db/types/inference.py
+++ b/db/types/inference.py
@@ -6,6 +6,8 @@
 
 logger = logging.getLogger(__name__)
 
+MAX_INFERENCE_DAG_DEPTH = 100
+
 TYPE_INFERENCE_DAG = {
     alteration.BOOLEAN: [],
     alteration.EMAIL: [],
@@ -34,7 +36,7 @@ def infer_column_type(
         depth=0,
         type_inference_dag=TYPE_INFERENCE_DAG,
 ):
-    if depth > 100:
+    if depth > MAX_INFERENCE_DAG_DEPTH:
         raise DagCycleError("The type_inference_dag likely has a cycle")
     supported_types = alteration.get_supported_alter_column_types(engine)
     reverse_type_map = {


Test patch:
diff --git a/db/tests/test_tables.py b/db/tests/test_tables.py
--- a/db/tests/test_tables.py
+++ b/db/tests/test_tables.py
@@ -1,6 +1,7 @@
 import os
+from unittest.mock import call, patch
 import pytest
-from sqlalchemy import text, MetaData, select
+from sqlalchemy import text, MetaData, select, Column, String
 from db import tables, constants, columns
 
 FILE_DIR = os.path.abspath(os.path.dirname(__file__))
@@ -54,10 +55,10 @@ def extracted_remainder_roster(engine_with_roster):
 
 
 def test_table_creation_doesnt_reuse_defaults(engine_with_schema):
-    columns = []
+    column_list = []
     engine = engine_with_schema
-    t1 = tables.create_mathesar_table("t1", APP_SCHEMA, columns, engine)
-    t2 = tables.create_mathesar_table("t2", APP_SCHEMA, columns, engine)
+    t1 = tables.create_mathesar_table("t1", APP_SCHEMA, column_list, engine)
+    t2 = tables.create_mathesar_table("t2", APP_SCHEMA, column_list, engine)
     assert all(
         [
             c1.name == c2.name and c1 != c2
@@ -355,3 +356,80 @@ def test_move_columns_moves_column_from_rem_to_ext(extracted_remainder_roster):
     actual_remainder_cols = [col.name for col in new_remainder.columns]
     assert sorted(actual_extracted_cols) == sorted(expect_extracted_cols)
     assert sorted(actual_remainder_cols) == sorted(expect_remainder_cols)
+
+
+def test_infer_table_column_types_doesnt_touch_defaults(engine_with_schema):
+    column_list = []
+    engine = engine_with_schema
+    table_name = "t1"
+    tables.create_mathesar_table(
+        table_name, APP_SCHEMA, column_list, engine
+    )
+    with patch.object(tables.inference, "infer_column_type") as mock_infer:
+        tables.infer_table_column_types(
+            APP_SCHEMA,
+            table_name,
+            engine
+        )
+    mock_infer.assert_not_called()
+
+
+def test_infer_table_column_types_infers_non_default_types(engine_with_schema):
+    col1 = Column("col1", String)
+    col2 = Column("col2", String)
+    column_list = [col1, col2]
+    engine = engine_with_schema
+    table_name = "table_with_columns"
+    tables.create_mathesar_table(
+        table_name, APP_SCHEMA, column_list, engine
+    )
+    with patch.object(tables.inference, "infer_column_type") as mock_infer:
+        tables.infer_table_column_types(
+            APP_SCHEMA,
+            table_name,
+            engine
+        )
+    expect_calls = [
+        call(
+            APP_SCHEMA,
+            table_name,
+            col1.name,
+            engine,
+        ),
+        call(
+            APP_SCHEMA,
+            table_name,
+            col2.name,
+            engine,
+        ),
+    ]
+    mock_infer.assert_has_calls(expect_calls)
+
+
+def test_infer_table_column_types_skips_pkey_columns(engine_with_schema):
+    column_list = [Column("checkcol", String, primary_key=True)]
+    engine = engine_with_schema
+    table_name = "t1"
+    tables.create_mathesar_table(
+        table_name, APP_SCHEMA, column_list, engine
+    )
+    with patch.object(tables.inference, "infer_column_type") as mock_infer:
+        tables.infer_table_column_types(
+            APP_SCHEMA,
+            table_name,
+            engine
+        )
+    mock_infer.assert_not_called()
+
+
+def test_infer_table_column_types_skips_fkey_columns(
+        extracted_remainder_roster
+):
+    _, remainder, _, engine = extracted_remainder_roster
+    with patch.object(tables.inference, "infer_column_type") as mock_infer:
+        tables.infer_table_column_types(
+            APP_SCHEMA,
+            remainder.name,
+            engine
+        )
+    assert all([call_[1][2] != FKEY_COL for call_ in mock_infer.mock_calls])