@sedrino/db-schema 0.1.1
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/README.md +69 -0
- package/dist/index.d.ts +1222 -0
- package/dist/index.js +1456 -0
- package/dist/index.js.map +1 -0
- package/docs/cli.md +70 -0
- package/docs/index.md +11 -0
- package/docs/migrations.md +65 -0
- package/docs/schema-document.md +48 -0
- package/package.json +41 -0
- package/src/apply.ts +234 -0
- package/src/cli.ts +209 -0
- package/src/drizzle.ts +178 -0
- package/src/index.ts +62 -0
- package/src/migration.ts +456 -0
- package/src/planner.ts +247 -0
- package/src/project.ts +79 -0
- package/src/schema.ts +53 -0
- package/src/sqlite.ts +172 -0
- package/src/types.ts +145 -0
- package/src/utils.ts +286 -0
package/src/migration.ts
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DefaultSpec,
|
|
3
|
+
FieldReferenceSpec,
|
|
4
|
+
FieldSpec,
|
|
5
|
+
IndexSpec,
|
|
6
|
+
LogicalTypeSpec,
|
|
7
|
+
StorageSpec,
|
|
8
|
+
TableSpec,
|
|
9
|
+
UniqueSpec,
|
|
10
|
+
} from "./types";
|
|
11
|
+
import {
|
|
12
|
+
defaultStorageForLogical,
|
|
13
|
+
fieldAutoId,
|
|
14
|
+
tableAutoId,
|
|
15
|
+
toSnakeCase,
|
|
16
|
+
validateDefaultCompatibility,
|
|
17
|
+
validateLogicalAndStorageCompatibility,
|
|
18
|
+
} from "./utils";
|
|
19
|
+
|
|
20
|
+
export type MigrationMeta = {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type MigrationOperation =
|
|
27
|
+
| { kind: "createTable"; table: TableSpec }
|
|
28
|
+
| { kind: "dropTable"; tableName: string }
|
|
29
|
+
| { kind: "renameTable"; from: string; to: string }
|
|
30
|
+
| { kind: "addField"; tableName: string; field: FieldSpec }
|
|
31
|
+
| { kind: "dropField"; tableName: string; fieldName: string }
|
|
32
|
+
| { kind: "renameField"; tableName: string; from: string; to: string }
|
|
33
|
+
| { kind: "addIndex"; tableName: string; index: IndexSpec }
|
|
34
|
+
| { kind: "dropIndex"; tableName: string; indexName: string }
|
|
35
|
+
| { kind: "addUnique"; tableName: string; unique: UniqueSpec }
|
|
36
|
+
| { kind: "dropUnique"; tableName: string; uniqueName: string };
|
|
37
|
+
|
|
38
|
+
export type MigrationDefinition = {
|
|
39
|
+
meta: MigrationMeta;
|
|
40
|
+
buildOperations(): MigrationOperation[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type FieldOptions = {
|
|
44
|
+
column?: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
storage?: StorageSpec;
|
|
47
|
+
references?: FieldReferenceSpec;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
class MutableFieldBuilder {
|
|
51
|
+
constructor(private field: FieldSpec) {}
|
|
52
|
+
|
|
53
|
+
required() {
|
|
54
|
+
this.field.nullable = false;
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
nullable() {
|
|
59
|
+
this.field.nullable = true;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
unique() {
|
|
64
|
+
this.field.unique = true;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
default(value: string | number | boolean | null) {
|
|
69
|
+
const nextDefault: DefaultSpec = {
|
|
70
|
+
kind: "literal",
|
|
71
|
+
value,
|
|
72
|
+
};
|
|
73
|
+
const issue = validateDefaultCompatibility(this.field, nextDefault);
|
|
74
|
+
if (issue) throw new Error(issue);
|
|
75
|
+
this.field.default = nextDefault;
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
defaultNow() {
|
|
80
|
+
const nextDefault: DefaultSpec = {
|
|
81
|
+
kind: "now",
|
|
82
|
+
};
|
|
83
|
+
const issue = validateDefaultCompatibility(this.field, nextDefault);
|
|
84
|
+
if (issue) throw new Error(issue);
|
|
85
|
+
this.field.default = nextDefault;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
references(reference: FieldReferenceSpec) {
|
|
90
|
+
this.field.references = reference;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
description(description: string) {
|
|
95
|
+
this.field.description = description;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
column(column: string) {
|
|
100
|
+
this.field.storage = {
|
|
101
|
+
...this.field.storage,
|
|
102
|
+
column,
|
|
103
|
+
};
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
build() {
|
|
108
|
+
return this.field;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createFieldSpec(
|
|
113
|
+
tableName: string,
|
|
114
|
+
fieldName: string,
|
|
115
|
+
logical: LogicalTypeSpec,
|
|
116
|
+
options: FieldOptions = {},
|
|
117
|
+
): FieldSpec {
|
|
118
|
+
const column = options.column ?? toSnakeCase(fieldName);
|
|
119
|
+
const storage = options.storage ?? defaultStorageForLogical(logical, column);
|
|
120
|
+
const storageIssue = validateLogicalAndStorageCompatibility(logical, storage);
|
|
121
|
+
if (storageIssue) throw new Error(storageIssue);
|
|
122
|
+
|
|
123
|
+
const field: FieldSpec = {
|
|
124
|
+
id: fieldAutoId(tableName, fieldName),
|
|
125
|
+
name: fieldName,
|
|
126
|
+
logical,
|
|
127
|
+
storage,
|
|
128
|
+
nullable: true,
|
|
129
|
+
primaryKey: false,
|
|
130
|
+
unique: false,
|
|
131
|
+
description: options.description,
|
|
132
|
+
references: options.references,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (logical.kind === "id") {
|
|
136
|
+
field.primaryKey = true;
|
|
137
|
+
field.nullable = false;
|
|
138
|
+
field.default = {
|
|
139
|
+
kind: "generatedId",
|
|
140
|
+
prefix: logical.prefix,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return field;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
class TableCreateBuilder {
|
|
148
|
+
private readonly fields: FieldSpec[] = [];
|
|
149
|
+
private readonly indexes: IndexSpec[] = [];
|
|
150
|
+
private readonly uniques: UniqueSpec[] = [];
|
|
151
|
+
private descriptionText?: string;
|
|
152
|
+
|
|
153
|
+
constructor(private readonly tableName: string) {}
|
|
154
|
+
|
|
155
|
+
description(text: string) {
|
|
156
|
+
this.descriptionText = text;
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private addField(field: FieldSpec) {
|
|
161
|
+
this.fields.push(field);
|
|
162
|
+
return new MutableFieldBuilder(field);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
id(fieldName: string, options: Omit<FieldOptions, "storage"> & { prefix: string }) {
|
|
166
|
+
return this.addField(
|
|
167
|
+
createFieldSpec(this.tableName, fieldName, { kind: "id", prefix: options.prefix }, options),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
string(fieldName: string, options: FieldOptions & { format?: "email" | "url" | "slug" } = {}) {
|
|
172
|
+
return this.addField(
|
|
173
|
+
createFieldSpec(
|
|
174
|
+
this.tableName,
|
|
175
|
+
fieldName,
|
|
176
|
+
{ kind: "string", format: options.format },
|
|
177
|
+
options,
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
text(fieldName: string, options: FieldOptions = {}) {
|
|
183
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "text" }, options));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
boolean(fieldName: string, options: FieldOptions = {}) {
|
|
187
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "boolean" }, options));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
integer(fieldName: string, options: FieldOptions = {}) {
|
|
191
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "integer" }, options));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
number(fieldName: string, options: FieldOptions = {}) {
|
|
195
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "number" }, options));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
enum(fieldName: string, values: string[], options: FieldOptions = {}) {
|
|
199
|
+
return this.addField(
|
|
200
|
+
createFieldSpec(this.tableName, fieldName, { kind: "enum", values }, options),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
json(fieldName: string, tsType: string, options: FieldOptions = {}) {
|
|
205
|
+
return this.addField(
|
|
206
|
+
createFieldSpec(this.tableName, fieldName, { kind: "json", tsType }, options),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
temporalInstant(fieldName: string, options: FieldOptions = {}) {
|
|
211
|
+
return this.addField(
|
|
212
|
+
createFieldSpec(this.tableName, fieldName, { kind: "temporal.instant" }, options),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
temporalPlainDate(fieldName: string, options: FieldOptions = {}) {
|
|
217
|
+
return this.addField(
|
|
218
|
+
createFieldSpec(this.tableName, fieldName, { kind: "temporal.plainDate" }, options),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
reference(
|
|
223
|
+
fieldName: string,
|
|
224
|
+
options: Omit<FieldOptions, "references"> & { references: FieldReferenceSpec },
|
|
225
|
+
) {
|
|
226
|
+
return this.addField(
|
|
227
|
+
createFieldSpec(
|
|
228
|
+
this.tableName,
|
|
229
|
+
fieldName,
|
|
230
|
+
{ kind: "string" },
|
|
231
|
+
{ ...options, references: options.references },
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
index(fields: string[], options: { name?: string } = {}) {
|
|
237
|
+
this.indexes.push({
|
|
238
|
+
fields,
|
|
239
|
+
name: options.name,
|
|
240
|
+
});
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
unique(fields: string[], options: { name?: string } = {}) {
|
|
245
|
+
this.uniques.push({
|
|
246
|
+
fields,
|
|
247
|
+
name: options.name,
|
|
248
|
+
});
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
build(): TableSpec {
|
|
253
|
+
return {
|
|
254
|
+
id: tableAutoId(this.tableName),
|
|
255
|
+
name: this.tableName,
|
|
256
|
+
description: this.descriptionText,
|
|
257
|
+
fields: this.fields,
|
|
258
|
+
indexes: this.indexes,
|
|
259
|
+
uniques: this.uniques,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
class TableAlterBuilder {
|
|
265
|
+
readonly operations: MigrationOperation[] = [];
|
|
266
|
+
|
|
267
|
+
constructor(private readonly tableName: string) {}
|
|
268
|
+
|
|
269
|
+
private addField(field: FieldSpec) {
|
|
270
|
+
const operation: MigrationOperation = {
|
|
271
|
+
kind: "addField",
|
|
272
|
+
tableName: this.tableName,
|
|
273
|
+
field,
|
|
274
|
+
};
|
|
275
|
+
this.operations.push(operation);
|
|
276
|
+
return new MutableFieldBuilder(field);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
string(fieldName: string, options: FieldOptions & { format?: "email" | "url" | "slug" } = {}) {
|
|
280
|
+
return this.addField(
|
|
281
|
+
createFieldSpec(
|
|
282
|
+
this.tableName,
|
|
283
|
+
fieldName,
|
|
284
|
+
{ kind: "string", format: options.format },
|
|
285
|
+
options,
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
text(fieldName: string, options: FieldOptions = {}) {
|
|
291
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "text" }, options));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
boolean(fieldName: string, options: FieldOptions = {}) {
|
|
295
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "boolean" }, options));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
integer(fieldName: string, options: FieldOptions = {}) {
|
|
299
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "integer" }, options));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
number(fieldName: string, options: FieldOptions = {}) {
|
|
303
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "number" }, options));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
enum(fieldName: string, values: string[], options: FieldOptions = {}) {
|
|
307
|
+
return this.addField(
|
|
308
|
+
createFieldSpec(this.tableName, fieldName, { kind: "enum", values }, options),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
json(fieldName: string, tsType: string, options: FieldOptions = {}) {
|
|
313
|
+
return this.addField(
|
|
314
|
+
createFieldSpec(this.tableName, fieldName, { kind: "json", tsType }, options),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
temporalInstant(fieldName: string, options: FieldOptions = {}) {
|
|
319
|
+
return this.addField(
|
|
320
|
+
createFieldSpec(this.tableName, fieldName, { kind: "temporal.instant" }, options),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
temporalPlainDate(fieldName: string, options: FieldOptions = {}) {
|
|
325
|
+
return this.addField(
|
|
326
|
+
createFieldSpec(this.tableName, fieldName, { kind: "temporal.plainDate" }, options),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
reference(
|
|
331
|
+
fieldName: string,
|
|
332
|
+
options: Omit<FieldOptions, "references"> & { references: FieldReferenceSpec },
|
|
333
|
+
) {
|
|
334
|
+
return this.addField(
|
|
335
|
+
createFieldSpec(
|
|
336
|
+
this.tableName,
|
|
337
|
+
fieldName,
|
|
338
|
+
{ kind: "string" },
|
|
339
|
+
{ ...options, references: options.references },
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
dropField(fieldName: string) {
|
|
345
|
+
this.operations.push({
|
|
346
|
+
kind: "dropField",
|
|
347
|
+
tableName: this.tableName,
|
|
348
|
+
fieldName,
|
|
349
|
+
});
|
|
350
|
+
return this;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
renameField(from: string, to: string) {
|
|
354
|
+
this.operations.push({
|
|
355
|
+
kind: "renameField",
|
|
356
|
+
tableName: this.tableName,
|
|
357
|
+
from,
|
|
358
|
+
to,
|
|
359
|
+
});
|
|
360
|
+
return this;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
addIndex(fields: string[], options: { name?: string } = {}) {
|
|
364
|
+
this.operations.push({
|
|
365
|
+
kind: "addIndex",
|
|
366
|
+
tableName: this.tableName,
|
|
367
|
+
index: {
|
|
368
|
+
fields,
|
|
369
|
+
name: options.name,
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
return this;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
dropIndex(indexName: string) {
|
|
376
|
+
this.operations.push({
|
|
377
|
+
kind: "dropIndex",
|
|
378
|
+
tableName: this.tableName,
|
|
379
|
+
indexName,
|
|
380
|
+
});
|
|
381
|
+
return this;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
addUnique(fields: string[], options: { name?: string } = {}) {
|
|
385
|
+
this.operations.push({
|
|
386
|
+
kind: "addUnique",
|
|
387
|
+
tableName: this.tableName,
|
|
388
|
+
unique: {
|
|
389
|
+
fields,
|
|
390
|
+
name: options.name,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
return this;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
dropUnique(uniqueName: string) {
|
|
397
|
+
this.operations.push({
|
|
398
|
+
kind: "dropUnique",
|
|
399
|
+
tableName: this.tableName,
|
|
400
|
+
uniqueName,
|
|
401
|
+
});
|
|
402
|
+
return this;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
class MigrationBuilderImpl {
|
|
407
|
+
readonly operations: MigrationOperation[] = [];
|
|
408
|
+
|
|
409
|
+
createTable(name: string, callback: (table: TableCreateBuilder) => void) {
|
|
410
|
+
const table = new TableCreateBuilder(name);
|
|
411
|
+
callback(table);
|
|
412
|
+
this.operations.push({
|
|
413
|
+
kind: "createTable",
|
|
414
|
+
table: table.build(),
|
|
415
|
+
});
|
|
416
|
+
return this;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
dropTable(tableName: string) {
|
|
420
|
+
this.operations.push({
|
|
421
|
+
kind: "dropTable",
|
|
422
|
+
tableName,
|
|
423
|
+
});
|
|
424
|
+
return this;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
renameTable(from: string, to: string) {
|
|
428
|
+
this.operations.push({
|
|
429
|
+
kind: "renameTable",
|
|
430
|
+
from,
|
|
431
|
+
to,
|
|
432
|
+
});
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
alterTable(tableName: string, callback: (table: TableAlterBuilder) => void) {
|
|
437
|
+
const table = new TableAlterBuilder(tableName);
|
|
438
|
+
callback(table);
|
|
439
|
+
this.operations.push(...table.operations);
|
|
440
|
+
return this;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function createMigration(
|
|
445
|
+
meta: MigrationMeta,
|
|
446
|
+
callback: (migration: MigrationBuilderImpl) => void,
|
|
447
|
+
): MigrationDefinition {
|
|
448
|
+
return {
|
|
449
|
+
meta,
|
|
450
|
+
buildOperations() {
|
|
451
|
+
const builder = new MigrationBuilderImpl();
|
|
452
|
+
callback(builder);
|
|
453
|
+
return builder.operations;
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
package/src/planner.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { DatabaseSchemaDocument, IndexSpec, TableSpec, UniqueSpec } from "./types";
|
|
2
|
+
import { type MigrationDefinition, type MigrationOperation } from "./migration";
|
|
3
|
+
import { assertValidSchemaDocument, createEmptySchema, findField, findTable } from "./schema";
|
|
4
|
+
import { renderSqliteMigration } from "./sqlite";
|
|
5
|
+
import { cloneSchema, createSchemaHash, renameStorageColumn } from "./utils";
|
|
6
|
+
|
|
7
|
+
export type PlannedMigration = {
|
|
8
|
+
migrationId: string;
|
|
9
|
+
migrationName: string;
|
|
10
|
+
fromSchemaHash: string;
|
|
11
|
+
toSchemaHash: string;
|
|
12
|
+
operations: MigrationOperation[];
|
|
13
|
+
nextSchema: DatabaseSchemaDocument;
|
|
14
|
+
sql: {
|
|
15
|
+
statements: string[];
|
|
16
|
+
warnings: string[];
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function planMigration(args: {
|
|
21
|
+
currentSchema?: DatabaseSchemaDocument;
|
|
22
|
+
migration: MigrationDefinition;
|
|
23
|
+
}): PlannedMigration {
|
|
24
|
+
const currentSchema = args.currentSchema
|
|
25
|
+
? assertValidSchemaDocument(args.currentSchema)
|
|
26
|
+
: createEmptySchema(args.migration.meta.id);
|
|
27
|
+
const operations = args.migration.buildOperations();
|
|
28
|
+
const nextSchema = applyOperationsToSchema(currentSchema, operations);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
migrationId: args.migration.meta.id,
|
|
32
|
+
migrationName: args.migration.meta.name,
|
|
33
|
+
fromSchemaHash: createSchemaHash(currentSchema),
|
|
34
|
+
toSchemaHash: createSchemaHash(nextSchema),
|
|
35
|
+
operations,
|
|
36
|
+
nextSchema,
|
|
37
|
+
sql: renderSqliteMigration(operations),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function materializeSchema(args: {
|
|
42
|
+
baseSchema?: DatabaseSchemaDocument;
|
|
43
|
+
migrations: MigrationDefinition[];
|
|
44
|
+
}) {
|
|
45
|
+
let schema = args.baseSchema ? assertValidSchemaDocument(args.baseSchema) : createEmptySchema();
|
|
46
|
+
const plans: PlannedMigration[] = [];
|
|
47
|
+
|
|
48
|
+
for (const migration of args.migrations) {
|
|
49
|
+
const plan = planMigration({
|
|
50
|
+
currentSchema: schema,
|
|
51
|
+
migration,
|
|
52
|
+
});
|
|
53
|
+
plans.push(plan);
|
|
54
|
+
schema = plan.nextSchema;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
schema,
|
|
59
|
+
plans,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function applyOperationsToSchema(
|
|
64
|
+
schemaInput: DatabaseSchemaDocument,
|
|
65
|
+
operations: MigrationOperation[],
|
|
66
|
+
) {
|
|
67
|
+
const schema = cloneSchema(schemaInput);
|
|
68
|
+
|
|
69
|
+
for (const operation of operations) {
|
|
70
|
+
switch (operation.kind) {
|
|
71
|
+
case "createTable":
|
|
72
|
+
if (findTable(schema, operation.table.name)) {
|
|
73
|
+
throw new Error(`Table ${operation.table.name} already exists`);
|
|
74
|
+
}
|
|
75
|
+
schema.tables.push(cloneSchema(operation.table));
|
|
76
|
+
break;
|
|
77
|
+
case "dropTable": {
|
|
78
|
+
const referencedBy = schema.tables.flatMap((table) =>
|
|
79
|
+
table.fields
|
|
80
|
+
.filter((field) => field.references?.table === operation.tableName)
|
|
81
|
+
.map((field) => `${table.name}.${field.name}`),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (referencedBy.length > 0) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Cannot drop table ${operation.tableName}; still referenced by ${referencedBy.join(", ")}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const index = schema.tables.findIndex((table) => table.name === operation.tableName);
|
|
91
|
+
if (index < 0) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
92
|
+
schema.tables.splice(index, 1);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "renameTable": {
|
|
96
|
+
const table = findTable(schema, operation.from);
|
|
97
|
+
if (!table) throw new Error(`Table ${operation.from} does not exist`);
|
|
98
|
+
if (findTable(schema, operation.to)) {
|
|
99
|
+
throw new Error(`Table ${operation.to} already exists`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
table.name = operation.to;
|
|
103
|
+
for (const candidateTable of schema.tables) {
|
|
104
|
+
for (const field of candidateTable.fields) {
|
|
105
|
+
if (field.references?.table === operation.from) {
|
|
106
|
+
field.references = {
|
|
107
|
+
...field.references,
|
|
108
|
+
table: operation.to,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case "addField": {
|
|
116
|
+
const table = findTable(schema, operation.tableName);
|
|
117
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
118
|
+
if (findField(table, operation.field.name)) {
|
|
119
|
+
throw new Error(`Field ${operation.tableName}.${operation.field.name} already exists`);
|
|
120
|
+
}
|
|
121
|
+
table.fields.push(cloneSchema(operation.field));
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case "dropField": {
|
|
125
|
+
const table = findTable(schema, operation.tableName);
|
|
126
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
127
|
+
const fieldIndex = table.fields.findIndex((field) => field.name === operation.fieldName);
|
|
128
|
+
if (fieldIndex < 0) {
|
|
129
|
+
throw new Error(`Field ${operation.tableName}.${operation.fieldName} does not exist`);
|
|
130
|
+
}
|
|
131
|
+
const referencedBy = schema.tables.flatMap((candidateTable) =>
|
|
132
|
+
candidateTable.fields
|
|
133
|
+
.filter(
|
|
134
|
+
(field) =>
|
|
135
|
+
field.references?.table === operation.tableName &&
|
|
136
|
+
field.references.field === operation.fieldName,
|
|
137
|
+
)
|
|
138
|
+
.map((field) => `${candidateTable.name}.${field.name}`),
|
|
139
|
+
);
|
|
140
|
+
if (referencedBy.length > 0) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Cannot drop field ${operation.tableName}.${operation.fieldName}; still referenced by ${referencedBy.join(", ")}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
table.fields.splice(fieldIndex, 1);
|
|
146
|
+
table.indexes = table.indexes.filter(
|
|
147
|
+
(index) => !index.fields.includes(operation.fieldName),
|
|
148
|
+
);
|
|
149
|
+
table.uniques = table.uniques.filter(
|
|
150
|
+
(unique) => !unique.fields.includes(operation.fieldName),
|
|
151
|
+
);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "renameField": {
|
|
155
|
+
const table = findTable(schema, operation.tableName);
|
|
156
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
157
|
+
const field = findField(table, operation.from);
|
|
158
|
+
if (!field)
|
|
159
|
+
throw new Error(`Field ${operation.tableName}.${operation.from} does not exist`);
|
|
160
|
+
if (findField(table, operation.to)) {
|
|
161
|
+
throw new Error(`Field ${operation.tableName}.${operation.to} already exists`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const renamed = renameStorageColumn(field, operation.to);
|
|
165
|
+
const index = table.fields.findIndex((candidate) => candidate.id === field.id);
|
|
166
|
+
table.fields[index] = renamed;
|
|
167
|
+
table.indexes = renameFieldInIndexes(table.indexes, operation.from, operation.to);
|
|
168
|
+
table.uniques = renameFieldInUniques(table.uniques, operation.from, operation.to);
|
|
169
|
+
|
|
170
|
+
for (const candidateTable of schema.tables) {
|
|
171
|
+
for (const candidateField of candidateTable.fields) {
|
|
172
|
+
if (
|
|
173
|
+
candidateField.references?.table === table.name &&
|
|
174
|
+
candidateField.references.field === operation.from
|
|
175
|
+
) {
|
|
176
|
+
candidateField.references = {
|
|
177
|
+
...candidateField.references,
|
|
178
|
+
field: operation.to,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "addIndex": {
|
|
186
|
+
const table = findTable(schema, operation.tableName);
|
|
187
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
188
|
+
table.indexes.push(cloneSchema(operation.index));
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case "dropIndex": {
|
|
192
|
+
const table = findTable(schema, operation.tableName);
|
|
193
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
194
|
+
const nextIndexes = table.indexes.filter(
|
|
195
|
+
(index) => resolveIndexName(table.name, index) !== operation.indexName,
|
|
196
|
+
);
|
|
197
|
+
if (nextIndexes.length === table.indexes.length) {
|
|
198
|
+
throw new Error(`Index ${operation.indexName} does not exist on ${table.name}`);
|
|
199
|
+
}
|
|
200
|
+
table.indexes = nextIndexes;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "addUnique": {
|
|
204
|
+
const table = findTable(schema, operation.tableName);
|
|
205
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
206
|
+
table.uniques.push(cloneSchema(operation.unique));
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "dropUnique": {
|
|
210
|
+
const table = findTable(schema, operation.tableName);
|
|
211
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
212
|
+
const nextUniques = table.uniques.filter(
|
|
213
|
+
(unique) => resolveUniqueName(table.name, unique) !== operation.uniqueName,
|
|
214
|
+
);
|
|
215
|
+
if (nextUniques.length === table.uniques.length) {
|
|
216
|
+
throw new Error(`Unique ${operation.uniqueName} does not exist on ${table.name}`);
|
|
217
|
+
}
|
|
218
|
+
table.uniques = nextUniques;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return assertValidSchemaDocument(schema);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function renameFieldInIndexes(indexes: IndexSpec[], from: string, to: string): IndexSpec[] {
|
|
228
|
+
return indexes.map((index) => ({
|
|
229
|
+
...index,
|
|
230
|
+
fields: index.fields.map((field) => (field === from ? to : field)),
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function renameFieldInUniques(uniques: UniqueSpec[], from: string, to: string): UniqueSpec[] {
|
|
235
|
+
return uniques.map((unique) => ({
|
|
236
|
+
...unique,
|
|
237
|
+
fields: unique.fields.map((field) => (field === from ? to : field)),
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolveIndexName(tableName: string, index: IndexSpec) {
|
|
242
|
+
return index.name ?? `${tableName}_${index.fields.join("_")}_idx`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function resolveUniqueName(tableName: string, unique: UniqueSpec) {
|
|
246
|
+
return unique.name ?? `${tableName}_${unique.fields.join("_")}_unique`;
|
|
247
|
+
}
|