uql-orm 0.46.0 → 0.47.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/dist/bunSql/bunSqlQuerier.d.ts +7 -0
- package/dist/bunSql/bunSqlQuerier.js +7 -0
- package/dist/bunSql/bunSqlQuerierPool.d.ts +1 -0
- package/dist/bunSql/bunSqlQuerierPool.js +6 -1
- package/dist/bunSql/index.d.ts +1 -0
- package/dist/bunSql/index.js +1 -0
- package/dist/dialect/abstractSqlDialect.d.ts +13 -0
- package/dist/dialect/abstractSqlDialect.js +30 -2
- package/dist/dialect/aliases.d.ts +4 -0
- package/dist/dialect/aliases.js +4 -0
- package/dist/dialect/pgLikeSqlDialect.d.ts +14 -0
- package/dist/dialect/pgLikeSqlDialect.js +40 -1
- package/dist/mongo/mongodbQuerier.d.ts +16 -0
- package/dist/mongo/mongodbQuerier.js +50 -1
- package/dist/querier/abstractQuerier.d.ts +18 -1
- package/dist/querier/abstractQuerier.js +50 -14
- package/dist/querier/abstractSqlQuerier.d.ts +14 -0
- package/dist/querier/abstractSqlQuerier.js +17 -0
- package/dist/util/relationQuery.util.d.ts +34 -1
- package/dist/util/relationQuery.util.js +40 -3
- package/package.json +1 -1
|
@@ -2,6 +2,13 @@ import type { ReservedSQL, SQL } from 'bun';
|
|
|
2
2
|
import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
3
3
|
import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
|
|
4
4
|
import type { ExtraOptions } from '../type/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Querier for `bun:sql`, Bun's built-in driver for Postgres, MySQL, MariaDB, CockroachDB and SQLite.
|
|
7
|
+
*
|
|
8
|
+
* @remarks Deliberately does not override `internalStream`, which every other SQL driver here does:
|
|
9
|
+
* Bun's `SQL.Query` is a `Promise` with no cursor or async-iterator API, so `findManyStream` falls back
|
|
10
|
+
* to the base class buffering the whole result, as `PgliteQuerier` does for the same reason.
|
|
11
|
+
*/
|
|
5
12
|
export declare class BunSqlQuerier extends AbstractPoolQuerier<ReservedSQL> {
|
|
6
13
|
readonly sql: SQL;
|
|
7
14
|
readonly extra?: ExtraOptions | undefined;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
|
|
2
2
|
import { getAffectedRows, getInsertId, isReservedConnection, normalizeRows } from './bunSql.util.js';
|
|
3
|
+
/**
|
|
4
|
+
* Querier for `bun:sql`, Bun's built-in driver for Postgres, MySQL, MariaDB, CockroachDB and SQLite.
|
|
5
|
+
*
|
|
6
|
+
* @remarks Deliberately does not override `internalStream`, which every other SQL driver here does:
|
|
7
|
+
* Bun's `SQL.Query` is a `Promise` with no cursor or async-iterator API, so `findManyStream` falls back
|
|
8
|
+
* to the base class buffering the whole result, as `PgliteQuerier` does for the same reason.
|
|
9
|
+
*/
|
|
3
10
|
export class BunSqlQuerier extends AbstractPoolQuerier {
|
|
4
11
|
sql;
|
|
5
12
|
extra;
|
|
@@ -7,6 +7,7 @@ export declare class BunSqlQuerierPool extends AbstractSqlQuerierPool<BunSqlQuer
|
|
|
7
7
|
readonly config: SQL.Options;
|
|
8
8
|
readonly sql: SQL;
|
|
9
9
|
readonly sqlDialectName: SqlDialectName;
|
|
10
|
+
private foreignKeysOn?;
|
|
10
11
|
constructor(config: SQL.Options, extra?: ExtraOptions);
|
|
11
12
|
/**
|
|
12
13
|
* Provides a pg-compatible interface for libraries like connect-pg-simple.
|
|
@@ -19,6 +19,7 @@ export class BunSqlQuerierPool extends AbstractSqlQuerierPool {
|
|
|
19
19
|
config;
|
|
20
20
|
sql;
|
|
21
21
|
sqlDialectName;
|
|
22
|
+
foreignKeysOn;
|
|
22
23
|
constructor(config, extra) {
|
|
23
24
|
const dialectName = inferDialectName(config);
|
|
24
25
|
super(new DialectMap[dialectName](dialectOptionsFrom(extra)), extra);
|
|
@@ -44,8 +45,12 @@ export class BunSqlQuerierPool extends AbstractSqlQuerierPool {
|
|
|
44
45
|
}
|
|
45
46
|
async getQuerier() {
|
|
46
47
|
const connFactory = async () => {
|
|
47
|
-
// Bun's SQLite adapter does not support connection reservation (it's unpooled)
|
|
48
|
+
// Bun's SQLite adapter does not support connection reservation (it's unpooled), and leaves
|
|
49
|
+
// `foreign_keys` off as `bun:sqlite` does, so without the pragma the constraints uql emits in
|
|
50
|
+
// its own DDL are decorative. One connection means one pragma, issued on the first acquisition.
|
|
48
51
|
if (!isPoolableDialect(this.sqlDialectName)) {
|
|
52
|
+
this.foreignKeysOn ??= this.sql.unsafe('PRAGMA foreign_keys = ON');
|
|
53
|
+
await this.foreignKeysOn;
|
|
49
54
|
return this.sql;
|
|
50
55
|
}
|
|
51
56
|
return this.sql.reserve();
|
package/dist/bunSql/index.d.ts
CHANGED
package/dist/bunSql/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryBuildFn, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryFilter, type QueryGroupMap, type QueryHavingMap, type QueryOptions, type QueryPager, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelectOptions, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorNear, type QueryWhere, type QueryWhereArray, type QueryWhereFieldOperatorMap, type QueryWhereMap, type QueryWhereOptions, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
|
|
2
|
+
import { type ParentPartition } from '../util/index.js';
|
|
2
3
|
import type { HydrateKind } from './hydrateColumn.js';
|
|
3
4
|
import { type JsonAccessMode } from './jsonSql.js';
|
|
4
5
|
import { type QueryJoins, type QuerySortOptions } from './queryJoins.js';
|
|
@@ -52,6 +53,18 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
52
53
|
*/
|
|
53
54
|
readonly maxBindValues: number;
|
|
54
55
|
getBeginTransactionStatements(isolationLevel?: IsolationLevel): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Every parent's own bounded page in one statement: a subquery per parent, each filtered to that
|
|
58
|
+
* parent alone and carrying its own `ORDER BY`, `LIMIT` and `OFFSET`. Universal, and reads
|
|
59
|
+
* `parents x (skip + limit)` rows where a `ROW_NUMBER` window reads every matching child.
|
|
60
|
+
* [The design](../../../../architecture/populate-limits.md).
|
|
61
|
+
*
|
|
62
|
+
* Each branch is a wrapped derived table rather than a bare parenthesised select: SQLite rejects
|
|
63
|
+
* `ORDER BY`/`LIMIT` on the latter, and the wrapper costs nothing elsewhere.
|
|
64
|
+
*/
|
|
65
|
+
findPerParent<E extends object>(ctx: QueryContext, entity: Type<E>, q: Query<E>, partition: ParentPartition): void;
|
|
66
|
+
/** The shape {@link findPerParent} emits, which the Postgres family replaces with a `LATERAL` join. */
|
|
67
|
+
protected appendPerParent<E extends object>(ctx: QueryContext, entity: Type<E>, q: Query<E>, { joins, parents }: ParentPartition): void;
|
|
55
68
|
createContext(): QueryContext;
|
|
56
69
|
/**
|
|
57
70
|
* Builds SQL text in isolation via `build`, so the caller can embed it inline (e.g.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { getMeta, soleIdOf } from '../entity/index.js';
|
|
2
2
|
import { parseQueryLock, QueryRaw, RAW_ALIAS, RAW_VALUE, VECTOR_QUERY_KEYS, } from '../type/index.js';
|
|
3
3
|
import { computedExpression, isInlinedExpression } from '../util/field.util.js';
|
|
4
|
-
import { asSelectMap, assertNonNegativeInteger, buildQueryWhereAsMap, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getSoftDeleteValue, hasKeys, columnFamily, isJsonUpdateOp, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationSize, parseSortByCount, populatesRelations, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
4
|
+
import { asSelectMap, assertNonNegativeInteger, buildQueryWhereAsMap, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getSoftDeleteValue, hasKeys, columnFamily, isJsonUpdateOp, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationSize, parseSortByCount, populatesRelations, queryChildrenOf, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
5
|
import { escapeAnsiSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
6
|
-
import { COUNT_ALIAS, DISTINCT_DERIVED_ALIAS, JSON_ELEM_ALIAS_PREFIX } from './aliases.js';
|
|
6
|
+
import { COUNT_ALIAS, DISTINCT_DERIVED_ALIAS, JSON_ELEM_ALIAS_PREFIX, PER_PARENT_BRANCH_ALIAS } from './aliases.js';
|
|
7
7
|
import { buildElemMatchConditions } from './jsonArrayElemMatchUtils.js';
|
|
8
8
|
import { isJsonbOp, jsonCompareMode, jsonElemExists } from './jsonSql.js';
|
|
9
9
|
import { SqlQueryContext } from './queryContext.js';
|
|
@@ -55,6 +55,34 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
55
55
|
// 'set-before' - MySQL/MariaDB pattern
|
|
56
56
|
return [`SET TRANSACTION ISOLATION LEVEL ${level}`, this.beginTransactionCommand];
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Every parent's own bounded page in one statement: a subquery per parent, each filtered to that
|
|
60
|
+
* parent alone and carrying its own `ORDER BY`, `LIMIT` and `OFFSET`. Universal, and reads
|
|
61
|
+
* `parents x (skip + limit)` rows where a `ROW_NUMBER` window reads every matching child.
|
|
62
|
+
* [The design](../../../../architecture/populate-limits.md).
|
|
63
|
+
*
|
|
64
|
+
* Each branch is a wrapped derived table rather than a bare parenthesised select: SQLite rejects
|
|
65
|
+
* `ORDER BY`/`LIMIT` on the latter, and the wrapper costs nothing elsewhere.
|
|
66
|
+
*/
|
|
67
|
+
findPerParent(ctx, entity, q, partition) {
|
|
68
|
+
if (!partition.parents.length) {
|
|
69
|
+
// Guarded on the contract rather than in either shape: this is the end that would otherwise
|
|
70
|
+
// append nothing and hand the driver an empty statement, and both shapes owe the same promise.
|
|
71
|
+
throw new TypeError('cannot read a bounded relation for no parents at all');
|
|
72
|
+
}
|
|
73
|
+
this.appendPerParent(ctx, entity, q, partition);
|
|
74
|
+
}
|
|
75
|
+
/** The shape {@link findPerParent} emits, which the Postgres family replaces with a `LATERAL` join. */
|
|
76
|
+
appendPerParent(ctx, entity, q, { joins, parents }) {
|
|
77
|
+
parents.forEach((parent, index) => {
|
|
78
|
+
if (index) {
|
|
79
|
+
ctx.append(' UNION ALL ');
|
|
80
|
+
}
|
|
81
|
+
ctx.append('SELECT * FROM (');
|
|
82
|
+
this.find(ctx, entity, queryChildrenOf(q, joins, parent));
|
|
83
|
+
ctx.append(`) ${this.escapeId(ctx.nextAlias(PER_PARENT_BRANCH_ALIAS))}`);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
58
86
|
createContext() {
|
|
59
87
|
return new SqlQueryContext(this);
|
|
60
88
|
}
|
|
@@ -14,6 +14,10 @@ export declare const COUNT_ALIAS = "_uql_count";
|
|
|
14
14
|
export declare const TOTAL_ALIAS = "_uql_total";
|
|
15
15
|
/** The derived table a `$distinct` count wraps its deduplicated set in. MySQL requires the alias. */
|
|
16
16
|
export declare const DISTINCT_DERIVED_ALIAS = "_uql_distinct";
|
|
17
|
+
/** Prefix for the derived table each branch of a per-parent bounded read is wrapped in. */
|
|
18
|
+
export declare const PER_PARENT_BRANCH_ALIAS = "_uql_p";
|
|
19
|
+
/** The row source a `LATERAL` per-parent read correlates each of its branches against. */
|
|
20
|
+
export declare const PER_PARENT_KEYS_ALIAS = "_uql_keys";
|
|
17
21
|
/** Prefix for the alias an exploded JSON array element is read through. */
|
|
18
22
|
export declare const JSON_ELEM_ALIAS_PREFIX = "_uql_elem";
|
|
19
23
|
/** The alias a `$pull` reads its surviving elements through, kept distinct from {@link JSON_ELEM_ALIAS_PREFIX}. */
|
package/dist/dialect/aliases.js
CHANGED
|
@@ -14,6 +14,10 @@ export const COUNT_ALIAS = '_uql_count';
|
|
|
14
14
|
export const TOTAL_ALIAS = '_uql_total';
|
|
15
15
|
/** The derived table a `$distinct` count wraps its deduplicated set in. MySQL requires the alias. */
|
|
16
16
|
export const DISTINCT_DERIVED_ALIAS = '_uql_distinct';
|
|
17
|
+
/** Prefix for the derived table each branch of a per-parent bounded read is wrapped in. */
|
|
18
|
+
export const PER_PARENT_BRANCH_ALIAS = '_uql_p';
|
|
19
|
+
/** The row source a `LATERAL` per-parent read correlates each of its branches against. */
|
|
20
|
+
export const PER_PARENT_KEYS_ALIAS = '_uql_keys';
|
|
17
21
|
/** Prefix for the alias an exploded JSON array element is read through. */
|
|
18
22
|
export const JSON_ELEM_ALIAS_PREFIX = '_uql_elem';
|
|
19
23
|
/** The alias a `$pull` reads its surviving elements through, kept distinct from {@link JSON_ELEM_ALIAS_PREFIX}. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DialectFeatures, type EntityMeta, type FieldOptions, type JsonColumnType, type Query, type QueryContext, type QuerySizeComparisonOps, type QueryTextSearchOptions, type Type, type VectorDistance, type VectorOperatorMetric } from '../type/index.js';
|
|
2
|
+
import { type ParentPartition } from '../util/relationQuery.util.js';
|
|
2
3
|
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
3
4
|
/**
|
|
4
5
|
* Shared AST/quoting/JSONB/full-text-search/vector-search implementation between Postgres and
|
|
@@ -21,6 +22,19 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
21
22
|
readonly commitTransactionCommand = "COMMIT";
|
|
22
23
|
readonly rollbackTransactionCommand = "ROLLBACK";
|
|
23
24
|
readonly alterColumnStrategy = "separate-clauses";
|
|
25
|
+
/**
|
|
26
|
+
* One `LATERAL` branch correlated against an array of the parent keys, in place of the base
|
|
27
|
+
* `UNION ALL` of a subquery per parent. Same rows and the same `parents x (skip + limit)` read, but
|
|
28
|
+
* the planner sees one correlated index loop rather than N branches to plan: flat in page size where
|
|
29
|
+
* `UNION ALL` is linear, and the statement's text stops changing with the number of parents, so one
|
|
30
|
+
* prepared statement serves every page.
|
|
31
|
+
*
|
|
32
|
+
* Postgres, CockroachDB, PGlite, Neon and bun-sql inherit it together. **MySQL has `LATERAL` and must
|
|
33
|
+
* not use it** - it does not plan this as a correlated index loop and measured slower than both its
|
|
34
|
+
* own `UNION ALL` and a query per parent, which is why this is an override rather than a capability
|
|
35
|
+
* flag. [The design](../../../../architecture/populate-limits.md).
|
|
36
|
+
*/
|
|
37
|
+
protected appendPerParent<E extends object>(ctx: QueryContext, entity: Type<E>, q: Query<E>, { joins, parents, parentFields }: ParentPartition): void;
|
|
24
38
|
/** `$N` placeholders carry their own index, so the upsert's assignments need no scratch context. */
|
|
25
39
|
protected readonly upsertUpdateBindsInPlace = true;
|
|
26
40
|
readonly insertIdSource = "returning";
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { canonicalToSql, fieldOptionsToCanonical } from '../schema/canonicalType.js';
|
|
1
2
|
import { QueryRaw, } from '../type/index.js';
|
|
2
3
|
import { hasVectorNear } from '../util/dialect.util.js';
|
|
4
|
+
import { raw } from '../util/raw.js';
|
|
5
|
+
import { queryNarrowedTo } from '../util/relationQuery.util.js';
|
|
3
6
|
import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
4
7
|
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
5
|
-
import { JSON_PULL_ALIAS } from './aliases.js';
|
|
8
|
+
import { JSON_PULL_ALIAS, PER_PARENT_BRANCH_ALIAS, PER_PARENT_KEYS_ALIAS } from './aliases.js';
|
|
6
9
|
import { jsonSetTarget } from './jsonSql.js';
|
|
7
10
|
import { resolveVectorCast, toSparsevecLiteral } from './vectorCast.js';
|
|
8
11
|
/**
|
|
@@ -47,6 +50,42 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
47
50
|
commitTransactionCommand = 'COMMIT';
|
|
48
51
|
rollbackTransactionCommand = 'ROLLBACK';
|
|
49
52
|
alterColumnStrategy = 'separate-clauses';
|
|
53
|
+
/**
|
|
54
|
+
* One `LATERAL` branch correlated against an array of the parent keys, in place of the base
|
|
55
|
+
* `UNION ALL` of a subquery per parent. Same rows and the same `parents x (skip + limit)` read, but
|
|
56
|
+
* the planner sees one correlated index loop rather than N branches to plan: flat in page size where
|
|
57
|
+
* `UNION ALL` is linear, and the statement's text stops changing with the number of parents, so one
|
|
58
|
+
* prepared statement serves every page.
|
|
59
|
+
*
|
|
60
|
+
* Postgres, CockroachDB, PGlite, Neon and bun-sql inherit it together. **MySQL has `LATERAL` and must
|
|
61
|
+
* not use it** - it does not plan this as a correlated index loop and measured slower than both its
|
|
62
|
+
* own `UNION ALL` and a query per parent, which is why this is an override rather than a capability
|
|
63
|
+
* flag. [The design](../../../../architecture/populate-limits.md).
|
|
64
|
+
*/
|
|
65
|
+
appendPerParent(ctx, entity, q, { joins, parents, parentFields }) {
|
|
66
|
+
const keys = this.escapeId(ctx.nextAlias(PER_PARENT_KEYS_ALIAS));
|
|
67
|
+
const branch = this.escapeId(ctx.nextAlias(PER_PARENT_BRANCH_ALIAS));
|
|
68
|
+
const column = (index) => `${keys}.k${index}`;
|
|
69
|
+
// `unnest` resolves an uncast parameter to `unknown` and refuses it ("function unnest(unknown) is
|
|
70
|
+
// not unique"), so the array says its type. It comes from the parent's key column, which always
|
|
71
|
+
// declares one, rather than the child's foreign key, which would have to be resolved through the
|
|
72
|
+
// reference it takes its own type from.
|
|
73
|
+
const sources = joins.map(({ parent }) => {
|
|
74
|
+
const field = parentFields[parent];
|
|
75
|
+
if (!field) {
|
|
76
|
+
throw new TypeError(`cannot page a relation per parent: '${parent}' is not a field of the parent entity`);
|
|
77
|
+
}
|
|
78
|
+
const values = parents.map((it) => it[parent]);
|
|
79
|
+
return `${this.addValue(ctx.values, values)}::${canonicalToSql(fieldOptionsToCanonical(field), this)}[]`;
|
|
80
|
+
});
|
|
81
|
+
const rowSource = `unnest(${sources.join(', ')}) AS ${keys}(${joins.map((_, index) => `k${index}`).join(', ')})`;
|
|
82
|
+
// The keys come from the row source rather than as values, which is the whole point of correlating:
|
|
83
|
+
// one branch, planned once, instead of one per parent.
|
|
84
|
+
const correlated = Object.fromEntries(joins.map(({ joined }, index) => [joined, raw(({ ctx: inner }) => inner.append(column(index)))]));
|
|
85
|
+
ctx.append(`SELECT ${branch}.* FROM ${rowSource} JOIN LATERAL (`);
|
|
86
|
+
this.find(ctx, entity, queryNarrowedTo(q, correlated));
|
|
87
|
+
ctx.append(`) ${branch} ON TRUE`);
|
|
88
|
+
}
|
|
50
89
|
/** `$N` placeholders carry their own index, so the upsert's assignments need no scratch context. */
|
|
51
90
|
upsertUpdateBindsInPlace = true;
|
|
52
91
|
insertIdSource = 'returning';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Document, MongoClient } from 'mongodb';
|
|
2
2
|
import { AbstractQuerier } from '../querier/index.js';
|
|
3
3
|
import type { EntityData, ExtraOptions, IdValue, PrimaryKey, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
4
|
+
import { type ParentPartition } from '../util/index.js';
|
|
4
5
|
import type { MongoDialect } from './mongoDialect.js';
|
|
5
6
|
export declare class MongodbQuerier extends AbstractQuerier {
|
|
6
7
|
readonly dialect: MongoDialect;
|
|
@@ -10,6 +11,21 @@ export declare class MongodbQuerier extends AbstractQuerier {
|
|
|
10
11
|
constructor(dialect: MongoDialect, conn: MongoClient, extra?: ExtraOptions | undefined);
|
|
11
12
|
private execute;
|
|
12
13
|
protected internalFindMany<E extends Document>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Every parent's own bounded page. One `$unionWith` per parent after the first, so the whole page is
|
|
16
|
+
* one round trip - measured ~6x faster than a query each (11.0 ms -> 1.9 ms at 50 parents, 87.5 ms
|
|
17
|
+
* -> 14.1 ms at 500), because `execute` serializes on the session and a query each is N round trips
|
|
18
|
+
* rather than N concurrent ones.
|
|
19
|
+
*
|
|
20
|
+
* Both arms return documents with their own relations already filled, so this only chooses between
|
|
21
|
+
* them: leaving that to the caller once meant the arm that fills its own did it twice.
|
|
22
|
+
* [The design](../../../../architecture/populate-limits.md).
|
|
23
|
+
*/
|
|
24
|
+
protected internalFindManyPerParent<E extends Document>(entity: Type<E>, q: Query<E>, { joins, parents }: ParentPartition): Promise<E[]>;
|
|
25
|
+
/** Every parent's page as one `$unionWith` pipeline. */
|
|
26
|
+
private readInOnePipeline;
|
|
27
|
+
/** A query each, for what one pipeline cannot carry. `internalFindMany` fills its own relations. */
|
|
28
|
+
private readEachInTurn;
|
|
13
29
|
protected internalFindManyStream<E extends Document>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): AsyncGenerator<E, void, unknown>;
|
|
14
30
|
private buildScalarProjection;
|
|
15
31
|
/** Build a MongoDB FindCursor with filter, projection, sort, skip, and limit from the query. */
|
|
@@ -2,7 +2,7 @@ import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
|
2
2
|
import { hasRequiredJoin } from '../dialect/queryJoins.js';
|
|
3
3
|
import { getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
4
4
|
import { AbstractQuerier, enrichError } from '../querier/index.js';
|
|
5
|
-
import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
|
+
import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, queryChildrenOf, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
6
6
|
/**
|
|
7
7
|
* `$limit: 0` asks for no rows, the way it does on every SQL dialect - but MongoDB reads `limit(0)`
|
|
8
8
|
* as *unlimited*, so a read that passed it straight to the driver came back with the whole
|
|
@@ -11,6 +11,12 @@ import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys,
|
|
|
11
11
|
function asksForNoRows(q) {
|
|
12
12
|
return q.$limit === 0;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* What MongoDB accepts in one pipeline. Bisected against a real server: 1000 top-level stages are
|
|
16
|
+
* accepted and 1001 refused (`Pipeline length must be no longer than 1000 stages`), and a
|
|
17
|
+
* `$unionWith`'s own sub-pipeline stages do not count toward it.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_PIPELINE_STAGES = 1000;
|
|
14
20
|
export class MongodbQuerier extends AbstractQuerier {
|
|
15
21
|
dialect;
|
|
16
22
|
conn;
|
|
@@ -63,6 +69,49 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
63
69
|
return documents;
|
|
64
70
|
});
|
|
65
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Every parent's own bounded page. One `$unionWith` per parent after the first, so the whole page is
|
|
74
|
+
* one round trip - measured ~6x faster than a query each (11.0 ms -> 1.9 ms at 50 parents, 87.5 ms
|
|
75
|
+
* -> 14.1 ms at 500), because `execute` serializes on the session and a query each is N round trips
|
|
76
|
+
* rather than N concurrent ones.
|
|
77
|
+
*
|
|
78
|
+
* Both arms return documents with their own relations already filled, so this only chooses between
|
|
79
|
+
* them: leaving that to the caller once meant the arm that fills its own did it twice.
|
|
80
|
+
* [The design](../../../../architecture/populate-limits.md).
|
|
81
|
+
*/
|
|
82
|
+
async internalFindManyPerParent(entity, q, { joins, parents }) {
|
|
83
|
+
const queries = parents.map((parent) => queryChildrenOf(q, joins, parent));
|
|
84
|
+
// A vector sort needs a pipeline of its own shape, which only `internalFindMany` builds.
|
|
85
|
+
if (this.dialect.extractVectorSort(q.$sort)) {
|
|
86
|
+
return this.readEachInTurn(entity, queries);
|
|
87
|
+
}
|
|
88
|
+
const pipelines = queries.map((it) => this.dialect.aggregationPipeline(entity, it));
|
|
89
|
+
// Counted, not estimated: the leading branch's own length grows with every `$lookup` a populate
|
|
90
|
+
// adds, so a fixed parent budget would let a richer query overflow at the server instead.
|
|
91
|
+
const stages = (pipelines[0]?.length ?? 0) + pipelines.length - 1;
|
|
92
|
+
return stages > MAX_PIPELINE_STAGES
|
|
93
|
+
? this.readEachInTurn(entity, queries)
|
|
94
|
+
: this.readInOnePipeline(entity, q, pipelines);
|
|
95
|
+
}
|
|
96
|
+
/** Every parent's page as one `$unionWith` pipeline. */
|
|
97
|
+
async readInOnePipeline(entity, q, pipelines) {
|
|
98
|
+
const meta = getMeta(entity);
|
|
99
|
+
const [first, ...rest] = pipelines;
|
|
100
|
+
const documents = await this.runPipeline(entity, meta, [
|
|
101
|
+
...first,
|
|
102
|
+
...rest.map((pipeline) => ({ $unionWith: { coll: meta.name, pipeline } })),
|
|
103
|
+
]);
|
|
104
|
+
await this.fillToManyRelations(entity, documents, q.$populate);
|
|
105
|
+
return documents;
|
|
106
|
+
}
|
|
107
|
+
/** A query each, for what one pipeline cannot carry. `internalFindMany` fills its own relations. */
|
|
108
|
+
async readEachInTurn(entity, queries) {
|
|
109
|
+
const documents = [];
|
|
110
|
+
for (const query of queries) {
|
|
111
|
+
documents.push(...(await this.internalFindMany(entity, query)));
|
|
112
|
+
}
|
|
113
|
+
return documents;
|
|
114
|
+
}
|
|
66
115
|
async *internalFindManyStream(entity, q, opts) {
|
|
67
116
|
if (asksForNoRows(q)) {
|
|
68
117
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryPopulate, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
|
-
import { LoggerWrapper, type ParentJoin } from '../util/index.js';
|
|
2
|
+
import { LoggerWrapper, type ParentJoin, type ParentPartition } from '../util/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Base class for all database queriers.
|
|
5
5
|
* It provides a standardized way to execute tasks serially to prevent race conditions on database connections.
|
|
@@ -64,6 +64,14 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
64
64
|
$entity: Type<E>;
|
|
65
65
|
}, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
|
|
66
66
|
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: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
|
|
67
|
+
/**
|
|
68
|
+
* The children of every parent in `parents`, at most `$limit` each after `$skip` - what a to-many
|
|
69
|
+
* `$populate` carrying either one means. One statement, not one per parent.
|
|
70
|
+
*
|
|
71
|
+
* Abstract rather than defaulted: a default would be N queries, which is the N+1 that batched
|
|
72
|
+
* population exists to prevent, and it would be invisible to whichever backend forgot to override.
|
|
73
|
+
*/
|
|
74
|
+
protected abstract internalFindManyPerParent<E extends object>(entity: Type<E>, q: Query<E>, partition: ParentPartition): Promise<E[]>;
|
|
67
75
|
protected abstract internalFindManyStream<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): AsyncIterable<E>;
|
|
68
76
|
/**
|
|
69
77
|
* Find multiple records and return both the records and total count.
|
|
@@ -137,6 +145,15 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
137
145
|
protected fillToManyRelations<E>(entity: Type<E>, payload: E[], populate?: QueryPopulate<E>): Promise<void>;
|
|
138
146
|
private fillToManyThroughRelation;
|
|
139
147
|
private fillToManyOneToMany;
|
|
148
|
+
/**
|
|
149
|
+
* The children of a whole page of parents, however the relation asked for them: one bounded branch
|
|
150
|
+
* per parent when it wants a share of its own, otherwise a single flat statement over an `IN (...)`
|
|
151
|
+
* list, which is both correct and cheaper.
|
|
152
|
+
*
|
|
153
|
+
* The one place that decision is made - a one-to-many and the junction of a many-to-many differ in
|
|
154
|
+
* what they query, never in how the page is spread over its parents.
|
|
155
|
+
*/
|
|
156
|
+
private findChildrenOf;
|
|
140
157
|
protected putChildrenInParents<E>(parents: E[], children: RawRow[], joins: readonly ParentJoin[], relKey: keyof E & string): void;
|
|
141
158
|
protected insertRelations<E extends object>(entity: Type<E>, payload: E[]): Promise<void>;
|
|
142
159
|
protected updateRelations<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertSoleId, getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
2
|
-
import { asSelectMap, augmentWhere, childrenOf, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, joinedRowKey, LoggerWrapper, parentJoins, parentRowKey,
|
|
2
|
+
import { asSelectMap, augmentWhere, childrenOf, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, joinedRowKey, LoggerWrapper, isBoundedPerParent, parentJoins, parentRowKey, queryChildrenOfAll, parseRelationAtKey, parseRelationQueryValue, runHooks, someKey, targetKeyColumns, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
4
|
import { fillRelationCounts, withIdForCounts } from './relationCount.js';
|
|
5
5
|
/**
|
|
@@ -299,22 +299,44 @@ export class AbstractQuerier {
|
|
|
299
299
|
const throughEntity = relOpts.through();
|
|
300
300
|
const throughMeta = getMeta(throughEntity);
|
|
301
301
|
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === targetColumn));
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
302
|
+
if (!targetRelKey) {
|
|
303
|
+
// Asserted rather than assumed: used as a key regardless, it spells the literal string
|
|
304
|
+
// `undefined`, and the statement asks the junction for a relation of that name.
|
|
305
|
+
throw new TypeError(`'${meta.name}.${relKey}' goes through '${throughMeta.name}', which declares no relation on its ` +
|
|
306
|
+
`'${targetColumn}' column. Give it one, so the target's rows can be read through it.`);
|
|
307
|
+
}
|
|
308
|
+
// A relation query names the target's columns, not the junction's, so its projection and filter
|
|
309
|
+
// belong on the populate below, resolved against the entity that has them. Spread onto the
|
|
310
|
+
// junction query instead they asked `ItemTag` for `Tag`'s columns and failed with "no such
|
|
311
|
+
// column".
|
|
312
|
+
//
|
|
313
|
+
// Ordering and paging split the other way: they describe the statement with one row per pairing,
|
|
314
|
+
// which is the junction's. Left on the populate they reached a to-one join, which rejects all
|
|
315
|
+
// four by name - so a many-to-many carrying any of them threw rather than paging.
|
|
316
|
+
//
|
|
317
|
+
// Those four are not a coincidence: they are exactly the clauses a joined relation rejects, for
|
|
318
|
+
// the same reason - each needs a statement with many rows per parent, which only the junction's
|
|
319
|
+
// is. The `satisfies` ties the two lists together, so a fifth clause added there fails to compile
|
|
320
|
+
// here rather than quietly staying on the populate and throwing again.
|
|
321
|
+
const { $sort, $limit, $skip, $distinct, ...targetQuery } = relationQuery;
|
|
322
|
+
const junctionClauses = {
|
|
323
|
+
$limit,
|
|
324
|
+
$skip,
|
|
325
|
+
$distinct,
|
|
326
|
+
// Qualified by the relation that reaches them, since the columns it names are the target's.
|
|
327
|
+
$sort: $sort && { [targetRelKey]: $sort },
|
|
328
|
+
};
|
|
329
|
+
const junctionQuery = {
|
|
309
330
|
$select: joinedColumns(joins),
|
|
331
|
+
...junctionClauses,
|
|
310
332
|
$populate: {
|
|
311
333
|
[targetRelKey]: {
|
|
312
|
-
...
|
|
334
|
+
...targetQuery,
|
|
313
335
|
$required: true,
|
|
314
336
|
},
|
|
315
337
|
},
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
};
|
|
339
|
+
const throughFounds = await this.findChildrenOf(throughEntity, junctionQuery, joins, payload, meta.fields);
|
|
318
340
|
// The junction's own columns carried onto the target's row, which is where `putChildrenInParents`
|
|
319
341
|
// reads them back from - a junction row holds the parent's key under `joined`, not under `parent`.
|
|
320
342
|
const founds = throughFounds.map((it) => ({
|
|
@@ -336,9 +358,23 @@ export class AbstractQuerier {
|
|
|
336
358
|
}
|
|
337
359
|
delete exclude?.[joined];
|
|
338
360
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
361
|
+
this.putChildrenInParents(payload, await this.findChildrenOf(relEntity, relationQuery, joins, payload, meta.fields), joins, relKey);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The children of a whole page of parents, however the relation asked for them: one bounded branch
|
|
365
|
+
* per parent when it wants a share of its own, otherwise a single flat statement over an `IN (...)`
|
|
366
|
+
* list, which is both correct and cheaper.
|
|
367
|
+
*
|
|
368
|
+
* The one place that decision is made - a one-to-many and the junction of a many-to-many differ in
|
|
369
|
+
* what they query, never in how the page is spread over its parents.
|
|
370
|
+
*/
|
|
371
|
+
async findChildrenOf(entity, query, joins, parents, parentFields) {
|
|
372
|
+
const founds = isBoundedPerParent(query)
|
|
373
|
+
? await this.internalFindManyPerParent(entity, query, { joins, parents, parentFields })
|
|
374
|
+
: await this.findMany(entity, queryChildrenOfAll(query, joins, parents));
|
|
375
|
+
// Read back as rows rather than as the entity they hydrate to: what follows regroups them by the
|
|
376
|
+
// join columns, which a projected entity type does not carry.
|
|
377
|
+
return founds;
|
|
342
378
|
}
|
|
343
379
|
putChildrenInParents(parents, children, joins, relKey) {
|
|
344
380
|
const childrenByParentId = {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
2
|
import type { EntityData, ExtraOptions, IdValue, 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';
|
|
3
4
|
import type { BuildUpdateResultPayload } from '../util/sql.util.js';
|
|
4
5
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
5
6
|
export declare abstract class AbstractSqlQuerier extends AbstractQuerier implements SqlQuerier {
|
|
@@ -57,6 +58,19 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
57
58
|
*/
|
|
58
59
|
private applyVectorTuning;
|
|
59
60
|
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[]>;
|
|
60
74
|
/**
|
|
61
75
|
* One statement for both: the page carries its own unpaged total in an extra column. An empty page
|
|
62
76
|
* has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
|
|
@@ -104,6 +104,23 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
104
104
|
async internalFindMany(entity, q, opts) {
|
|
105
105
|
return this.hydrateRows(entity, q, await this.selectRows(entity, q, opts));
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* One bounded subquery per parent, concatenated with `UNION ALL`, so each parent gets its own
|
|
109
|
+
* `$limit` rather than a share of one. Universal, and reads `parents x (skip + limit)` rows where a
|
|
110
|
+
* `ROW_NUMBER` window reads every matching child. [The design](../../../../architecture/populate-limits.md).
|
|
111
|
+
*
|
|
112
|
+
* Each branch is a wrapped derived table: SQLite rejects `ORDER BY`/`LIMIT` on a bare parenthesised
|
|
113
|
+
* compound branch, and the wrapper costs nothing elsewhere.
|
|
114
|
+
*
|
|
115
|
+
* Unlike {@link selectRows} this asserts no lock and tunes no vector search: `$lock` and
|
|
116
|
+
* `$candidates` describe the statement, and `parseRelationQueryValue` refuses both on a relation
|
|
117
|
+
* query, so neither can reach here.
|
|
118
|
+
*/
|
|
119
|
+
async internalFindManyPerParent(entity, q, partition) {
|
|
120
|
+
const ctx = this.dialect.createContext();
|
|
121
|
+
this.dialect.findPerParent(ctx, entity, q, partition);
|
|
122
|
+
return this.hydrateRows(entity, q, await this.all(ctx.sql, ctx.values));
|
|
123
|
+
}
|
|
107
124
|
/**
|
|
108
125
|
* One statement for both: the page carries its own unpaged total in an extra column. An empty page
|
|
109
126
|
* has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityMeta, Except, Query, QueryPopulate, RelationKey, RelationMeta } from '../type/index.js';
|
|
1
|
+
import type { EntityMeta, FieldMeta, Except, Query, QueryPopulate, RelationKey, RelationMeta } from '../type/index.js';
|
|
2
2
|
export type RelationRequestSummary<E> = {
|
|
3
3
|
readonly requestedKeys: RelationKey<E>[];
|
|
4
4
|
readonly joinableKeys: RelationKey<E>[];
|
|
@@ -51,6 +51,39 @@ export declare function joinedRowKey(joins: readonly ParentJoin[], row: unknown)
|
|
|
51
51
|
* cheaper than the row-value comparison no engine spells the same way.
|
|
52
52
|
*/
|
|
53
53
|
export declare function parentsIn(joins: readonly ParentJoin[], parents: readonly unknown[]): Record<string, unknown[]>;
|
|
54
|
+
/**
|
|
55
|
+
* The parents a bounded to-many read fans out over: the rows themselves, the columns matching them to
|
|
56
|
+
* their children.
|
|
57
|
+
*/
|
|
58
|
+
export type ParentPartition = {
|
|
59
|
+
readonly joins: readonly ParentJoin[];
|
|
60
|
+
readonly parents: readonly unknown[];
|
|
61
|
+
/** The parent's own fields: a `LATERAL` row source has to spell its key column's type. */
|
|
62
|
+
readonly parentFields: Readonly<Record<string, FieldMeta | undefined>>;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Whether a to-many's own query asks for a share *per parent* rather than a slice of the whole page.
|
|
66
|
+
* Only `$limit`/`$skip` do: without one, a single flat statement over an `IN (...)` list is both
|
|
67
|
+
* correct and cheaper.
|
|
68
|
+
*/
|
|
69
|
+
export declare function isBoundedPerParent(query: Pick<RelationQuery, '$limit' | '$skip'>): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* `query` narrowed to one parent's children: what a single branch of a bounded per-parent read asks
|
|
72
|
+
* for. Shared by the backends so how the parent's filter merges into the relation's own is decided
|
|
73
|
+
* once - both spelled it out, and a rule that ever needs more than a spread would have to change twice.
|
|
74
|
+
*/
|
|
75
|
+
export declare function queryChildrenOf<E>(query: Query<E>, joins: readonly ParentJoin[], parent: unknown): Query<E>;
|
|
76
|
+
/**
|
|
77
|
+
* `query` narrowed to the children of a whole page of parents, which is the flat read a relation with
|
|
78
|
+
* no share of its own takes. Over-selects on a composite key exactly as {@link parentsIn} does.
|
|
79
|
+
*/
|
|
80
|
+
export declare function queryChildrenOfAll<E>(query: Query<E>, joins: readonly ParentJoin[], parents: readonly unknown[]): Query<E>;
|
|
81
|
+
/**
|
|
82
|
+
* `query` with `filter` merged into its own `$where`: the one rule for narrowing a relation's query to
|
|
83
|
+
* the parents it is being read for, whether the filter names their keys as values or, for a correlated
|
|
84
|
+
* shape, as a reference to a row source.
|
|
85
|
+
*/
|
|
86
|
+
export declare function queryNarrowedTo<E>(query: Query<E>, filter: Record<string, unknown>): Query<E>;
|
|
54
87
|
/**
|
|
55
88
|
* The `$where` naming exactly the children of the rows `parentIds` identifies: an `IN` over the one
|
|
56
89
|
* column a single key contributes, an OR of key maps for several.
|
|
@@ -62,6 +62,45 @@ export function joinedRowKey(joins, row) {
|
|
|
62
62
|
export function parentsIn(joins, parents) {
|
|
63
63
|
return Object.fromEntries(joins.map(({ parent, joined }) => [joined, parents.map((it) => read(it, parent))]));
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether a to-many's own query asks for a share *per parent* rather than a slice of the whole page.
|
|
67
|
+
* Only `$limit`/`$skip` do: without one, a single flat statement over an `IN (...)` list is both
|
|
68
|
+
* correct and cheaper.
|
|
69
|
+
*/
|
|
70
|
+
export function isBoundedPerParent(query) {
|
|
71
|
+
return query.$limit !== undefined || query.$skip !== undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The `$where` naming exactly one parent's children: every joined column equal to that parent's value.
|
|
75
|
+
* What a per-parent bounded read filters each of its branches by, and the composite half of
|
|
76
|
+
* {@link childrenOf}.
|
|
77
|
+
*/
|
|
78
|
+
function childOf(joins, parent) {
|
|
79
|
+
return Object.fromEntries(joins.map(({ parent: key, joined }) => [joined, read(parent, key)]));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* `query` narrowed to one parent's children: what a single branch of a bounded per-parent read asks
|
|
83
|
+
* for. Shared by the backends so how the parent's filter merges into the relation's own is decided
|
|
84
|
+
* once - both spelled it out, and a rule that ever needs more than a spread would have to change twice.
|
|
85
|
+
*/
|
|
86
|
+
export function queryChildrenOf(query, joins, parent) {
|
|
87
|
+
return queryNarrowedTo(query, childOf(joins, parent));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* `query` narrowed to the children of a whole page of parents, which is the flat read a relation with
|
|
91
|
+
* no share of its own takes. Over-selects on a composite key exactly as {@link parentsIn} does.
|
|
92
|
+
*/
|
|
93
|
+
export function queryChildrenOfAll(query, joins, parents) {
|
|
94
|
+
return queryNarrowedTo(query, parentsIn(joins, parents));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `query` with `filter` merged into its own `$where`: the one rule for narrowing a relation's query to
|
|
98
|
+
* the parents it is being read for, whether the filter names their keys as values or, for a correlated
|
|
99
|
+
* shape, as a reference to a row source.
|
|
100
|
+
*/
|
|
101
|
+
export function queryNarrowedTo(query, filter) {
|
|
102
|
+
return { ...query, $where: { ...query.$where, ...filter } };
|
|
103
|
+
}
|
|
65
104
|
/**
|
|
66
105
|
* The `$where` naming exactly the children of the rows `parentIds` identifies: an `IN` over the one
|
|
67
106
|
* column a single key contributes, an OR of key maps for several.
|
|
@@ -74,9 +113,7 @@ export function childrenOf(joins, parentIds) {
|
|
|
74
113
|
if (joins.length === 1) {
|
|
75
114
|
return { [first.joined]: parentIds };
|
|
76
115
|
}
|
|
77
|
-
return {
|
|
78
|
-
$or: parentIds.map((id) => Object.fromEntries(joins.map(({ parent, joined }) => [joined, read(id, parent)]))),
|
|
79
|
-
};
|
|
116
|
+
return { $or: parentIds.map((id) => childOf(joins, id)) };
|
|
80
117
|
}
|
|
81
118
|
function read(row, key) {
|
|
82
119
|
return row[key];
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.47.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|