validateMigrations function

void validateMigrations(
  1. List<Migration> migrations, {
  2. required SqlDialect dialect,
})

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.',
        );
      }
    }
  }
}