@visorcraft/mongreldb-kit 0.7.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.
Files changed (65) hide show
  1. package/dist/constraints.d.ts +35 -0
  2. package/dist/constraints.d.ts.map +1 -0
  3. package/dist/constraints.js +404 -0
  4. package/dist/constraints.js.map +1 -0
  5. package/dist/db.d.ts +135 -0
  6. package/dist/db.d.ts.map +1 -0
  7. package/dist/db.js +495 -0
  8. package/dist/db.js.map +1 -0
  9. package/dist/defaults.d.ts +26 -0
  10. package/dist/defaults.d.ts.map +1 -0
  11. package/dist/defaults.js +56 -0
  12. package/dist/defaults.js.map +1 -0
  13. package/dist/errors.d.ts +54 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/errors.js +104 -0
  16. package/dist/errors.js.map +1 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +13 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/internalTables.d.ts +63 -0
  22. package/dist/internalTables.d.ts.map +1 -0
  23. package/dist/internalTables.js +60 -0
  24. package/dist/internalTables.js.map +1 -0
  25. package/dist/keys.d.ts +7 -0
  26. package/dist/keys.d.ts.map +1 -0
  27. package/dist/keys.js +84 -0
  28. package/dist/keys.js.map +1 -0
  29. package/dist/migrate.d.ts +132 -0
  30. package/dist/migrate.d.ts.map +1 -0
  31. package/dist/migrate.js +1004 -0
  32. package/dist/migrate.js.map +1 -0
  33. package/dist/packing.d.ts +12 -0
  34. package/dist/packing.d.ts.map +1 -0
  35. package/dist/packing.js +137 -0
  36. package/dist/packing.js.map +1 -0
  37. package/dist/query.d.ts +423 -0
  38. package/dist/query.d.ts.map +1 -0
  39. package/dist/query.js +1645 -0
  40. package/dist/query.js.map +1 -0
  41. package/dist/remote.d.ts +29 -0
  42. package/dist/remote.d.ts.map +1 -0
  43. package/dist/remote.js +42 -0
  44. package/dist/remote.js.map +1 -0
  45. package/dist/rows.d.ts +5 -0
  46. package/dist/rows.d.ts.map +1 -0
  47. package/dist/rows.js +38 -0
  48. package/dist/rows.js.map +1 -0
  49. package/dist/schema.d.ts +91 -0
  50. package/dist/schema.d.ts.map +1 -0
  51. package/dist/schema.js +206 -0
  52. package/dist/schema.js.map +1 -0
  53. package/dist/tsv.d.ts +4 -0
  54. package/dist/tsv.d.ts.map +1 -0
  55. package/dist/tsv.js +102 -0
  56. package/dist/tsv.js.map +1 -0
  57. package/dist/types.d.ts +99 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +2 -0
  60. package/dist/types.js.map +1 -0
  61. package/dist/validation.d.ts +3 -0
  62. package/dist/validation.d.ts.map +1 -0
  63. package/dist/validation.js +98 -0
  64. package/dist/validation.js.map +1 -0
  65. package/package.json +50 -0
