mirror of
https://github.com/simonw/datasette.git
synced 2026-10-18 23:36:57 +02:00
Match primary key names case-insensitively when forcing NOT NULL
SQLite resolves identifiers case-insensitively, so a request with pk "code" against a column named "Code" still creates that column as the primary key. The membership check was case-sensitive, so that column was left out of not_null and the API could still create a nullable text primary key, which is the exact unviewable/undeletable NULL-PK row this change is meant to prevent. The integer-rowid-alias exemption is matched the same way, so an integer pk whose casing differs still stays a plain rowid alias rather than picking up a constraint it does not need. Two cases added to test_create_table_primary_keys_are_not_null; the text one fails without this change (notnull 0, expected 1).
This commit is contained in:
parent
3956e63650
commit
c9a365e1ef
2 changed files with 58 additions and 3 deletions
|
|
@ -905,16 +905,21 @@ class TableCreateView(BaseView):
|
|||
# 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())
|
||||
# Matched case-insensitively because SQLite resolves identifiers
|
||||
# that way: pk "code" against a column named "Code" still creates
|
||||
# that column as the primary key.
|
||||
pk_names = {
|
||||
name.lower() for name in (pks if pks else ([pk] if pk else []))
|
||||
}
|
||||
if len(pk_names) == 1 and any(
|
||||
column.name in pk_names and column.type == "integer"
|
||||
column.name.lower() 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
|
||||
if column.not_null or column.name.lower() in pk_names
|
||||
]
|
||||
defaults = {}
|
||||
for column in columns:
|
||||
|
|
|
|||
|
|
@ -2352,6 +2352,56 @@ async def test_create_table_primary_keys_are_not_null(ds_write):
|
|||
assert columns["b"]["notnull"] == 1
|
||||
assert columns["value"]["notnull"] == 0
|
||||
|
||||
# Primary key named with different casing from the column. SQLite resolves
|
||||
# identifiers case-insensitively, so this still creates "Code" as the primary
|
||||
# key and it must pick up NOT NULL too.
|
||||
response = await ds_write.client.post(
|
||||
"/data/-/create",
|
||||
json={
|
||||
"table": "pk_mixed_case",
|
||||
"columns": [
|
||||
{"name": "Code", "type": "text"},
|
||||
{"name": "name", "type": "text"},
|
||||
],
|
||||
"pk": "code",
|
||||
},
|
||||
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_mixed_case')")
|
||||
).dicts()
|
||||
}
|
||||
assert columns["Code"]["pk"] == 1
|
||||
assert columns["Code"]["notnull"] == 1
|
||||
assert columns["name"]["notnull"] == 0
|
||||
|
||||
# The integer-rowid-alias exemption is matched case-insensitively too, so an
|
||||
# integer pk stays a plain rowid alias rather than gaining a constraint.
|
||||
response = await ds_write.client.post(
|
||||
"/data/-/create",
|
||||
json={
|
||||
"table": "pk_mixed_case_integer",
|
||||
"columns": [
|
||||
{"name": "Id", "type": "integer"},
|
||||
{"name": "name", "type": "text"},
|
||||
],
|
||||
"pk": "id",
|
||||
},
|
||||
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_mixed_case_integer')")
|
||||
).dicts()
|
||||
}
|
||||
assert columns["Id"]["pk"] == 1
|
||||
assert columns["Id"]["notnull"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue