Repository: mathesar-foundation/mathesar

Issue:
Add views and indexes to a list of dependents that the main query returns
## Problem
Dependents endpoint should return all DB objects, including those that Mathesar does not support. In this PR, add support to return views and indexes. 

## Proposed solution
- Add functions for getting views and indexes, and utilize it in a form of CTE, combining with other parts of the graph

## Additional context
<!-- Add any other context or screenshots about the feature request here.-->



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
@@ -78,14 +78,60 @@ def _get_table_dependents(foreign_key_dependents, pg_constraint_table):
             pg_identify_refobject.c.type)
 
 
-def _get_foreign_key_constraint_dependents(pg_identify_object, dependency_pair):
-    return dependency_pair.where(pg_identify_object.c.type == 'table constraint')
+def _get_foreign_key_constraint_dependents(pg_identify_object, dependency_pairs):
+    return dependency_pairs.where(pg_identify_object.c.type == 'table constraint')
+
+
+def _get_index_dependents(pg_identify_object, dependency_pairs):
+    return dependency_pairs.where(pg_identify_object.c.type == 'index')
+
+
+def _get_rule_dependents(pg_identify_object, dependency_pairs):
+    return dependency_pairs.where(pg_identify_object.c.type == 'rule')
+
+
+def _get_view_dependents(pg_identify_object, pg_rewrite_table, rule_dependents):
+    pg_identify_object = _get_pg_identify_object_lateral_stmt(
+        text(f'{PG_CLASS_CATALOGUE_NAME}::regclass::oid'), pg_rewrite_table.c.ev_class, 0)
+
+    pg_identify_refobject = _get_pg_identify_object_lateral_stmt(
+        rule_dependents.c.refclassid, rule_dependents.c.refobjid, 0)
+
+    return select(
+        rule_dependents.c.classid,
+        pg_rewrite_table.c.ev_class.label('objid'),
+        rule_dependents.c.objsubid,
+        rule_dependents.c.refclassid,
+        rule_dependents.c.refobjid,
+        rule_dependents.c.refobjsubid,
+        rule_dependents.c.deptype,
+        pg_identify_object.c.name.label('objname'),
+        pg_identify_object.c.type.label('objtype'),
+        pg_identify_refobject.c.name.label('refobjname'),
+        pg_identify_refobject.c.type.label('refobjtype')) \
+        .select_from(rule_dependents) \
+        .join(pg_rewrite_table, rule_dependents.c.objid == pg_rewrite_table.c.oid) \
+        .join(pg_identify_object, true()) \
+        .join(pg_identify_refobject, true()) \
+        .group_by(
+            rule_dependents,
+            pg_rewrite_table.c.ev_class,
+            pg_identify_object.c.type,
+            pg_identify_object.c.name,
+            pg_identify_refobject.c.name,
+            pg_identify_refobject.c.type)
 
 
 # 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,
+        pg_depend.c.classid,
+        pg_depend.c.objid,
+        pg_depend.c.objsubid,
+        pg_depend.c.refclassid,
+        pg_depend.c.refobjid,
+        literal(0).label('refobjsubid'),  # subject to change when implementing columns
+        pg_depend.c.deptype,
         pg_identify_object.c.name.label('objname'),
         pg_identify_object.c.type.label('objtype'),
         pg_identify_refobject.c.name.label('refobjname'),
@@ -100,7 +146,7 @@ def _get_dependency_pairs_stmt(pg_depend, pg_identify_object, pg_identify_refobj
             pg_identify_object.c.name,
             pg_identify_object.c.type,
             pg_identify_refobject.c.name,
-            pg_identify_refobject.c.type)
+            pg_identify_refobject.c.type).distinct()
 
     return result
 
@@ -113,6 +159,10 @@ def _get_pg_constraint_table(engine, metadata):
     return Table("pg_constraint", metadata, autoload_with=engine)
 
 
