From ad074bd405ded4da2c6f026276445493f24fa3df Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Sat, 20 Jun 2026 16:46:23 +0200 Subject: [PATCH] feat: generate down migrations for db manager DDL (postgres) --- frontend/src/lib/components/DBManager.svelte | 4 +- frontend/src/lib/components/dbOps.ts | 51 +++++++++++++++----- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/frontend/src/lib/components/DBManager.svelte b/frontend/src/lib/components/DBManager.svelte index 46177bc494..8669fa76bb 100644 --- a/frontend/src/lib/components/DBManager.svelte +++ b/frontend/src/lib/components/DBManager.svelte @@ -599,7 +599,9 @@ onConfirm={async ({ values }) => { if (dbTableEditorState.alterTableKey && dbTableEditorAlterTableData.current) { let diff = diffTableEditorValues(dbTableEditorAlterTableData.current, values) - await dbSchemaOps.onAlter({ schema: selected.schemaKey, values: diff }) + // Reverse diff (new → old) so the migration's down undoes the alter. + let reverse = diffTableEditorValues(values, dbTableEditorAlterTableData.current) + await dbSchemaOps.onAlter({ schema: selected.schemaKey, values: diff, reverse }) } else { await dbSchemaOps.onCreate({ values, schema: selected.schemaKey }) } diff --git a/frontend/src/lib/components/dbOps.ts b/frontend/src/lib/components/dbOps.ts index ca56d8dbdd..cbc4985f96 100644 --- a/frontend/src/lib/components/dbOps.ts +++ b/frontend/src/lib/components/dbOps.ts @@ -128,7 +128,12 @@ export type IDbSchemaOps = { onDelete: (params: { tableKey: string; schema?: string }) => Promise onCreate: (params: { values: TableEditorValues; schema?: string }) => Promise previewCreateSql: (params: { values: TableEditorValues; schema?: string }) => Promise - onAlter: (params: { values: AlterTableValues; schema?: string }) => Promise + onAlter: (params: { + values: AlterTableValues + /** Reverse diff (new → old), used to generate the down migration. */ + reverse?: AlterTableValues + schema?: string + }) => Promise previewAlterSql: (params: { values: AlterTableValues; schema?: string }) => Promise onCreateSchema: (params: { schema: string }) => Promise onDeleteSchema: (params: { schema: string }) => Promise @@ -171,10 +176,18 @@ export function dbSchemaOpsWithPreviewScripts({ return safe ? `${op}_${safe}_${slug}` : `${op}_${slug}` } + // Frame a single (or multi-) statement body in an explicit, `;`-terminated + // transaction, matching the data table migration convention. + function wrapMigration(sql: string): string { + const t = sql.trimEnd() + return `BEGIN;\n\n${t.endsWith(';') ? t : `${t};`}\n\nEND;` + } + // Apply a DDL marker. For a data table that has migrations enabled this // creates a migration and runs it (rolling the record back if the run fails); - // otherwise it runs ad-hoc via the internal-db job as before. - async function applyDdl(migName: string, content: string): Promise { + // otherwise it runs ad-hoc via the internal-db job as before. `downContent`, + // when provided, is expanded into the migration's down SQL (Postgres only). + async function applyDdl(migName: string, content: string, downContent?: string): Promise { let migrationsEnabled = false if (datatableName) { try { @@ -191,12 +204,17 @@ export function dbSchemaOpsWithPreviewScripts({ await runScriptAndPollResult({ workspace, requestBody: { args: dbArg, content, language } }) return } - const sql = (await expandMarker(workspace, language, content)).trimEnd() - const body = sql.endsWith(';') ? sql : `${sql};` + const codeUp = wrapMigration(await expandMarker(workspace, language, content)) + // Down migrations are only generated for Postgres for now. + let codeDown: string | undefined + if (downContent && dbType === 'postgresql') { + const downSql = (await expandMarker(workspace, language, downContent)).trim() + if (downSql) codeDown = wrapMigration(downSql) + } const created = await WorkspaceService.createDatatableMigration({ workspace, datatableName, - requestBody: { name: migName, code_up: `BEGIN;\n\n${body}\n\nEND;` } + requestBody: { name: migName, code_up: codeUp, ...(codeDown ? { code_down: codeDown } : {}) } }) try { await WorkspaceService.runDatatableMigrations({ @@ -226,7 +244,8 @@ export function dbSchemaOpsWithPreviewScripts({ foreignKeys: values.foreignKeys, schema }) - await applyDdl(migrationName('create', values.name), content) + const downContent = makeMarker('DROP_TABLE', { table: values.name, schema }) + await applyDdl(migrationName('create', values.name), content, downContent) }, previewCreateSql: async ({ values, schema }) => { const content = makeMarker('CREATE_TABLE', { @@ -237,13 +256,21 @@ export function dbSchemaOpsWithPreviewScripts({ }) return expandMarker(workspace, language, content) }, - onAlter: async ({ values, schema }) => { + onAlter: async ({ values, reverse, schema }) => { const content = makeMarker('ALTER_TABLE', { name: values.name, operations: values.operations, schema }) - await applyDdl(migrationName('alter', values.name), content) + // The down is the same alter run in the opposite direction. + const downContent = reverse + ? makeMarker('ALTER_TABLE', { + name: reverse.name, + operations: reverse.operations, + schema + }) + : undefined + await applyDdl(migrationName('alter', values.name), content, downContent) }, previewAlterSql: async ({ values, schema }) => { const content = makeMarker('ALTER_TABLE', { @@ -255,11 +282,13 @@ export function dbSchemaOpsWithPreviewScripts({ }, onCreateSchema: async ({ schema }) => { const content = makeMarker('CREATE_SCHEMA', { schema }) - await applyDdl(migrationName('create_schema', schema), content) + const downContent = makeMarker('DROP_SCHEMA', { schema }) + await applyDdl(migrationName('create_schema', schema), content, downContent) }, onDeleteSchema: async ({ schema }) => { const content = makeMarker('DROP_SCHEMA', { schema }) - await applyDdl(migrationName('drop_schema', schema), content) + const downContent = makeMarker('CREATE_SCHEMA', { schema }) + await applyDdl(migrationName('drop_schema', schema), content, downContent) }, onFetchTableEditorDefinition: async ({ table, schema, colDefs }) => { let foreignKeys: import('./apps/components/display/dbtable/tableEditor').TableEditorForeignKey[] =