@syncular/migrations 0.0.6-244 → 0.0.6-246
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 +42 -11
- package/dist/checksum.d.ts +9 -0
- package/dist/checksum.d.ts.map +1 -0
- package/dist/checksum.js +142 -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 +50 -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 +15 -15
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/checksum.ts +190 -0
- package/src/define.ts +49 -201
- package/src/index.ts +1 -0
- package/src/runner.ts +94 -20
- package/src/tracking.ts +36 -3
- package/src/types.ts +21 -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,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
DISABLED_MIGRATION_CHECKSUM,
|
|
7
|
+
DISABLED_MIGRATION_CHECKSUM_ALGORITHM,
|
|
8
|
+
getLegacyMigrationChecksum,
|
|
9
|
+
getMigrationChecksumAlgorithm,
|
|
10
|
+
getStoredDeterministicChecksum,
|
|
11
|
+
LEGACY_SOURCE_MIGRATION_CHECKSUM_ALGORITHM,
|
|
12
|
+
} from './checksum';
|
|
9
13
|
import { DEFAULT_MIGRATION_TRACKING_TABLE } from './naming';
|
|
10
14
|
import {
|
|
11
15
|
clearAppliedMigrations,
|
|
@@ -15,6 +19,9 @@ import {
|
|
|
15
19
|
removeAppliedMigration,
|
|
16
20
|
} from './tracking';
|
|
17
21
|
import type {
|
|
22
|
+
DefinedMigrations,
|
|
23
|
+
MigrationChecksumAlgorithm,
|
|
24
|
+
ParsedMigration,
|
|
18
25
|
RunMigrationsOptions,
|
|
19
26
|
RunMigrationsResult,
|
|
20
27
|
RunMigrationsToVersionOptions,
|
|
@@ -35,6 +42,43 @@ function isAlreadyExistsSchemaError(error: unknown): boolean {
|
|
|
35
42
|
);
|
|
36
43
|
}
|
|
37
44
|
|
|
45
|
+
function isDeterministicMigration<DB>(migration: ParsedMigration<DB>): boolean {
|
|
46
|
+
return migration.checksum === 'deterministic';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getDeterministicMigrations<DB>(
|
|
50
|
+
migrations: DefinedMigrations<DB>
|
|
51
|
+
): ParsedMigration<DB>[] {
|
|
52
|
+
return migrations.migrations.filter(isDeterministicMigration);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function getStoredChecksumForMigration<DB>(
|
|
56
|
+
options: RunMigrationsOptions<DB>,
|
|
57
|
+
migration: ParsedMigration<DB>
|
|
58
|
+
): Promise<string> {
|
|
59
|
+
return getStoredDeterministicChecksum(migration, options.checksums);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function getChecksumForAlgorithm<DB>(
|
|
63
|
+
options: RunMigrationsOptions<DB>,
|
|
64
|
+
migration: ParsedMigration<DB>,
|
|
65
|
+
algorithm: MigrationChecksumAlgorithm
|
|
66
|
+
): Promise<string> {
|
|
67
|
+
if (algorithm === DISABLED_MIGRATION_CHECKSUM_ALGORITHM) {
|
|
68
|
+
return DISABLED_MIGRATION_CHECKSUM;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (algorithm === LEGACY_SOURCE_MIGRATION_CHECKSUM_ALGORITHM) {
|
|
72
|
+
return getLegacyMigrationChecksum(migration);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (algorithm === 'sql_trace_v1') {
|
|
76
|
+
return await getStoredChecksumForMigration(options, migration);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new Error(`Unsupported migration checksum algorithm: ${algorithm}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
38
82
|
async function runWithMigrationQueue<T>(
|
|
39
83
|
queueKey: string,
|
|
40
84
|
task: () => Promise<T>
|
|
@@ -66,8 +110,14 @@ async function runWithMigrationQueue<T>(
|
|
|
66
110
|
* import { defineMigrations, runMigrations } from '@syncular/migrations';
|
|
67
111
|
*
|
|
68
112
|
* const migrations = defineMigrations({
|
|
69
|
-
* v1:
|
|
70
|
-
*
|
|
113
|
+
* v1: {
|
|
114
|
+
* up: async (db) => { ... },
|
|
115
|
+
* down: async (db) => { ... },
|
|
116
|
+
* },
|
|
117
|
+
* v2: {
|
|
118
|
+
* up: async (db) => { ... },
|
|
119
|
+
* down: async (db) => { ... },
|
|
120
|
+
* },
|
|
71
121
|
* });
|
|
72
122
|
*
|
|
73
123
|
* const result = await runMigrations({
|
|
@@ -130,16 +180,28 @@ export async function runMigrationsToVersion<DB>(
|
|
|
130
180
|
const revertedVersions: number[] = [];
|
|
131
181
|
let wasReset = false;
|
|
132
182
|
let recoveredFromSchemaConflict = false;
|
|
183
|
+
const deterministicMigrations = getDeterministicMigrations(migrations);
|
|
133
184
|
|
|
134
185
|
// Check for checksum mismatches up-front when reset mode is enabled
|
|
135
186
|
if (onChecksumMismatch === 'reset' && applied.length > 0) {
|
|
136
|
-
|
|
187
|
+
let hasMismatch = false;
|
|
188
|
+
|
|
189
|
+
for (const migration of deterministicMigrations) {
|
|
137
190
|
const existing = appliedByVersion.get(migration.version);
|
|
138
|
-
if (!existing)
|
|
139
|
-
|
|
140
|
-
|
|
191
|
+
if (!existing) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const currentChecksum = await getChecksumForAlgorithm(
|
|
196
|
+
options,
|
|
197
|
+
migration,
|
|
198
|
+
existing.checksum_algorithm
|
|
141
199
|
);
|
|
142
|
-
|
|
200
|
+
if (existing.checksum !== currentChecksum) {
|
|
201
|
+
hasMismatch = true;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
143
205
|
|
|
144
206
|
if (hasMismatch) {
|
|
145
207
|
// Let caller drop application tables first
|
|
@@ -159,9 +221,18 @@ export async function runMigrationsToVersion<DB>(
|
|
|
159
221
|
if (!existing) {
|
|
160
222
|
continue;
|
|
161
223
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
224
|
+
|
|
225
|
+
if (migration.checksum === 'disabled') {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const currentChecksum = await getChecksumForAlgorithm(
|
|
230
|
+
options,
|
|
231
|
+
migration,
|
|
232
|
+
existing.checksum_algorithm
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (existing.checksum !== currentChecksum) {
|
|
165
236
|
throw new Error(
|
|
166
237
|
`Migration v${migration.version} (${migration.name}) has changed since it was applied. ` +
|
|
167
238
|
`Stored checksum ${existing.checksum} is not compatible with current checksum ${currentChecksum}. ` +
|
|
@@ -209,10 +280,19 @@ export async function runMigrationsToVersion<DB>(
|
|
|
209
280
|
continue;
|
|
210
281
|
}
|
|
211
282
|
|
|
283
|
+
const checksum = await getStoredChecksumForMigration(
|
|
284
|
+
options,
|
|
285
|
+
migration
|
|
286
|
+
);
|
|
287
|
+
|
|
212
288
|
await recordAppliedMigration(db, trackingTable, {
|
|
213
289
|
version: migration.version,
|
|
214
290
|
name: migration.name,
|
|
215
|
-
checksum
|
|
291
|
+
checksum,
|
|
292
|
+
checksum_algorithm: getMigrationChecksumAlgorithm(
|
|
293
|
+
migration,
|
|
294
|
+
options.checksums
|
|
295
|
+
),
|
|
216
296
|
});
|
|
217
297
|
appliedVersions.push(migration.version);
|
|
218
298
|
}
|
|
@@ -228,12 +308,6 @@ export async function runMigrationsToVersion<DB>(
|
|
|
228
308
|
`Cannot revert migration v${version}: migration is not defined in current migration set.`
|
|
229
309
|
);
|
|
230
310
|
}
|
|
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
311
|
await migration.down(db);
|
|
238
312
|
await removeAppliedMigration(db, trackingTable, version);
|
|
239
313
|
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 MigrationChecksumAlgorithm =
|
|
15
|
+
| 'legacy_source_v1'
|
|
16
|
+
| 'sql_trace_v1'
|
|
17
|
+
| 'disabled';
|
|
18
|
+
|
|
19
|
+
export type MigrationChecksums = Record<string, string>;
|
|
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
|
/**
|
|
@@ -84,6 +87,8 @@ export interface RunMigrationsOptions<DB = unknown> {
|
|
|
84
87
|
db: Kysely<DB>;
|
|
85
88
|
/** Defined migrations from defineMigrations() */
|
|
86
89
|
migrations: DefinedMigrations<DB>;
|
|
90
|
+
/** Generated deterministic checksums for this migration set. */
|
|
91
|
+
checksums?: MigrationChecksums;
|
|
87
92
|
/** Name of the tracking table (default: 'sync_migration_state') */
|
|
88
93
|
trackingTable?: string;
|
|
89
94
|
/** What to do when a migration's checksum doesn't match. Default: 'error' */
|