Refine column_details metadata shape

This commit is contained in:
Simon Willison 2026-07-03 16:08:34 -07:00
commit b476218edb
4 changed files with 101 additions and 25 deletions

View file

@ -347,18 +347,32 @@ def column_detail_as_json(column):
return {
"type": column.type,
"sqlite_type": SQLiteType.from_declared_type(column.type).value,
"notnull": column.notnull,
"notnull": bool(column.notnull),
"default": column.default_value,
"is_pk": bool(column.is_pk),
"hidden": bool(column.hidden),
"pk_position": column.is_pk,
"hidden": column.hidden,
}
class ColumnDetailsExtra(Extra):
description = (
"SQLite schema details for columns in this table. The dictionary maps "
"column names to ``type``, ``sqlite_type``, ``notnull``, ``default``, "
"``is_pk`` and ``hidden`` values."
"column names to objects describing the schema for each column."
)
docs_note = (
"Each object has ``type`` as the declared type string returned by "
'SQLite, or ``""`` if no type was declared; ``sqlite_type`` as the '
"normalized SQLite affinity, one of ``TEXT``, ``INTEGER``, ``REAL``, "
"``BLOB`` or ``NUMERIC``; ``notnull`` as a boolean; ``default`` "
'as the raw SQL default expression string, such as ``"42"``, '
"``\"'hello'\"`` or ``\"datetime('now')\"``, or ``null`` if there is "
"no default; ``is_pk`` as a boolean; ``pk_position`` as the integer "
"primary key position reported by SQLite, or ``0`` for columns that "
"are not part of the primary key; and ``hidden`` as the integer value "
"reported by SQLite's ``PRAGMA table_xinfo``. ``hidden`` is ``0`` for "
"normal columns, ``1`` for hidden virtual table columns, ``2`` for "
"virtual generated columns and ``3`` for stored generated columns."
)
example = ExtraExample("/fixtures/binary_data.json?_size=0&_extra=column_details")
examples = {

View file

@ -403,7 +403,7 @@ The available table extras are listed below.
]
``column_details``
SQLite schema details for columns in this table. The dictionary maps column names to ``type``, ``sqlite_type``, ``notnull``, ``default``, ``is_pk`` and ``hidden`` values.
SQLite schema details for columns in this table. The dictionary maps column names to objects describing the schema for each column. (Each object has ``type`` as the declared type string returned by SQLite, or ``""`` if no type was declared; ``sqlite_type`` as the normalized SQLite affinity, one of ``TEXT``, ``INTEGER``, ``REAL``, ``BLOB`` or ``NUMERIC``; ``notnull`` as a boolean; ``default`` as the raw SQL default expression string, such as ``"42"``, ``"'hello'"`` or ``"datetime('now')"``, or ``null`` if there is no default; ``is_pk`` as a boolean; ``pk_position`` as the integer primary key position reported by SQLite, or ``0`` for columns that are not part of the primary key; and ``hidden`` as the integer value reported by SQLite's ``PRAGMA table_xinfo``. ``hidden`` is ``0`` for normal columns, ``1`` for hidden virtual table columns, ``2`` for virtual generated columns and ``3`` for stored generated columns.)
``GET /fixtures/binary_data.json?_size=0&_extra=column_details``
@ -413,10 +413,11 @@ The available table extras are listed below.
"data": {
"type": "BLOB",
"sqlite_type": "BLOB",
"notnull": 0,
"notnull": false,
"default": null,
"is_pk": false,
"hidden": false
"pk_position": 0,
"hidden": 0
}
}
@ -826,7 +827,7 @@ The following extras are available for row JSON responses.
]
``column_details``
SQLite schema details for columns in this table. The dictionary maps column names to ``type``, ``sqlite_type``, ``notnull``, ``default``, ``is_pk`` and ``hidden`` values.
SQLite schema details for columns in this table. The dictionary maps column names to objects describing the schema for each column. (Each object has ``type`` as the declared type string returned by SQLite, or ``""`` if no type was declared; ``sqlite_type`` as the normalized SQLite affinity, one of ``TEXT``, ``INTEGER``, ``REAL``, ``BLOB`` or ``NUMERIC``; ``notnull`` as a boolean; ``default`` as the raw SQL default expression string, such as ``"42"``, ``"'hello'"`` or ``"datetime('now')"``, or ``null`` if there is no default; ``is_pk`` as a boolean; ``pk_position`` as the integer primary key position reported by SQLite, or ``0`` for columns that are not part of the primary key; and ``hidden`` as the integer value reported by SQLite's ``PRAGMA table_xinfo``. ``hidden`` is ``0`` for normal columns, ``1`` for hidden virtual table columns, ``2`` for virtual generated columns and ``3`` for stored generated columns.)
``GET /fixtures/binary_data/1.json?_extra=column_details``
@ -836,10 +837,11 @@ The following extras are available for row JSON responses.
"data": {
"type": "BLOB",
"sqlite_type": "BLOB",
"notnull": 0,
"notnull": false,
"default": null,
"is_pk": false,
"hidden": false
"pk_position": 0,
"hidden": 0
}
}

