@bjnstnkvc/db 0.1.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.
- package/LICENSE +21 -0
- package/README.md +1290 -0
- package/dist/main.cjs +4414 -0
- package/dist/main.d.cts +1372 -0
- package/dist/main.d.ts +1372 -0
- package/dist/main.js +4349 -0
- package/package.json +51 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,4349 @@
|
|
|
1
|
+
// src/exceptions/CheckConstraintViolationException.ts
|
|
2
|
+
var CheckConstraintViolationException = class extends Error {
|
|
3
|
+
/**
|
|
4
|
+
* Create a new exception for a value the column does not accept.
|
|
5
|
+
*/
|
|
6
|
+
constructor(table, column, value, accepted) {
|
|
7
|
+
super(`Column [${column}] of table [${table}] does not accept [${String(value)}]. It accepts [${accepted.join(", ")}].`);
|
|
8
|
+
this.name = "CheckConstraintViolationException";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/exceptions/ConnectionNotConfiguredException.ts
|
|
13
|
+
var ConnectionNotConfiguredException = class extends Error {
|
|
14
|
+
/**
|
|
15
|
+
* Create a new exception for an unconfigured connection.
|
|
16
|
+
*/
|
|
17
|
+
constructor(connection) {
|
|
18
|
+
super(`Database connection [${connection}] is not configured.`);
|
|
19
|
+
this.name = "ConnectionNotConfiguredException";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/exceptions/DatabaseBlockedException.ts
|
|
24
|
+
var DatabaseBlockedException = class extends Error {
|
|
25
|
+
/**
|
|
26
|
+
* Create a new exception for a database blocked by another connection.
|
|
27
|
+
*/
|
|
28
|
+
constructor(database) {
|
|
29
|
+
super(`Database [${database}] is blocked by a connection in another tab holding an older version. Close the other tabs and try again.`);
|
|
30
|
+
this.name = "DatabaseBlockedException";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// src/exceptions/MigrationMismatchException.ts
|
|
35
|
+
var MigrationMismatchException = class extends Error {
|
|
36
|
+
/**
|
|
37
|
+
* Create a new exception for a migration list that diverged from what has run.
|
|
38
|
+
*/
|
|
39
|
+
constructor(ran, registered) {
|
|
40
|
+
super(`Registered migrations [${registered.join(", ")}] do not match the migrations already run [${ran.join(", ")}]. Migrations are forward-only, so they may only be appended, never reordered or removed. If your bundler mangles class names, override name() on each migration.`);
|
|
41
|
+
this.name = "MigrationMismatchException";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/exceptions/MigrationTransactionClosedException.ts
|
|
46
|
+
var MigrationTransactionClosedException = class extends Error {
|
|
47
|
+
/**
|
|
48
|
+
* Create a new exception for a migration that outlived its transaction.
|
|
49
|
+
*/
|
|
50
|
+
constructor(migration) {
|
|
51
|
+
super(`Migration [${migration}] continued after its transaction closed. A migration may only await database operations from this package - awaiting a fetch, a timer or any other promise ends the transaction.`);
|
|
52
|
+
this.name = "MigrationTransactionClosedException";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// src/exceptions/MultipleRecordsFoundException.ts
|
|
57
|
+
var MultipleRecordsFoundException = class extends Error {
|
|
58
|
+
/**
|
|
59
|
+
* Create a new exception for a query that matched more records than it should.
|
|
60
|
+
*/
|
|
61
|
+
constructor(table) {
|
|
62
|
+
super(`More than one record found in table [${table}], where exactly one was expected.`);
|
|
63
|
+
this.name = "MultipleRecordsFoundException";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/exceptions/NotNullConstraintViolationException.ts
|
|
68
|
+
var NotNullConstraintViolationException = class extends Error {
|
|
69
|
+
/**
|
|
70
|
+
* Create a new exception for a null value in a non-nullable column.
|
|
71
|
+
*/
|
|
72
|
+
constructor(table, column) {
|
|
73
|
+
super(`Column [${column}] of table [${table}] may not be null.`);
|
|
74
|
+
this.name = "NotNullConstraintViolationException";
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/exceptions/QuotaExceededException.ts
|
|
79
|
+
var QuotaExceededException = class extends Error {
|
|
80
|
+
/**
|
|
81
|
+
* Create a new exception for storage the browser refused to grant.
|
|
82
|
+
*/
|
|
83
|
+
constructor(message = "The storage quota for this origin is full. Free some space, or ask the user to, before writing again.") {
|
|
84
|
+
super(message);
|
|
85
|
+
this.name = "QuotaExceededException";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/exceptions/RecordsNotFoundException.ts
|
|
90
|
+
var RecordsNotFoundException = class extends Error {
|
|
91
|
+
/**
|
|
92
|
+
* Create a new exception for a query that matched no records.
|
|
93
|
+
*/
|
|
94
|
+
constructor(message = "No records found.") {
|
|
95
|
+
super(message);
|
|
96
|
+
this.name = "RecordsNotFoundException";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/exceptions/ReservedTableException.ts
|
|
101
|
+
var ReservedTableException = class extends Error {
|
|
102
|
+
/**
|
|
103
|
+
* Create a new exception for a table name reserved by the database layer.
|
|
104
|
+
*/
|
|
105
|
+
constructor(table) {
|
|
106
|
+
super(`Table name [${table}] is reserved by the database layer.`);
|
|
107
|
+
this.name = "ReservedTableException";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/exceptions/SchemaException.ts
|
|
112
|
+
var SchemaException = class extends Error {
|
|
113
|
+
/**
|
|
114
|
+
* Create a new exception for a failed schema operation.
|
|
115
|
+
*/
|
|
116
|
+
constructor(message = "Schema operation failed.") {
|
|
117
|
+
super(message);
|
|
118
|
+
this.name = "SchemaException";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/exceptions/TableNotFoundException.ts
|
|
123
|
+
var TableNotFoundException = class extends Error {
|
|
124
|
+
/**
|
|
125
|
+
* Create a new exception for a missing table.
|
|
126
|
+
*/
|
|
127
|
+
constructor(table) {
|
|
128
|
+
super(`Table [${table}] does not exist.`);
|
|
129
|
+
this.name = "TableNotFoundException";
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/exceptions/UniqueConstraintViolationException.ts
|
|
134
|
+
var UniqueConstraintViolationException = class extends Error {
|
|
135
|
+
/**
|
|
136
|
+
* Create a new exception for a violated unique index.
|
|
137
|
+
*/
|
|
138
|
+
constructor(table, index) {
|
|
139
|
+
super(`Unique constraint violated on index [${index}] of table [${table}].`);
|
|
140
|
+
this.name = "UniqueConstraintViolationException";
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/schema/ColumnDefinition.ts
|
|
145
|
+
var ColumnDefinition = class {
|
|
146
|
+
/**
|
|
147
|
+
* The name of the column.
|
|
148
|
+
*/
|
|
149
|
+
#name;
|
|
150
|
+
/**
|
|
151
|
+
* The declared type of the column.
|
|
152
|
+
*/
|
|
153
|
+
#type;
|
|
154
|
+
/**
|
|
155
|
+
* Whether the column accepts a null value.
|
|
156
|
+
*/
|
|
157
|
+
#nullable = false;
|
|
158
|
+
/**
|
|
159
|
+
* The default value applied when the column is absent.
|
|
160
|
+
*/
|
|
161
|
+
#default = void 0;
|
|
162
|
+
/**
|
|
163
|
+
* Whether a default value has been declared.
|
|
164
|
+
*/
|
|
165
|
+
#hasDefault = false;
|
|
166
|
+
/**
|
|
167
|
+
* Whether the column is the key path of its table.
|
|
168
|
+
*/
|
|
169
|
+
#primary = false;
|
|
170
|
+
/**
|
|
171
|
+
* Whether the column is generated by the database.
|
|
172
|
+
*/
|
|
173
|
+
#increments = false;
|
|
174
|
+
/**
|
|
175
|
+
* The number of decimal places a scaled column carries.
|
|
176
|
+
*/
|
|
177
|
+
#places = null;
|
|
178
|
+
/**
|
|
179
|
+
* The values an enumerated column accepts.
|
|
180
|
+
*/
|
|
181
|
+
#values = null;
|
|
182
|
+
/**
|
|
183
|
+
* The indexes requested for the column.
|
|
184
|
+
*/
|
|
185
|
+
#requested = [];
|
|
186
|
+
/**
|
|
187
|
+
* Create a new column definition.
|
|
188
|
+
*/
|
|
189
|
+
constructor(name, type) {
|
|
190
|
+
this.#name = name;
|
|
191
|
+
this.#type = type;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get the name of the column.
|
|
195
|
+
*/
|
|
196
|
+
get name() {
|
|
197
|
+
return this.#name;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get the declared type of the column.
|
|
201
|
+
*/
|
|
202
|
+
get type() {
|
|
203
|
+
return this.#type;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Record the scale of a decimal column.
|
|
207
|
+
*/
|
|
208
|
+
scaled(places) {
|
|
209
|
+
this.#places = places;
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Record the values an enumerated column accepts.
|
|
214
|
+
*/
|
|
215
|
+
accepts(values) {
|
|
216
|
+
this.#values = values;
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Allow the column to hold a null value.
|
|
221
|
+
*/
|
|
222
|
+
nullable(value = true) {
|
|
223
|
+
this.#nullable = value;
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Set the value applied when the column is absent.
|
|
228
|
+
*/
|
|
229
|
+
default(value) {
|
|
230
|
+
this.#default = value;
|
|
231
|
+
this.#hasDefault = true;
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Make the column the key path of its table.
|
|
236
|
+
*/
|
|
237
|
+
primary() {
|
|
238
|
+
this.#primary = true;
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Let the database generate the column value.
|
|
243
|
+
*/
|
|
244
|
+
increments() {
|
|
245
|
+
this.#increments = true;
|
|
246
|
+
return this;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Request an index over the column.
|
|
250
|
+
*/
|
|
251
|
+
index(name = null) {
|
|
252
|
+
this.#requested.push({ name, unique: false, multiEntry: false });
|
|
253
|
+
return this;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Request a unique index over the column.
|
|
257
|
+
*/
|
|
258
|
+
unique(name = null) {
|
|
259
|
+
this.#requested.push({ name, unique: true, multiEntry: false });
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Index each element of the column's array value, requesting an index when none was.
|
|
264
|
+
*/
|
|
265
|
+
multiEntry() {
|
|
266
|
+
if (this.#requested.length === 0) {
|
|
267
|
+
this.index();
|
|
268
|
+
}
|
|
269
|
+
for (const requested of this.#requested) {
|
|
270
|
+
requested.multiEntry = true;
|
|
271
|
+
}
|
|
272
|
+
return this;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get the indexes requested for the column.
|
|
276
|
+
*/
|
|
277
|
+
requested() {
|
|
278
|
+
return this.#requested;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Get the schema describing the column.
|
|
282
|
+
*/
|
|
283
|
+
toSchema() {
|
|
284
|
+
return {
|
|
285
|
+
name: this.#name,
|
|
286
|
+
type: this.#type,
|
|
287
|
+
nullable: this.#nullable,
|
|
288
|
+
default: this.#default,
|
|
289
|
+
hasDefault: this.#hasDefault,
|
|
290
|
+
primary: this.#primary,
|
|
291
|
+
increments: this.#increments,
|
|
292
|
+
places: this.#places,
|
|
293
|
+
values: this.#values
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// src/schema/Blueprint.ts
|
|
299
|
+
var Blueprint = class {
|
|
300
|
+
/**
|
|
301
|
+
* The name of the table.
|
|
302
|
+
*/
|
|
303
|
+
#table;
|
|
304
|
+
/**
|
|
305
|
+
* The schema of the table as it already exists, when altering.
|
|
306
|
+
*/
|
|
307
|
+
#existing;
|
|
308
|
+
/**
|
|
309
|
+
* The columns declared on the blueprint.
|
|
310
|
+
*/
|
|
311
|
+
#columns = [];
|
|
312
|
+
/**
|
|
313
|
+
* The indexes declared at table level on the blueprint.
|
|
314
|
+
*/
|
|
315
|
+
#indexes = [];
|
|
316
|
+
/**
|
|
317
|
+
* The names of the columns to drop.
|
|
318
|
+
*/
|
|
319
|
+
#dropped = [];
|
|
320
|
+
/**
|
|
321
|
+
* The columns to rename.
|
|
322
|
+
*/
|
|
323
|
+
#renamed = [];
|
|
324
|
+
/**
|
|
325
|
+
* The names of the indexes to drop.
|
|
326
|
+
*/
|
|
327
|
+
#unindexed = [];
|
|
328
|
+
/**
|
|
329
|
+
* Whether the table manages timestamps.
|
|
330
|
+
*/
|
|
331
|
+
#timestamps;
|
|
332
|
+
/**
|
|
333
|
+
* Create a new blueprint for a table.
|
|
334
|
+
*/
|
|
335
|
+
constructor(table, existing = null) {
|
|
336
|
+
this.#table = table;
|
|
337
|
+
this.#existing = existing;
|
|
338
|
+
this.#timestamps = existing !== null && existing.timestamps;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Get the name of the table.
|
|
342
|
+
*/
|
|
343
|
+
get table() {
|
|
344
|
+
return this.#table;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Add an auto incrementing primary key column.
|
|
348
|
+
*/
|
|
349
|
+
id(column = "id") {
|
|
350
|
+
return this.integer(column).primary().increments();
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Add a string column holding a universally unique identifier.
|
|
354
|
+
*/
|
|
355
|
+
uuid(column) {
|
|
356
|
+
return this.string(column);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Add a string column.
|
|
360
|
+
*/
|
|
361
|
+
string(column) {
|
|
362
|
+
return this.#add(column, "string");
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Add an integer column.
|
|
366
|
+
*/
|
|
367
|
+
integer(column) {
|
|
368
|
+
return this.#add(column, "integer");
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Add a floating point column.
|
|
372
|
+
*/
|
|
373
|
+
float(column) {
|
|
374
|
+
return this.#add(column, "float");
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Add a boolean column.
|
|
378
|
+
*/
|
|
379
|
+
boolean(column) {
|
|
380
|
+
return this.#add(column, "boolean");
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Add a date column.
|
|
384
|
+
*/
|
|
385
|
+
date(column) {
|
|
386
|
+
return this.#add(column, "date");
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Add a date and time column.
|
|
390
|
+
*/
|
|
391
|
+
datetime(column) {
|
|
392
|
+
return this.#add(column, "datetime");
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Add a column holding an arbitrary structure.
|
|
396
|
+
*/
|
|
397
|
+
json(column) {
|
|
398
|
+
return this.#add(column, "json");
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Add a fixed point column, stored as an integer number of its smallest unit.
|
|
402
|
+
*/
|
|
403
|
+
decimal(column, places = 2) {
|
|
404
|
+
return this.#add(column, "decimal").scaled(places);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Add a column accepting only one of the given values.
|
|
408
|
+
*/
|
|
409
|
+
enum(column, values) {
|
|
410
|
+
const accepted = this.#enumerated(column, values);
|
|
411
|
+
if (accepted.length === 0) {
|
|
412
|
+
throw new SchemaException(`Column [${column}] of table [${this.#table}] is enumerated over no values, so nothing could ever be written to it.`);
|
|
413
|
+
}
|
|
414
|
+
return this.#add(column, "enum").accepts(accepted);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Add nullable creation and update timestamp columns.
|
|
418
|
+
*/
|
|
419
|
+
timestamps() {
|
|
420
|
+
this.#timestamps = true;
|
|
421
|
+
this.datetime("created_at").nullable();
|
|
422
|
+
this.datetime("updated_at").nullable();
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Add an index over the given columns.
|
|
426
|
+
*/
|
|
427
|
+
index(columns, name = null) {
|
|
428
|
+
return this.#indexed(columns, name, false);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Add a unique index over the given columns.
|
|
432
|
+
*/
|
|
433
|
+
unique(columns, name = null) {
|
|
434
|
+
return this.#indexed(columns, name, true);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Drop the given columns from the table.
|
|
438
|
+
*/
|
|
439
|
+
dropColumn(...columns) {
|
|
440
|
+
for (const column of columns) {
|
|
441
|
+
if (!this.#has(column)) {
|
|
442
|
+
throw new SchemaException(`Column [${column}] does not exist on table [${this.#table}].`);
|
|
443
|
+
}
|
|
444
|
+
this.#dropped.push(column);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Rename a column of the table.
|
|
449
|
+
*/
|
|
450
|
+
renameColumn(from, to) {
|
|
451
|
+
if (!this.#has(from)) {
|
|
452
|
+
throw new SchemaException(`Column [${from}] does not exist on table [${this.#table}].`);
|
|
453
|
+
}
|
|
454
|
+
this.#renamed.push({ from, to });
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Drop an index from the table.
|
|
458
|
+
*/
|
|
459
|
+
dropIndex(name) {
|
|
460
|
+
const exists = this.#indexesOf().some((index) => index.name === name);
|
|
461
|
+
if (!exists) {
|
|
462
|
+
throw new SchemaException(`Index [${name}] does not exist on table [${this.#table}].`);
|
|
463
|
+
}
|
|
464
|
+
this.#unindexed.push(name);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Get the schema describing the table once the blueprint is applied.
|
|
468
|
+
*/
|
|
469
|
+
toSchema() {
|
|
470
|
+
const columns = this.#resolved();
|
|
471
|
+
const indexes = this.#resolvedIndexes();
|
|
472
|
+
const primary = columns.filter((column) => column.primary);
|
|
473
|
+
if (primary.length > 1) {
|
|
474
|
+
throw new SchemaException(`Table [${this.#table}] declares more than one primary column [${primary.map((column) => column.name).join(", ")}].`);
|
|
475
|
+
}
|
|
476
|
+
const key = primary[0];
|
|
477
|
+
return {
|
|
478
|
+
table: this.#table,
|
|
479
|
+
key: key === void 0 ? null : key.name,
|
|
480
|
+
increments: key === void 0 ? true : key.increments,
|
|
481
|
+
timestamps: this.#timestamps,
|
|
482
|
+
columns,
|
|
483
|
+
indexes
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Get the operations the blueprint performs against the existing table.
|
|
488
|
+
*/
|
|
489
|
+
operations() {
|
|
490
|
+
return {
|
|
491
|
+
added: this.#columns.map((column) => column.toSchema()),
|
|
492
|
+
dropped: this.#dropped,
|
|
493
|
+
renamed: this.#renamed,
|
|
494
|
+
indexed: this.#declaredIndexes(),
|
|
495
|
+
unindexed: this.#unindexed
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Add a column of the given type to the blueprint.
|
|
500
|
+
*/
|
|
501
|
+
#add(column, type) {
|
|
502
|
+
const definition = new ColumnDefinition(column, type);
|
|
503
|
+
this.#columns.push(definition);
|
|
504
|
+
return definition;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Reduce a list, an enum or a constant object to the values a column accepts.
|
|
508
|
+
*/
|
|
509
|
+
#enumerated(column, values) {
|
|
510
|
+
const listed = Array.isArray(values) ? [...values] : Object.values(values);
|
|
511
|
+
if (listed.some((value) => typeof value !== "string")) {
|
|
512
|
+
throw new SchemaException(`Column [${column}] of table [${this.#table}] is enumerated over a numeric enum, which has no string form to store. Give the enum string values, or use integer() instead.`);
|
|
513
|
+
}
|
|
514
|
+
return [...new Set(listed)];
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Record an index over the given columns.
|
|
518
|
+
*/
|
|
519
|
+
#indexed(columns, name, unique) {
|
|
520
|
+
const over = Array.isArray(columns) ? columns : [columns];
|
|
521
|
+
const index = {
|
|
522
|
+
name: name ?? `${this.#table}_${over.join("_")}_${unique ? "unique" : "index"}`,
|
|
523
|
+
columns: over,
|
|
524
|
+
unique,
|
|
525
|
+
multiEntry: false
|
|
526
|
+
};
|
|
527
|
+
this.#indexes.push(index);
|
|
528
|
+
return index;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Get the columns the table already has.
|
|
532
|
+
*/
|
|
533
|
+
#columnsOf() {
|
|
534
|
+
return this.#existing === null ? [] : this.#existing.columns;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Get the indexes the table already has.
|
|
538
|
+
*/
|
|
539
|
+
#indexesOf() {
|
|
540
|
+
return this.#existing === null ? [] : this.#existing.indexes;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Determine whether the column exists on the table, declared or already present.
|
|
544
|
+
*/
|
|
545
|
+
#has(column) {
|
|
546
|
+
return this.#columnsOf().some((existing) => existing.name === column) || this.#columns.some((declared) => declared.name === column);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Get the columns of the table once the blueprint is applied.
|
|
550
|
+
*/
|
|
551
|
+
#resolved() {
|
|
552
|
+
const renamed = new Map(this.#renamed.map((rename) => [rename.from, rename.to]));
|
|
553
|
+
const kept = this.#columnsOf().filter((column) => !this.#dropped.includes(column.name)).map((column) => {
|
|
554
|
+
const to = renamed.get(column.name);
|
|
555
|
+
return to === void 0 ? column : { ...column, name: to };
|
|
556
|
+
});
|
|
557
|
+
const added = this.#columns.map((column) => column.toSchema());
|
|
558
|
+
const columns = [...kept, ...added];
|
|
559
|
+
const seen = /* @__PURE__ */ new Set();
|
|
560
|
+
for (const column of columns) {
|
|
561
|
+
if (seen.has(column.name)) {
|
|
562
|
+
throw new SchemaException(`Column [${column.name}] is declared more than once on table [${this.#table}].`);
|
|
563
|
+
}
|
|
564
|
+
seen.add(column.name);
|
|
565
|
+
}
|
|
566
|
+
return columns;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Get the indexes declared by this blueprint, at table level and on columns.
|
|
570
|
+
*/
|
|
571
|
+
#declaredIndexes() {
|
|
572
|
+
const indexes = [...this.#indexes];
|
|
573
|
+
for (const column of this.#columns) {
|
|
574
|
+
for (const requested of column.requested()) {
|
|
575
|
+
indexes.push(this.#requested(column.name, requested));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return indexes;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Build the index schema for a column level request.
|
|
582
|
+
*/
|
|
583
|
+
#requested(column, requested) {
|
|
584
|
+
return {
|
|
585
|
+
name: requested.name ?? `${this.#table}_${column}_${requested.unique ? "unique" : "index"}`,
|
|
586
|
+
columns: [column],
|
|
587
|
+
unique: requested.unique,
|
|
588
|
+
multiEntry: requested.multiEntry
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Get the indexes of the table once the blueprint is applied.
|
|
593
|
+
*/
|
|
594
|
+
#resolvedIndexes() {
|
|
595
|
+
const kept = this.#indexesOf().filter((index) => !this.#unindexed.includes(index.name));
|
|
596
|
+
const indexes = [...kept, ...this.#declaredIndexes()];
|
|
597
|
+
const seen = /* @__PURE__ */ new Set();
|
|
598
|
+
for (const index of indexes) {
|
|
599
|
+
if (seen.has(index.name)) {
|
|
600
|
+
throw new SchemaException(`Index [${index.name}] is declared more than once on table [${this.#table}].`);
|
|
601
|
+
}
|
|
602
|
+
seen.add(index.name);
|
|
603
|
+
}
|
|
604
|
+
return indexes;
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// src/events/DatabaseBlocked.ts
|
|
609
|
+
var DatabaseBlocked = class extends Event {
|
|
610
|
+
/**
|
|
611
|
+
* The name of the database.
|
|
612
|
+
*/
|
|
613
|
+
#database;
|
|
614
|
+
/**
|
|
615
|
+
* Create a new Database Blocked event instance.
|
|
616
|
+
*/
|
|
617
|
+
constructor(database) {
|
|
618
|
+
super("db:database-blocked");
|
|
619
|
+
this.#database = database;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Get the name of the database.
|
|
623
|
+
*/
|
|
624
|
+
get database() {
|
|
625
|
+
return this.#database;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// src/events/MigrationEnded.ts
|
|
630
|
+
var MigrationEnded = class extends Event {
|
|
631
|
+
/**
|
|
632
|
+
* The name of the migration.
|
|
633
|
+
*/
|
|
634
|
+
#migration;
|
|
635
|
+
/**
|
|
636
|
+
* Create a new Migration Ended event instance.
|
|
637
|
+
*/
|
|
638
|
+
constructor(migration) {
|
|
639
|
+
super("db:migration-ended");
|
|
640
|
+
this.#migration = migration;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Get the name of the migration.
|
|
644
|
+
*/
|
|
645
|
+
get migration() {
|
|
646
|
+
return this.#migration;
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// src/events/MigrationsEnded.ts
|
|
651
|
+
var MigrationsEnded = class extends Event {
|
|
652
|
+
/**
|
|
653
|
+
* The name of the connection.
|
|
654
|
+
*/
|
|
655
|
+
#connection;
|
|
656
|
+
/**
|
|
657
|
+
* The names of the migrations.
|
|
658
|
+
*/
|
|
659
|
+
#migrations;
|
|
660
|
+
/**
|
|
661
|
+
* Create a new Migrations Ended event instance.
|
|
662
|
+
*/
|
|
663
|
+
constructor(connection, migrations) {
|
|
664
|
+
super("db:migrations-ended");
|
|
665
|
+
this.#connection = connection;
|
|
666
|
+
this.#migrations = migrations;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Get the name of the connection.
|
|
670
|
+
*/
|
|
671
|
+
get connection() {
|
|
672
|
+
return this.#connection;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Get the names of the migrations.
|
|
676
|
+
*/
|
|
677
|
+
get migrations() {
|
|
678
|
+
return this.#migrations;
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// src/events/MigrationsStarted.ts
|
|
683
|
+
var MigrationsStarted = class extends Event {
|
|
684
|
+
/**
|
|
685
|
+
* The name of the connection.
|
|
686
|
+
*/
|
|
687
|
+
#connection;
|
|
688
|
+
/**
|
|
689
|
+
* The names of the migrations.
|
|
690
|
+
*/
|
|
691
|
+
#migrations;
|
|
692
|
+
/**
|
|
693
|
+
* Create a new Migrations Started event instance.
|
|
694
|
+
*/
|
|
695
|
+
constructor(connection, migrations) {
|
|
696
|
+
super("db:migrations-started");
|
|
697
|
+
this.#connection = connection;
|
|
698
|
+
this.#migrations = migrations;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Get the name of the connection.
|
|
702
|
+
*/
|
|
703
|
+
get connection() {
|
|
704
|
+
return this.#connection;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Get the names of the migrations.
|
|
708
|
+
*/
|
|
709
|
+
get migrations() {
|
|
710
|
+
return this.#migrations;
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// src/events/MigrationStarted.ts
|
|
715
|
+
var MigrationStarted = class extends Event {
|
|
716
|
+
/**
|
|
717
|
+
* The name of the migration.
|
|
718
|
+
*/
|
|
719
|
+
#migration;
|
|
720
|
+
/**
|
|
721
|
+
* Create a new Migration Started event instance.
|
|
722
|
+
*/
|
|
723
|
+
constructor(migration) {
|
|
724
|
+
super("db:migration-started");
|
|
725
|
+
this.#migration = migration;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Get the name of the migration.
|
|
729
|
+
*/
|
|
730
|
+
get migration() {
|
|
731
|
+
return this.#migration;
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
// src/events/NoPendingMigrations.ts
|
|
736
|
+
var NoPendingMigrations = class extends Event {
|
|
737
|
+
/**
|
|
738
|
+
* The name of the connection.
|
|
739
|
+
*/
|
|
740
|
+
#connection;
|
|
741
|
+
/**
|
|
742
|
+
* Create a new No Pending Migrations event instance.
|
|
743
|
+
*/
|
|
744
|
+
constructor(connection) {
|
|
745
|
+
super("db:no-pending-migrations");
|
|
746
|
+
this.#connection = connection;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Get the name of the connection.
|
|
750
|
+
*/
|
|
751
|
+
get connection() {
|
|
752
|
+
return this.#connection;
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// src/events/QueryExecuted.ts
|
|
757
|
+
var QueryExecuted = class extends Event {
|
|
758
|
+
/**
|
|
759
|
+
* The name of the connection the query ran on.
|
|
760
|
+
*/
|
|
761
|
+
#connection;
|
|
762
|
+
/**
|
|
763
|
+
* The name of the table the query ran against.
|
|
764
|
+
*/
|
|
765
|
+
#table;
|
|
766
|
+
/**
|
|
767
|
+
* The description of the plan the query ran under.
|
|
768
|
+
*/
|
|
769
|
+
#plan;
|
|
770
|
+
/**
|
|
771
|
+
* The constraints the query was compiled from.
|
|
772
|
+
*/
|
|
773
|
+
#constraints;
|
|
774
|
+
/**
|
|
775
|
+
* The orders the query was compiled from.
|
|
776
|
+
*/
|
|
777
|
+
#orders;
|
|
778
|
+
/**
|
|
779
|
+
* The maximum number of records the query was allowed to return.
|
|
780
|
+
*/
|
|
781
|
+
#limit;
|
|
782
|
+
/**
|
|
783
|
+
* The time the query took in milliseconds.
|
|
784
|
+
*/
|
|
785
|
+
#duration;
|
|
786
|
+
/**
|
|
787
|
+
* The number of records the query returned or affected.
|
|
788
|
+
*/
|
|
789
|
+
#records;
|
|
790
|
+
/**
|
|
791
|
+
* Create a new Query Executed event instance.
|
|
792
|
+
*/
|
|
793
|
+
constructor(connection, table, plan, constraints, orders, limit, duration, records) {
|
|
794
|
+
super("db:query");
|
|
795
|
+
this.#connection = connection;
|
|
796
|
+
this.#table = table;
|
|
797
|
+
this.#plan = plan;
|
|
798
|
+
this.#constraints = constraints;
|
|
799
|
+
this.#orders = orders;
|
|
800
|
+
this.#limit = limit;
|
|
801
|
+
this.#duration = duration;
|
|
802
|
+
this.#records = records;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Get the name of the connection the query ran on.
|
|
806
|
+
*/
|
|
807
|
+
get connection() {
|
|
808
|
+
return this.#connection;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Get the name of the table the query ran against.
|
|
812
|
+
*/
|
|
813
|
+
get table() {
|
|
814
|
+
return this.#table;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Get the description of the plan the query ran under.
|
|
818
|
+
*/
|
|
819
|
+
get plan() {
|
|
820
|
+
return this.#plan;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Get the constraints the query was compiled from.
|
|
824
|
+
*/
|
|
825
|
+
get constraints() {
|
|
826
|
+
return this.#constraints;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Get the orders the query was compiled from.
|
|
830
|
+
*/
|
|
831
|
+
get orders() {
|
|
832
|
+
return this.#orders;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Get the maximum number of records the query was allowed to return.
|
|
836
|
+
*/
|
|
837
|
+
get limit() {
|
|
838
|
+
return this.#limit;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Get the time the query took in milliseconds.
|
|
842
|
+
*/
|
|
843
|
+
get duration() {
|
|
844
|
+
return this.#duration;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Get the number of records the query returned or affected.
|
|
848
|
+
*/
|
|
849
|
+
get records() {
|
|
850
|
+
return this.#records;
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// src/events/SeederEnded.ts
|
|
855
|
+
var SeederEnded = class extends Event {
|
|
856
|
+
/**
|
|
857
|
+
* The name of the seeder.
|
|
858
|
+
*/
|
|
859
|
+
#seeder;
|
|
860
|
+
/**
|
|
861
|
+
* Create a new Seeder Ended event instance.
|
|
862
|
+
*/
|
|
863
|
+
constructor(seeder) {
|
|
864
|
+
super("db:seeder-ended");
|
|
865
|
+
this.#seeder = seeder;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Get the name of the seeder.
|
|
869
|
+
*/
|
|
870
|
+
get seeder() {
|
|
871
|
+
return this.#seeder;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// src/events/SeederStarted.ts
|
|
876
|
+
var SeederStarted = class extends Event {
|
|
877
|
+
/**
|
|
878
|
+
* The name of the seeder.
|
|
879
|
+
*/
|
|
880
|
+
#seeder;
|
|
881
|
+
/**
|
|
882
|
+
* Create a new Seeder Started event instance.
|
|
883
|
+
*/
|
|
884
|
+
constructor(seeder) {
|
|
885
|
+
super("db:seeder-started");
|
|
886
|
+
this.#seeder = seeder;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Get the name of the seeder.
|
|
890
|
+
*/
|
|
891
|
+
get seeder() {
|
|
892
|
+
return this.#seeder;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
// src/events/SeedingEnded.ts
|
|
897
|
+
var SeedingEnded = class extends Event {
|
|
898
|
+
/**
|
|
899
|
+
* The name of the connection.
|
|
900
|
+
*/
|
|
901
|
+
#connection;
|
|
902
|
+
/**
|
|
903
|
+
* The names of the seeders.
|
|
904
|
+
*/
|
|
905
|
+
#seeders;
|
|
906
|
+
/**
|
|
907
|
+
* Create a new Seeding Ended event instance.
|
|
908
|
+
*/
|
|
909
|
+
constructor(connection, seeders) {
|
|
910
|
+
super("db:seeding-ended");
|
|
911
|
+
this.#connection = connection;
|
|
912
|
+
this.#seeders = seeders;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Get the name of the connection.
|
|
916
|
+
*/
|
|
917
|
+
get connection() {
|
|
918
|
+
return this.#connection;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Get the names of the seeders.
|
|
922
|
+
*/
|
|
923
|
+
get seeders() {
|
|
924
|
+
return this.#seeders;
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
// src/events/SeedingStarted.ts
|
|
929
|
+
var SeedingStarted = class extends Event {
|
|
930
|
+
/**
|
|
931
|
+
* The name of the connection.
|
|
932
|
+
*/
|
|
933
|
+
#connection;
|
|
934
|
+
/**
|
|
935
|
+
* The names of the seeders.
|
|
936
|
+
*/
|
|
937
|
+
#seeders;
|
|
938
|
+
/**
|
|
939
|
+
* Create a new Seeding Started event instance.
|
|
940
|
+
*/
|
|
941
|
+
constructor(connection, seeders) {
|
|
942
|
+
super("db:seeding-started");
|
|
943
|
+
this.#connection = connection;
|
|
944
|
+
this.#seeders = seeders;
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Get the name of the connection.
|
|
948
|
+
*/
|
|
949
|
+
get connection() {
|
|
950
|
+
return this.#connection;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Get the names of the seeders.
|
|
954
|
+
*/
|
|
955
|
+
get seeders() {
|
|
956
|
+
return this.#seeders;
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
// src/events/TransactionBeginning.ts
|
|
961
|
+
var TransactionBeginning = class extends Event {
|
|
962
|
+
/**
|
|
963
|
+
* The name of the connection.
|
|
964
|
+
*/
|
|
965
|
+
#connection;
|
|
966
|
+
/**
|
|
967
|
+
* Create a new Transaction Beginning event instance.
|
|
968
|
+
*/
|
|
969
|
+
constructor(connection) {
|
|
970
|
+
super("db:transaction-beginning");
|
|
971
|
+
this.#connection = connection;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Get the name of the connection.
|
|
975
|
+
*/
|
|
976
|
+
get connection() {
|
|
977
|
+
return this.#connection;
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
// src/events/TransactionCommitted.ts
|
|
982
|
+
var TransactionCommitted = class extends Event {
|
|
983
|
+
/**
|
|
984
|
+
* The name of the connection.
|
|
985
|
+
*/
|
|
986
|
+
#connection;
|
|
987
|
+
/**
|
|
988
|
+
* Create a new Transaction Committed event instance.
|
|
989
|
+
*/
|
|
990
|
+
constructor(connection) {
|
|
991
|
+
super("db:transaction-committed");
|
|
992
|
+
this.#connection = connection;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Get the name of the connection.
|
|
996
|
+
*/
|
|
997
|
+
get connection() {
|
|
998
|
+
return this.#connection;
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// src/events/TransactionRolledBack.ts
|
|
1003
|
+
var TransactionRolledBack = class extends Event {
|
|
1004
|
+
/**
|
|
1005
|
+
* The name of the connection.
|
|
1006
|
+
*/
|
|
1007
|
+
#connection;
|
|
1008
|
+
/**
|
|
1009
|
+
* The reason the transaction was rolled back.
|
|
1010
|
+
*/
|
|
1011
|
+
#reason;
|
|
1012
|
+
/**
|
|
1013
|
+
* Create a new Transaction Rolled Back event instance.
|
|
1014
|
+
*/
|
|
1015
|
+
constructor(connection, reason) {
|
|
1016
|
+
super("db:transaction-rolled-back");
|
|
1017
|
+
this.#connection = connection;
|
|
1018
|
+
this.#reason = reason;
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Get the name of the connection.
|
|
1022
|
+
*/
|
|
1023
|
+
get connection() {
|
|
1024
|
+
return this.#connection;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Get the reason the transaction was rolled back.
|
|
1028
|
+
*/
|
|
1029
|
+
get reason() {
|
|
1030
|
+
return this.#reason;
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
// src/events/Dispatcher.ts
|
|
1035
|
+
var Dispatcher = class {
|
|
1036
|
+
/**
|
|
1037
|
+
* The target every database event is dispatched through.
|
|
1038
|
+
*/
|
|
1039
|
+
static #target = new EventTarget();
|
|
1040
|
+
/**
|
|
1041
|
+
* Dispatch an event to the registered listeners.
|
|
1042
|
+
*/
|
|
1043
|
+
static dispatch(event) {
|
|
1044
|
+
this.#target.dispatchEvent(event);
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Register a listener for an event type.
|
|
1048
|
+
*/
|
|
1049
|
+
static listen(type, listener, once = false) {
|
|
1050
|
+
this.#target.addEventListener(type, listener, { once });
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Remove a listener for an event type.
|
|
1054
|
+
*/
|
|
1055
|
+
static forget(type, listener) {
|
|
1056
|
+
this.#target.removeEventListener(type, listener);
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
// src/database/Request.ts
|
|
1061
|
+
var Request = class {
|
|
1062
|
+
/**
|
|
1063
|
+
* Resolve when the request succeeds, reject when it fails.
|
|
1064
|
+
*/
|
|
1065
|
+
static settle(request, tolerate = false) {
|
|
1066
|
+
return new Promise((resolve, reject) => {
|
|
1067
|
+
request.onsuccess = () => resolve(request.result);
|
|
1068
|
+
request.onerror = (event) => {
|
|
1069
|
+
if (tolerate) {
|
|
1070
|
+
event.preventDefault();
|
|
1071
|
+
event.stopPropagation();
|
|
1072
|
+
}
|
|
1073
|
+
reject(this.translate(request.error));
|
|
1074
|
+
};
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Walk a cursor, invoking the callback for each record until it asks to stop.
|
|
1079
|
+
*/
|
|
1080
|
+
static walk(request, callback) {
|
|
1081
|
+
return new Promise((resolve, reject) => {
|
|
1082
|
+
request.onsuccess = () => {
|
|
1083
|
+
const cursor = request.result;
|
|
1084
|
+
if (cursor === null) {
|
|
1085
|
+
resolve();
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (callback(cursor) === false) {
|
|
1089
|
+
resolve();
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
cursor.continue();
|
|
1093
|
+
};
|
|
1094
|
+
request.onerror = () => reject(this.translate(request.error));
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Name the failure, where the platform reports one this package can say more about.
|
|
1099
|
+
*/
|
|
1100
|
+
static translate(error) {
|
|
1101
|
+
if (error !== null && error.name === "QuotaExceededError") {
|
|
1102
|
+
return new QuotaExceededException();
|
|
1103
|
+
}
|
|
1104
|
+
return error;
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
// src/schema/Coercer.ts
|
|
1109
|
+
var FALSY = ["false", "0"];
|
|
1110
|
+
var Coercer = class {
|
|
1111
|
+
/**
|
|
1112
|
+
* Coerce a value into its declared column type.
|
|
1113
|
+
*/
|
|
1114
|
+
static coerce(value, type, strict) {
|
|
1115
|
+
if (value === null || value === void 0) {
|
|
1116
|
+
return value;
|
|
1117
|
+
}
|
|
1118
|
+
switch (type) {
|
|
1119
|
+
case "string":
|
|
1120
|
+
return String(value);
|
|
1121
|
+
case "integer":
|
|
1122
|
+
return this.#numeric(value, strict, true);
|
|
1123
|
+
case "float":
|
|
1124
|
+
return this.#numeric(value, strict, false);
|
|
1125
|
+
case "boolean":
|
|
1126
|
+
return typeof value === "string" && FALSY.includes(value) ? false : Boolean(value);
|
|
1127
|
+
case "decimal":
|
|
1128
|
+
return this.#scaled(value, strict);
|
|
1129
|
+
case "enum":
|
|
1130
|
+
return String(value);
|
|
1131
|
+
case "date":
|
|
1132
|
+
case "datetime":
|
|
1133
|
+
return this.#temporal(value, strict);
|
|
1134
|
+
default:
|
|
1135
|
+
return this.#structured(value, strict);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Prepare a record for insertion, applying defaults, timestamps and coercion.
|
|
1140
|
+
*/
|
|
1141
|
+
static insertable(record, schema, strict, at) {
|
|
1142
|
+
const prepared = { ...record };
|
|
1143
|
+
this.#stamp(prepared, schema, at, true);
|
|
1144
|
+
for (const column of schema.columns) {
|
|
1145
|
+
if (this.#generated(column, prepared)) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (!Object.hasOwn(prepared, column.name) && column.hasDefault) {
|
|
1149
|
+
prepared[column.name] = column.default;
|
|
1150
|
+
}
|
|
1151
|
+
prepared[column.name] = this.#value(prepared[column.name], column, schema, strict);
|
|
1152
|
+
}
|
|
1153
|
+
return prepared;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Prepare a partial record for update, touching timestamps and coercing provided columns.
|
|
1157
|
+
*/
|
|
1158
|
+
static updatable(record, schema, strict, at) {
|
|
1159
|
+
const prepared = { ...record };
|
|
1160
|
+
this.#stamp(prepared, schema, at, false);
|
|
1161
|
+
for (const column of schema.columns) {
|
|
1162
|
+
if (!Object.hasOwn(prepared, column.name)) {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
prepared[column.name] = this.#value(prepared[column.name], column, schema, strict);
|
|
1166
|
+
}
|
|
1167
|
+
return prepared;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Coerce a single column value, enforcing nullability.
|
|
1171
|
+
*/
|
|
1172
|
+
static #value(value, column, schema, strict) {
|
|
1173
|
+
const coerced = this.#accepted(this.coerce(value, column.type, strict), column, schema, strict);
|
|
1174
|
+
if (coerced !== null && coerced !== void 0) {
|
|
1175
|
+
return coerced;
|
|
1176
|
+
}
|
|
1177
|
+
if (column.nullable) {
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
if (strict) {
|
|
1181
|
+
throw new NotNullConstraintViolationException(schema.table, column.name);
|
|
1182
|
+
}
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Reject a value an enumerated column does not accept.
|
|
1187
|
+
*/
|
|
1188
|
+
static #accepted(value, column, schema, strict) {
|
|
1189
|
+
if (column.values === null || value === null || value === void 0 || column.values.includes(value)) {
|
|
1190
|
+
return value;
|
|
1191
|
+
}
|
|
1192
|
+
if (strict) {
|
|
1193
|
+
throw new CheckConstraintViolationException(schema.table, column.name, value, column.values);
|
|
1194
|
+
}
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Coerce a value into a whole number of a decimal column's smallest unit.
|
|
1199
|
+
*/
|
|
1200
|
+
static #scaled(value, strict) {
|
|
1201
|
+
const number = Number(value);
|
|
1202
|
+
if (Number.isNaN(number)) {
|
|
1203
|
+
if (strict) {
|
|
1204
|
+
throw new TypeError(`Unable to coerce [${String(value)}] into a number.`);
|
|
1205
|
+
}
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
if (!Number.isInteger(number)) {
|
|
1209
|
+
if (strict) {
|
|
1210
|
+
throw new TypeError(`A decimal column stores a whole number of its smallest unit, so [${String(value)}] cannot be written. Scale it first, as in Math.round(19.99 * 100).`);
|
|
1211
|
+
}
|
|
1212
|
+
return Math.round(number);
|
|
1213
|
+
}
|
|
1214
|
+
return number;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Determine whether the column is a key the database generates.
|
|
1218
|
+
*/
|
|
1219
|
+
static #generated(column, record) {
|
|
1220
|
+
return column.primary && column.increments && !Object.hasOwn(record, column.name);
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Fill the timestamp columns the table declares.
|
|
1224
|
+
*/
|
|
1225
|
+
static #stamp(record, schema, at, creating) {
|
|
1226
|
+
if (!schema.timestamps) {
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (creating && !Object.hasOwn(record, "created_at")) {
|
|
1230
|
+
record["created_at"] = at;
|
|
1231
|
+
}
|
|
1232
|
+
if (!Object.hasOwn(record, "updated_at")) {
|
|
1233
|
+
record["updated_at"] = at;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Coerce a value into a number, truncating when the column is an integer.
|
|
1238
|
+
*/
|
|
1239
|
+
static #numeric(value, strict, truncate) {
|
|
1240
|
+
const number = Number(value);
|
|
1241
|
+
if (Number.isNaN(number)) {
|
|
1242
|
+
if (strict) {
|
|
1243
|
+
throw new TypeError(`Unable to coerce [${String(value)}] into a number.`);
|
|
1244
|
+
}
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
return truncate ? Math.trunc(number) : number;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Coerce a value into a date.
|
|
1251
|
+
*/
|
|
1252
|
+
static #temporal(value, strict) {
|
|
1253
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
1254
|
+
if (Number.isNaN(date.getTime())) {
|
|
1255
|
+
if (strict) {
|
|
1256
|
+
throw new TypeError(`Unable to coerce [${String(value)}] into a date.`);
|
|
1257
|
+
}
|
|
1258
|
+
return null;
|
|
1259
|
+
}
|
|
1260
|
+
return date;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Coerce a value into a structure, parsing it when it arrives as a string.
|
|
1264
|
+
*/
|
|
1265
|
+
static #structured(value, strict) {
|
|
1266
|
+
if (typeof value !== "string") {
|
|
1267
|
+
return value;
|
|
1268
|
+
}
|
|
1269
|
+
try {
|
|
1270
|
+
return JSON.parse(value);
|
|
1271
|
+
} catch {
|
|
1272
|
+
if (strict) {
|
|
1273
|
+
throw new TypeError(`Unable to coerce [${value}] into a structure.`);
|
|
1274
|
+
}
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
// src/query/Columns.ts
|
|
1281
|
+
var Columns = class {
|
|
1282
|
+
/**
|
|
1283
|
+
* Split a column into its table and its name.
|
|
1284
|
+
*/
|
|
1285
|
+
static split(column) {
|
|
1286
|
+
const separator = column.indexOf(".");
|
|
1287
|
+
if (separator === -1) {
|
|
1288
|
+
return { table: null, name: column };
|
|
1289
|
+
}
|
|
1290
|
+
return { table: column.slice(0, separator), name: column.slice(separator + 1) };
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Qualify a column with the table that owns it, or fail when that is not decidable.
|
|
1294
|
+
*/
|
|
1295
|
+
static resolve(column, tables) {
|
|
1296
|
+
const { table, name } = this.split(column);
|
|
1297
|
+
if (table !== null) {
|
|
1298
|
+
if (!tables.has(table)) {
|
|
1299
|
+
throw new SchemaException(`Column [${column}] names table [${table}], which this query does not join.`);
|
|
1300
|
+
}
|
|
1301
|
+
return column;
|
|
1302
|
+
}
|
|
1303
|
+
const owners = [...tables].filter(([, columns]) => columns.includes(name)).map(([owner]) => owner);
|
|
1304
|
+
if (owners.length > 1) {
|
|
1305
|
+
throw new SchemaException(`Column [${name}] is ambiguous across tables [${owners.join(", ")}]. Qualify it, as in [${owners[0]}.${name}].`);
|
|
1306
|
+
}
|
|
1307
|
+
if (owners.length === 0) {
|
|
1308
|
+
throw new SchemaException(`Column [${name}] does not exist on any table this query reads.`);
|
|
1309
|
+
}
|
|
1310
|
+
return `${owners[0]}.${name}`;
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Parse a projection, which may alias the column it selects.
|
|
1314
|
+
*/
|
|
1315
|
+
static parse(expression) {
|
|
1316
|
+
const alias = expression.toLowerCase().indexOf(" as ");
|
|
1317
|
+
if (alias === -1) {
|
|
1318
|
+
return { column: expression, alias: this.split(expression).name };
|
|
1319
|
+
}
|
|
1320
|
+
return {
|
|
1321
|
+
column: expression.slice(0, alias).trim(),
|
|
1322
|
+
alias: expression.slice(alias + 4).trim()
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
// src/query/Comparator.ts
|
|
1328
|
+
var Comparator = class {
|
|
1329
|
+
/**
|
|
1330
|
+
* Sort rows by the requested orders, reading each column through the accessor.
|
|
1331
|
+
*/
|
|
1332
|
+
static sort(rows, orders, value) {
|
|
1333
|
+
if (orders.length === 0) {
|
|
1334
|
+
return rows;
|
|
1335
|
+
}
|
|
1336
|
+
return [...rows].sort((a, b) => {
|
|
1337
|
+
for (const order of orders) {
|
|
1338
|
+
const compared = this.compare(value(a, order.column), value(b, order.column));
|
|
1339
|
+
if (compared !== 0) {
|
|
1340
|
+
return order.direction === "desc" ? -compared : compared;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return 0;
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Compare two column values, treating null as the lowest value.
|
|
1348
|
+
*/
|
|
1349
|
+
static compare(a, b) {
|
|
1350
|
+
const left = a instanceof Date ? a.getTime() : a;
|
|
1351
|
+
const right = b instanceof Date ? b.getTime() : b;
|
|
1352
|
+
if (this.#missing(left) || this.#missing(right)) {
|
|
1353
|
+
return this.#missing(left) && this.#missing(right) ? 0 : this.#missing(left) ? -1 : 1;
|
|
1354
|
+
}
|
|
1355
|
+
if (left === right) {
|
|
1356
|
+
return 0;
|
|
1357
|
+
}
|
|
1358
|
+
return left < right ? -1 : 1;
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Determine whether the value is absent, which SQL orders below everything else.
|
|
1362
|
+
*/
|
|
1363
|
+
static #missing(value) {
|
|
1364
|
+
return value === null || value === void 0;
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
// src/query/Join.ts
|
|
1369
|
+
var Join = class {
|
|
1370
|
+
/**
|
|
1371
|
+
* The conditions the tables are joined on.
|
|
1372
|
+
*/
|
|
1373
|
+
#conditions = [];
|
|
1374
|
+
/**
|
|
1375
|
+
* Join on a pair of columns.
|
|
1376
|
+
*/
|
|
1377
|
+
on(first, operator, second) {
|
|
1378
|
+
return this.#condition("and", first, operator, second);
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Join on a pair of columns, disjunctively.
|
|
1382
|
+
*/
|
|
1383
|
+
orOn(first, operator, second) {
|
|
1384
|
+
return this.#condition("or", first, operator, second);
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Get the conditions the tables are joined on.
|
|
1388
|
+
*/
|
|
1389
|
+
conditions() {
|
|
1390
|
+
return this.#conditions;
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Record a condition, allowing the operator to be left implicit.
|
|
1394
|
+
*/
|
|
1395
|
+
#condition(conjunction, first, operator, second) {
|
|
1396
|
+
const resolved = second === void 0 ? { operator: "=", second: operator } : { operator, second };
|
|
1397
|
+
this.#conditions.push({ first, operator: resolved.operator, second: resolved.second, conjunction });
|
|
1398
|
+
return this;
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
// src/query/Predicate.ts
|
|
1403
|
+
var Predicate = class {
|
|
1404
|
+
/**
|
|
1405
|
+
* Compile a list of constraints into a record test.
|
|
1406
|
+
*/
|
|
1407
|
+
static compile(constraints) {
|
|
1408
|
+
if (constraints.length === 0) {
|
|
1409
|
+
return () => true;
|
|
1410
|
+
}
|
|
1411
|
+
const groups = this.#grouped(constraints);
|
|
1412
|
+
return (record) => groups.some(
|
|
1413
|
+
(group) => group.every(
|
|
1414
|
+
(constraint) => this.#test(constraint, record)
|
|
1415
|
+
)
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Split the constraints into disjunctive groups, so and binds tighter than or.
|
|
1420
|
+
*/
|
|
1421
|
+
static #grouped(constraints) {
|
|
1422
|
+
const groups = [];
|
|
1423
|
+
for (const [index, constraint] of constraints.entries()) {
|
|
1424
|
+
if (index === 0 || constraint.conjunction === "or") {
|
|
1425
|
+
groups.push([]);
|
|
1426
|
+
}
|
|
1427
|
+
groups[groups.length - 1]?.push(constraint);
|
|
1428
|
+
}
|
|
1429
|
+
return groups;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Test a single constraint against a record.
|
|
1433
|
+
*/
|
|
1434
|
+
static #test(constraint, record) {
|
|
1435
|
+
if (constraint.type === "nested") {
|
|
1436
|
+
return this.#negate(constraint.not, this.compile(constraint.constraints)(record));
|
|
1437
|
+
}
|
|
1438
|
+
const held = record[constraint.column];
|
|
1439
|
+
if (constraint.type === "null") {
|
|
1440
|
+
return this.#negate(constraint.not, held === null || held === void 0);
|
|
1441
|
+
}
|
|
1442
|
+
if (held === null || held === void 0) {
|
|
1443
|
+
return false;
|
|
1444
|
+
}
|
|
1445
|
+
if (constraint.type === "column") {
|
|
1446
|
+
const other = record[constraint.other];
|
|
1447
|
+
if (other === null || other === void 0) {
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
return this.#negate(constraint.not, this.#compare(held, constraint.operator, other));
|
|
1451
|
+
}
|
|
1452
|
+
if (constraint.type === "part") {
|
|
1453
|
+
return this.#negate(constraint.not, this.#part(held, constraint.part) === constraint.value);
|
|
1454
|
+
}
|
|
1455
|
+
if (constraint.type === "in") {
|
|
1456
|
+
return this.#negate(constraint.not, constraint.values.some((value) => this.#compare(held, "==", value)));
|
|
1457
|
+
}
|
|
1458
|
+
if (constraint.type === "between") {
|
|
1459
|
+
return this.#negate(constraint.not, this.#compare(held, ">=", constraint.from) && this.#compare(held, "<=", constraint.to));
|
|
1460
|
+
}
|
|
1461
|
+
return this.#negate(constraint.not, this.#compare(held, constraint.operator, constraint.value));
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Read one part of a value that should hold a date.
|
|
1465
|
+
*/
|
|
1466
|
+
static #part(held, part) {
|
|
1467
|
+
const date = held instanceof Date ? held : new Date(held);
|
|
1468
|
+
if (Number.isNaN(date.getTime())) {
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
if (part === "year") {
|
|
1472
|
+
return date.getFullYear();
|
|
1473
|
+
}
|
|
1474
|
+
return part === "month" ? date.getMonth() + 1 : date.getDate();
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Negate a result when the constraint asks for it.
|
|
1478
|
+
*/
|
|
1479
|
+
static #negate(not, result) {
|
|
1480
|
+
return not ? !result : result;
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Compare a held value against a given one under the operator.
|
|
1484
|
+
*/
|
|
1485
|
+
static #compare(held, operator, given) {
|
|
1486
|
+
if (operator === "like" || operator === "not like") {
|
|
1487
|
+
const matched = typeof held === "string" && this.#like(String(given), held);
|
|
1488
|
+
return operator === "like" ? matched : !matched;
|
|
1489
|
+
}
|
|
1490
|
+
const a = this.#comparable(held);
|
|
1491
|
+
const b = this.#comparable(given);
|
|
1492
|
+
switch (operator) {
|
|
1493
|
+
case "=":
|
|
1494
|
+
case "==":
|
|
1495
|
+
return a == b;
|
|
1496
|
+
case "===":
|
|
1497
|
+
return a === b;
|
|
1498
|
+
case "!=":
|
|
1499
|
+
case "<>":
|
|
1500
|
+
return a != b;
|
|
1501
|
+
case "!==":
|
|
1502
|
+
return a !== b;
|
|
1503
|
+
case "<":
|
|
1504
|
+
return a < b;
|
|
1505
|
+
case ">":
|
|
1506
|
+
return a > b;
|
|
1507
|
+
case "<=":
|
|
1508
|
+
return a <= b;
|
|
1509
|
+
default:
|
|
1510
|
+
return a >= b;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Reduce a value to something the comparison operators can order.
|
|
1515
|
+
*/
|
|
1516
|
+
static #comparable(value) {
|
|
1517
|
+
return value instanceof Date ? value.getTime() : value;
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Determine whether a subject matches a like pattern.
|
|
1521
|
+
*/
|
|
1522
|
+
static #like(pattern, subject) {
|
|
1523
|
+
const tokens = this.#tokens(pattern);
|
|
1524
|
+
let token = 0;
|
|
1525
|
+
let index = 0;
|
|
1526
|
+
let wildcard = -1;
|
|
1527
|
+
let resume = 0;
|
|
1528
|
+
while (index < subject.length) {
|
|
1529
|
+
const current = tokens[token];
|
|
1530
|
+
if (current !== void 0 && current.kind === "any") {
|
|
1531
|
+
wildcard = token;
|
|
1532
|
+
resume = index;
|
|
1533
|
+
token++;
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
if (current !== void 0 && (current.kind === "one" || current.value === subject[index].toLowerCase())) {
|
|
1537
|
+
token++;
|
|
1538
|
+
index++;
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
if (wildcard === -1) {
|
|
1542
|
+
return false;
|
|
1543
|
+
}
|
|
1544
|
+
token = wildcard + 1;
|
|
1545
|
+
resume++;
|
|
1546
|
+
index = resume;
|
|
1547
|
+
}
|
|
1548
|
+
while (tokens[token]?.kind === "any") {
|
|
1549
|
+
token++;
|
|
1550
|
+
}
|
|
1551
|
+
return token === tokens.length;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Reduce a like pattern to the tokens it matches by.
|
|
1555
|
+
*/
|
|
1556
|
+
static #tokens(pattern) {
|
|
1557
|
+
const tokens = [];
|
|
1558
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
1559
|
+
const character = pattern[index];
|
|
1560
|
+
if (character === "\\" && (pattern[index + 1] === "%" || pattern[index + 1] === "_")) {
|
|
1561
|
+
tokens.push({ kind: "literal", value: pattern[index + 1].toLowerCase() });
|
|
1562
|
+
index++;
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
if (character === "%") {
|
|
1566
|
+
if (tokens.at(-1)?.kind !== "any") {
|
|
1567
|
+
tokens.push({ kind: "any", value: "" });
|
|
1568
|
+
}
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
if (character === "_") {
|
|
1572
|
+
tokens.push({ kind: "one", value: "" });
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
tokens.push({ kind: "literal", value: character.toLowerCase() });
|
|
1576
|
+
}
|
|
1577
|
+
return tokens;
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
// src/query/Joiner.ts
|
|
1582
|
+
var Joiner = class {
|
|
1583
|
+
/**
|
|
1584
|
+
* Prefix every key of the given records with the table that owns it.
|
|
1585
|
+
*/
|
|
1586
|
+
static qualify(records, table) {
|
|
1587
|
+
return records.map((record) => {
|
|
1588
|
+
const qualified = {};
|
|
1589
|
+
for (const [column, value] of Object.entries(record)) {
|
|
1590
|
+
qualified[`${table}.${column}`] = value;
|
|
1591
|
+
}
|
|
1592
|
+
return qualified;
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Join two sets of qualified records, carrying unmatched rows through where the type asks for it.
|
|
1597
|
+
*/
|
|
1598
|
+
static join(left, right, clause, columns) {
|
|
1599
|
+
if (clause.type === "cross") {
|
|
1600
|
+
return left.flatMap((row) => right.map((match) => ({ ...row, ...match })));
|
|
1601
|
+
}
|
|
1602
|
+
if (clause.type === "right") {
|
|
1603
|
+
return this.#matched(right, left, clause, this.#columnsOf(left)).map(
|
|
1604
|
+
(row) => row
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
return this.#matched(left, right, clause, columns);
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Pair each row of the driving side with the rows of the other that satisfy the conditions.
|
|
1611
|
+
*/
|
|
1612
|
+
static #matched(driving, other, clause, columns) {
|
|
1613
|
+
const hashed = this.#hashable(clause) ? this.#hash(other, clause) : null;
|
|
1614
|
+
const matches = Predicate.compile(this.#constraints(clause));
|
|
1615
|
+
const joined = [];
|
|
1616
|
+
for (const row of driving) {
|
|
1617
|
+
const candidates = hashed === null ? other : hashed.get(row[clause.conditions[0].first]) ?? [];
|
|
1618
|
+
const paired = candidates.map((candidate) => ({ ...row, ...candidate })).filter(matches);
|
|
1619
|
+
if (paired.length > 0) {
|
|
1620
|
+
joined.push(...paired);
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
if (clause.type !== "inner") {
|
|
1624
|
+
joined.push({ ...row, ...this.#absent(columns) });
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return joined;
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Determine whether the conditions reduce to a single equality, which a hash can serve.
|
|
1631
|
+
*/
|
|
1632
|
+
static #hashable(clause) {
|
|
1633
|
+
const condition = clause.conditions[0];
|
|
1634
|
+
return clause.conditions.length === 1 && condition !== void 0 && condition.operator === "=";
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Index the other side by the value its join column holds.
|
|
1638
|
+
*/
|
|
1639
|
+
static #hash(records, clause) {
|
|
1640
|
+
const column = clause.conditions[0].second;
|
|
1641
|
+
const hashed = /* @__PURE__ */ new Map();
|
|
1642
|
+
for (const record of records) {
|
|
1643
|
+
const key = record[column];
|
|
1644
|
+
const bucket = hashed.get(key);
|
|
1645
|
+
if (bucket === void 0) {
|
|
1646
|
+
hashed.set(key, [record]);
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
bucket.push(record);
|
|
1650
|
+
}
|
|
1651
|
+
return hashed;
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Compile the join conditions into constraints over the merged row.
|
|
1655
|
+
*/
|
|
1656
|
+
static #constraints(clause) {
|
|
1657
|
+
return clause.conditions.map((condition, index) => ({
|
|
1658
|
+
type: "column",
|
|
1659
|
+
column: condition.first,
|
|
1660
|
+
operator: condition.operator,
|
|
1661
|
+
other: condition.second,
|
|
1662
|
+
conjunction: index === 0 ? "and" : condition.conjunction,
|
|
1663
|
+
not: false
|
|
1664
|
+
}));
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Build the null columns SQL gives an unmatched row.
|
|
1668
|
+
*/
|
|
1669
|
+
static #absent(columns) {
|
|
1670
|
+
return Object.fromEntries(columns.map((column) => [column, null]));
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Get every qualified column present across the given records.
|
|
1674
|
+
*/
|
|
1675
|
+
static #columnsOf(records) {
|
|
1676
|
+
const columns = /* @__PURE__ */ new Set();
|
|
1677
|
+
for (const record of records) {
|
|
1678
|
+
for (const column of Object.keys(record)) {
|
|
1679
|
+
columns.add(column);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return [...columns];
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1686
|
+
// src/query/Planner.ts
|
|
1687
|
+
var RANGEABLE = ["=", "==", "===", ">", ">=", "<", "<="];
|
|
1688
|
+
var Planner = class {
|
|
1689
|
+
/**
|
|
1690
|
+
* Compile the constraints and orders into an execution plan.
|
|
1691
|
+
*/
|
|
1692
|
+
static plan(constraints, orders, schema) {
|
|
1693
|
+
const candidate = this.#disjunctive(constraints) ? null : this.#candidate(constraints, schema);
|
|
1694
|
+
const ordering = this.#disjunctive(constraints) ? null : this.#ordering(orders, schema);
|
|
1695
|
+
if (candidate === null) {
|
|
1696
|
+
return this.#ordered(constraints, orders, ordering);
|
|
1697
|
+
}
|
|
1698
|
+
const aligned = ordering !== null && ordering.source === candidate.source && ordering.index === candidate.index && candidate.values === null;
|
|
1699
|
+
return {
|
|
1700
|
+
source: candidate.source,
|
|
1701
|
+
index: candidate.index,
|
|
1702
|
+
range: candidate.range,
|
|
1703
|
+
values: candidate.values,
|
|
1704
|
+
direction: aligned ? this.#direction(orders) : "next",
|
|
1705
|
+
ordered: aligned,
|
|
1706
|
+
residual: constraints.filter((constraint) => constraint !== candidate.constraint)
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Describe a plan for the query log.
|
|
1711
|
+
*/
|
|
1712
|
+
static describe(plan) {
|
|
1713
|
+
if (plan.source === "scan") {
|
|
1714
|
+
return "scan";
|
|
1715
|
+
}
|
|
1716
|
+
return plan.source === "key" ? "key" : `index:${plan.index}`;
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Build the plan for a query no constraint could drive.
|
|
1720
|
+
*/
|
|
1721
|
+
static #ordered(constraints, orders, ordering) {
|
|
1722
|
+
if (ordering === null) {
|
|
1723
|
+
return { source: "scan", index: null, range: null, values: null, direction: "next", ordered: false, residual: constraints };
|
|
1724
|
+
}
|
|
1725
|
+
return {
|
|
1726
|
+
source: ordering.source,
|
|
1727
|
+
index: ordering.index,
|
|
1728
|
+
range: null,
|
|
1729
|
+
values: null,
|
|
1730
|
+
direction: this.#direction(orders),
|
|
1731
|
+
ordered: true,
|
|
1732
|
+
residual: constraints
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Determine whether any top level constraint is disjunctive.
|
|
1737
|
+
*/
|
|
1738
|
+
static #disjunctive(constraints) {
|
|
1739
|
+
return constraints.some((constraint, index) => index > 0 && constraint.conjunction === "or");
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Get the most selective constraint able to drive the scan.
|
|
1743
|
+
*/
|
|
1744
|
+
static #candidate(constraints, schema) {
|
|
1745
|
+
let best = null;
|
|
1746
|
+
for (const constraint of constraints) {
|
|
1747
|
+
const candidate = this.#candidacy(constraint, schema);
|
|
1748
|
+
if (candidate !== null && (best === null || candidate.rank < best.rank)) {
|
|
1749
|
+
best = candidate;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return best;
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Assess whether a single constraint can drive the scan.
|
|
1756
|
+
*/
|
|
1757
|
+
static #candidacy(constraint, schema) {
|
|
1758
|
+
if (constraint.type === "nested" || constraint.type === "null" || constraint.type === "column" || constraint.type === "part" || constraint.not) {
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
const target = this.#target(constraint.column, schema);
|
|
1762
|
+
if (target === null) {
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
if (constraint.type === "in") {
|
|
1766
|
+
if (constraint.values.length === 0 || !constraint.values.every((value) => this.#keyable(value))) {
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
return { constraint, ...target, range: null, values: constraint.values };
|
|
1770
|
+
}
|
|
1771
|
+
if (constraint.type === "between") {
|
|
1772
|
+
if (!this.#keyable(constraint.from) || !this.#keyable(constraint.to)) {
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
return { constraint, ...target, range: IDBKeyRange.bound(constraint.from, constraint.to, false, false), values: null };
|
|
1776
|
+
}
|
|
1777
|
+
if (!RANGEABLE.includes(constraint.operator) || !this.#keyable(constraint.value)) {
|
|
1778
|
+
return null;
|
|
1779
|
+
}
|
|
1780
|
+
return { constraint, ...target, range: this.#range(constraint.operator, constraint.value), values: null };
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Resolve the column to the key path or a single column index.
|
|
1784
|
+
*/
|
|
1785
|
+
static #target(column, schema) {
|
|
1786
|
+
if (schema.key === column) {
|
|
1787
|
+
return { source: "key", index: null, rank: 0 };
|
|
1788
|
+
}
|
|
1789
|
+
const index = schema.indexes.find(
|
|
1790
|
+
(candidate) => candidate.columns.length === 1 && candidate.columns[0] === column
|
|
1791
|
+
);
|
|
1792
|
+
if (index === void 0) {
|
|
1793
|
+
return null;
|
|
1794
|
+
}
|
|
1795
|
+
return { source: "index", index: index.name, rank: index.unique ? 1 : 2 };
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Get the index able to satisfy the requested order without dropping records.
|
|
1799
|
+
*/
|
|
1800
|
+
static #ordering(orders, schema) {
|
|
1801
|
+
const order = orders[0];
|
|
1802
|
+
if (orders.length !== 1 || order === void 0) {
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
const target = this.#target(order.column, schema);
|
|
1806
|
+
if (target === null) {
|
|
1807
|
+
return null;
|
|
1808
|
+
}
|
|
1809
|
+
const column = schema.columns.find((candidate) => candidate.name === order.column);
|
|
1810
|
+
if (column !== void 0 && column.nullable) {
|
|
1811
|
+
return null;
|
|
1812
|
+
}
|
|
1813
|
+
return { constraint: { type: "null", column: order.column, conjunction: "and", not: false }, ...target, range: null, values: null };
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Get the cursor direction the orders ask for.
|
|
1817
|
+
*/
|
|
1818
|
+
static #direction(orders) {
|
|
1819
|
+
return orders[0]?.direction === "desc" ? "prev" : "next";
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Build the key range for a comparison operator.
|
|
1823
|
+
*/
|
|
1824
|
+
static #range(operator, value) {
|
|
1825
|
+
switch (operator) {
|
|
1826
|
+
case ">":
|
|
1827
|
+
return IDBKeyRange.lowerBound(value, true);
|
|
1828
|
+
case ">=":
|
|
1829
|
+
return IDBKeyRange.lowerBound(value, false);
|
|
1830
|
+
case "<":
|
|
1831
|
+
return IDBKeyRange.upperBound(value, true);
|
|
1832
|
+
case "<=":
|
|
1833
|
+
return IDBKeyRange.upperBound(value, false);
|
|
1834
|
+
default:
|
|
1835
|
+
return IDBKeyRange.only(value);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Determine whether the value may be used as an IndexedDB key.
|
|
1840
|
+
*/
|
|
1841
|
+
static #keyable(value) {
|
|
1842
|
+
return value !== null && value !== void 0;
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
// src/query/Signature.ts
|
|
1847
|
+
var SEPARATOR = "";
|
|
1848
|
+
var Signature = class {
|
|
1849
|
+
/**
|
|
1850
|
+
* Build a signature identifying a record by every column it holds.
|
|
1851
|
+
*/
|
|
1852
|
+
static of(record) {
|
|
1853
|
+
return Object.keys(record).sort().map((column) => this.#segment(column) + this.#segment(this.value(record[column]))).join("");
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Build a signature identifying an ordered list of values.
|
|
1857
|
+
*/
|
|
1858
|
+
static ofValues(values) {
|
|
1859
|
+
return values.map((value) => this.#segment(this.value(value))).join("");
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Encode one part of a signature so that its own content cannot be read as a boundary.
|
|
1863
|
+
*/
|
|
1864
|
+
static #segment(part) {
|
|
1865
|
+
return `${part.length}${SEPARATOR}${part}`;
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Encode a single value, keeping types and absence distinguishable.
|
|
1869
|
+
*/
|
|
1870
|
+
static value(value) {
|
|
1871
|
+
if (value === void 0) {
|
|
1872
|
+
return "?";
|
|
1873
|
+
}
|
|
1874
|
+
if (value === null) {
|
|
1875
|
+
return "~";
|
|
1876
|
+
}
|
|
1877
|
+
if (value instanceof Date) {
|
|
1878
|
+
return `d:${value.getTime()}`;
|
|
1879
|
+
}
|
|
1880
|
+
if (typeof value === "object") {
|
|
1881
|
+
return `o:${JSON.stringify(value)}`;
|
|
1882
|
+
}
|
|
1883
|
+
return `${(typeof value).charAt(0)}:${String(value)}`;
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
// src/query/Grouping.ts
|
|
1888
|
+
var Grouping = class _Grouping {
|
|
1889
|
+
/**
|
|
1890
|
+
* Fetch the records matching the query the grouping was opened from.
|
|
1891
|
+
*/
|
|
1892
|
+
#records;
|
|
1893
|
+
/**
|
|
1894
|
+
* The columns the records are grouped by.
|
|
1895
|
+
*/
|
|
1896
|
+
#columns;
|
|
1897
|
+
/**
|
|
1898
|
+
* The aggregations computed for each group.
|
|
1899
|
+
*/
|
|
1900
|
+
#aggregations = {};
|
|
1901
|
+
/**
|
|
1902
|
+
* The constraints the groups are filtered by.
|
|
1903
|
+
*/
|
|
1904
|
+
#constraints = [];
|
|
1905
|
+
/**
|
|
1906
|
+
* The orders the groups are sorted by.
|
|
1907
|
+
*/
|
|
1908
|
+
#orders = [];
|
|
1909
|
+
/**
|
|
1910
|
+
* The maximum number of groups returned.
|
|
1911
|
+
*/
|
|
1912
|
+
#limit = null;
|
|
1913
|
+
/**
|
|
1914
|
+
* The number of groups skipped.
|
|
1915
|
+
*/
|
|
1916
|
+
#offset = 0;
|
|
1917
|
+
/**
|
|
1918
|
+
* Create a new grouping.
|
|
1919
|
+
*/
|
|
1920
|
+
constructor(records, columns) {
|
|
1921
|
+
this.#records = records;
|
|
1922
|
+
this.#columns = columns;
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Compute the given aggregations for each group.
|
|
1926
|
+
*/
|
|
1927
|
+
aggregate(aggregations) {
|
|
1928
|
+
const grouping = new _Grouping(this.#records, this.#columns);
|
|
1929
|
+
grouping.#aggregations = aggregations;
|
|
1930
|
+
grouping.#constraints = this.#constraints;
|
|
1931
|
+
grouping.#orders = this.#orders;
|
|
1932
|
+
grouping.#limit = this.#limit;
|
|
1933
|
+
grouping.#offset = this.#offset;
|
|
1934
|
+
return grouping;
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Constrain the groups the query returns.
|
|
1938
|
+
*/
|
|
1939
|
+
having(column, operator, value) {
|
|
1940
|
+
return this.#constrain("and", column, operator, value);
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Add a disjunctive constraint on the groups the query returns.
|
|
1944
|
+
*/
|
|
1945
|
+
orHaving(column, operator, value) {
|
|
1946
|
+
return this.#constrain("or", column, operator, value);
|
|
1947
|
+
}
|
|
1948
|
+
/**
|
|
1949
|
+
* Sort the groups by a column or an aggregate.
|
|
1950
|
+
*/
|
|
1951
|
+
orderBy(column, direction = "asc") {
|
|
1952
|
+
this.#orders.push({ column, direction });
|
|
1953
|
+
return this;
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Limit the number of groups the query returns.
|
|
1957
|
+
*/
|
|
1958
|
+
limit(value) {
|
|
1959
|
+
this.#limit = value;
|
|
1960
|
+
return this;
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Skip the given number of groups.
|
|
1964
|
+
*/
|
|
1965
|
+
offset(value) {
|
|
1966
|
+
this.#offset = value;
|
|
1967
|
+
return this;
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Get every group matching the query.
|
|
1971
|
+
*/
|
|
1972
|
+
async get() {
|
|
1973
|
+
const grouped = this.#grouped(await this.#records());
|
|
1974
|
+
const rows = [];
|
|
1975
|
+
for (const members of grouped.values()) {
|
|
1976
|
+
rows.push(this.#row(members));
|
|
1977
|
+
}
|
|
1978
|
+
const matches = Predicate.compile(this.#constraints);
|
|
1979
|
+
const kept = rows.filter(matches);
|
|
1980
|
+
const sorted = this.#sorted(kept);
|
|
1981
|
+
const from = this.#offset;
|
|
1982
|
+
const paged = this.#limit === null ? sorted.slice(from) : sorted.slice(from, from + this.#limit);
|
|
1983
|
+
return paged;
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Get the first group matching the query.
|
|
1987
|
+
*/
|
|
1988
|
+
async first() {
|
|
1989
|
+
return (await this.get())[0] ?? null;
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Count the groups matching the query.
|
|
1993
|
+
*/
|
|
1994
|
+
async count() {
|
|
1995
|
+
return (await this.get()).length;
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Collect the records into groups, keyed by their grouped column values.
|
|
1999
|
+
*/
|
|
2000
|
+
#grouped(records) {
|
|
2001
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2002
|
+
for (const record of records) {
|
|
2003
|
+
const key = Signature.ofValues(this.#columns.map((column) => record[column]));
|
|
2004
|
+
const members = grouped.get(key);
|
|
2005
|
+
if (members === void 0) {
|
|
2006
|
+
grouped.set(key, [record]);
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
members.push(record);
|
|
2010
|
+
}
|
|
2011
|
+
return grouped;
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Build the row for a group, carrying its columns and its aggregates.
|
|
2015
|
+
*/
|
|
2016
|
+
#row(members) {
|
|
2017
|
+
const row = {};
|
|
2018
|
+
const first = members[0];
|
|
2019
|
+
for (const column of this.#columns) {
|
|
2020
|
+
row[column] = first[column];
|
|
2021
|
+
}
|
|
2022
|
+
for (const [alias, aggregation] of Object.entries(this.#aggregations)) {
|
|
2023
|
+
row[alias] = this.#aggregate(aggregation, members);
|
|
2024
|
+
}
|
|
2025
|
+
return row;
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Compute a single aggregate over the members of a group.
|
|
2029
|
+
*/
|
|
2030
|
+
#aggregate(aggregation, members) {
|
|
2031
|
+
if ("count" in aggregation) {
|
|
2032
|
+
return aggregation.count === "*" ? members.length : this.#values(aggregation.count, members).length;
|
|
2033
|
+
}
|
|
2034
|
+
const column = "sum" in aggregation ? aggregation.sum : "avg" in aggregation ? aggregation.avg : "min" in aggregation ? aggregation.min : aggregation.max;
|
|
2035
|
+
const values = this.#values(column, members);
|
|
2036
|
+
if ("sum" in aggregation) {
|
|
2037
|
+
return values.reduce((carry, value) => carry + value, 0);
|
|
2038
|
+
}
|
|
2039
|
+
if (values.length === 0) {
|
|
2040
|
+
return null;
|
|
2041
|
+
}
|
|
2042
|
+
if ("avg" in aggregation) {
|
|
2043
|
+
return values.reduce((carry, value) => carry + value, 0) / values.length;
|
|
2044
|
+
}
|
|
2045
|
+
return "min" in aggregation ? Math.min(...values) : Math.max(...values);
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Get the numeric values of a column across the members of a group.
|
|
2049
|
+
*/
|
|
2050
|
+
#values(column, members) {
|
|
2051
|
+
return members.map((member) => member[column]).filter((value) => value !== null && value !== void 0).map((value) => Number(value));
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Add a constraint on the groups the query returns.
|
|
2055
|
+
*/
|
|
2056
|
+
#constrain(conjunction, column, operator, value) {
|
|
2057
|
+
const resolved = value === void 0 ? { operator: "=", value: operator } : { operator, value };
|
|
2058
|
+
this.#constraints.push({ type: "basic", column, operator: resolved.operator, value: resolved.value, conjunction, not: false });
|
|
2059
|
+
return this;
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Sort the group rows by the requested orders.
|
|
2063
|
+
*/
|
|
2064
|
+
#sorted(rows) {
|
|
2065
|
+
return Comparator.sort(rows, this.#orders, (row, column) => row[column]);
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
|
|
2069
|
+
// src/query/Builder.ts
|
|
2070
|
+
var Builder = class _Builder {
|
|
2071
|
+
/**
|
|
2072
|
+
* The connection the query runs on.
|
|
2073
|
+
*/
|
|
2074
|
+
#connection;
|
|
2075
|
+
/**
|
|
2076
|
+
* The name of the table the query runs against.
|
|
2077
|
+
*/
|
|
2078
|
+
#table;
|
|
2079
|
+
/**
|
|
2080
|
+
* The transaction the query joins, when it runs inside one.
|
|
2081
|
+
*/
|
|
2082
|
+
#transaction;
|
|
2083
|
+
/**
|
|
2084
|
+
* The constraints the query filters by.
|
|
2085
|
+
*/
|
|
2086
|
+
#constraints = [];
|
|
2087
|
+
/**
|
|
2088
|
+
* The orders the query sorts by.
|
|
2089
|
+
*/
|
|
2090
|
+
#orders = [];
|
|
2091
|
+
/**
|
|
2092
|
+
* The maximum number of records the query returns.
|
|
2093
|
+
*/
|
|
2094
|
+
#limit = null;
|
|
2095
|
+
/**
|
|
2096
|
+
* The number of records the query skips.
|
|
2097
|
+
*/
|
|
2098
|
+
#offset = 0;
|
|
2099
|
+
/**
|
|
2100
|
+
* The columns the query projects, or null for every column.
|
|
2101
|
+
*/
|
|
2102
|
+
#columns = null;
|
|
2103
|
+
/**
|
|
2104
|
+
* Whether the query removes duplicate records.
|
|
2105
|
+
*/
|
|
2106
|
+
#distinct = false;
|
|
2107
|
+
/**
|
|
2108
|
+
* The tables joined onto this one.
|
|
2109
|
+
*/
|
|
2110
|
+
#joins = [];
|
|
2111
|
+
/**
|
|
2112
|
+
* Create a new query builder.
|
|
2113
|
+
*/
|
|
2114
|
+
constructor(connection, table, transaction = null) {
|
|
2115
|
+
this.#connection = connection;
|
|
2116
|
+
this.#table = table;
|
|
2117
|
+
this.#transaction = transaction;
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Get the name of the table the query runs against.
|
|
2121
|
+
*/
|
|
2122
|
+
get table() {
|
|
2123
|
+
return this.#table;
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Add a constraint to the query.
|
|
2127
|
+
*/
|
|
2128
|
+
where(column, operator, value) {
|
|
2129
|
+
return this.#constrain("and", false, column, operator, value);
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Add a disjunctive constraint to the query.
|
|
2133
|
+
*/
|
|
2134
|
+
orWhere(column, operator, value) {
|
|
2135
|
+
return this.#constrain("or", false, column, operator, value);
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Add a negated constraint to the query.
|
|
2139
|
+
*/
|
|
2140
|
+
whereNot(column, operator, value) {
|
|
2141
|
+
return this.#constrain("and", true, column, operator, value);
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Constrain a column to one of the given values.
|
|
2145
|
+
*/
|
|
2146
|
+
whereIn(column, values) {
|
|
2147
|
+
return this.#push({ type: "in", column, values, conjunction: "and", not: false });
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Constrain a column to one of the given values, disjunctively.
|
|
2151
|
+
*/
|
|
2152
|
+
orWhereIn(column, values) {
|
|
2153
|
+
return this.#push({ type: "in", column, values, conjunction: "or", not: false });
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Constrain a column to none of the given values.
|
|
2157
|
+
*/
|
|
2158
|
+
whereNotIn(column, values) {
|
|
2159
|
+
return this.#push({ type: "in", column, values, conjunction: "and", not: true });
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Constrain a column to none of the given values, disjunctively.
|
|
2163
|
+
*/
|
|
2164
|
+
orWhereNotIn(column, values) {
|
|
2165
|
+
return this.#push({ type: "in", column, values, conjunction: "or", not: true });
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Constrain a column to be null.
|
|
2169
|
+
*/
|
|
2170
|
+
whereNull(column) {
|
|
2171
|
+
return this.#push({ type: "null", column, conjunction: "and", not: false });
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Constrain a column to be null, disjunctively.
|
|
2175
|
+
*/
|
|
2176
|
+
orWhereNull(column) {
|
|
2177
|
+
return this.#push({ type: "null", column, conjunction: "or", not: false });
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Constrain a column to not be null.
|
|
2181
|
+
*/
|
|
2182
|
+
whereNotNull(column) {
|
|
2183
|
+
return this.#push({ type: "null", column, conjunction: "and", not: true });
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Constrain a column to not be null, disjunctively.
|
|
2187
|
+
*/
|
|
2188
|
+
orWhereNotNull(column) {
|
|
2189
|
+
return this.#push({ type: "null", column, conjunction: "or", not: true });
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Constrain a column to fall between two values, inclusive.
|
|
2193
|
+
*/
|
|
2194
|
+
whereBetween(column, values) {
|
|
2195
|
+
return this.#push({ type: "between", column, from: values[0], to: values[1], conjunction: "and", not: false });
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Constrain a column to fall between two values, disjunctively.
|
|
2199
|
+
*/
|
|
2200
|
+
orWhereBetween(column, values) {
|
|
2201
|
+
return this.#push({ type: "between", column, from: values[0], to: values[1], conjunction: "or", not: false });
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* Constrain a column to fall outside two values.
|
|
2205
|
+
*/
|
|
2206
|
+
whereNotBetween(column, values) {
|
|
2207
|
+
return this.#push({ type: "between", column, from: values[0], to: values[1], conjunction: "and", not: true });
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Constrain a column to fall outside two values, disjunctively.
|
|
2211
|
+
*/
|
|
2212
|
+
orWhereNotBetween(column, values) {
|
|
2213
|
+
return this.#push({ type: "between", column, from: values[0], to: values[1], conjunction: "or", not: true });
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Constrain a column to match a pattern.
|
|
2217
|
+
*/
|
|
2218
|
+
whereLike(column, pattern) {
|
|
2219
|
+
return this.#push({ type: "basic", column, operator: "like", value: pattern, conjunction: "and", not: false });
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Constrain a column to match a pattern, disjunctively.
|
|
2223
|
+
*/
|
|
2224
|
+
orWhereLike(column, pattern) {
|
|
2225
|
+
return this.#push({ type: "basic", column, operator: "like", value: pattern, conjunction: "or", not: false });
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Constrain a column to not match a pattern.
|
|
2229
|
+
*/
|
|
2230
|
+
whereNotLike(column, pattern) {
|
|
2231
|
+
return this.#push({ type: "basic", column, operator: "not like", value: pattern, conjunction: "and", not: false });
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Constrain a column to not match a pattern, disjunctively.
|
|
2235
|
+
*/
|
|
2236
|
+
orWhereNotLike(column, pattern) {
|
|
2237
|
+
return this.#push({ type: "basic", column, operator: "not like", value: pattern, conjunction: "or", not: false });
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* Constrain a date column to fall on a given day.
|
|
2241
|
+
*/
|
|
2242
|
+
whereDate(column, value) {
|
|
2243
|
+
const day = new Date(value);
|
|
2244
|
+
const from = new Date(day.getFullYear(), day.getMonth(), day.getDate());
|
|
2245
|
+
const to = new Date(from.getFullYear(), from.getMonth(), from.getDate() + 1);
|
|
2246
|
+
return this.#push({ type: "between", column, from, to: new Date(to.getTime() - 1), conjunction: "and", not: false });
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Constrain a date column to fall in a given year.
|
|
2250
|
+
*/
|
|
2251
|
+
whereYear(column, value) {
|
|
2252
|
+
return this.#part("and", column, "year", value);
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Constrain a date column to fall in a given month, numbered from one.
|
|
2256
|
+
*/
|
|
2257
|
+
whereMonth(column, value) {
|
|
2258
|
+
return this.#part("and", column, "month", value);
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Constrain a date column to fall on a given day of the month.
|
|
2262
|
+
*/
|
|
2263
|
+
whereDay(column, value) {
|
|
2264
|
+
return this.#part("and", column, "day", value);
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Join another table, keeping only the rows that match.
|
|
2268
|
+
*/
|
|
2269
|
+
join(table, first, operator, second) {
|
|
2270
|
+
return this.#join("inner", table, first, operator, second);
|
|
2271
|
+
}
|
|
2272
|
+
/**
|
|
2273
|
+
* Join another table, keeping every row of this one.
|
|
2274
|
+
*/
|
|
2275
|
+
leftJoin(table, first, operator, second) {
|
|
2276
|
+
return this.#join("left", table, first, operator, second);
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Join another table, keeping every row of it.
|
|
2280
|
+
*/
|
|
2281
|
+
rightJoin(table, first, operator, second) {
|
|
2282
|
+
return this.#join("right", table, first, operator, second);
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Pair every row of this table with every row of another.
|
|
2286
|
+
*/
|
|
2287
|
+
crossJoin(table) {
|
|
2288
|
+
this.#joins.push({ table, type: "cross", conditions: [] });
|
|
2289
|
+
return this;
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Constrain a column against another column of the same row.
|
|
2293
|
+
*/
|
|
2294
|
+
whereColumn(column, operator, other) {
|
|
2295
|
+
return this.#compared("and", column, operator, other);
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Constrain a column against another column of the same row, disjunctively.
|
|
2299
|
+
*/
|
|
2300
|
+
orWhereColumn(column, operator, other) {
|
|
2301
|
+
return this.#compared("or", column, operator, other);
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Project only the given columns, which may alias what they select.
|
|
2305
|
+
*/
|
|
2306
|
+
select(...columns) {
|
|
2307
|
+
this.#columns = columns.flat();
|
|
2308
|
+
return this;
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Remove duplicate records from the result.
|
|
2312
|
+
*/
|
|
2313
|
+
distinct(value = true) {
|
|
2314
|
+
this.#distinct = value;
|
|
2315
|
+
return this;
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* Group the matching records by one or more columns.
|
|
2319
|
+
*/
|
|
2320
|
+
groupBy(...columns) {
|
|
2321
|
+
const records = this.clone();
|
|
2322
|
+
records.#orders = [];
|
|
2323
|
+
records.#limit = null;
|
|
2324
|
+
records.#offset = 0;
|
|
2325
|
+
return new Grouping(
|
|
2326
|
+
async () => await records.#records(),
|
|
2327
|
+
columns
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Sort the result by a column.
|
|
2332
|
+
*/
|
|
2333
|
+
orderBy(column, direction = "asc") {
|
|
2334
|
+
this.#orders.push({ column, direction });
|
|
2335
|
+
return this;
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Sort the result by a column, newest first.
|
|
2339
|
+
*/
|
|
2340
|
+
latest(column = "created_at") {
|
|
2341
|
+
return this.orderBy(column, "desc");
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Sort the result by a column, oldest first.
|
|
2345
|
+
*/
|
|
2346
|
+
oldest(column = "created_at") {
|
|
2347
|
+
return this.orderBy(column, "asc");
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Limit the number of records the query returns.
|
|
2351
|
+
*/
|
|
2352
|
+
limit(value) {
|
|
2353
|
+
this.#limit = value;
|
|
2354
|
+
return this;
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Limit the number of records the query returns.
|
|
2358
|
+
*/
|
|
2359
|
+
take(value) {
|
|
2360
|
+
return this.limit(value);
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Skip the given number of records.
|
|
2364
|
+
*/
|
|
2365
|
+
offset(value) {
|
|
2366
|
+
this.#offset = value;
|
|
2367
|
+
return this;
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Skip the given number of records.
|
|
2371
|
+
*/
|
|
2372
|
+
skip(value) {
|
|
2373
|
+
return this.offset(value);
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Limit the query to a single page of records.
|
|
2377
|
+
*/
|
|
2378
|
+
forPage(page, perPage = 15) {
|
|
2379
|
+
return this.offset((page - 1) * perPage).limit(perPage);
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Apply the callback when the value is truthy.
|
|
2383
|
+
*/
|
|
2384
|
+
when(value, callback) {
|
|
2385
|
+
if (value) {
|
|
2386
|
+
callback(this, value);
|
|
2387
|
+
}
|
|
2388
|
+
return this;
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Apply the callback when the value is falsy.
|
|
2392
|
+
*/
|
|
2393
|
+
unless(value, callback) {
|
|
2394
|
+
return this.when(!value, (query) => callback(query, value));
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Pass the query to the callback and carry on.
|
|
2398
|
+
*/
|
|
2399
|
+
tap(callback) {
|
|
2400
|
+
callback(this);
|
|
2401
|
+
return this;
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Get a copy of the query.
|
|
2405
|
+
*/
|
|
2406
|
+
clone() {
|
|
2407
|
+
const clone = new _Builder(this.#connection, this.#table, this.#transaction);
|
|
2408
|
+
clone.#constraints = [...this.#constraints];
|
|
2409
|
+
clone.#orders = [...this.#orders];
|
|
2410
|
+
clone.#limit = this.#limit;
|
|
2411
|
+
clone.#offset = this.#offset;
|
|
2412
|
+
clone.#columns = this.#columns === null ? null : [...this.#columns];
|
|
2413
|
+
clone.#distinct = this.#distinct;
|
|
2414
|
+
clone.#joins = [...this.#joins];
|
|
2415
|
+
return clone;
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* Dump the state of the query.
|
|
2419
|
+
*/
|
|
2420
|
+
dump() {
|
|
2421
|
+
console.log({
|
|
2422
|
+
table: this.#table,
|
|
2423
|
+
constraints: this.#constraints,
|
|
2424
|
+
orders: this.#orders,
|
|
2425
|
+
limit: this.#limit,
|
|
2426
|
+
offset: this.#offset,
|
|
2427
|
+
columns: this.#columns,
|
|
2428
|
+
distinct: this.#distinct
|
|
2429
|
+
});
|
|
2430
|
+
return this;
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Describe the plan the query would run under.
|
|
2434
|
+
*/
|
|
2435
|
+
async explain() {
|
|
2436
|
+
const schema = await this.#connection.schema(this.#table);
|
|
2437
|
+
return Planner.describe(Planner.plan(this.#constraints, this.#orders, schema));
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Get every record matching the query.
|
|
2441
|
+
*/
|
|
2442
|
+
async get() {
|
|
2443
|
+
return this.#shape(await this.#records());
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Get the first record matching the query.
|
|
2447
|
+
*/
|
|
2448
|
+
async first() {
|
|
2449
|
+
const records = await this.clone().limit(1).get();
|
|
2450
|
+
return records[0] ?? null;
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2453
|
+
* Get the first record matching the query, or fail.
|
|
2454
|
+
*/
|
|
2455
|
+
async firstOrFail() {
|
|
2456
|
+
const record = await this.first();
|
|
2457
|
+
if (record === null) {
|
|
2458
|
+
throw new RecordsNotFoundException(`No records found in table [${this.#table}].`);
|
|
2459
|
+
}
|
|
2460
|
+
return record;
|
|
2461
|
+
}
|
|
2462
|
+
/**
|
|
2463
|
+
* Get the one record matching the query, failing when there is not exactly one.
|
|
2464
|
+
*/
|
|
2465
|
+
async sole() {
|
|
2466
|
+
const records = await this.clone().limit(2).get();
|
|
2467
|
+
if (records.length === 0) {
|
|
2468
|
+
throw new RecordsNotFoundException(`No records found in table [${this.#table}].`);
|
|
2469
|
+
}
|
|
2470
|
+
if (records.length > 1) {
|
|
2471
|
+
throw new MultipleRecordsFoundException(this.#table);
|
|
2472
|
+
}
|
|
2473
|
+
return records[0];
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* Get the record with the given key.
|
|
2477
|
+
*/
|
|
2478
|
+
async find(key) {
|
|
2479
|
+
const store = await this.#store("readonly");
|
|
2480
|
+
const started = performance.now();
|
|
2481
|
+
const record = await Request.settle(store.get(key));
|
|
2482
|
+
this.#emit("key", started, record === void 0 ? 0 : 1);
|
|
2483
|
+
return record ?? null;
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Get the record with the given key, or fail.
|
|
2487
|
+
*/
|
|
2488
|
+
async findOrFail(key) {
|
|
2489
|
+
const record = await this.find(key);
|
|
2490
|
+
if (record === null) {
|
|
2491
|
+
throw new RecordsNotFoundException(`No record with key [${String(key)}] in table [${this.#table}].`);
|
|
2492
|
+
}
|
|
2493
|
+
return record;
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Get a single column from the first record matching the query.
|
|
2497
|
+
*/
|
|
2498
|
+
async value(column) {
|
|
2499
|
+
const record = await this.clone().first();
|
|
2500
|
+
if (record === null) {
|
|
2501
|
+
return null;
|
|
2502
|
+
}
|
|
2503
|
+
return record[column] ?? null;
|
|
2504
|
+
}
|
|
2505
|
+
async pluck(column, key) {
|
|
2506
|
+
const records = await this.#records();
|
|
2507
|
+
if (key === void 0) {
|
|
2508
|
+
return records.map((record) => record[column]);
|
|
2509
|
+
}
|
|
2510
|
+
return Object.fromEntries(records.map((record) => [String(record[key]), record[column]]));
|
|
2511
|
+
}
|
|
2512
|
+
/**
|
|
2513
|
+
* Determine whether any record matches the query.
|
|
2514
|
+
*/
|
|
2515
|
+
async exists() {
|
|
2516
|
+
return (await this.clone().limit(1).#records()).length > 0;
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Determine whether no record matches the query.
|
|
2520
|
+
*/
|
|
2521
|
+
async doesntExist() {
|
|
2522
|
+
return !await this.exists();
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Count the records matching the query.
|
|
2526
|
+
*/
|
|
2527
|
+
async count() {
|
|
2528
|
+
const schema = await this.#connection.schema(this.#table);
|
|
2529
|
+
const plan = Planner.plan(this.#constraints, this.#orders, schema);
|
|
2530
|
+
if (plan.residual.length > 0 || plan.values !== null) {
|
|
2531
|
+
return (await this.#records()).length;
|
|
2532
|
+
}
|
|
2533
|
+
const store = await this.#store("readonly");
|
|
2534
|
+
const started = performance.now();
|
|
2535
|
+
const source = plan.index === null ? store : store.index(plan.index);
|
|
2536
|
+
const count = await Request.settle(source.count(plan.range ?? void 0));
|
|
2537
|
+
this.#emit(Planner.describe(plan), started, count);
|
|
2538
|
+
return count;
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Sum a column across the records matching the query.
|
|
2542
|
+
*/
|
|
2543
|
+
async sum(column) {
|
|
2544
|
+
return (await this.#numbers(column)).reduce((carry, value) => carry + value, 0);
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Average a column across the records matching the query.
|
|
2548
|
+
*/
|
|
2549
|
+
async avg(column) {
|
|
2550
|
+
const values = await this.#numbers(column);
|
|
2551
|
+
if (values.length === 0) {
|
|
2552
|
+
return null;
|
|
2553
|
+
}
|
|
2554
|
+
return values.reduce((carry, value) => carry + value, 0) / values.length;
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* Get the smallest value of a column across the records matching the query.
|
|
2558
|
+
*/
|
|
2559
|
+
async min(column) {
|
|
2560
|
+
return this.#extreme(column, "next");
|
|
2561
|
+
}
|
|
2562
|
+
/**
|
|
2563
|
+
* Get the largest value of a column across the records matching the query.
|
|
2564
|
+
*/
|
|
2565
|
+
async max(column) {
|
|
2566
|
+
return this.#extreme(column, "prev");
|
|
2567
|
+
}
|
|
2568
|
+
/**
|
|
2569
|
+
* Get the value at one end of a column's range.
|
|
2570
|
+
*/
|
|
2571
|
+
async #extreme(column, direction) {
|
|
2572
|
+
const index = await this.#sole(column);
|
|
2573
|
+
if (index !== null) {
|
|
2574
|
+
const store = await this.#store("readonly");
|
|
2575
|
+
const started = performance.now();
|
|
2576
|
+
const cursor = await Request.settle(store.index(index.name).openCursor(null, direction));
|
|
2577
|
+
this.#emit(`index:${index.name}`, started, cursor === null ? 0 : 1);
|
|
2578
|
+
return cursor === null ? null : Number(cursor.key);
|
|
2579
|
+
}
|
|
2580
|
+
const values = await this.#numbers(column);
|
|
2581
|
+
if (values.length === 0) {
|
|
2582
|
+
return null;
|
|
2583
|
+
}
|
|
2584
|
+
return values.reduce((carry, value) => direction === "next" ? Math.min(carry, value) : Math.max(carry, value));
|
|
2585
|
+
}
|
|
2586
|
+
/**
|
|
2587
|
+
* Get the single column index that can answer an unconstrained extreme, if there is one.
|
|
2588
|
+
*/
|
|
2589
|
+
async #sole(column) {
|
|
2590
|
+
if (this.#joins.length > 0 || this.#constraints.length > 0) {
|
|
2591
|
+
return null;
|
|
2592
|
+
}
|
|
2593
|
+
const schema = await this.#connection.schema(this.#table);
|
|
2594
|
+
return schema.indexes.find((index) => index.columns.length === 1 && index.columns[0] === column && !index.multiEntry) ?? null;
|
|
2595
|
+
}
|
|
2596
|
+
/**
|
|
2597
|
+
* Get a single page of records, alongside the totals a pager needs.
|
|
2598
|
+
*/
|
|
2599
|
+
async paginate(page = 1, perPage = 15) {
|
|
2600
|
+
const counted = this.clone();
|
|
2601
|
+
counted.#limit = null;
|
|
2602
|
+
counted.#offset = 0;
|
|
2603
|
+
const total = await counted.count();
|
|
2604
|
+
const data = await this.clone().forPage(page, perPage).get();
|
|
2605
|
+
return {
|
|
2606
|
+
data,
|
|
2607
|
+
total,
|
|
2608
|
+
perPage,
|
|
2609
|
+
currentPage: page,
|
|
2610
|
+
lastPage: Math.max(1, Math.ceil(total / perPage))
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
/**
|
|
2614
|
+
* Walk the records matching the query in chunks.
|
|
2615
|
+
*/
|
|
2616
|
+
async chunk(size, callback) {
|
|
2617
|
+
if (this.#joins.length > 0) {
|
|
2618
|
+
const rows = await this.#records();
|
|
2619
|
+
for (let index = 0; index < rows.length; index += size) {
|
|
2620
|
+
if (await callback(rows.slice(index, index + size), Math.floor(index / size) + 1) === false) {
|
|
2621
|
+
return false;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
return true;
|
|
2625
|
+
}
|
|
2626
|
+
const keys = await this.#keys();
|
|
2627
|
+
for (let index = 0; index < keys.length; index += size) {
|
|
2628
|
+
const page = keys.slice(index, index + size);
|
|
2629
|
+
const store = await this.#store("readonly");
|
|
2630
|
+
const records = await Promise.all(
|
|
2631
|
+
page.map((key) => Request.settle(store.get(key)))
|
|
2632
|
+
);
|
|
2633
|
+
const present = records.filter((record) => record !== void 0);
|
|
2634
|
+
if (await callback(this.#shape(present), Math.floor(index / size) + 1) === false) {
|
|
2635
|
+
return false;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
return true;
|
|
2639
|
+
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Walk the records matching the query as an async iterable.
|
|
2642
|
+
*/
|
|
2643
|
+
async *lazy(size = 100) {
|
|
2644
|
+
if (this.#joins.length > 0) {
|
|
2645
|
+
yield* await this.#records();
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
const keys = await this.#keys();
|
|
2649
|
+
for (let index = 0; index < keys.length; index += size) {
|
|
2650
|
+
const page = keys.slice(index, index + size);
|
|
2651
|
+
const store = await this.#store("readonly");
|
|
2652
|
+
const records = await Promise.all(
|
|
2653
|
+
page.map((key) => Request.settle(store.get(key)))
|
|
2654
|
+
);
|
|
2655
|
+
yield* this.#shape(records.filter((record) => record !== void 0));
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Walk the records matching the query one at a time.
|
|
2660
|
+
*/
|
|
2661
|
+
async each(callback) {
|
|
2662
|
+
let index = 0;
|
|
2663
|
+
return this.chunk(1, async (records) => callback(records[0], index++));
|
|
2664
|
+
}
|
|
2665
|
+
/**
|
|
2666
|
+
* Insert one or more records into the table.
|
|
2667
|
+
*/
|
|
2668
|
+
async insert(records) {
|
|
2669
|
+
const rows = Array.isArray(records) ? records : [records];
|
|
2670
|
+
const schema = await this.#connection.schema(this.#table);
|
|
2671
|
+
const store = await this.#store("readwrite");
|
|
2672
|
+
const started = performance.now();
|
|
2673
|
+
for (const row of rows) {
|
|
2674
|
+
await this.#add(store, schema, row);
|
|
2675
|
+
}
|
|
2676
|
+
this.#emit("insert", started, rows.length);
|
|
2677
|
+
return rows.length;
|
|
2678
|
+
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Insert one or more records, skipping any the unique indexes reject.
|
|
2681
|
+
*/
|
|
2682
|
+
async insertOrIgnore(records) {
|
|
2683
|
+
const rows = Array.isArray(records) ? records : [records];
|
|
2684
|
+
const schema = await this.#connection.schema(this.#table);
|
|
2685
|
+
const store = await this.#store("readwrite");
|
|
2686
|
+
const started = performance.now();
|
|
2687
|
+
let inserted = 0;
|
|
2688
|
+
for (const row of rows) {
|
|
2689
|
+
try {
|
|
2690
|
+
await this.#add(store, schema, row);
|
|
2691
|
+
inserted++;
|
|
2692
|
+
} catch (error) {
|
|
2693
|
+
if (!(error instanceof UniqueConstraintViolationException)) {
|
|
2694
|
+
throw error;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
this.#emit("insert", started, inserted);
|
|
2699
|
+
return inserted;
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Insert a record and get the key the database gave it.
|
|
2703
|
+
*/
|
|
2704
|
+
async insertGetId(record) {
|
|
2705
|
+
const schema = await this.#connection.schema(this.#table);
|
|
2706
|
+
const store = await this.#store("readwrite");
|
|
2707
|
+
const started = performance.now();
|
|
2708
|
+
const key = await this.#add(store, schema, record);
|
|
2709
|
+
this.#emit("insert", started, 1);
|
|
2710
|
+
return key;
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Update every record matching the query.
|
|
2714
|
+
*/
|
|
2715
|
+
async update(values) {
|
|
2716
|
+
const schema = await this.#connection.schema(this.#table);
|
|
2717
|
+
const prepared = Coercer.updatable(values, schema, this.#connection.strict, /* @__PURE__ */ new Date());
|
|
2718
|
+
this.#settled(schema, prepared);
|
|
2719
|
+
return this.#modify((cursor) => {
|
|
2720
|
+
cursor.update({ ...cursor.value, ...prepared });
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
/**
|
|
2724
|
+
* Update the matching record, inserting it when there is none.
|
|
2725
|
+
*/
|
|
2726
|
+
async updateOrInsert(attributes, values = {}) {
|
|
2727
|
+
const query = this.clone().where(attributes);
|
|
2728
|
+
if (await query.exists()) {
|
|
2729
|
+
await query.update(values);
|
|
2730
|
+
return false;
|
|
2731
|
+
}
|
|
2732
|
+
await this.clone().insert({ ...attributes, ...values });
|
|
2733
|
+
return true;
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Insert records, updating those that already exist.
|
|
2737
|
+
*/
|
|
2738
|
+
async upsert(values, uniqueBy, update) {
|
|
2739
|
+
const schema = await this.#connection.schema(this.#table);
|
|
2740
|
+
const columns = Array.isArray(uniqueBy) ? uniqueBy : [uniqueBy];
|
|
2741
|
+
const target = this.#conflict(schema, columns);
|
|
2742
|
+
const store = await this.#store("readwrite");
|
|
2743
|
+
const started = performance.now();
|
|
2744
|
+
for (const value of values) {
|
|
2745
|
+
await this.#merge(store, schema, columns, target, value, update);
|
|
2746
|
+
}
|
|
2747
|
+
this.#emit("upsert", started, values.length);
|
|
2748
|
+
return values.length;
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* Add the given amount to a column of every record matching the query.
|
|
2752
|
+
*/
|
|
2753
|
+
async increment(column, amount = 1, extra = {}) {
|
|
2754
|
+
return this.#step(column, amount, extra);
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Subtract the given amount from a column of every record matching the query.
|
|
2758
|
+
*/
|
|
2759
|
+
async decrement(column, amount = 1, extra = {}) {
|
|
2760
|
+
return this.#step(column, -amount, extra);
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* Delete every record matching the query.
|
|
2764
|
+
*/
|
|
2765
|
+
async delete() {
|
|
2766
|
+
return this.#modify((cursor) => {
|
|
2767
|
+
cursor.delete();
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Delete every record in the table.
|
|
2772
|
+
*/
|
|
2773
|
+
async truncate() {
|
|
2774
|
+
const store = await this.#store("readwrite");
|
|
2775
|
+
const started = performance.now();
|
|
2776
|
+
await Request.settle(store.clear());
|
|
2777
|
+
this.#emit("truncate", started, 0);
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Add a record to the store, reporting a violated constraint by its index.
|
|
2781
|
+
*/
|
|
2782
|
+
async #add(store, schema, record) {
|
|
2783
|
+
const prepared = Coercer.insertable(record, schema, this.#connection.strict, /* @__PURE__ */ new Date());
|
|
2784
|
+
try {
|
|
2785
|
+
return await Request.settle(store.add(prepared), true);
|
|
2786
|
+
} catch (error) {
|
|
2787
|
+
if (error instanceof DOMException && error.name === "ConstraintError") {
|
|
2788
|
+
const index = await this.#violated(store, schema, prepared);
|
|
2789
|
+
if (index !== null) {
|
|
2790
|
+
throw new UniqueConstraintViolationException(this.#table, index);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
throw error;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Find the unique index the record collides with, or null when it cannot be attributed.
|
|
2798
|
+
*/
|
|
2799
|
+
async #violated(store, schema, record) {
|
|
2800
|
+
if (schema.key !== null && this.#keyable(record[schema.key])) {
|
|
2801
|
+
if (await Request.settle(store.count(IDBKeyRange.only(record[schema.key]))) > 0) {
|
|
2802
|
+
return schema.key;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
for (const index of schema.indexes.filter((candidate) => candidate.unique)) {
|
|
2806
|
+
if (!index.columns.every((column) => this.#keyable(record[column]))) {
|
|
2807
|
+
continue;
|
|
2808
|
+
}
|
|
2809
|
+
const key = this.#keyOf(index.columns, record);
|
|
2810
|
+
if (await Request.settle(store.index(index.name).count(IDBKeyRange.only(key))) > 0) {
|
|
2811
|
+
return index.name;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
return null;
|
|
2815
|
+
}
|
|
2816
|
+
/**
|
|
2817
|
+
* Determine whether the value may be used as an IndexedDB key.
|
|
2818
|
+
*/
|
|
2819
|
+
#keyable(value) {
|
|
2820
|
+
return value !== null && value !== void 0;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Build the index key the given columns of a record form.
|
|
2824
|
+
*/
|
|
2825
|
+
#keyOf(columns, record) {
|
|
2826
|
+
if (columns.length === 1) {
|
|
2827
|
+
return record[columns[0]];
|
|
2828
|
+
}
|
|
2829
|
+
return columns.map((column) => record[column]);
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Resolve the conflict target of an upsert, or fail when it cannot be enforced.
|
|
2833
|
+
*/
|
|
2834
|
+
#conflict(schema, columns) {
|
|
2835
|
+
if (columns.length === 1 && columns[0] === schema.key) {
|
|
2836
|
+
return null;
|
|
2837
|
+
}
|
|
2838
|
+
const index = schema.indexes.find((candidate) => candidate.unique && candidate.columns.length === columns.length && candidate.columns.every((column, position) => column === columns[position]));
|
|
2839
|
+
if (index === void 0) {
|
|
2840
|
+
throw new SchemaException(`Upsert on table [${this.#table}] requires [${columns.join(", ")}] to be the key path or a unique index.`);
|
|
2841
|
+
}
|
|
2842
|
+
return index;
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Insert a record, or merge it into the one already holding its conflict key.
|
|
2846
|
+
*/
|
|
2847
|
+
async #merge(store, schema, columns, target, value, update) {
|
|
2848
|
+
if (target === null) {
|
|
2849
|
+
await Request.settle(store.put(Coercer.insertable(value, schema, this.#connection.strict, /* @__PURE__ */ new Date())));
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
const key = this.#keyOf(columns, value);
|
|
2853
|
+
const existing = await Request.settle(store.index(target.name).get(IDBKeyRange.only(key)));
|
|
2854
|
+
if (existing === void 0) {
|
|
2855
|
+
await this.#add(store, schema, value);
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
const changes = update === void 0 ? value : Object.fromEntries(update.map((column) => [column, value[column]]));
|
|
2859
|
+
const prepared = Coercer.updatable(changes, schema, this.#connection.strict, /* @__PURE__ */ new Date());
|
|
2860
|
+
this.#settled(schema, prepared);
|
|
2861
|
+
await Request.settle(store.put({ ...existing, ...prepared }));
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Add the given amount to a column of every record matching the query.
|
|
2865
|
+
*/
|
|
2866
|
+
async #step(column, amount, extra) {
|
|
2867
|
+
const schema = await this.#connection.schema(this.#table);
|
|
2868
|
+
const prepared = Coercer.updatable(extra, schema, this.#connection.strict, /* @__PURE__ */ new Date());
|
|
2869
|
+
this.#settled(schema, prepared);
|
|
2870
|
+
return this.#modify((cursor) => {
|
|
2871
|
+
const record = { ...cursor.value };
|
|
2872
|
+
const current = Number(record[column] ?? 0);
|
|
2873
|
+
cursor.update({ ...record, ...prepared, [column]: current + amount });
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* Assert the changes leave the key path of the record alone.
|
|
2878
|
+
*/
|
|
2879
|
+
#settled(schema, changes) {
|
|
2880
|
+
if (schema.key !== null && Object.hasOwn(changes, schema.key)) {
|
|
2881
|
+
throw new SchemaException(`Column [${schema.key}] is the key path of table [${this.#table}] and may not be updated.`);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
/**
|
|
2885
|
+
* Apply a change to every record matching the query, in the order the plan scans them.
|
|
2886
|
+
*/
|
|
2887
|
+
async #modify(apply) {
|
|
2888
|
+
const schema = await this.#connection.schema(this.#table);
|
|
2889
|
+
const plan = Planner.plan(this.#constraints, this.#orders, schema);
|
|
2890
|
+
const store = await this.#store("readwrite");
|
|
2891
|
+
const started = performance.now();
|
|
2892
|
+
const matches = Predicate.compile(plan.residual);
|
|
2893
|
+
const ceiling = this.#limit === null ? null : this.#offset + this.#limit;
|
|
2894
|
+
let seen = 0;
|
|
2895
|
+
let affected = 0;
|
|
2896
|
+
const visit = (cursor) => {
|
|
2897
|
+
if (!matches(cursor.value)) {
|
|
2898
|
+
return true;
|
|
2899
|
+
}
|
|
2900
|
+
seen++;
|
|
2901
|
+
if (seen > this.#offset) {
|
|
2902
|
+
apply(cursor);
|
|
2903
|
+
affected++;
|
|
2904
|
+
}
|
|
2905
|
+
return ceiling === null || seen < ceiling;
|
|
2906
|
+
};
|
|
2907
|
+
if (plan.values === null) {
|
|
2908
|
+
const source = plan.index === null ? store : store.index(plan.index);
|
|
2909
|
+
await Request.walk(source.openCursor(plan.range, plan.direction), visit);
|
|
2910
|
+
} else {
|
|
2911
|
+
for (const value of plan.values) {
|
|
2912
|
+
const source = plan.index === null ? store : store.index(plan.index);
|
|
2913
|
+
await Request.walk(source.openCursor(IDBKeyRange.only(value)), visit);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
this.#emit(Planner.describe(plan), started, affected);
|
|
2917
|
+
return affected;
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Add a constraint of the given shape to the query.
|
|
2921
|
+
*/
|
|
2922
|
+
#constrain(conjunction, not, column, operator, value) {
|
|
2923
|
+
if (typeof column === "function") {
|
|
2924
|
+
const nested = new _Builder(this.#connection, this.#table, this.#transaction);
|
|
2925
|
+
column(nested);
|
|
2926
|
+
return this.#push({ type: "nested", constraints: nested.#constraints, conjunction, not });
|
|
2927
|
+
}
|
|
2928
|
+
if (typeof column === "object" && column !== null) {
|
|
2929
|
+
const constraints = Object.entries(column).map(([key, held]) => ({
|
|
2930
|
+
type: "basic",
|
|
2931
|
+
column: key,
|
|
2932
|
+
operator: "=",
|
|
2933
|
+
value: held,
|
|
2934
|
+
conjunction: "and",
|
|
2935
|
+
not: false
|
|
2936
|
+
}));
|
|
2937
|
+
return this.#push({ type: "nested", constraints, conjunction, not });
|
|
2938
|
+
}
|
|
2939
|
+
const resolved = value === void 0 ? { operator: "=", value: operator } : { operator, value };
|
|
2940
|
+
return this.#push({ type: "basic", column, operator: resolved.operator, value: resolved.value, conjunction, not });
|
|
2941
|
+
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Add a constraint on one part of a date column.
|
|
2944
|
+
*/
|
|
2945
|
+
#part(conjunction, column, part, value) {
|
|
2946
|
+
return this.#push({ type: "part", column, part, value, conjunction, not: false });
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Add a constraint comparing two columns, allowing the operator to be left implicit.
|
|
2950
|
+
*/
|
|
2951
|
+
#compared(conjunction, column, operator, other) {
|
|
2952
|
+
const resolved = other === void 0 ? { operator: "=", other: operator } : { operator, other };
|
|
2953
|
+
return this.#push({ type: "column", column, operator: resolved.operator, other: resolved.other, conjunction, not: false });
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Append a constraint to the query.
|
|
2957
|
+
*/
|
|
2958
|
+
#push(constraint) {
|
|
2959
|
+
this.#constraints.push(constraint);
|
|
2960
|
+
return this;
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* Get the object store the query reads from.
|
|
2964
|
+
*/
|
|
2965
|
+
async #store(mode, table = this.#table) {
|
|
2966
|
+
if (this.#transaction !== null) {
|
|
2967
|
+
return this.#transaction.objectStore(table);
|
|
2968
|
+
}
|
|
2969
|
+
const database = await this.#connection.open();
|
|
2970
|
+
await this.#connection.schema(table);
|
|
2971
|
+
return database.transaction(table, mode).objectStore(table);
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Get the numeric values of a column across the records matching the query.
|
|
2975
|
+
*/
|
|
2976
|
+
async #numbers(column) {
|
|
2977
|
+
const records = await this.#records();
|
|
2978
|
+
return records.map((record) => record[column]).filter((value) => value !== null && value !== void 0).map((value) => Number(value));
|
|
2979
|
+
}
|
|
2980
|
+
/**
|
|
2981
|
+
* Get the records matching the query, unshaped.
|
|
2982
|
+
*/
|
|
2983
|
+
async #records() {
|
|
2984
|
+
return (await this.#matched()).records;
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2987
|
+
* Get the keys of the records matching the query.
|
|
2988
|
+
*/
|
|
2989
|
+
async #keys() {
|
|
2990
|
+
return (await this.#matched()).keys;
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Record a join, accepting either the column shorthand or a closure of conditions.
|
|
2994
|
+
*/
|
|
2995
|
+
#join(type, table, first, operator, second) {
|
|
2996
|
+
const clause = new Join();
|
|
2997
|
+
if (typeof first === "function") {
|
|
2998
|
+
first(clause);
|
|
2999
|
+
} else {
|
|
3000
|
+
clause.on(first, operator, second);
|
|
3001
|
+
}
|
|
3002
|
+
this.#joins.push({ table, type, conditions: clause.conditions() });
|
|
3003
|
+
return this;
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* Get the columns of every table the query reads, keyed by table.
|
|
3007
|
+
*/
|
|
3008
|
+
async #tables() {
|
|
3009
|
+
const tables = /* @__PURE__ */ new Map();
|
|
3010
|
+
const names = [this.#table, ...this.#joins.map((clause) => clause.table)];
|
|
3011
|
+
for (const name of names) {
|
|
3012
|
+
const schema = await this.#connection.schema(name);
|
|
3013
|
+
tables.set(name, schema.columns.map((column) => column.name));
|
|
3014
|
+
}
|
|
3015
|
+
return tables;
|
|
3016
|
+
}
|
|
3017
|
+
/**
|
|
3018
|
+
* Run the joins, returning rows whose keys are all qualified by table.
|
|
3019
|
+
*/
|
|
3020
|
+
async #joined() {
|
|
3021
|
+
const tables = await this.#tables();
|
|
3022
|
+
const store = await this.#store("readonly");
|
|
3023
|
+
const started = performance.now();
|
|
3024
|
+
let rows = Joiner.qualify(
|
|
3025
|
+
await Request.settle(store.getAll()),
|
|
3026
|
+
this.#table
|
|
3027
|
+
);
|
|
3028
|
+
for (const clause of this.#joins) {
|
|
3029
|
+
const other = await this.#store("readonly", clause.table);
|
|
3030
|
+
const records = await Request.settle(other.getAll());
|
|
3031
|
+
const columns = tables.get(clause.table).map((column) => `${clause.table}.${column}`);
|
|
3032
|
+
rows = Joiner.join(rows, Joiner.qualify(records, clause.table), clause, columns);
|
|
3033
|
+
}
|
|
3034
|
+
const constraints = this.#constraints.map((constraint) => this.#qualified(constraint, tables));
|
|
3035
|
+
const orders = this.#orders.map((order) => ({ ...order, column: Columns.resolve(order.column, tables) }));
|
|
3036
|
+
const matches = Predicate.compile(constraints);
|
|
3037
|
+
const kept = rows.filter(matches);
|
|
3038
|
+
const sorted = Comparator.sort(kept, orders, (row, column) => row[column]);
|
|
3039
|
+
const from = this.#offset;
|
|
3040
|
+
const paged = this.#limit === null ? sorted.slice(from) : sorted.slice(from, from + this.#limit);
|
|
3041
|
+
this.#emit("join", started, paged.length);
|
|
3042
|
+
return this.#flatten(paged, tables);
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Qualify every column a constraint names with the table that owns it.
|
|
3046
|
+
*/
|
|
3047
|
+
#qualified(constraint, tables) {
|
|
3048
|
+
if (constraint.type === "nested") {
|
|
3049
|
+
return { ...constraint, constraints: constraint.constraints.map((nested) => this.#qualified(nested, tables)) };
|
|
3050
|
+
}
|
|
3051
|
+
if (constraint.type === "column") {
|
|
3052
|
+
return { ...constraint, column: Columns.resolve(constraint.column, tables), other: Columns.resolve(constraint.other, tables) };
|
|
3053
|
+
}
|
|
3054
|
+
return { ...constraint, column: Columns.resolve(constraint.column, tables) };
|
|
3055
|
+
}
|
|
3056
|
+
/**
|
|
3057
|
+
* Flatten qualified rows the way SQL does, letting later tables win a collision.
|
|
3058
|
+
*/
|
|
3059
|
+
#flatten(rows, tables) {
|
|
3060
|
+
if (this.#columns !== null) {
|
|
3061
|
+
return rows.map((row) => Object.fromEntries(
|
|
3062
|
+
this.#columns.map((expression) => {
|
|
3063
|
+
const projection = Columns.parse(expression);
|
|
3064
|
+
return [projection.alias, row[Columns.resolve(projection.column, tables)]];
|
|
3065
|
+
})
|
|
3066
|
+
));
|
|
3067
|
+
}
|
|
3068
|
+
const order = [...tables.keys()];
|
|
3069
|
+
return rows.map((row) => {
|
|
3070
|
+
const flat = {};
|
|
3071
|
+
for (const table of order) {
|
|
3072
|
+
for (const column of tables.get(table)) {
|
|
3073
|
+
if (Object.hasOwn(row, `${table}.${column}`)) {
|
|
3074
|
+
flat[column] = row[`${table}.${column}`];
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
return flat;
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Run the query, collecting the matching records and their keys.
|
|
3083
|
+
*/
|
|
3084
|
+
async #matched() {
|
|
3085
|
+
if (this.#joins.length > 0) {
|
|
3086
|
+
const rows = await this.#joined();
|
|
3087
|
+
return { records: rows, keys: [] };
|
|
3088
|
+
}
|
|
3089
|
+
const schema = await this.#connection.schema(this.#table);
|
|
3090
|
+
const plan = Planner.plan(this.#constraints, this.#orders, schema);
|
|
3091
|
+
const store = await this.#store("readonly");
|
|
3092
|
+
const started = performance.now();
|
|
3093
|
+
const matches = Predicate.compile(plan.residual);
|
|
3094
|
+
const collected = plan.values === null ? await this.#cursored(store, plan, matches) : await this.#points(store, plan, matches);
|
|
3095
|
+
const ordered = plan.ordered ? collected : this.#sorted(collected);
|
|
3096
|
+
const paged = this.#paged(ordered);
|
|
3097
|
+
this.#emit(Planner.describe(plan), started, paged.length);
|
|
3098
|
+
return {
|
|
3099
|
+
records: paged.map((entry) => entry.record),
|
|
3100
|
+
keys: paged.map((entry) => entry.key)
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
/**
|
|
3104
|
+
* Collect the records a cursor over the planned source yields.
|
|
3105
|
+
*/
|
|
3106
|
+
async #cursored(store, plan, matches) {
|
|
3107
|
+
const source = plan.index === null ? store : store.index(plan.index);
|
|
3108
|
+
const collected = [];
|
|
3109
|
+
const ceiling = plan.ordered && this.#limit !== null ? this.#offset + this.#limit : null;
|
|
3110
|
+
await Request.walk(source.openCursor(plan.range, plan.direction), (cursor) => {
|
|
3111
|
+
if (matches(cursor.value)) {
|
|
3112
|
+
collected.push({ record: cursor.value, key: cursor.primaryKey });
|
|
3113
|
+
}
|
|
3114
|
+
return ceiling === null || collected.length < ceiling;
|
|
3115
|
+
});
|
|
3116
|
+
return collected;
|
|
3117
|
+
}
|
|
3118
|
+
/**
|
|
3119
|
+
* Collect the records the planned point lookups yield.
|
|
3120
|
+
*/
|
|
3121
|
+
async #points(store, plan, matches) {
|
|
3122
|
+
const source = plan.index === null ? store : store.index(plan.index);
|
|
3123
|
+
const collected = [];
|
|
3124
|
+
for (const value of plan.values) {
|
|
3125
|
+
const range = IDBKeyRange.only(value);
|
|
3126
|
+
await Request.walk(source.openCursor(range), (cursor) => {
|
|
3127
|
+
if (matches(cursor.value)) {
|
|
3128
|
+
collected.push({ record: cursor.value, key: cursor.primaryKey });
|
|
3129
|
+
}
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
return collected;
|
|
3133
|
+
}
|
|
3134
|
+
/**
|
|
3135
|
+
* Sort the collected records by the requested orders.
|
|
3136
|
+
*/
|
|
3137
|
+
#sorted(collected) {
|
|
3138
|
+
return Comparator.sort(
|
|
3139
|
+
collected,
|
|
3140
|
+
this.#orders,
|
|
3141
|
+
(entry, column) => entry.record[column]
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
/**
|
|
3145
|
+
* Apply the offset and limit to the collected records.
|
|
3146
|
+
*/
|
|
3147
|
+
#paged(collected) {
|
|
3148
|
+
const from = this.#offset;
|
|
3149
|
+
return this.#limit === null ? collected.slice(from) : collected.slice(from, from + this.#limit);
|
|
3150
|
+
}
|
|
3151
|
+
/**
|
|
3152
|
+
* Project and deduplicate the records the query returns.
|
|
3153
|
+
*/
|
|
3154
|
+
#shape(records) {
|
|
3155
|
+
const projected = this.#columns === null || this.#joins.length > 0 ? records : records.map((record) => Object.fromEntries(
|
|
3156
|
+
this.#columns.map((expression) => {
|
|
3157
|
+
const projection = Columns.parse(expression);
|
|
3158
|
+
return [projection.alias, record[projection.column]];
|
|
3159
|
+
})
|
|
3160
|
+
));
|
|
3161
|
+
if (!this.#distinct) {
|
|
3162
|
+
return projected;
|
|
3163
|
+
}
|
|
3164
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3165
|
+
return projected.filter((record) => {
|
|
3166
|
+
const signature = Signature.of(record);
|
|
3167
|
+
if (seen.has(signature)) {
|
|
3168
|
+
return false;
|
|
3169
|
+
}
|
|
3170
|
+
seen.add(signature);
|
|
3171
|
+
return true;
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
/**
|
|
3175
|
+
* Announce that the query ran.
|
|
3176
|
+
*/
|
|
3177
|
+
#emit(plan, started, records) {
|
|
3178
|
+
Dispatcher.dispatch(new QueryExecuted(
|
|
3179
|
+
this.#connection.name,
|
|
3180
|
+
this.#table,
|
|
3181
|
+
plan,
|
|
3182
|
+
this.#constraints,
|
|
3183
|
+
this.#orders,
|
|
3184
|
+
this.#limit,
|
|
3185
|
+
performance.now() - started,
|
|
3186
|
+
records
|
|
3187
|
+
));
|
|
3188
|
+
}
|
|
3189
|
+
};
|
|
3190
|
+
|
|
3191
|
+
// src/schema/Registry.ts
|
|
3192
|
+
var Registry = class {
|
|
3193
|
+
/**
|
|
3194
|
+
* The name of the reserved store holding the table schemas.
|
|
3195
|
+
*/
|
|
3196
|
+
static table = "schema";
|
|
3197
|
+
/**
|
|
3198
|
+
* Create the schema store, unless it already exists.
|
|
3199
|
+
*/
|
|
3200
|
+
static create(database) {
|
|
3201
|
+
if (database.objectStoreNames.contains(this.table)) {
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
database.createObjectStore(this.table, { keyPath: "table" });
|
|
3205
|
+
}
|
|
3206
|
+
/**
|
|
3207
|
+
* Get the schema of every table.
|
|
3208
|
+
*/
|
|
3209
|
+
static all(transaction) {
|
|
3210
|
+
return Request.settle(transaction.objectStore(this.table).getAll());
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* Record the schema of a table.
|
|
3214
|
+
*/
|
|
3215
|
+
static put(transaction, schema) {
|
|
3216
|
+
transaction.objectStore(this.table).put(schema);
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Forget the schema of a table.
|
|
3220
|
+
*/
|
|
3221
|
+
static forget(transaction, table) {
|
|
3222
|
+
transaction.objectStore(this.table).delete(table);
|
|
3223
|
+
}
|
|
3224
|
+
};
|
|
3225
|
+
|
|
3226
|
+
// src/migrations/Repository.ts
|
|
3227
|
+
var Repository = class {
|
|
3228
|
+
/**
|
|
3229
|
+
* The name of the reserved store holding the migration records.
|
|
3230
|
+
*/
|
|
3231
|
+
static table = "migrations";
|
|
3232
|
+
/**
|
|
3233
|
+
* Create the migrations store, unless it already exists.
|
|
3234
|
+
*/
|
|
3235
|
+
static create(database) {
|
|
3236
|
+
if (database.objectStoreNames.contains(this.table)) {
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
database.createObjectStore(this.table, { keyPath: "order" });
|
|
3240
|
+
}
|
|
3241
|
+
/**
|
|
3242
|
+
* Record that a migration ran.
|
|
3243
|
+
*/
|
|
3244
|
+
static log(transaction, order, migration, at) {
|
|
3245
|
+
transaction.objectStore(this.table).put({ order, migration, at: at.toISOString() });
|
|
3246
|
+
}
|
|
3247
|
+
/**
|
|
3248
|
+
* Get every recorded migration, in the order they were applied.
|
|
3249
|
+
*/
|
|
3250
|
+
static ran(transaction) {
|
|
3251
|
+
return Request.settle(transaction.objectStore(this.table).getAll());
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
|
|
3255
|
+
// src/migrations/Migrator.ts
|
|
3256
|
+
var Migrator = class {
|
|
3257
|
+
/**
|
|
3258
|
+
* The context of the migration currently running.
|
|
3259
|
+
*/
|
|
3260
|
+
static #context = null;
|
|
3261
|
+
/**
|
|
3262
|
+
* Get the context of the migration currently running.
|
|
3263
|
+
*/
|
|
3264
|
+
static context() {
|
|
3265
|
+
return this.#context;
|
|
3266
|
+
}
|
|
3267
|
+
/**
|
|
3268
|
+
* Get the version a database with the given migrations should be opened at.
|
|
3269
|
+
*/
|
|
3270
|
+
static version(migrations) {
|
|
3271
|
+
return migrations.length + 1;
|
|
3272
|
+
}
|
|
3273
|
+
/**
|
|
3274
|
+
* Get the index of the first migration still pending at the given version.
|
|
3275
|
+
*/
|
|
3276
|
+
static pending(version) {
|
|
3277
|
+
return Math.max(0, version - 1);
|
|
3278
|
+
}
|
|
3279
|
+
/**
|
|
3280
|
+
* Get the names of the given migrations, in order.
|
|
3281
|
+
*/
|
|
3282
|
+
static names(migrations) {
|
|
3283
|
+
return migrations.map((migration) => new migration().name());
|
|
3284
|
+
}
|
|
3285
|
+
/**
|
|
3286
|
+
* Assert that the migrations already run are a prefix of those registered.
|
|
3287
|
+
*/
|
|
3288
|
+
static verify(ran, registered) {
|
|
3289
|
+
const applied = ran.map((record) => record.migration);
|
|
3290
|
+
const diverged = applied.length > registered.length || applied.some((migration, index) => registered[index] !== migration);
|
|
3291
|
+
if (diverged) {
|
|
3292
|
+
throw new MigrationMismatchException(applied, registered);
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
/**
|
|
3296
|
+
* Assert that the active migration still holds its transaction.
|
|
3297
|
+
*/
|
|
3298
|
+
static alive() {
|
|
3299
|
+
const context = this.#context;
|
|
3300
|
+
if (context === null) {
|
|
3301
|
+
throw new MigrationTransactionClosedException("unknown");
|
|
3302
|
+
}
|
|
3303
|
+
if (!context.alive) {
|
|
3304
|
+
throw new MigrationTransactionClosedException(context.migration);
|
|
3305
|
+
}
|
|
3306
|
+
return context;
|
|
3307
|
+
}
|
|
3308
|
+
/**
|
|
3309
|
+
* Run the migrations still pending inside the version change transaction.
|
|
3310
|
+
*/
|
|
3311
|
+
static async run(connection, database, transaction, migrations, from, at) {
|
|
3312
|
+
Repository.create(database);
|
|
3313
|
+
Registry.create(database);
|
|
3314
|
+
const records = Repository.ran(transaction);
|
|
3315
|
+
const registry = Registry.all(transaction);
|
|
3316
|
+
const [ran, schemas] = await Promise.all([records, registry]);
|
|
3317
|
+
this.verify(ran, this.names(migrations));
|
|
3318
|
+
this.#context = {
|
|
3319
|
+
database,
|
|
3320
|
+
transaction,
|
|
3321
|
+
migration: "unknown",
|
|
3322
|
+
schemas: new Map(schemas.map((schema) => [schema.table, schema])),
|
|
3323
|
+
alive: true
|
|
3324
|
+
};
|
|
3325
|
+
transaction.addEventListener("complete", this.#close);
|
|
3326
|
+
transaction.addEventListener("abort", this.#close);
|
|
3327
|
+
try {
|
|
3328
|
+
return await this.#migrate(connection, migrations, from, at);
|
|
3329
|
+
} finally {
|
|
3330
|
+
this.#context = null;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
/**
|
|
3334
|
+
* Run each pending migration in order, recording it as it completes.
|
|
3335
|
+
*/
|
|
3336
|
+
static async #migrate(connection, migrations, from, at) {
|
|
3337
|
+
const pending = migrations.slice(from);
|
|
3338
|
+
if (pending.length === 0) {
|
|
3339
|
+
Dispatcher.dispatch(new NoPendingMigrations(connection));
|
|
3340
|
+
return [];
|
|
3341
|
+
}
|
|
3342
|
+
const ran = [];
|
|
3343
|
+
Dispatcher.dispatch(new MigrationsStarted(connection, this.names(pending)));
|
|
3344
|
+
for (const [offset, constructor] of pending.entries()) {
|
|
3345
|
+
const migration = new constructor();
|
|
3346
|
+
const name = migration.name();
|
|
3347
|
+
const context = this.alive();
|
|
3348
|
+
context.migration = name;
|
|
3349
|
+
Dispatcher.dispatch(new MigrationStarted(name));
|
|
3350
|
+
await migration.up();
|
|
3351
|
+
Repository.log(this.alive().transaction, from + offset + 1, name, at);
|
|
3352
|
+
Dispatcher.dispatch(new MigrationEnded(name));
|
|
3353
|
+
ran.push(name);
|
|
3354
|
+
}
|
|
3355
|
+
Dispatcher.dispatch(new MigrationsEnded(connection, ran));
|
|
3356
|
+
return ran;
|
|
3357
|
+
}
|
|
3358
|
+
/**
|
|
3359
|
+
* Mark the active context as no longer holding its transaction.
|
|
3360
|
+
*/
|
|
3361
|
+
static #close = () => {
|
|
3362
|
+
if (this.#context !== null) {
|
|
3363
|
+
this.#context.alive = false;
|
|
3364
|
+
}
|
|
3365
|
+
};
|
|
3366
|
+
};
|
|
3367
|
+
|
|
3368
|
+
// src/database/Resolver.ts
|
|
3369
|
+
var Resolver = class {
|
|
3370
|
+
/**
|
|
3371
|
+
* The connection standing in for the configured default.
|
|
3372
|
+
*/
|
|
3373
|
+
static #override = null;
|
|
3374
|
+
/**
|
|
3375
|
+
* Get the connection standing in for the configured default.
|
|
3376
|
+
*/
|
|
3377
|
+
static override() {
|
|
3378
|
+
return this.#override;
|
|
3379
|
+
}
|
|
3380
|
+
/**
|
|
3381
|
+
* Run the callback with the given connection standing in as the default.
|
|
3382
|
+
*/
|
|
3383
|
+
static async during(name, callback) {
|
|
3384
|
+
const previous = this.#override;
|
|
3385
|
+
this.#override = name;
|
|
3386
|
+
try {
|
|
3387
|
+
return await callback();
|
|
3388
|
+
} finally {
|
|
3389
|
+
this.#override = previous;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
};
|
|
3393
|
+
|
|
3394
|
+
// src/database/Transaction.ts
|
|
3395
|
+
var Transaction = class {
|
|
3396
|
+
/**
|
|
3397
|
+
* The connection the transaction runs on.
|
|
3398
|
+
*/
|
|
3399
|
+
#connection;
|
|
3400
|
+
/**
|
|
3401
|
+
* The underlying transaction.
|
|
3402
|
+
*/
|
|
3403
|
+
#transaction;
|
|
3404
|
+
/**
|
|
3405
|
+
* Create a new transaction.
|
|
3406
|
+
*/
|
|
3407
|
+
constructor(connection, transaction) {
|
|
3408
|
+
this.#connection = connection;
|
|
3409
|
+
this.#transaction = transaction;
|
|
3410
|
+
}
|
|
3411
|
+
/**
|
|
3412
|
+
* Get the names of the tables the transaction is scoped to.
|
|
3413
|
+
*/
|
|
3414
|
+
get tables() {
|
|
3415
|
+
return Array.from(this.#transaction.objectStoreNames);
|
|
3416
|
+
}
|
|
3417
|
+
/**
|
|
3418
|
+
* Begin a query against a table inside the transaction.
|
|
3419
|
+
*/
|
|
3420
|
+
table(name) {
|
|
3421
|
+
if (!this.#transaction.objectStoreNames.contains(name)) {
|
|
3422
|
+
throw new SchemaException(`Table [${name}] is outside the scope of this transaction.`);
|
|
3423
|
+
}
|
|
3424
|
+
return this.#connection.table(name, this.#transaction);
|
|
3425
|
+
}
|
|
3426
|
+
};
|
|
3427
|
+
|
|
3428
|
+
// src/database/Connection.ts
|
|
3429
|
+
var Connection = class {
|
|
3430
|
+
/**
|
|
3431
|
+
* The name of the connection.
|
|
3432
|
+
*/
|
|
3433
|
+
#name;
|
|
3434
|
+
/**
|
|
3435
|
+
* The configuration of the connection.
|
|
3436
|
+
*/
|
|
3437
|
+
#config;
|
|
3438
|
+
/**
|
|
3439
|
+
* The open database handle.
|
|
3440
|
+
*/
|
|
3441
|
+
#database = null;
|
|
3442
|
+
/**
|
|
3443
|
+
* The open in flight, so concurrent callers share one handle.
|
|
3444
|
+
*/
|
|
3445
|
+
#opening = null;
|
|
3446
|
+
/**
|
|
3447
|
+
* The table schemas, read once per open and held in memory.
|
|
3448
|
+
*/
|
|
3449
|
+
#schemas = /* @__PURE__ */ new Map();
|
|
3450
|
+
/**
|
|
3451
|
+
* The transaction currently running, so a nested call joins it.
|
|
3452
|
+
*/
|
|
3453
|
+
#active = null;
|
|
3454
|
+
/**
|
|
3455
|
+
* The names of the migrations run by the last open.
|
|
3456
|
+
*/
|
|
3457
|
+
#migrated = [];
|
|
3458
|
+
/**
|
|
3459
|
+
* Create a new connection.
|
|
3460
|
+
*/
|
|
3461
|
+
constructor(name, config) {
|
|
3462
|
+
this.#name = name;
|
|
3463
|
+
this.#config = config;
|
|
3464
|
+
}
|
|
3465
|
+
/**
|
|
3466
|
+
* Get the name of the connection.
|
|
3467
|
+
*/
|
|
3468
|
+
get name() {
|
|
3469
|
+
return this.#name;
|
|
3470
|
+
}
|
|
3471
|
+
/**
|
|
3472
|
+
* Get the name of the underlying database.
|
|
3473
|
+
*/
|
|
3474
|
+
get database() {
|
|
3475
|
+
return this.#config.database;
|
|
3476
|
+
}
|
|
3477
|
+
/**
|
|
3478
|
+
* Determine whether the connection enforces its column declarations.
|
|
3479
|
+
*/
|
|
3480
|
+
get strict() {
|
|
3481
|
+
return this.#config.strict !== false;
|
|
3482
|
+
}
|
|
3483
|
+
/**
|
|
3484
|
+
* Get the migrations registered on the connection.
|
|
3485
|
+
*/
|
|
3486
|
+
get migrations() {
|
|
3487
|
+
return this.#config.migrations ?? [];
|
|
3488
|
+
}
|
|
3489
|
+
/**
|
|
3490
|
+
* Get the seeders registered on the connection.
|
|
3491
|
+
*/
|
|
3492
|
+
get seeders() {
|
|
3493
|
+
return this.#config.seeders ?? [];
|
|
3494
|
+
}
|
|
3495
|
+
/**
|
|
3496
|
+
* Open the underlying database, running any pending migrations.
|
|
3497
|
+
*/
|
|
3498
|
+
async open() {
|
|
3499
|
+
if (this.#database !== null) {
|
|
3500
|
+
return this.#database;
|
|
3501
|
+
}
|
|
3502
|
+
if (this.#opening === null) {
|
|
3503
|
+
this.#opening = this.#connect();
|
|
3504
|
+
}
|
|
3505
|
+
try {
|
|
3506
|
+
return await this.#opening;
|
|
3507
|
+
} finally {
|
|
3508
|
+
this.#opening = null;
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
/**
|
|
3512
|
+
* Run any pending migrations, returning the names of those this call ran.
|
|
3513
|
+
*/
|
|
3514
|
+
async migrate() {
|
|
3515
|
+
await this.open();
|
|
3516
|
+
const migrated = this.#migrated;
|
|
3517
|
+
this.#migrated = [];
|
|
3518
|
+
return migrated;
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* Run every registered seeder, returning their names.
|
|
3522
|
+
*/
|
|
3523
|
+
async seed() {
|
|
3524
|
+
await this.open();
|
|
3525
|
+
const seeders = this.seeders;
|
|
3526
|
+
if (seeders.length === 0) {
|
|
3527
|
+
return [];
|
|
3528
|
+
}
|
|
3529
|
+
const names = seeders.map((seeder) => new seeder().name());
|
|
3530
|
+
Dispatcher.dispatch(new SeedingStarted(this.#name, names));
|
|
3531
|
+
await Resolver.during(this.#name, async () => {
|
|
3532
|
+
for (const constructor of seeders) {
|
|
3533
|
+
const seeder = new constructor();
|
|
3534
|
+
const name = seeder.name();
|
|
3535
|
+
Dispatcher.dispatch(new SeederStarted(name));
|
|
3536
|
+
await seeder.run();
|
|
3537
|
+
Dispatcher.dispatch(new SeederEnded(name));
|
|
3538
|
+
}
|
|
3539
|
+
});
|
|
3540
|
+
Dispatcher.dispatch(new SeedingEnded(this.#name, names));
|
|
3541
|
+
return names;
|
|
3542
|
+
}
|
|
3543
|
+
/**
|
|
3544
|
+
* Delete the database and replay every migration.
|
|
3545
|
+
*/
|
|
3546
|
+
async fresh(options = {}) {
|
|
3547
|
+
this.disconnect();
|
|
3548
|
+
await new Promise((resolve, reject) => {
|
|
3549
|
+
const request = indexedDB.deleteDatabase(this.#config.database);
|
|
3550
|
+
request.onsuccess = () => resolve();
|
|
3551
|
+
request.onerror = () => reject(request.error);
|
|
3552
|
+
request.onblocked = () => {
|
|
3553
|
+
Dispatcher.dispatch(new DatabaseBlocked(this.#config.database));
|
|
3554
|
+
reject(new DatabaseBlockedException(this.#config.database));
|
|
3555
|
+
};
|
|
3556
|
+
});
|
|
3557
|
+
const migrated = await this.migrate();
|
|
3558
|
+
if (options.seed === true) {
|
|
3559
|
+
await this.seed();
|
|
3560
|
+
}
|
|
3561
|
+
return migrated;
|
|
3562
|
+
}
|
|
3563
|
+
/**
|
|
3564
|
+
* Get the state of every registered migration, without running any of them.
|
|
3565
|
+
*/
|
|
3566
|
+
async status() {
|
|
3567
|
+
const applied = new Map(
|
|
3568
|
+
(await this.#recorded()).map((record) => [record.migration, record.at])
|
|
3569
|
+
);
|
|
3570
|
+
return Migrator.names(this.migrations).map((migration) => ({
|
|
3571
|
+
migration,
|
|
3572
|
+
ran: applied.has(migration),
|
|
3573
|
+
at: applied.get(migration) ?? null
|
|
3574
|
+
}));
|
|
3575
|
+
}
|
|
3576
|
+
/**
|
|
3577
|
+
* Begin a query against a table.
|
|
3578
|
+
*/
|
|
3579
|
+
table(table, transaction = null) {
|
|
3580
|
+
return new Builder(this, table, transaction);
|
|
3581
|
+
}
|
|
3582
|
+
/**
|
|
3583
|
+
* Run the callback inside a transaction, committing when it resolves.
|
|
3584
|
+
*/
|
|
3585
|
+
async transaction(callback, options = {}) {
|
|
3586
|
+
if (this.#active !== null) {
|
|
3587
|
+
return callback(this.#active);
|
|
3588
|
+
}
|
|
3589
|
+
const database = await this.open();
|
|
3590
|
+
const tables = options.tables ?? Array.from(database.objectStoreNames);
|
|
3591
|
+
const handle = database.transaction(tables, "readwrite");
|
|
3592
|
+
const transaction = new Transaction(this, handle);
|
|
3593
|
+
let finished = false;
|
|
3594
|
+
let aborting = false;
|
|
3595
|
+
handle.onerror = () => {
|
|
3596
|
+
aborting = true;
|
|
3597
|
+
};
|
|
3598
|
+
const settled = new Promise((resolve, reject) => {
|
|
3599
|
+
handle.oncomplete = () => {
|
|
3600
|
+
finished = true;
|
|
3601
|
+
resolve();
|
|
3602
|
+
};
|
|
3603
|
+
handle.onabort = () => {
|
|
3604
|
+
finished = true;
|
|
3605
|
+
reject(Request.translate(handle.error) ?? new DOMException("The transaction was aborted.", "AbortError"));
|
|
3606
|
+
};
|
|
3607
|
+
});
|
|
3608
|
+
settled.catch(() => {
|
|
3609
|
+
});
|
|
3610
|
+
this.#active = transaction;
|
|
3611
|
+
Dispatcher.dispatch(new TransactionBeginning(this.#name));
|
|
3612
|
+
try {
|
|
3613
|
+
const result = await callback(transaction);
|
|
3614
|
+
await settled;
|
|
3615
|
+
this.#active = null;
|
|
3616
|
+
Dispatcher.dispatch(new TransactionCommitted(this.#name));
|
|
3617
|
+
return result;
|
|
3618
|
+
} catch (error) {
|
|
3619
|
+
this.#active = null;
|
|
3620
|
+
if (!finished) {
|
|
3621
|
+
if (!aborting) {
|
|
3622
|
+
handle.abort();
|
|
3623
|
+
}
|
|
3624
|
+
Dispatcher.dispatch(new TransactionRolledBack(this.#name, error));
|
|
3625
|
+
}
|
|
3626
|
+
throw error;
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
/**
|
|
3630
|
+
* Get the schema of a table.
|
|
3631
|
+
*/
|
|
3632
|
+
async schema(table) {
|
|
3633
|
+
await this.open();
|
|
3634
|
+
const schema = this.#schemas.get(table);
|
|
3635
|
+
if (schema === void 0) {
|
|
3636
|
+
throw new TableNotFoundException(table);
|
|
3637
|
+
}
|
|
3638
|
+
return schema;
|
|
3639
|
+
}
|
|
3640
|
+
/**
|
|
3641
|
+
* Get the names of every table.
|
|
3642
|
+
*/
|
|
3643
|
+
async tables() {
|
|
3644
|
+
await this.open();
|
|
3645
|
+
return [...this.#schemas.keys()];
|
|
3646
|
+
}
|
|
3647
|
+
/**
|
|
3648
|
+
* Determine whether a table exists.
|
|
3649
|
+
*/
|
|
3650
|
+
async hasTable(table) {
|
|
3651
|
+
return (await this.tables()).includes(table);
|
|
3652
|
+
}
|
|
3653
|
+
/**
|
|
3654
|
+
* Determine whether a table has a column.
|
|
3655
|
+
*/
|
|
3656
|
+
async hasColumn(table, column) {
|
|
3657
|
+
return (await this.getColumns(table)).some((candidate) => candidate.name === column);
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
3660
|
+
* Get the columns of a table.
|
|
3661
|
+
*/
|
|
3662
|
+
async getColumns(table) {
|
|
3663
|
+
return (await this.schema(table)).columns;
|
|
3664
|
+
}
|
|
3665
|
+
/**
|
|
3666
|
+
* Get the indexes of a table.
|
|
3667
|
+
*/
|
|
3668
|
+
async getIndexes(table) {
|
|
3669
|
+
return (await this.schema(table)).indexes;
|
|
3670
|
+
}
|
|
3671
|
+
/**
|
|
3672
|
+
* Close the underlying database.
|
|
3673
|
+
*/
|
|
3674
|
+
disconnect() {
|
|
3675
|
+
this.#database?.close();
|
|
3676
|
+
this.#database = null;
|
|
3677
|
+
this.#schemas = /* @__PURE__ */ new Map();
|
|
3678
|
+
}
|
|
3679
|
+
/**
|
|
3680
|
+
* Open the database at the version the registered migrations ask for.
|
|
3681
|
+
*/
|
|
3682
|
+
#connect() {
|
|
3683
|
+
const migrations = this.migrations;
|
|
3684
|
+
const version = Migrator.version(migrations);
|
|
3685
|
+
return new Promise((resolve, reject) => {
|
|
3686
|
+
const request = indexedDB.open(this.#config.database, version);
|
|
3687
|
+
let runner = null;
|
|
3688
|
+
let failure = null;
|
|
3689
|
+
request.onupgradeneeded = (event) => {
|
|
3690
|
+
runner = Migrator.run(
|
|
3691
|
+
this.#name,
|
|
3692
|
+
request.result,
|
|
3693
|
+
request.transaction,
|
|
3694
|
+
migrations,
|
|
3695
|
+
Migrator.pending(event.oldVersion),
|
|
3696
|
+
/* @__PURE__ */ new Date()
|
|
3697
|
+
);
|
|
3698
|
+
runner.catch((error) => {
|
|
3699
|
+
failure = error;
|
|
3700
|
+
request.transaction?.abort();
|
|
3701
|
+
});
|
|
3702
|
+
};
|
|
3703
|
+
request.onblocked = () => {
|
|
3704
|
+
Dispatcher.dispatch(new DatabaseBlocked(this.#config.database));
|
|
3705
|
+
reject(new DatabaseBlockedException(this.#config.database));
|
|
3706
|
+
};
|
|
3707
|
+
request.onerror = () => {
|
|
3708
|
+
if (failure !== null) {
|
|
3709
|
+
reject(failure);
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
if (request.error?.name === "VersionError") {
|
|
3713
|
+
this.#recorded().then(
|
|
3714
|
+
(ran) => reject(new MigrationMismatchException(ran.map((record) => record.migration), Migrator.names(migrations))),
|
|
3715
|
+
reject
|
|
3716
|
+
);
|
|
3717
|
+
return;
|
|
3718
|
+
}
|
|
3719
|
+
reject(request.error);
|
|
3720
|
+
};
|
|
3721
|
+
request.onsuccess = () => {
|
|
3722
|
+
this.#ready(request.result, runner).then(resolve, reject);
|
|
3723
|
+
};
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
/**
|
|
3727
|
+
* Get the recorded migrations, opening at whatever version is already stored.
|
|
3728
|
+
*/
|
|
3729
|
+
async #recorded() {
|
|
3730
|
+
const database = await new Promise((resolve, reject) => {
|
|
3731
|
+
const request = indexedDB.open(this.#config.database);
|
|
3732
|
+
request.onsuccess = () => resolve(request.result);
|
|
3733
|
+
request.onerror = () => reject(request.error);
|
|
3734
|
+
});
|
|
3735
|
+
try {
|
|
3736
|
+
if (!database.objectStoreNames.contains(Repository.table)) {
|
|
3737
|
+
return [];
|
|
3738
|
+
}
|
|
3739
|
+
return await Repository.ran(database.transaction(Repository.table, "readonly"));
|
|
3740
|
+
} finally {
|
|
3741
|
+
database.close();
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
/**
|
|
3745
|
+
* Finish opening: await the migrations, verify them and cache the schemas.
|
|
3746
|
+
*/
|
|
3747
|
+
async #ready(database, runner) {
|
|
3748
|
+
this.#migrated = runner === null ? [] : await runner;
|
|
3749
|
+
const transaction = database.transaction([Repository.table, Registry.table], "readonly");
|
|
3750
|
+
const records = Repository.ran(transaction);
|
|
3751
|
+
const schemas = Registry.all(transaction);
|
|
3752
|
+
const [ran, registry] = await Promise.all([records, schemas]);
|
|
3753
|
+
Migrator.verify(ran, Migrator.names(this.migrations));
|
|
3754
|
+
this.#schemas = new Map(registry.map((schema) => [schema.table, schema]));
|
|
3755
|
+
this.#database = database;
|
|
3756
|
+
database.onversionchange = () => this.disconnect();
|
|
3757
|
+
return database;
|
|
3758
|
+
}
|
|
3759
|
+
};
|
|
3760
|
+
|
|
3761
|
+
// src/database/DatabaseManager.ts
|
|
3762
|
+
var DatabaseManager = class {
|
|
3763
|
+
/**
|
|
3764
|
+
* The registered configuration.
|
|
3765
|
+
*/
|
|
3766
|
+
static #config = null;
|
|
3767
|
+
/**
|
|
3768
|
+
* The connections resolved so far, keyed by name.
|
|
3769
|
+
*/
|
|
3770
|
+
static #connections = /* @__PURE__ */ new Map();
|
|
3771
|
+
/**
|
|
3772
|
+
* The queries recorded while the log is enabled.
|
|
3773
|
+
*/
|
|
3774
|
+
static #log = [];
|
|
3775
|
+
/**
|
|
3776
|
+
* The listener recording queries, present only while the log is enabled.
|
|
3777
|
+
*/
|
|
3778
|
+
static #logger = null;
|
|
3779
|
+
/**
|
|
3780
|
+
* Register the database configuration, replacing anything registered before.
|
|
3781
|
+
*/
|
|
3782
|
+
static configure(config) {
|
|
3783
|
+
for (const connection of this.#connections.values()) {
|
|
3784
|
+
connection.disconnect();
|
|
3785
|
+
}
|
|
3786
|
+
this.#config = config;
|
|
3787
|
+
this.#connections = /* @__PURE__ */ new Map();
|
|
3788
|
+
}
|
|
3789
|
+
/**
|
|
3790
|
+
* Resolve a connection by name, or the default one.
|
|
3791
|
+
*/
|
|
3792
|
+
static connection(name) {
|
|
3793
|
+
const config = this.#config;
|
|
3794
|
+
if (config === null) {
|
|
3795
|
+
throw new ConnectionNotConfiguredException(name ?? "default");
|
|
3796
|
+
}
|
|
3797
|
+
const resolved = name ?? Resolver.override() ?? config.default;
|
|
3798
|
+
const cached = this.#connections.get(resolved);
|
|
3799
|
+
if (cached !== void 0) {
|
|
3800
|
+
return cached;
|
|
3801
|
+
}
|
|
3802
|
+
const entry = config.connections[resolved];
|
|
3803
|
+
if (entry === void 0) {
|
|
3804
|
+
throw new ConnectionNotConfiguredException(resolved);
|
|
3805
|
+
}
|
|
3806
|
+
const connection = new Connection(resolved, entry);
|
|
3807
|
+
this.#connections.set(resolved, connection);
|
|
3808
|
+
return connection;
|
|
3809
|
+
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Begin a query against a table on the default connection.
|
|
3812
|
+
*/
|
|
3813
|
+
static table(table) {
|
|
3814
|
+
return this.connection().table(table);
|
|
3815
|
+
}
|
|
3816
|
+
/**
|
|
3817
|
+
* Run the callback inside a transaction on the default connection.
|
|
3818
|
+
*/
|
|
3819
|
+
static transaction(callback, options) {
|
|
3820
|
+
return this.connection().transaction(callback, options);
|
|
3821
|
+
}
|
|
3822
|
+
/**
|
|
3823
|
+
* Run any pending migrations, returning the names of those this call ran.
|
|
3824
|
+
*/
|
|
3825
|
+
static migrate(name) {
|
|
3826
|
+
return this.connection(name).migrate();
|
|
3827
|
+
}
|
|
3828
|
+
/**
|
|
3829
|
+
* Run every registered seeder, returning their names.
|
|
3830
|
+
*/
|
|
3831
|
+
static seed(name) {
|
|
3832
|
+
return this.connection(name).seed();
|
|
3833
|
+
}
|
|
3834
|
+
/**
|
|
3835
|
+
* Delete the database and replay every migration, optionally seeding afterwards.
|
|
3836
|
+
*/
|
|
3837
|
+
static fresh(name, options) {
|
|
3838
|
+
return this.connection(name).fresh(options);
|
|
3839
|
+
}
|
|
3840
|
+
/**
|
|
3841
|
+
* Get the state of every registered migration.
|
|
3842
|
+
*/
|
|
3843
|
+
static status(name) {
|
|
3844
|
+
return this.connection(name).status();
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* Determine whether a table exists.
|
|
3848
|
+
*/
|
|
3849
|
+
static hasTable(table, name) {
|
|
3850
|
+
return this.connection(name).hasTable(table);
|
|
3851
|
+
}
|
|
3852
|
+
/**
|
|
3853
|
+
* Determine whether a table has a column.
|
|
3854
|
+
*/
|
|
3855
|
+
static hasColumn(table, column, name) {
|
|
3856
|
+
return this.connection(name).hasColumn(table, column);
|
|
3857
|
+
}
|
|
3858
|
+
/**
|
|
3859
|
+
* Get the names of every table.
|
|
3860
|
+
*/
|
|
3861
|
+
static getTables(name) {
|
|
3862
|
+
return this.connection(name).tables();
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* Get the columns of a table.
|
|
3866
|
+
*/
|
|
3867
|
+
static getColumns(table, name) {
|
|
3868
|
+
return this.connection(name).getColumns(table);
|
|
3869
|
+
}
|
|
3870
|
+
/**
|
|
3871
|
+
* Get the indexes of a table.
|
|
3872
|
+
*/
|
|
3873
|
+
static getIndexes(table, name) {
|
|
3874
|
+
return this.connection(name).getIndexes(table);
|
|
3875
|
+
}
|
|
3876
|
+
/**
|
|
3877
|
+
* Close a connection, leaving it registered so the next query reopens it.
|
|
3878
|
+
*/
|
|
3879
|
+
static disconnect(name) {
|
|
3880
|
+
this.connection(name).disconnect();
|
|
3881
|
+
}
|
|
3882
|
+
/**
|
|
3883
|
+
* Close a connection and drop it, so the next resolve rebuilds it from configuration.
|
|
3884
|
+
*/
|
|
3885
|
+
static purge(name) {
|
|
3886
|
+
const connection = this.connection(name);
|
|
3887
|
+
connection.disconnect();
|
|
3888
|
+
this.#connections.delete(connection.name);
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Get how much storage this origin is using, and how much it may use.
|
|
3892
|
+
*/
|
|
3893
|
+
static async estimate() {
|
|
3894
|
+
const storage = globalThis.navigator?.storage;
|
|
3895
|
+
if (storage === void 0) {
|
|
3896
|
+
return {};
|
|
3897
|
+
}
|
|
3898
|
+
return storage.estimate();
|
|
3899
|
+
}
|
|
3900
|
+
/**
|
|
3901
|
+
* Ask the browser not to evict this origin's storage under pressure.
|
|
3902
|
+
*/
|
|
3903
|
+
static async persist() {
|
|
3904
|
+
const storage = globalThis.navigator?.storage;
|
|
3905
|
+
if (storage === void 0) {
|
|
3906
|
+
return false;
|
|
3907
|
+
}
|
|
3908
|
+
return storage.persist();
|
|
3909
|
+
}
|
|
3910
|
+
/**
|
|
3911
|
+
* Determine whether this origin's storage is already exempt from eviction.
|
|
3912
|
+
*/
|
|
3913
|
+
static async persisted() {
|
|
3914
|
+
const storage = globalThis.navigator?.storage;
|
|
3915
|
+
if (storage === void 0) {
|
|
3916
|
+
return false;
|
|
3917
|
+
}
|
|
3918
|
+
return storage.persisted();
|
|
3919
|
+
}
|
|
3920
|
+
/**
|
|
3921
|
+
* Register an event listener.
|
|
3922
|
+
*/
|
|
3923
|
+
static listen(event, listener, options = {}) {
|
|
3924
|
+
Dispatcher.listen(`db:${event}`, listener, options.once ?? false);
|
|
3925
|
+
}
|
|
3926
|
+
/**
|
|
3927
|
+
* Remove an event listener.
|
|
3928
|
+
*/
|
|
3929
|
+
static forget(event, listener) {
|
|
3930
|
+
Dispatcher.forget(`db:${event}`, listener);
|
|
3931
|
+
}
|
|
3932
|
+
/**
|
|
3933
|
+
* Register a listener on the "query" event.
|
|
3934
|
+
*/
|
|
3935
|
+
static onQueryExecuted(listener, options) {
|
|
3936
|
+
this.listen("query", listener, options);
|
|
3937
|
+
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Register a listener on the "transaction-beginning" event.
|
|
3940
|
+
*/
|
|
3941
|
+
static onTransactionBeginning(listener, options) {
|
|
3942
|
+
this.listen("transaction-beginning", listener, options);
|
|
3943
|
+
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Register a listener on the "transaction-committed" event.
|
|
3946
|
+
*/
|
|
3947
|
+
static onTransactionCommitted(listener, options) {
|
|
3948
|
+
this.listen("transaction-committed", listener, options);
|
|
3949
|
+
}
|
|
3950
|
+
/**
|
|
3951
|
+
* Register a listener on the "transaction-rolled-back" event.
|
|
3952
|
+
*/
|
|
3953
|
+
static onTransactionRolledBack(listener, options) {
|
|
3954
|
+
this.listen("transaction-rolled-back", listener, options);
|
|
3955
|
+
}
|
|
3956
|
+
/**
|
|
3957
|
+
* Register a listener on the "migrations-started" event.
|
|
3958
|
+
*/
|
|
3959
|
+
static onMigrationsStarted(listener, options) {
|
|
3960
|
+
this.listen("migrations-started", listener, options);
|
|
3961
|
+
}
|
|
3962
|
+
/**
|
|
3963
|
+
* Register a listener on the "migration-started" event.
|
|
3964
|
+
*/
|
|
3965
|
+
static onMigrationStarted(listener, options) {
|
|
3966
|
+
this.listen("migration-started", listener, options);
|
|
3967
|
+
}
|
|
3968
|
+
/**
|
|
3969
|
+
* Register a listener on the "migration-ended" event.
|
|
3970
|
+
*/
|
|
3971
|
+
static onMigrationEnded(listener, options) {
|
|
3972
|
+
this.listen("migration-ended", listener, options);
|
|
3973
|
+
}
|
|
3974
|
+
/**
|
|
3975
|
+
* Register a listener on the "migrations-ended" event.
|
|
3976
|
+
*/
|
|
3977
|
+
static onMigrationsEnded(listener, options) {
|
|
3978
|
+
this.listen("migrations-ended", listener, options);
|
|
3979
|
+
}
|
|
3980
|
+
/**
|
|
3981
|
+
* Register a listener on the "no-pending-migrations" event.
|
|
3982
|
+
*/
|
|
3983
|
+
static onNoPendingMigrations(listener, options) {
|
|
3984
|
+
this.listen("no-pending-migrations", listener, options);
|
|
3985
|
+
}
|
|
3986
|
+
/**
|
|
3987
|
+
* Register a listener on the "database-blocked" event.
|
|
3988
|
+
*/
|
|
3989
|
+
static onDatabaseBlocked(listener, options) {
|
|
3990
|
+
this.listen("database-blocked", listener, options);
|
|
3991
|
+
}
|
|
3992
|
+
/**
|
|
3993
|
+
* Register a listener on the "seeding-started" event.
|
|
3994
|
+
*/
|
|
3995
|
+
static onSeedingStarted(listener, options) {
|
|
3996
|
+
this.listen("seeding-started", listener, options);
|
|
3997
|
+
}
|
|
3998
|
+
/**
|
|
3999
|
+
* Register a listener on the "seeder-started" event.
|
|
4000
|
+
*/
|
|
4001
|
+
static onSeederStarted(listener, options) {
|
|
4002
|
+
this.listen("seeder-started", listener, options);
|
|
4003
|
+
}
|
|
4004
|
+
/**
|
|
4005
|
+
* Register a listener on the "seeder-ended" event.
|
|
4006
|
+
*/
|
|
4007
|
+
static onSeederEnded(listener, options) {
|
|
4008
|
+
this.listen("seeder-ended", listener, options);
|
|
4009
|
+
}
|
|
4010
|
+
/**
|
|
4011
|
+
* Register a listener on the "seeding-ended" event.
|
|
4012
|
+
*/
|
|
4013
|
+
static onSeedingEnded(listener, options) {
|
|
4014
|
+
this.listen("seeding-ended", listener, options);
|
|
4015
|
+
}
|
|
4016
|
+
/**
|
|
4017
|
+
* Start recording every query that runs.
|
|
4018
|
+
*/
|
|
4019
|
+
static enableQueryLog() {
|
|
4020
|
+
if (this.#logger !== null) {
|
|
4021
|
+
return;
|
|
4022
|
+
}
|
|
4023
|
+
this.#logger = (event) => {
|
|
4024
|
+
const query = event;
|
|
4025
|
+
this.#log.push({
|
|
4026
|
+
connection: query.connection,
|
|
4027
|
+
table: query.table,
|
|
4028
|
+
plan: query.plan,
|
|
4029
|
+
duration: query.duration,
|
|
4030
|
+
records: query.records
|
|
4031
|
+
});
|
|
4032
|
+
};
|
|
4033
|
+
Dispatcher.listen("db:query", this.#logger);
|
|
4034
|
+
}
|
|
4035
|
+
/**
|
|
4036
|
+
* Stop recording queries.
|
|
4037
|
+
*/
|
|
4038
|
+
static disableQueryLog() {
|
|
4039
|
+
if (this.#logger === null) {
|
|
4040
|
+
return;
|
|
4041
|
+
}
|
|
4042
|
+
Dispatcher.forget("db:query", this.#logger);
|
|
4043
|
+
this.#logger = null;
|
|
4044
|
+
}
|
|
4045
|
+
/**
|
|
4046
|
+
* Get the recorded queries.
|
|
4047
|
+
*/
|
|
4048
|
+
static getQueryLog() {
|
|
4049
|
+
return [...this.#log];
|
|
4050
|
+
}
|
|
4051
|
+
/**
|
|
4052
|
+
* Discard the recorded queries.
|
|
4053
|
+
*/
|
|
4054
|
+
static flushQueryLog() {
|
|
4055
|
+
this.#log = [];
|
|
4056
|
+
}
|
|
4057
|
+
/**
|
|
4058
|
+
* Determine whether queries are being recorded.
|
|
4059
|
+
*/
|
|
4060
|
+
static logging() {
|
|
4061
|
+
return this.#logger !== null;
|
|
4062
|
+
}
|
|
4063
|
+
};
|
|
4064
|
+
|
|
4065
|
+
// src/migrations/Migration.ts
|
|
4066
|
+
var Migration = class {
|
|
4067
|
+
/**
|
|
4068
|
+
* Get the name of the migration.
|
|
4069
|
+
*/
|
|
4070
|
+
name() {
|
|
4071
|
+
return this.constructor.name;
|
|
4072
|
+
}
|
|
4073
|
+
};
|
|
4074
|
+
|
|
4075
|
+
// src/schema/Schema.ts
|
|
4076
|
+
var Schema = class {
|
|
4077
|
+
/**
|
|
4078
|
+
* Get the table names the database layer reserves for itself.
|
|
4079
|
+
*/
|
|
4080
|
+
static reserved() {
|
|
4081
|
+
return [Repository.table, Registry.table];
|
|
4082
|
+
}
|
|
4083
|
+
/**
|
|
4084
|
+
* Get a connection to read schema information from.
|
|
4085
|
+
*/
|
|
4086
|
+
static connection(name) {
|
|
4087
|
+
return DatabaseManager.connection(name);
|
|
4088
|
+
}
|
|
4089
|
+
/**
|
|
4090
|
+
* Determine whether a table exists.
|
|
4091
|
+
*/
|
|
4092
|
+
static hasTable(table) {
|
|
4093
|
+
return this.connection().hasTable(table);
|
|
4094
|
+
}
|
|
4095
|
+
/**
|
|
4096
|
+
* Determine whether a table has a column.
|
|
4097
|
+
*/
|
|
4098
|
+
static hasColumn(table, column) {
|
|
4099
|
+
return this.connection().hasColumn(table, column);
|
|
4100
|
+
}
|
|
4101
|
+
/**
|
|
4102
|
+
* Get the names of every table.
|
|
4103
|
+
*/
|
|
4104
|
+
static getTables() {
|
|
4105
|
+
return this.connection().tables();
|
|
4106
|
+
}
|
|
4107
|
+
/**
|
|
4108
|
+
* Get the columns of a table.
|
|
4109
|
+
*/
|
|
4110
|
+
static getColumns(table) {
|
|
4111
|
+
return this.connection().getColumns(table);
|
|
4112
|
+
}
|
|
4113
|
+
/**
|
|
4114
|
+
* Get the indexes of a table.
|
|
4115
|
+
*/
|
|
4116
|
+
static getIndexes(table) {
|
|
4117
|
+
return this.connection().getIndexes(table);
|
|
4118
|
+
}
|
|
4119
|
+
/**
|
|
4120
|
+
* Create a table.
|
|
4121
|
+
*/
|
|
4122
|
+
static async create(table, callback) {
|
|
4123
|
+
const context = this.#context("create");
|
|
4124
|
+
this.#available(table);
|
|
4125
|
+
if (context.database.objectStoreNames.contains(table)) {
|
|
4126
|
+
throw new SchemaException(`Table [${table}] already exists.`);
|
|
4127
|
+
}
|
|
4128
|
+
const blueprint = new Blueprint(table);
|
|
4129
|
+
callback(blueprint);
|
|
4130
|
+
const schema = blueprint.toSchema();
|
|
4131
|
+
const store = context.database.createObjectStore(table, this.#options(schema));
|
|
4132
|
+
for (const index of schema.indexes) {
|
|
4133
|
+
this.#createIndex(store, index);
|
|
4134
|
+
}
|
|
4135
|
+
this.#record(context, schema);
|
|
4136
|
+
await Promise.resolve();
|
|
4137
|
+
}
|
|
4138
|
+
/**
|
|
4139
|
+
* Alter a table.
|
|
4140
|
+
*/
|
|
4141
|
+
static async table(table, callback) {
|
|
4142
|
+
const context = this.#context("table");
|
|
4143
|
+
const existing = this.#existing(context, table);
|
|
4144
|
+
const blueprint = new Blueprint(table, existing);
|
|
4145
|
+
callback(blueprint);
|
|
4146
|
+
const schema = blueprint.toSchema();
|
|
4147
|
+
const operations = blueprint.operations();
|
|
4148
|
+
this.#immovable(existing, operations);
|
|
4149
|
+
const store = context.transaction.objectStore(table);
|
|
4150
|
+
for (const name of operations.unindexed) {
|
|
4151
|
+
store.deleteIndex(name);
|
|
4152
|
+
}
|
|
4153
|
+
for (const index of operations.indexed) {
|
|
4154
|
+
this.#createIndex(store, index);
|
|
4155
|
+
}
|
|
4156
|
+
await this.#rewrite(store, operations);
|
|
4157
|
+
this.#record(context, schema);
|
|
4158
|
+
}
|
|
4159
|
+
/**
|
|
4160
|
+
* Drop a table.
|
|
4161
|
+
*/
|
|
4162
|
+
static async drop(table) {
|
|
4163
|
+
const context = this.#context("drop");
|
|
4164
|
+
this.#existing(context, table);
|
|
4165
|
+
this.#delete(context, table);
|
|
4166
|
+
await Promise.resolve();
|
|
4167
|
+
}
|
|
4168
|
+
/**
|
|
4169
|
+
* Drop a table, if it exists.
|
|
4170
|
+
*/
|
|
4171
|
+
static async dropIfExists(table) {
|
|
4172
|
+
const context = this.#context("dropIfExists");
|
|
4173
|
+
if (context.schemas.has(table)) {
|
|
4174
|
+
this.#delete(context, table);
|
|
4175
|
+
}
|
|
4176
|
+
await Promise.resolve();
|
|
4177
|
+
}
|
|
4178
|
+
/**
|
|
4179
|
+
* Rename a table, copying every record into the new one.
|
|
4180
|
+
*/
|
|
4181
|
+
static async rename(from, to) {
|
|
4182
|
+
const context = this.#context("rename");
|
|
4183
|
+
const schema = this.#existing(context, from);
|
|
4184
|
+
this.#available(to);
|
|
4185
|
+
if (context.database.objectStoreNames.contains(to)) {
|
|
4186
|
+
throw new SchemaException(`Table [${to}] already exists.`);
|
|
4187
|
+
}
|
|
4188
|
+
const target = context.database.createObjectStore(to, this.#options(schema));
|
|
4189
|
+
for (const index of schema.indexes) {
|
|
4190
|
+
this.#createIndex(target, index);
|
|
4191
|
+
}
|
|
4192
|
+
const source = context.transaction.objectStore(from);
|
|
4193
|
+
await Request.walk(source.openCursor(), (cursor) => {
|
|
4194
|
+
if (schema.key === null) {
|
|
4195
|
+
target.add(cursor.value, cursor.primaryKey);
|
|
4196
|
+
return;
|
|
4197
|
+
}
|
|
4198
|
+
target.add(cursor.value);
|
|
4199
|
+
});
|
|
4200
|
+
this.#delete(context, from);
|
|
4201
|
+
this.#record(context, { ...schema, table: to });
|
|
4202
|
+
}
|
|
4203
|
+
/**
|
|
4204
|
+
* Get the active migration context, or fail when there is none.
|
|
4205
|
+
*/
|
|
4206
|
+
static #context(operation) {
|
|
4207
|
+
if (Migrator.context() === null) {
|
|
4208
|
+
throw new SchemaException(`Schema.${operation}() may only be called inside a migration.`);
|
|
4209
|
+
}
|
|
4210
|
+
return Migrator.alive();
|
|
4211
|
+
}
|
|
4212
|
+
/**
|
|
4213
|
+
* Get the schema of a table that must already exist.
|
|
4214
|
+
*/
|
|
4215
|
+
static #existing(context, table) {
|
|
4216
|
+
const schema = context.schemas.get(table);
|
|
4217
|
+
if (schema === void 0) {
|
|
4218
|
+
throw new TableNotFoundException(table);
|
|
4219
|
+
}
|
|
4220
|
+
return schema;
|
|
4221
|
+
}
|
|
4222
|
+
/**
|
|
4223
|
+
* Assert the table name is not reserved by the database layer.
|
|
4224
|
+
*/
|
|
4225
|
+
static #available(table) {
|
|
4226
|
+
if (this.reserved().includes(table)) {
|
|
4227
|
+
throw new ReservedTableException(table);
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
/**
|
|
4231
|
+
* Assert the operations leave the key path of the table alone.
|
|
4232
|
+
*/
|
|
4233
|
+
static #immovable(existing, operations) {
|
|
4234
|
+
if (existing.key === null) {
|
|
4235
|
+
return;
|
|
4236
|
+
}
|
|
4237
|
+
const moved = operations.dropped.includes(existing.key) || operations.renamed.some((rename) => rename.from === existing.key);
|
|
4238
|
+
if (moved) {
|
|
4239
|
+
throw new SchemaException(`Column [${existing.key}] is the key path of table [${existing.table}] and may not be dropped or renamed.`);
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
/**
|
|
4243
|
+
* Get the store options describing the key of a table.
|
|
4244
|
+
*/
|
|
4245
|
+
static #options(schema) {
|
|
4246
|
+
if (schema.key === null) {
|
|
4247
|
+
return { autoIncrement: true };
|
|
4248
|
+
}
|
|
4249
|
+
return { keyPath: schema.key, autoIncrement: schema.increments };
|
|
4250
|
+
}
|
|
4251
|
+
/**
|
|
4252
|
+
* Create an index on a store.
|
|
4253
|
+
*/
|
|
4254
|
+
static #createIndex(store, index) {
|
|
4255
|
+
const path = index.columns.length === 1 ? index.columns[0] : index.columns;
|
|
4256
|
+
store.createIndex(index.name, path, { unique: index.unique, multiEntry: index.multiEntry });
|
|
4257
|
+
}
|
|
4258
|
+
/**
|
|
4259
|
+
* Rewrite every record of a store to match the applied operations.
|
|
4260
|
+
*/
|
|
4261
|
+
static async #rewrite(store, operations) {
|
|
4262
|
+
const backfill = operations.added.filter((column) => column.hasDefault);
|
|
4263
|
+
if (backfill.length === 0 && operations.dropped.length === 0 && operations.renamed.length === 0) {
|
|
4264
|
+
return;
|
|
4265
|
+
}
|
|
4266
|
+
await Request.walk(store.openCursor(), (cursor) => {
|
|
4267
|
+
const record = { ...cursor.value };
|
|
4268
|
+
for (const column of backfill) {
|
|
4269
|
+
if (!Object.hasOwn(record, column.name)) {
|
|
4270
|
+
record[column.name] = column.default;
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
for (const rename of operations.renamed) {
|
|
4274
|
+
record[rename.to] = record[rename.from];
|
|
4275
|
+
delete record[rename.from];
|
|
4276
|
+
}
|
|
4277
|
+
for (const column of operations.dropped) {
|
|
4278
|
+
delete record[column];
|
|
4279
|
+
}
|
|
4280
|
+
cursor.update(record);
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
4283
|
+
/**
|
|
4284
|
+
* Record the schema of a table, in the registry and the context cache.
|
|
4285
|
+
*/
|
|
4286
|
+
static #record(context, schema) {
|
|
4287
|
+
Registry.put(context.transaction, schema);
|
|
4288
|
+
context.schemas.set(schema.table, schema);
|
|
4289
|
+
}
|
|
4290
|
+
/**
|
|
4291
|
+
* Delete a table, from the database, the registry and the context cache.
|
|
4292
|
+
*/
|
|
4293
|
+
static #delete(context, table) {
|
|
4294
|
+
context.database.deleteObjectStore(table);
|
|
4295
|
+
Registry.forget(context.transaction, table);
|
|
4296
|
+
context.schemas.delete(table);
|
|
4297
|
+
}
|
|
4298
|
+
};
|
|
4299
|
+
|
|
4300
|
+
// src/seeders/Seeder.ts
|
|
4301
|
+
var Seeder = class {
|
|
4302
|
+
/**
|
|
4303
|
+
* Get the name of the seeder.
|
|
4304
|
+
*/
|
|
4305
|
+
name() {
|
|
4306
|
+
return this.constructor.name;
|
|
4307
|
+
}
|
|
4308
|
+
};
|
|
4309
|
+
export {
|
|
4310
|
+
Blueprint,
|
|
4311
|
+
Builder,
|
|
4312
|
+
CheckConstraintViolationException,
|
|
4313
|
+
ColumnDefinition,
|
|
4314
|
+
Connection,
|
|
4315
|
+
ConnectionNotConfiguredException,
|
|
4316
|
+
DatabaseManager as DB,
|
|
4317
|
+
DatabaseBlocked,
|
|
4318
|
+
DatabaseBlockedException,
|
|
4319
|
+
DatabaseManager,
|
|
4320
|
+
Grouping,
|
|
4321
|
+
Join,
|
|
4322
|
+
Migration,
|
|
4323
|
+
MigrationEnded,
|
|
4324
|
+
MigrationMismatchException,
|
|
4325
|
+
MigrationStarted,
|
|
4326
|
+
MigrationTransactionClosedException,
|
|
4327
|
+
MigrationsEnded,
|
|
4328
|
+
MigrationsStarted,
|
|
4329
|
+
MultipleRecordsFoundException,
|
|
4330
|
+
NoPendingMigrations,
|
|
4331
|
+
NotNullConstraintViolationException,
|
|
4332
|
+
QueryExecuted,
|
|
4333
|
+
QuotaExceededException,
|
|
4334
|
+
RecordsNotFoundException,
|
|
4335
|
+
ReservedTableException,
|
|
4336
|
+
Schema,
|
|
4337
|
+
SchemaException,
|
|
4338
|
+
Seeder,
|
|
4339
|
+
SeederEnded,
|
|
4340
|
+
SeederStarted,
|
|
4341
|
+
SeedingEnded,
|
|
4342
|
+
SeedingStarted,
|
|
4343
|
+
TableNotFoundException,
|
|
4344
|
+
Transaction,
|
|
4345
|
+
TransactionBeginning,
|
|
4346
|
+
TransactionCommitted,
|
|
4347
|
+
TransactionRolledBack,
|
|
4348
|
+
UniqueConstraintViolationException
|
|
4349
|
+
};
|