Skip to content

Create table, alter table - APIs and modals#2789

Merged
simonw merged 31 commits into
mainfrom
codex/alter-table-modal
Jun 22, 2026
Merged

Create table, alter table - APIs and modals#2789
simonw merged 31 commits into
mainfrom
codex/alter-table-modal

Conversation

@simonw

@simonw simonw commented Jun 17, 2026

Copy link
Copy Markdown
Owner

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/

Comment thread datasette/static/app.css
Comment on lines +1752 to +1776
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;
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

@simonw

simonw commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

Pyodide tests are failing with:

ValueError: Couldn't find a pure Python 3 wheel for 'pydantic-core==2.46.4'. You can use micropip.install(..., keep_going=True) to get a list of all packages with missing wheels.

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

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 744 lines in your changes missing coverage. Please review.
✅ Project coverage is 0.00%. Comparing base (57e7bba) to head (b3b5c25).
⚠️ Report is 33 commits behind head on main.

Files with missing lines Patch % Lines
datasette/views/table_create_alter.py 0.00% 670 Missing ⚠️
datasette/views/table.py 0.00% 45 Missing ⚠️
datasette/default_table_actions.py 0.00% 12 Missing ⚠️
datasette/views/database.py 0.00% 7 Missing ⚠️
datasette/views/table_extras.py 0.00% 6 Missing ⚠️
datasette/app.py 0.00% 4 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

simonw added a commit that referenced this pull request Jun 17, 2026
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)
@simonw

simonw commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

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.

@simonw simonw marked this pull request as ready for review June 17, 2026 18:37
simonw added a commit that referenced this pull request Jun 17, 2026
- 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)
@simonw simonw force-pushed the codex/alter-table-modal branch from 2930f7a to fbab41f Compare June 17, 2026 21:03
simonw added 21 commits June 22, 2026 10:11
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.
@simonw simonw force-pushed the codex/alter-table-modal branch from d6a826e to c4aead6 Compare June 22, 2026 17:13
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]+$")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +63 to +74
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;
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to make this an object instead.

Comment thread datasette/views/row.py
Comment on lines 207 to 216
"table_page_data": await _table_page_data(
self.ds,
request,
db,
database,
table,
not is_table,
None,
None,
),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard to read

@simonw

simonw commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

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:

I compared the running forms and checked the shared JS.

Create Table

  • Creates a brand new table; has a Table name field.
  • Starts with id integer primary key plus one blank text column.
  • Per column: name, SQLite type, move/remove, advanced options.
  • Advanced options: custom Datasette column type, foreign key target, primary key.
  • Foreign keys are exposed in the UI, but only for non-first columns.
  • Primary key UI is effectively first-column-only.
  • Submits immediately with Create table; no review step.
  • No UI for not null, default value, or default expression.

Alter Table

  • Edits an existing table; no table name field.
  • Preloads every existing column.
  • Per column: rename, change SQLite type, reorder, drop/remove, advanced options.
  • Advanced options: custom Datasette column type, Not null, default value, default expression, primary key.
  • Can add new columns and change/drop/reorder existing ones.
  • Has Review changes then Apply changes, with a warning for dropped columns.
  • Includes Drop table in the same modal.
  • No foreign-key editing control in the form right now.

So the big asymmetry is: create has foreign-key selection, alter has defaults/not-null/review/drop-table. They share the column row mechanics, type/custom-type controls, move behavior, and primary-key ordering rules.

Need to unify the code and features before landing this.

@simonw

simonw commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

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".

Comment on lines +311 to +315
DEFAULT_EXPR_SQL = {
"current_timestamp": "CURRENT_TIMESTAMP",
"current_date": "CURRENT_DATE",
"current_time": "CURRENT_TIME",
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add unix timestamp options here as well.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • current_unixtime - integer seconds since the epoch
  • current_unixtime_ms - integer ms since the epoch

@simonw

simonw commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

Found this sqlite-utils bug while trying this out:

CleanShot 2026-06-22 at 11 49 38@2x

I'm going to fix that in sqlite-utils rather than working around it here.

@simonw

simonw commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

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 Cannot sort table by old_name_of_column.

Instead it should remove that _sort= before the redirect.

@simonw

simonw commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

The alter table form needs to let you rename a table.

@simonw

simonw commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

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.

@simonw

simonw commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

When selecting a foreign key column, if the column name has not yet been set, set the column name to table_id e.g. if the foreign key is to pypi_versions.id then the column name should be set to pypi_versions_id.

@simonw

simonw commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

Here's a video of everything so far:

CleanShot.2026-06-22.at.12.27.25.mp4

@simonw

simonw commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

The video shows that in macOS Safari the select boxes are not the same height as the input boxes.

@simonw

simonw commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

That fixed the Safari select problem:

CleanShot 2026-06-22 at 12 32 10@2x

simonw added 5 commits June 22, 2026 12:47
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.
@simonw simonw merged commit f0645c6 into main Jun 22, 2026
33 of 43 checks passed
@simonw simonw deleted the codex/alter-table-modal branch June 22, 2026 20:54
simonw added a commit that referenced this pull request Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant