Preserve SQLite defaults in bulk insert

Omit columns that are absent from pasted bulk insert data so SQLite defaults can fire during insert. Keep preview cells blank for omitted values while preserving explicit null rendering.

Refs [https://github.com/simonw/datasette/pull/2813#issuecomment-4878063728](https://github.com/simonw/datasette/pull/2813#issuecomment-4878063728)
This commit is contained in:
Simon Willison 2026-07-03 09:45:34 -07:00
commit 4bd9d41c43
2 changed files with 66 additions and 11 deletions

View file

@ -5586,8 +5586,8 @@ function resetBulkInsertPreview(state) {
syncRowEditInsertModeUi(state);
}
function normalizeBulkInsertCell(column, value, isMissing) {
if (isMissing || typeof value === "undefined") {
function normalizeBulkInsertCell(column, value) {
if (typeof value === "undefined") {
return column.notnull ? "" : null;
}
if (value === null) {
@ -5609,11 +5609,10 @@ function rowObjectForBulkInsert(valuesByColumn, columns) {
valuesByColumn,
column.name,
);
row[column.name] = normalizeBulkInsertCell(
column,
hasValue ? valuesByColumn[column.name] : undefined,
!hasValue,
);
if (!hasValue) {
return;
}
row[column.name] = normalizeBulkInsertCell(column, valuesByColumn[column.name]);
});
return row;
}
@ -5869,8 +5868,9 @@ function renderBulkInsertPreview(state, rows) {
var tr = document.createElement("tr");
state.bulkInsertColumnDetails.forEach(function (column) {
var td = document.createElement("td");
var value = row[column.name];
td.textContent = bulkInsertPreviewValue(value);
var hasValue = Object.prototype.hasOwnProperty.call(row, column.name);
var value = hasValue ? row[column.name] : "";
td.textContent = hasValue ? bulkInsertPreviewValue(value) : "";
if (value === null) {
td.className = "row-edit-bulk-preview-null";
}

View file

@ -102,6 +102,12 @@ def write_playwright_database(db_path):
id integer primary key,
created_ms integer default (CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER))
);
create table bulk_defaults (
id integer primary key,
title text not null,
status text not null default 'todo',
score integer default 5
);
insert into projects (title, metadata, logo, notes, score) values
(
'Build Datasette',
@ -146,6 +152,11 @@ def write_playwright_config(config_path):
"alter-table": True,
},
},
"bulk_defaults": {
"permissions": {
"insert-row": True,
},
},
},
},
},
@ -279,6 +290,16 @@ def project_row(datasette_server, pk):
return rows[0]
def bulk_default_rows(datasette_server, **filters):
params = {
"_shape": "objects",
**{key: str(value) for key, value in filters.items()},
}
response = httpx.get(f"{datasette_server}data/bulk_defaults.json", params=params)
response.raise_for_status()
return response.json()["rows"]
def open_jump_menu(page):
page.keyboard.press("/")
page.locator("navigation-search .search-input").wait_for()
@ -1007,7 +1028,8 @@ def test_insert_row_flow_uses_custom_column_field(page, datasette_server):
assert "metadata" in preview_text
assert "id" not in preview_text
assert "From CSV" in preview_text
assert "null" in preview_text
assert "null" not in preview_text
assert "undefined" not in preview_text
preview_cell_style = dialog.locator(
".row-edit-bulk-preview-table td"
).first.evaluate(
@ -1097,6 +1119,38 @@ def test_bulk_insert_preview_inserts_rows(page, datasette_server):
assert project_rows(datasette_server, title="Bulk two")
@pytest.mark.playwright
def test_bulk_insert_omits_columns_absent_from_pasted_input(page, datasette_server):
page.goto(f"{datasette_server}data/bulk_defaults")
page.locator('button[data-table-action="insert-row"]').click()
dialog = page.locator("#row-edit-dialog")
dialog.wait_for()
dialog.locator(".row-edit-bulk-insert").click()
dialog.locator(".row-edit-bulk-textarea").fill("title\nOnly title")
dialog.locator(".row-edit-save").click()
assert dialog.locator(".row-edit-save").inner_text() == "Insert these rows"
preview_text = dialog.locator(".row-edit-bulk-preview-table").inner_text()
assert "Only title" in preview_text
assert "undefined" not in preview_text
dialog.locator(".row-edit-save").click()
dialog.locator(
".row-edit-bulk-progress-status", has_text="1 row inserted."
).wait_for()
rows = bulk_default_rows(datasette_server, title="Only title")
assert rows == [
{
"id": 1,
"title": "Only title",
"status": "todo",
"score": 5,
}
]
@pytest.mark.playwright
def test_bulk_insert_preview_accepts_single_column_input(page, datasette_server):
page.goto(f"{datasette_server}data/projects")
@ -1116,7 +1170,8 @@ def test_bulk_insert_preview_accepts_single_column_input(page, datasette_server)
assert "one" in preview_text
assert "two" in preview_text
assert "three" in preview_text
assert "null" in preview_text
assert "null" not in preview_text
assert "undefined" not in preview_text
@pytest.mark.playwright