edinburgh 0.4.4 → 0.4.6
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 +119 -150
- package/build/src/edinburgh.js +4 -2
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +2 -3
- package/build/src/indexes.js +3 -5
- package/build/src/indexes.js.map +1 -1
- package/build/src/migrate-cli.d.ts +1 -16
- package/build/src/migrate-cli.js +56 -42
- package/build/src/migrate-cli.js.map +1 -1
- package/build/src/migrate.d.ts +15 -11
- package/build/src/migrate.js +53 -39
- package/build/src/migrate.js.map +1 -1
- package/build/src/models.d.ts +8 -2
- package/build/src/models.js +59 -51
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +2 -1
- package/package.json +6 -4
- package/skill/BaseIndex.md +16 -0
- package/skill/BaseIndex_batchProcess.md +10 -0
- package/skill/BaseIndex_find.md +7 -0
- package/skill/DatabaseError.md +9 -0
- package/skill/Model.md +22 -0
- package/skill/Model_delete.md +14 -0
- package/skill/Model_findAll.md +12 -0
- package/skill/Model_getPrimaryKeyHash.md +5 -0
- package/skill/Model_isValid.md +14 -0
- package/skill/Model_migrate.md +34 -0
- package/skill/Model_preCommit.md +28 -0
- package/skill/Model_preventPersist.md +15 -0
- package/skill/Model_replaceInto.md +16 -0
- package/skill/Model_validate.md +21 -0
- package/skill/PrimaryIndex.md +8 -0
- package/skill/PrimaryIndex_get.md +17 -0
- package/skill/PrimaryIndex_getLazy.md +13 -0
- package/skill/SKILL.md +96 -714
- package/skill/SecondaryIndex.md +9 -0
- package/skill/UniqueIndex.md +9 -0
- package/skill/UniqueIndex_get.md +17 -0
- package/skill/array.md +23 -0
- package/skill/dump.md +8 -0
- package/skill/field.md +29 -0
- package/skill/index.md +32 -0
- package/skill/init.md +17 -0
- package/skill/link.md +27 -0
- package/skill/literal.md +22 -0
- package/skill/opt.md +22 -0
- package/skill/or.md +22 -0
- package/skill/primary.md +26 -0
- package/skill/record.md +21 -0
- package/skill/registerModel.md +26 -0
- package/skill/runMigration.md +10 -0
- package/skill/set.md +23 -0
- package/skill/setMaxRetryCount.md +10 -0
- package/skill/setOnSaveCallback.md +12 -0
- package/skill/transact.md +49 -0
- package/skill/unique.md +32 -0
- package/src/edinburgh.ts +4 -2
- package/src/indexes.ts +3 -6
- package/src/migrate-cli.ts +44 -46
- package/src/migrate.ts +64 -46
- package/src/models.ts +59 -51
package/src/migrate.ts
CHANGED
|
@@ -11,12 +11,14 @@ const INDEX_ID_PREFIX = -2;
|
|
|
11
11
|
export interface MigrationOptions {
|
|
12
12
|
/** Limit migration to specific table names. */
|
|
13
13
|
tables?: string[];
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
|
|
14
|
+
/** Populate secondary indexes for rows at old schema versions (default: true). */
|
|
15
|
+
populateSecondaries?: boolean;
|
|
16
|
+
/** Convert old primary indices when primary key fields changed (default: true). */
|
|
17
|
+
migratePrimaries?: boolean;
|
|
18
|
+
/** Rewrite all row data to the latest schema version (default: false). */
|
|
19
|
+
rewriteData?: boolean;
|
|
20
|
+
/** Delete orphaned secondary/unique index entries (default: true). */
|
|
21
|
+
removeOrphans?: boolean;
|
|
20
22
|
/** Progress callback. */
|
|
21
23
|
onProgress?: (info: ProgressInfo) => void;
|
|
22
24
|
}
|
|
@@ -29,14 +31,16 @@ export interface ProgressInfo {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export interface MigrationResult {
|
|
32
|
-
/** Per-table
|
|
34
|
+
/** Per-table counts of secondary index entries populated. */
|
|
33
35
|
secondaries: Record<string, number>;
|
|
34
|
-
/** Per-table
|
|
36
|
+
/** Per-table counts of old primary rows migrated. */
|
|
35
37
|
primaries: Record<string, number>;
|
|
36
38
|
/** Per-table conversion failure counts by reason. */
|
|
37
39
|
conversionFailures: Record<string, Record<string, number>>;
|
|
40
|
+
/** Per-table counts of rows rewritten to latest version. */
|
|
41
|
+
rewritten: Record<string, number>;
|
|
38
42
|
/** Number of orphaned index entries deleted. */
|
|
39
|
-
|
|
43
|
+
orphans: number;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
interface IndexDef {
|
|
@@ -91,23 +95,25 @@ async function forEachRow(
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
/**
|
|
94
|
-
* Run database migration:
|
|
95
|
-
* convert old primary indices, and clean up orphaned
|
|
98
|
+
* Run database migration: populate secondary indexes for old-version rows,
|
|
99
|
+
* convert old primary indices, rewrite row data, and clean up orphaned indices.
|
|
96
100
|
*/
|
|
97
101
|
export async function runMigration(options: MigrationOptions = {}): Promise<MigrationResult> {
|
|
98
102
|
// Ensure any pending model/index inits are completed before building index maps
|
|
99
103
|
await transact(() => {});
|
|
100
104
|
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
105
|
+
const populateSecondaries = options.populateSecondaries ?? true;
|
|
106
|
+
const migratePrimaries = options.migratePrimaries ?? true;
|
|
107
|
+
const rewriteData = options.rewriteData ?? false;
|
|
108
|
+
const removeOrphans = options.removeOrphans ?? true;
|
|
104
109
|
const onProgress = options.onProgress;
|
|
105
110
|
|
|
106
111
|
const result: MigrationResult = {
|
|
107
112
|
secondaries: {},
|
|
108
113
|
primaries: {},
|
|
109
114
|
conversionFailures: {},
|
|
110
|
-
|
|
115
|
+
rewritten: {},
|
|
116
|
+
orphans: 0,
|
|
111
117
|
};
|
|
112
118
|
|
|
113
119
|
// Build maps of known index IDs
|
|
@@ -147,10 +153,11 @@ export async function runMigration(options: MigrationOptions = {}): Promise<Migr
|
|
|
147
153
|
allIndexDefs.push({ id, tableName, typeName, fieldNames, fieldTypes });
|
|
148
154
|
});
|
|
149
155
|
|
|
150
|
-
// Phase 1:
|
|
151
|
-
if (
|
|
156
|
+
// Phase 1: Populate secondary indexes and/or rewrite row data
|
|
157
|
+
if (populateSecondaries || rewriteData) {
|
|
152
158
|
for (const [indexId, { model, primary }] of primaryByIndexId) {
|
|
153
|
-
let
|
|
159
|
+
let secondaryCount = 0;
|
|
160
|
+
let rewrittenCount = 0;
|
|
154
161
|
const migrateFn = (model as any).migrate as ((record: Record<string, any>) => void) | undefined;
|
|
155
162
|
const secondaries = model._secondaries || [];
|
|
156
163
|
|
|
@@ -176,45 +183,56 @@ export async function runMigration(options: MigrationOptions = {}): Promise<Migr
|
|
|
176
183
|
const preMigrate = migrateFn ? structuredClone(record) : undefined;
|
|
177
184
|
if (migrateFn) migrateFn(record);
|
|
178
185
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
sec._write(txn, keyBuf, record as any);
|
|
193
|
-
upgraded++;
|
|
194
|
-
}
|
|
195
|
-
} else {
|
|
196
|
-
// Existing secondary, update if migrate changed any of its fields
|
|
197
|
-
for (const [field, type] of sec._fieldTypes.entries()) {
|
|
198
|
-
if (!type.equals(preMigrate[field], record[field])) {
|
|
186
|
+
// Populate/update secondary indexes
|
|
187
|
+
if (populateSecondaries) {
|
|
188
|
+
for (const sec of secondaries) {
|
|
189
|
+
if (!versionInfo.secondaryKeys.has(sec._signature!)) {
|
|
190
|
+
// New secondary, write entry
|
|
191
|
+
sec._write(txn, keyBuf, record as any);
|
|
192
|
+
secondaryCount++;
|
|
193
|
+
} else if (preMigrate) {
|
|
194
|
+
if (sec._computeFn) {
|
|
195
|
+
// Computed indexes: compare serialized keys to avoid unnecessary re-indexing
|
|
196
|
+
const oldKeyBytes = sec._serializeKeyFields(preMigrate).toUint8Array();
|
|
197
|
+
const newKeyBytes = sec._serializeKeyFields(record).toUint8Array();
|
|
198
|
+
if (!bytesEqual(oldKeyBytes, newKeyBytes)) {
|
|
199
199
|
sec._delete(txn, keyBuf, preMigrate as any);
|
|
200
200
|
sec._write(txn, keyBuf, record as any);
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
secondaryCount++;
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
// Existing secondary, update if migrate changed any of its fields
|
|
205
|
+
for (const [field, type] of sec._fieldTypes.entries()) {
|
|
206
|
+
if (!type.equals(preMigrate[field], record[field])) {
|
|
207
|
+
sec._delete(txn, keyBuf, preMigrate as any);
|
|
208
|
+
sec._write(txn, keyBuf, record as any);
|
|
209
|
+
secondaryCount++;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
203
212
|
}
|
|
204
213
|
}
|
|
205
214
|
}
|
|
206
215
|
}
|
|
207
216
|
}
|
|
208
217
|
|
|
218
|
+
// Rewrite primary row data to current version
|
|
219
|
+
if (rewriteData) {
|
|
220
|
+
primary._write(txn, keyBuf, record);
|
|
221
|
+
rewrittenCount++;
|
|
222
|
+
}
|
|
209
223
|
});
|
|
210
224
|
|
|
211
|
-
onProgress?.({ phase: 'secondaries', processed:
|
|
212
|
-
if (
|
|
225
|
+
onProgress?.({ phase: 'secondaries', processed: secondaryCount, table: model.tableName });
|
|
226
|
+
if (secondaryCount > 0) result.secondaries[model.tableName] = secondaryCount;
|
|
227
|
+
if (rewrittenCount > 0) {
|
|
228
|
+
onProgress?.({ phase: 'rewritten', processed: rewrittenCount, table: model.tableName });
|
|
229
|
+
result.rewritten[model.tableName] = rewrittenCount;
|
|
230
|
+
}
|
|
213
231
|
}
|
|
214
232
|
}
|
|
215
233
|
|
|
216
234
|
// Phase 2: Convert old primary indices with known table names
|
|
217
|
-
if (
|
|
235
|
+
if (migratePrimaries) {
|
|
218
236
|
for (const oldDef of allIndexDefs) {
|
|
219
237
|
if (oldDef.typeName !== 'primary') continue;
|
|
220
238
|
if (knownIndexIds.has(oldDef.id)) continue; // Known index, skip
|
|
@@ -266,14 +284,14 @@ export async function runMigration(options: MigrationOptions = {}): Promise<Migr
|
|
|
266
284
|
}
|
|
267
285
|
|
|
268
286
|
// Phase 3: Delete orphaned secondary/unique index entries
|
|
269
|
-
if (
|
|
287
|
+
if (removeOrphans) {
|
|
270
288
|
for (const def of allIndexDefs) {
|
|
271
289
|
if (knownIndexIds.has(def.id) || def.typeName === 'primary') continue;
|
|
272
290
|
await forEachRow(def.id, (txn, keyBuf) => {
|
|
273
291
|
dbDel(txn.id, keyBuf);
|
|
274
|
-
result.
|
|
292
|
+
result.orphans++;
|
|
275
293
|
});
|
|
276
|
-
onProgress?.({ phase: '
|
|
294
|
+
onProgress?.({ phase: 'orphans', processed: result.orphans });
|
|
277
295
|
}
|
|
278
296
|
}
|
|
279
297
|
|
package/src/models.ts
CHANGED
|
@@ -166,7 +166,7 @@ export interface Model<SUB> {
|
|
|
166
166
|
*
|
|
167
167
|
* Models represent database entities with typed fields, automatic serialization,
|
|
168
168
|
* change tracking, and relationship management. All model classes should extend
|
|
169
|
-
* this base class and be decorated with `@registerModel`.
|
|
169
|
+
* this base class and be decorated with `@E.registerModel`.
|
|
170
170
|
*
|
|
171
171
|
* ### Schema Evolution
|
|
172
172
|
*
|
|
@@ -283,7 +283,7 @@ export abstract class Model<SUB> {
|
|
|
283
283
|
// This constructor will only be called once, from `initModels`. All other instances will
|
|
284
284
|
// be created by the 'fake' constructor. The typing for `initial` *is* important though.
|
|
285
285
|
if (initial as any === INIT_INSTANCE_SYMBOL) return;
|
|
286
|
-
throw new DatabaseError("The model needs a @registerModel decorator", 'INIT_ERROR');
|
|
286
|
+
throw new DatabaseError("The model needs a @E.registerModel decorator", 'INIT_ERROR');
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
/**
|
|
@@ -313,70 +313,78 @@ export abstract class Model<SUB> {
|
|
|
313
313
|
*/
|
|
314
314
|
preCommit?(): void;
|
|
315
315
|
|
|
316
|
-
|
|
316
|
+
/**
|
|
317
|
+
* Transform the model's `E.field` properties into the appropriate JavaScript properties. Normally this is done
|
|
318
|
+
* automatically when using `transact()`, but in case you need to access `Model.fields` directly before the first
|
|
319
|
+
* transaction, you can call this method manually.
|
|
320
|
+
*/
|
|
321
|
+
static initFields(reset?: boolean): void {
|
|
317
322
|
const MockModel = getMockModel(this);
|
|
318
323
|
|
|
319
|
-
if (
|
|
324
|
+
if (reset) {
|
|
320
325
|
MockModel._primary._indexId = undefined;
|
|
321
326
|
MockModel._primary._versions.clear();
|
|
322
327
|
for (const sec of MockModel._secondaries || []) sec._indexId = undefined;
|
|
323
328
|
}
|
|
324
329
|
|
|
325
|
-
if (
|
|
326
|
-
// First-time init: gather field configs from a temporary instance of the original class.
|
|
327
|
-
const OrgModel = (MockModel as any)._original || this;
|
|
328
|
-
const instance = new (OrgModel as any)(INIT_INSTANCE_SYMBOL);
|
|
330
|
+
if (MockModel.fields) return;
|
|
329
331
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
332
|
+
// First-time init: gather field configs from a temporary instance of the original class.
|
|
333
|
+
const OrgModel = (MockModel as any)._original || this;
|
|
334
|
+
const instance = new (OrgModel as any)(INIT_INSTANCE_SYMBOL);
|
|
335
|
+
|
|
336
|
+
// If no primary key exists, create one using 'id' field
|
|
337
|
+
if (!MockModel._primary) {
|
|
338
|
+
if (!instance.id) {
|
|
339
|
+
instance.id = { type: identifier };
|
|
337
340
|
}
|
|
341
|
+
// @ts-ignore-next-line - `id` is not part of the type, but the user probably shouldn't touch it anyhow
|
|
342
|
+
new PrimaryIndex(MockModel, ['id']);
|
|
343
|
+
}
|
|
338
344
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
345
|
+
MockModel.fields = {};
|
|
346
|
+
for (const key in instance) {
|
|
347
|
+
const value = instance[key] as FieldConfig<unknown>;
|
|
348
|
+
// Check if this property contains field metadata
|
|
349
|
+
if (value && value.type instanceof TypeWrapper) {
|
|
350
|
+
// Set the configuration on the constructor's `fields` property
|
|
351
|
+
MockModel.fields[key] = value;
|
|
352
|
+
|
|
353
|
+
// Set default value on the prototype
|
|
354
|
+
const defObj = value.default===undefined ? value.type : value;
|
|
355
|
+
const def = defObj.default;
|
|
356
|
+
if (typeof def === 'function') {
|
|
357
|
+
// The default is a function. We'll define a getter on the property in the model prototype,
|
|
358
|
+
// and once it is read, we'll run the function and set the value as a plain old property
|
|
359
|
+
// on the instance object.
|
|
360
|
+
Object.defineProperty(MockModel.prototype, key, {
|
|
361
|
+
get() {
|
|
362
|
+
// This will call set(), which will define the property on the instance.
|
|
363
|
+
return (this[key] = def.call(defObj, this));
|
|
364
|
+
},
|
|
365
|
+
set(val: any) {
|
|
366
|
+
Object.defineProperty(this, key, {
|
|
367
|
+
value: val,
|
|
368
|
+
configurable: true,
|
|
369
|
+
writable: true,
|
|
370
|
+
enumerable: true,
|
|
371
|
+
})
|
|
372
|
+
},
|
|
373
|
+
configurable: true,
|
|
374
|
+
});
|
|
375
|
+
} else if (def !== undefined) {
|
|
376
|
+
(MockModel.prototype as any)[key] = def;
|
|
372
377
|
}
|
|
373
378
|
}
|
|
379
|
+
}
|
|
374
380
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
381
|
+
if (logLevel >= 1) {
|
|
382
|
+
console.log(`[edinburgh] Registered model ${MockModel.tableName} with fields: ${Object.keys(MockModel.fields).join(' ')}`);
|
|
378
383
|
}
|
|
384
|
+
}
|
|
379
385
|
|
|
386
|
+
static async _loadCreateIndexes(): Promise<void> {
|
|
387
|
+
const MockModel = getMockModel(this);
|
|
380
388
|
// Always run index inits (idempotent, skip if already initialized)
|
|
381
389
|
await MockModel._primary._delayedInit();
|
|
382
390
|
for (const sec of MockModel._secondaries || []) await sec._delayedInit();
|