Skip to content

Commit 9fcc240

Browse files
committed
fix: harden database table admin client
1 parent 1a14a17 commit 9fcc240

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

insforge/database/client.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ async def get_table_schema(
3737
) -> DatabaseTableSchemaResponse:
3838
payload = await self._client._request_json(
3939
"GET",
40-
f"/api/database/tables/{table_name}/schema",
40+
f"/api/database/tables/{quote_path_segment(table_name)}/schema",
4141
access_token=access_token,
4242
)
4343
return DatabaseTableSchemaResponse.model_validate(payload)
@@ -75,6 +75,19 @@ async def update_table_schema(
7575
rename_table: DatabaseTableSchemaRenameRequest | dict[str, object] | None = None,
7676
access_token: str | None = None,
7777
) -> DatabaseTableMutationResponse:
78+
if not any(
79+
value is not None
80+
for value in (
81+
add_columns,
82+
drop_columns,
83+
update_columns,
84+
add_foreign_keys,
85+
drop_foreign_keys,
86+
rename_table,
87+
)
88+
):
89+
raise ValueError("update_table_schema requires at least one schema operation")
90+
7891
payload = DatabaseTableSchemaUpdateRequest(
7992
add_columns=add_columns,
8093
drop_columns=drop_columns,

tests/database/test_database_admin.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,31 @@ async def fake_request(method: str, url: httpx.URL, **kwargs: object) -> httpx.R
8282
assert result.columns[0].name == "id"
8383

8484

85+
def test_get_table_schema_quotes_table_name_path_segment() -> None:
86+
async def scenario() -> dict[str, object]:
87+
captured: dict[str, object] = {}
88+
89+
async def fake_request(method: str, url: httpx.URL, **kwargs: object) -> httpx.Response:
90+
captured["method"] = method
91+
captured["url"] = str(url)
92+
captured["kwargs"] = kwargs
93+
return httpx.Response(200, json={"table_name": "analytics/events", "columns": []})
94+
95+
async with InsforgeClient(
96+
base_url="https://example.com",
97+
api_key="ins_test",
98+
) as client:
99+
client.http_client.request = fake_request # type: ignore[method-assign]
100+
result = await client.database.get_table_schema("analytics/events")
101+
assert isinstance(result, DatabaseTableSchemaResponse)
102+
return captured
103+
104+
captured = asyncio.run(scenario())
105+
106+
assert captured["method"] == "GET"
107+
assert captured["url"] == "https://example.com/api/database/tables/analytics%2Fevents/schema"
108+
109+
85110
def test_create_table_uses_post_schema_endpoint_and_serializes_request_body() -> None:
86111
async def scenario() -> tuple[object, dict[str, object]]:
87112
captured: dict[str, object] = {}
@@ -164,6 +189,22 @@ async def fake_request(method: str, url: httpx.URL, **kwargs: object) -> httpx.R
164189
assert result.table_name == "posts"
165190

166191

192+
def test_update_table_schema_rejects_empty_mutation_request() -> None:
193+
async def scenario() -> None:
194+
async with InsforgeClient(
195+
base_url="https://example.com",
196+
api_key="ins_test",
197+
) as client:
198+
await client.database.update_table_schema("posts")
199+
200+
try:
201+
asyncio.run(scenario())
202+
except ValueError as exc:
203+
assert str(exc) == "update_table_schema requires at least one schema operation"
204+
else:
205+
raise AssertionError("update_table_schema should reject empty mutation requests")
206+
207+
167208
def test_update_table_schema_uses_patch_schema_endpoint_and_serializes_request_body() -> None:
168209
async def scenario() -> tuple[object, dict[str, object]]:
169210
captured: dict[str, object] = {}

0 commit comments

Comments
 (0)