Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement tables.patch RPC endpoint #3618

Merged
merged 9 commits into from
Jun 13, 2024
Merged
48 changes: 41 additions & 7 deletions db/sql/00_msar.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1057,7 +1057,7 @@ Args:
new_tab_name: unquoted, unqualified table name
*/
BEGIN
RETURN __msar.rename_table(__msar.get_relation_name(tab_id), quote_ident(new_tab_name));
RETURN __msar.rename_table(msar.get_relation_name_or_null(tab_id), quote_ident(new_tab_name));
END;
$$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT;

Expand Down Expand Up @@ -1089,8 +1089,12 @@ Args:
tab_name: The qualified, quoted name of the table whose comment we will change.
comment_: The new comment. Any quotes or special characters must be escaped.
*/
SELECT __msar.exec_ddl('COMMENT ON TABLE %s IS %s', tab_name, comment_);
$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT;
DECLARE
comment_or_null text := COALESCE(comment_, 'NULL');
BEGIN
RETURN __msar.exec_ddl('COMMENT ON TABLE %s IS %s', tab_name, comment_or_null);
END;
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION
Expand All @@ -1101,8 +1105,8 @@ Args:
tab_id: The OID of the table whose comment we will change.
comment_: The new comment.
*/
SELECT __msar.comment_on_table(__msar.get_relation_name(tab_id), quote_literal(comment_));
$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT;
SELECT __msar.comment_on_table(msar.get_relation_name_or_null(tab_id), quote_literal(comment_));
$$ LANGUAGE SQL;


CREATE OR REPLACE FUNCTION
Expand All @@ -1118,10 +1122,40 @@ SELECT __msar.comment_on_table(
msar.get_fully_qualified_object_name(sch_name, tab_name),
quote_literal(comment_)
);
$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT;
$$ LANGUAGE SQL;


-- Alter table -------------------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION
msar.alter_table(tab_id oid, tab_alters jsonb) RETURNS text AS $$/*
Alter columns of the given table in bulk, returning the IDs of the columns so altered.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't accurate. It returns the name of the table.


Args:
tab_id: The OID of the table whose columns we'll alter.
tab_alters: a JSONB describing the alterations to make.

The tab_alters should have the form:
{
"name": <str>,
"description": <str>
"columns": <col_alters>,
}
*/
DECLARE
new_tab_name text;
comment text;
col_alters jsonb;
BEGIN
new_tab_name := tab_alters->>'name';
comment := tab_alters->>'description';
col_alters := tab_alters->'columns';
PERFORM msar.rename_table(tab_id, new_tab_name);
PERFORM msar.comment_on_table(tab_id, comment);
PERFORM msar.alter_columns(tab_id, col_alters);
RETURN msar.get_relation_name_or_null(tab_id);
END;
$$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT;

-- Alter Table: LEFT IN PYTHON (for now) -----------------------------------------------------------

----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
Expand Down
20 changes: 20 additions & 0 deletions db/tables/operations/alter.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ def alter_table(table_name, table_oid, schema, engine, update_data):
batch_update_columns(table_oid, engine, update_data['columns'])


def alter_table_on_database(table_oid, table_data_dict, conn):
"""
Alter the name, description, or columns of a table, returning name of the altered table.

Args:
table_oid: The OID of the table to be altered.
table_data_dict: A dict describing the alterations to make.

table_data_dict should have the form:
{
"name": <str>,
"description": <str>,
"columns": <list> of column_data describing columns to alter.
}
"""
return db_conn.exec_msar_func(
conn, 'alter_table', table_oid, table_data_dict
).fetchone()[0]


