@assemora/database 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +163 -0
- package/dist/adapter.d.ts +133 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +26 -0
- package/dist/adapter.js.map +1 -0
- package/dist/errors.d.ts +48 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +54 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/join-table.d.ts +66 -0
- package/dist/join-table.d.ts.map +1 -0
- package/dist/join-table.js +265 -0
- package/dist/join-table.js.map +1 -0
- package/dist/memory.d.ts +22 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +398 -0
- package/dist/memory.js.map +1 -0
- package/dist/query-ast.d.ts +79 -0
- package/dist/query-ast.d.ts.map +1 -0
- package/dist/query-ast.js +29 -0
- package/dist/query-ast.js.map +1 -0
- package/dist/schema-diff.d.ts +187 -0
- package/dist/schema-diff.d.ts.map +1 -0
- package/dist/schema-diff.js +473 -0
- package/dist/schema-diff.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { ColumnDescriptor, RelationDescriptor, TableDescriptor } from './adapter.js';
|
|
2
|
+
/**
|
|
3
|
+
* The two ways a change can go wrong, carried by every change so that a caller never
|
|
4
|
+
* has to narrow before it can ask (SPEC.md §34).
|
|
5
|
+
*
|
|
6
|
+
* They are different questions. A destructive change succeeds and takes data with it;
|
|
7
|
+
* one that may fail on existing rows takes nothing, because the database refuses it
|
|
8
|
+
* until somebody fills the empty rows in or removes the duplicates. A change can be
|
|
9
|
+
* both, and a change that is neither is safe to apply to a live table.
|
|
10
|
+
*/
|
|
11
|
+
export type ChangeRisk = {
|
|
12
|
+
/**
|
|
13
|
+
* Applying it may destroy data no later migration can bring back.
|
|
14
|
+
*
|
|
15
|
+
* A drop answers this for certain. A type change answers it as a possibility: a
|
|
16
|
+
* value that does not fit the new type is either rewritten or refused, and which of
|
|
17
|
+
* the two happens is the engine's decision rather than a property of the types, so
|
|
18
|
+
* an unclassified narrowing raises the warning that costs a person the most to
|
|
19
|
+
* ignore.
|
|
20
|
+
*/
|
|
21
|
+
readonly destructive: boolean;
|
|
22
|
+
/** Applying it may be refused by a table whose rows do not already comply. */
|
|
23
|
+
readonly mayFailOnExistingRows: boolean;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A new table, whole.
|
|
27
|
+
*
|
|
28
|
+
* Its columns, indexes and foreign keys travel in the descriptor rather than as
|
|
29
|
+
* changes of their own — the generator that creates a table creates all of it.
|
|
30
|
+
*/
|
|
31
|
+
export type TableAdded = ChangeRisk & {
|
|
32
|
+
readonly kind: 'tableAdded';
|
|
33
|
+
readonly table: string;
|
|
34
|
+
readonly after: TableDescriptor;
|
|
35
|
+
};
|
|
36
|
+
export type TableRemoved = ChangeRisk & {
|
|
37
|
+
readonly kind: 'tableRemoved';
|
|
38
|
+
readonly table: string;
|
|
39
|
+
readonly before: TableDescriptor;
|
|
40
|
+
};
|
|
41
|
+
/** A new column, with its uniqueness and its index already in the descriptor. */
|
|
42
|
+
export type ColumnAdded = ChangeRisk & {
|
|
43
|
+
readonly kind: 'columnAdded';
|
|
44
|
+
readonly table: string;
|
|
45
|
+
readonly column: string;
|
|
46
|
+
readonly after: ColumnDescriptor;
|
|
47
|
+
/**
|
|
48
|
+
* Whether this column arrives because the table is becoming translatable (SPEC.md §131).
|
|
49
|
+
*
|
|
50
|
+
* A column that cannot be null normally has no value to give the rows already there,
|
|
51
|
+
* which is why adding one is refused. `locale` is the exception and the only one: what
|
|
52
|
+
* every existing row is written in is the deployment's default language, and that is a
|
|
53
|
+
* fact the framework holds rather than a guess about the data.
|
|
54
|
+
*/
|
|
55
|
+
readonly becomesTranslatable?: boolean;
|
|
56
|
+
};
|
|
57
|
+
export type ColumnRemoved = ChangeRisk & {
|
|
58
|
+
readonly kind: 'columnRemoved';
|
|
59
|
+
readonly table: string;
|
|
60
|
+
readonly column: string;
|
|
61
|
+
readonly before: ColumnDescriptor;
|
|
62
|
+
};
|
|
63
|
+
export type ColumnTypeChanged = ChangeRisk & {
|
|
64
|
+
readonly kind: 'columnTypeChanged';
|
|
65
|
+
readonly table: string;
|
|
66
|
+
readonly column: string;
|
|
67
|
+
readonly before: ColumnDescriptor;
|
|
68
|
+
readonly after: ColumnDescriptor;
|
|
69
|
+
};
|
|
70
|
+
export type ColumnNullabilityChanged = ChangeRisk & {
|
|
71
|
+
readonly kind: 'columnNullabilityChanged';
|
|
72
|
+
readonly table: string;
|
|
73
|
+
readonly column: string;
|
|
74
|
+
readonly before: ColumnDescriptor;
|
|
75
|
+
readonly after: ColumnDescriptor;
|
|
76
|
+
};
|
|
77
|
+
export type ColumnUniquenessChanged = ChangeRisk & {
|
|
78
|
+
readonly kind: 'columnUniquenessChanged';
|
|
79
|
+
readonly table: string;
|
|
80
|
+
readonly column: string;
|
|
81
|
+
readonly before: ColumnDescriptor;
|
|
82
|
+
readonly after: ColumnDescriptor;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The allowed values of an enum column moved.
|
|
86
|
+
*
|
|
87
|
+
* `added` and `removed` are computed once here: which values went is what decides
|
|
88
|
+
* both the warning and whether the change can be applied at all, and every caller
|
|
89
|
+
* would otherwise take the same two set differences.
|
|
90
|
+
*/
|
|
91
|
+
export type ColumnEnumChanged = ChangeRisk & {
|
|
92
|
+
readonly kind: 'columnEnumChanged';
|
|
93
|
+
readonly table: string;
|
|
94
|
+
readonly column: string;
|
|
95
|
+
readonly before: ColumnDescriptor;
|
|
96
|
+
readonly after: ColumnDescriptor;
|
|
97
|
+
readonly added: readonly string[];
|
|
98
|
+
readonly removed: readonly string[];
|
|
99
|
+
};
|
|
100
|
+
export type PrimaryKeyMoved = ChangeRisk & {
|
|
101
|
+
readonly kind: 'primaryKeyMoved';
|
|
102
|
+
readonly table: string;
|
|
103
|
+
readonly before: string;
|
|
104
|
+
readonly after: string;
|
|
105
|
+
};
|
|
106
|
+
export type IndexAdded = ChangeRisk & {
|
|
107
|
+
readonly kind: 'indexAdded';
|
|
108
|
+
readonly table: string;
|
|
109
|
+
readonly column: string;
|
|
110
|
+
readonly after: ColumnDescriptor;
|
|
111
|
+
};
|
|
112
|
+
export type IndexRemoved = ChangeRisk & {
|
|
113
|
+
readonly kind: 'indexRemoved';
|
|
114
|
+
readonly table: string;
|
|
115
|
+
readonly column: string;
|
|
116
|
+
readonly before: ColumnDescriptor;
|
|
117
|
+
};
|
|
118
|
+
/** `column` is the local column the constraint sits on, not the relation's name. */
|
|
119
|
+
export type ForeignKeyAdded = ChangeRisk & {
|
|
120
|
+
readonly kind: 'foreignKeyAdded';
|
|
121
|
+
readonly table: string;
|
|
122
|
+
readonly column: string;
|
|
123
|
+
readonly after: RelationDescriptor;
|
|
124
|
+
};
|
|
125
|
+
export type ForeignKeyRemoved = ChangeRisk & {
|
|
126
|
+
readonly kind: 'foreignKeyRemoved';
|
|
127
|
+
readonly table: string;
|
|
128
|
+
readonly column: string;
|
|
129
|
+
readonly before: RelationDescriptor;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* One difference between two schemas.
|
|
133
|
+
*
|
|
134
|
+
* `before` is always the state going away and `after` the state arriving, so an
|
|
135
|
+
* addition has only an `after` and a removal only a `before`. A generator switches on
|
|
136
|
+
* `kind` and writes both directions from what the change carries; it never has to
|
|
137
|
+
* find the descriptors again. Adding a member here is a compile error in every
|
|
138
|
+
* generator that exhausts the union, which is the point of it being one.
|
|
139
|
+
*/
|
|
140
|
+
/**
|
|
141
|
+
* A group of columns unique together, arriving on or leaving a table that stays.
|
|
142
|
+
*
|
|
143
|
+
* `uniqueTogether` used to travel only inside `tableAdded` and `tableRemoved`, on the
|
|
144
|
+
* reasoning that a composite unique cannot move on a table that stays. Localisation
|
|
145
|
+
* moves one: `slug` unique on its own becomes `(slug, locale)` unique the moment a model
|
|
146
|
+
* is translatable, and without this the generated migration dropped the old constraint
|
|
147
|
+
* and added nothing — a table that had lost a guarantee and said nothing about it.
|
|
148
|
+
*/
|
|
149
|
+
export type UniqueTogetherAdded = ChangeRisk & {
|
|
150
|
+
readonly kind: 'uniqueTogetherAdded';
|
|
151
|
+
readonly table: string;
|
|
152
|
+
readonly columns: readonly string[];
|
|
153
|
+
};
|
|
154
|
+
export type UniqueTogetherRemoved = ChangeRisk & {
|
|
155
|
+
readonly kind: 'uniqueTogetherRemoved';
|
|
156
|
+
readonly table: string;
|
|
157
|
+
readonly columns: readonly string[];
|
|
158
|
+
};
|
|
159
|
+
export type SchemaChange = TableAdded | TableRemoved | ColumnAdded | ColumnRemoved | ColumnTypeChanged | ColumnNullabilityChanged | ColumnUniquenessChanged | ColumnEnumChanged | PrimaryKeyMoved | IndexAdded | IndexRemoved | ForeignKeyAdded | ForeignKeyRemoved | UniqueTogetherAdded | UniqueTogetherRemoved;
|
|
160
|
+
export type SchemaDiff = {
|
|
161
|
+
/** In the order they must be applied. Empty when the two schemas agree. */
|
|
162
|
+
readonly changes: readonly SchemaChange[];
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* What has to happen for `before` to become `after` (SPEC.md §34).
|
|
166
|
+
*
|
|
167
|
+
* Pure, and dialect-neutral by construction: it produces a list of changes, and a
|
|
168
|
+
* generator turns each one into the statements its engine needs.
|
|
169
|
+
*/
|
|
170
|
+
export declare const diffSchema: (before: readonly TableDescriptor[], after: readonly TableDescriptor[]) => SchemaDiff;
|
|
171
|
+
/** Whether applying the diff loses data. What SPEC.md §34 asks for a warning about. */
|
|
172
|
+
export declare const isDestructive: (diff: SchemaDiff) => boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Whether applying the diff can be refused by a table that already holds rows.
|
|
175
|
+
*
|
|
176
|
+
* The other half of the warning: these changes lose nothing, they simply do not run
|
|
177
|
+
* until somebody has filled in or cleaned up what is already stored.
|
|
178
|
+
*/
|
|
179
|
+
export declare const mayFailOnExistingRows: (diff: SchemaDiff) => boolean;
|
|
180
|
+
/**
|
|
181
|
+
* One change as a sentence a person can act on (SPEC.md §34).
|
|
182
|
+
*
|
|
183
|
+
* The switch has no default: a new kind of change has to be given words before it can
|
|
184
|
+
* reach anybody.
|
|
185
|
+
*/
|
|
186
|
+
export declare const describeChange: (change: SchemaChange) => string;
|
|
187
|
+
//# sourceMappingURL=schema-diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-diff.d.ts","sourceRoot":"","sources":["../src/schema-diff.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema diffing (SPEC.md §34).
|
|
3
|
+
*
|
|
4
|
+
* `assemora db:generate` writes a migration, and a migration is the difference
|
|
5
|
+
* between two schemas rather than a schema. Working that difference out needs no
|
|
6
|
+
* SQL: whether a column can still hold what it held is a property of the types, and
|
|
7
|
+
* the types are the ones every adapter already shares. So the answer lives here, and
|
|
8
|
+
* each dialect turns the same list of changes into its own statements.
|
|
9
|
+
*
|
|
10
|
+
* Three things a descriptor carries are deliberately not compared. `hasDefault` is a
|
|
11
|
+
* data-layer concern that never reaches the DDL (ADR-0011), so it changes neither the
|
|
12
|
+
* statements nor the warning about them; `softDeleteColumn` names an ordinary column,
|
|
13
|
+
* already compared as one; and `isPrimary` is ignored in favour of
|
|
14
|
+
* `TableDescriptor.primaryKey`, because two fields stating one fact would report a
|
|
15
|
+
* moved key twice.
|
|
16
|
+
*
|
|
17
|
+
* What a descriptor cannot express, this cannot see: an index is a flag on a single
|
|
18
|
+
* column, so composite and partial indexes are invisible, and a foreign key exists
|
|
19
|
+
* only where a `belongsTo` relation puts one. `uniqueTogether` is expressible and
|
|
20
|
+
* still travels whole, inside `tableAdded` and `tableRemoved` — a composite unique
|
|
21
|
+
* moving on a table that stays needs a `SchemaChange` of its own, and no declaration
|
|
22
|
+
* can produce that yet: the only writer of one is the join table below, whose
|
|
23
|
+
* constraint is fixed by the two columns it is made of.
|
|
24
|
+
*
|
|
25
|
+
* Both sides have to be descriptors the framework produced — the snapshot in
|
|
26
|
+
* `.assemora/generated/` against the model registry (ADR-0021). What a database
|
|
27
|
+
* reports is not one: `DatabaseAdapter.introspect()` carries no relations and maps an
|
|
28
|
+
* enum column back to the `text` it is stored as, so diffing a live database against
|
|
29
|
+
* the registry adds every foreign key and re-enums every enum column, on every run.
|
|
30
|
+
* Comparing the two becomes possible when introspection reads constraints, and not
|
|
31
|
+
* before.
|
|
32
|
+
*/
|
|
33
|
+
import { AssemoraError } from '@assemora/core';
|
|
34
|
+
import { withJoinTables } from './join-table.js';
|
|
35
|
+
/**
|
|
36
|
+
* Type changes that hold every value the old type could.
|
|
37
|
+
*
|
|
38
|
+
* The list is short on purpose. Calling a safe change destructive costs somebody a
|
|
39
|
+
* warning they can read and dismiss; calling a destructive one safe costs them the
|
|
40
|
+
* column, so a pair that is not listed here counts as a narrowing.
|
|
41
|
+
*/
|
|
42
|
+
const WIDENINGS = {
|
|
43
|
+
uuid: ['string', 'text'],
|
|
44
|
+
string: ['text'],
|
|
45
|
+
integer: ['bigint', 'number', 'decimal'],
|
|
46
|
+
bigint: ['decimal'],
|
|
47
|
+
number: ['decimal'],
|
|
48
|
+
date: ['timestamp'],
|
|
49
|
+
enum: ['text'],
|
|
50
|
+
};
|
|
51
|
+
const widens = (from, to) => (WIDENINGS[from] ?? []).includes(to);
|
|
52
|
+
/**
|
|
53
|
+
* Conversions that rewrite every value, including the ones that fit.
|
|
54
|
+
*
|
|
55
|
+
* These are the type changes whose risk is certain in both directions: no row can
|
|
56
|
+
* refuse them, and what the column used to hold is gone. Every other narrowing is
|
|
57
|
+
* decided by the rows — see `riskOfTypeChange`.
|
|
58
|
+
*/
|
|
59
|
+
const REWRITINGS = {
|
|
60
|
+
decimal: ['integer', 'bigint', 'number'],
|
|
61
|
+
number: ['integer', 'bigint'],
|
|
62
|
+
timestamp: ['date'],
|
|
63
|
+
integer: ['boolean'],
|
|
64
|
+
};
|
|
65
|
+
const rewrites = (from, to) => (REWRITINGS[from] ?? []).includes(to);
|
|
66
|
+
/**
|
|
67
|
+
* What a change of type risks (SPEC.md §34).
|
|
68
|
+
*
|
|
69
|
+
* A widening keeps every value, so neither question applies. A rewriting loses the
|
|
70
|
+
* values it converts and refuses nothing. Everything else narrows: some stored value
|
|
71
|
+
* may not fit, and whether the engine cuts it down or refuses the statement is the
|
|
72
|
+
* engine's choice, not the type pair's — PostgreSQL refuses `text -> string` with
|
|
73
|
+
* "value too long" and `text -> enum` with a check violation, where a lax engine
|
|
74
|
+
* would truncate. So an unclassified narrowing raises both warnings rather than
|
|
75
|
+
* guessing which one arrives, and neither is silently answered "no".
|
|
76
|
+
*/
|
|
77
|
+
const riskOfTypeChange = (from, to) => {
|
|
78
|
+
if (widens(from, to))
|
|
79
|
+
return { destructive: false, mayFailOnExistingRows: false };
|
|
80
|
+
if (rewrites(from, to))
|
|
81
|
+
return { destructive: true, mayFailOnExistingRows: false };
|
|
82
|
+
return { destructive: true, mayFailOnExistingRows: true };
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Where each kind of change sits in a migration.
|
|
86
|
+
*
|
|
87
|
+
* Constraints come off first, so nothing holds on to the columns underneath them;
|
|
88
|
+
* structure is created before anything can reference it; and the two changes that
|
|
89
|
+
* take data with them come last, so a migration that is going to fail fails while
|
|
90
|
+
* everything is still there.
|
|
91
|
+
*/
|
|
92
|
+
const RANK = {
|
|
93
|
+
foreignKeyRemoved: 0,
|
|
94
|
+
indexRemoved: 1,
|
|
95
|
+
// Before the columns move: a group being dropped is what frees a column to stop being
|
|
96
|
+
// unique on its own, or to start.
|
|
97
|
+
uniqueTogetherRemoved: 2,
|
|
98
|
+
tableAdded: 3,
|
|
99
|
+
columnAdded: 4,
|
|
100
|
+
columnTypeChanged: 5,
|
|
101
|
+
columnEnumChanged: 6,
|
|
102
|
+
columnNullabilityChanged: 7,
|
|
103
|
+
columnUniquenessChanged: 8,
|
|
104
|
+
primaryKeyMoved: 9,
|
|
105
|
+
// After them, for the mirror reason: `(slug, locale)` cannot be declared until
|
|
106
|
+
// `locale` is there and filled in.
|
|
107
|
+
uniqueTogetherAdded: 10,
|
|
108
|
+
indexAdded: 11,
|
|
109
|
+
foreignKeyAdded: 12,
|
|
110
|
+
columnRemoved: 13,
|
|
111
|
+
tableRemoved: 14,
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Keeping one of two objects that share a name would generate a migration for a
|
|
115
|
+
* shape nobody declared, and the mistake would only surface as SQL.
|
|
116
|
+
*/
|
|
117
|
+
const byName = (items, nameOf, what) => {
|
|
118
|
+
const found = new Map();
|
|
119
|
+
for (const item of items) {
|
|
120
|
+
const name = nameOf(item);
|
|
121
|
+
if (found.has(name)) {
|
|
122
|
+
throw new AssemoraError('DUPLICATE_DESCRIPTOR', `Two ${what} are both named "${name}"`, {
|
|
123
|
+
status: 500,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
found.set(name, item);
|
|
127
|
+
}
|
|
128
|
+
return found;
|
|
129
|
+
};
|
|
130
|
+
const columnsOf = (table) => byName(table.columns, (column) => column.name, `columns of "${table.name}"`);
|
|
131
|
+
/**
|
|
132
|
+
* The duplicate-column guard, for a table that never reaches the comparison.
|
|
133
|
+
*
|
|
134
|
+
* A table on one side alone is added or dropped whole, so its columns are paired with
|
|
135
|
+
* nothing — and a repeated name is likeliest in a table somebody has just written.
|
|
136
|
+
* The `create table` it produces is refused by PostgreSQL with 42701, long after the
|
|
137
|
+
* declaration that caused it was read.
|
|
138
|
+
*/
|
|
139
|
+
const assertColumnNamesAreDistinct = (table) => {
|
|
140
|
+
columnsOf(table);
|
|
141
|
+
};
|
|
142
|
+
const namesOf = (before, after) => [...new Set([...before.keys(), ...after.keys()])].sort();
|
|
143
|
+
/**
|
|
144
|
+
* A foreign key identified by what it constrains, not by the relation's name.
|
|
145
|
+
*
|
|
146
|
+
* Renaming `author` to `writer` changes no constraint and must produce no migration;
|
|
147
|
+
* pointing it at another table changes one. Two relations that describe the identical
|
|
148
|
+
* constraint are one constraint.
|
|
149
|
+
*/
|
|
150
|
+
const foreignKeysOf = (table) => {
|
|
151
|
+
const keys = new Map();
|
|
152
|
+
for (const relation of table.relations) {
|
|
153
|
+
if (relation.kind !== 'belongsTo')
|
|
154
|
+
continue;
|
|
155
|
+
keys.set(`${relation.foreignKey} -> ${relation.target}.${relation.ownerKey}`, relation);
|
|
156
|
+
}
|
|
157
|
+
return keys;
|
|
158
|
+
};
|
|
159
|
+
const columnAdded = (table, column, after, becomesTranslatable = false) => ({
|
|
160
|
+
kind: 'columnAdded',
|
|
161
|
+
...(becomesTranslatable ? { becomesTranslatable } : {}),
|
|
162
|
+
table,
|
|
163
|
+
column,
|
|
164
|
+
after,
|
|
165
|
+
destructive: false,
|
|
166
|
+
// A required column with nothing to put in the rows that already exist is refused
|
|
167
|
+
// rather than filled with a guess. Nothing is lost; the migration is.
|
|
168
|
+
//
|
|
169
|
+
// `hasDefault` cannot excuse it: a model default is applied by the data layer on
|
|
170
|
+
// insert and never reaches the DDL (ADR-0011), so `add column ... not null` meets
|
|
171
|
+
// the existing rows with nothing either way. Reading it here once told somebody
|
|
172
|
+
// that `enumOf('draft', 'published').default('draft')` was safe to add to a table
|
|
173
|
+
// that already held rows, and PostgreSQL answered with 23502.
|
|
174
|
+
mayFailOnExistingRows: !after.isNullable,
|
|
175
|
+
});
|
|
176
|
+
const columnRemoved = (table, column, before) => ({
|
|
177
|
+
kind: 'columnRemoved',
|
|
178
|
+
table,
|
|
179
|
+
column,
|
|
180
|
+
before,
|
|
181
|
+
destructive: true,
|
|
182
|
+
mayFailOnExistingRows: false,
|
|
183
|
+
});
|
|
184
|
+
/** Everything that moved on a column both schemas have. */
|
|
185
|
+
const diffColumn = (table, column, before, after) => {
|
|
186
|
+
const changes = [];
|
|
187
|
+
if (before.type !== after.type) {
|
|
188
|
+
changes.push({
|
|
189
|
+
kind: 'columnTypeChanged',
|
|
190
|
+
table,
|
|
191
|
+
column,
|
|
192
|
+
before,
|
|
193
|
+
after,
|
|
194
|
+
...riskOfTypeChange(before.type, after.type),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
else if (after.type === 'enum') {
|
|
198
|
+
// Only when both sides are enums. Becoming one, or ceasing to be one, is a type
|
|
199
|
+
// change, and the values travel in its descriptors.
|
|
200
|
+
const was = before.enumValues ?? [];
|
|
201
|
+
const is = after.enumValues ?? [];
|
|
202
|
+
const added = is.filter((value) => !was.includes(value));
|
|
203
|
+
const removed = was.filter((value) => !is.includes(value));
|
|
204
|
+
if (added.length > 0 || removed.length > 0) {
|
|
205
|
+
changes.push({
|
|
206
|
+
kind: 'columnEnumChanged',
|
|
207
|
+
table,
|
|
208
|
+
column,
|
|
209
|
+
before,
|
|
210
|
+
after,
|
|
211
|
+
added,
|
|
212
|
+
removed,
|
|
213
|
+
destructive: false,
|
|
214
|
+
// The new set is what a row is measured against, so the warning belongs to
|
|
215
|
+
// the side that arrives: a column that declared no values constrained
|
|
216
|
+
// nothing, and every row in it may be holding something the set it gains
|
|
217
|
+
// does not allow. Losing the last value is the mirror case — the constraint
|
|
218
|
+
// goes away entirely, and nothing can refuse that.
|
|
219
|
+
mayFailOnExistingRows: after.enumValues !== undefined && (removed.length > 0 || before.enumValues === undefined),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (before.isNullable !== after.isNullable) {
|
|
224
|
+
changes.push({
|
|
225
|
+
kind: 'columnNullabilityChanged',
|
|
226
|
+
table,
|
|
227
|
+
column,
|
|
228
|
+
before,
|
|
229
|
+
after,
|
|
230
|
+
destructive: false,
|
|
231
|
+
mayFailOnExistingRows: !after.isNullable,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (before.isUnique !== after.isUnique) {
|
|
235
|
+
changes.push({
|
|
236
|
+
kind: 'columnUniquenessChanged',
|
|
237
|
+
table,
|
|
238
|
+
column,
|
|
239
|
+
before,
|
|
240
|
+
after,
|
|
241
|
+
destructive: false,
|
|
242
|
+
mayFailOnExistingRows: after.isUnique,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (before.isIndexed !== after.isIndexed) {
|
|
246
|
+
changes.push(after.isIndexed
|
|
247
|
+
? {
|
|
248
|
+
kind: 'indexAdded',
|
|
249
|
+
table,
|
|
250
|
+
column,
|
|
251
|
+
after,
|
|
252
|
+
destructive: false,
|
|
253
|
+
mayFailOnExistingRows: false,
|
|
254
|
+
}
|
|
255
|
+
: {
|
|
256
|
+
kind: 'indexRemoved',
|
|
257
|
+
table,
|
|
258
|
+
column,
|
|
259
|
+
before,
|
|
260
|
+
destructive: false,
|
|
261
|
+
mayFailOnExistingRows: false,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return changes;
|
|
265
|
+
};
|
|
266
|
+
const diffColumns = (table, before, after, becomesTranslatable = false) => {
|
|
267
|
+
const changes = [];
|
|
268
|
+
for (const column of namesOf(before, after)) {
|
|
269
|
+
const was = before.get(column);
|
|
270
|
+
const is = after.get(column);
|
|
271
|
+
if (was !== undefined && is !== undefined)
|
|
272
|
+
changes.push(...diffColumn(table, column, was, is));
|
|
273
|
+
else if (is !== undefined)
|
|
274
|
+
changes.push(columnAdded(table, column, is, becomesTranslatable));
|
|
275
|
+
else if (was !== undefined)
|
|
276
|
+
changes.push(columnRemoved(table, column, was));
|
|
277
|
+
}
|
|
278
|
+
return changes;
|
|
279
|
+
};
|
|
280
|
+
const diffTable = (before, after) => {
|
|
281
|
+
const table = after.name;
|
|
282
|
+
const changes = diffColumns(table, columnsOf(before), columnsOf(after), before.translatable !== true && after.translatable === true);
|
|
283
|
+
if (before.primaryKey !== after.primaryKey) {
|
|
284
|
+
changes.push({
|
|
285
|
+
kind: 'primaryKeyMoved',
|
|
286
|
+
table,
|
|
287
|
+
before: before.primaryKey,
|
|
288
|
+
after: after.primaryKey,
|
|
289
|
+
destructive: false,
|
|
290
|
+
// The rows have to be complete and unique on the new column before the database
|
|
291
|
+
// accepts it as a key.
|
|
292
|
+
mayFailOnExistingRows: true,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Compared as sorted, joined names, because a group is a set: `(slug, locale)` and
|
|
297
|
+
* `(locale, slug)` are one constraint, and a declaration that listed them the other
|
|
298
|
+
* way round must not read as a change.
|
|
299
|
+
*/
|
|
300
|
+
const groupsOf = (table) => new Map((table.uniqueTogether ?? []).map((columns) => [[...columns].sort().join(','), columns]));
|
|
301
|
+
const wasGroups = groupsOf(before);
|
|
302
|
+
const isGroups = groupsOf(after);
|
|
303
|
+
for (const key of new Set([...wasGroups.keys(), ...isGroups.keys()])) {
|
|
304
|
+
const was = wasGroups.get(key);
|
|
305
|
+
const is = isGroups.get(key);
|
|
306
|
+
if (was !== undefined && is === undefined) {
|
|
307
|
+
changes.push({
|
|
308
|
+
kind: 'uniqueTogetherRemoved',
|
|
309
|
+
table,
|
|
310
|
+
columns: was,
|
|
311
|
+
destructive: false,
|
|
312
|
+
mayFailOnExistingRows: false,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
else if (was === undefined && is !== undefined) {
|
|
316
|
+
changes.push({
|
|
317
|
+
kind: 'uniqueTogetherAdded',
|
|
318
|
+
table,
|
|
319
|
+
columns: is,
|
|
320
|
+
destructive: false,
|
|
321
|
+
// Rows that are already duplicates on the group refuse the constraint.
|
|
322
|
+
mayFailOnExistingRows: true,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const wasKeys = foreignKeysOf(before);
|
|
327
|
+
const isKeys = foreignKeysOf(after);
|
|
328
|
+
for (const key of namesOf(wasKeys, isKeys)) {
|
|
329
|
+
const was = wasKeys.get(key);
|
|
330
|
+
const is = isKeys.get(key);
|
|
331
|
+
if (was !== undefined && is === undefined) {
|
|
332
|
+
changes.push({
|
|
333
|
+
kind: 'foreignKeyRemoved',
|
|
334
|
+
table,
|
|
335
|
+
column: was.foreignKey,
|
|
336
|
+
before: was,
|
|
337
|
+
destructive: false,
|
|
338
|
+
mayFailOnExistingRows: false,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
else if (was === undefined && is !== undefined) {
|
|
342
|
+
changes.push({
|
|
343
|
+
kind: 'foreignKeyAdded',
|
|
344
|
+
table,
|
|
345
|
+
column: is.foreignKey,
|
|
346
|
+
after: is,
|
|
347
|
+
destructive: false,
|
|
348
|
+
// A row already pointing at something that is not there refuses the
|
|
349
|
+
// constraint.
|
|
350
|
+
mayFailOnExistingRows: true,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return changes;
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* What has to happen for `before` to become `after` (SPEC.md §34).
|
|
358
|
+
*
|
|
359
|
+
* Pure, and dialect-neutral by construction: it produces a list of changes, and a
|
|
360
|
+
* generator turns each one into the statements its engine needs.
|
|
361
|
+
*/
|
|
362
|
+
export const diffSchema = (before, after) => {
|
|
363
|
+
// A join table has no model, so neither side names it: it is derived from the
|
|
364
|
+
// `belongsToMany` relations on the tables that do (SPEC.md §23). Deriving it here
|
|
365
|
+
// rather than asking every caller to means a many-to-many arriving in the registry
|
|
366
|
+
// arrives in the migration, and `db:generate` needs to know nothing about it. The
|
|
367
|
+
// expansion is idempotent, so a snapshot that already holds the join table still
|
|
368
|
+
// compares clean against a registry that derives it.
|
|
369
|
+
const previous = withJoinTables(before);
|
|
370
|
+
const current = withJoinTables(after);
|
|
371
|
+
const was = byName(previous, (table) => table.name, 'tables');
|
|
372
|
+
const is = byName(current, (table) => table.name, 'tables');
|
|
373
|
+
const changes = [];
|
|
374
|
+
// Every table on both sides, not only the ones that end up being compared.
|
|
375
|
+
for (const table of [...previous, ...current])
|
|
376
|
+
assertColumnNamesAreDistinct(table);
|
|
377
|
+
for (const name of namesOf(was, is)) {
|
|
378
|
+
const previous = was.get(name);
|
|
379
|
+
const current = is.get(name);
|
|
380
|
+
if (previous !== undefined && current !== undefined) {
|
|
381
|
+
changes.push(...diffTable(previous, current));
|
|
382
|
+
}
|
|
383
|
+
else if (current !== undefined) {
|
|
384
|
+
changes.push({
|
|
385
|
+
kind: 'tableAdded',
|
|
386
|
+
table: name,
|
|
387
|
+
after: current,
|
|
388
|
+
destructive: false,
|
|
389
|
+
mayFailOnExistingRows: false,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
else if (previous !== undefined) {
|
|
393
|
+
changes.push({
|
|
394
|
+
kind: 'tableRemoved',
|
|
395
|
+
table: name,
|
|
396
|
+
before: previous,
|
|
397
|
+
destructive: true,
|
|
398
|
+
mayFailOnExistingRows: false,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Stable, so the order the changes were produced in — table by table and column by
|
|
403
|
+
// column, both by name — survives inside each rank. Two runs are the same migration
|
|
404
|
+
// byte for byte, and reordering the model registry does not rewrite it.
|
|
405
|
+
return { changes: changes.sort((left, right) => RANK[left.kind] - RANK[right.kind]) };
|
|
406
|
+
};
|
|
407
|
+
/** Whether applying the diff loses data. What SPEC.md §34 asks for a warning about. */
|
|
408
|
+
export const isDestructive = (diff) => diff.changes.some((change) => change.destructive);
|
|
409
|
+
/**
|
|
410
|
+
* Whether applying the diff can be refused by a table that already holds rows.
|
|
411
|
+
*
|
|
412
|
+
* The other half of the warning: these changes lose nothing, they simply do not run
|
|
413
|
+
* until somebody has filled in or cleaned up what is already stored.
|
|
414
|
+
*/
|
|
415
|
+
export const mayFailOnExistingRows = (diff) => diff.changes.some((change) => change.mayFailOnExistingRows);
|
|
416
|
+
const quoteValues = (values) => values.map((value) => `"${value}"`).join(', ');
|
|
417
|
+
/** How a person names a column: the table it is in, then the field. */
|
|
418
|
+
const at = (change) => `${change.table}.${change.column}`;
|
|
419
|
+
/**
|
|
420
|
+
* One change as a sentence a person can act on (SPEC.md §34).
|
|
421
|
+
*
|
|
422
|
+
* The switch has no default: a new kind of change has to be given words before it can
|
|
423
|
+
* reach anybody.
|
|
424
|
+
*/
|
|
425
|
+
export const describeChange = (change) => {
|
|
426
|
+
switch (change.kind) {
|
|
427
|
+
case 'tableAdded':
|
|
428
|
+
return `creates table ${change.table}`;
|
|
429
|
+
case 'tableRemoved':
|
|
430
|
+
return `drops table ${change.table}`;
|
|
431
|
+
case 'columnAdded':
|
|
432
|
+
return change.mayFailOnExistingRows
|
|
433
|
+
? // "database" is the load-bearing word: the column may well declare a
|
|
434
|
+
// default, and the person reading this has to learn that the schema does
|
|
435
|
+
// not carry it (ADR-0011) before they can act on the warning.
|
|
436
|
+
`adds required column ${at(change)} with no database default`
|
|
437
|
+
: `adds column ${at(change)}`;
|
|
438
|
+
case 'columnRemoved':
|
|
439
|
+
return `drops column ${at(change)}`;
|
|
440
|
+
case 'columnTypeChanged':
|
|
441
|
+
return `changes ${at(change)} from ${change.before.type} to ${change.after.type}`;
|
|
442
|
+
case 'columnNullabilityChanged':
|
|
443
|
+
return change.after.isNullable
|
|
444
|
+
? `makes ${at(change)} optional`
|
|
445
|
+
: `makes ${at(change)} required`;
|
|
446
|
+
case 'columnUniquenessChanged':
|
|
447
|
+
return change.after.isUnique
|
|
448
|
+
? `makes ${at(change)} unique`
|
|
449
|
+
: `drops the unique constraint on ${at(change)}`;
|
|
450
|
+
case 'columnEnumChanged': {
|
|
451
|
+
const parts = [
|
|
452
|
+
...(change.added.length > 0 ? [`adds ${quoteValues(change.added)}`] : []),
|
|
453
|
+
...(change.removed.length > 0 ? [`removes ${quoteValues(change.removed)}`] : []),
|
|
454
|
+
];
|
|
455
|
+
return `${parts.join(' and ')} on ${at(change)}`;
|
|
456
|
+
}
|
|
457
|
+
case 'primaryKeyMoved':
|
|
458
|
+
return `moves the primary key of ${change.table} from ${change.before} to ${change.after}`;
|
|
459
|
+
case 'indexAdded':
|
|
460
|
+
return `indexes ${at(change)}`;
|
|
461
|
+
case 'indexRemoved':
|
|
462
|
+
return `drops the index on ${at(change)}`;
|
|
463
|
+
case 'foreignKeyAdded':
|
|
464
|
+
return `adds a foreign key from ${at(change)} to ${change.after.target}.${change.after.ownerKey}`;
|
|
465
|
+
case 'foreignKeyRemoved':
|
|
466
|
+
return `drops the foreign key from ${at(change)} to ${change.before.target}.${change.before.ownerKey}`;
|
|
467
|
+
case 'uniqueTogetherAdded':
|
|
468
|
+
return `makes ${change.columns.join(' and ')} unique together on ${change.table}`;
|
|
469
|
+
case 'uniqueTogetherRemoved':
|
|
470
|
+
return `drops the unique constraint on ${change.columns.join(' and ')} of ${change.table}`;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
//# sourceMappingURL=schema-diff.js.map
|