mirror of
https://github.com/simonw/datasette.git
synced 2026-07-19 22:14:35 +02:00
Ensure primary keys created via the JSON API are NOT NULL
The table-create JSON API built its own CREATE TABLE without marking primary key columns NOT NULL, so a text or composite primary key could be created nullable. SQLite then allows rows with a NULL primary key, which cannot be viewed, edited or deleted. Force primary key columns NOT NULL, leaving a lone integer primary key alone since it aliases the rowid and is never NULL. Refs #2807 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6f27aa112a
commit
3956e63650
2 changed files with 73 additions and 1 deletions
|
|
@ -900,7 +900,22 @@ class TableCreateView(BaseView):
|
|||
rows, pk=pks or pk, ignore=ignore, replace=replace, alter=alter
|
||||
)
|
||||
else:
|
||||
not_null = [column.name for column in columns if column.not_null]
|
||||
# Force primary key columns NOT NULL so the API cannot create
|
||||
# NULL primary keys, which SQLite otherwise allows and which
|
||||
# leaves rows that cannot be viewed, edited or deleted (see
|
||||
# #2807). A single integer primary key aliases the rowid and is
|
||||
# never NULL, so it needs no explicit constraint.
|
||||
pk_names = set(pks) if pks else ({pk} if pk else set())
|
||||
if len(pk_names) == 1 and any(
|
||||
column.name in pk_names and column.type == "integer"
|
||||
for column in columns
|
||||
):
|
||||
pk_names = set()
|
||||
not_null = [
|
||||
column.name
|
||||
for column in columns
|
||||
if column.not_null or column.name in pk_names
|
||||
]
|
||||
defaults = {}
|
||||
for column in columns:
|
||||
if "default_expr" in column.model_fields_set:
|
||||
|
|
|
|||
|
|
@ -2296,6 +2296,63 @@ async def test_create_table_with_column_constraints(ds_write):
|
|||
assert columns[4]["dflt_value"] == "'hello)'"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_table_primary_keys_are_not_null(ds_write):
|
||||
# Primary keys created via the JSON API must be NOT NULL - see #2807
|
||||
token = write_token(ds_write)
|
||||
db = ds_write.get_database("data")
|
||||
|
||||
# Single (non-integer) primary key
|
||||
response = await ds_write.client.post(
|
||||
"/data/-/create",
|
||||
json={
|
||||
"table": "pk_single",
|
||||
"columns": [
|
||||
{"name": "code", "type": "text"},
|
||||
{"name": "name", "type": "text"},
|
||||
],
|
||||
"pk": "code",
|
||||
},
|
||||
headers=_headers(token),
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
assert "PRIMARY KEY NOT NULL" in response.json()["schema"]
|
||||
columns = {
|
||||
column["name"]: column
|
||||
for column in (
|
||||
await db.execute("select * from pragma_table_info('pk_single')")
|
||||
).dicts()
|
||||
}
|
||||
assert columns["code"]["pk"] == 1
|
||||
assert columns["code"]["notnull"] == 1
|
||||
assert columns["name"]["notnull"] == 0
|
||||
|
||||
# Composite primary key - every key column is NOT NULL
|
||||
response = await ds_write.client.post(
|
||||
"/data/-/create",
|
||||
json={
|
||||
"table": "pk_composite",
|
||||
"columns": [
|
||||
{"name": "a", "type": "text"},
|
||||
{"name": "b", "type": "text"},
|
||||
{"name": "value", "type": "text"},
|
||||
],
|
||||
"pks": ["a", "b"],
|
||||
},
|
||||
headers=_headers(token),
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
columns = {
|
||||
column["name"]: column
|
||||
for column in (
|
||||
await db.execute("select * from pragma_table_info('pk_composite')")
|
||||
).dicts()
|
||||
}
|
||||
assert columns["a"]["notnull"] == 1
|
||||
assert columns["b"]["notnull"] == 1
|
||||
assert columns["value"]["notnull"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"default_expr,minimum_value,expected_schema",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue