planSchemaChange function

MigrationPlan planSchemaChange(
  1. SchemaSnapshot before,
  2. SchemaSnapshot after
)

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);
}