+def _get_pg_rewrite(engine, metadata):
+    return Table("pg_rewrite", metadata, autoload_with=engine)
+
+
 def _get_pg_identify_object_lateral_stmt(classid, objid, objsubid):
     return select(
         column("name"),
@@ -133,6 +183,7 @@ def _get_typed_dependency_pairs_stmt(engine):
     pg_identify_refobject = _get_pg_identify_object_lateral_stmt(
         pg_depend.c.refclassid, pg_depend.c.refobjid, 0)
     pg_constraint = _get_pg_constraint_table(engine, metadata)
+    pg_rewrite = _get_pg_rewrite(engine, metadata)
 
     # each statement filters the base statement extracting dependents of a specific type
     # so it's easy to exclude particular types or add new
@@ -140,9 +191,19 @@ def _get_typed_dependency_pairs_stmt(engine):
     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')
 
+    # 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')
+
+    index_dependents = _get_index_dependents(pg_identify_object, dependency_pairs).cte('index_dependents')
+
     return union(
         select(foreign_key_constraint_dependents),
-        select(table_dependents))
+        select(table_dependents),
+        select(view_dependents),
+        select(index_dependents))
 
 
 def has_dependencies(referenced_object_id, engine):
@@ -170,8 +231,16 @@ def _get_structured_result(dependency_graph_result):
     for dependency_pair in dependency_graph_result:
         d = {}
         d['level'] = dependency_pair.level
-        d['obj'] = {'objid': dependency_pair.objid, 'type': dependency_pair.objtype}
-        d['parent_obj'] = {'objid': dependency_pair.refobjid, 'type': dependency_pair.refobjtype}
+        d['obj'] = {
+            'objid': dependency_pair.objid,
+            'type': dependency_pair.objtype,
+            'name': dependency_pair.objname
+        }
+        d['parent_obj'] = {
+            'objid': dependency_pair.refobjid,
+            'type': dependency_pair.refobjtype,
+            'name': dependency_pair.refobjname
+        }
         result.append(d)
 
     return result
diff --git a/mathesar/api/serializers/dependents.py b/mathesar/api/serializers/dependents.py
--- a/mathesar/api/serializers/dependents.py
+++ b/mathesar/api/serializers/dependents.py
@@ -28,6 +28,7 @@ def to_representation(self, instance):
 class DependentNonMathesarObjectSerializer(serializers.Serializer):
     objid = serializers.CharField()
     type = serializers.CharField()
+    name = serializers.CharField()
 
 
 class BaseDependentObjectSerializer(


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,4 +1,5 @@
-from sqlalchemy import Column, ForeignKey, Integer, MetaData, Table
+from sqlalchemy import Column, ForeignKey, Integer, MetaData, Table, 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
@@ -17,6 +18,10 @@ def _get_object_dependents_oids(dependents_graph, object_oid):
     return [dependent['obj']['objid'] for dependent in _get_object_dependents(dependents_graph, object_oid)]
 
 
+def _get_object_dependents_by_name(dependents_graph, object_oid, name):
+    return [dependent['obj'] for dependent in _get_object_dependents(dependents_graph, object_oid) if dependent['obj']['name'] == name]
+
+
 def test_correct_dependents_amount_and_level(engine, library_tables_oids):
     publishers_dependents_graph = get_dependents_graph(library_tables_oids['Publishers'], engine)
 
@@ -161,3 +166,31 @@ def test_dependents_graph_max_level(engine_with_schema):
     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
+
+
+def test_views_as_dependents(engine_with_schema, library_db_tables, library_tables_oids):
+    engine, schema = engine_with_schema
+    metadata = MetaData(schema=schema, bind=engine)
+
+    publications = library_db_tables['Publications']
+    new_publications_view = select(publications).where(publications.c['Publication Year'] >= 2000)
+    view_name = 'new_publications'
+    create_view(view_name, new_publications_view, metadata)
+    metadata.create_all(engine)
+
+    publications_oid = library_tables_oids['Publications']
+    publications_dependents_graph = get_dependents_graph(publications_oid, engine)
+    publications_view_dependent = _get_object_dependents_by_name(publications_dependents_graph, publications_oid, view_name)[0]
+
+    assert publications_view_dependent['name'] == view_name
+
+
+def test_indexex_as_dependents(engine, library_db_tables, library_tables_oids):
+    index_name = 'index'
+    index = Index(index_name, library_db_tables['Publishers'].c.id)
+    index.create(engine)
+
+    publishers_dependents_graph = get_dependents_graph(library_tables_oids['Publishers'], engine)
+    publishers_index_dependent = _get_object_dependents_by_name(publishers_dependents_graph, library_tables_oids['Publishers'], index_name)[0]
+
+    assert publishers_index_dependent['name'] == index_name