- Add POST /<database>/<table>/-/alter with Pydantic validation and dry-run support.
- Support add, rename, alter, drop, primary-key and reorder operations, including allow-listed default expressions.
- Document the endpoint and cover schema changes, validation, permissions, events and dry runs.
Refs #2788
Adds a permission-gated database action that opens a create table modal on database pages, backed by the existing create-table JSON API.
The modal starts with an id integer primary key column plus a blank text column, supports SQLite type selection, and shows custom column type controls only when the actor can set column types.
Selected custom column types are applied after table creation with follow-up set-column-type API calls. Includes styling plus HTML and Playwright coverage for the action payload and create-table flow.
Previously there were four lines of whitespace, but that
meant users had to delete that whitespace themselves when
they started editing. Now it is four lines tall without that.
Add a Create table starter template to the execute-write page, alongside the existing table-specific insert/update/delete templates.
Keep template button clicks in-page by updating CodeMirror and the URL without collapsing the template controls.
Refresh schema metadata after successful schema-changing execute-write statements so newly created tables appear immediately.
- Simplify JavaScript column field context:
- expose `isPk` instead of `isPrimaryKey`
- expose `defaultExpression` instead of separate SQLite default flags
- remove value/default state from plugin context
- Update field helper behavior:
- `setValue()` no longer dispatches input/change events
- remove dispatch options and `resetValue()`
- add `markClean()` for plugin-normalized initial values
- track clean field state for reliable dirty detection
Also:
- Prompt before closing row insert/edit dialogs when there are unsaved changes
- Map declared SQLite types to affinities, returning `BLOB` for typeless columns and `NUMERIC` for numeric/date/boolean-like declarations
Replace the value/valueType/originalValue/originalValueType fields on makeColumnField() contexts with an explicit field object API for reading, writing, resetting, comparing and validating field values.
Normalize columnType to {type, config}, rename the SQLite default metadata so it is clearly SQLite-specific, and document that plugins submit only string, number, boolean or null values. Plugins that need structured data should serialize it themselves instead of relying on Datasette to special-case JSON.
Move the built-in json column type behavior onto the same plugin API used by external plugins: validate the textarea with field.setValidity() as the value changes, but submit plain text. Harden row edit value comparison so fixing invalid JSON in an existing row is not blocked by the original invalid value.
Update the JavaScript plugin documentation and Node-based tests for the revised field contract.
Expose single-primary-key foreign key autocomplete URLs in table page metadata and load the autocomplete component when needed.
Enhance insert and edit dialogs to wrap foreign-key inputs with the autocomplete web component, show linked selected-row labels, reserve metadata space, and keep the dropdown as a fixed overlay above modal chrome.
Add an explicit _initial=1 autocomplete mode for empty-field starter suggestions while keeping blank q responses empty by default, with tests for the endpoint and table metadata.
Add a permission-gated Insert row button to mutable table pages and expose the metadata needed by the client-side UI, including the insert API path, table name, primary keys, editable columns, defaults, nullability, and column type information.
Reuse the existing row edit modal for inserts. Insert submissions now use the JSON API with return=true, derive the new row's tilde-encoded row path from the returned primary key values, fetch the matching table fragment, and insert the rendered row into the current table. Successful inserts and updates now show mutation status messages above the table.
Support SQLite defaults in insert forms by showing default expressions as non-editable values with Set value / Use default controls. Keep those controls aligned and stable so toggling between default and custom values does not shift the modal layout.
Refine the edit modal at the same time: send only changed fields on update, skip the update API entirely when nothing changed, clear stale mutation status for no-op saves, and simplify modal headings so insert/edit context is shown in the bold title instead of duplicated summary text.
Add tests for the insert button and metadata, including omitted integer primary keys, default values, table names, and compound primary keys.
Use a compact data-row attribute on table row fragments and derive row API URLs in JavaScript from a page-level table URL. Add a /-/fragment endpoint so edited rows can be re-rendered with the active table template and render_cell hooks, then replaced in place after a successful save.
Document the custom _table.html data-row contract and cover the fragment endpoint, base_url handling, and row markup with tests.
Adds a per-request cache for permission check results, plus wiring that
resolves action permissions in bulk before plugin hooks need them:
- New _permission_check_cache contextvar, set to a fresh dict for each
request by DatasetteRouter and reset when the request ends. Keys
include the full serialized actor, so actors differing in any field
(e.g. token restrictions) never share entries. SkipPermissions mode
bypasses the cache entirely.
- datasette.allowed_many() now consults the cache and stores its
results there, so repeated datasette.allowed() checks within one
request resolve without further SQL.
- Table pages resolve all registered table-level actions against the
current table and all database-level actions against its database
(database pages likewise) in batched queries before invoking the
table_actions/database_actions plugin hooks - allowed() calls made
inside those hooks are then served from the cache with no plugin
changes required. Actions with no permission rules from any plugin
are resolved to False without touching the database.
Benchmarks (benchmarks/) with a simulated 12-plugin ecosystem making
18 checks per table page show 34 -> 13 internal-DB queries per page;
with 2ms-per-query internal DB latency (modelling Datasette Cloud)
table page time drops from 77.9ms to 27.6ms - the caching layer
accounts for ~91% of that improvement over allowed_many() alone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
datasette/template_contexts.py is now a thin index with no
documentation strings of its own - the docs live next to the code:
- Each page's Context class (DatabaseContext, QueryContext,
TableContext, RowContext) carries a docstring, its template name and
help metadata on view-added fields, in the view module itself
- extra_field() fields document themselves from the Extra classes
- The keys render_template() adds to every page are documented in
TEMPLATE_BASE_CONTEXT in app.py, next to the code that adds them,
with the contract tests keeping the two in sync
docs/template_context.rst is regenerated from the dataclasses, so the
table and row pages now include field types like the others.
Refs #2127
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RowView declares context_class = RowContext; BaseView.render()
constructs the dataclass from the assembled context, dropping any keys
not declared on the class, after select_templates and
alternate_url_json have been added. Extras-named fields use
extra_field() so their documentation comes from the Extra classes;
view-added fields carry help metadata next to the view code.
Refs #2127
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The table HTML view constructs a TableContext instead of an ad-hoc
dict, matching how the database and query pages already work. Fields
resolved by registered extras are declared with extra_field() so their
documentation lives on the Extra classes in table_extras.py; fields
added by the view code carry help metadata next to the view.
render_template() now converts Context dataclasses shallowly instead
of via dataclasses.asdict(), which deep-copied every value and would
fail on values like sqlite3.Row.
Keys not declared on TableContext - extras requested with ?_extra= on
the HTML page, or extra filter context from filters_from_request
plugins - are now dropped from the HTML template context rather than
passed through undocumented.
Refs #2127
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Context dataclass field declared with extra_field() takes its
documentation from the description on the registered Extra of the same
name, validated against the class's extras_scope. This keeps doc
strings next to the resolve() code instead of duplicating them on the
dataclass, ahead of introducing TableContext and RowContext.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Datasette instances created with plugins_dir register their plugins on
the global plugin manager for the rest of the process, so the contract
tests could see extra_template_vars keys leaked from earlier test
modules (e.g. the session-scoped ds_client fixture). A fixture now
unregisters non-default plugins implementing extra_template_vars for
the duration of each contract test and restores them afterwards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/template_context.rst is generated by cog from the manifest in
datasette/template_contexts.py, following the json_api_doc.py pattern.
It documents the base context available on every page plus the
database, query, table and row pages, including the stability policy
for custom template authors.
Refs #2127
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
datasette/template_contexts.py is the source of truth for the template
context contract: the variables custom templates can rely on for the
database, query, table and row pages, plus the base context that
render_template() adds to every page.
Documentation for each key comes from the Context dataclass field help
(database, query), the Extra class description (table and row extras)
or inline docs in the manifest (keys added by view code).
Contract tests render each page with template_debug ?_context=1 and
assert the real context keys exactly match the documented set, in both
directions - an undocumented addition or a removed documented key both
fail.
Refs #1510, #2127
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Context dataclasses now expose documented_fields(), returning
ContextField(name, type_name, help) for each field
- ExtraRegistry.internal_classes_for_scope() returns the Extra classes
that are available to HTML templates but excluded from JSON
- Tests enforce that every registered Extra has a description and every
DatabaseContext/QueryContext field has help metadata
Refs #1510, #2127
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The HTML branch of QueryView built an empty data dict before looping
over register_output_renderer can_render callbacks, so renderers that
depend on the result columns or rows (e.g. datasette-atom,
datasette-ics) never appeared as export options for canned queries.
Populate data with the executed query's rows, columns, SQL and query
name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>