Create table, alter table - APIs and modals#2789
Conversation
| dialog.table-create-dialog { | ||
| --ink: #0f0f0f; | ||
| --paper: #eef6ff; | ||
| --muted: #6b6b6b; | ||
| --rule: #d8e6f5; | ||
| --accent: #1a56db; | ||
| --card: #ffffff; | ||
| border: none; | ||
| border-radius: var(--modal-border-radius, 0.75rem); | ||
| padding: 0; | ||
| margin: auto; | ||
| width: min(760px, calc(100vw - 32px)); | ||
| max-width: 95vw; | ||
| max-height: min(780px, calc(100vh - 32px)); | ||
| box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)); | ||
| animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out; | ||
| overflow: hidden; | ||
| font-family: system-ui, -apple-system, sans-serif; | ||
| background: var(--card); | ||
| } | ||
|
|
||
| dialog.table-create-dialog[open] { | ||
| display: flex; | ||
| flex-direction: column; | ||
| } |
There was a problem hiding this comment.
This is consistent with our other dialogs but there's a whole lot of duplicate model code now. Filing an issue to clean that up later:
|
Pyodide tests are failing with:
That shouldn't be an issue now, see: https://simonwillison.net/2026/Jun/13/publishing-wasm-wheels/ Might need to upgrade Pyodide (here and in Datasette Lite). |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2789 +/- ##
======================================
Coverage 0.00% 0.00%
======================================
Files 70 72 +2
Lines 11183 11789 +606
======================================
- Misses 11183 11789 +606 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Now that we depend on pydantic we need a more recent pyodide in order to load the emscripten build of pydantic-core. Refs #2789 (comment)
|
There's a feature missing from both create and alter: foreign key constraints. These are worth supporting, particularly since the edit/insert modals have neat support for them. Related to that: setting the "label column" for a table. Might need a larger issue for that since we need to store label columns somewhere, probably in the same place as custom column types. |
- Add fk_table and optional fk_column support to create-table columns. - Validate create-table requests with Pydantic while preserving existing errors. - Document the API and cover inferred primary-key and validation cases. Refs #2789 (comment)
2930f7a to
fbab41f
Compare
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.
- 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
- Register a built-in table action and expose alter-table metadata to table pages. - Build the client-side modal for editing columns, defaults, ordering, primary keys, and custom column types. - Add a review/apply confirmation flow with HTML and Playwright coverage. Refs #2788
- Use a per-process socket path for the UDS test fixture. - Clean up stale socket files before and after the fixture runs. - Close the HTTP client and wait for the Datasette subprocess to exit.
- Extract reusable helpers for database and table action permission preloading. - Precompute those permissions before building table-page HTML data. - Document the default table actions plugin.
Now that we depend on pydantic we need a more recent pyodide in order to load the emscripten build of pydantic-core. Refs #2789 (comment)
- Move create-table and alter-table API views into table_create_alter.py. - Keep create and alter schema-editing constants and helpers together. - Rename the create table modal context helper.
- Add fk_table and optional fk_column support to create-table columns. - Validate create-table requests with Pydantic while preserving existing errors. - Document the API and cover inferred primary-key and validation cases. Refs #2789 (comment)
- Add add_foreign_key, drop_foreign_key, and set_foreign_keys operations. - Validate flat fk_table and fk_column arguments with Pydantic. - Document the API and cover inferred primary-key and validation cases.
Improved version of the implementation datasette-edit-schema
Returns a list of tables with a single primary key, and for each one the name of that primary key column and its SQLite type affinity. This will be used by the create table UI to suggest foreign keys.
- Add foreignKeyTargetsPath to create table page data - Filter hidden tables from database-level foreign key target results - Update JSON API docs and tests for filtered targets
- Add create table advanced controls for foreign keys and first-column primary keys - Share schema dialog row helpers between create and alter dialogs - Move custom type into advanced options and add Add column icons
In the create table dialog a column can now have either a custom display type or a foreign key target, but not both - a foreign key column's type is determined by the referenced primary key, so a custom type doesn't apply. Setting one clears and disables the other, and the foreign key select stays disabled on the primary key column and when no targets exist. Also add "Controls how Datasette displays and edits this column" help text (with aria-describedby) under the custom type selector in both the create and alter dialogs, and style the alter dialog help text.
d6a826e to
c4aead6
Compare
It works by doing conn.backup(memory_conn) which could use a lot of memory for a large database.
| sqlite_type: column_type | ||
| for column_type, sqlite_type in CREATE_TABLE_SQLITE_TYPES.items() | ||
| } | ||
| TABLE_NAME_RE = re.compile(r"^(?!sqlite_)[^\n]+$") |
There was a problem hiding this comment.
Because SQLite preserves that prefix for internal use:
python3 << 'EOF'
import sqlite3
conn = sqlite3.connect(":memory:")
cur = conn.cursor()
# Attempt 1: plain CREATE TABLE with the reserved-looking name
try:
cur.execute("CREATE TABLE sqlite_misx (id INTEGER PRIMARY KEY, name TEXT)")
print("Attempt 1 (plain CREATE TABLE): SUCCESS")
except sqlite3.Error as e:
print(f"Attempt 1 (plain CREATE TABLE): FAILED -> {type(e).__name__}: {e}")
conn.close()
EOF| function sqliteColumnTypeLabel(type) { | ||
| if (type === "float") { | ||
| return "floating point number"; | ||
| } | ||
| if (type === "real") { | ||
| return "floating point number"; | ||
| } | ||
| if (type === "blob") { | ||
| return "blob - binary data"; | ||
| } | ||
| return type; | ||
| } |
There was a problem hiding this comment.
I'm going to make this an object instead.
| "table_page_data": await _table_page_data( | ||
| self.ds, | ||
| request, | ||
| db, | ||
| database, | ||
| table, | ||
| not is_table, | ||
| None, | ||
| None, | ||
| ), |
|
Alter table doesn't currently let you set foreign keys. Create table doesn't let you set a default expression. I had Codex review them for other differences:
Need to unify the code and features before landing this. |
|
Let's hide the default value stuff in a summary/details thing on both UIs - change text to "or default to a specific value" - and in the expression menu extend it to say things like "Current timestamp in uTC, e.g. 2026-05-01 13:34:00". |
So we don't get test failures from reformatted SQL.
| DEFAULT_EXPR_SQL = { | ||
| "current_timestamp": "CURRENT_TIMESTAMP", | ||
| "current_date": "CURRENT_DATE", | ||
| "current_time": "CURRENT_TIME", | ||
| } |
There was a problem hiding this comment.
Let's add unix timestamp options here as well.
There was a problem hiding this comment.
current_unixtime- integer seconds since the epochcurrent_unixtime_ms- integer ms since the epoch
|
If you sort by a column, then rename that column using alter table, you get redirected back to a page with a 400 error saying Instead it should remove that |
|
The alter table form needs to let you rename a table. |
|
In create table in the foreign key list we should show ALL foreign keys, not just the ones that match the selected SQLite column type - then if the user picks a foreign key that is of a different column type we should switch the column type in that select box to match the selected foreign key. |
|
When selecting a foreign key column, if the column name has not yet been set, set the column name to |
|
Here's a video of everything so far: CleanShot.2026-06-22.at.12.27.25.mp4 |
|
The video shows that in macOS Safari the select boxes are not the same height as the input boxes. |
Include current foreign key metadata in the alter table page data and allow the foreign-key-targets endpoint to be read by actors with alter-table permission for a specific table. Add API and HTML data tests for the new alter-table foreign key support.
Share default value controls between the create and alter table dialogs and expose create-table default expressions to the frontend. Add create-table not-null/default handling and align the shared foreign key picker behavior across both dialogs.
Add a collapsed rename-table section to the alter table modal and include rename_table operations in the review/apply flow. Redirect to the renamed table URL after applying changes and cover the review text in Playwright.
Plus tweaked how alter table changing those works a bit.


Refs:
Video demo (before a few final cosmetic fixes):
CleanShot.2026-06-22.at.12.27.25.mp4
📚 Documentation preview 📚: https://datasette--2789.org.readthedocs.build/en/2789/