Repository: mathesar-foundation/mathesar

Issue:
Redundant methods for creating constraints
## Problem
<!-- A clear and concise description of what the bug is. -->
Notice the `db.constraints.operations.create` and `db.constraints.base`, looks like we have 2 ways to achieve creating the same constraint. 

## Expected behavior
<!-- A clear and concise description of what you expected to happen. -->
We should be having all the constraints creation logic in the `db.constraints.operations.create` and there should be only one method that actually creates different constraints in the db.

## Additional context
<!-- Add any other context about the problem or screenshots here. -->
Related to RSQLA #2737


Discussion:
cc @mathemancer 
can you assign me

Sorry @ayesha-firdaus, I am currently working on this and the details regarding solving this issue are still being figured out. Please feel free to look through other issues to solve. 

Resolving patch:
diff --git a/db/constraints/base.py b/db/constraints/base.py
--- a/db/constraints/base.py
+++ b/db/constraints/base.py
@@ -1,29 +1,35 @@
-"""TODO: This needs to be consolidated with db.constraints.operations.create."""
-
-from abc import ABC, abstractmethod
 import json
-
-from db.connection import execute_msar_func_with_engine
+from abc import ABC, abstractmethod
 from db.constraints.utils import (
     get_constraint_match_char_from_type, get_constraint_char_from_action
 )
 
 
 class Constraint(ABC):
-
     @abstractmethod
-    def add_constraint(self, schema, engine, connection_to_use):
+    def get_constraint_def_json(self):
         pass
 
-    @abstractmethod
-    def constraint_type(self):
-        pass
 
+class UniqueConstraint(Constraint):
+    def __init__(self, name, table_oid, columns_attnum):
+        self.name = name
+        self.table_oid = table_oid
+        self.columns_attnum = columns_attnum
 
-class ForeignKeyConstraint(Constraint):
-    def constraint_type(self):
-        return "foreignkey"
+    def get_constraint_def_json(self):
+        return json.dumps(
+            [
+                {
+                    'name': self.name,
+                    'type': 'u',
+                    'columns': self.columns_attnum
+                }
+            ],
+        )
 
