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

Add records.get RPC function #3740

Merged
merged 9 commits into from
Aug 7, 2024
23 changes: 23 additions & 0 deletions db/records/operations/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,29 @@ def list_records_from_table(
return result


def get_record_from_table(
conn,
record_id,
table_oid,
):
"""
Get single record from a table by its primary key

Only data from which the user is granted `SELECT` is returned.

Args:
record_id: The primary key value of the record.
table_id: The OID of the table whose record we'll get.
"""
result = db_conn.exec_msar_func(
conn,
'get_record_from_table',
table_oid,
record_id,
).fetchone()[0]
return result


def search_records_from_table(
conn,
table_oid,
Expand Down
27 changes: 25 additions & 2 deletions db/sql/00_msar.sql
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ Return the first column attnum in the primary key of a given relation (e.g., tab
Args:
rel_id: The OID of the relation.
*/
SELECT conkey[1]
SELECT CASE WHEN array_length(conkey, 1) = 1 THEN conkey[1] END
FROM pg_constraint
WHERE contype='p'
AND conrelid=rel_id;
Expand All @@ -586,7 +586,7 @@ Args:
sch_name: The schema of the relation, unquoted.
rel_name: The name of the relation, unqualified and unquoted.
*/
SELECT conkey[1]
SELECT CASE WHEN array_length(conkey, 1) = 1 THEN conkey[1] END
FROM pg_constraint
WHERE contype='p'
AND conrelid=msar.get_relation_oid(sch_name, rel_name);
Expand Down Expand Up @@ -3714,3 +3714,26 @@ BEGIN
RETURN records;
END;
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION
msar.get_record_from_table(tab_id oid, rec_id anyelement) RETURNS jsonb AS $$/*
Get single record from a table. Only columns to which the user has access are returned.

Args:
tab_id: The OID of the table whose record we'll get.
rec_id: The id value of the record.

The table must have a single primary key column.
*/
SELECT msar.list_records_from_table(
tab_id, null, null, null,
jsonb_build_object(
'type', 'equal', 'args', jsonb_build_array(
jsonb_build_object('type', 'attnum', 'value', msar.get_pk_column(tab_id)),
jsonb_build_object('type', 'literal', 'value', rec_id)
)
),
null
)
$$ LANGUAGE SQL STABLE RETURNS NULL ON NULL INPUT;
42 changes: 42 additions & 0 deletions db/sql/test_00_msar.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3696,3 +3696,45 @@ BEGIN
RETURN NEXT is((search_result -> 'count')::integer, 3);
END;
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION test_get_record_from_table() RETURNS SETOF TEXT AS $$
DECLARE
rel_id oid;
BEGIN
PERFORM __setup_list_records_table();
rel_id := 'atable'::regclass::oid;
RETURN NEXT is(
msar.get_record_from_table(rel_id, 2),
$j${
"count": 1,
"results": [
{"1": 2, "2": 34, "3": "sdflfflsk", "4": null, "5": [1, 2, 3, 4]}
],
"grouping": null
}$j$ || jsonb_build_object(
'query', concat(
'SELECT msar.format_data(id) AS "1", msar.format_data(col1) AS "2",',
' msar.format_data(col2) AS "3", msar.format_data(col3) AS "4",',
' msar.format_data(col4) AS "5" FROM public.atable WHERE (id) = (''2'')',
' ORDER BY "1" ASC LIMIT NULL OFFSET NULL'
)
)
);
RETURN NEXT is(
msar.get_record_from_table(rel_id, 200),
$j${
"count": 0,
"results": [],
"grouping": null
}$j$ || jsonb_build_object(
'query', concat(
'SELECT msar.format_data(id) AS "1", msar.format_data(col1) AS "2",',
' msar.format_data(col2) AS "3", msar.format_data(col3) AS "4",',
' msar.format_data(col4) AS "5" FROM public.atable WHERE (id) = (''200'')',
' ORDER BY "1" ASC LIMIT NULL OFFSET NULL'
)
)
);
END;
$$ LANGUAGE plpgsql;
1 change: 1 addition & 0 deletions docs/docs/api/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ To use an RPC function:
options:
members:
- list_
- get
- search
- RecordList
- OrderBy
Expand Down
31 changes: 31 additions & 0 deletions mathesar/rpc/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,37 @@ def list_(
return RecordList.from_dict(record_info)


@rpc_method(name="records.get")
@http_basic_auth_login_required
@handle_rpc_exceptions
def get(
*,
record_id: Any,
table_oid: int,
database_id: int,
**kwargs
) -> RecordList:
"""
Get single record from a table by its primary key.

Args:
record_id: The primary key value of the record to be gotten.
table_oid: Identity of the table in the user's database.
database_id: The Django id of the database containing the table.

Returns:
The requested record, along with some metadata.
"""
user = kwargs.get(REQUEST_KEY).user
with connect(database_id, user) as conn:
record_info = record_select.get_record_from_table(
conn,
record_id,
table_oid,
)
return RecordList.from_dict(record_info)


@rpc_method(name="records.search")
@http_basic_auth_login_required
@handle_rpc_exceptions
Expand Down
5 changes: 5 additions & 0 deletions mathesar/tests/rpc/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@
"records.list",
[user_is_authenticated]
),
(
records.get,
"records.get",
[user_is_authenticated]
),
(
records.search,
"records.search",
Expand Down
48 changes: 48 additions & 0 deletions mathesar/tests/rpc/test_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,54 @@ def mock_list_records(
assert actual_records_list == expect_records_list


def test_records_get(rf, monkeypatch):
username = 'alice'
password = 'pass1234'
table_oid = 23457
database_id = 2
record_id = 2342
request = rf.post('/api/rpc/v0/', data={})
request.user = User(username=username, password=password)

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

def mock_get_record(
conn,
_record_id,
_table_oid,
):
if _table_oid != table_oid or _record_id != record_id:
raise AssertionError('incorrect parameters passed')
return {
"count": 1,
"results": [{"1": "abcde", "2": 12345}, {"1": "fghij", "2": 67890}],
"query": 'SELECT mycol AS "1", anothercol AS "2" FROM mytable LIMIT 2',
"grouping": None,
}

monkeypatch.setattr(records, 'connect', mock_connect)
monkeypatch.setattr(records.record_select, 'get_record_from_table', mock_get_record)
expect_record = {
"count": 1,
"results": [{"1": "abcde", "2": 12345}, {"1": "fghij", "2": 67890}],
"grouping": None,
"preview_data": [],
"query": 'SELECT mycol AS "1", anothercol AS "2" FROM mytable LIMIT 2',
}
actual_record = records.get(
record_id=record_id, table_oid=table_oid, database_id=database_id, request=request
)
assert actual_record == expect_record


def test_records_search(rf, monkeypatch):
username = 'alice'
password = 'pass1234'
Expand Down
Loading