validateMigrations function
Validates local ordering, checksum links and operations without opening a database.
Implementation
void validateMigrations(
List<Migration> migrations, {
required SqlDialect dialect,
}) {
String? last;
String? previousChecksum;
for (final migration in migrations) {
if (last != null && last.compareTo(migration.id) >= 0) {
throw const OrmException(
'MIGRATION.ORDER',
'Migration IDs must be unique and strictly ordered.',
);
}
last = migration.id;
if (migration.previous != null && migration.previous != previousChecksum) {
throw const OrmException(
'MIGRATION.CHAIN',
'Migration previous-checksum chain is broken.',
);
}
previousChecksum = migration.checksum;
if (migration.dialect != dialect) {
throw OrmException(
'MIGRATION.TARGET',
'${migration.id} targets ${migration.dialect.name}, but this history requires ${dialect.name}.',
);
}
if (isMysqlFamily(dialect) && migration.id.length > 191) {
throw const OrmException(
'MIGRATION.ID',
'MySQL migration IDs must not exceed 191 characters.',
);
}
for (final step in migration.steps) {
if (step is CheckedTableSql) {
if (!isMysqlFamily(dialect)) {
throw const OrmException(
'MIGRATION.TARGET',
'CheckedTableSql requires MySQL or MariaDB.',
);
}
for (final table in [
step.before,
step.after,
].whereType<TableSchema>()) {
validateSchema([table], dialect);
}
_validateSql(step.sql, transactional: false);
validateMysqlStatement(step.sql);
if (!{
'CREATE',
'ALTER',
'DROP',
'RENAME',
}.contains(sqlWords(step.sql).first)) {
throw const OrmException(
'MIGRATION.DDL',
'CheckedTableSql must contain a reviewed table DDL statement.',
);
}
continue;
}
if (step is Backfill) {
validateSchema([step.table], dialect);
continue;
}
if (step is CheckedSql) {
if (dialect != SqlDialect.postgres) {
throw const OrmException(
'MIGRATION.TARGET',
'Recoverable autocommit migrations currently require PostgreSQL.',
);
}
_validateSql(step.sql, transactional: false);
for (final probe in [step.readyWhen, step.doneWhen]) {
if (sqlWords(probe).firstOrNull != 'SELECT') {
throw const OrmException(
'MIGRATION.PROBE',
'Recovery conditions must be SELECT queries.',
);
}
}
continue;
}
if (step is DropTable) {
if (isMysqlFamily(dialect)) {
throw const OrmException(
'MIGRATION.RECOVERY',
'MySQL table removal requires CheckedTableSql with its before schema.',
);
}
continue;
}
if (step is RebuildTable) {
if (dialect != SqlDialect.sqlite) {
throw const OrmException(
'MIGRATION.TARGET',
'Table rebuilds are SQLite operations.',
);
}
validateSchema([step.before], dialect);
validateSchema([step.after], dialect);
continue;
}
if (step is DropConstraint) {
if (dialect != SqlDialect.postgres) {
throw const OrmException(
'MIGRATION.TARGET',
'Constraint resolution is a PostgreSQL operation.',
);
}
continue;
}
final sql = (step as ExecuteSql).sql;
_validateSql(sql, transactional: true);
if (isMysqlFamily(dialect)) {
validateMysqlStatement(sql);
}
if (isMysqlFamily(dialect) &&
!{
'INSERT',
'UPDATE',
'DELETE',
'REPLACE',
}.contains(sqlWords(sql).first)) {
throw const OrmException(
'MIGRATION.RECOVERY',
'MySQL ExecuteSql accepts DML only. DDL requires CheckedTableSql with reviewed before/after schemas.',
);
}
}
}
}