Files
WRNexusJS/packages/db/src/migration-safety.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

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);
}