diffSchema<E extends DatumEntityInterface> function
SchemaDiffResult
diffSchema<E extends DatumEntityInterface>({
- required DatumSchema<
E> schema, - required SchemaShape actual,
- required bool dropRemovedColumns,
- required bool sqlPath,
- 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);
}