Skip to content

Commit

Permalink
Merge pull request #3599 from mathesar-foundation/tables_list
Browse files Browse the repository at this point in the history
Implement `tables.list` rpc endpoint
  • Loading branch information
mathemancer authored May 27, 2024
2 parents fdc9353 + b35148f commit bb111fc
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 1 deletion.
3 changes: 2 additions & 1 deletion config/settings/common_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ def pipe_delim(pipe_string):
MODERNRPC_METHODS_MODULES = [
'mathesar.rpc.connections',
'mathesar.rpc.columns',
'mathesar.rpc.schemas'
'mathesar.rpc.schemas',
'mathesar.rpc.tables'
]

TEMPLATES = [
Expand Down
39 changes: 39 additions & 0 deletions db/sql/00_msar.sql
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,17 @@ from Python through a single Python module.
END
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION msar.obj_description(obj_id oid, catalog_name text) RETURNS text AS $$/*
Transparent wrapper for obj_description. Putting it in the `msar` namespace helps route all DB calls
from Python through a single Python module.
*/
BEGIN
RETURN obj_description(obj_id, catalog_name);
END
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION __msar.jsonb_key_exists(data jsonb, key text) RETURNS boolean AS $$/*
Wraps the `?` jsonb operator for improved readability.
*/
Expand Down Expand Up @@ -682,6 +693,34 @@ SELECT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid=tab_id AND attname=col_
$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT;


CREATE OR REPLACE FUNCTION msar.get_table_info(sch_id regnamespace) RETURNS jsonb AS $$/*
Given a schema identifier, return an array of objects describing the tables of the schema.
Each returned JSON object in the array will have the form:
{
"oid": <int>,
"name": <str>,
"schema": <int>,
"description": <str>
}
Args:
sch_id: The OID or name of the schema.
*/
SELECT jsonb_agg(
jsonb_build_object(
'oid', pgc.oid,
'name', pgc.relname,
'schema', pgc.relnamespace,
'description', msar.obj_description(pgc.oid, 'pg_class')
)
)
FROM pg_catalog.pg_class AS pgc
LEFT JOIN pg_catalog.pg_namespace AS pgn ON pgc.relnamespace = pgn.oid
WHERE pgc.relnamespace = sch_id AND pgc.relkind = 'r';
$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT;


CREATE OR REPLACE FUNCTION msar.get_schemas() RETURNS jsonb AS $$/*
Return a json array of objects describing the user-defined schemas in the database.
Expand Down
55 changes: 55 additions & 0 deletions db/sql/test_00_msar.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2570,6 +2570,61 @@ END;
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION __setup_get_table_info() RETURNS SETOF TEXT AS $$
BEGIN
CREATE SCHEMA pi;
-- Two tables with one having description
CREATE TABLE pi.three(id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY);
CREATE TABLE pi.one(id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY);
COMMENT ON TABLE pi.one IS 'first decimal digit of pi';

CREATE SCHEMA alice;
-- No tables in the schema
END;
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION test_get_table_info() RETURNS SETOF TEXT AS $$
DECLARE
pi_table_info jsonb;
alice_table_info jsonb;
BEGIN
PERFORM __setup_get_table_info();
SELECT msar.get_table_info('pi') INTO pi_table_info;
SELECT msar.get_table_info('alice') INTO alice_table_info;

-- Test table info for schema 'pi'
-- Check if all the required keys exist in the json blob
-- Check whether the correct name is returned
-- Check whether the correct description is returned
RETURN NEXT is(
pi_table_info->0 ?& array['oid', 'name', 'schema', 'description'], true
);
RETURN NEXT is(
pi_table_info->0->>'name', 'three'
);
RETURN NEXT is(
pi_table_info->0->>'description', null
);

RETURN NEXT is(
pi_table_info->1 ?& array['oid', 'name', 'schema', 'description'], true
);
RETURN NEXT is(
pi_table_info->1->>'name', 'one'
);
RETURN NEXT is(
pi_table_info->1->>'description', 'first decimal digit of pi'
);

-- Test table info for schema 'alice' that contains no tables
RETURN NEXT is(
alice_table_info, null
);
END;
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION test_get_schemas() RETURNS SETOF TEXT AS $$
DECLARE
initial_schema_count int;
Expand Down
23 changes: 23 additions & 0 deletions db/tables/operations/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
)
from sqlalchemy.dialects.postgresql import JSONB

from db.connection import exec_msar_func
from db.utils import execute_statement, get_pg_catalog_table

BASE = 'base'
Expand All @@ -14,6 +15,28 @@
MULTIPLE_RESULTS = 'multiple_results'


def get_table_info(schema, conn):
"""
Return a list of dictionaries describing the tables of a schema.
The `schema` can be given as either a "qualified name", or an OID.
The OID is the preferred identifier, since it's much more robust.
The returned list contains dictionaries of the following form:
{
"oid": <int>,
"name": <str>,
"schema": <int>,
"description": <str>
}
Args:
schema: The schema for which we want table info.
"""
return exec_msar_func(conn, 'get_table_info', schema).fetchone()[0]


def reflect_table(name, schema, engine, metadata, connection_to_use=None, keep_existing=False):
extend_existing = not keep_existing
autoload_with = engine if connection_to_use is None else connection_to_use
Expand Down
9 changes: 9 additions & 0 deletions db/tests/tables/operations/test_select.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
from unittest.mock import patch
from sqlalchemy import text
from db.columns.operations.select import get_column_name_from_attnum
from db.tables.operations import select as ma_sel
Expand Down Expand Up @@ -34,6 +35,14 @@
MULTIPLE_RESULTS = ma_sel.MULTIPLE_RESULTS


def test_get_table_info():
with patch.object(ma_sel, 'exec_msar_func') as mock_exec:
mock_exec.return_value.fetchone = lambda: ('a', 'b')
result = ma_sel.get_table_info('schema', 'conn')
mock_exec.assert_called_once_with('conn', 'get_table_info', 'schema')
assert result == 'a'


def _transform_row_to_names(row, engine):
metadata = get_empty_metadata()
output_dict = {
Expand Down
8 changes: 8 additions & 0 deletions docs/docs/api/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ To use an RPC function:

---

::: mathesar.rpc.tables
options:
members:
- list_
- TableInfo

---

::: mathesar.rpc.columns
options:
members:
Expand Down
46 changes: 46 additions & 0 deletions mathesar/rpc/tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import Optional, TypedDict

from modernrpc.core import rpc_method, REQUEST_KEY
from modernrpc.auth.basic import http_basic_auth_login_required

from db.tables.operations.select import get_table_info
from mathesar.rpc.exceptions.handlers import handle_rpc_exceptions
from mathesar.rpc.utils import connect


class TableInfo(TypedDict):
"""
Information about a table.
Attributes:
oid: The `oid` of the table in the schema.
name: The name of the table.
schema: The `oid` of the schema where the table lives.
description: The description of the table.
"""
oid: int
name: str
schema: int
description: Optional[str]


@rpc_method(name="tables.list")
@http_basic_auth_login_required
@handle_rpc_exceptions
def list_(*, schema_oid: int, database_id: int, **kwargs) -> list[TableInfo]:
"""
List information about tables for a schema. Exposed as `list`.
Args:
schema_oid: Identity of the schema in the user's database.
database_id: The Django id of the database containing the table.
Returns:
A list of table details.
"""
user = kwargs.get(REQUEST_KEY).user
with connect(database_id, user) as conn:
raw_table_info = get_table_info(schema_oid, conn)
return [
TableInfo(tab) for tab in raw_table_info
]
6 changes: 6 additions & 0 deletions mathesar/tests/rpc/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from mathesar.rpc import columns
from mathesar.rpc import connections
from mathesar.rpc import schemas
from mathesar.rpc import tables

METHODS = [
(
Expand Down Expand Up @@ -38,6 +39,11 @@
"schemas.list",
[user_is_authenticated]
),
(
tables.list_,
"tables.list",
[user_is_authenticated]
),
]


Expand Down
64 changes: 64 additions & 0 deletions mathesar/tests/rpc/test_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
This file tests the table RPC functions.
Fixtures:
rf(pytest-django): Provides mocked `Request` objects.
monkeypatch(pytest): Lets you monkeypatch an object for testing.
"""
from contextlib import contextmanager

from mathesar.rpc import tables
from mathesar.models.users import User


def test_tables_list(rf, monkeypatch):
request = rf.post('/api/rpc/v0', data={})
request.user = User(username='alice', password='pass1234')
schema_oid = 2200
database_id = 11

@contextmanager
def mock_connect(_database_id, user):
if _database_id == database_id and user.username == 'alice':
try:
yield True
finally:
pass
else:
raise AssertionError('incorrect parameters passed')

def mock_table_info(_schema_oid, conn):
if _schema_oid != schema_oid:
raise AssertionError('incorrect parameters passed')
return [
{
'oid': 17408,
'name': 'Authors',
'schema': schema_oid,
'description': 'a description on the authors table.'
},
{
'oid': 17809,
'name': 'Books',
'schema': schema_oid,
'description': None
}
]
monkeypatch.setattr(tables, 'connect', mock_connect)
monkeypatch.setattr(tables, 'get_table_info', mock_table_info)
expect_table_list = [
{
'oid': 17408,
'name': 'Authors',
'schema': schema_oid,
'description': 'a description on the authors table.'
},
{
'oid': 17809,
'name': 'Books',
'schema': schema_oid,
'description': None
}
]
actual_table_list = tables.list_(schema_oid=2200, database_id=11, request=request)
assert actual_table_list == expect_table_list

0 comments on commit bb111fc

Please sign in to comment.