@syncular/migrations 0.0.6-243 → 0.0.6-245
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 +29 -5
- package/dist/checksum.d.ts +11 -0
- package/dist/checksum.d.ts.map +1 -0
- package/dist/checksum.js +355 -0
- package/dist/checksum.js.map +1 -0
- package/dist/define.d.ts +22 -18
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +39 -166
- package/dist/define.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/runner.d.ts +8 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +69 -15
- package/dist/runner.js.map +1 -1
- package/dist/tracking.d.ts.map +1 -1
- package/dist/tracking.js +32 -3
- package/dist/tracking.js.map +1 -1
- package/dist/types.d.ts +13 -15
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -2
- package/src/checksum.ts +528 -0
- package/src/define.ts +49 -201
- package/src/index.ts +1 -0
- package/src/runner.ts +134 -19
- package/src/tracking.ts +36 -3
- package/src/types.ts +19 -16
package/src/define.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
DefinedMigrations,
|
|
7
|
+
MigrationChecksumMode,
|
|
7
8
|
MigrationDefinition,
|
|
8
|
-
MigrationFn,
|
|
9
9
|
MigrationRecord,
|
|
10
10
|
ParsedMigration,
|
|
11
11
|
} from './types';
|
|
@@ -21,149 +21,26 @@ function parseVersionKey(key: string): number | null {
|
|
|
21
21
|
return Number.isNaN(version) ? null : version;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*/
|
|
29
|
-
function stripCommentsPreservingStrings(source: string): string {
|
|
30
|
-
let out = '';
|
|
31
|
-
let i = 0;
|
|
32
|
-
let mode:
|
|
33
|
-
| 'code'
|
|
34
|
-
| 'singleQuote'
|
|
35
|
-
| 'doubleQuote'
|
|
36
|
-
| 'template'
|
|
37
|
-
| 'lineComment'
|
|
38
|
-
| 'blockComment' = 'code';
|
|
39
|
-
|
|
40
|
-
while (i < source.length) {
|
|
41
|
-
const char = source[i]!;
|
|
42
|
-
const next = source[i + 1];
|
|
43
|
-
|
|
44
|
-
if (mode === 'lineComment') {
|
|
45
|
-
if (char === '\n') {
|
|
46
|
-
out += '\n';
|
|
47
|
-
mode = 'code';
|
|
48
|
-
}
|
|
49
|
-
i += 1;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (mode === 'blockComment') {
|
|
54
|
-
if (char === '*' && next === '/') {
|
|
55
|
-
i += 2;
|
|
56
|
-
mode = 'code';
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (char === '\n') {
|
|
60
|
-
out += '\n';
|
|
61
|
-
}
|
|
62
|
-
i += 1;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (mode === 'singleQuote') {
|
|
67
|
-
out += char;
|
|
68
|
-
if (char === '\\' && next !== undefined) {
|
|
69
|
-
out += next;
|
|
70
|
-
i += 2;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (char === "'") {
|
|
74
|
-
mode = 'code';
|
|
75
|
-
}
|
|
76
|
-
i += 1;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (mode === 'doubleQuote') {
|
|
81
|
-
out += char;
|
|
82
|
-
if (char === '\\' && next !== undefined) {
|
|
83
|
-
out += next;
|
|
84
|
-
i += 2;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (char === '"') {
|
|
88
|
-
mode = 'code';
|
|
89
|
-
}
|
|
90
|
-
i += 1;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (mode === 'template') {
|
|
95
|
-
out += char;
|
|
96
|
-
if (char === '\\' && next !== undefined) {
|
|
97
|
-
out += next;
|
|
98
|
-
i += 2;
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (char === '`') {
|
|
102
|
-
mode = 'code';
|
|
103
|
-
}
|
|
104
|
-
i += 1;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (char === '/' && next === '/') {
|
|
109
|
-
mode = 'lineComment';
|
|
110
|
-
i += 2;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (char === '/' && next === '*') {
|
|
114
|
-
mode = 'blockComment';
|
|
115
|
-
i += 2;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (char === "'") {
|
|
119
|
-
mode = 'singleQuote';
|
|
120
|
-
out += char;
|
|
121
|
-
i += 1;
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (char === '"') {
|
|
125
|
-
mode = 'doubleQuote';
|
|
126
|
-
out += char;
|
|
127
|
-
i += 1;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (char === '`') {
|
|
131
|
-
mode = 'template';
|
|
132
|
-
out += char;
|
|
133
|
-
i += 1;
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
out += char;
|
|
138
|
-
i += 1;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return out;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function normalizeSource(source: string): string {
|
|
145
|
-
return stripCommentsPreservingStrings(source)
|
|
146
|
-
.replace(/\s+/g, ' ') // collapse whitespace
|
|
147
|
-
.trim();
|
|
24
|
+
function isMigrationDefinitionObject<DB>(
|
|
25
|
+
value: MigrationDefinition<DB>
|
|
26
|
+
): value is MigrationDefinition<DB> {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
148
28
|
}
|
|
149
29
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
30
|
+
function normalizeChecksumMode(
|
|
31
|
+
key: string,
|
|
32
|
+
checksum: MigrationChecksumMode | undefined
|
|
33
|
+
): MigrationChecksumMode {
|
|
34
|
+
if (checksum === undefined) {
|
|
35
|
+
return 'deterministic';
|
|
36
|
+
}
|
|
37
|
+
if (checksum === 'deterministic' || checksum === 'disabled') {
|
|
38
|
+
return checksum;
|
|
159
39
|
}
|
|
160
|
-
return hash.toString(16).padStart(8, '0');
|
|
161
|
-
}
|
|
162
40
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
)
|
|
166
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Invalid migration "${key}": "checksum" must be "deterministic" or "disabled" when provided.`
|
|
43
|
+
);
|
|
167
44
|
}
|
|
168
45
|
|
|
169
46
|
/**
|
|
@@ -172,16 +49,28 @@ function isMigrationDefinitionObject<DB>(
|
|
|
172
49
|
* @example
|
|
173
50
|
* ```typescript
|
|
174
51
|
* export const migrations = defineMigrations({
|
|
175
|
-
* v1:
|
|
176
|
-
*
|
|
177
|
-
* .
|
|
178
|
-
*
|
|
179
|
-
*
|
|
52
|
+
* v1: {
|
|
53
|
+
* up: async (db) => {
|
|
54
|
+
* await db.schema.createTable('tasks')
|
|
55
|
+
* .addColumn('id', 'text', col => col.primaryKey())
|
|
56
|
+
* .addColumn('title', 'text', col => col.notNull())
|
|
57
|
+
* .execute();
|
|
58
|
+
* },
|
|
59
|
+
* down: async (db) => {
|
|
60
|
+
* await db.schema.dropTable('tasks').ifExists().execute();
|
|
61
|
+
* },
|
|
180
62
|
* },
|
|
181
|
-
* v2:
|
|
182
|
-
*
|
|
183
|
-
* .
|
|
184
|
-
*
|
|
63
|
+
* v2: {
|
|
64
|
+
* up: async (db) => {
|
|
65
|
+
* await db.schema.alterTable('tasks')
|
|
66
|
+
* .addColumn('priority', 'integer', col => col.defaultTo(0))
|
|
67
|
+
* .execute();
|
|
68
|
+
* },
|
|
69
|
+
* down: async (db) => {
|
|
70
|
+
* await db.schema.alterTable('tasks')
|
|
71
|
+
* .dropColumn('priority')
|
|
72
|
+
* .execute();
|
|
73
|
+
* },
|
|
185
74
|
* },
|
|
186
75
|
* });
|
|
187
76
|
* ```
|
|
@@ -205,45 +94,27 @@ export function defineMigrations<
|
|
|
205
94
|
);
|
|
206
95
|
}
|
|
207
96
|
|
|
208
|
-
|
|
209
|
-
? definition.up
|
|
210
|
-
: definition;
|
|
211
|
-
const down = isMigrationDefinitionObject(definition)
|
|
212
|
-
? definition.down
|
|
213
|
-
: undefined;
|
|
214
|
-
const compatibleChecksums = isMigrationDefinitionObject(definition)
|
|
215
|
-
? (definition.compatibleChecksums ?? [])
|
|
216
|
-
: [];
|
|
217
|
-
if (typeof up !== 'function') {
|
|
97
|
+
if (!isMigrationDefinitionObject(definition)) {
|
|
218
98
|
throw new Error(
|
|
219
|
-
`Invalid migration "${key}": expected
|
|
99
|
+
`Invalid migration "${key}": expected a { up, down } object. Shorthand migration functions are not supported.`
|
|
220
100
|
);
|
|
221
101
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
102
|
+
|
|
103
|
+
const { up, down } = definition;
|
|
104
|
+
const checksum = normalizeChecksumMode(key, definition.checksum);
|
|
105
|
+
|
|
106
|
+
if (typeof up !== 'function') {
|
|
107
|
+
throw new Error(`Invalid migration "${key}": "up" must be a function.`);
|
|
226
108
|
}
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
compatibleChecksums.some(
|
|
230
|
-
(checksum) =>
|
|
231
|
-
typeof checksum !== 'string' || checksum.trim().length === 0
|
|
232
|
-
)
|
|
233
|
-
) {
|
|
234
|
-
throw new Error(
|
|
235
|
-
`Invalid migration "${key}": "compatibleChecksums" must be an array of non-empty strings when provided.`
|
|
236
|
-
);
|
|
109
|
+
if (typeof down !== 'function') {
|
|
110
|
+
throw new Error(`Invalid migration "${key}": "down" must be a function.`);
|
|
237
111
|
}
|
|
238
|
-
|
|
239
112
|
migrations.push({
|
|
240
113
|
version,
|
|
241
114
|
name: key,
|
|
242
115
|
up,
|
|
243
116
|
down,
|
|
244
|
-
|
|
245
|
-
...new Set(compatibleChecksums.map((v) => v.trim())),
|
|
246
|
-
],
|
|
117
|
+
checksum,
|
|
247
118
|
});
|
|
248
119
|
}
|
|
249
120
|
|
|
@@ -268,26 +139,3 @@ export function defineMigrations<
|
|
|
268
139
|
},
|
|
269
140
|
};
|
|
270
141
|
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Get the checksum for a migration.
|
|
274
|
-
*/
|
|
275
|
-
export function getMigrationChecksum<DB>(
|
|
276
|
-
migration: ParsedMigration<DB>
|
|
277
|
-
): string {
|
|
278
|
-
return computeChecksum(migration.up);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Get the accepted checksums for a migration, including the current checksum.
|
|
283
|
-
*/
|
|
284
|
-
export function getCompatibleMigrationChecksums<DB>(
|
|
285
|
-
migration: ParsedMigration<DB>
|
|
286
|
-
): string[] {
|
|
287
|
-
return [
|
|
288
|
-
...new Set([
|
|
289
|
-
getMigrationChecksum(migration),
|
|
290
|
-
...migration.compatibleChecksums,
|
|
291
|
-
]),
|
|
292
|
-
];
|
|
293
|
-
}
|
package/src/index.ts
CHANGED
package/src/runner.ts
CHANGED
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
DISABLED_MIGRATION_CHECKSUM,
|
|
7
|
+
DISABLED_MIGRATION_CHECKSUM_ALGORITHM,
|
|
8
|
+
getLegacyMigrationChecksum,
|
|
7
9
|
getMigrationChecksum,
|
|
8
|
-
|
|
10
|
+
getMigrationChecksumAlgorithm,
|
|
11
|
+
inferMigrationChecksumDialect,
|
|
12
|
+
LEGACY_SOURCE_MIGRATION_CHECKSUM_ALGORITHM,
|
|
13
|
+
SQL_TRACE_MIGRATION_CHECKSUM_ALGORITHM,
|
|
14
|
+
} from './checksum';
|
|
9
15
|
import { DEFAULT_MIGRATION_TRACKING_TABLE } from './naming';
|
|
10
16
|
import {
|
|
11
17
|
clearAppliedMigrations,
|
|
@@ -15,6 +21,10 @@ import {
|
|
|
15
21
|
removeAppliedMigration,
|
|
16
22
|
} from './tracking';
|
|
17
23
|
import type {
|
|
24
|
+
DefinedMigrations,
|
|
25
|
+
MigrationChecksumAlgorithm,
|
|
26
|
+
MigrationChecksumDialect,
|
|
27
|
+
ParsedMigration,
|
|
18
28
|
RunMigrationsOptions,
|
|
19
29
|
RunMigrationsResult,
|
|
20
30
|
RunMigrationsToVersionOptions,
|
|
@@ -35,6 +45,77 @@ function isAlreadyExistsSchemaError(error: unknown): boolean {
|
|
|
35
45
|
);
|
|
36
46
|
}
|
|
37
47
|
|
|
48
|
+
function isDeterministicMigration<DB>(migration: ParsedMigration<DB>): boolean {
|
|
49
|
+
return migration.checksum === 'deterministic';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getDeterministicMigrations<DB>(
|
|
53
|
+
migrations: DefinedMigrations<DB>
|
|
54
|
+
): ParsedMigration<DB>[] {
|
|
55
|
+
return migrations.migrations.filter(isDeterministicMigration);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function requireChecksumDialect<DB>(
|
|
59
|
+
options: RunMigrationsOptions<DB>
|
|
60
|
+
): MigrationChecksumDialect {
|
|
61
|
+
const dialect = inferMigrationChecksumDialect(options.db);
|
|
62
|
+
|
|
63
|
+
if (dialect) {
|
|
64
|
+
return dialect;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error(
|
|
68
|
+
'Deterministic migration checksums are not supported for this runtime or dialect. ' +
|
|
69
|
+
'Set `checksum: "disabled"` on these migrations if they must run without checksum validation.'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function getStoredChecksumForMigration<DB>(
|
|
74
|
+
options: RunMigrationsOptions<DB>,
|
|
75
|
+
migration: ParsedMigration<DB>,
|
|
76
|
+
dialect: MigrationChecksumDialect | null
|
|
77
|
+
): Promise<string> {
|
|
78
|
+
if (migration.checksum === 'disabled') {
|
|
79
|
+
return DISABLED_MIGRATION_CHECKSUM;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const resolvedDialect = dialect ?? requireChecksumDialect(options);
|
|
83
|
+
const checksum = await getMigrationChecksum(
|
|
84
|
+
options.migrations,
|
|
85
|
+
migration,
|
|
86
|
+
resolvedDialect
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (!checksum) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Migration v${migration.version} (${migration.name}) is configured for deterministic checksums but did not produce one.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return checksum;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function getChecksumForAlgorithm<DB>(
|
|
99
|
+
options: RunMigrationsOptions<DB>,
|
|
100
|
+
migration: ParsedMigration<DB>,
|
|
101
|
+
algorithm: MigrationChecksumAlgorithm,
|
|
102
|
+
dialect: MigrationChecksumDialect | null
|
|
103
|
+
): Promise<string> {
|
|
104
|
+
if (algorithm === DISABLED_MIGRATION_CHECKSUM_ALGORITHM) {
|
|
105
|
+
return DISABLED_MIGRATION_CHECKSUM;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (algorithm === LEGACY_SOURCE_MIGRATION_CHECKSUM_ALGORITHM) {
|
|
109
|
+
return getLegacyMigrationChecksum(migration);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (algorithm === SQL_TRACE_MIGRATION_CHECKSUM_ALGORITHM) {
|
|
113
|
+
return await getStoredChecksumForMigration(options, migration, dialect);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw new Error(`Unsupported migration checksum algorithm: ${algorithm}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
38
119
|
async function runWithMigrationQueue<T>(
|
|
39
120
|
queueKey: string,
|
|
40
121
|
task: () => Promise<T>
|
|
@@ -66,8 +147,14 @@ async function runWithMigrationQueue<T>(
|
|
|
66
147
|
* import { defineMigrations, runMigrations } from '@syncular/migrations';
|
|
67
148
|
*
|
|
68
149
|
* const migrations = defineMigrations({
|
|
69
|
-
* v1:
|
|
70
|
-
*
|
|
150
|
+
* v1: {
|
|
151
|
+
* up: async (db) => { ... },
|
|
152
|
+
* down: async (db) => { ... },
|
|
153
|
+
* },
|
|
154
|
+
* v2: {
|
|
155
|
+
* up: async (db) => { ... },
|
|
156
|
+
* down: async (db) => { ... },
|
|
157
|
+
* },
|
|
71
158
|
* });
|
|
72
159
|
*
|
|
73
160
|
* const result = await runMigrations({
|
|
@@ -130,16 +217,33 @@ export async function runMigrationsToVersion<DB>(
|
|
|
130
217
|
const revertedVersions: number[] = [];
|
|
131
218
|
let wasReset = false;
|
|
132
219
|
let recoveredFromSchemaConflict = false;
|
|
220
|
+
const deterministicMigrations = getDeterministicMigrations(migrations);
|
|
221
|
+
const checksumDialect =
|
|
222
|
+
deterministicMigrations.length > 0
|
|
223
|
+
? requireChecksumDialect(options)
|
|
224
|
+
: null;
|
|
133
225
|
|
|
134
226
|
// Check for checksum mismatches up-front when reset mode is enabled
|
|
135
227
|
if (onChecksumMismatch === 'reset' && applied.length > 0) {
|
|
136
|
-
|
|
228
|
+
let hasMismatch = false;
|
|
229
|
+
|
|
230
|
+
for (const migration of deterministicMigrations) {
|
|
137
231
|
const existing = appliedByVersion.get(migration.version);
|
|
138
|
-
if (!existing)
|
|
139
|
-
|
|
140
|
-
|
|
232
|
+
if (!existing) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const currentChecksum = await getChecksumForAlgorithm(
|
|
237
|
+
options,
|
|
238
|
+
migration,
|
|
239
|
+
existing.checksum_algorithm,
|
|
240
|
+
checksumDialect
|
|
141
241
|
);
|
|
142
|
-
|
|
242
|
+
if (existing.checksum !== currentChecksum) {
|
|
243
|
+
hasMismatch = true;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
143
247
|
|
|
144
248
|
if (hasMismatch) {
|
|
145
249
|
// Let caller drop application tables first
|
|
@@ -159,9 +263,19 @@ export async function runMigrationsToVersion<DB>(
|
|
|
159
263
|
if (!existing) {
|
|
160
264
|
continue;
|
|
161
265
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
266
|
+
|
|
267
|
+
if (migration.checksum === 'disabled') {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const currentChecksum = await getChecksumForAlgorithm(
|
|
272
|
+
options,
|
|
273
|
+
migration,
|
|
274
|
+
existing.checksum_algorithm,
|
|
275
|
+
checksumDialect
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (existing.checksum !== currentChecksum) {
|
|
165
279
|
throw new Error(
|
|
166
280
|
`Migration v${migration.version} (${migration.name}) has changed since it was applied. ` +
|
|
167
281
|
`Stored checksum ${existing.checksum} is not compatible with current checksum ${currentChecksum}. ` +
|
|
@@ -209,10 +323,17 @@ export async function runMigrationsToVersion<DB>(
|
|
|
209
323
|
continue;
|
|
210
324
|
}
|
|
211
325
|
|
|
326
|
+
const checksum = await getStoredChecksumForMigration(
|
|
327
|
+
options,
|
|
328
|
+
migration,
|
|
329
|
+
checksumDialect
|
|
330
|
+
);
|
|
331
|
+
|
|
212
332
|
await recordAppliedMigration(db, trackingTable, {
|
|
213
333
|
version: migration.version,
|
|
214
334
|
name: migration.name,
|
|
215
|
-
checksum
|
|
335
|
+
checksum,
|
|
336
|
+
checksum_algorithm: getMigrationChecksumAlgorithm(migration),
|
|
216
337
|
});
|
|
217
338
|
appliedVersions.push(migration.version);
|
|
218
339
|
}
|
|
@@ -228,12 +349,6 @@ export async function runMigrationsToVersion<DB>(
|
|
|
228
349
|
`Cannot revert migration v${version}: migration is not defined in current migration set.`
|
|
229
350
|
);
|
|
230
351
|
}
|
|
231
|
-
if (typeof migration.down !== 'function') {
|
|
232
|
-
throw new Error(
|
|
233
|
-
`Cannot revert migration v${version} (${migration.name}): down migration is not defined.`
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
352
|
await migration.down(db);
|
|
238
353
|
await removeAppliedMigration(db, trackingTable, version);
|
|
239
354
|
revertedVersions.push(version);
|
package/src/tracking.ts
CHANGED
|
@@ -3,8 +3,22 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { type Kysely, sql } from 'kysely';
|
|
6
|
+
import { LEGACY_SOURCE_MIGRATION_CHECKSUM_ALGORITHM } from './checksum';
|
|
6
7
|
import type { MigrationStateRow } from './types';
|
|
7
8
|
|
|
9
|
+
function isDuplicateColumnError(error: unknown): boolean {
|
|
10
|
+
if (!(error instanceof Error)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const message = error.message.toLowerCase();
|
|
15
|
+
return (
|
|
16
|
+
message.includes('duplicate column') ||
|
|
17
|
+
message.includes('already exists') ||
|
|
18
|
+
(message.includes('column') && message.includes('exists'))
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
/**
|
|
9
23
|
* Ensure the migration tracking table exists.
|
|
10
24
|
*/
|
|
@@ -19,7 +33,19 @@ export async function ensureTrackingTable<DB>(
|
|
|
19
33
|
.addColumn('name', 'text', (col) => col.notNull())
|
|
20
34
|
.addColumn('applied_at', 'text', (col) => col.notNull())
|
|
21
35
|
.addColumn('checksum', 'text', (col) => col.notNull())
|
|
36
|
+
.addColumn('checksum_algorithm', 'text', (col) => col.notNull())
|
|
22
37
|
.execute();
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await sql`
|
|
41
|
+
alter table ${sql.table(tableName)}
|
|
42
|
+
add column checksum_algorithm text not null default ${sql.raw(`'${LEGACY_SOURCE_MIGRATION_CHECKSUM_ALGORITHM}'`)}
|
|
43
|
+
`.execute(db);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (!isDuplicateColumnError(error)) {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
23
49
|
}
|
|
24
50
|
|
|
25
51
|
/**
|
|
@@ -32,7 +58,7 @@ export async function getAppliedMigrations<DB, TTableName extends string>(
|
|
|
32
58
|
await ensureTrackingTable(db, tableName);
|
|
33
59
|
|
|
34
60
|
const result = await sql<MigrationStateRow>`
|
|
35
|
-
select version, name, applied_at, checksum
|
|
61
|
+
select version, name, applied_at, checksum, checksum_algorithm
|
|
36
62
|
from ${sql.table(tableName)}
|
|
37
63
|
order by version asc
|
|
38
64
|
`.execute(db);
|
|
@@ -51,12 +77,19 @@ export async function recordAppliedMigration<DB, TTableName extends string>(
|
|
|
51
77
|
await ensureTrackingTable(db, tableName);
|
|
52
78
|
|
|
53
79
|
await sql`
|
|
54
|
-
insert into ${sql.table(tableName)} (
|
|
80
|
+
insert into ${sql.table(tableName)} (
|
|
81
|
+
version,
|
|
82
|
+
name,
|
|
83
|
+
applied_at,
|
|
84
|
+
checksum,
|
|
85
|
+
checksum_algorithm
|
|
86
|
+
)
|
|
55
87
|
values (
|
|
56
88
|
${migration.version},
|
|
57
89
|
${migration.name},
|
|
58
90
|
${new Date().toISOString()},
|
|
59
|
-
${migration.checksum}
|
|
91
|
+
${migration.checksum},
|
|
92
|
+
${migration.checksum_algorithm}
|
|
60
93
|
)
|
|
61
94
|
`.execute(db);
|
|
62
95
|
}
|
package/src/types.ts
CHANGED
|
@@ -9,6 +9,15 @@ import type { Kysely } from 'kysely';
|
|
|
9
9
|
*/
|
|
10
10
|
export type MigrationFn<DB = unknown> = (db: Kysely<DB>) => Promise<void>;
|
|
11
11
|
|
|
12
|
+
export type MigrationChecksumMode = 'deterministic' | 'disabled';
|
|
13
|
+
|
|
14
|
+
export type MigrationChecksumDialect = 'sqlite' | 'postgres';
|
|
15
|
+
|
|
16
|
+
export type MigrationChecksumAlgorithm =
|
|
17
|
+
| 'legacy_source_v1'
|
|
18
|
+
| 'sql_trace_v1'
|
|
19
|
+
| 'disabled';
|
|
20
|
+
|
|
12
21
|
/**
|
|
13
22
|
* A reversible migration definition.
|
|
14
23
|
*/
|
|
@@ -16,21 +25,14 @@ export interface ReversibleMigrationDefinition<DB = unknown> {
|
|
|
16
25
|
/** Apply schema/data changes for this version. */
|
|
17
26
|
up: MigrationFn<DB>;
|
|
18
27
|
/** Revert schema/data changes for this version. */
|
|
19
|
-
down
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
* copies of this migration.
|
|
23
|
-
*/
|
|
24
|
-
compatibleChecksums?: string[];
|
|
28
|
+
down: MigrationFn<DB>;
|
|
29
|
+
/** Controls whether this migration participates in checksum validation. */
|
|
30
|
+
checksum?: MigrationChecksumMode;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
/**
|
|
28
|
-
* A migration definition can be a single "up" function or
|
|
29
|
-
* an object with explicit up/down handlers.
|
|
30
|
-
*/
|
|
33
|
+
/** A migration definition must explicitly provide both up/down handlers. */
|
|
31
34
|
export type MigrationDefinition<DB = unknown> =
|
|
32
|
-
|
|
33
|
-
| ReversibleMigrationDefinition<DB>;
|
|
35
|
+
ReversibleMigrationDefinition<DB>;
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* Record of versioned migrations keyed by version string (e.g., 'v1', 'v2').
|
|
@@ -48,10 +50,10 @@ export interface ParsedMigration<DB = unknown> {
|
|
|
48
50
|
name: string;
|
|
49
51
|
/** Up migration function. */
|
|
50
52
|
up: MigrationFn<DB>;
|
|
51
|
-
/**
|
|
52
|
-
down
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/** Down migration function. */
|
|
54
|
+
down: MigrationFn<DB>;
|
|
55
|
+
/** Controls whether this migration participates in checksum validation. */
|
|
56
|
+
checksum: MigrationChecksumMode;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/**
|
|
@@ -74,6 +76,7 @@ export interface MigrationStateRow {
|
|
|
74
76
|
name: string;
|
|
75
77
|
applied_at: string;
|
|
76
78
|
checksum: string;
|
|
79
|
+
checksum_algorithm: MigrationChecksumAlgorithm;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
/**
|