freezeSnapshot function
Copies and validates a snapshot without retaining mutable application data.
SQLite physical identifiers use ASCII case folding; PostgreSQL quoted names
preserve case and must fit within 63 UTF-8 bytes. SQLite's sqlite_ table
prefix and the migration history table are reserved.
String defaults must not contain NUL.
Implementation
SchemaSnapshot freezeSnapshot(SchemaSnapshot snapshot) {
final tables = <TableDefinition>[];
final tableNames = <String>{};
for (final table in snapshot.tables) {
_validatePhysicalIdentifier(snapshot.engine, table.name);
final tableIdentity = physicalIdentity(snapshot.engine, table.name);
if (tableIdentity == '_orm_migrations' ||
(snapshot.engine == Engine.sqlite &&
tableIdentity.startsWith('sqlite_')) ||
!tableNames.add(tableIdentity)) {
throw ArgumentError('Duplicate or reserved table ${table.name}.');
}
if (table.columns.isEmpty) {
throw ArgumentError('Table ${table.name} has no columns.');
}
final names = <String>{};
final fields = <String>{};
var primaryKeys = 0;
final columns = <ColumnDefinition>[];
for (final column in table.columns) {
_validatePhysicalIdentifier(snapshot.engine, column.name);
if (!names.add(physicalIdentity(snapshot.engine, column.name)) ||
!fields.add(column.field)) {
throw ArgumentError('Duplicate column or field in ${table.name}.');
}
if (column.field.isEmpty) {
throw ArgumentError('Column field must not be empty.');
}
if (column.primaryKey) primaryKeys++;
if (column.primaryKey && column.nullable) {
throw ArgumentError('Primary keys cannot be nullable.');
}
if (column.identity &&
(!column.primaryKey ||
column.type != ScalarType.integer ||
column.defaultValue != null)) {
throw ArgumentError(
'Identity requires an integer primary key without a default.',
);
}
final reference = column.references;
if (reference != null) {
_validatePhysicalIdentifier(snapshot.engine, reference.table);
_validatePhysicalIdentifier(snapshot.engine, reference.column);
if (!const {
'restrict',
'cascade',
'set null',
'no action',
}.contains(reference.onDelete.toLowerCase())) {
throw ArgumentError('Unsupported ON DELETE ${reference.onDelete}.');
}
if (reference.onDelete.toLowerCase() == 'set null' &&
!column.nullable) {
throw ArgumentError('SET NULL requires a nullable foreign key.');
}
}
final value = column.defaultValue;
validateDefault(column);
columns.add(
ColumnDefinition(
name: column.name,
field: column.field,
type: column.type,
nullable: column.nullable,
primaryKey: column.primaryKey,
identity: column.identity,
unique: column.unique,
defaultValue: value is Uint8List
? Uint8List.fromList(value).asUnmodifiableView()
: value,
references: reference,
),
);
}
if (primaryKeys > 1) {
throw ArgumentError('Composite primary keys are not supported.');
}
tables.add(TableDefinition(table.name, List.unmodifiable(columns)));
}
final result = SchemaSnapshot(
engine: snapshot.engine,
tables: List.unmodifiable(tables),
);
for (final table in result.tables) {
for (final column in table.columns) {
final reference = column.references;
if (reference == null) continue;
final target = result.tables
.where(
(t) =>
physicalIdentity(snapshot.engine, t.name) ==
physicalIdentity(snapshot.engine, reference.table),
)
.firstOrNull;
final key = target?.columns
.where(
(c) =>
physicalIdentity(snapshot.engine, c.name) ==
physicalIdentity(snapshot.engine, reference.column),
)
.firstOrNull;
if (key == null ||
(!key.primaryKey && !key.unique) ||
key.type != column.type) {
throw ArgumentError(
'Foreign key ${table.name}.${column.name} needs a matching unique target.',
);
}
}
}
return result;
}