Repository: mathesar-foundation/mathesar

Issue:
Remove 'table from fk object' type of dependents from the graph
## Problem
At the beginning of work on the dependents graph, one of the CTEs in the query returned tables as dependents that were implied out of foreign keys. Later, when the purpose of the project cleared out, it became clear these tables are misleading cause they won't be deleted when deleting a referenced object.

## Proposed solution
Remove this CTE and adjust tests to a new result of the query.

## Additional context
It may be reapplied later if proved necessary.


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
@@ -59,8 +59,7 @@ def get_dependents_graph(referenced_object_id, engine, exclude_types, attnum=Non
 
     recursive = recursive.join(anchor, dependency_pairs_cte.c.refobjid == anchor.c.objid)
 
-    recursive_stmt = anchor.union(recursive)
-    stmt = select(recursive_stmt)
+    stmt = select(anchor.union(recursive))
 
     with warnings.catch_warnings():
         warnings.filterwarnings("ignore", message="SELECT statement has a cartesian product")
@@ -70,36 +69,7 @@ def get_dependents_graph(referenced_object_id, engine, exclude_types, attnum=Non
     return _get_structured_result(result)
 
 
-# finding table dependents based on foreign key constraints from the referenced tables
-def _get_table_dependents_from_fk(foreign_key_dependents, pg_constraint_table):
-    pg_identify_object = _get_pg_identify_object_lateral_stmt(
-        text(f'{PG_CLASS_CATALOGUE_NAME}::regclass::oid'), pg_constraint_table.c.conrelid, DEFAULT_NON_COLUMN_OBJSUBID)
-
-    # conrelid in this case is the oid of the table which a constraint resides in
-    return (
-        select(
-            foreign_key_dependents.c.classid,
-            pg_constraint_table.c.conrelid.label('objid'),
-            foreign_key_dependents.c.objsubid,
-            foreign_key_dependents.c.refclassid,
-            foreign_key_dependents.c.refobjid,
-            foreign_key_dependents.c.refobjsubid,
-            foreign_key_dependents.c.deptype,
-            pg_identify_object.c.name.label('objname'),
-            pg_identify_object.c.type.label('objtype')
-        )
-        .select_from(foreign_key_dependents)
-        .join(pg_constraint_table, pg_constraint_table.c.oid == foreign_key_dependents.c.objid)
-        .join(pg_identify_object, true())
-        .group_by(
-            foreign_key_dependents,
-            pg_constraint_table.c.conrelid,
-            pg_identify_object.c.name,
-            pg_identify_object.c.type)
-    )
-
-
-def _get_foreign_key_constraint_dependents(pg_identify_object, dependency_pairs):
+def _get_constraint_dependents(pg_identify_object, dependency_pairs):
     return dependency_pairs.where(pg_identify_object.c.type == 'table constraint')
 
 
@@ -164,10 +134,6 @@ def _get_pg_depend_table(engine, metadata):
     return get_pg_catalog_table("pg_depend", engine, metadata=metadata)
 
 
-def _get_pg_constraint_table(engine, metadata):
-    return get_pg_catalog_table("pg_constraint", engine, metadata=metadata)
-
-
 def _get_pg_rewrite(engine, metadata):
     return get_pg_catalog_table("pg_rewrite", engine, metadata=metadata)
 
@@ -192,36 +158,32 @@ def _get_typed_dependency_pairs_stmt(engine, exclude_types):
     pg_depend = _get_pg_depend_table(engine, metadata)
     pg_identify_object = _get_pg_identify_object_lateral_stmt(
         pg_depend.c.classid, pg_depend.c.objid, pg_depend.c.objsubid)
-    pg_constraint = _get_pg_constraint_table(engine, metadata)
     pg_rewrite = _get_pg_rewrite(engine, metadata)
 
     type_dependents = {}
     # each statement filters the base statement extracting dependents of a specific type
     # so it's easy to exclude particular types or add new
     dependency_pairs = _get_dependency_pairs_stmt(pg_depend, pg_identify_object)
-    foreign_key_constraint_dependents = _get_foreign_key_constraint_dependents(pg_identify_object, dependency_pairs).cte('foreign_key_constraint_dependents')
-    type_dependents['table constraint'] = [foreign_key_constraint_dependents]
+    constraint_dependents = _get_constraint_dependents(pg_identify_object, dependency_pairs).cte('constraint_dependents')
+    type_dependents['table constraint'] = constraint_dependents
 
-    table_from_fk_dependents = _get_table_dependents_from_fk(foreign_key_constraint_dependents, pg_constraint).cte('table_from_fk_dependents')
     table_dependents = _get_table_dependents(pg_identify_object, dependency_pairs).cte('table_dependents')
-    type_dependents['table'] = [table_from_fk_dependents, table_dependents]
+    type_dependents['table'] = table_dependents
 
     # should not be returned directly, used for getting views
     # this relation is required because views in PostgreSQL are implemented using the rule system
     # views don't depend on tables directly but through rules, that are mapped one-to-one
     rule_dependents = _get_rule_dependents(pg_identify_object, dependency_pairs).cte('rule_dependents')
     view_dependents = _get_view_dependents(pg_identify_object, pg_rewrite, rule_dependents).cte('view_dependents')
-    type_dependents['view'] = [view_dependents]
+    type_dependents['view'] = view_dependents
 
     index_dependents = _get_index_dependents(pg_identify_object, dependency_pairs).cte('index_dependents')
-    type_dependents['index'] = [index_dependents]
+    type_dependents['index'] = index_dependents
 
     dependent_selects = [
         select(dependent)
-        for type, dependents in type_dependents.items()
-        if type not in exclude_types
-        for dependent in dependents
-    ]
+        for type, dependent in type_dependents.items()
+        if type not in exclude_types]
 
     return union(*dependent_selects)
 


Test patch:
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,9 +1,8 @@
 import pytest
-from sqlalchemy import Column, ForeignKey, Integer, MetaData, Table, select, Index
+from sqlalchemy import MetaData, select, Index
 from sqlalchemy_utils import create_view
 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
@@ -28,38 +27,14 @@ 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 len(publishers_dependents) == 2
     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):
