verifySchema function

Compares modeled tables, columns, keys, indexes, and checks with the catalog.

This does not infer or apply a migration. Inspect the returned unmanaged objects as well as the modeled differences before changing an existing table.

Implementation

Future<SchemaVerification> verifySchema(
  SqlDatabase<Backend> db,
  SchemaSnapshot expected,
) async {
  if (isMysqlFamily(db.dialect)) return mysqlVerifySchema(db, expected);
  final differences = <String>[], unmanaged = <CatalogObject>[];
  for (final table in expected.tables) {
    final actual = await inspectTable(db, table.name);
    unmanaged.addAll(actual.unmanaged);
    final columns = {for (final c in actual.columns) c.name: c};
    var checkContextMatches = true;
    for (final column in table.columns) {
      final found = columns.remove(column.name),
          path = '${table.name}.${column.name}';
      if (found == null) {
        checkContextMatches = false;
        differences.add('$path is missing');
        continue;
      }
      if (found.storageType != columnStorageType(column, db.dialect)) {
        checkContextMatches = false;
        differences.add('$path type differs');
      }
      if (found.nullable != column.nullable) {
        differences.add('$path nullability differs');
      }
      if ((found.temporalPrecision ?? 6) != (column.temporalPrecision ?? 6)) {
        differences.add('$path temporal precision differs');
      }
      if (!matchesDecimalDigits(column, found)) {
        differences.add('$path decimal precision/scale differs');
      }
      if (db.dialect == SqlDialect.sqlite && !matchesCollation(column, found)) {
        differences.add('$path collation differs');
      }
      if (column.codec.sqlType == 'integer' &&
          (found.integerBits ?? 64) != (column.integerBits ?? 64)) {
        differences.add('$path integer width differs');
      }
      if (_columnDefault(found.defaultSql, column) !=
          _columnDefault(
            column.defaultSql == null
                ? null
                : coerceColumn(column.defaultSql!, column, db.dialect),
            column,
          )) {
        differences.add('$path default differs');
      }
      if (db.dialect == SqlDialect.postgres &&
          column.computed == null &&
          found.computed == null &&
          found.generated != column.generated) {
        differences.add('$path generation differs');
      }
      if (db.dialect == SqlDialect.sqlite &&
          found.generated &&
          found.computed == null) {
        differences.add('$path uses an unmodeled generated expression');
      }
    }
    for (final name in columns.keys) {
      differences.add('${table.name}.$name is unmanaged');
    }
    differences.addAll(
      await verifyComputed(
        db,
        table,
        actual.columns,
        contextMatches: checkContextMatches,
      ),
    );
    void compare(String kind, Object? desired, Object? found) {
      if (migrationHash(desired) != migrationHash(found)) {
        differences.add('${table.name} $kind differs');
      }
    }

    List<String> set(Iterable<Object?> values) =>
        values.map((v) => jsonEncode(canonicalMigrationValue(v))).toList()
          ..sort();
    compare('primary key', table.primaryKey, actual.primaryKey);
    compare('unique keys', set(table.uniqueKeys), set(actual.uniqueKeys));
    // Expected SQL may no longer resolve when a referenced column/type drifted.
    // Report that schema drift instead of attempting an invalid EXPLAIN.
    final checkMatches = checkContextMatches
        ? await matchChecks(db, table.name, table.checks, actual.checks)
        : List<int?>.filled(table.checks.length, null);
    if (checkMatches.any((i) => i == null) ||
        checkMatches.length != actual.checks.length) {
      differences.add('${table.name} checks differs');
      for (var i = 0; i < actual.checks.length; i++) {
        if (!checkMatches.contains(i)) {
          final c = actual.checks[i];
          unmanaged.add(
            CatalogObject('check', c.name ?? '${table.name}#$i', c.expression),
          );
        }
      }
    }
    compare(
      'foreign keys',
      set(table.foreignKeys.map(foreignKeyJson)),
      set(actual.foreignKeys.map(foreignKeyJson)),
    );
    compare(
      'indexes',
      set(table.indexes.map(indexJson)),
      set(actual.indexes.map(indexJson)),
    );
  }
  return SchemaVerification(
    List.unmodifiable(differences),
    List.unmodifiable(unmanaged),
  );
}