View file

@ -449,35 +449,36 @@ async def test_row_extras(ds_client):
assert data["column_details"]["id"] == {
"type": "INTEGER",
"sqlite_type": "INTEGER",
"notnull": 0,
"notnull": False,
"default": None,
"is_pk": True,
"hidden": False,
"pk_position": 1,
"hidden": 0,
}
assert data["column_details"]["content"] == {
"type": "TEXT",
"sqlite_type": "TEXT",
"notnull": 0,
"notnull": False,
"default": None,
"is_pk": False,
"hidden": False,
"pk_position": 0,
"hidden": 0,
}
@pytest.mark.asyncio
async def test_column_details_extra_row_for_null_blob(ds_client):
response = await ds_client.get(
"/fixtures/binary_data/3.json?_extra=column_details"
)
response = await ds_client.get("/fixtures/binary_data/3.json?_extra=column_details")
assert response.status_code == 200
assert response.json()["column_details"] == {
"data": {
"type": "BLOB",
"sqlite_type": "BLOB",
"notnull": 0,
"notnull": False,
"default": None,
"is_pk": False,
"hidden": False,
"pk_position": 0,
"hidden": 0,
}
}

View file

@ -1346,10 +1346,11 @@ async def test_column_details_extra_table(ds_client):
"data": {
"type": "BLOB",
"sqlite_type": "BLOB",
"notnull": 0,
"notnull": False,
"default": None,
"is_pk": False,
"hidden": False,
"pk_position": 0,
"hidden": 0,
}
}
@ -1361,21 +1362,79 @@ async def test_column_details_extra_table(ds_client):
"id": {
"type": "INTEGER",
"sqlite_type": "INTEGER",
"notnull": 0,
"notnull": False,
"default": None,
"is_pk": True,
"hidden": False,
"pk_position": 1,
"hidden": 0,
},
"content": {
"type": "TEXT",
"sqlite_type": "TEXT",
"notnull": 0,
"notnull": False,
"default": None,
"is_pk": False,
"hidden": False,
"pk_position": 0,
"hidden": 0,
},
}
response = await ds_client.get(
"/fixtures/compound_three_primary_keys.json?_size=0&_extra=column_details"
)
assert response.status_code == 200
column_details = response.json()["column_details"]
assert column_details["pk1"]["is_pk"] is True
assert column_details["pk1"]["pk_position"] == 1
assert column_details["pk2"]["is_pk"] is True
assert column_details["pk2"]["pk_position"] == 2
assert column_details["pk3"]["is_pk"] is True
assert column_details["pk3"]["pk_position"] == 3
assert column_details["content"]["is_pk"] is False
assert column_details["content"]["pk_position"] == 0
def test_column_details_extra_defaults_and_notnull():
with make_app_client(extra_databases={"defaults.db": """
CREATE TABLE defaults (
i INTEGER NOT NULL DEFAULT 42,
s TEXT DEFAULT 'hello',
dt TEXT DEFAULT (datetime('now'))
);
"""}) as client:
response = client.get("/defaults/defaults.json?_size=0&_extra=column_details")
assert response.status == 200
column_details = response.json["column_details"]
assert column_details["i"]["notnull"] is True
assert column_details["i"]["default"] == "42"
assert column_details["s"]["notnull"] is False
assert column_details["s"]["default"] == "'hello'"
assert column_details["dt"]["default"] == "datetime('now')"
@pytest.mark.skipif(
sqlite_version() < (3, 31, 0),
reason="generated columns were added in SQLite 3.31.0",
)
def test_column_details_extra_generated_columns():
with make_app_client(extra_databases={"generated.db": """
CREATE TABLE generated_columns (
body TEXT,
body_length_virtual INTEGER
GENERATED ALWAYS AS (length(body)) VIRTUAL,
body_length_stored INTEGER
GENERATED ALWAYS AS (length(body)) STORED
);
"""}) as client:
response = client.get(
"/generated/generated_columns.json?_size=0&_extra=column_details"
)
assert response.status == 200
column_details = response.json["column_details"]
assert column_details["body"]["hidden"] == 0
assert column_details["body_length_virtual"]["hidden"] == 2
assert column_details["body_length_stored"]["hidden"] == 3
@pytest.mark.asyncio
@pytest.mark.parametrize(