Repository: mathesar-foundation/mathesar

Issue:
Implement `tables.list` RPC method



Discussion:

Resolving patch:
diff --git a/config/settings/common_settings.py b/config/settings/common_settings.py
--- a/config/settings/common_settings.py
+++ b/config/settings/common_settings.py
@@ -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 = [
diff --git a/db/tables/operations/select.py b/db/tables/operations/select.py
--- a/db/tables/operations/select.py
+++ b/db/tables/operations/select.py
@@ -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'
@@ -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
diff --git a/mathesar/rpc/tables.py b/mathesar/rpc/tables.py
new file mode 100644
--- /dev/null
+++ b/mathesar/rpc/tables.py
@@ -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
+    ]


Test patch:
diff --git a/db/sql/test_00_msar.sql b/db/sql/test_00_msar.sql
--- a/db/sql/test_00_msar.sql
+++ b/db/sql/test_00_msar.sql
@@ -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;
diff --git a/db/tests/tables/operations/test_select.py b/db/tests/tables/operations/test_select.py
--- a/db/tests/tables/operations/test_select.py
+++ b/db/tests/tables/operations/test_select.py
@@ -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
@@ -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 = {
diff --git a/mathesar/tests/rpc/test_endpoints.py b/mathesar/tests/rpc/test_endpoints.py
--- a/mathesar/tests/rpc/test_endpoints.py
+++ b/mathesar/tests/rpc/test_endpoints.py
@@ -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 = [
     (
@@ -38,6 +39,11 @@
         "schemas.list",
         [user_is_authenticated]
     ),
+    (
+        tables.list_,
+        "tables.list",
+        [user_is_authenticated]
+    ),
 ]
 
 
diff --git a/mathesar/tests/rpc/test_tables.py b/mathesar/tests/rpc/test_tables.py
new file mode 100644
--- /dev/null
+++ b/mathesar/tests/rpc/test_tables.py
@@ -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