def update_pk_sequence_to_latest(engine, table, connection=None):
"""
Update the primary key sequence to the current maximum.
Expand Down
14 changes: 14 additions & 0 deletions db/tests/tables/operations/test_alter.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,17 @@ def test_comment_on_table(engine_with_schema):
assert call_args[2] == schema_name
assert call_args[3] == "comment_on_me"
assert call_args[4] == "This is a comment"


def test_alter_table():
with patch.object(tab_alter.db_conn, 'exec_msar_func') as mock_exec:
tab_alter.alter_table_on_database(
12345,
{"name": "newname", "description": "this is a comment", "columns": {}},
"conn"
)
call_args = mock_exec.call_args_list[0][0]
assert call_args[0] == "conn"
assert call_args[1] == "alter_table"
assert call_args[2] == 12345
assert call_args[3] == {"name": "newname", "description": "this is a comment", "columns": {}}
2 changes: 2 additions & 0 deletions docs/docs/api/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ To use an RPC function:
- get
- add
- delete
- patch
- TableInfo
- SettableTableInfo

---

Expand Down
46 changes: 45 additions & 1 deletion mathesar/rpc/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from db.tables.operations.select import get_table_info, get_table
from db.tables.operations.drop import drop_table_from_database
from db.tables.operations.create import create_table_on_database
from mathesar.rpc.columns import CreateableColumnInfo
from db.tables.operations.alter import alter_table_on_database
from mathesar.rpc.columns import CreateableColumnInfo, SettableColumnInfo
from mathesar.rpc.constraints import CreateableConstraintInfo
from mathesar.rpc.exceptions.handlers import handle_rpc_exceptions
from mathesar.rpc.utils import connect
Expand All @@ -28,6 +29,27 @@ class TableInfo(TypedDict):
description: Optional[str]


class SettableTableInfo(TypedDict):
"""
Information about a table, restricted to settable fields.

When possible, Passing `null` for a key will clear the underlying
setting. E.g.,

- `description = null` clears the table description.

Setting any of `name`, `columns` to `null` is a noop.

Attributes:
name: The new name of the table.
description: The description of the table.
columns: A list describing desired column alterations.
"""
name: Optional[str]
description: Optional[str]
columns: Optional[list[SettableColumnInfo]]


@rpc_method(name="tables.list")
@http_basic_auth_login_required
@handle_rpc_exceptions
Expand Down Expand Up @@ -125,3 +147,25 @@ def delete(
user = kwargs.get(REQUEST_KEY).user
with connect(database_id, user) as conn:
return drop_table_from_database(table_oid, conn, cascade)


@rpc_method(name="tables.patch")
@http_basic_auth_login_required
@handle_rpc_exceptions
def patch(
*, table_oid: str, table_data_dict: SettableTableInfo, database_id: int, **kwargs
) -> str:
"""
Alter details of a preexisting table in a database.

Args:
table_oid: Identity of the table whose name, description or columns we'll modify.
table_data_dict: A list describing desired table alterations.
database_id: The Django id of the database containing the table.

Returns:
The name of the altered table.
"""
user = kwargs.get(REQUEST_KEY).user
with connect(database_id, user) as conn:
return alter_table_on_database(table_oid, table_data_dict, conn)
5 changes: 5 additions & 0 deletions mathesar/tests/rpc/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@
tables.delete,
"tables.delete",
[user_is_authenticated]
),
(
tables.patch,
"tables.patch",
[user_is_authenticated]
)
]

Expand Down
40 changes: 40 additions & 0 deletions mathesar/tests/rpc/test_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,43 @@ def mock_table_add(table_name, _schema_oid, conn, column_data_list, constraint_d
monkeypatch.setattr(tables, 'create_table_on_database', mock_table_add)
actual_table_oid = tables.add(table_name='newtable', schema_oid=2200, database_id=11, request=request)
assert actual_table_oid == 1964474


def test_tables_patch(rf, monkeypatch):
request = rf.post('/api/rpc/v0', data={})
request.user = User(username='alice', password='pass1234')
table_oid = 1964474
database_id = 11
table_data_dict = {
"name": "newtabname",
"description": "this is a description",
"columns": {}
}

@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_patch(_table_oid, _table_data_dict, conn):
if _table_oid != table_oid and _table_data_dict != table_data_dict:
raise AssertionError('incorrect parameters passed')
return 'newtabname'
monkeypatch.setattr(tables, 'connect', mock_connect)
monkeypatch.setattr(tables, 'alter_table_on_database', mock_table_patch)
altered_table_name = tables.patch(
table_oid=1964474,
table_data_dict={
"name": "newtabname",
"description": "this is a description",
"columns": {}
},
database_id=11,
request=request
)
assert altered_table_name == 'newtabname'
Loading