diffSchema<E extends DatumEntityInterface> function

SchemaDiffResult diffSchema<E extends DatumEntityInterface>({
  1. required DatumSchema<E> schema,
  2. required SchemaShape actual,
  3. required bool dropRemovedColumns,
  4. required bool sqlPath,
  5. SqlDialect dialect = SqlDialect.sqlite,
})

Diffs schema against the observed actual shape.

Pure and synchronous. sqlPath tells the differ whether it is deciding for a rigid SQL table (every row has every column; a rename with both columns present is impossible) or for per-row maps (a rename is applied row-by-row and skips already-migrated rows).

Throws MigrationException — before anything touches the store — when a missing field is non-nullable and has no defaultValue, listing every such field at once.

Implementation

SchemaDiffResult diffSchema<E extends DatumEntityInterface>({
  required DatumSchema<E> schema,
  required SchemaShape actual,
  required bool dropRemovedColumns,
  required bool sqlPath,
  SqlDialect dialect = SqlDialect.sqlite,
}) {
  final changes = <SchemaChange>[];
  final renames = <ColumnOperation>[];
  final adds = <ColumnOperation>[];
  final removes = <ColumnOperation>[];
  final warnings = <String>[];
  final problems = <String>[];
  final renamedAway = <String>{};

  for (final field in schema.fields) {
    if (actual.universalKeys.contains(field.name)) continue;

    final from = field.renamedFrom;
    // On the SQL path a declared column that already exists never reaches
    // here (universalKeys == allKeys), so this only guards inconsistent
    // shapes; on the map path a partially-renamed store still renames —
    // SchemaRenameOperation is row-safe.
    if (from != null && actual.allKeys.contains(from) && !(sqlPath && actual.allKeys.contains(field.name))) {
      changes.add(SchemaColumnRenamed(from, field));
      renames.add(SchemaRenameOperation(from, to: field.name));
      renamedAway.add(from);
      continue;
    }

    if (actual.allKeys.contains(field.name)) {
      // Present on some rows only (map path): backfill via AddColumn below.
      if (sqlPath) continue; // SQL columns exist on every row; nothing to do.
    }
    if (!field.isNullable && field.defaultValue == null) {
      problems.add('Cannot auto-add non-nullable "${field.name}" without a defaultValue — '
          'set defaultValue: on its DatumFieldSpec, or make the type nullable.');
      continue;
    }
    changes.add(SchemaColumnAdded(field));
    adds.add(AddColumn(
      field.name,
      defaultValue: field.defaultValue == null ? null : field.encode(field.defaultValue),
      sqlType: field.resolveSqlType(dialect: dialect),
    ));
  }

  if (problems.isNotEmpty) {
    throw MigrationException(
      code: DatumExceptionCode.schemaMismatch,
      message: 'Auto-migration for "${schema.name}" cannot proceed:\n${problems.join('\n')}',
    );
  }

  final declaredNames = schema.fields.map((f) => f.name).toSet();
  for (final key in actual.allKeys) {
    if (declaredNames.contains(key) || kReservedColumnNames.contains(key) || renamedAway.contains(key) || key.startsWith('__')) {
      continue;
    }
    if (dropRemovedColumns) {
      changes.add(SchemaColumnRemoved(key));
      removes.add(RemoveColumn(key));
    } else {
      warnings.add('Column "$key" is not in the "${schema.name}" schema; '
          'kept (autoMigrateDropColumns: false).');
    }
  }

  return (changes: changes, operations: [...renames, ...adds, ...removes], warnings: warnings);
}