108 lines
3.1 KiB
TypeScript
108 lines
3.1 KiB
TypeScript
import type { Migration } from "./migrate.ts";
|
|
|
|
export interface MigrationSafetyIssue {
|
|
code:
|
|
| "WRN-DB-DROP-TABLE"
|
|
| "WRN-DB-DROP-COLUMN"
|
|
| "WRN-DB-RENAME"
|
|
| "WRN-DB-TYPE-CHANGE"
|
|
| "WRN-DB-SET-NOT-NULL"
|
|
| "WRN-DB-ADD-REQUIRED"
|
|
| "WRN-DB-BLOCKING-INDEX";
|
|
severity: "error" | "warning";
|
|
migration: string;
|
|
statement: string;
|
|
recommendation: string;
|
|
}
|
|
|
|
function statements(sql: string): string[] {
|
|
return sql
|
|
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
.split(";")
|
|
.map((statement) =>
|
|
statement
|
|
.replace(/--[^\r\n]*/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim(),
|
|
)
|
|
.filter(Boolean);
|
|
}
|
|
|
|
export function analyzeMigrationSafety(migration: Migration): MigrationSafetyIssue[] {
|
|
const issues: MigrationSafetyIssue[] = [];
|
|
const add = (
|
|
code: MigrationSafetyIssue["code"],
|
|
severity: MigrationSafetyIssue["severity"],
|
|
statement: string,
|
|
recommendation: string,
|
|
): void => {
|
|
issues.push({ code, severity, migration: migration.name, statement, recommendation });
|
|
};
|
|
for (const statement of statements(migration.up)) {
|
|
if (/\bDROP\s+TABLE\b/i.test(statement))
|
|
add(
|
|
"WRN-DB-DROP-TABLE",
|
|
"error",
|
|
statement,
|
|
"Deprecate reads/writes first; drop in a later contract release.",
|
|
);
|
|
if (
|
|
/\bDROP\s+(?:COLUMN\s+)?[A-Za-z_][\w$]*/i.test(statement) &&
|
|
/\bALTER\s+TABLE\b/i.test(statement)
|
|
)
|
|
add(
|
|
"WRN-DB-DROP-COLUMN",
|
|
"error",
|
|
statement,
|
|
"Stop all old-version reads before a separate contract migration.",
|
|
);
|
|
if (/\bRENAME\s+(?:COLUMN\s+)?\b|\bRENAME\s+TO\b/i.test(statement))
|
|
add(
|
|
"WRN-DB-RENAME",
|
|
"error",
|
|
statement,
|
|
"Add the new name, dual-write/backfill, switch readers, then remove the old name.",
|
|
);
|
|
if (
|
|
/\bALTER\s+(?:COLUMN\s+)?[A-Za-z_][\w$]*\s+(?:TYPE|SET\s+DATA\s+TYPE)\b|\bMODIFY\s+(?:COLUMN\s+)?[A-Za-z_]/i.test(
|
|
statement,
|
|
)
|
|
)
|
|
add(
|
|
"WRN-DB-TYPE-CHANGE",
|
|
"error",
|
|
statement,
|
|
"Add a compatible column and backfill before switching readers.",
|
|
);
|
|
if (/\bALTER\s+(?:COLUMN\s+)?[A-Za-z_][\w$]*\s+SET\s+NOT\s+NULL\b/i.test(statement))
|
|
add(
|
|
"WRN-DB-SET-NOT-NULL",
|
|
"error",
|
|
statement,
|
|
"Backfill and validate existing rows before enforcing NOT NULL.",
|
|
);
|
|
if (
|
|
/\bADD\s+(?:COLUMN\s+)?[A-Za-z_][\w$]*[\s\S]*\bNOT\s+NULL\b/i.test(statement) &&
|
|
!/\bDEFAULT\b/i.test(statement)
|
|
)
|
|
add(
|
|
"WRN-DB-ADD-REQUIRED",
|
|
"error",
|
|
statement,
|
|
"Add nullable, backfill in batches, then enforce the constraint.",
|
|
);
|
|
if (/^CREATE\s+(?:UNIQUE\s+)?INDEX\b/i.test(statement) && !/\bCONCURRENTLY\b/i.test(statement))
|
|
add(
|
|
"WRN-DB-BLOCKING-INDEX",
|
|
"warning",
|
|
statement,
|
|
"Use an online/concurrent index operation when the database supports it.",
|
|
);
|
|
}
|
|
return issues;
|
|
}
|
|
|
|
export function analyzeMigrations(migrations: readonly Migration[]): MigrationSafetyIssue[] {
|
|
return migrations.flatMap(analyzeMigrationSafety);
|
|
}
|