@@ -87,8 +62,7 @@ def test_response_format(engine, library_tables_oids):
     )
 
 
-# TODO: add other types when they are added as dependents
-def test_specific_object_types(engine, library_tables_oids, library_db_tables):
+def test_constrains_as_dependents(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)
@@ -97,11 +71,11 @@ def test_specific_object_types(engine, library_tables_oids, library_db_tables):
         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)
+    checkouts_items_fk_oid = get_constraint_oid_by_name_and_table_oid(
+        'Checkouts_Item id_fkey', library_tables_oids['Checkouts'], engine
+    )
 
-    assert sorted(items_dependents_oids) == sorted(items_constraint_oids + [checkouts_oid] + [checkouts_items_fk_oid])
+    assert sorted(items_dependents_oids) == sorted(items_constraint_oids + [checkouts_items_fk_oid])
 
 
 # if a table contains a foreign key referencing itself, it shouldn't be treated as a dependent
@@ -143,29 +117,20 @@ def test_circular_reference(engine_with_schema, library_tables_oids):
     assert publishers_oid not in publications_dependents_oids
 
 
-def test_dependents_graph_max_level(engine_with_schema):
+def test_dependents_graph_max_level(engine_with_schema, library_db_tables, library_tables_oids):
     engine, schema = engine_with_schema
     metadata = MetaData(schema=schema, bind=engine)
-    t0 = Table(
-        't0', metadata,
-        Column('id', Integer, primary_key=True))
-    t0.create()
+    source = library_db_tables['Checkouts'].c.id
 
-    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, [])
+    for i in range(15):
+        view_name = str(i)
+        source = create_view(view_name, select(source), metadata)
+    metadata.create_all(engine)
 
-    tables_count = len(metadata.tables.keys())
-    assert tables_count == 12
+    checkouts_dependents_graph = get_dependents_graph(library_tables_oids['Checkouts'], engine, [])
 
     # by default, dependents graph max level is 10
-    dependents_by_level = sorted(t0_dependents_graph, key=lambda x: x['level'])
+    dependents_by_level = sorted(checkouts_dependents_graph, key=lambda x: x['level'])
     assert dependents_by_level[0]['level'] == 1
     assert dependents_by_level[-1]['level'] == 10
 
@@ -174,13 +139,13 @@ def test_column_dependents(engine, library_tables_oids):
     publications_oid = library_tables_oids['Publications']
     items_oid = library_tables_oids['Items']
     publications_id_column_attnum = get_column_attnum_from_name(publications_oid, 'id', engine, metadata=get_empty_metadata())
-    publishers_dependents_graph = get_dependents_graph(publications_oid, engine, [], publications_id_column_attnum)
+    publications_id_column_dependents_graph = get_dependents_graph(publications_oid, engine, [], publications_id_column_attnum)
 
     publications_pk_oid = get_constraint_oid_by_name_and_table_oid('Publications_pkey', publications_oid, engine)
     items_publications_fk_oid = get_constraint_oid_by_name_and_table_oid('Items_Publications_id_fkey', items_oid, engine)
 
-    publications_dependents = _get_object_dependents(publishers_dependents_graph, publications_oid)
-    publications_dependent_oids = _get_object_dependents_oids(publishers_dependents_graph, publications_oid)
+    publications_dependents = _get_object_dependents(publications_id_column_dependents_graph, publications_oid)
+    publications_dependent_oids = _get_object_dependents_oids(publications_id_column_dependents_graph, publications_oid)
     assert all(
         [
             r['parent_obj']['objsubid'] == 1
@@ -190,7 +155,7 @@ def test_column_dependents(engine, library_tables_oids):
     assert all(
         [
             oid in publications_dependent_oids
-            for oid in [publications_pk_oid, items_oid, items_publications_fk_oid]
+            for oid in [publications_pk_oid, items_publications_fk_oid]
         ]
     )
 
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
@@ -16,7 +16,7 @@ def test_dependents_response_attrs(library_ma_tables, client):
     response_data = response.json()
 
     dependent_expected_attrs = ['obj', 'parent_obj']
-    assert len(response_data) == 7
+    assert len(response_data) == 3
     assert all(
         [
             all(attr in dependent for attr in dependent_expected_attrs)
@@ -44,7 +44,7 @@ def test_dependents_response(library_ma_tables, client):
     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(items_dependent_ids) == sorted(items_constraints_ids + [checkouts_id] + checkouts_items_fkey_id)
+    assert sorted(items_dependent_ids) == sorted(items_constraints_ids + checkouts_items_fkey_id)
 
 
 def test_schema_dependents(library_ma_tables, client):
@@ -73,7 +73,7 @@ def test_column_dependents(library_ma_tables, client):
     patrons_pk_id = [c['id'] for c in patrons_constraints if c['name'] == 'Patrons_pkey']
     checkouts_patrons_fk_id = [c['id'] for c in checkouts_constraints if c['name'] == 'Checkouts_Patron id_fkey']
 
-    assert sorted(patrons_id_dependents_ids) == sorted([checkouts.id] + patrons_pk_id + checkouts_patrons_fk_id)
+    assert sorted(patrons_id_dependents_ids) == sorted(patrons_pk_id + checkouts_patrons_fk_id)
 
 
 def test_dependents_filters(library_ma_tables, client):