uql-orm 0.55.0 → 0.57.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/README.md +1 -1
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/bunSql/bunSql.util.js +1 -1
- package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
- package/dist/cockroachdb/cockroachDialect.js +2 -10
- package/dist/d1/d1SqliteDialect.d.ts +1 -0
- package/dist/d1/d1SqliteDialect.js +2 -0
- package/dist/dialect/abstractSqlDialect.d.ts +195 -32
- package/dist/dialect/abstractSqlDialect.js +406 -199
- package/dist/dialect/aliases.d.ts +10 -7
- package/dist/dialect/aliases.js +12 -7
- package/dist/dialect/hydrateColumn.d.ts +8 -2
- package/dist/dialect/hydrateColumn.js +33 -1
- package/dist/dialect/jsonSql.d.ts +13 -5
- package/dist/dialect/jsonSql.js +24 -7
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +30 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +58 -7
- package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
- package/dist/dialect/pgLikeSqlDialect.js +25 -50
- package/dist/dialect/pgVectorMetrics.d.ts +13 -0
- package/dist/dialect/pgVectorMetrics.js +17 -0
- package/dist/dialect/queryContext.d.ts +3 -7
- package/dist/dialect/queryContext.js +13 -8
- package/dist/dialect/queryJoins.d.ts +8 -4
- package/dist/dialect/queryJoins.js +27 -15
- package/dist/dialect/vectorSqlDialect.d.ts +2 -2
- package/dist/dialect/vectorSqlDialect.js +2 -3
- package/dist/entity/index.d.ts +1 -1
- package/dist/entity/index.js +1 -1
- package/dist/entity/metadata/definition.d.ts +4 -2
- package/dist/entity/metadata/definition.js +27 -29
- package/dist/maria/mariaDialect.d.ts +13 -6
- package/dist/maria/mariaDialect.js +29 -9
- package/dist/migrate/builder/splitSqlStatements.js +2 -2
- package/dist/migrate/cli.d.ts +2 -3
- package/dist/migrate/cli.js +4 -11
- package/dist/migrate/codegen/fieldOptionsSource.js +1 -1
- package/dist/migrate/ddl/index.d.ts +1 -5
- package/dist/migrate/ddl/index.js +14 -25
- package/dist/migrate/ddl/indexDdl.d.ts +11 -2
- package/dist/migrate/ddl/indexDdl.js +17 -1
- package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
- package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
- package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
- package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -3
- package/dist/migrate/ddl/pgIndexDdl.js +29 -3
- package/dist/migrate/drift/driftDetector.js +21 -8
- package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
- package/dist/migrate/introspection/baseSqlIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/baseSqlIntrospector.js +65 -76
- package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
- package/dist/migrate/introspection/mysqlIntrospector.js +5 -8
- package/dist/migrate/introspection/sqliteIntrospector.js +2 -5
- package/dist/migrate/migrator.d.ts +7 -4
- package/dist/migrate/migrator.js +9 -14
- package/dist/migrate/schemaGenerator.d.ts +3 -3
- package/dist/migrate/schemaGenerator.js +7 -13
- package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
- package/dist/mongo/mongoDialect.d.ts +31 -18
- package/dist/mongo/mongoDialect.js +147 -108
- package/dist/mongo/mongodbQuerier.d.ts +10 -17
- package/dist/mongo/mongodbQuerier.js +34 -108
- package/dist/mssql/mssqlDialect.d.ts +16 -0
- package/dist/mssql/mssqlDialect.js +26 -4
- package/dist/mysql/mysqlDialect.d.ts +2 -0
- package/dist/mysql/mysqlDialect.js +4 -0
- package/dist/querier/abstractQuerier.d.ts +20 -36
- package/dist/querier/abstractQuerier.js +44 -143
- package/dist/querier/abstractQuerierPool.d.ts +2 -2
- package/dist/querier/abstractSqlQuerier.d.ts +11 -22
- package/dist/querier/abstractSqlQuerier.js +49 -53
- package/dist/schema/canonicalType.js +4 -6
- package/dist/schema/dependencyGraph.js +2 -4
- package/dist/schema/indexDifferences.js +5 -5
- package/dist/schema/schemaASTBuilder.js +34 -12
- package/dist/schema/schemaASTDiffer.d.ts +10 -2
- package/dist/schema/schemaASTDiffer.js +17 -16
- package/dist/schema/types.d.ts +1 -1
- package/dist/sqlite/sqliteDialect.d.ts +20 -1
- package/dist/sqlite/sqliteDialect.js +40 -8
- package/dist/turso/tursoDialect.d.ts +2 -0
- package/dist/turso/tursoDialect.js +2 -0
- package/dist/type/config.d.ts +2 -2
- package/dist/type/dialect.d.ts +4 -5
- package/dist/type/entity.d.ts +2 -1
- package/dist/type/migratorDialect.d.ts +4 -0
- package/dist/type/querier.d.ts +6 -6
- package/dist/type/query.d.ts +25 -48
- package/dist/type/query.js +10 -5
- package/dist/type/queryAggregate.d.ts +10 -10
- package/dist/type/queryAggregate.js +1 -1
- package/dist/type/universalQuerier.d.ts +4 -4
- package/dist/util/dialect.util.d.ts +8 -2
- package/dist/util/dialect.util.js +19 -0
- package/dist/util/field.util.d.ts +5 -0
- package/dist/util/field.util.js +19 -0
- package/dist/util/logger.d.ts +10 -1
- package/dist/util/logger.js +18 -0
- package/dist/util/object.util.d.ts +4 -0
- package/dist/util/object.util.js +8 -0
- package/dist/util/relationQuery.util.d.ts +15 -68
- package/dist/util/relationQuery.util.js +35 -83
- package/dist/util/rowKey.util.d.ts +1 -11
- package/dist/util/rowKey.util.js +1 -13
- package/package.json +1 -1
- package/dist/querier/relationCount.d.ts +0 -16
- package/dist/querier/relationCount.js +0 -121
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { assertSoleId, getMeta, idOf, namesKey, soleIdOf } from '../entity/index.js';
|
|
2
|
-
import {
|
|
1
|
+
import { assertSoleId, getMeta, idOf, namesKey, relationOf, soleIdOf } from '../entity/index.js';
|
|
2
|
+
import { childrenOf, clone, entityName, fillOnFields, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, LoggerWrapper, parentJoins, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
|
-
import { fillRelationCounts, withIdForCounts } from './relationCount.js';
|
|
5
4
|
/**
|
|
6
5
|
* Rejects a nullish primary key before it reaches a statement. The by-id methods reduce to
|
|
7
6
|
* `{ $where: { id } }`, and a key compared to `undefined` is *no filter*, so an unchecked one
|
|
@@ -78,13 +77,10 @@ export class AbstractQuerier {
|
|
|
78
77
|
logger;
|
|
79
78
|
constructor(extra) {
|
|
80
79
|
this.extra = extra;
|
|
81
|
-
this.logger =
|
|
82
|
-
logValues: extra?.logValues,
|
|
83
|
-
slowQuery: extra?.slowQuery,
|
|
84
|
-
});
|
|
80
|
+
this.logger = queryLoggerFor(extra);
|
|
85
81
|
}
|
|
86
82
|
validateProjectionQuery(entity, q) {
|
|
87
|
-
this.validateProjectionQueryRecursive(entity, q, getMeta(entity)
|
|
83
|
+
this.validateProjectionQueryRecursive(entity, q, entityName(getMeta(entity)));
|
|
88
84
|
}
|
|
89
85
|
validateProjectionQueryRecursive(entity, q, path) {
|
|
90
86
|
const meta = getMeta(entity);
|
|
@@ -96,9 +92,7 @@ export class AbstractQuerier {
|
|
|
96
92
|
}
|
|
97
93
|
}
|
|
98
94
|
forEachRequestedRelation(meta, q.$populate, (relKey, relValue) => {
|
|
99
|
-
const relOpts = meta
|
|
100
|
-
if (!relOpts)
|
|
101
|
-
return;
|
|
95
|
+
const relOpts = relationOf(meta, relKey);
|
|
102
96
|
const relEntity = relOpts.entity();
|
|
103
97
|
const parsed = parseRelationQueryValue(relValue);
|
|
104
98
|
if (parsed.nested) {
|
|
@@ -135,9 +129,12 @@ export class AbstractQuerier {
|
|
|
135
129
|
async findMany(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
136
130
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
|
|
137
131
|
this.validateProjectionQuery(entity, q);
|
|
138
|
-
const founds = await this.internalFindMany(entity,
|
|
139
|
-
|
|
140
|
-
|
|
132
|
+
const founds = await this.internalFindMany(entity, q, opts);
|
|
133
|
+
// Guarded here rather than only inside: awaiting a call that returns at once still costs every read
|
|
134
|
+
// a promise and a turn of the microtask queue, and most reads hook nothing.
|
|
135
|
+
if (this.listensForLoad(entity, q.$populate)) {
|
|
136
|
+
await this.emitLoaded(entity, founds, q.$populate);
|
|
137
|
+
}
|
|
141
138
|
return founds;
|
|
142
139
|
}
|
|
143
140
|
findManyStream(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
@@ -148,9 +145,10 @@ export class AbstractQuerier {
|
|
|
148
145
|
async findManyAndCount(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
149
146
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
|
|
150
147
|
this.validateProjectionQuery(entity, q);
|
|
151
|
-
const [founds, count] = await this.internalFindManyAndCount(entity,
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
const [founds, count] = await this.internalFindManyAndCount(entity, q, opts);
|
|
149
|
+
if (this.listensForLoad(entity, q.$populate)) {
|
|
150
|
+
await this.emitLoaded(entity, founds, q.$populate);
|
|
151
|
+
}
|
|
154
152
|
return [founds, count];
|
|
155
153
|
}
|
|
156
154
|
/**
|
|
@@ -341,125 +339,6 @@ export class AbstractQuerier {
|
|
|
341
339
|
await (toInsert.length && toUpsert.length ? this.transaction(write) : write());
|
|
342
340
|
return ids;
|
|
343
341
|
}
|
|
344
|
-
async fillToManyRelations(entity, payload, populate) {
|
|
345
|
-
if (!payload.length) {
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
const meta = getMeta(entity);
|
|
349
|
-
const relKeys = getRelationRequestSummary(meta, populate).toManyKeys;
|
|
350
|
-
for (const relKey of relKeys) {
|
|
351
|
-
const relOpts = meta.relations[relKey];
|
|
352
|
-
if (!relOpts)
|
|
353
|
-
continue;
|
|
354
|
-
const relEntity = relOpts.entity();
|
|
355
|
-
const relationQuery = clone(parseRelationAtKey(relKey, populate).query);
|
|
356
|
-
if (relOpts.through) {
|
|
357
|
-
await this.fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery);
|
|
358
|
-
}
|
|
359
|
-
else if (relOpts.cardinality === '1m') {
|
|
360
|
-
await this.fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
async fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery) {
|
|
365
|
-
const joins = parentJoins(relOpts, meta.ids.length);
|
|
366
|
-
const [targetColumn] = targetKeyColumns(relOpts, meta.ids.length);
|
|
367
|
-
const throughEntity = relOpts.through();
|
|
368
|
-
const throughMeta = getMeta(throughEntity);
|
|
369
|
-
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === targetColumn));
|
|
370
|
-
if (!targetRelKey) {
|
|
371
|
-
// Asserted rather than assumed: used as a key regardless, it spells the literal string
|
|
372
|
-
// `undefined`, and the statement asks the junction for a relation of that name.
|
|
373
|
-
throw new TypeError(`'${meta.name}.${relKey}' goes through '${throughMeta.name}', which declares no relation on its ` +
|
|
374
|
-
`'${targetColumn}' column. Give it one, so the target's rows can be read through it.`);
|
|
375
|
-
}
|
|
376
|
-
// A relation query names the target's columns, not the junction's, so its projection and filter
|
|
377
|
-
// belong on the populate below, resolved against the entity that has them. Spread onto the
|
|
378
|
-
// junction query instead they asked `ItemTag` for `Tag`'s columns and failed with "no such
|
|
379
|
-
// column".
|
|
380
|
-
//
|
|
381
|
-
// Ordering and paging split the other way: they describe the statement with one row per pairing,
|
|
382
|
-
// which is the junction's. Left on the populate they reached a to-one join, which rejects all
|
|
383
|
-
// four by name - so a many-to-many carrying any of them threw rather than paging.
|
|
384
|
-
//
|
|
385
|
-
// Those four are not a coincidence: they are exactly the clauses a joined relation rejects, for
|
|
386
|
-
// the same reason - each needs a statement with many rows per parent, which only the junction's
|
|
387
|
-
// is. The `satisfies` ties the two lists together, so a fifth clause added there fails to compile
|
|
388
|
-
// here rather than quietly staying on the populate and throwing again.
|
|
389
|
-
const { $sort, $limit, $skip, $distinct, ...targetQuery } = relationQuery;
|
|
390
|
-
const junctionClauses = {
|
|
391
|
-
$limit,
|
|
392
|
-
$skip,
|
|
393
|
-
$distinct,
|
|
394
|
-
// Qualified by the relation that reaches them, since the columns it names are the target's.
|
|
395
|
-
$sort: $sort && { [targetRelKey]: $sort },
|
|
396
|
-
};
|
|
397
|
-
const junctionQuery = {
|
|
398
|
-
$select: joinedColumns(joins),
|
|
399
|
-
...junctionClauses,
|
|
400
|
-
$populate: {
|
|
401
|
-
[targetRelKey]: {
|
|
402
|
-
...targetQuery,
|
|
403
|
-
$required: true,
|
|
404
|
-
},
|
|
405
|
-
},
|
|
406
|
-
};
|
|
407
|
-
const throughFounds = await this.findChildrenOf(throughEntity, junctionQuery, joins, payload, meta.fields);
|
|
408
|
-
// The junction's own columns carried onto the target's row, which is where `putChildrenInParents`
|
|
409
|
-
// reads them back from - a junction row holds the parent's key under `joined`, not under `parent`.
|
|
410
|
-
const founds = throughFounds.map((it) => ({
|
|
411
|
-
...it[targetRelKey],
|
|
412
|
-
...Object.fromEntries(joins.map(({ joined }) => [joined, it[joined]])),
|
|
413
|
-
}));
|
|
414
|
-
this.putChildrenInParents(payload, founds, joins, relKey);
|
|
415
|
-
}
|
|
416
|
-
async fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity) {
|
|
417
|
-
const joins = parentJoins(relOpts, meta.ids.length);
|
|
418
|
-
// The FK is what putChildrenInParents groups on, so it outlives the relation's projection
|
|
419
|
-
// either way: added to a whitelisting `$select` (the raw-array form has nothing to augment),
|
|
420
|
-
// dropped from a subtractive `$exclude`. `relationQuery` is already a clone.
|
|
421
|
-
const select = asSelectMap(relationQuery.$select);
|
|
422
|
-
const exclude = relationQuery.$exclude;
|
|
423
|
-
for (const { joined } of joins) {
|
|
424
|
-
if (select && !select[joined]) {
|
|
425
|
-
select[joined] = true;
|
|
426
|
-
}
|
|
427
|
-
delete exclude?.[joined];
|
|
428
|
-
}
|
|
429
|
-
this.putChildrenInParents(payload, await this.findChildrenOf(relEntity, relationQuery, joins, payload, meta.fields), joins, relKey);
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* The children of a whole page of parents, however the relation asked for them: one bounded branch
|
|
433
|
-
* per parent when it wants a share of its own, otherwise a single flat statement over an `IN (...)`
|
|
434
|
-
* list, which is both correct and cheaper.
|
|
435
|
-
*
|
|
436
|
-
* The one place that decision is made - a one-to-many and the junction of a many-to-many differ in
|
|
437
|
-
* what they query, never in how the page is spread over its parents.
|
|
438
|
-
*/
|
|
439
|
-
async findChildrenOf(entity, query, joins, parents, parentFields) {
|
|
440
|
-
const founds = isBoundedPerParent(query)
|
|
441
|
-
? await this.internalFindManyPerParent(entity, query, { joins, parents, parentFields })
|
|
442
|
-
: await this.findMany(entity, queryChildrenOfAll(query, joins, parents));
|
|
443
|
-
// Read back as rows rather than as the entity they hydrate to: what follows regroups them by the
|
|
444
|
-
// join columns, which a projected entity type does not carry.
|
|
445
|
-
return founds;
|
|
446
|
-
}
|
|
447
|
-
putChildrenInParents(parents, children, joins, relKey) {
|
|
448
|
-
const childrenByParentId = dataKeyed();
|
|
449
|
-
// Every joined column, so two children agreeing on one column of a composite key are not
|
|
450
|
-
// gathered under the same parent. Both column lists are read once, not once per row.
|
|
451
|
-
const joinedKeys = keyColumns(joins, 'joined');
|
|
452
|
-
const parentKeys = keyColumns(joins, 'parent');
|
|
453
|
-
for (const child of children) {
|
|
454
|
-
(childrenByParentId[rowKey(child, joinedKeys)] ??= []).push(child);
|
|
455
|
-
}
|
|
456
|
-
for (const parent of parents) {
|
|
457
|
-
// `[]` rather than nothing for a parent with no children: a populated to-many is a list the
|
|
458
|
-
// caller asked for, so it maps and counts without a guard, and its type can say so. An
|
|
459
|
-
// unpopulated one stays absent, which is what tells the two apart.
|
|
460
|
-
parent[relKey] = (childrenByParentId[rowKey(parent, parentKeys)] ?? []);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
342
|
async insertRelations(entity, payload) {
|
|
464
343
|
const meta = getMeta(entity);
|
|
465
344
|
const entries = payload.reduce((acc, it) => {
|
|
@@ -498,9 +377,7 @@ export class AbstractQuerier {
|
|
|
498
377
|
const relKeys = filterPersistableRelationKeys(meta, meta.relations, 'delete');
|
|
499
378
|
// Cascade forwards `opts` (including `hardDelete`); each child soft-deletes only if it can.
|
|
500
379
|
for (const relKey of relKeys) {
|
|
501
|
-
const relOpts = meta
|
|
502
|
-
if (!relOpts)
|
|
503
|
-
continue;
|
|
380
|
+
const relOpts = relationOf(meta, relKey);
|
|
504
381
|
const relEntity = relOpts.entity();
|
|
505
382
|
const target = relOpts.through ? relOpts.through() : relEntity;
|
|
506
383
|
const where = childrenOf(parentJoins(relOpts, meta.ids.length), ids);
|
|
@@ -514,9 +391,7 @@ export class AbstractQuerier {
|
|
|
514
391
|
*/
|
|
515
392
|
async saveRelation(entity, ids, relValue, relKey, isUpdate) {
|
|
516
393
|
const meta = getMeta(entity);
|
|
517
|
-
const relOpts = meta
|
|
518
|
-
if (!relOpts)
|
|
519
|
-
return;
|
|
394
|
+
const relOpts = relationOf(meta, relKey);
|
|
520
395
|
// Here rather than only in the callers below: writing the parent's key into a child is one column
|
|
521
396
|
// per key, so a composite takes a statement per parent. `soleParentColumn` and the sole
|
|
522
397
|
// `targetKeyColumns` under it read the *first* pair, which is a real column of a wrong pairing
|
|
@@ -630,6 +505,32 @@ export class AbstractQuerier {
|
|
|
630
505
|
hasHook(entity, event) {
|
|
631
506
|
return (this.extra?.listeners?.some((listener) => listener[event]) || (getMeta(entity).hooks?.[event]?.length ?? 0) > 0);
|
|
632
507
|
}
|
|
508
|
+
/** Whether an `afterLoad` listens on the entity a read returns or on any relation it populated. */
|
|
509
|
+
listensForLoad(entity, populate) {
|
|
510
|
+
if (this.hasHook(entity, 'afterLoad')) {
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
const meta = getMeta(entity);
|
|
514
|
+
return getRelationRequestSummary(meta, populate).requestedKeys.some((relKey) => this.listensForLoad(relationOf(meta, relKey).entity(), parseRelationAtKey(relKey, populate).query.$populate));
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* `afterLoad` for every row a read loaded, a populated relation's before the rows holding them, so a
|
|
518
|
+
* parent's hook sees its children as their own hooks left them. Rows are walked only where a hook
|
|
519
|
+
* listens.
|
|
520
|
+
*/
|
|
521
|
+
async emitLoaded(entity, rows, populate) {
|
|
522
|
+
const meta = getMeta(entity);
|
|
523
|
+
for (const relKey of getRelationRequestSummary(meta, populate).requestedKeys) {
|
|
524
|
+
const relEntity = relationOf(meta, relKey).entity();
|
|
525
|
+
const relPopulate = parseRelationAtKey(relKey, populate).query.$populate;
|
|
526
|
+
if (this.listensForLoad(relEntity, relPopulate)) {
|
|
527
|
+
// A to-many holds a list and a to-one a row, which `flatMap` takes alike; an absent one adds none.
|
|
528
|
+
const loaded = rows.flatMap((row) => row[relKey] ?? []);
|
|
529
|
+
await this.emitLoaded(relEntity, loaded, relPopulate);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
await this.emitHook(entity, 'afterLoad', rows);
|
|
533
|
+
}
|
|
633
534
|
/**
|
|
634
535
|
* The ids of `rows`, read back by the columns an upsert matched them on, for a statement that could
|
|
635
536
|
* not report them in payload order. A row that no read row matches, or that two do, keeps
|
|
@@ -684,7 +585,7 @@ export class AbstractQuerier {
|
|
|
684
585
|
* another one would wait for a task queued behind itself. Callers below keep their `serialize` calls
|
|
685
586
|
* sequential rather than nested.
|
|
686
587
|
*/
|
|
687
|
-
|
|
588
|
+
serialize(task) {
|
|
688
589
|
const res = this.taskQueue.then(task);
|
|
689
590
|
this.taskQueue = res.catch(() => { });
|
|
690
591
|
return res;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AbstractDialect } from '../dialect/index.js';
|
|
2
|
-
import type { EntityData, EntityId, ExtraOptions, FieldKey, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch,
|
|
2
|
+
import type { EntityData, EntityId, ExtraOptions, FieldKey, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Base pool: dialect id and behavior come only from the `dialect` instance (see {@link QuerierPool}).
|
|
5
5
|
*/
|
|
@@ -31,7 +31,7 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
|
|
|
31
31
|
* The connection outlives the call here: it is held until the iterator is drained or closed by a
|
|
32
32
|
* `break`/`throw`. Abandoning the iterator instead leaks it until GC, so consume it in a `for await`.
|
|
33
33
|
*/
|
|
34
|
-
findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q:
|
|
34
|
+
findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): AsyncGenerator<QueryFindResult<E, S, V, X, P, C>>;
|
|
35
35
|
findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P, C>[], number]>;
|
|
36
36
|
count<E extends object>(entity: Type<E>, q?: QueryPage<E>, opts?: QueryOptions): Promise<number>;
|
|
37
37
|
exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<boolean>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
2
|
import type { EntityData, ExtraOptions, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
|
-
import { type ParentPartition } from '../util/index.js';
|
|
4
3
|
import type { BuildUpdateResultPayload } from '../util/sql.util.js';
|
|
5
4
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
6
5
|
export declare abstract class AbstractSqlQuerier extends AbstractQuerier implements SqlQuerier {
|
|
@@ -58,19 +57,6 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
58
57
|
*/
|
|
59
58
|
private applyVectorTuning;
|
|
60
59
|
protected internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
61
|
-
/**
|
|
62
|
-
* One bounded subquery per parent, concatenated with `UNION ALL`, so each parent gets its own
|
|
63
|
-
* `$limit` rather than a share of one. Universal, and reads `parents x (skip + limit)` rows where a
|
|
64
|
-
* `ROW_NUMBER` window reads every matching child. [The design](../../../../architecture/populate-limits.md).
|
|
65
|
-
*
|
|
66
|
-
* Each branch is a wrapped derived table: SQLite rejects `ORDER BY`/`LIMIT` on a bare parenthesised
|
|
67
|
-
* compound branch, and the wrapper costs nothing elsewhere.
|
|
68
|
-
*
|
|
69
|
-
* Unlike {@link selectRows} this asserts no lock and tunes no vector search: `$lock` and
|
|
70
|
-
* `$candidates` describe the statement, and `parseRelationQueryValue` refuses both on a relation
|
|
71
|
-
* query, so neither can reach here.
|
|
72
|
-
*/
|
|
73
|
-
protected internalFindManyPerParent<E extends object>(entity: Type<E>, q: Query<E>, partition: ParentPartition): Promise<E[]>;
|
|
74
60
|
/**
|
|
75
61
|
* How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
|
|
76
62
|
* `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
|
|
@@ -100,14 +86,17 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
100
86
|
*/
|
|
101
87
|
protected internalStream<T>(query: string, values?: unknown[]): AsyncIterable<T>;
|
|
102
88
|
/**
|
|
103
|
-
* Turn what a driver returned back into the types the entity declares, for
|
|
104
|
-
* populated under
|
|
105
|
-
* `decodeColumn`. Both live with the dialect, because a `sparsevec` is
|
|
106
|
-
*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
*
|
|
89
|
+
* Turn what a driver returned back into the types the entity declares, for every row and everything
|
|
90
|
+
* populated under them. Which columns, and as what, is `hydratableFields`, resolved once for all the
|
|
91
|
+
* rows; the per-cell decode is `decodeColumn`. Both live with the dialect, because a `sparsevec` is
|
|
92
|
+
* only sparse on Postgres.
|
|
93
|
+
*/
|
|
94
|
+
private hydrateAll;
|
|
95
|
+
/**
|
|
96
|
+
* One row of {@link hydrateAll}. A related row arrives as its parent's statement read it: a to-one
|
|
97
|
+
* joined and unflattened, there only when its key is, since an unmatched join still fills a computed
|
|
98
|
+
* column or a to-many's empty array; a to-many as a JSON array, which a driver may hand over as text.
|
|
99
|
+
* Each is an object of its own, so the walk reaches none twice.
|
|
111
100
|
*/
|
|
112
101
|
private hydrateFields;
|
|
113
102
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { COUNT_ALIAS, TOTAL_ALIAS } from '../dialect/aliases.js';
|
|
2
2
|
import { decodeColumn } from '../dialect/hydrateColumn.js';
|
|
3
3
|
import { getMeta, idOf, namesKey } from '../entity/index.js';
|
|
4
|
-
import {
|
|
4
|
+
import { COUNT_RESULT_KEY } from '../type/index.js';
|
|
5
|
+
import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, insertShapeOf, idOnlyQuery, isAutoIncrement, isPagedQuery, isRecord, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
6
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
6
7
|
import { enrichError } from './queryError.js';
|
|
7
8
|
/**
|
|
@@ -153,24 +154,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
async internalFindMany(entity, q, opts) {
|
|
156
|
-
return this.hydrateRows(entity,
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* One bounded subquery per parent, concatenated with `UNION ALL`, so each parent gets its own
|
|
160
|
-
* `$limit` rather than a share of one. Universal, and reads `parents x (skip + limit)` rows where a
|
|
161
|
-
* `ROW_NUMBER` window reads every matching child. [The design](../../../../architecture/populate-limits.md).
|
|
162
|
-
*
|
|
163
|
-
* Each branch is a wrapped derived table: SQLite rejects `ORDER BY`/`LIMIT` on a bare parenthesised
|
|
164
|
-
* compound branch, and the wrapper costs nothing elsewhere.
|
|
165
|
-
*
|
|
166
|
-
* Unlike {@link selectRows} this asserts no lock and tunes no vector search: `$lock` and
|
|
167
|
-
* `$candidates` describe the statement, and `parseRelationQueryValue` refuses both on a relation
|
|
168
|
-
* query, so neither can reach here.
|
|
169
|
-
*/
|
|
170
|
-
async internalFindManyPerParent(entity, q, partition) {
|
|
171
|
-
const ctx = this.dialect.createContext();
|
|
172
|
-
this.dialect.findPerParent(ctx, entity, q, partition);
|
|
173
|
-
return this.hydrateRows(entity, q, await this.all(ctx.sql, ctx.values));
|
|
157
|
+
return this.hydrateRows(entity, await this.selectRows(entity, q, opts));
|
|
174
158
|
}
|
|
175
159
|
/**
|
|
176
160
|
* How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
|
|
@@ -208,7 +192,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
208
192
|
for (const row of rows) {
|
|
209
193
|
delete row[TOTAL_ALIAS];
|
|
210
194
|
}
|
|
211
|
-
return [
|
|
195
|
+
return [this.hydrateRows(entity, rows), total];
|
|
212
196
|
}
|
|
213
197
|
async selectRows(entity, q, opts, totalAlias) {
|
|
214
198
|
this.assertLockable(entity, q);
|
|
@@ -222,9 +206,9 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
222
206
|
this.dialect.find(ctx, entity, q, opts, totalAlias);
|
|
223
207
|
return this.all(ctx.sql, ctx.values);
|
|
224
208
|
}
|
|
225
|
-
|
|
226
|
-
const founds = unflatObjects(rows)
|
|
227
|
-
|
|
209
|
+
hydrateRows(entity, rows) {
|
|
210
|
+
const founds = unflatObjects(rows);
|
|
211
|
+
this.hydrateAll(entity, founds);
|
|
228
212
|
return founds;
|
|
229
213
|
}
|
|
230
214
|
async *internalFindManyStream(entity, q, opts) {
|
|
@@ -234,10 +218,6 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
234
218
|
await this.applyVectorTuning(entity, q);
|
|
235
219
|
}
|
|
236
220
|
const meta = getMeta(entity);
|
|
237
|
-
const { toManyKeys } = getRelationRequestSummary(meta, q.$populate);
|
|
238
|
-
if (toManyKeys.length) {
|
|
239
|
-
throw new TypeError(`findManyStream does not load to-many relations (${toManyKeys.join(', ')}). Use findMany so fillToManyRelations can run, or omit those keys from the stream query.`);
|
|
240
|
-
}
|
|
241
221
|
// The one path that does not go through `all`/`run`, so it connects on its own: streaming first on
|
|
242
222
|
// a freshly acquired querier used to reach `getConn()` with nothing acquired.
|
|
243
223
|
await this.lazyConnect();
|
|
@@ -245,11 +225,14 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
245
225
|
// context holds was normalized as it was bound.
|
|
246
226
|
const ctx = this.dialect.createContext();
|
|
247
227
|
this.dialect.find(ctx, entity, q, opts);
|
|
228
|
+
const fields = this.dialect.hydratableFields(entity);
|
|
248
229
|
let attrsPaths;
|
|
249
230
|
try {
|
|
250
231
|
for await (const row of this.internalStream(ctx.sql, ctx.values)) {
|
|
251
232
|
attrsPaths ??= obtainAttrsPaths(row);
|
|
252
|
-
|
|
233
|
+
const found = unflatObject(row, attrsPaths);
|
|
234
|
+
this.hydrateFields(meta, fields, found);
|
|
235
|
+
yield found;
|
|
253
236
|
}
|
|
254
237
|
}
|
|
255
238
|
catch (err) {
|
|
@@ -265,53 +248,66 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
265
248
|
yield* await this.internalAll(query, values);
|
|
266
249
|
}
|
|
267
250
|
/**
|
|
268
|
-
* Turn what a driver returned back into the types the entity declares, for
|
|
269
|
-
* populated under
|
|
270
|
-
* `decodeColumn`. Both live with the dialect, because a `sparsevec` is
|
|
271
|
-
*
|
|
272
|
-
* `visited` guards a populated graph that points back at itself, and makes a node two paths reach
|
|
273
|
-
* decode once. Only a relation can lead the walk back somewhere it has been, so an entity that
|
|
274
|
-
* declares none skips the guard rather than allocating a set per row to hold a single object -
|
|
275
|
-
* which cost more than the decoding it guards, on a flat read.
|
|
251
|
+
* Turn what a driver returned back into the types the entity declares, for every row and everything
|
|
252
|
+
* populated under them. Which columns, and as what, is `hydratableFields`, resolved once for all the
|
|
253
|
+
* rows; the per-cell decode is `decodeColumn`. Both live with the dialect, because a `sparsevec` is
|
|
254
|
+
* only sparse on Postgres.
|
|
276
255
|
*/
|
|
277
|
-
|
|
278
|
-
if (!dto || typeof dto !== 'object' || visited?.has(dto)) {
|
|
279
|
-
return dto;
|
|
280
|
-
}
|
|
256
|
+
hydrateAll(entity, dtos) {
|
|
281
257
|
const meta = getMeta(entity);
|
|
258
|
+
const fields = this.dialect.hydratableFields(entity);
|
|
259
|
+
for (const dto of dtos) {
|
|
260
|
+
this.hydrateFields(meta, fields, dto);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* One row of {@link hydrateAll}. A related row arrives as its parent's statement read it: a to-one
|
|
265
|
+
* joined and unflattened, there only when its key is, since an unmatched join still fills a computed
|
|
266
|
+
* column or a to-many's empty array; a to-many as a JSON array, which a driver may hand over as text.
|
|
267
|
+
* Each is an object of its own, so the walk reaches none twice.
|
|
268
|
+
*/
|
|
269
|
+
hydrateFields(meta, fields, dto) {
|
|
282
270
|
const row = dto;
|
|
283
|
-
for (const [key, kind] of
|
|
271
|
+
for (const [key, kind] of fields) {
|
|
284
272
|
const value = row[key];
|
|
285
273
|
if (value != null) {
|
|
286
274
|
row[key] = decodeColumn(value, kind);
|
|
287
275
|
}
|
|
288
276
|
}
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
|
|
277
|
+
// A tally read inside the statement comes back as its driver reads a COUNT, which on some is text.
|
|
278
|
+
const counts = row[COUNT_RESULT_KEY];
|
|
279
|
+
if (isRecord(counts)) {
|
|
280
|
+
for (const relKey in counts) {
|
|
281
|
+
counts[relKey] = Number(counts[relKey]);
|
|
282
|
+
}
|
|
293
283
|
}
|
|
294
|
-
visited?.add(dto);
|
|
295
284
|
// The value is read before the relation's target is resolved: a query that populated nothing
|
|
296
285
|
// still walks every relation the entity declares, and `rel.entity()` is a call per row per
|
|
297
286
|
// relation that only the populated ones need.
|
|
298
287
|
for (const key in meta.relations) {
|
|
299
288
|
const value = row[key];
|
|
300
|
-
if (!value
|
|
289
|
+
if (!value)
|
|
301
290
|
continue;
|
|
302
291
|
const rel = meta.relations[key];
|
|
303
292
|
if (!rel)
|
|
304
293
|
continue;
|
|
305
294
|
const relEntity = rel.entity();
|
|
306
|
-
if (Array.isArray(value)) {
|
|
307
|
-
|
|
308
|
-
|
|
295
|
+
if (typeof value === 'string' || Array.isArray(value)) {
|
|
296
|
+
// A to-many's rows, as flat as a statement's own.
|
|
297
|
+
const rows = unflatObjects(typeof value === 'string' ? JSON.parse(value) : value);
|
|
298
|
+
row[key] = rows;
|
|
299
|
+
this.hydrateAll(relEntity, rows);
|
|
300
|
+
}
|
|
301
|
+
else if (isRecord(value)) {
|
|
302
|
+
const relMeta = getMeta(relEntity);
|
|
303
|
+
if (value[relMeta.ids[0]] == null) {
|
|
304
|
+
delete row[key];
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
this.hydrateFields(relMeta, this.dialect.hydratableFields(relEntity), value);
|
|
309
308
|
}
|
|
310
|
-
continue;
|
|
311
309
|
}
|
|
312
|
-
this.hydrateFields(relEntity, value, visited);
|
|
313
310
|
}
|
|
314
|
-
return dto;
|
|
315
311
|
}
|
|
316
312
|
/**
|
|
317
313
|
* Runs a statement whose one row carries a {@link COUNT_ALIAS} column. `Number` because `COUNT(*)` is BIGINT and
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Canonical types (dialect-agnostic)
|
|
7
7
|
* - TypeScript types (for entity generation)
|
|
8
8
|
*/
|
|
9
|
-
import { columnFamily } from '../util/field.util.js';
|
|
9
|
+
import { columnFamily, isIntegerColumn } from '../util/field.util.js';
|
|
10
10
|
/** Whether a category is one of the vector types, narrowing it to the cast pgvector names use. */
|
|
11
11
|
export function isVectorCategory(category) {
|
|
12
12
|
return category === 'vector' || category === 'halfvec' || category === 'sparsevec';
|
|
@@ -395,9 +395,9 @@ export function fieldOptionsToCanonical(options) {
|
|
|
395
395
|
case 'numeric':
|
|
396
396
|
// BIGINT for every `Number` without a scale, key or not: a 32-bit column is a migration waiting
|
|
397
397
|
// to happen, and the pools decode it back to a JS number at the wire (see `pgNumericTypes`).
|
|
398
|
-
return
|
|
399
|
-
? { category: '
|
|
400
|
-
: { category: '
|
|
398
|
+
return isIntegerColumn(options)
|
|
399
|
+
? { category: 'integer', size: 'big' }
|
|
400
|
+
: { category: 'decimal', precision: options.precision, scale: options.scale };
|
|
401
401
|
case 'boolean':
|
|
402
402
|
return { category: 'boolean' };
|
|
403
403
|
case 'date':
|
|
@@ -492,7 +492,5 @@ export function canonicalToColumnType(type) {
|
|
|
492
492
|
return 'halfvec';
|
|
493
493
|
case 'sparsevec':
|
|
494
494
|
return 'sparsevec';
|
|
495
|
-
default:
|
|
496
|
-
return 'varchar';
|
|
497
495
|
}
|
|
498
496
|
}
|
|
@@ -38,11 +38,9 @@ export function findCycles(nodes, dependenciesOf) {
|
|
|
38
38
|
const visited = new Set();
|
|
39
39
|
const onPath = new Set();
|
|
40
40
|
const visit = (node, path) => {
|
|
41
|
+
// A node on the walk was handed down in `path` too, so the cycle closes where it first appears.
|
|
41
42
|
if (onPath.has(node)) {
|
|
42
|
-
|
|
43
|
-
if (start !== -1) {
|
|
44
|
-
cycles.push(path.slice(start));
|
|
45
|
-
}
|
|
43
|
+
cycles.push(path.slice(path.indexOf(node)));
|
|
46
44
|
return;
|
|
47
45
|
}
|
|
48
46
|
if (visited.has(node)) {
|
|
@@ -61,20 +61,20 @@ export function describeIndexDifferences(source, target, facets) {
|
|
|
61
61
|
if (comparableEntries) {
|
|
62
62
|
const [sourceColumns, targetColumns] = [source.entries, target.entries].map((entries) => entries.map((entry) => entrySignature(entry, facets)).join(', '));
|
|
63
63
|
if (sourceColumns !== targetColumns) {
|
|
64
|
-
differences.push(`columns: (${targetColumns})
|
|
64
|
+
differences.push(`columns: (${targetColumns}) -> (${sourceColumns})`);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
if (
|
|
68
|
-
differences.push(`unique: ${target.unique
|
|
67
|
+
if (source.unique !== target.unique) {
|
|
68
|
+
differences.push(`unique: ${target.unique} -> ${source.unique}`);
|
|
69
69
|
}
|
|
70
70
|
if (facets.has('accessMethod') && (source.type ?? 'btree') !== (target.type ?? 'btree')) {
|
|
71
|
-
differences.push(`type: ${target.type ?? 'btree'}
|
|
71
|
+
differences.push(`type: ${target.type ?? 'btree'} -> ${source.type ?? 'btree'}`);
|
|
72
72
|
}
|
|
73
73
|
if (facets.has('include')) {
|
|
74
74
|
// Order carries no meaning in an `INCLUDE` list, so it is compared as a set.
|
|
75
75
|
const [sourceInclude, targetInclude] = [source.include ?? [], target.include ?? []].map((columns) => [...columns].sort().join(', '));
|
|
76
76
|
if (sourceInclude !== targetInclude) {
|
|
77
|
-
differences.push(`include: (${targetInclude})
|
|
77
|
+
differences.push(`include: (${targetInclude}) -> (${sourceInclude})`);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
return differences;
|