From 7d431d3ccd84722a268ef77e48f9bb0bad76252e Mon Sep 17 00:00:00 2001 From: ciat-777 Date: Sat, 14 Mar 2026 19:25:55 +0800 Subject: [PATCH] fix(db): detect duplicate-column from nested migration error cause --- src/server/db/migrate.test.ts | 61 +++++++++++++++++++++++++++++++++++ src/server/db/migrate.ts | 26 +++++++++++++-- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/server/db/migrate.test.ts b/src/server/db/migrate.test.ts index 68ec2ef..e509598 100644 --- a/src/server/db/migrate.test.ts +++ b/src/server/db/migrate.test.ts @@ -125,4 +125,65 @@ describe('sqlite migrate bootstrap', () => { sqlite.close(); }); + + it('recovers when duplicate-column message appears only in error cause', async () => { + const dataDir = mkdtempSync(join(tmpdir(), 'metapi-migrate-recover-cause-')); + process.env.DATA_DIR = dataDir; + vi.resetModules(); + + const migrateModule = await import('./migrate.js'); + const { __migrateTestUtils } = migrateModule; + + const sqlite = new Database(':memory:'); + sqlite.exec(` + CREATE TABLE account_tokens ( + id integer PRIMARY KEY AUTOINCREMENT NOT NULL, + token_group text + ); + `); + + const tempMigrationsDir = mkdtempSync(join(tmpdir(), 'metapi-migration-files-cause-')); + mkdirSync(join(tempMigrationsDir, 'meta'), { recursive: true }); + + writeFileSync( + join(tempMigrationsDir, 'meta', '_journal.json'), + JSON.stringify({ + entries: [ + { + tag: '0007_account_token_group', + when: 1772500000001, + }, + ], + }), + ); + + writeFileSync( + join(tempMigrationsDir, '0007_account_token_group.sql'), + 'ALTER TABLE `account_tokens` ADD `token_group` text;\n', + ); + + const drizzleLikeError = { + message: "DrizzleError: Failed to run the query 'ALTER TABLE `account_tokens` ADD `token_group` text;\n'", + cause: { + message: 'SqliteError: duplicate column name: token_group', + }, + }; + + const recovered = __migrateTestUtils.tryRecoverDuplicateColumnMigrationError( + sqlite, + tempMigrationsDir, + drizzleLikeError, + ); + + expect(recovered).toBe(true); + + const applied = sqlite + .prepare('SELECT hash, created_at FROM __drizzle_migrations') + .all() as Array<{ hash: string; created_at: number }>; + + expect(applied).toHaveLength(1); + expect(Number(applied[0].created_at)).toBe(1772500000001); + + sqlite.close(); + }); }); diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts index d74d336..0359658 100644 --- a/src/server/db/migrate.ts +++ b/src/server/db/migrate.ts @@ -197,9 +197,31 @@ function markMigrationRecordIfMissing(sqlite: Database.Database, record: Migrati } function normalizeSchemaErrorMessage(error: unknown): string { - if (typeof error === 'object' && error && 'message' in error) { - return String((error as { message?: unknown }).message || ''); + if (!error || typeof error !== 'object') { + return String(error || ''); } + + const collected: string[] = []; + let cursor: unknown = error; + let depth = 0; + + while (cursor && typeof cursor === 'object' && depth < 8) { + const current = cursor as { message?: unknown; cause?: unknown }; + if (current.message !== undefined && current.message !== null) { + const text = String(current.message).trim(); + if (text.length > 0) { + collected.push(text); + } + } + + cursor = current.cause; + depth += 1; + } + + if (collected.length > 0) { + return collected.join(' | '); + } + return String(error || ''); }