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