Skip to content

Commit

Permalink
Merge pull request #3767 from mathesar-foundation/add_links_rpc_funcs
Browse files Browse the repository at this point in the history
Add link-adding RPC functions
  • Loading branch information
Anish9901 authored Aug 21, 2024
2 parents 45e2ef1 + 7f70db0 commit 7dddf3a
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 36 deletions.
1 change: 1 addition & 0 deletions config/settings/common_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def pipe_delim(pipe_string):
'mathesar.rpc.constraints',
'mathesar.rpc.columns',
'mathesar.rpc.columns.metadata',
'mathesar.rpc.data_modeling',
'mathesar.rpc.database_privileges',
'mathesar.rpc.database_setup',
'mathesar.rpc.databases',
Expand Down
80 changes: 73 additions & 7 deletions db/links/operations/create.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from db.connection import execute_msar_func_with_engine
import json

from db.connection import execute_msar_func_with_engine, exec_msar_func


def create_foreign_key_link(
Expand All @@ -25,14 +27,43 @@ def create_foreign_key_link(
"""
return execute_msar_func_with_engine(
engine,
'create_many_to_one_link',
referent_table_oid,
referrer_table_oid,
'add_foreign_key_column',
referrer_column_name,
referrer_table_oid,
referent_table_oid,
unique_link
).fetchone()[0]


def add_foreign_key_column(
conn,
column_name,
referrer_table_oid,
referent_table_oid,
unique_link=False
):
"""
Creates a Many-to-One or One-to-One link.
Args:
conn: psycopg3 connection object.
column_name: Name of the new column to be created in the referrer
table.
referrer_table_oid: The OID of the referrer table.
referent_table_oid: The OID of the referent table.
unique_link: Whether to make the link one-to-one
instead of many-to-one.
"""
exec_msar_func(
conn,
'add_foreign_key_column',
column_name,
referrer_table_oid,
referent_table_oid,
unique_link
)


def create_many_to_many_link(engine, schema_oid, map_table_name, referents_dict):
"""
Creates a Many-to-Many link.
Expand All @@ -51,9 +82,44 @@ def create_many_to_many_link(engine, schema_oid, map_table_name, referents_dict)
"""
return execute_msar_func_with_engine(
engine,
'create_many_to_many_link',
'add_mapping_table',
schema_oid,
map_table_name,
referents_dict['referent_table_oids'],
referents_dict['column_names']
json.dumps(
[
{"column_name": c, "referent_table_oid": i}
for c, i in zip(
referents_dict['column_names'],
referents_dict['referent_table_oids'],
)
]
)
).fetchone()[0]


def add_mapping_table(
conn,
schema_oid,
table_name,
mapping_columns,
):
"""
Add a mapping table to give a many-to-many link between referents.
Args:
conn: psycopg3 connection object.
schema_oid: The OID of the schema for the mapping table.
table_name: The name for the new mapping table.
mapping_columns: A list of dictionaries giving the foreign key
columns to create in the mapping table.
The elements of the mapping_columns list must have the form
{"column_name": <str>, "referent_table_oid": <int>}
"""
exec_msar_func(
conn,
'add_mapping_table',
schema_oid,
table_name,
json.dumps(mapping_columns)
)
33 changes: 17 additions & 16 deletions db/sql/00_msar.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3112,19 +3112,19 @@ $$ LANGUAGE SQL;


CREATE OR REPLACE FUNCTION
msar.create_many_to_one_link(
frel_id oid,
rel_id oid,
msar.add_foreign_key_column(
col_name text,
rel_id oid,
frel_id oid,
unique_link boolean DEFAULT false
) RETURNS smallint AS $$/*
Create a many-to-one or a one-to-one link between tables, returning the attnum of the newly created
column, returning the attnum of the added column.
Args:
frel_id: The OID of the referent table, named for confrelid in the pg_attribute table.
rel_id: The OID of the referrer table, named for conrelid in the pg_attribute table.
col_name: Name of the new column to be created in the referrer table, unquoted.
rel_id: The OID of the referrer table, named for conrelid in the pg_attribute table.
frel_id: The OID of the referent table, named for confrelid in the pg_attribute table.
unique_link: Whether to make the link one-to-one instead of many-to-one.
*/
DECLARE
Expand Down Expand Up @@ -3168,27 +3168,28 @@ $$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT;


CREATE OR REPLACE FUNCTION
msar.create_many_to_many_link(
msar.add_mapping_table(
sch_id oid,
tab_name text,
from_rel_ids oid[],
col_names text[]
) RETURNS oid AS $$/*
mapping_columns jsonb
) RETURNS oid AS $$/*
Create a many-to-many link between tables, returning the oid of the newly created table.
Args:
sch_id: The OID of the schema in which new referrer table is to be created.
tab_name: Name of the referrer table to be created.
from_rel_ids: The OIDs of the referent tables.
col_names: Names of the new column to be created in the referrer table, unqoted.
mapping_columns: An array of objects giving the foreign key columns for the new table.
The elements of the mapping_columns array must have the form
{"column_name": <str>, "referent_table_oid": <int>}
*/
DECLARE
added_table_id oid;
BEGIN
added_table_id := msar.add_mathesar_table(sch_id, tab_name , NULL, NULL, NULL);
PERFORM msar.create_many_to_one_link(a.rel_id, added_table_id, b.col_name)
FROM unnest(from_rel_ids) WITH ORDINALITY AS a(rel_id, idx)
JOIN unnest(col_names) WITH ORDINALITY AS b(col_name, idx) USING (idx);
added_table_id := msar.add_mathesar_table(sch_id, tab_name, NULL, NULL, NULL);
PERFORM msar.add_foreign_key_column(column_name, added_table_id, referent_table_oid)
FROM jsonb_to_recordset(mapping_columns) AS x(column_name text, referent_table_oid oid);
RETURN added_table_id;
END;
$$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT;
Expand Down Expand Up @@ -3236,7 +3237,7 @@ BEGIN
format('Extracted from %s', __msar.get_qualified_relation_name(tab_id))
);
-- Create a new fkey column and foreign key linking the original table to the extracted one.
fkey_attnum := msar.create_many_to_one_link(extracted_table_id, tab_id, fkey_name);
fkey_attnum := msar.add_foreign_key_column(fkey_name, tab_id, extracted_table_id);
-- Insert the data from the original table's columns into the extracted columns, and add
-- appropriate fkey values to the new fkey column in the original table to give the proper
-- mapping.
Expand Down
16 changes: 9 additions & 7 deletions db/sql/test_00_msar.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1129,10 +1129,10 @@ END;
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION test_create_many_to_one_link() RETURNS SETOF TEXT AS $$
CREATE OR REPLACE FUNCTION test_add_foreign_key_column() RETURNS SETOF TEXT AS $$
BEGIN
PERFORM __setup_link_tables();
PERFORM msar.create_many_to_one_link(
PERFORM msar.add_foreign_key_column(
frel_id => 'actors'::regclass::oid,
rel_id => 'movies'::regclass::oid,
col_name => 'act_id'
Expand All @@ -1147,7 +1147,7 @@ $$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION test_create_one_to_one_link() RETURNS SETOF TEXT AS $$
BEGIN
PERFORM __setup_link_tables();
PERFORM msar.create_many_to_one_link(
PERFORM msar.add_foreign_key_column(
frel_id => 'actors'::regclass::oid,
rel_id => 'movies'::regclass::oid,
col_name => 'act_id',
Expand All @@ -1161,14 +1161,16 @@ END;
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION test_create_many_to_many_link() RETURNS SETOF TEXT AS $$
CREATE OR REPLACE FUNCTION test_add_mapping_table() RETURNS SETOF TEXT AS $$
BEGIN
PERFORM __setup_link_tables();
PERFORM msar.create_many_to_many_link(
PERFORM msar.add_mapping_table(
sch_id => 'public'::regnamespace::oid,
tab_name => 'movies_actors',
from_rel_ids => '{}'::oid[] || 'movies'::regclass::oid || 'actors'::regclass::oid,
col_names => '{"movie_id", "actor_id"}'::text[]
mapping_columns => jsonb_build_array(
jsonb_build_object('column_name', 'movie_id', 'referent_table_oid', 'movies'::regclass::oid),
jsonb_build_object('column_name', 'actor_id', 'referent_table_oid', 'actors'::regclass::oid)
)
);
RETURN NEXT has_table('public'::name, 'movies_actors'::name);
RETURN NEXT has_column('movies_actors', 'movie_id');
Expand Down
15 changes: 9 additions & 6 deletions db/tests/links/operations/test_create.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import pytest
from unittest.mock import patch
import db.links.operations.create as link_create
Expand All @@ -18,10 +19,10 @@ def test_create_foreign_key_link(engine_with_schema, unique_link):
)
call_args = mock_exec.call_args_list[0][0]
assert call_args[0] == engine
assert call_args[1] == "create_many_to_one_link"
assert call_args[2] == 12345
assert call_args[1] == "add_foreign_key_column"
assert call_args[2] == "actor_id"
assert call_args[3] == 54321
assert call_args[4] == "actor_id"
assert call_args[4] == 12345
assert call_args[5] == unique_link or False


Expand All @@ -37,8 +38,10 @@ def test_many_to_many_link(engine_with_schema):
)
call_args = mock_exec.call_args_list[0][0]
assert call_args[0] == engine
assert call_args[1] == "create_many_to_many_link"
assert call_args[1] == "add_mapping_table"
assert call_args[2] == 2200
assert call_args[3] == "movies_actors"
assert call_args[4] == referents['referent_table_oids']
assert call_args[5] == referents['column_names']
assert json.loads(call_args[4]) == [
{"column_name": "movie_id", "referent_table_oid": 12345},
{"column_name": "actor_id", "referent_table_oid": 54321}
]
9 changes: 9 additions & 0 deletions docs/docs/api/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,15 @@ To use an RPC function:
- list_
- ServerInfo

## Data Modeling

:::data_modeling
options:
members:
- add_foreign_key_column
- add_mapping_table
- MappingColumn

## Responses

### Success
Expand Down
82 changes: 82 additions & 0 deletions mathesar/rpc/data_modeling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""
Classes and functions exposed to the RPC endpoint for managing data models.
"""
from typing import TypedDict

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

from db.links.operations import create as links_create
from mathesar.rpc.exceptions.handlers import handle_rpc_exceptions
from mathesar.rpc.utils import connect


@rpc_method(name="data_modeling.add_foreign_key_column")
@http_basic_auth_login_required
@handle_rpc_exceptions
def add_foreign_key_column(
*,
column_name: str,
referrer_table_oid: int,
referent_table_oid: int,
database_id: int,
**kwargs
) -> None:
"""
Add a foreign key column to a table.
The foreign key column will be newly created, and will reference the
`id` column of the referent table.
Args:
column_name: The name of the column to create.
referrer_table_oid: The OID of the table getting the new column.
referent_table_oid: The OID of the table being referenced.
"""
user = kwargs.get(REQUEST_KEY).user
with connect(database_id, user) as conn:
links_create.add_foreign_key_column(
conn, column_name, referrer_table_oid, referent_table_oid
)


class MappingColumn(TypedDict):
"""
An object defining a foreign key column in a mapping table.
Attributes:
column_name: The name of the foreign key column.
referent_table_oid: The OID of the table the column references.
"""
column_name: str
referent_table_oid: int


@rpc_method(name="data_modeling.add_mapping_table")
@http_basic_auth_login_required
@handle_rpc_exceptions
def add_mapping_table(
*,
table_name: str,
mapping_columns: list[MappingColumn],
schema_oid: int,
database_id: int,
**kwargs
) -> None:
"""
Add a mapping table to give a many-to-many link between referents.
The foreign key columns in the mapping table will reference the `id`
column of the referent tables.
Args:
table_name: The name for the new mapping table.
schema_oid: The OID of the schema for the mapping table.
mapping_columns: The foreign key columns to create in the
mapping table.
"""
user = kwargs.get(REQUEST_KEY).user
with connect(database_id, user) as conn:
links_create.add_mapping_table(
conn, schema_oid, table_name, mapping_columns
)
Loading

0 comments on commit 7dddf3a

Please sign in to comment.