+
+class ForeignKeyConstraint(Constraint):
     def __init__(
             self, name,
             table_oid,
@@ -39,54 +45,22 @@ def __init__(
         self.referent_columns = referent_columns_attnum
         self.options = options
 
-    def add_constraint(self, schema, engine, connection_to_use):
+    def get_constraint_def_json(self):
         match_type = get_constraint_match_char_from_type(self.options.get('match'))
         on_update = get_constraint_char_from_action(self.options.get('onupdate'))
         on_delete = get_constraint_char_from_action(self.options.get('ondelete'))
-        return execute_msar_func_with_engine(
-            engine,
-            'add_constraints',
-            self.table_oid,
-            json.dumps(
-                [
-                    {
-                        'name': self.name,
-                        'type': 'f',
-                        'columns': self.columns_attnum,
-                        'deferrable': self.options.get('deferrable'),
-                        'fkey_relation_id': self.referent_table_oid,
-                        'fkey_columns': self.referent_columns,
-                        'fkey_update_action': on_update,
-                        'fkey_delete_action': on_delete,
-                        'fkey_match_type': match_type,
-                    }
-                ]
-            )
-        ).fetchone()[0]
-
-
-class UniqueConstraint(Constraint):
-
-    def __init__(self, name, table_oid, columns_attnum):
-        self.name = name
-        self.table_oid = table_oid
-        self.columns_attnum = columns_attnum
-
-    def add_constraint(self, schema, engine, connection_to_use):
-        return execute_msar_func_with_engine(
-            engine,
-            'add_constraints',
-            self.table_oid,
-            json.dumps(
-                [
-                    {
-                        'name': self.name,
-                        'type': 'u',
-                        'columns': self.columns_attnum
-                    }
-                ],
-            )
-        ).fetchone()[0]
-
-    def constraint_type(self):
-        return "unique"
+        return json.dumps(
+            [
+                {
+                    'name': self.name,
+                    'type': 'f',
+                    'columns': self.columns_attnum,
+                    'deferrable': self.options.get('deferrable'),
+                    'fkey_relation_id': self.referent_table_oid,
+                    'fkey_columns': self.referent_columns,
+                    'fkey_update_action': on_update,
+                    'fkey_delete_action': on_delete,
+                    'fkey_match_type': match_type,
+                }
+            ]
+        )
diff --git a/db/constraints/operations/create.py b/db/constraints/operations/create.py
--- a/db/constraints/operations/create.py
+++ b/db/constraints/operations/create.py
@@ -1,34 +1,21 @@
-"""TODO This needs to be consolidated with db.constraints.base"""
-import json
-
 from db.connection import execute_msar_func_with_engine
-from db.constraints.utils import get_constraint_type_from_char, ConstraintType
 
 
-def create_unique_constraint(table_name, schema, engine, columns, constraint_name=None):
+def add_constraint(constraint_obj, engine):
+    """
+    Add a constraint.
+
+    Args:
+        constraint_obj: A constraint object instantiatated with appropriate
+                        params.
+        engine: SQLAlchemy engine object for connecting.
+
+    Returns:
+        Returns a list of oid(s) of constraints for a given table.
+    """
     return execute_msar_func_with_engine(
         engine,
         'add_constraints',
-        schema,
-        table_name,
-        json.dumps([{'name': constraint_name, 'type': 'u', 'columns': columns}])
+        constraint_obj.table_oid,
+        constraint_obj.get_constraint_def_json()
     ).fetchone()[0]
-
-
-def create_constraint(schema, engine, constraint_obj):
-    with engine.begin() as conn:
-        return constraint_obj.add_constraint(schema, engine, conn)
-
-
-def copy_constraint(_, engine, constraint, from_column_attnum, to_column_attnum):
-    constraint_type = get_constraint_type_from_char(constraint.contype)
-    if constraint_type == ConstraintType.UNIQUE.value:
-        return execute_msar_func_with_engine(
-            engine,
-            'copy_constraint',
-            constraint.oid,
-            from_column_attnum,
-            to_column_attnum
-        ).fetchone()[0]
-    else:
-        raise NotImplementedError
diff --git a/mathesar/models/base.py b/mathesar/models/base.py
--- a/mathesar/models/base.py
+++ b/mathesar/models/base.py
@@ -17,7 +17,7 @@
     get_column_attnum_from_names_as_map, get_column_name_from_attnum,
     get_map_of_attnum_to_column_name, get_map_of_attnum_and_table_oid_to_column_name,
 )
-from db.constraints.operations.create import create_constraint
+from db.constraints.operations.create import add_constraint
 from db.constraints.operations.drop import drop_constraint
 from db.constraints.operations.select import get_constraint_record_from_oid
 from db.constraints import utils as constraint_utils
@@ -532,11 +532,7 @@ def add_constraint(self, constraint_obj):
         # the most newly-created constraint. Other methods (e.g., trying to get
         # a constraint by name when it wasn't set here) are even less robust.
         constraint_oid = max(
-            create_constraint(
-                self._sa_table.schema,
-                self.schema._sa_engine,
-                constraint_obj
-            )
+            add_constraint(constraint_obj, engine=self._sa_engine)
         )
         result = Constraint.current_objects.create(oid=constraint_oid, table=self)
         reset_reflection(db_name=self.schema.database.name)


Test patch:
diff --git a/db/tests/constraints/operations/test_drop.py b/db/tests/constraints/operations/test_drop.py
--- a/db/tests/constraints/operations/test_drop.py
+++ b/db/tests/constraints/operations/test_drop.py
@@ -1,6 +1,6 @@
 from sqlalchemy import String, Integer, Column, Table, MetaData
-
-from db.constraints.operations.create import create_unique_constraint
+from db.constraints.base import UniqueConstraint
+from db.constraints.operations.create import add_constraint
 from db.constraints.operations.drop import drop_constraint
 from db.tables.operations.select import get_oid_from_table, reflect_table_from_oid
 from db.tests.constraints import utils as test_utils
@@ -20,7 +20,8 @@ def test_drop_unique_constraint(engine_with_schema):
     table.create()
 
     table_oid = get_oid_from_table(table_name, schema, engine)
-    create_unique_constraint(table.name, schema, engine, [unique_column_name])
+    uq_constraint = UniqueConstraint(None, table_oid, [unique_column_name])
+    add_constraint(uq_constraint, engine)
     altered_table = reflect_table_from_oid(table_oid, engine, metadata=get_empty_metadata())
     test_utils.assert_primary_key_and_unique_present(altered_table)
     unique_constraint = test_utils.get_first_unique_constraint(altered_table)
diff --git a/db/tests/dependents/test_dependents.py b/db/tests/dependents/test_dependents.py
--- a/db/tests/dependents/test_dependents.py
+++ b/db/tests/dependents/test_dependents.py
@@ -1,12 +1,12 @@
 import pytest
 from sqlalchemy import MetaData, select, Index
 from sqlalchemy_utils import create_view
+from db.constraints.operations.create import add_constraint
 from db.constraints.base import ForeignKeyConstraint
 from db.dependents.dependents_utils import get_dependents_graph
 from db.constraints.operations.select import get_constraint_oid_by_name_and_table_oid
 from db.columns.operations.create import create_column
 from db.columns.operations.select import get_column_attnum_from_name
-from db.constraints.operations.create import create_constraint
 from db.types.base import PostgresType
 from db.metadata import get_empty_metadata
 
@@ -93,7 +93,7 @@ def test_self_reference(engine_with_schema, library_tables_oids):
     fk_column_attnum = create_column(engine, publishers_oid, {'name': 'Parent Publisher', 'type': PostgresType.INTEGER.id})[0]
     pk_column_attnum = get_column_attnum_from_name(publishers_oid, 'id', engine, metadata=get_empty_metadata())
     fk_constraint = ForeignKeyConstraint('Publishers_Publisher_fkey', publishers_oid, [fk_column_attnum], publishers_oid, [pk_column_attnum], {})
-    create_constraint(schema, engine, fk_constraint)
+    add_constraint(fk_constraint, engine)
 
     publishers_oid = library_tables_oids['Publishers']
     publishers_dependents_graph = get_dependents_graph(publishers_oid, engine, [])
@@ -114,7 +114,7 @@ def test_circular_reference(engine_with_schema, library_tables_oids):
     fk_column_attnum = create_column(engine, publishers_oid, {'name': 'Top Publication', 'type': PostgresType.INTEGER.id})[0]
     publications_pk_column_attnum = get_column_attnum_from_name(publications_oid, 'id', engine, metadata=get_empty_metadata())
     fk_constraint = ForeignKeyConstraint('Publishers_Publications_fkey', publishers_oid, [fk_column_attnum], publications_oid, [publications_pk_column_attnum], {})
-    create_constraint(schema, engine, fk_constraint)
+    add_constraint(fk_constraint, engine)
 
     publishers_dependents_graph = get_dependents_graph(publishers_oid, engine, [])
     publications_dependents_oids = _get_object_dependents_oids(publishers_dependents_graph, publications_oid)