computeSchemaDiff function

List<SchemaDiff> computeSchemaDiff(
  1. SchemaSnapshot old,
  2. SchemaSnapshot newSnap
)

: compute the diff between two snapshots. Returns a deterministic list of SchemaDiffs. The caller decides what to do with unsafe entries (typically: log them, surface them via Db.pendingSchemaDiff(), and DO NOT apply them).

Implementation

List<SchemaDiff> computeSchemaDiff(
  SchemaSnapshot old,
  SchemaSnapshot newSnap,
) {
  // Sanity: the snapshot versions must be
  // compatible. A 1.1.x runtime that somehow
  // finds a 2.0.0 snapshot in the table should
  // refuse, not silently corrupt.
  if (old.version > newSnap.version) {
    throw StateError(
      'Schema snapshot downgrade detected: stored '
      'snapshot is v${old.version}, runtime is '
      'v${newSnap.version}. The database was likely '
      'written by a newer d_rocket. Refusing to '
      'migrate.',
    );
  }

  final List<SchemaDiff> out = <SchemaDiff>[];

  // Build lookup maps keyed by table name.
  final Map<String, SchemaTable> oldTables = <String, SchemaTable>{
    for (final SchemaTable t in old.tables) t.name: t,
  };
  final Map<String, SchemaTable> newTables = <String, SchemaTable>{
    for (final SchemaTable t in newSnap.tables) t.name: t,
  };

  // 1. Tables that are in new but not in old:
  //    CREATE TABLE (safe).
  for (final SchemaTable t in newSnap.tables) {
    if (!oldTables.containsKey(t.name)) {
      out.add(SchemaDiff(
        severity: DiffSeverity.safe,
        type: SchemaOperationType.createTable,
        tableName: t.name,
        sql: _createTableSql(t),
        reason:
            'New entity; CREATE TABLE IF NOT EXISTS is '
            'idempotent and non-destructive.',
      ));
    }
  }

  // 2. Tables that are in old but not in new:
  //    DROP TABLE (unsafe).
  for (final SchemaTable t in old.tables) {
    if (!newTables.containsKey(t.name)) {
      out.add(SchemaDiff(
        severity: DiffSeverity.unsafe,
        type: SchemaOperationType.dropTable,
        tableName: t.name,
        sql: 'DROP TABLE ${t.name}',
        reason:
            'Entity removed from code; dropping the '
            'table would lose all its data. Confirm '
            'manually and write a hand-rolled '
            'migration that does the drop explicitly.',
      ));
    }
  }

  // 3. Tables that are in both: compare columns
  //    and indexes. Skip tables that are in
  //    neither (already handled above).
  for (final SchemaTable newT in newSnap.tables) {
    final SchemaTable? oldT = oldTables[newT.name];
    if (oldT == null) continue;
    _diffTable(out, oldT, newT);
  }

  // 4. Rename heuristic. After the column-level
  //    diff is complete, look for unsafe
  //    dropColumn + safe addColumn pairs that
  //    could be a RENAME. This is a best-effort
  //    suggestion only; the user is expected to
  //    confirm manually.
  _appendRenameSuggestions(out, old, newSnap);

  return out;
}