Add column_details JSON extra

This commit is contained in:
Simon Willison 2026-07-03 14:45:38 -07:00
commit 8856914be8
5 changed files with 156 additions and 1 deletions

View file

@ -1,6 +1,7 @@
import itertools
from dataclasses import dataclass
from datasette.column_types import SQLiteType
from datasette.database import QueryInterrupted
from datasette.extras import Extra, ExtraExample, ExtraRegistry, ExtraScope, Provider
from datasette.plugins import pm
@ -342,6 +343,41 @@ class PrimaryKeysExtra(Extra):
return context.pks
def column_detail_as_json(column):
return {
"type": column.type,
"sqlite_type": SQLiteType.from_declared_type(column.type).value,
"notnull": column.notnull,
"default": column.default_value,
"is_pk": bool(column.is_pk),
"hidden": bool(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."
)
example = ExtraExample("/fixtures/binary_data.json?_size=0&_extra=column_details")
examples = {
ExtraScope.ROW: ExtraExample(
"/fixtures/binary_data/1.json?_extra=column_details"
)
}
scopes = {ExtraScope.TABLE, ExtraScope.ROW}
async def resolve(self, context):
column_details = await context.datasette._get_resource_column_details(
context.database_name, context.table_name
)
return {
column_name: column_detail_as_json(column)
for column_name, column in column_details.items()
}
class ActionsExtra(Extra):
description = 'Async callable returning table or view actions made available by core and plugin hooks. Each item is either a link with ``href``, ``label`` and optional ``description`` keys, or a button with ``type: "button"``, ``label``, optional ``description`` and optional ``attrs``. See :ref:`plugin_actions`, :ref:`plugin_hook_table_actions` and :ref:`plugin_hook_view_actions`.'
scopes = {ExtraScope.TABLE}
@ -1206,6 +1242,7 @@ TABLE_EXTRA_CLASSES = [
ColumnsExtra,
AllColumnsExtra,
PrimaryKeysExtra,
ColumnDetailsExtra,
DisplayColumnsAndRowsProvider,
DisplayColumnsExtra,
DisplayRowsExtra,

View file

@ -4,6 +4,13 @@
Changelog
=========
.. _v1_0_a36:
1.0a36 (in development)
-----------------------
- The table and row JSON APIs now support ``?_extra=column_details`` for returning SQLite schema details for columns, including declared type, SQLite affinity, primary key, ``NOT NULL``, default and hidden-column metadata.
.. _v1_0_a35:
1.0a35 (2026-06-23)

View file

@ -402,6 +402,24 @@ The available table extras are listed below.
"pk"
]
``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.
``GET /fixtures/binary_data.json?_size=0&_extra=column_details``
.. code-block:: json
{
"data": {
"type": "BLOB",
"sqlite_type": "BLOB",
"notnull": 0,
"default": null,
"is_pk": false,
"hidden": false
}
}
``display_columns``
Column metadata used by the HTML table display. Each item includes ``name``, ``sortable``, ``is_pk``, ``type``, ``notnull``, ``description``, ``column_type`` and ``column_type_config`` keys.
@ -807,6 +825,24 @@ The following extras are available for row JSON responses.
"id"
]
``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.
``GET /fixtures/binary_data/1.json?_extra=column_details``
.. code-block:: json
{
"data": {
"type": "BLOB",
"sqlite_type": "BLOB",
"notnull": 0,
"default": null,
"is_pk": false,
"hidden": false
}
}
``render_cell``
Rendered HTML for each cell using the render_cell plugin hook (See the :ref:`render_cell() plugin hook <plugin_hook_render_cell>` documentation.)

View file

@ -429,7 +429,7 @@ async def test_row_foreign_key_tables(ds_client):
@pytest.mark.asyncio
async def test_row_extras(ds_client):
response = await ds_client.get(
"/fixtures/simple_primary_key/1.json?_extra=database,table,primary_keys,query,request,debug,foreign_key_tables"
"/fixtures/simple_primary_key/1.json?_extra=database,table,primary_keys,query,request,debug,foreign_key_tables,column_details"
)
assert response.status_code == 200
data = response.json()
@ -446,6 +446,40 @@ async def test_row_extras(ds_client):
"format": "json",
}
assert len(data["foreign_key_tables"]) == 5
assert data["column_details"]["id"] == {
"type": "INTEGER",
"sqlite_type": "INTEGER",
"notnull": 0,
"default": None,
"is_pk": True,
"hidden": False,
}
assert data["column_details"]["content"] == {
"type": "TEXT",
"sqlite_type": "TEXT",
"notnull": 0,
"default": None,
"is_pk": False,
"hidden": False,
}
@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"
)
assert response.status_code == 200
assert response.json()["column_details"] == {
"data": {
"type": "BLOB",
"sqlite_type": "BLOB",
"notnull": 0,
"default": None,
"is_pk": False,
"hidden": False,
}
}
@pytest.mark.asyncio

View file

@ -1336,6 +1336,47 @@ async def test_binary_data_in_json(ds_client, path, expected_json, expected_text
assert response.text == expected_text
@pytest.mark.asyncio
async def test_column_details_extra_table(ds_client):
response = await ds_client.get(
"/fixtures/binary_data.json?_size=0&_extra=column_details"
)
assert response.status_code == 200
assert response.json()["column_details"] == {
"data": {
"type": "BLOB",
"sqlite_type": "BLOB",
"notnull": 0,
"default": None,
"is_pk": False,
"hidden": False,
}
}
response = await ds_client.get(
"/fixtures/simple_primary_key.json?_size=0&_extra=column_details"
)
assert response.status_code == 200
assert response.json()["column_details"] == {
"id": {
"type": "INTEGER",
"sqlite_type": "INTEGER",
"notnull": 0,
"default": None,
"is_pk": True,
"hidden": False,
},
"content": {
"type": "TEXT",
"sqlite_type": "TEXT",
"notnull": 0,
"default": None,
"is_pk": False,
"hidden": False,
},
}
@pytest.mark.asyncio
@pytest.mark.parametrize(
"qs",