diff --git a/db/sql/00_msar.sql b/db/sql/00_msar.sql index a18dabb1c4..c6693b00a6 100644 --- a/db/sql/00_msar.sql +++ b/db/sql/00_msar.sql @@ -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; @@ -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 @@ -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 @@ -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 the name, description, or columns of a table, returning name of the altered 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": , + "description": + "columns": , + } +*/ +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) ----------------------------------------------------------- ---------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- diff --git a/db/tables/operations/alter.py b/db/tables/operations/alter.py index 7ecb160163..d1d554bc29 100644 --- a/db/tables/operations/alter.py +++ b/db/tables/operations/alter.py @@ -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": , + "description": , + "columns": 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. diff --git a/db/tests/tables/operations/test_alter.py b/db/tests/tables/operations/test_alter.py index 993e307364..754a588956 100644 --- a/db/tests/tables/operations/test_alter.py +++ b/db/tests/tables/operations/test_alter.py @@ -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": {}} diff --git a/docs/docs/api/rpc.md b/docs/docs/api/rpc.md index 9366a78156..6de0d60fbb 100644 --- a/docs/docs/api/rpc.md +++ b/docs/docs/api/rpc.md @@ -64,7 +64,9 @@ To use an RPC function: - get - add - delete + - patch - TableInfo + - SettableTableInfo --- diff --git a/mathesar/rpc/tables.py b/mathesar/rpc/tables.py index 8c6d4575a3..0af725241d 100644 --- a/mathesar/rpc/tables.py +++ b/mathesar/rpc/tables.py @@ -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 CreatableColumnInfo +from db.tables.operations.alter import alter_table_on_database +from mathesar.rpc.columns import CreatableColumnInfo, SettableColumnInfo from mathesar.rpc.constraints import CreatableConstraintInfo from mathesar.rpc.exceptions.handlers import handle_rpc_exceptions from mathesar.rpc.utils import connect @@ -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 @@ -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) diff --git a/mathesar/tests/rpc/test_endpoints.py b/mathesar/tests/rpc/test_endpoints.py index 6db4a98248..33a5b9d862 100644 --- a/mathesar/tests/rpc/test_endpoints.py +++ b/mathesar/tests/rpc/test_endpoints.py @@ -73,6 +73,11 @@ tables.delete, "tables.delete", [user_is_authenticated] + ), + ( + tables.patch, + "tables.patch", + [user_is_authenticated] ) ] diff --git a/mathesar/tests/rpc/test_tables.py b/mathesar/tests/rpc/test_tables.py index 27b678cc54..76da0bbb1d 100644 --- a/mathesar/tests/rpc/test_tables.py +++ b/mathesar/tests/rpc/test_tables.py @@ -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'