forked from boos/BoosAPI
Merge pull request #85 from cita-777/fix/render-generated-assets
fix: make external db bootstrap idempotent
This commit is contained in:
@@ -38,4 +38,79 @@ describe('runtime schema bootstrap', () => {
|
||||
|
||||
expect(executedSql.slice(0, expectedBootstrapSql.length)).toEqual(expectedBootstrapSql);
|
||||
});
|
||||
|
||||
it('ignores duplicate mysql index errors when replaying bootstrap statements', async () => {
|
||||
const executedSql: string[] = [];
|
||||
const targetSql = 'CREATE UNIQUE INDEX `model_availability_account_model_unique` ON `model_availability` (`account_id`, `model_name`(191))';
|
||||
|
||||
await ensureRuntimeDatabaseSchema({
|
||||
...createStubClient('mysql', executedSql),
|
||||
execute: async (sqlText: string) => {
|
||||
executedSql.push(sqlText);
|
||||
if (sqlText === targetSql) {
|
||||
const error = new Error("Duplicate key name 'model_availability_account_model_unique'") as Error & { code?: string };
|
||||
error.code = 'ER_DUP_KEYNAME';
|
||||
throw error;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
expect(executedSql).toContain(targetSql);
|
||||
});
|
||||
|
||||
it('replays the mysql bootstrap against an already-initialized schema', async () => {
|
||||
const executedSql: string[] = [];
|
||||
const createdStatements = new Set<string>();
|
||||
|
||||
const mysqlClient = createStubClient('mysql', executedSql);
|
||||
mysqlClient.execute = async (sqlText: string) => {
|
||||
executedSql.push(sqlText);
|
||||
const normalized = sqlText.trim().toLowerCase();
|
||||
const createsSchemaObject = normalized.startsWith('create table if not exists')
|
||||
|| normalized.startsWith('create index')
|
||||
|| normalized.startsWith('create unique index');
|
||||
|
||||
if (createsSchemaObject) {
|
||||
if (createdStatements.has(sqlText)) {
|
||||
const error = new Error(
|
||||
normalized.startsWith('create table')
|
||||
? 'Table already exists'
|
||||
: 'Duplicate key name during bootstrap replay',
|
||||
) as Error & { code?: string };
|
||||
error.code = normalized.startsWith('create table') ? 'ER_TABLE_EXISTS_ERROR' : 'ER_DUP_KEYNAME';
|
||||
throw error;
|
||||
}
|
||||
createdStatements.add(sqlText);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
await ensureRuntimeDatabaseSchema(mysqlClient);
|
||||
await ensureRuntimeDatabaseSchema(mysqlClient);
|
||||
|
||||
expect(executedSql.length).toBeGreaterThan(createdStatements.size);
|
||||
expect(createdStatements.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('ignores postgres relation-already-exists errors when replaying bootstrap statements', async () => {
|
||||
const executedSql: string[] = [];
|
||||
const targetSql = 'CREATE UNIQUE INDEX "model_availability_account_model_unique" ON "model_availability" ("account_id", "model_name")';
|
||||
|
||||
await ensureRuntimeDatabaseSchema({
|
||||
...createStubClient('postgres', executedSql),
|
||||
execute: async (sqlText: string) => {
|
||||
executedSql.push(sqlText);
|
||||
if (sqlText === targetSql) {
|
||||
const error = new Error('relation "model_availability_account_model_unique" already exists') as Error & { code?: string };
|
||||
error.code = '42P07';
|
||||
throw error;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
expect(executedSql).toContain(targetSql);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,38 @@ export interface RuntimeSchemaConnectionInput {
|
||||
ssl?: boolean;
|
||||
}
|
||||
|
||||
function normalizeSchemaErrorMessage(error: unknown): string {
|
||||
if (typeof error === 'object' && error && 'message' in error) {
|
||||
return String((error as { message?: unknown }).message || '');
|
||||
}
|
||||
return String(error || '');
|
||||
}
|
||||
|
||||
function isExistingSchemaObjectError(error: unknown): boolean {
|
||||
const lowered = normalizeSchemaErrorMessage(error).toLowerCase();
|
||||
const code = typeof error === 'object' && error && 'code' in error
|
||||
? String((error as { code?: unknown }).code || '')
|
||||
: '';
|
||||
|
||||
return code === 'ER_DUP_KEYNAME'
|
||||
|| code === 'ER_TABLE_EXISTS_ERROR'
|
||||
|| code === '42P07'
|
||||
|| code === '42710'
|
||||
|| lowered.includes('already exists')
|
||||
|| lowered.includes('duplicate key name')
|
||||
|| lowered.includes('relation') && lowered.includes('already exists');
|
||||
}
|
||||
|
||||
async function executeBootstrapStatement(client: RuntimeSchemaClient, sqlText: string): Promise<void> {
|
||||
try {
|
||||
await client.execute(sqlText);
|
||||
} catch (error) {
|
||||
if (!isExistingSchemaObjectError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateIdentifier(identifier: string): string {
|
||||
if (!/^[a-z_][a-z0-9_]*$/i.test(identifier)) {
|
||||
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
||||
@@ -212,7 +244,7 @@ export async function ensureRuntimeDatabaseSchema(client: RuntimeSchemaClient):
|
||||
: readGeneratedBootstrapStatements(client.dialect);
|
||||
|
||||
for (const sqlText of statements) {
|
||||
await client.execute(sqlText);
|
||||
await executeBootstrapStatement(client, sqlText);
|
||||
}
|
||||
|
||||
await ensureLegacySchemaCompatibility(createLegacySchemaInspector(client));
|
||||
|
||||
Reference in New Issue
Block a user