planSchemaChange function
Plans conservative additions between two complete frozen snapshots.
Implementation
MigrationPlan planSchemaChange(SchemaSnapshot before, SchemaSnapshot after) {
final old = freezeSnapshot(before);
final next = freezeSnapshot(after);
if (old.engine != next.engine) throw ArgumentError('Schema engines differ.');
final oldTables = {
for (final table in old.tables)
physicalIdentity(next.engine, table.name): table,
};
final nextTables = {
for (final table in next.tables)
physicalIdentity(next.engine, table.name): table,
};
for (final name in oldTables.keys) {
if (!nextTables.containsKey(name)) {
throw StateError(
'Dropping or renaming table $name requires explicit SQL.',
);
}
}
final steps = <String>[];
final pending = {
for (final table in next.tables)
if (!oldTables.containsKey(physicalIdentity(next.engine, table.name)))
physicalIdentity(next.engine, table.name): table,
};
final available = oldTables.keys.toSet();
while (pending.isNotEmpty) {
final ready = pending.values
.where(
(table) => table.columns.every(
(column) =>
column.references == null ||
physicalIdentity(next.engine, column.references!.table) ==
physicalIdentity(next.engine, table.name) ||
available.contains(
physicalIdentity(next.engine, column.references!.table),
),
),
)
.firstOrNull;
if (ready == null) {
throw StateError('Cyclic new-table references require explicit SQL.');
}
steps.add(
'CREATE TABLE ${quoteIdentifier(ready.name)} (${ready.columns.map((c) => columnSql(next.engine, c)).join(', ')})',
);
pending.remove(physicalIdentity(next.engine, ready.name));
available.add(physicalIdentity(next.engine, ready.name));
}
for (final table in next.tables) {
final previous = oldTables[physicalIdentity(next.engine, table.name)];
if (previous == null) continue;
final previousColumns = {
for (final column in previous.columns)
physicalIdentity(next.engine, column.name): column,
};
final nextColumns = {
for (final column in table.columns)
physicalIdentity(next.engine, column.name): column,
};
for (final name in previousColumns.keys) {
if (!nextColumns.containsKey(name)) {
throw StateError(
'Dropping or renaming ${table.name}.$name requires explicit SQL.',
);
}
}
for (final column in table.columns) {
final previousColumn =
previousColumns[physicalIdentity(next.engine, column.name)];
if (previousColumn != null) {
if (!_sameStorage(next.engine, previousColumn, column)) {
throw StateError(
'Changing ${table.name}.${column.name} requires explicit SQL.',
);
}
continue;
}
if ((!column.nullable && column.defaultValue == null) ||
column.primaryKey ||
column.identity ||
column.unique ||
(column.references != null && column.defaultValue != null)) {
throw StateError(
'Adding ${table.name}.${column.name} requires explicit SQL for its constraints or existing rows.',
);
}
steps.add(
'ALTER TABLE ${quoteIdentifier(table.name)} ADD COLUMN ${columnSql(next.engine, column)}',
);
}
}
return MigrationPlan(engine: next.engine, steps: steps, snapshot: next);
}