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.
Addd for Codex Desktop, which has real trouble with edits
to files like this as the in-app browser does not seem to
have a cache-busting reload option.
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>
Review of the generated ?_extra= documentation found several extras
with no example output or with examples that needed explanation:
- extras: now shows an abbreviated example of the toggle list and has
a clearer description (which also improves the live API output)
- set_column_type_ui: example of the shape seen with set-column-type
permission, plus a note that it is null otherwise
- column_types: live example generated from a table with an assigned
column type instead of an empty {}
- metadata: live table example now demonstrates a table description
and column descriptions; row and query examples gained explanatory
notes
- expandable_columns, foreign_key_tables, facets_timed_out, next_url,
renderers: notes explaining the shape of their output
Also added docs_note cross-references to the relevant documentation:
facets, pagination, render_cell and register_output_renderer plugin
hooks, column type configuration and API, metadata, custom templates,
permissions and foreign key label expansion. foreign_key_tables is
now flagged as potentially executing additional queries.
https://claude.ai/code/session_01EfjBe6E817m9XNFW7EX3Vm
Co-authored-by: Claude <noreply@anthropic.com>
asyncinject 0.7 fixed the parallel executor stalling when every
initially-ready node is a seeded value, and made seeded values take
precedence over registered functions. That lets the shared per-scope
registries receive the per-request context directly via
resolve_multi(results={'context': ...}) instead of the
contextvars.ContextVar workaround.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ul.tight-bullets li uses word-break: break-all so long facet labels can
wrap, but that also let the count number break across lines. Wrap each
count in a span.facet-count with white-space: nowrap so the label can
still wrap while the count stays on one line.
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>
_check_permission_for_actor() constructed child resources with
resource_class(database=parent, table=child), but QueryResource takes a
"query" argument, not "table", so /-/check?action=delete-query (and
view-query / update-query) raised TypeError. Construct the resource
positionally so it works for any child resource class.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
datasette --default-deny --root with no config file previously 500'd on
the instance and database index pages: rendering them computes is_private
(include_is_private=True), which references the anon_rules CTE, but that
CTE was only defined when anonymous permission rules existed.
This was fixed by the empty-anon_rules fallback added in 4b5fac9c; this
commit adds a regression test that fails without that fallback (SQLite
"no such table: anon_rules" -> 500).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
asgi_send_redirect() only collapsed leading forward slashes, so a path
like /\example.com/ produced a Location of /\example.com. Browsers
normalise backslashes to forward slashes, turning that into the
protocol-relative //example.com and redirecting off-site. Collapse any
run of leading slashes and backslashes to a single slash.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
escape_sqlite() wrapped identifiers in [brackets] without escaping any ]
characters inside the string. Since SQLite does not support escaping ]
within bracket quoting, an identifier containing ] could break out and
inject arbitrary SQL. Fall back to double-quote quoting (doubling any
embedded ") when the identifier contains ].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ExtraRegistry.resolve() previously constructed a fresh asyncinject
Registry on every table, row and query request - instantiating all
~37 Extra classes and re-running inspect.signature reflection over
each resolve method every time. The Extra classes are stateless, so
the asyncinject Registry for each scope is now built lazily once and
shared, along with the allowed-name sets.
The per-request context reaches the shared registry through a
contextvars.ContextVar provider rather than resolve_multi(results=...)
seeding: asyncinject's parallel executor never schedules anything when
the only initially-ready node is an unregistered pre-seeded value, so
seeding would have stalled every resolution. asyncio tasks copy the
caller's context, which keeps concurrent resolves isolated - covered
by a new test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frozenset({...}) was immutability ceremony for class attributes that
nothing mutates. scopes = {ExtraScope.TABLE} reads cleaner.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- TableExtraContext.next_value, RowExtraContext.resolved and
QueryExtraContext.stored_query/stored_query_write/error had no
readers - drop the fields and the arguments that populated them
- Extra.documentation() and the stable classvar were unused parallel
descriptions of what the docs generator reads directly
- ExtraRegistry.resolve no longer carries an always-true membership
guard (resolve_multi returns every requested registered name)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extras were resolved before the format dispatch, so a .csv request
carrying ?_extra= parameters paid for extras (including per-cell
render_cell plugin calls) whose results were then discarded, and the
HTML path duplicated the stored-query metadata derivation. Extras now
resolve inside the renderer-dispatch branch only, and both consumers
share a query_metadata() helper that no longer fetches database
metadata just to throw it away for stored queries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These three extras return values that exist for the HTML templates -
a Filters instance, an async function and markupsafe/sqlite3.Row data
- so requesting them on a .json page returned a 500 serialization
error, while the generated documentation and ?_extra=extras both
advertised them as API surface. They are now public=False: ignored
like any unknown name on JSON requests, omitted from the docs and the
extras list, and still resolved for the HTML view via the new
include_internal flag on ExtraRegistry.resolve().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QueryExtra re-derived named parameters from the SQL with a regex,
which missed parameters declared in a stored query's params list,
reported magic _-prefixed parameters with raw querystring values that
were never bound, and echoed the entire querystring when no SQL was
present. QueryView now passes its named_parameter_values dict - the
parameters it actually bound - through QueryExtraContext.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QueryView hardcoded private=False unless the request was for a stored
query, so /db/-/query.json?_extra=private reported false even when
execute-sql was restricted to the authenticated actor. Use
check_visibility() like the table and row views do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TableExtraContext, RowExtraContext and QueryExtraContext now share
normalized table_name, is_view, pks and query_name fields (defaulting
to None/False where inapplicable) so DebugExtra, RenderCellExtra and
RenderersExtra can read them directly. RenderCellExtra uses
context.columns in every scope - the table and row views both derive
columns from results.description so output is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pre-1.0 ?_extras= (plural) parameter was kept for backwards
compatibility with the old row JSON API. ?_extra= is the documented
mechanism now that row pages share the extras registry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
execute_isolated_fn() always opened its temporary connection with
write=True, which is not allowed for immutable databases - so APIs
that rely on it, like SQL analysis when storing a query, failed.
An immutable database can never receive writes, so there is no write
queue to block: in that case the function now opens a read-only
connection and runs it on the executor, bypassing the write thread
entirely. Mutable databases keep the existing write-thread behavior.
Also fixed a latent bug in the write thread where a connect() failure
for an isolated task would crash the thread instead of delivering the
exception back to the caller.
Closes#2768
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>