Repository: mathesar-foundation/mathesar

Issue:
DB function to edit column names
**Problem**
<!-- Please provide a clear and concise description of the problem that this feature request is designed to solve.-->
The user might want to edit column names.

**Proposed solution**
<!-- A clear and concise description of your proposed solution or feature. -->
We need a `db` function to allow the user to edit the name of a column. We need to check and fix foreign keys and other references to the column.


Discussion:

Resolving patch:
diff --git a/db/columns.py b/db/columns.py
--- a/db/columns.py
+++ b/db/columns.py
@@ -1,4 +1,4 @@
-from sqlalchemy import Column, Integer, ForeignKey
+from sqlalchemy import Column, Integer, ForeignKey, Table, DDL, MetaData
 from db import constants
 
 
@@ -88,3 +88,19 @@ def init_mathesar_table_column_list_with_defaults(column_list):
     default_columns = get_default_mathesar_column_list()
     given_columns = [MathesarColumn.from_column(c) for c in column_list]
     return default_columns + given_columns
+
+
+def rename_column(schema, table_name, column_name, new_column_name, engine):
+    _preparer = engine.dialect.identifier_preparer
+    with engine.begin() as conn:
+        metadata = MetaData(bind=engine, schema=schema)
+        table = Table(table_name, metadata, schema=schema, autoload_with=engine)
+        column = table.columns[column_name]
+        prepared_table_name = _preparer.format_table(table)
+        prepared_column_name = _preparer.format_column(column)
+        prepared_new_column_name = _preparer.quote(new_column_name)
+        alter_stmt = f"""
+        ALTER TABLE {prepared_table_name}
+        RENAME {prepared_column_name} TO {prepared_new_column_name}
+        """
+        conn.execute(DDL(alter_stmt))


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
@@ -1,7 +1,7 @@
 import re
 import pytest
-from sqlalchemy import String, Integer, ForeignKey
-from db import columns
+from sqlalchemy import String, Integer, ForeignKey, Column, select, Table, MetaData
+from db import columns, tables, constants
 
 
 def init_column(*args, **kwargs):
@@ -18,6 +18,17 @@ class method returns the same value of the original __init__ for a given
     return columns.MathesarColumn.from_column(col)
 
 
+def _rename_column(schema, table_name, old_col_name, new_col_name, engine):
+    """
+    Renames the colum of a table and assert the change went through
+    """
+    columns.rename_column(schema, table_name, old_col_name, new_col_name, engine)
+    table = tables.reflect_table(table_name, schema, engine)
+    assert new_col_name in table.columns
+    assert old_col_name not in table.columns
+    return table
+
+
 column_builder_list = [init_column, from_column_column]
 
 
@@ -126,6 +137,71 @@ def test_MC_is_default_when_false_for_pk():
         assert not col.is_default
 
 
+def test_rename_column(engine_with_schema):
+    old_col_name = "col1"
+    new_col_name = "col2"
+    table_name = "table_with_columns"
+    engine, schema = engine_with_schema
+    metadata = MetaData(bind=engine, schema=schema)
+    Table(table_name, metadata, Column(old_col_name, String)).create()
+    _rename_column(schema, table_name, old_col_name, new_col_name, engine)
+
+
+def test_rename_column_foreign_keys(engine_with_schema):
+    engine, schema = engine_with_schema
+    table_name = "table_to_split"
+    columns_list = [Column("Filler 1", Integer), Column("Filler 2", Integer)]
+    tables.create_mathesar_table(table_name, schema, columns_list, engine)
+    extracted, remainder, fk_name = tables.extract_columns_from_table(
+        table_name, ["Filler 1"], "Extracted", "Remainder", schema, engine
+    )
+    new_fk_name = "new_" + fk_name
+    remainder = _rename_column(schema, remainder.name, fk_name, new_fk_name, engine)
+
+    fk = list(remainder.foreign_keys)[0]
+    assert fk.parent.name == new_fk_name
+    assert fk.column.table.name == extracted.name
+
+
+def test_rename_column_sequence(engine_with_schema):
+    old_col_name = constants.ID
+    new_col_name = "new_" + constants.ID
+    engine, schema = engine_with_schema
+    table_name = "table_with_columns"
+    table = tables.create_mathesar_table(table_name, schema, [], engine)
+    with engine.begin() as conn:
+        ins = table.insert()
+        conn.execute(ins)
+
+    table = _rename_column(schema, table_name, old_col_name, new_col_name, engine)
+
+    with engine.begin() as conn:
+        ins = table.insert()
+        conn.execute(ins)
+        slct = select(table)
+        result = conn.execute(slct)
+    new_value = result.fetchall()[-1][new_col_name]
+    assert new_value == 2
+
+
+def test_rename_column_index(engine_with_schema):
+    old_col_name = constants.ID
+    new_col_name = "new_" + constants.ID
+    engine, schema = engine_with_schema
+    table_name = "table_with_index"
+    metadata = MetaData(bind=engine, schema=schema)
+    table = Table(table_name, metadata, Column(old_col_name, Integer, index=True))
+    table.create()
+
+    table = _rename_column(schema, table_name, old_col_name, new_col_name, engine)
+
+    with engine.begin() as conn:
+        index = engine.dialect.get_indexes(conn, table_name, schema)[0]
+        index_columns = index["column_names"]
+    assert old_col_name not in index_columns
+    assert new_col_name in index_columns
+
+
 def get_mathesar_column_init_args():
     init_code = columns.MathesarColumn.__init__.__code__
     return init_code.co_varnames[1:init_code.co_argcount]