From 3d4ea80fe2c14f51cd65b141f850a03696f5cf5c Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Tue, 25 Aug 2026 17:18:27 +0200 Subject: [PATCH] fix: never rerun SQL whose applied state is unknown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `unknown` covers two different unknowns, and this treated them as one. The schema could not be read, or the SQL names no table `expectedTables` can resolve — and the second is arbitrary published SQL, which may carry a non-idempotent INSERT or ALTER. The row offered "Run migrations" and the footer claimed rerunning was safe; both were claims this code cannot make. An unknown row now offers "Check again", which re-reads and executes nothing. That settles the case which actually recovers — a database briefly unreachable — and leaves Skip, which states the uncertainty, as the way past one that does not. The partitions behind the copy also missed `failed` rows entirely: the footer rendered a title with no body, and Skip described them as unreadable. Both now group by what it costs the project — tables that are missing (never created, or a migration that failed) against tables that could not be verified — which is also what makes the sentences true: a failed row is configured, so "this data table does not exist yet" was wrong about it. Skip and the footer now read the same partition instead of each computing one, so they cannot disagree again. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx --- .../src/lib/components/ImportSetupStep.svelte | 82 +++++++++++++------ 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/frontend/src/lib/components/ImportSetupStep.svelte b/frontend/src/lib/components/ImportSetupStep.svelte index f1b4662764..ef44bf9ce1 100644 --- a/frontend/src/lib/components/ImportSetupStep.svelte +++ b/frontend/src/lib/components/ImportSetupStep.svelte @@ -84,7 +84,13 @@ // Split because the two say different things to the user: one data table was never // created, the other exists and could not be read. Telling someone to set up what they // have already set up is how a warning stops being believed. - const unmadeTables = $derived(rows.filter((r) => r.status === 'unconfigured')) + // Grouped by what it costs the project, not by row status. A row whose migrations failed is + // configured, but its tables are as absent as one that was never created and the remedy is + // the same — so they share a message. An `unknown` row is the odd one: it is set up, and + // whether its tables are there is precisely what could not be established. + const missingTables = $derived( + rows.filter((r) => r.status === 'unconfigured' || r.status === 'failed') + ) const uncheckedTables = $derived(rows.filter((r) => r.status === 'unknown')) /** Rows the user has not dealt with, of either kind. */ const outstanding = $derived(pendingTables.length + blanks.filter((b) => !b.done).length) @@ -360,26 +366,38 @@ // hub export. A hub is not ours — `hub_base_url` is an instance setting and the // wizard can be pointed at any of them — so a name carrying an event-bearing // element would otherwise run script in this authenticated origin. - const names = pendingTables.map((r) => escapeHtml(r.name)).join(', ') - const one = pendingTables.length === 1 - // An `unknown` row is set up — only its schema could not be read — so it cannot be - // told it is "not set up". Where the list mixes the two, say the weaker thing that - // is true of both rather than the stronger one that is false of half. - const anyUnmade = unmadeTables.length > 0 - const confirmed = await confirmationModal.ask({ - title: anyUnmade ? 'The project will not run' : 'This has not been verified', - confirmationText: 'Skip anyway', - type: anyUnmade ? 'danger' : 'info', - children: anyUnmade - ? `${one ? 'The data table' : 'The data tables'} ${names} ` + - `${one ? 'is' : 'are'} not set up, so the tables this project's apps and flows read ` + - `do not exist. Every one of them will fail as soon as it opens.

` + + // One block per outcome, the way the footer alert does it. A single sentence over a + // mixed list has to be wrong about half of it: an `unknown` data table is set up — + // only its schema could not be read — so naming it under "not set up" tells the user + // to do something they have already done. + const missing = missingTables + const unverified = uncheckedTables + const listOf = (rs: Row[]) => rs.map((r) => escapeHtml(r.name)).join(', ') + const blocks: string[] = [] + if (missing.length > 0) { + const one = missing.length === 1 + blocks.push( + `The tables ${one ? 'the data table' : 'the data tables'} ${listOf(missing)} ` + + `${one ? 'holds' : 'hold'} do not exist, and this project's apps and flows read ` + + `them. Every one of those fails as soon as it opens.

` + `Setting ${one ? 'it' : 'them'} up later from workspace settings creates the ` + `connection but not the tables — only this step runs the project's migration.` - : `${one ? 'The data table' : 'The data tables'} ${names} ` + + ) + } + if (unverified.length > 0) { + const one = unverified.length === 1 + blocks.push( + `${one ? 'The data table' : 'The data tables'} ${listOf(unverified)} ` + `${one ? 'is' : 'are'} set up, but ${one ? 'its' : 'their'} schema could not be ` + `read, so whether this project's tables exist is unknown. Its apps and flows will ` + `fail wherever they query a table that is missing.` + ) + } + const confirmed = await confirmationModal.ask({ + title: missing.length > 0 ? 'The project will not run' : 'This has not been verified', + confirmationText: 'Skip anyway', + type: missing.length > 0 ? 'danger' : 'info', + children: blocks.join('

') }) if (!confirmed) return } @@ -501,7 +519,22 @@ - {#if hasTable && row.status !== 'done' && row.status !== 'running'} + {#if row.status === 'unknown'} + + + {:else if hasTable && row.status !== 'done' && row.status !== 'running'}