@@ -0,0 +1,1004 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { tableFromIPC } from 'apache-arrow';
3
+ import { table, int, text } from './schema.js';
4
+ import { toCells, pkValueFromRow, parentExists } from './constraints.js';
5
+ import { validateRow } from './validation.js';
6
+ import { KIT_KEY_VERSION, encodedPk, encodeRowGuardKey, encodeUniqueKey } from './keys.js';
7
+ import { KitMigrationError, KitSchemaDriftError, KitForeignKeyError } from './errors.js';
8
+ import { kitSchemaMigrations, kitSchemaCatalog, kitMigrationLocks, kitUniqueKeys, kitRowGuards } from './internalTables.js';
9
+ /**
10
+ * Schema of the legacy Kit sequence table that existed before engine-native
11
+ * auto-increment. When present, its rows are used as a one-time fallback to
12
+ * seed per-table engine counters so upgraded databases do not hand out ids
13
+ * below a sequence that was already advanced.
14
+ */
15
+ const legacyKitSequences = table('__kit_sequences', {
16
+ columns: [
17
+ text('sequence_name', { primaryKey: true }),
18
+ int('next_value', { nullable: false })
19
+ ],
20
+ primaryKey: ['sequence_name']
21
+ });
22
+ function seedFromLegacyKitSequences(kit) {
23
+ const db = kit.nativeDb;
24
+ if (!db.tableNames().includes('__kit_sequences')) {
25
+ return;
26
+ }
27
+ for (const { row } of fullScanRows(db, legacyKitSequences)) {
28
+ const sequenceName = row.sequence_name;
29
+ const nextValue = row.next_value;
30
+ if (!sequenceName || nextValue === null || nextValue <= 1n) {
31
+ continue;
32
+ }
33
+ const tableName = sequenceName.endsWith('_id_seq')
34
+ ? sequenceName.slice(0, -'_id_seq'.length)
35
+ : sequenceName;
36
+ if (!db.tableNames().includes(tableName)) {
37
+ continue;
38
+ }
39
+ // Advance the engine counter until it is at least the legacy next value.
40
+ // reserveAutoIncSync seeds from max(existing id) on its first call, so this
41
+ // also covers the case where rows already exist with higher ids.
42
+ while (true) {
43
+ const reserved = kit.reserveAutoIncSync(tableName);
44
+ if (reserved === null)
45
+ break;
46
+ if (reserved >= nextValue)
47
+ break;
48
+ }
49
+ }
50
+ }
51
+ const I64_MIN = -9223372036854775808n;
52
+ const I64_MAX = 9223372036854775807n;
53
+ const LOCK_NAME = 'default';
54
+ const LOCK_HOLDER = 'kit';
55
+ const LOCK_TTL_MS = 5 * 60 * 1000;
56
+ function columnId(table, name) {
57
+ const col = table.columns.find((c) => c.name === name);
58
+ if (!col) {
59
+ throw new Error(`Column "${name}" not found in table "${table.name}"`);
60
+ }
61
+ return col.id;
62
+ }
63
+ function isoNow() {
64
+ return new Date().toISOString();
65
+ }
66
+ function toMongrelColumnType(storageType) {
67
+ switch (storageType) {
68
+ case 'bool':
69
+ return 0 /* ColumnType.Bool */;
70
+ case 'int64':
71
+ case 'timestamp':
72
+ return 1 /* ColumnType.Int64 */;
73
+ case 'float64':
74
+ return 2 /* ColumnType.Float64 */;
75
+ case 'date':
76
+ return 4 /* ColumnType.Date32 */;
77
+ case 'text':
78
+ case 'bytes':
79
+ case 'json':
80
+ return 5 /* ColumnType.Bytes */;
81
+ case 'embedding':
82
+ return 6 /* ColumnType.Embedding */;
83
+ case 'sparse':
84
+ return 5 /* ColumnType.Bytes */;
85
+ }
86
+ }
87
+ function toMongrelColumnSpec(column) {
88
+ return {
89
+ id: column.id,
90
+ name: column.name,
91
+ ty: toMongrelColumnType(column.storageType),
92
+ primaryKey: column.primaryKey,
93
+ nullable: column.nullable,
94
+ autoIncrement: column.default?.kind === 'sequence',
95
+ embeddingDim: column.embeddingDim
96
+ };
97
+ }
98
+ function toMongrelSchema(table) {
99
+ const indexes = table.indexes.flatMap((idx) => idx.columns.map((colName) => {
100
+ const col = table.columns.find((c) => c.name === colName);
101
+ if (!col) {
102
+ throw new Error(`Index column "${colName}" not found in table "${table.name}"`);
103
+ }
104
+ return {
105
+ name: `${idx.name}_${colName}`,
106
+ columnId: col.id,
107
+ kind: 0 /* IndexKindSpec.Bitmap */
108
+ };
109
+ }));
110
+ const indexedColumns = new Set(table.indexes.flatMap((idx) => idx.columns));
111
+ for (const pk of table.primaryKey) {
112
+ if (indexedColumns.has(pk))
113
+ continue;
114
+ const col = table.columns.find((c) => c.name === pk);
115
+ if (!col) {
116
+ throw new Error(`Primary key column "${pk}" not found in table "${table.name}"`);
117
+ }
118
+ indexes.push({
119
+ name: `pk_${pk}`,
120
+ columnId: col.id,
121
+ kind: 0 /* IndexKindSpec.Bitmap */
122
+ });
123
+ indexedColumns.add(pk);
124
+ }
125
+ for (const fk of table.foreignKeys) {
126
+ for (const colName of fk.columns) {
127
+ if (indexedColumns.has(colName))
128
+ continue;
129
+ const col = table.columns.find((c) => c.name === colName);
130
+ if (!col) {
131
+ throw new Error(`Foreign key column "${colName}" not found in table "${table.name}"`);
132
+ }
133
+ indexes.push({
134
+ name: `fk_${fk.name}_${colName}`,
135
+ columnId: col.id,
136
+ kind: 0 /* IndexKindSpec.Bitmap */
137
+ });
138
+ indexedColumns.add(colName);
139
+ }
140
+ }
141
+ return {
142
+ columns: table.columns.map(toMongrelColumnSpec),
143
+ indexes
144
+ };
145
+ }
146
+ function cellValue(cell) {
147
+ if (!cell)
148
+ return null;
149
+ if (cell.text !== undefined)
150
+ return cell.text;
151
+ if (cell.int64 !== undefined)
152
+ return cell.int64;
153
+ if (cell.boolean !== undefined)
154
+ return cell.boolean;
155
+ if (cell.float64 !== undefined)
156
+ return cell.float64;
157
+ if (cell.bytes !== undefined)
158
+ return cell.bytes;
159
+ return null;
160
+ }
161
+ function rowFromRowJs(table, rowJs) {
162
+ const row = {};
163
+ for (const col of table.columns) {
164
+ const cell = rowJs.cells.find((c) => c.columnId === col.id);
165
+ row[col.name] = cellValue(cell);
166
+ }
167
+ return row;
168
+ }
169
+ function fullScanCondition(table) {
170
+ const intColumn = table.columns.find((c) => c.storageType === 'int64' || c.storageType === 'timestamp');
171
+ if (intColumn) {
172
+ return {
173
+ kind: 2 /* ConditionKind.RangeInt */,
174
+ columnId: intColumn.id,
175
+ int64Lo: I64_MIN,
176
+ int64Hi: I64_MAX
177
+ };
178
+ }
179
+ const floatColumn = table.columns.find((c) => c.storageType === 'float64');
180
+ if (floatColumn) {
181
+ return {
182
+ kind: 3 /* ConditionKind.RangeF64 */,
183
+ columnId: floatColumn.id,
184
+ float64Lo: -Infinity,
185
+ float64Hi: Infinity
186
+ };
187
+ }
188
+ throw new KitMigrationError(`Full table scan on "${table.name}" requires an int64, timestamp, or float64 column`);
189
+ }
190
+ function fullScanRows(db, table) {
191
+ const condition = fullScanCondition(table);
192
+ return db
193
+ .table(table.name)
194
+ .query([condition])
195
+ .map((rowJs) => ({ rowId: rowJs.rowId, row: rowFromRowJs(table, rowJs) }));
196
+ }
197
+ /**
198
+ * Canonical, language-neutral serialization of a single migration op. The key
199
+ * order is fixed and string values use standard JSON escaping (`JSON.stringify`),
200
+ * so this is byte-for-byte identical to the Rust kit's `canonical_op`
201
+ * (`crates/mongreldb-kit-core/src/migrations.rs`).
202
+ */
203
+ function canonicalOp(op) {
204
+ const s = (value) => JSON.stringify(value);
205
+ switch (op.kind) {
206
+ case 'createTable':
207
+ return `{"op":"create_table","name":${s(op.name)}}`;
208
+ case 'dropTable':
209
+ return `{"op":"drop_table","name":${s(op.name)}}`;
210
+ case 'addColumn':
211
+ return `{"op":"add_column","table":${s(op.table)},"column":${s(op.column)}}`;
212
+ case 'dropColumn':
213
+ return `{"op":"drop_column","table":${s(op.table)},"column":${s(op.column)}}`;
214
+ case 'alterColumn':
215
+ return `{"op":"alter_column","table":${s(op.table)},"column":${s(op.column)}}`;
216
+ case 'addIndex':
217
+ return `{"op":"add_index","table":${s(op.table)},"index":${s(op.index)}}`;
218
+ case 'dropIndex':
219
+ return `{"op":"drop_index","table":${s(op.table)},"index":${s(op.index)}}`;
220
+ case 'addUnique':
221
+ return `{"op":"add_unique","table":${s(op.table)},"constraint":${s(op.constraint)}}`;
222
+ case 'dropUnique':
223
+ return `{"op":"drop_unique","table":${s(op.table)},"constraint":${s(op.constraint)}}`;
224
+ case 'addForeignKey':
225
+ return `{"op":"add_foreign_key","table":${s(op.table)},"constraint":${s(op.constraint)}}`;
226
+ case 'dropForeignKey':
227
+ return `{"op":"drop_foreign_key","table":${s(op.table)},"constraint":${s(op.constraint)}}`;
228
+ case 'addCheck':
229
+ return `{"op":"add_check","table":${s(op.table)},"constraint":${s(op.constraint)}}`;
230
+ case 'dropCheck':
231
+ return `{"op":"drop_check","table":${s(op.table)},"constraint":${s(op.constraint)}}`;
232
+ case 'rawSql':
233
+ return `{"op":"raw_sql","sql":${s(op.sql)}}`;
234
+ default: {
235
+ const _exhaustive = op;
236
+ throw new Error(`Unknown migration op: ${JSON.stringify(_exhaustive)}`);
237
+ }
238
+ }
239
+ }
240
+ /**
241
+ * The canonical content string a migration's checksum is computed over. Shape:
242
+ * `{"version":<n>,"name":<json>,"ops":[<op>,...]}` with no insignificant
243
+ * whitespace, byte-identical to the Rust kit's `canonical_content`.
244
+ */
245
+ export function migrationContent(migration) {
246
+ const opsJson = (migration.ops ?? []).map(canonicalOp).join(',');
247
+ return `{"version":${migration.version},"name":${JSON.stringify(migration.name)},"ops":[${opsJson}]}`;
248
+ }
249
+ /**
250
+ * Content-aware SHA-256 checksum for a migration. Covers the version, name, and
251
+ * ordered op list via {@link migrationContent}; the same logical migration
252
+ * produces the identical checksum in TypeScript, Rust, and Python.
253
+ */
254
+ export function migrationChecksum(migration) {
255
+ return createHash('sha256').update(migrationContent(migration)).digest('hex');
256
+ }
257
+ function schemaCatalogJson(schema) {
258
+ return JSON.stringify(schema.tablesList().map((t) => ({
259
+ tableId: t.tableId,
260
+ name: t.name,
261
+ columns: t.columns.map((c) => ({
262
+ id: c.id,
263
+ name: c.name,
264
+ storageType: c.storageType,
265
+ applicationType: c.applicationType,
266
+ nullable: c.nullable,
267
+ primaryKey: c.primaryKey,
268
+ enumValues: c.enumValues,
269
+ min: c.min,
270
+ max: c.max,
271
+ minLength: c.minLength,
272
+ maxLength: c.maxLength,
273
+ regex: c.regex?.source
274
+ })),
275
+ primaryKey: t.primaryKey,
276
+ indexes: t.indexes,
277
+ foreignKeys: t.foreignKeys,
278
+ unique: t.unique,
279
+ checks: t.checks.map((c) => ({ name: c.name }))
280
+ })));
281
+ }
282
+ async function runSql(db, sql) {
283
+ const ipc = await db.sql(sql);
284
+ const arrowTable = tableFromIPC(ipc);
285
+ return [...arrowTable].map((row) => ({ ...row }));
286
+ }
287
+ async function acquireLock(kit) {
288
+ const db = kit.nativeDb;
289
+ const locks = db.table('__kit_migration_locks');
290
+ const now = new Date();
291
+ const expiresAt = new Date(now.getTime() + LOCK_TTL_MS);
292
+ const existing = locks.getByPkText(LOCK_NAME);
293
+ if (existing) {
294
+ const expiresAtCell = existing.cells.find((c) => c.columnId === columnId(kitMigrationLocks, 'expires_at'));
295
+ const existingExpires = expiresAtCell?.text;
296
+ if (existingExpires && new Date(existingExpires) > now) {
297
+ throw new KitMigrationError('migration lock is already held');
298
+ }
299
+ locks.deleteByPkText(LOCK_NAME);
300
+ }
301
+ locks.put([
302
+ { columnId: columnId(kitMigrationLocks, 'lock_name'), text: LOCK_NAME },
303
+ { columnId: columnId(kitMigrationLocks, 'holder'), text: LOCK_HOLDER },
304
+ { columnId: columnId(kitMigrationLocks, 'acquired_at'), text: now.toISOString() },
305
+ { columnId: columnId(kitMigrationLocks, 'expires_at'), text: expiresAt.toISOString() }
306
+ ]);
307
+ locks.commit();
308
+ }
309
+ async function releaseLock(kit) {
310
+ const locks = kit.nativeDb.table('__kit_migration_locks');
311
+ locks.deleteByPkText(LOCK_NAME);
312
+ locks.commit();
313
+ }
314
+ async function readAppliedMigrations(kit) {
315
+ const rows = fullScanRows(kit.nativeDb, kitSchemaMigrations);
316
+ return rows
317
+ .map((m) => ({
318
+ version: m.row.version,
319
+ name: m.row.name,
320
+ checksum: m.row.checksum,
321
+ status: m.row.status
322
+ }))
323
+ .sort((a, b) => Number(a.version - b.version));
324
+ }
325
+ async function insertMigrationRecord(kit, migration, status) {
326
+ const table = kit.nativeDb.table('__kit_schema_migrations');
327
+ table.put([
328
+ { columnId: columnId(kitSchemaMigrations, 'version'), int64: BigInt(migration.version) },
329
+ { columnId: columnId(kitSchemaMigrations, 'name'), text: migration.name },
330
+ { columnId: columnId(kitSchemaMigrations, 'checksum'), text: migrationChecksum(migration) },
331
+ { columnId: columnId(kitSchemaMigrations, 'applied_at'), text: isoNow() },
332
+ { columnId: columnId(kitSchemaMigrations, 'kit_version'), text: kitVersion() },
333
+ { columnId: columnId(kitSchemaMigrations, 'status'), text: status }
334
+ ]);
335
+ table.commit();
336
+ }
337
+ async function updateMigrationStatus(kit, version, status) {
338
+ const table = kit.nativeDb.table('__kit_schema_migrations');
339
+ const rowJs = table.getByPkInt64(BigInt(version));
340
+ if (!rowJs) {
341
+ throw new KitMigrationError(`migration record ${version} not found`);
342
+ }
343
+ const row = rowFromRowJs(kitSchemaMigrations, rowJs);
344
+ const updated = { ...row, status };
345
+ table.put(toCells(kitSchemaMigrations, updated));
346
+ table.commit();
347
+ }
348
+ async function writeSchemaCatalog(kit, schema) {
349
+ const db = kit.nativeDb;
350
+ const catalog = db.table('__kit_schema_catalog');
351
+ const schemaJson = schemaCatalogJson(schema);
352
+ const checksum = createHash('sha256').update(schemaJson).digest('hex');
353
+ const now = isoNow();
354
+ await db.transaction(async (txn) => {
355
+ txn.put('__kit_schema_catalog', [
356
+ { columnId: columnId(kitSchemaCatalog, 'schema_version'), int64: 1n },
357
+ { columnId: columnId(kitSchemaCatalog, 'schema_json'), text: schemaJson },
358
+ { columnId: columnId(kitSchemaCatalog, 'checksum'), text: checksum },
359
+ { columnId: columnId(kitSchemaCatalog, 'written_at'), text: now }
360
+ ]);
361
+ });
362
+ }
363
+ function kitVersion() {
364
+ // Keep in sync with package.json. Avoiding a JSON import keeps the ESM bundle simple.
365
+ return '0.3.0';
366
+ }
367
+ function makeContext(kit) {
368
+ return {
369
+ kit,
370
+ db: kit.nativeDb,
371
+ ensureTable: (table) => createTable(kit, table),
372
+ addColumn: (tableName, column) => addColumn(kit, tableName, column),
373
+ dropColumn: (tableName, columnName) => dropColumn(kit, tableName, columnName),
374
+ alterColumn: (tableName, columnName, newColumn) => alterColumn(kit, tableName, columnName, newColumn),
375
+ addIndex: (tableName, index) => addIndex(kit, tableName, index),
376
+ dropIndex: (tableName, indexName) => dropIndex(kit, tableName, indexName),
377
+ sql: (sql) => runSql(kit.nativeDb, sql)
378
+ };
379
+ }
380
+ function acquireLockSync(kit) {
381
+ const db = kit.nativeDb;
382
+ const locks = db.table('__kit_migration_locks');
383
+ const now = new Date();
384
+ const expiresAt = new Date(now.getTime() + LOCK_TTL_MS);
385
+ const existing = locks.getByPkText(LOCK_NAME);
386
+ if (existing) {
387
+ const expiresAtCell = existing.cells.find((c) => c.columnId === columnId(kitMigrationLocks, 'expires_at'));
388
+ const existingExpires = expiresAtCell?.text;
389
+ if (existingExpires && new Date(existingExpires) > now) {
390
+ throw new KitMigrationError('migration lock is already held');
391
+ }
392
+ locks.deleteByPkText(LOCK_NAME);
393
+ }
394
+ locks.put([
395
+ { columnId: columnId(kitMigrationLocks, 'lock_name'), text: LOCK_NAME },
396
+ { columnId: columnId(kitMigrationLocks, 'holder'), text: LOCK_HOLDER },
397
+ { columnId: columnId(kitMigrationLocks, 'acquired_at'), text: now.toISOString() },
398
+ { columnId: columnId(kitMigrationLocks, 'expires_at'), text: expiresAt.toISOString() }
399
+ ]);
400
+ locks.commit();
401
+ }
402
+ function releaseLockSync(kit) {
403
+ const locks = kit.nativeDb.table('__kit_migration_locks');
404
+ locks.deleteByPkText(LOCK_NAME);
405
+ locks.commit();
406
+ }
407
+ function readAppliedMigrationsSync(kit) {
408
+ const rows = fullScanRows(kit.nativeDb, kitSchemaMigrations);
409
+ return rows
410
+ .map((m) => ({
411
+ version: m.row.version,
412
+ name: m.row.name,
413
+ checksum: m.row.checksum,
414
+ status: m.row.status
415
+ }))
416
+ .sort((a, b) => Number(a.version - b.version));
417
+ }
418
+ function insertMigrationRecordSync(kit, migration, status) {
419
+ const table = kit.nativeDb.table('__kit_schema_migrations');
420
+ table.put([
421
+ { columnId: columnId(kitSchemaMigrations, 'version'), int64: BigInt(migration.version) },
422
+ { columnId: columnId(kitSchemaMigrations, 'name'), text: migration.name },
423
+ { columnId: columnId(kitSchemaMigrations, 'checksum'), text: migrationChecksum(migration) },
424
+ { columnId: columnId(kitSchemaMigrations, 'applied_at'), text: isoNow() },
425
+ { columnId: columnId(kitSchemaMigrations, 'kit_version'), text: kitVersion() },
426
+ { columnId: columnId(kitSchemaMigrations, 'status'), text: status }
427
+ ]);
428
+ table.commit();
429
+ }
430
+ function updateMigrationStatusSync(kit, version, status) {
431
+ const table = kit.nativeDb.table('__kit_schema_migrations');
432
+ const rowJs = table.getByPkInt64(BigInt(version));
433
+ if (!rowJs) {
434
+ throw new KitMigrationError(`migration record ${version} not found`);
435
+ }
436
+ const row = rowFromRowJs(kitSchemaMigrations, rowJs);
437
+ const updated = { ...row, status };
438
+ table.put(toCells(kitSchemaMigrations, updated));
439
+ table.commit();
440
+ }
441
+ function writeSchemaCatalogSync(kit, schema) {
442
+ const db = kit.nativeDb;
443
+ const schemaJson = schemaCatalogJson(schema);
444
+ const checksum = createHash('sha256').update(schemaJson).digest('hex');
445
+ const now = isoNow();
446
+ const txn = db.begin();
447
+ try {
448
+ txn.put('__kit_schema_catalog', [
449
+ { columnId: columnId(kitSchemaCatalog, 'schema_version'), int64: 1n },
450
+ { columnId: columnId(kitSchemaCatalog, 'schema_json'), text: schemaJson },
451
+ { columnId: columnId(kitSchemaCatalog, 'checksum'), text: checksum },
452
+ { columnId: columnId(kitSchemaCatalog, 'written_at'), text: now }
453
+ ]);
454
+ txn.commit();
455
+ }
456
+ catch (err) {
457
+ txn.rollback();
458
+ throw err;
459
+ }
460
+ }
461
+ function makeContextSync(kit) {
462
+ return {
463
+ kit,
464
+ db: kit.nativeDb,
465
+ ensureTable: (table) => createTableSync(kit, table),
466
+ addColumn: (tableName, column) => addColumnSync(kit, tableName, column),
467
+ dropColumn: (tableName, columnName) => dropColumnSync(kit, tableName, columnName),
468
+ alterColumn: (tableName, columnName, newColumn) => alterColumnSync(kit, tableName, columnName, newColumn),
469
+ addIndex: (tableName, index) => addIndexSync(kit, tableName, index),
470
+ dropIndex: (tableName, indexName) => dropIndexSync(kit, tableName, indexName),
471
+ sql: () => {
472
+ throw new KitMigrationError('sql() is not available in synchronous migrations');
473
+ }
474
+ };
475
+ }
476
+ export function migrateSync(kit, schema, migrations) {
477
+ acquireLockSync(kit);
478
+ try {
479
+ migrations = [...migrations].sort((a, b) => a.version - b.version);
480
+ const applied = readAppliedMigrationsSync(kit);
481
+ verifyMigrationChecksums(applied, migrations);
482
+ const maxApplied = applied.reduce((max, m) => (m.version > max ? m.version : max), 0n);
483
+ const pending = migrations.filter((m) => BigInt(m.version) > maxApplied);
484
+ for (const migration of pending) {
485
+ insertMigrationRecordSync(kit, migration, 'in_progress');
486
+ const txn = kit.nativeDb.begin();
487
+ try {
488
+ const result = migration.up(makeContextSync(kit));
489
+ if (result && typeof result.then === 'function') {
490
+ throw new KitMigrationError('async migration up() cannot be used with migrateSync');
491
+ }
492
+ txn.commit();
493
+ updateMigrationStatusSync(kit, migration.version, 'applied');
494
+ }
495
+ catch (cause) {
496
+ txn.rollback();
497
+ updateMigrationStatusSync(kit, migration.version, 'failed');
498
+ const message = cause instanceof Error ? cause.message : String(cause);
499
+ throw new KitMigrationError(`migration ${migration.version} failed: ${message}`);
500
+ }
501
+ }
502
+ writeSchemaCatalogSync(kit, schema);
503
+ seedFromLegacyKitSequences(kit);
504
+ }
505
+ finally {
506
+ releaseLockSync(kit);
507
+ }
508
+ }
509
+ export async function migrate(kit, schema, migrations) {
510
+ await acquireLock(kit);
511
+ try {
512
+ migrations = [...migrations].sort((a, b) => a.version - b.version);
513
+ const applied = await readAppliedMigrations(kit);
514
+ verifyMigrationChecksums(applied, migrations);
515
+ const maxApplied = applied.reduce((max, m) => (m.version > max ? m.version : max), 0n);
516
+ const pending = migrations.filter((m) => BigInt(m.version) > maxApplied);
517
+ for (const migration of pending) {
518
+ await insertMigrationRecord(kit, migration, 'in_progress');
519
+ try {
520
+ await kit.nativeDb.transaction(async () => {
521
+ await migration.up(makeContext(kit));
522
+ });
523
+ await updateMigrationStatus(kit, migration.version, 'applied');
524
+ }
525
+ catch (cause) {
526
+ await updateMigrationStatus(kit, migration.version, 'failed');
527
+ const message = cause instanceof Error ? cause.message : String(cause);
528
+ throw new KitMigrationError(`migration ${migration.version} failed: ${message}`);
529
+ }
530
+ }
531
+ await writeSchemaCatalog(kit, schema);
532
+ seedFromLegacyKitSequences(kit);
533
+ }
534
+ finally {
535
+ await releaseLock(kit);
536
+ }
537
+ }
538
+ /**
539
+ * Reject edited or reordered historical migrations by comparing the stored
540
+ * checksum/name of every applied migration record against the corresponding
541
+ * migration in the supplied list. Drift here would silently change the meaning
542
+ * of an already-applied migration and corrupt the schema catalog.
543
+ */
544
+ function verifyMigrationChecksums(applied, migrations) {
545
+ const byVersion = new Map(migrations.map((m) => [BigInt(m.version), m]));
546
+ for (const record of applied) {
547
+ if (record.status === 'failed') {
548
+ // Failed migrations are not part of the canonical history; the
549
+ // caller is expected to repair them before re-running.
550
+ continue;
551
+ }
552
+ const current = byVersion.get(record.version);
553
+ if (!current) {
554
+ throw new KitSchemaDriftError(`migration ${record.version} (${record.name}) is recorded as applied but is missing from the supplied migrations list`);
555
+ }
556
+ const expected = migrationChecksum(current);
557
+ if (expected !== record.checksum || current.name !== record.name) {
558
+ throw new KitSchemaDriftError(`migration ${record.version} (${record.name}) checksum mismatch: stored ${record.checksum}, expected ${expected}`);
559
+ }
560
+ }
561
+ }
562
+ function createTableSync(kit, table) {
563
+ if (kit.nativeDb.tableNames().includes(table.name))
564
+ return;
565
+ kit.nativeDb.createTable(table.name, toMongrelSchema(table));
566
+ }
567
+ export async function createTable(kit, table) {
568
+ createTableSync(kit, table);
569
+ }
570
+ /**
571
+ * Drop `tableName` and remove every unique-key and row guard it owned. The
572
+ * application row data and its guards are gone after this; the schema catalog is
573
+ * re-persisted by the migration runner once all ops complete.
574
+ */
575
+ export async function dropTable(kit, tableName) {
576
+ const db = kit.nativeDb;
577
+ if (!db.tableNames().includes(tableName)) {
578
+ throw new KitMigrationError(`Table "${tableName}" does not exist`);
579
+ }
580
+ db.dropTable(tableName);
581
+ cleanTableGuards(kit, tableName);
582
+ }
583
+ /** Delete unique-key and row guards owned by a dropped table. */
584
+ function cleanTableGuards(kit, tableName) {
585
+ const db = kit.nativeDb;
586
+ const ukHandle = db.table('__kit_unique_keys');
587
+ const ukGuards = ukHandle.query([
588
+ {
589
+ kind: 1 /* ConditionKind.BitmapEq */,
590
+ columnId: columnId(kitUniqueKeys, 'owner_table'),
591
+ text: tableName
592
+ }
593
+ ]);
594
+ let ukMutated = false;
595
+ for (const guard of ukGuards) {
596
+ const key = guard.cells.find((c) => c.columnId === columnId(kitUniqueKeys, 'encoded_key'))?.text;
597
+ if (key) {
598
+ ukHandle.deleteByPkText(key);
599
+ ukMutated = true;
600
+ }
601
+ }
602
+ if (ukMutated)
603
+ ukHandle.commit();
604
+ const rgHandle = db.table('__kit_row_guards');
605
+ const rgGuards = rgHandle.query([
606
+ {
607
+ kind: 1 /* ConditionKind.BitmapEq */,
608
+ columnId: columnId(kitRowGuards, 'table_name'),
609
+ text: tableName
610
+ }
611
+ ]);
612
+ let rgMutated = false;
613
+ for (const guard of rgGuards) {
614
+ const key = guard.cells.find((c) => c.columnId === columnId(kitRowGuards, 'encoded_guard_key'))?.text;
615
+ if (key) {
616
+ rgHandle.deleteByPkText(key);
617
+ rgMutated = true;
618
+ }
619
+ }
620
+ if (rgMutated)
621
+ rgHandle.commit();
622
+ }
623
+ function computeDefaultValue(column, kit) {
624
+ const source = column.default ?? (column.generated === 'uuid' ? { kind: 'uuid' } : column.generated === 'now' ? { kind: 'now' } : null);
625
+ if (!source)
626
+ return null;
627
+ switch (source.kind) {
628
+ case 'static':
629
+ return source.value;
630
+ case 'now':
631
+ return isoNow();
632
+ case 'uuid':
633
+ return randomUUID();
634
+ case 'sequence':
635
+ // Sequences are now engine-managed AUTO_INCREMENT, so a freshly
636
+ // created table with a sequence column migrates fine (the engine
637
+ // allocates on insert). What is NOT supported is back-filling an
638
+ // AUTO_INCREMENT value onto existing rows during `addColumn` — each
639
+ // row would need its own id and re-putting changes identities.
640
+ throw new KitMigrationError(`AUTO_INCREMENT column "${column.name}" cannot be added with a NOT NULL backfill; add it nullable (or create the table with it)`);
641
+ case 'custom':
642
+ return source.fn();
643
+ default:
644
+ return null;
645
+ }
646
+ }
647
+ function addColumnSync(kit, tableName, column) {
648
+ if (!column.nullable && !column.default && !column.generated) {
649
+ throw new KitMigrationError(`Column "${column.name}" on "${tableName}" must be nullable or have a default value`);
650
+ }
651
+ const db = kit.nativeDb;
652
+ const table = kit.schema.table(tableName);
653
+ if (db.tableNames().includes(tableName)) {
654
+ const dbColumns = db.tableColumns(tableName);
655
+ if (dbColumns.includes(column.name))
656
+ return;
657
+ }
658
+ const updatedTable = {
659
+ ...table,
660
+ columns: [...table.columns, column]
661
+ };
662
+ db.addColumn(tableName, toMongrelColumnSpec(column));
663
+ if (!column.nullable) {
664
+ const defaultValue = computeDefaultValue(column, kit);
665
+ const rows = fullScanRows(db, table);
666
+ for (const { rowId, row } of rows) {
667
+ const backfilled = { ...row, [column.name]: defaultValue };
668
+ validateRow(updatedTable, backfilled);
669
+ db.table(tableName).put(toCells(updatedTable, backfilled));
670
+ }
671
+ db.table(tableName).commit();
672
+ }
673
+ }
674
+ export async function addColumn(kit, tableName, column) {
675
+ addColumnSync(kit, tableName, column);
676
+ }
677
+ /**
678
+ * Change the declared type or constraints of an existing column in place.
679
+ *
680
+ * Kit keeps application-only changes metadata-local when the old and new
681
+ * storage types map to the same engine {@link ColumnType}. Native schema
682
+ * changes (rename, storage type, nullability, primary-key/sequence flags) are
683
+ * delegated to MongrelDB's native ALTER COLUMN validation.
684
+ */
685
+ export async function alterColumn(kit, tableName, columnName, newColumn) {
686
+ alterColumnSync(kit, tableName, columnName, newColumn);
687
+ }
688
+ function alterColumnSync(kit, tableName, columnName, newColumn) {
689
+ const table = kit.schema.table(tableName);
690
+ const existingIndex = table.columns.findIndex((c) => c.name === columnName);
691
+ if (existingIndex === -1) {
692
+ throw new KitMigrationError(`Column "${columnName}" not found in table "${tableName}"`);
693
+ }
694
+ const existing = table.columns[existingIndex];
695
+ const resolved = { ...newColumn, id: existing.id };
696
+ if (resolved.name !== existing.name &&
697
+ table.columns.some((c, idx) => idx !== existingIndex && c.name === resolved.name)) {
698
+ throw new KitMigrationError(`Column "${resolved.name}" already exists in table "${tableName}"`);
699
+ }
700
+ const oldNativeTy = toMongrelColumnType(existing.storageType);
701
+ const newNativeTy = toMongrelColumnType(resolved.storageType);
702
+ const nativeChange = resolved.name !== existing.name ||
703
+ newNativeTy !== oldNativeTy ||
704
+ resolved.nullable !== existing.nullable ||
705
+ resolved.primaryKey !== existing.primaryKey ||
706
+ (resolved.default?.kind === 'sequence') !== (existing.default?.kind === 'sequence');
707
+ if (nativeChange) {
708
+ const db = kit.nativeDb;
709
+ if (typeof db.alterColumn !== 'function') {
710
+ throw new KitMigrationError(`alterColumn for "${tableName}"."${columnName}" requires a MongrelDB native addon with alterColumn support`);
711
+ }
712
+ try {
713
+ db.alterColumn(tableName, columnName, toMongrelColumnSpec(resolved));
714
+ }
715
+ catch (cause) {
716
+ const message = cause instanceof Error ? cause.message : String(cause);
717
+ throw new KitMigrationError(`alterColumn failed for "${tableName}"."${columnName}": ${message}`);
718
+ }
719
+ }
720
+ table.columns[existingIndex] = resolved;
721
+ if (resolved.name !== existing.name) {
722
+ renameColumnReferences(table, existing.name, resolved.name);
723
+ for (const candidate of kit.schema.tablesList()) {
724
+ for (const fk of candidate.foreignKeys) {
725
+ if (fk.referencesTable !== tableName)
726
+ continue;
727
+ fk.referencesColumns = fk.referencesColumns.map((name) => name === existing.name ? resolved.name : name);
728
+ }
729
+ }
730
+ }
731
+ }
732
+ function renameColumnReferences(table, oldName, newName) {
733
+ table.primaryKey = table.primaryKey.map((name) => (name === oldName ? newName : name));
734
+ for (const idx of table.indexes) {
735
+ idx.columns = idx.columns.map((name) => (name === oldName ? newName : name));
736
+ }
737
+ for (const constraint of table.unique) {
738
+ constraint.columns = constraint.columns.map((name) => (name === oldName ? newName : name));
739
+ }
740
+ for (const fk of table.foreignKeys) {
741
+ fk.columns = fk.columns.map((name) => (name === oldName ? newName : name));
742
+ }
743
+ }
744
+ function rebuildTableSync(kit, table, updatedTable) {
745
+ const db = kit.nativeDb;
746
+ const tempName = `__kit_tmp_rebuild_${table.name}_${Date.now()}`;
747
+ db.createTable(tempName, toMongrelSchema(updatedTable));
748
+ try {
749
+ const rows = fullScanRows(db, table);
750
+ for (const { row } of rows) {
751
+ db.table(tempName).put(toCells(updatedTable, row));
752
+ }
753
+ db.table(tempName).commit();
754
+ db.dropTable(table.name);
755
+ db.createTable(table.name, toMongrelSchema(updatedTable));
756
+ const tempRows = fullScanRows(db, { ...updatedTable, name: tempName });
757
+ for (const { row } of tempRows) {
758
+ db.table(table.name).put(toCells(updatedTable, row));
759
+ }
760
+ db.table(table.name).commit();
761
+ }
762
+ finally {
763
+ if (db.tableNames().includes(tempName)) {
764
+ db.dropTable(tempName);
765
+ }
766
+ }
767
+ }
768
+ function dropUniqueGuards(kit, tableName, constraintName) {
769
+ const handle = kit.nativeDb.table('__kit_unique_keys');
770
+ const guards = handle.query([
771
+ {
772
+ kind: 1 /* ConditionKind.BitmapEq */,
773
+ columnId: columnId(kitUniqueKeys, 'owner_table'),
774
+ text: tableName
775
+ }
776
+ ]);
777
+ let mutated = false;
778
+ for (const guard of guards) {
779
+ const constraint = guard.cells.find((c) => c.columnId === columnId(kitUniqueKeys, 'constraint_name'))?.text;
780
+ if (constraint !== constraintName)
781
+ continue;
782
+ const key = guard.cells.find((c) => c.columnId === columnId(kitUniqueKeys, 'encoded_key'))?.text;
783
+ if (!key)
784
+ continue;
785
+ handle.deleteByPkText(key);
786
+ mutated = true;
787
+ }
788
+ if (mutated)
789
+ handle.commit();
790
+ }
791
+ function addIndexSync(kit, tableName, index) {
792
+ const table = kit.schema.table(tableName);
793
+ if (table.indexes.some((idx) => idx.name === index.name)) {
794
+ throw new KitMigrationError(`Index "${index.name}" already exists on "${tableName}"`);
795
+ }
796
+ const updatedTable = {
797
+ ...table,
798
+ indexes: [...table.indexes, index]
799
+ };
800
+ const addsUnique = index.unique &&
801
+ !table.unique.some((constraint) => constraint.columns.join('\0') === index.columns.join('\0'));
802
+ if (addsUnique) {
803
+ addUniqueSync(kit, tableName, { name: index.name, columns: [...index.columns] });
804
+ }
805
+ try {
806
+ rebuildTableSync(kit, table, updatedTable);
807
+ }
808
+ catch (cause) {
809
+ if (addsUnique) {
810
+ table.unique = table.unique.filter((constraint) => constraint.name !== index.name);
811
+ dropUniqueGuards(kit, tableName, index.name);
812
+ }
813
+ throw cause;
814
+ }
815
+ table.indexes.push(index);
816
+ }
817
+ export async function addIndex(kit, tableName, index) {
818
+ addIndexSync(kit, tableName, index);
819
+ }
820
+ function dropIndexSync(kit, tableName, indexName) {
821
+ const table = kit.schema.table(tableName);
822
+ const index = table.indexes.find((idx) => idx.name === indexName);
823
+ if (!index) {
824
+ throw new KitMigrationError(`Index "${indexName}" does not exist on "${tableName}"`);
825
+ }
826
+ const updatedTable = {
827
+ ...table,
828
+ indexes: table.indexes.filter((idx) => idx.name !== indexName),
829
+ unique: index.unique ? table.unique.filter((constraint) => constraint.name !== indexName) : table.unique
830
+ };
831
+ rebuildTableSync(kit, table, updatedTable);
832
+ table.indexes = updatedTable.indexes;
833
+ if (index.unique) {
834
+ table.unique = updatedTable.unique;
835
+ dropUniqueGuards(kit, tableName, indexName);
836
+ }
837
+ }
838
+ export async function dropIndex(kit, tableName, indexName) {
839
+ dropIndexSync(kit, tableName, indexName);
840
+ }
841
+ function dropColumnSync(kit, tableName, columnName) {
842
+ const table = kit.schema.table(tableName);
843
+ if (!table.columns.some((column) => column.name === columnName)) {
844
+ throw new KitMigrationError(`Column "${columnName}" does not exist on "${tableName}"`);
845
+ }
846
+ if (table.primaryKey.includes(columnName)) {
847
+ throw new KitMigrationError(`Cannot drop primary-key column "${columnName}" from "${tableName}"`);
848
+ }
849
+ for (const candidate of kit.schema.tablesList()) {
850
+ for (const fk of candidate.foreignKeys) {
851
+ if (fk.referencesTable === tableName && fk.referencesColumns.includes(columnName)) {
852
+ throw new KitMigrationError(`Cannot drop "${tableName}"."${columnName}" while foreign key "${fk.name}" references it`);
853
+ }
854
+ }
855
+ }
856
+ const removedUnique = table.unique
857
+ .filter((constraint) => constraint.columns.includes(columnName))
858
+ .map((constraint) => constraint.name);
859
+ const updatedTable = {
860
+ ...table,
861
+ columns: table.columns.filter((column) => column.name !== columnName),
862
+ indexes: table.indexes.filter((idx) => !idx.columns.includes(columnName)),
863
+ unique: table.unique.filter((constraint) => !constraint.columns.includes(columnName)),
864
+ foreignKeys: table.foreignKeys.filter((fk) => !fk.columns.includes(columnName))
865
+ };
866
+ rebuildTableSync(kit, table, updatedTable);
867
+ table.columns = updatedTable.columns;
868
+ table.indexes = updatedTable.indexes;
869
+ table.unique = updatedTable.unique;
870
+ table.foreignKeys = updatedTable.foreignKeys;
871
+ for (const constraint of removedUnique) {
872
+ dropUniqueGuards(kit, tableName, constraint);
873
+ }
874
+ }
875
+ export async function dropColumn(kit, tableName, columnName) {
876
+ dropColumnSync(kit, tableName, columnName);
877
+ }
878
+ /**
879
+ * Add a unique constraint and backfill its `__kit_unique_keys` guards for every
880
+ * existing row (PLAN "Migrations"). Rows whose unique columns are all non-null
881
+ * reserve a guard; a guard key produced by two different rows means the existing
882
+ * data already violates the constraint and the migration is rejected. The
883
+ * constraint is added to the in-memory table so subsequent writes enforce it.
884
+ */
885
+ function addUniqueSync(kit, tableName, unique) {
886
+ const db = kit.nativeDb;
887
+ const table = kit.schema.table(tableName);
888
+ if (table.unique.some((u) => u.name === unique.name)) {
889
+ throw new KitMigrationError(`Unique constraint "${unique.name}" already exists on "${tableName}"`);
890
+ }
891
+ for (const colName of unique.columns) {
892
+ if (!table.columns.some((c) => c.name === colName)) {
893
+ throw new KitMigrationError(`Unique column "${colName}" not found in table "${tableName}"`);
894
+ }
895
+ }
896
+ const ukHandle = db.table('__kit_unique_keys');
897
+ // Pre-existing guards for this table make the backfill idempotent.
898
+ const existingKeys = new Set();
899
+ for (const guard of ukHandle.query([
900
+ { kind: 1 /* ConditionKind.BitmapEq */, columnId: columnId(kitUniqueKeys, 'owner_table'), text: tableName }
901
+ ])) {
902
+ const key = guard.cells.find((c) => c.columnId === columnId(kitUniqueKeys, 'encoded_key'))?.text;
903
+ if (key)
904
+ existingKeys.add(key);
905
+ }
906
+ const seen = new Map();
907
+ const now = isoNow();
908
+ let mutated = false;
909
+ for (const { row } of fullScanRows(db, table)) {
910
+ const values = unique.columns.map((colName) => row[colName] === undefined ? null : row[colName]);
911
+ if (values.some((value) => value === null))
912
+ continue; // nullable-unique: nulls never collide
913
+ const encodedKey = encodeUniqueKey(KIT_KEY_VERSION, unique.name, values);
914
+ const ownerPk = encodedPk(pkValueFromRow(table, row));
915
+ const prior = seen.get(encodedKey);
916
+ if (prior !== undefined && prior !== ownerPk) {
917
+ throw new KitMigrationError(`cannot add unique constraint "${unique.name}" on "${tableName}": existing rows violate it`);
918
+ }
919
+ seen.set(encodedKey, ownerPk);
920
+ if (existingKeys.has(encodedKey))
921
+ continue;
922
+ ukHandle.put([
923
+ { columnId: columnId(kitUniqueKeys, 'encoded_key'), text: encodedKey },
924
+ { columnId: columnId(kitUniqueKeys, 'constraint_name'), text: unique.name },
925
+ { columnId: columnId(kitUniqueKeys, 'owner_table'), text: tableName },
926
+ { columnId: columnId(kitUniqueKeys, 'owner_pk'), text: ownerPk },
927
+ { columnId: columnId(kitUniqueKeys, 'created_at'), text: now }
928
+ ]);
929
+ mutated = true;
930
+ }
931
+ if (mutated)
932
+ ukHandle.commit();
933
+ table.unique.push(unique);
934
+ }
935
+ export async function addUnique(kit, tableName, unique) {
936
+ addUniqueSync(kit, tableName, unique);
937
+ }
938
+ /** Build the parent primary-key value referenced by a foreign key. */
939
+ function buildFkParentPk(parent, fkValues) {
940
+ if (parent.primaryKey.length === 1) {
941
+ const value = fkValues[0];
942
+ if (typeof value !== 'string' && typeof value !== 'bigint') {
943
+ throw new KitMigrationError('Foreign key value must be string or bigint');
944
+ }
945
+ return value;
946
+ }
947
+ return fkValues.map((value) => {
948
+ if (value === null || value === undefined)
949
+ return null;
950
+ if (typeof value !== 'string' && typeof value !== 'bigint') {
951
+ throw new KitMigrationError('Foreign key value must be string, bigint, or null');
952
+ }
953
+ return value;
954
+ });
955
+ }
956
+ /**
957
+ * Add a foreign key and backfill parent `__kit_row_guards` (PLAN "Migrations").
958
+ * Each existing child row with a non-null FK must reference an existing parent;
959
+ * a missing parent rejects the migration. The referenced parent's row guard is
960
+ * touched so a later concurrent parent delete conflicts. The FK is added to the
961
+ * in-memory table so subsequent writes enforce it.
962
+ */
963
+ export async function addForeignKey(kit, tableName, fk) {
964
+ const db = kit.nativeDb;
965
+ const table = kit.schema.table(tableName);
966
+ const parent = kit.schema.table(fk.referencesTable);
967
+ if (table.foreignKeys.some((f) => f.name === fk.name)) {
968
+ throw new KitMigrationError(`Foreign key "${fk.name}" already exists on "${tableName}"`);
969
+ }
970
+ const ckit = { db, schema: kit.schema };
971
+ const rgHandle = db.table('__kit_row_guards');
972
+ const touched = new Set();
973
+ let mutated = false;
974
+ for (const { row } of fullScanRows(db, table)) {
975
+ const fkValues = fk.columns.map((colName) => row[colName]);
976
+ if (fkValues.some((value) => value === null || value === undefined))
977
+ continue;
978
+ const parentPk = buildFkParentPk(parent, fkValues);
979
+ if (!parentExists(ckit, parent.name, parentPk)) {
980
+ throw new KitForeignKeyError(tableName, fk.name);
981
+ }
982
+ const guardKey = encodeRowGuardKey(parent.name, parentPk);
983
+ if (touched.has(guardKey))
984
+ continue;
985
+ touched.add(guardKey);
986
+ const existing = rgHandle.getByPkText(guardKey);
987
+ const version = existing
988
+ ? (existing.cells.find((c) => c.columnId === columnId(kitRowGuards, 'version'))?.int64 ??
989
+ 0n) + 1n
990
+ : 1n;
991
+ rgHandle.put([
992
+ { columnId: columnId(kitRowGuards, 'encoded_guard_key'), text: guardKey },
993
+ { columnId: columnId(kitRowGuards, 'table_name'), text: parent.name },
994
+ { columnId: columnId(kitRowGuards, 'primary_key'), text: encodedPk(parentPk) },
995
+ { columnId: columnId(kitRowGuards, 'version'), int64: version },
996
+ { columnId: columnId(kitRowGuards, 'updated_at'), text: isoNow() }
997
+ ]);
998
+ mutated = true;
999
+ }
1000
+ if (mutated)
1001
+ rgHandle.commit();
1002
+ table.foreignKeys.push(fk);
1003
+ }
1004
+ //# sourceMappingURL=migrate.js.map