Repository: mathesar-foundation/mathesar

Issue:
Move columns between tables
**Problem**
<!-- Please provide a clear and concise description of the problem that this feature request is designed to solve.-->
While data modeling, it may be that a user would want to be able to move a set of columns from Table A to Table B, where these tables are connected by a foreign key relationship.

**Proposed solution**
<!-- A clear and concise description of your proposed solution or feature. -->

We should provide this functionality, with the following restrictions (for now):
- The tables involved must be connected by a foreign key.
- The columns being moved cannot be refered to by a foreign key, or be a foreign key column
- The columns being moved cannot contain the primary key of a table.


Discussion:

Resolving patch:
diff --git a/db/tables.py b/db/tables.py
--- a/db/tables.py
+++ b/db/tables.py
@@ -79,8 +79,8 @@ def extract_columns_from_table(
             remainder_fk,
         )
         conn.execute(split_ins)
-        if drop_original_table:
-            old_table.drop()
+    if drop_original_table:
+        old_table.drop()
 
     return extracted_table, remainder_table, remainder_fk
 
@@ -213,12 +213,119 @@ def merge_tables(
             select(merged_columns, distinct=True).select_from(merge_join)
         )
         conn.execute(insert_stmt)
-        if drop_original_tables:
-            table_one.drop()
-            table_two.drop()
+
+    if drop_original_tables:
+        if table_one.foreign_keys:
+            table_one.drop(bind=engine)
+            table_two.drop(bind=engine)
+        else:
+            table_two.drop(bind=engine)
+            table_one.drop(bind=engine)
+
     return merged_table
 
 
+def move_column_between_related_tables(
+        source_table_name,
+        target_table_name,
+        column_names,
+        schema,
+        engine,
+):
+    TEMP_MERGED_TABLE_NAME = f"{constants.MATHESAR_PREFIX}_temp_merge_table"
+    source_table = reflect_table(source_table_name, schema, engine)
+    target_table = reflect_table(
+        target_table_name, schema, engine, metadata=source_table.metadata
+    )
+    relationship = _find_table_relationship(source_table, target_table)
+    moving_columns = [source_table.columns[n] for n in column_names]
+    assert _check_columns(relationship, source_table, moving_columns)
+    ext_args = _get_column_moving_extraction_args(
+        relationship,
+        target_table,
+        target_table_name,
+        source_table,
+        source_table_name,
+        moving_columns,
+        column_names,
+    )
+    (extracted_table_name, remainder_table_name, extraction_columns) = ext_args
+    merge_tables(
+        source_table_name,
+        target_table_name,
+        TEMP_MERGED_TABLE_NAME,
+        schema,
+        engine,
+        drop_original_tables=True,
+    )
+    extracted_table, remainder_table, _ = extract_columns_from_table(
+        TEMP_MERGED_TABLE_NAME,
+        [c.name for c in extraction_columns],
+        extracted_table_name,
+        remainder_table_name,
+        schema,
+        engine,
+        drop_original_table=True,
+    )
+    return extracted_table, remainder_table
+
+
+def _find_table_relationship(table_one, table_two):
+    """
+    This function takes two tables, and returns a dict defining the direction
+    of the foreign key constraint relating the tables (if one exists)
+    """
+    one_referencing_two = [
+        fkey_constraint for fkey_constraint in table_one.foreign_key_constraints
+        if fkey_constraint.referred_table == table_two
+    ]
+    two_referencing_one = [
+        fkey_constraint for fkey_constraint in table_two.foreign_key_constraints
+        if fkey_constraint.referred_table == table_one
+    ]
+    if one_referencing_two and not two_referencing_one:
+        relationship = {"referencing": table_one, "referenced": table_two}
+    elif two_referencing_one and not one_referencing_two:
+        relationship = {"referencing": table_two, "referenced": table_one}
+    else:
+        relationship = None
+    return relationship
+
+
+def _check_columns(relationship, source_table, moving_columns):
+    return (
+        relationship is not None
+        and all([not c.foreign_keys for c in moving_columns])
+    )
+
+
+def _get_column_moving_extraction_args(
+        relationship,
+        target_table,
+        target_table_name,
+        source_table,
+        source_table_name,
+        moving_columns,
+        column_names,
+):
+    if relationship["referenced"] == target_table:
+        extracted_table_name = target_table_name
+        remainder_table_name = source_table_name
+        extraction_columns = [
+            col for col in target_table.columns
+            if not columns.MathesarColumn.from_column(col).is_default
+        ] + moving_columns
+    else:
+        extracted_table_name = source_table_name
+        remainder_table_name = target_table_name
+        extraction_columns = [
+            col for col in target_table.columns
+            if not columns.MathesarColumn.from_column(col).is_default
+            and col not in column_names
+        ]
+    return extracted_table_name, remainder_table_name, extraction_columns
+
+
 def insert_rows_into_table(table, rows, engine):
     with engine.begin() as connection:
         result = connection.execute(table.insert(), rows)


Test patch: