Repository: mathesar-foundation/mathesar

Issue:
Refactor dependents tests to use library usecase
## Problem
Initially, both DB and API tests were created using dummy data and were quite difficult to read. 

## Proposed solution
Refactor them to use a common use case and simplify the cases.



Discussion:

Resolving patch:
diff --git a/db/dependents/dependents_utils.py b/db/dependents/dependents_utils.py
--- a/db/dependents/dependents_utils.py
+++ b/db/dependents/dependents_utils.py
@@ -26,7 +26,7 @@ def get_dependents_graph(referenced_object_id, engine):
     # recursive member which includes dependents for each object of the previous level
     recursive = select(
         dependency_pairs_cte,
-        (anchor.c.level + 1).label('level'),
+        (anchor.c.level + 1),
         anchor.c.dependency_chain + array([anchor.c.objid])) \
         .where(anchor.c.level < MAX_LEVEL) \
         .where(dependency_pairs_cte.c.objid != any_(anchor.c.dependency_chain)) \
@@ -82,7 +82,7 @@ def _get_foreign_key_constraint_dependents(pg_identify_object, dependency_pair):
     return dependency_pair.where(pg_identify_object.c.type == 'table constraint')
 
 
-# getting a full list of dependents and identifying them
+# stmt for getting a full list of dependents and identifying them
 def _get_dependency_pairs_stmt(pg_depend, pg_identify_object, pg_identify_refobject):
     result = select(
         pg_depend,
@@ -136,8 +136,8 @@ def _get_typed_dependency_pairs_stmt(engine):
 
     # each statement filters the base statement extracting dependents of a specific type
     # so it's easy to exclude particular types or add new
-    dependency_pair = _get_dependency_pairs_stmt(pg_depend, pg_identify_object, pg_identify_refobject)
-    foreign_key_constraint_dependents = _get_foreign_key_constraint_dependents(pg_identify_object, dependency_pair).cte('foreign_key_constraint_dependents')
+    dependency_pairs = _get_dependency_pairs_stmt(pg_depend, pg_identify_object, pg_identify_refobject)
+    foreign_key_constraint_dependents = _get_foreign_key_constraint_dependents(pg_identify_object, dependency_pairs).cte('foreign_key_constraint_dependents')
     table_dependents = _get_table_dependents(foreign_key_constraint_dependents, pg_constraint).cte('table_dependents')
 
     return union(


Test patch:
diff --git a/db/tests/dependents/__init__.py b/db/tests/dependents/__init__.py
new file mode 100644
diff --git a/db/tests/dependents/conftest.py b/db/tests/dependents/conftest.py
new file mode 100644
--- /dev/null
+++ b/db/tests/dependents/conftest.py
@@ -0,0 +1,24 @@
+import pytest
+
+from db.tables.operations.select import get_oid_from_table
+
+
+@pytest.fixture
+def library_tables_oids(engine_with_library):
+    engine, schema = engine_with_library
+
+    authors_oid = get_oid_from_table('Authors', schema, engine)
+    checkouts_oid = get_oid_from_table('Checkouts', schema, engine)
+    items_oid = get_oid_from_table('Items', schema, engine)
+    patrons_oid = get_oid_from_table('Patrons', schema, engine)
+    publications_oid = get_oid_from_table('Publications', schema, engine)
+    publishers_oid = get_oid_from_table('Publishers', schema, engine)
+
+    return {
+        'Authors': authors_oid,
+        'Checkouts': checkouts_oid,
+        'Items': items_oid,
+        'Patrons': patrons_oid,
+        'Publications': publications_oid,
+        'Publishers': publishers_oid
+    }
diff --git a/db/tests/dependents/test_dependents.py b/db/tests/dependents/test_dependents.py
new file mode 100644
--- /dev/null
+++ b/db/tests/dependents/test_dependents.py
@@ -0,0 +1,163 @@
+from sqlalchemy import Column, ForeignKey, Integer, MetaData, Table
+from db.constraints.base import ForeignKeyConstraint
+from db.dependents.dependents_utils import get_dependents_graph
+from db.tables.operations.select import get_oid_from_table
+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
+
+
+def _get_object_dependents(dependents_graph, object_oid):
+    return list(filter(lambda x: x['parent_obj']['objid'] == object_oid, dependents_graph))
+
+
+def _get_object_dependents_oids(dependents_graph, object_oid):
+    return [dependent['obj']['objid'] for dependent in _get_object_dependents(dependents_graph, object_oid)]
+
+
+def test_correct_dependents_amount_and_level(engine, library_tables_oids):
+    publishers_dependents_graph = get_dependents_graph(library_tables_oids['Publishers'], engine)
+
+    publishers_dependents = _get_object_dependents(publishers_dependents_graph, library_tables_oids['Publishers'])
+    publications_dependents = _get_object_dependents(publishers_dependents_graph, library_tables_oids['Publications'])
+    items_dependents = _get_object_dependents(publishers_dependents_graph, library_tables_oids['Items'])
+    checkouts_dependents = _get_object_dependents(publishers_dependents_graph, library_tables_oids['Checkouts'])
+
+    assert len(publishers_dependents) == 3
+    assert len(publications_dependents) == 5
+    assert len(items_dependents) == 4
+    assert len(checkouts_dependents) == 3
+    assert all(
+        [
+            r['level'] == 1
+            for r in publishers_dependents
+        ]
+    )
+    assert all(
+        [
+            r['level'] == 2
+            for r in publications_dependents
+        ]
+    )
+    assert all(
+        [
+            r['level'] == 3
+            for r in items_dependents
+        ]
+    )
+    assert all(
+        [
+            r['level'] == 4
+            for r in checkouts_dependents
+        ]
+    )
+
+
+def test_response_format(engine, library_tables_oids):
+    publishers_dependents_graph = get_dependents_graph(library_tables_oids['Publishers'], engine)
+
+    dependent_expected_attrs = ['obj', 'parent_obj', 'level']
+    obj_expected_attrs = ['objid', 'type']
+    assert all(
+        [
+            all(attr in dependent for attr in dependent_expected_attrs)
+            for dependent in publishers_dependents_graph
+        ]
+    )
+    assert all(
+        [
+            all(attr in dependent['obj'] for attr in obj_expected_attrs)
+            for dependent in publishers_dependents_graph
+        ]
+    )
+    assert all(
+        [
+            all(attr in dependent['parent_obj'] for attr in obj_expected_attrs)
+            for dependent in publishers_dependents_graph
+        ]
+    )
+
+
+# TODO: add other types when they are added as dependents
+def test_specific_object_types(engine, library_tables_oids, library_db_tables):
+    items_oid = library_tables_oids['Items']
+    items_dependents_graph = get_dependents_graph(items_oid, engine)
+    items_dependents_oids = _get_object_dependents_oids(items_dependents_graph, items_oid)
+
+    items_constraint_oids = [
+        get_constraint_oid_by_name_and_table_oid(constraint.name, items_oid, engine)
+        for constraint in library_db_tables['Items'].constraints]
+
+    checkouts_oid = library_tables_oids['Checkouts']
+    checkouts_items_fk = [c for c in library_db_tables['Checkouts'].foreign_key_constraints if 'Item' in c][0]
+    checkouts_items_fk_oid = get_constraint_oid_by_name_and_table_oid(checkouts_items_fk.name, checkouts_oid, engine)
+
+    assert sorted(items_dependents_oids) == sorted(items_constraint_oids + [checkouts_oid] + [checkouts_items_fk_oid])
+
+
+# if a table contains a foreign key referencing itself, it shouldn't be treated as a dependent
+def test_self_reference(engine_with_schema, library_tables_oids):
+    engine, schema = engine_with_schema
+
+    publishers_oid = library_tables_oids['Publishers']
+
+    # remove when library_without_checkouts.sql is updated and includes self-reference case
+    fk_column = create_column(engine, publishers_oid, {'name': 'Parent Publisher', 'type': PostgresType.INTEGER.id})
+    pk_column_attnum = get_column_attnum_from_name(publishers_oid, 'id', engine)
+    fk_constraint = ForeignKeyConstraint('Publishers_Publisher_fkey', publishers_oid, [fk_column.column_attnum], publishers_oid, [pk_column_attnum], {})
+    create_constraint(schema, engine, fk_constraint)
+
+    publishers_oid = library_tables_oids['Publishers']
+    publishers_dependents_graph = get_dependents_graph(publishers_oid, engine)
+
+    publishers_dependents_oids = _get_object_dependents_oids(publishers_dependents_graph, publishers_oid)
+    assert publishers_oid not in publishers_dependents_oids
+
+
+# if two tables depend on each other, we should return dependence only for the topmost object in the graph
+# excluding the possibility of circulal reference
+def test_circular_reference(engine_with_schema, library_tables_oids):
+    engine, schema = engine_with_schema
+
+    publishers_oid = library_tables_oids['Publishers']
+    publications_oid = library_tables_oids['Publications']
+
+    # remove when library_without_checkouts.sql is updated and includes circular reference case
+    fk_column = create_column(engine, publishers_oid, {'name': 'Top Publication', 'type': PostgresType.INTEGER.id})
+    publications_pk_column_attnum = get_column_attnum_from_name(publications_oid, 'id', engine)
+    fk_constraint = ForeignKeyConstraint('Publishers_Publications_fkey', publishers_oid, [fk_column.column_attnum], publications_oid, [publications_pk_column_attnum], {})
+    create_constraint(schema, engine, fk_constraint)
+
+    publishers_dependents_graph = get_dependents_graph(publishers_oid, engine)
+    publications_dependents_oids = _get_object_dependents_oids(publishers_dependents_graph, publications_oid)
+
+    assert publishers_oid not in publications_dependents_oids
+
+
+def test_dependents_graph_max_level(engine_with_schema):
+    engine, schema = engine_with_schema
+    metadata = MetaData(schema=schema, bind=engine)
+    t0 = Table(
+        't0', metadata,
+        Column('id', Integer, primary_key=True))
+    t0.create()
+
+    for i in range(11):
+        t = Table(
+            f"t{i+1}", metadata,
+            Column('id', Integer, primary_key=True),
+            Column(f't{i}_id', Integer, ForeignKey(f"t{i}.id")))
+        t.create()
+
+    t0_oid = get_oid_from_table(t0.name, schema, engine)
+    t0_dependents_graph = get_dependents_graph(t0_oid, engine)
+
+    tables_count = len(metadata.tables.keys())
+    assert tables_count == 12
+
+    # by default, dependents graph max level is 10
+    dependents_by_level = sorted(t0_dependents_graph, key=lambda x: x['level'])
+    assert dependents_by_level[0]['level'] == 1
+    assert dependents_by_level[-1]['level'] == 10
diff --git a/db/tests/tables/test_dependents.py b/db/tests/tables/test_dependents.py
deleted file mode 100644
--- a/db/tests/tables/test_dependents.py
+++ /dev/null
@@ -1,170 +0,0 @@
-from sqlalchemy import Column, ForeignKey, Integer, MetaData, PrimaryKeyConstraint, Table
-from db.dependents.dependents_utils import get_dependents_graph
-from db.tables.operations.select import get_oid_from_table
-from db.constraints.operations.select import get_constraint_oid_by_name_and_table_oid
-
-
-def test_table_dependents(engine_with_schema):
-    engine, schema = engine_with_schema
-    metadata = MetaData(schema=schema, bind=engine)
-    t1 = Table(
-        't1', metadata,
-        Column('id', Integer, primary_key=True)
-    )
-    t1.create()
-    t2 = Table(
-        't2', metadata,
-        Column('id', Integer, primary_key=True),
-        Column('t1_id', Integer, ForeignKey(t1.c.id))
-    )
-    t2.create()
-
-    t1_oid = get_oid_from_table(t1.name, schema, engine)
-    t2_oid = get_oid_from_table(t2.name, schema, engine)
-    t1_dependents_graph = get_dependents_graph(t1_oid, engine)
-
-    t1_dependents = list(filter(lambda x: x['parent_obj']['objid'] == t1_oid, t1_dependents_graph))
-    t2_dependents = list(filter(lambda x: x['parent_obj']['objid'] == t2_oid, t1_dependents_graph))
-
-    assert len(t1_dependents_graph) == 5
-    assert len(t1_dependents) == 3
-    assert len(t2_dependents) == 2
-    assert all(
-        [
-            r['level'] == 1
-            for r in t1_dependents
-        ]
-    )
-    assert all(
-        [
-            r['level'] == 2
-            for r in t2_dependents
-        ]
-    )
-
-
-def test_response_format(engine_with_schema):
-    engine, schema = engine_with_schema
-    metadata = MetaData(schema=schema, bind=engine)
-    t1 = Table(
-        't1', metadata,
-        Column('id', Integer, primary_key=True)
-    )
-    t1.create()
-    t2 = Table(
-        't2', metadata,
-        Column('id', Integer, primary_key=True),
-        Column('t1_id', Integer, ForeignKey(t1.c.id))
-    )
-    t2.create()
-
-    t1_oid = get_oid_from_table(t1.name, schema, engine)
-    t1_dependents_graph = get_dependents_graph(t1_oid, engine)
-
-    dependent_expected_attrs = ['obj', 'parent_obj', 'level']
-    obj_expected_attrs = ['objid', 'type']
-    parent_expected_attrs = ['objid']  # TODO: add type when it's returned for the parent obj
-    assert all(
-        [
-            all(attr in dependent for attr in dependent_expected_attrs)
-            for dependent in t1_dependents_graph
-        ]
-    )
-    assert all(
-        [
-            all(attr in dependent['obj'] for attr in obj_expected_attrs)
-            for dependent in t1_dependents_graph
-        ]
-    )
-    assert all(
-        [
-            all(attr in dependent['parent_obj'] for attr in parent_expected_attrs)
-            for dependent in t1_dependents_graph
-        ]
-    )
-
-
-def test_graph_max_level(engine_with_schema):
-    engine, schema = engine_with_schema
-    metadata = MetaData(schema=schema, bind=engine)
-    t0 = Table(
-        't0', metadata,
-        Column('id', Integer, primary_key=True))
-    t0.create()
-
-    for i in range(10):
-        t = Table(
-            f"t{i+1}", metadata,
-            Column('id', Integer, primary_key=True),
-            Column(f't{i}_id', Integer, ForeignKey(f"t{i}.id")))
-        t.create()
-
-    t0_oid = get_oid_from_table(t0.name, schema, engine)
-    dependents = get_dependents_graph(t0_oid, engine)
-
-    tables_count = len(metadata.tables.keys())
-    assert tables_count == 11
-
-    dependents_by_level = sorted(dependents, key=lambda x: x['level'], reverse=True)
-    assert dependents_by_level[-1]['level'] == 1
-    assert dependents_by_level[0]['level'] == 10
-
-
-def test_specific_object_types(engine_with_schema):
-    engine, schema = engine_with_schema
-    metadata = MetaData(schema=schema, bind=engine)
-    t1_pk_name = "t1_pk_name"
-    t1 = Table(
-        't1', metadata,
-        Column('id', Integer),
-        PrimaryKeyConstraint('id', name=t1_pk_name)
-    )
-    t1.create()
-
-    t2_fk_name = "t2_fk_name"
-    t2 = Table(
-        't2', metadata,
-        Column('id', Integer, primary_key=True),
-        Column('t1_id', Integer, ForeignKey(t1.c.id, name=t2_fk_name)))
-    t2.create()
-
-    t1_oid = get_oid_from_table(t1.name, schema, engine)
-    dependents = get_dependents_graph(t1_oid, engine)
-
-    t1_pk_oid = get_constraint_oid_by_name_and_table_oid(t1_pk_name, t1_oid, engine)
-    t2_oid = get_oid_from_table(t2.name, schema, engine)
-    t2_fk_oid = get_constraint_oid_by_name_and_table_oid(t2_fk_name, t2_oid, engine)
-    t1_dependent_oids = [d['obj']['objid'] for d in list(filter(lambda x: x['parent_obj']['objid'] == t1_oid, dependents))]
-
-    assert t1_pk_oid in t1_dependent_oids
-    assert t2_oid in t1_dependent_oids
-    assert t2_fk_oid in t1_dependent_oids
-
-
-def test_circular_referene(engine_with_schema):
-    engine, schema = engine_with_schema
-    metadata = MetaData(schema=schema, bind=engine)
-    t1 = Table(
-        't1', metadata,
-        Column('id', Integer, primary_key=True)
-    )
-
-    t1.create()
-    t2 = Table(
-        't2', metadata,
-        Column('id', Integer, primary_key=True),
-        Column('t1_id', Integer, ForeignKey(t1.c.id))
-    )
-    t2.create()
-    t1.append_column(Column('t2_id', Integer, ForeignKey(t2.c.id)))
-
-    t1_oid = get_oid_from_table(t1.name, schema, engine)
-    t2_oid = get_oid_from_table(t2.name, schema, engine)
-    t1_dependents_graph = get_dependents_graph(t1_oid, engine)
-
-    t2_dependents = list(filter(lambda x: x['parent_obj']['objid'] == t2_oid, t1_dependents_graph))
-    t2_dependent_oids = [d['obj']['objid'] for d in list(filter(lambda x: x['parent_obj']['objid'] == t1_oid, t2_dependents))]
-
-    dependents_by_level = sorted(t1_dependents_graph, key=lambda x: x['level'], reverse=True)
-    assert dependents_by_level[0]['level'] == 2
-    assert t1_oid not in t2_dependent_oids
diff --git a/mathesar/tests/api/test_dependents_api.py b/mathesar/tests/api/test_dependents_api.py
--- a/mathesar/tests/api/test_dependents_api.py
+++ b/mathesar/tests/api/test_dependents_api.py
@@ -1,25 +1,30 @@
-from db.constraints.base import UniqueConstraint
+import pytest
 
 
-def test_dependents_response_attrs(two_foreign_key_tables, client):
-    referrer_table, referent_table = two_foreign_key_tables
-    referent_column = referent_table.get_columns_by_name(["Id"])[0]
-    referrer_column = referrer_table.get_columns_by_name(["Center"])[0]
-    referent_table.add_constraint(
-        UniqueConstraint(None, referent_table.oid, [referent_column.attnum])
-    )
-    data = {
-        'type': 'foreignkey',
-        'columns': [referrer_column.id],
-        'referent_columns': [referent_column.id]
+def _get_object_dependent_ids(dependents, object_id):
+    return [int(d['obj']['id']) for d in dependents if int(d['parent_obj']['id']) == object_id]
+
+
+def _get_constraint_ids(table_constraint_results):
+    return [r['id'] for r in table_constraint_results]
+
+
+@pytest.fixture
+def library_ma_tables(db_table_to_dj_table, library_db_tables):
+    return {
+        table_name: db_table_to_dj_table(db_table)
+        for table_name, db_table
+        in library_db_tables.items()
     }
-    client.post(f'/api/db/v0/tables/{referrer_table.id}/constraints/', data)
 
-    response = client.get(f'/api/db/v0/tables/{referent_table.id}/dependents/')
+
+def test_dependents_response_attrs(library_ma_tables, client):
+    items_id = library_ma_tables["Items"].id
+    response = client.get(f'/api/db/v0/tables/{items_id}/dependents/')
     response_data = response.json()
 
     dependent_expected_attrs = ['obj', 'parent_obj']
-    assert len(response_data) == 6
+    assert len(response_data) == 7
     assert all(
         [
             all(attr in dependent for attr in dependent_expected_attrs)
@@ -34,34 +39,17 @@ def test_dependents_response_attrs(two_foreign_key_tables, client):
     )
 
 
-def test_dependents_response(two_foreign_key_tables, client):
-    referrer_table, referent_table = two_foreign_key_tables
-    referent_column = referent_table.get_columns_by_name(["Id"])[0]
-    referrer_column = referrer_table.get_columns_by_name(["Center"])[0]
-    referent_table.add_constraint(
-        UniqueConstraint(None, referent_table.oid, [referent_column.attnum])
-    )
-    data = {
-        'type': 'foreignkey',
-        'columns': [referrer_column.id],
-        'referent_columns': [referent_column.id]
-    }
-    client.post(f'/api/db/v0/tables/{referrer_table.id}/constraints/', data)
-
-    response = client.get(f'/api/db/v0/tables/{referent_table.id}/dependents/')
-    response_data = response.json()
-
-    referrer_table_constraints_results = client.get(f'/api/db/v0/tables/{referrer_table.id}/constraints/').json()['results']
-    referent_table_constraints_results = client.get(f'/api/db/v0/tables/{referent_table.id}/constraints/').json()['results']
+def test_dependents_response(library_ma_tables, client):
+    items_id = library_ma_tables["Items"].id
+    checkouts_id = library_ma_tables["Checkouts"].id
 
-    referent_constraint_ids = [r['id'] for r in referent_table_constraints_results]
-    referer_constraint_ids = [r['id'] for r in referrer_table_constraints_results]
-    referer_foreign_key_constraint_id = [r['id'] for r in referrer_table_constraints_results if r['type'] == 'foreignkey']
+    items_dependents = client.get(f'/api/db/v0/tables/{items_id}/dependents/').json()
+    items_dependent_ids = _get_object_dependent_ids(items_dependents, items_id)
 
-    referent_expected_dependent_ids = referent_constraint_ids + referer_foreign_key_constraint_id + [referrer_table.id]
-    referent_actual_dependents_ids = [int(d['obj']['id']) for d in response_data if int(d['parent_obj']['id']) == referent_table.id]
+    items_constraints = client.get(f'/api/db/v0/tables/{items_id}/constraints/').json()['results']
+    checkouts_constraints = client.get(f'/api/db/v0/tables/{checkouts_id}/constraints/').json()['results']
 
-    referrer_dependents_ids = [int(d['obj']['id']) for d in response_data if int(d['parent_obj']['id']) == referrer_table.id]
+    items_constraints_ids = _get_constraint_ids(items_constraints)
+    checkouts_items_fkey_id = [c['id'] for c in checkouts_constraints if "Item" in c['name']]
 
-    assert sorted(referent_expected_dependent_ids) == sorted(referent_actual_dependents_ids)
-    assert sorted(referer_constraint_ids) == sorted(referrer_dependents_ids)
+    assert sorted(items_dependent_ids) == sorted(items_constraints_ids + [checkouts_id] + checkouts_items_fkey_id)