diff --git a/db/sql/10_msar_joinable_tables.sql b/db/sql/10_msar_joinable_tables.sql index eab50b25ec..14902fbf05 100644 --- a/db/sql/10_msar_joinable_tables.sql +++ b/db/sql/10_msar_joinable_tables.sql @@ -31,8 +31,8 @@ whether to travel from referrer to referant (when False) or from referant to ref DROP TYPE IF EXISTS msar.joinable_tables CASCADE; CREATE TYPE msar.joinable_tables AS ( - base integer, -- The OID of the table from which the paths start - target integer, -- The OID of the table where the paths end + base bigint, -- The OID of the table from which the paths start + target bigint, -- The OID of the table where the paths end join_path jsonb, -- A JSONB array of arrays of arrays fkey_path jsonb, depth integer, @@ -40,6 +40,7 @@ CREATE TYPE msar.joinable_tables AS ( ); +DROP FUNCTION IF EXISTS msar.get_joinable_tables(integer); CREATE OR REPLACE FUNCTION msar.get_joinable_tables(max_depth integer) RETURNS SETOF msar.joinable_tables AS $$/* This function returns a table of msar.joinable_tables objects, giving paths to various @@ -128,11 +129,36 @@ UNION ALL FROM search_fkey_graph ) SELECT * FROM output_cte; -$$ LANGUAGE sql; +$$ LANGUAGE SQL STABLE; +DROP FUNCTION IF EXISTS msar.get_joinable_tables(integer, oid); CREATE OR REPLACE FUNCTION msar.get_joinable_tables(max_depth integer, table_id oid) RETURNS - SETOF msar.joinable_tables AS $$ - SELECT * FROM msar.get_joinable_tables(max_depth) WHERE base=table_id -$$ LANGUAGE sql; +jsonb AS $$ + WITH jt_cte AS ( + SELECT * FROM msar.get_joinable_tables(max_depth) WHERE base=table_id + ), target_cte AS ( + SELECT pga.attrelid AS tt_oid, + jsonb_build_object( + 'name', msar.get_relation_name(pga.attrelid), + 'columns', jsonb_object_agg( + pga.attnum, jsonb_build_object( + 'name', pga.attname, + 'type', CASE WHEN attndims>0 THEN '_array' ELSE atttypid::regtype::text END + ) + ) + ) AS tt_info + FROM pg_catalog.pg_attribute AS pga, jt_cte + WHERE pga.attrelid=jt_cte.target AND pga.attnum > 0 and NOT pga.attisdropped + GROUP BY pga.attrelid + ), joinable_tables AS ( + SELECT jsonb_agg(to_jsonb(jt_cte.*)) AS jt FROM jt_cte + ), target_table_info AS ( + SELECT jsonb_object_agg(tt_oid, tt_info) AS tt FROM target_cte + ) + SELECT jsonb_build_object( + 'joinable_tables', joinable_tables.jt, + 'target_table_info', target_table_info.tt + ) FROM joinable_tables, target_table_info; +$$ LANGUAGE SQL STABLE RETURNS NULL ON NULL INPUT; diff --git a/db/tables/operations/select.py b/db/tables/operations/select.py index e0d4b7365e..a25fca1d43 100644 --- a/db/tables/operations/select.py +++ b/db/tables/operations/select.py @@ -3,7 +3,7 @@ ) from sqlalchemy.dialects.postgresql import JSONB -from db.connection import exec_msar_func, select_from_msar_func +from db.connection import exec_msar_func from db.utils import execute_statement, get_pg_catalog_table BASE = 'base' @@ -60,7 +60,7 @@ def get_table_info(schema, conn): def list_joinable_tables(table_oid, conn, max_depth): - return select_from_msar_func(conn, 'get_joinable_tables', max_depth, table_oid) + return exec_msar_func(conn, 'get_joinable_tables', max_depth, table_oid).fetchone()[0] def reflect_table(name, schema, engine, metadata, connection_to_use=None, keep_existing=False): diff --git a/docs/docs/api/rpc.md b/docs/docs/api/rpc.md index 36b2c9aee4..6d9d8e013e 100644 --- a/docs/docs/api/rpc.md +++ b/docs/docs/api/rpc.md @@ -113,6 +113,7 @@ To use an RPC function: - list_joinable - TableInfo - SettableTableInfo + - JoinableTableRecord - JoinableTableInfo ## Table Metadata diff --git a/mathesar/rpc/tables/base.py b/mathesar/rpc/tables/base.py index 8b076a94e5..9ee9b4e73e 100644 --- a/mathesar/rpc/tables/base.py +++ b/mathesar/rpc/tables/base.py @@ -56,9 +56,9 @@ class SettableTableInfo(TypedDict): columns: Optional[list[SettableColumnInfo]] -class JoinableTableInfo(TypedDict): +class JoinableTableRecord(TypedDict): """ - Information about a joinable table. + Information about a singular joinable table. Attributes: base: The OID of the table from which the paths start @@ -102,6 +102,25 @@ def from_dict(cls, joinables): ) +class JoinableTableInfo(TypedDict): + """ + Information about joinable table(s). + + Attributes: + joinable_tables: List of reachable joinable table(s) from a base table. + target_table_info: Additional info about target table(s) and its column(s). + """ + joinable_tables: list[JoinableTableRecord] + target_table_info: list + + @classmethod + def from_dict(cls, joinable_dict): + return cls( + joinable_tables=[JoinableTableRecord.from_dict(j) for j in joinable_dict["joinable_tables"]], + target_table_info=joinable_dict["target_table_info"] + ) + + @rpc_method(name="tables.list") @http_basic_auth_login_required @handle_rpc_exceptions @@ -290,7 +309,7 @@ def list_joinable( database_id: int, max_depth: int = 3, **kwargs -) -> list[JoinableTableInfo]: +) -> JoinableTableInfo: """ List details for joinable tables. @@ -304,8 +323,8 @@ def list_joinable( """ user = kwargs.get(REQUEST_KEY).user with connect(database_id, user) as conn: - joinables = list_joinable_tables(table_oid, conn, max_depth) - return [JoinableTableInfo.from_dict(joinable) for joinable in joinables] + joinable_dict = list_joinable_tables(table_oid, conn, max_depth) + return JoinableTableInfo.from_dict(joinable_dict) @rpc_method(name="tables.list_with_metadata") diff --git a/mathesar/tests/rpc/tables/test_t_base.py b/mathesar/tests/rpc/tables/test_t_base.py index 3190502f9b..714c1e4af3 100644 --- a/mathesar/tests/rpc/tables/test_t_base.py +++ b/mathesar/tests/rpc/tables/test_t_base.py @@ -288,107 +288,83 @@ def mock_connect(_database_id, user): def mock_list_joinable_tables(_table_oid, conn, max_depth): if _table_oid != table_oid: raise AssertionError('incorrect parameters passed') - return [ + return { + 'joinable_tables': [ + { + 'base': 2254329, + 'depth': 1, + 'target': 2254334, + 'fkey_path': [[2254406, False]], + 'join_path': [[[2254329, 2], [2254334, 1]]], + 'multiple_results': False + }, + { + 'base': 2254329, + 'depth': 1, + 'target': 2254350, + 'fkey_path': [[2254411, False]], + 'join_path': [[[2254329, 3], [2254350, 1]]], + 'multiple_results': False + }], + 'target_table_info': { + '2254334': { + 'name': 'Items', + 'columns': { + '1': {'name': 'id', 'type': 'integer'}, + '2': {'name': 'Barcode', 'type': 'text'}, + '3': {'name': 'Acquisition Date', 'type': 'date'}, + '5': {'name': 'Book', 'type': 'integer'} + } + }, + '2254350': { + 'name': 'Patrons', + 'columns': { + '1': {'name': 'id', 'type': 'integer'}, + '2': {'name': 'First Name', 'type': 'text'}, + '3': {'name': 'Last Name', 'type': 'text'} + } + } + } + } + expected_dict = { + 'joinable_tables': [ { 'base': 2254329, + 'depth': 1, 'target': 2254334, - 'join_path': [[[2254329, 2], [2254334, 1]]], 'fkey_path': [[2254406, False]], - 'depth': 1, + 'join_path': [[[2254329, 2], [2254334, 1]]], 'multiple_results': False }, { 'base': 2254329, + 'depth': 1, 'target': 2254350, - 'join_path': [[[2254329, 3], [2254350, 1]]], 'fkey_path': [[2254411, False]], - 'depth': 1, - 'multiple_results': False - }, - { - 'base': 2254329, - 'target': 2254321, - 'join_path': [[[2254329, 2], [2254334, 1]], [[2254334, 5], [2254321, 1]]], - 'fkey_path': [[2254406, False], [2254399, False]], - 'depth': 2, - 'multiple_results': False - }, - { - 'base': 2254329, - 'target': 2254358, - 'join_path': [ - [[2254329, 2], [2254334, 1]], - [[2254334, 5], [2254321, 1]], - [[2254321, 11], [2254358, 1]] - ], - 'fkey_path': [[2254406, False], [2254399, False], [2254394, False]], - 'depth': 3, + 'join_path': [[[2254329, 3], [2254350, 1]]], 'multiple_results': False + }], + 'target_table_info': { + '2254334': { + 'name': 'Items', + 'columns': { + '1': {'name': 'id', 'type': 'integer'}, + '2': {'name': 'Barcode', 'type': 'text'}, + '3': {'name': 'Acquisition Date', 'type': 'date'}, + '5': {'name': 'Book', 'type': 'integer'} + } }, - { - 'base': 2254329, - 'target': 2254313, - 'join_path': [ - [[2254329, 2], [2254334, 1]], - [[2254334, 5], [2254321, 1]], - [[2254321, 10], [2254313, 1]] - ], - 'fkey_path': [[2254406, False], [2254399, False], [2254389, False]], - 'depth': 3, - 'multiple_results': False + '2254350': { + 'name': 'Patrons', + 'columns': { + '1': {'name': 'id', 'type': 'integer'}, + '2': {'name': 'First Name', 'type': 'text'}, + '3': {'name': 'Last Name', 'type': 'text'} + } } - ] - expected_list = [ - { - 'base': 2254329, - 'target': 2254334, - 'join_path': [[[2254329, 2], [2254334, 1]]], - 'fkey_path': [[2254406, False]], - 'depth': 1, - 'multiple_results': False - }, - { - 'base': 2254329, - 'target': 2254350, - 'join_path': [[[2254329, 3], [2254350, 1]]], - 'fkey_path': [[2254411, False]], - 'depth': 1, - 'multiple_results': False - }, - { - 'base': 2254329, - 'target': 2254321, - 'join_path': [[[2254329, 2], [2254334, 1]], [[2254334, 5], [2254321, 1]]], - 'fkey_path': [[2254406, False], [2254399, False]], - 'depth': 2, - 'multiple_results': False - }, - { - 'base': 2254329, - 'target': 2254358, - 'join_path': [ - [[2254329, 2], [2254334, 1]], - [[2254334, 5], [2254321, 1]], - [[2254321, 11], [2254358, 1]] - ], - 'fkey_path': [[2254406, False], [2254399, False], [2254394, False]], - 'depth': 3, - 'multiple_results': False - }, - { - 'base': 2254329, - 'target': 2254313, - 'join_path': [ - [[2254329, 2], [2254334, 1]], - [[2254334, 5], [2254321, 1]], - [[2254321, 10], [2254313, 1]] - ], - 'fkey_path': [[2254406, False], [2254399, False], [2254389, False]], - 'depth': 3, - 'multiple_results': False } - ] + } monkeypatch.setattr(tables.base, 'connect', mock_connect) monkeypatch.setattr(tables.base, 'list_joinable_tables', mock_list_joinable_tables) - actual_list = tables.list_joinable(table_oid=2254329, database_id=11, max_depth=3, request=request) - assert expected_list == actual_list + actual_dict = tables.list_joinable(table_oid=2254329, database_id=11, max_depth=1, request=request) + assert expected_dict == actual_dict