uql-orm 0.37.0 → 0.37.1
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/cockroachdb/cockroachDialect.js +1 -1
- package/dist/dialect/abstractSqlDialect.d.ts +18 -7
- package/dist/dialect/abstractSqlDialect.js +24 -8
- package/dist/dialect/aliases.d.ts +30 -0
- package/dist/dialect/aliases.js +32 -0
- package/dist/dialect/jsonSql.d.ts +0 -20
- package/dist/dialect/jsonSql.js +0 -20
- package/dist/dialect/mysqlLikeSqlDialect.js +3 -2
- package/dist/dialect/pgLikeSqlDialect.d.ts +2 -0
- package/dist/dialect/pgLikeSqlDialect.js +4 -1
- package/dist/mongo/mongoDialect.d.ts +0 -9
- package/dist/mongo/mongoDialect.js +10 -20
- package/dist/mongo/mongodbQuerier.d.ts +3 -3
- package/dist/mongo/mongodbQuerier.js +12 -9
- package/dist/postgres/postgresDialect.js +1 -1
- package/dist/querier/abstractQuerier.js +4 -5
- package/dist/querier/abstractSqlQuerier.d.ts +11 -1
- package/dist/querier/abstractSqlQuerier.js +24 -7
- package/dist/querier/relationCount.js +7 -14
- package/dist/sqlite/sqliteDialect.js +2 -1
- package/dist/type/dialect.d.ts +6 -0
- package/dist/util/dialect.util.d.ts +0 -10
- package/dist/util/dialect.util.js +0 -10
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, 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 QueryWhere, type QueryWhereArray, type QueryWhereFieldOperatorMap, type QueryWhereMap, type QueryWhereOptions, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
|
|
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 QueryWhere, type QueryWhereArray, type QueryWhereFieldOperatorMap, type QueryWhereMap, type QueryWhereOptions, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
|
|
2
2
|
import type { HydrateKind } from './hydrateColumn.js';
|
|
3
3
|
import { IndexSqlDialect } from './indexSqlDialect.js';
|
|
4
4
|
import { type QueryJoins, type QuerySortOptions } from './queryJoins.js';
|
|
@@ -7,11 +7,6 @@ type PersistKind = 'plain' | 'json' | 'vector';
|
|
|
7
7
|
/** One entry of {@link AbstractSqlDialect.hydratableFields}: a field key and how it decodes. */
|
|
8
8
|
type HydratableField = readonly [string, HydrateKind];
|
|
9
9
|
export type { HydrateKind };
|
|
10
|
-
/**
|
|
11
|
-
* The column a counting statement answers in. Shared rather than spelled at each end: the dialects
|
|
12
|
-
* emit it and `runCount` reads it back, and a rename on one side alone would quietly count zero.
|
|
13
|
-
*/
|
|
14
|
-
export declare const COUNT_ALIAS = "count";
|
|
15
10
|
export declare abstract class AbstractSqlDialect extends IndexSqlDialect implements QueryDialect, SqlQueryDialect {
|
|
16
11
|
abstract readonly dialectName: SqlDialectName;
|
|
17
12
|
abstract readonly escapeIdChar: '"' | '`';
|
|
@@ -49,7 +44,7 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
49
44
|
* bound value on `$n`-placeholder dialects once `ctx` isn't otherwise empty. Generated aliases are
|
|
50
45
|
* shared for the same reason - see {@link SqlQueryContext}.
|
|
51
46
|
*/
|
|
52
|
-
protected buildFragment(ctx: QueryContext, build:
|
|
47
|
+
protected buildFragment(ctx: QueryContext, build: QueryBuildFn): string;
|
|
53
48
|
addValue(values: unknown[], value: unknown): string;
|
|
54
49
|
/**
|
|
55
50
|
* Normalizes a parameter value for the database driver.
|
|
@@ -236,6 +231,12 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
236
231
|
pager(ctx: QueryContext, opts: QueryPager): void;
|
|
237
232
|
/** Whether this engine has row locks at all. The SQLite family locks the database instead. */
|
|
238
233
|
readonly supportsRowLocks: boolean;
|
|
234
|
+
/**
|
|
235
|
+
* Whether a `FOR UPDATE` may share a statement with a window function. The MySQL family runs the
|
|
236
|
+
* pair; the Postgres family rejects it outright ("FOR UPDATE is not allowed with window functions"),
|
|
237
|
+
* which is what a paged read carrying its own `COUNT(*) OVER ()` total becomes under a `$lock`.
|
|
238
|
+
*/
|
|
239
|
+
readonly supportsWindowWithRowLock: boolean;
|
|
239
240
|
/** MariaDB is the one engine here that cannot narrow a lock to one table of a join. */
|
|
240
241
|
readonly supportsLockOf: boolean;
|
|
241
242
|
/** Validated before the querier checks for a transaction, so the clearer error wins. */
|
|
@@ -247,6 +248,16 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
247
248
|
*/
|
|
248
249
|
protected appendLock<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, joins?: QueryJoins): void;
|
|
249
250
|
count<E>(ctx: QueryContext, entity: Type<E>, q: QueryFilter<E>, opts?: QueryOptions): void;
|
|
251
|
+
/**
|
|
252
|
+
* How many rows a `$distinct` read returns, which `COUNT(*)` cannot answer: the deduplication
|
|
253
|
+
* happens after it counts, and a window function is no better - it counts before `DISTINCT` too.
|
|
254
|
+
* So the deduplicated set is made a derived table and its rows are counted. Every engine here
|
|
255
|
+
* supports one; MySQL is the reason it is aliased.
|
|
256
|
+
*
|
|
257
|
+
* The inner query takes the projection and the filter but never the page: the caller is asking how
|
|
258
|
+
* many rows there are beyond the page it already has.
|
|
259
|
+
*/
|
|
260
|
+
countDistinct<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, opts?: QueryOptions): void;
|
|
250
261
|
/**
|
|
251
262
|
* The statistic the engine already keeps, as a `count` column. Overridden by the dialects that
|
|
252
263
|
* keep one; the rest throw, because falling back to `COUNT(*)` would run exactly the scan the
|
|
@@ -2,17 +2,13 @@ import { getMeta } from '../entity/index.js';
|
|
|
2
2
|
import { parseQueryLock, QueryRaw, RAW_ALIAS, RAW_VALUE, } from '../type/index.js';
|
|
3
3
|
import { asSelectMap, assertNonNegativeInteger, buildQueryWhereAsMap, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getSoftDeleteValue, hasKeys, isBooleanType, isJsonType, isJsonUpdateOp, isNumericType, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationSize, parseSortByCount, populatesRelations, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
4
4
|
import { escapeAnsiSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
5
|
+
import { COUNT_ALIAS, DISTINCT_DERIVED_ALIAS, JSON_ELEM_ALIAS_PREFIX } from './aliases.js';
|
|
5
6
|
import { IndexSqlDialect } from './indexSqlDialect.js';
|
|
6
7
|
import { buildElemMatchConditions } from './jsonArrayElemMatchUtils.js';
|
|
7
|
-
import { isJsonbOp,
|
|
8
|
+
import { isJsonbOp, jsonCompareMode, jsonElemExists } from './jsonSql.js';
|
|
8
9
|
import { SqlQueryContext } from './queryContext.js';
|
|
9
10
|
import { NO_JOINS, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
|
|
10
11
|
import { isVectorFieldType, resolveVectorCast } from './vectorCast.js';
|
|
11
|
-
/**
|
|
12
|
-
* The column a counting statement answers in. Shared rather than spelled at each end: the dialects
|
|
13
|
-
* emit it and `runCount` reads it back, and a rename on one side alone would quietly count zero.
|
|
14
|
-
*/
|
|
15
|
-
export const COUNT_ALIAS = 'count';
|
|
16
12
|
export class AbstractSqlDialect extends IndexSqlDialect {
|
|
17
13
|
/**
|
|
18
14
|
* How this engine declares a namespace, so a generated migration creates the schemas its tables
|
|
@@ -96,8 +92,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
96
92
|
}
|
|
97
93
|
returningId(entity) {
|
|
98
94
|
const meta = getMeta(entity);
|
|
99
|
-
const
|
|
100
|
-
const idName = this.columnOf(meta, idKey);
|
|
95
|
+
const idName = this.columnOf(meta, meta.id);
|
|
101
96
|
return `RETURNING ${this.escapeId(idName)} ${this.escapeId('id')}`;
|
|
102
97
|
}
|
|
103
98
|
search(ctx, entity, q = {}, opts = {}, joins = NO_JOINS) {
|
|
@@ -753,6 +748,12 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
753
748
|
}
|
|
754
749
|
/** Whether this engine has row locks at all. The SQLite family locks the database instead. */
|
|
755
750
|
supportsRowLocks = true;
|
|
751
|
+
/**
|
|
752
|
+
* Whether a `FOR UPDATE` may share a statement with a window function. The MySQL family runs the
|
|
753
|
+
* pair; the Postgres family rejects it outright ("FOR UPDATE is not allowed with window functions"),
|
|
754
|
+
* which is what a paged read carrying its own `COUNT(*) OVER ()` total becomes under a `$lock`.
|
|
755
|
+
*/
|
|
756
|
+
supportsWindowWithRowLock = true;
|
|
756
757
|
/** MariaDB is the one engine here that cannot narrow a lock to one table of a join. */
|
|
757
758
|
supportsLockOf = true;
|
|
758
759
|
/** Validated before the querier checks for a transaction, so the clearer error wins. */
|
|
@@ -794,6 +795,21 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
794
795
|
this.select(ctx, entity, { $select: [raw('COUNT(*)', COUNT_ALIAS)] });
|
|
795
796
|
this.search(ctx, entity, { $where: q.$where }, opts);
|
|
796
797
|
}
|
|
798
|
+
/**
|
|
799
|
+
* How many rows a `$distinct` read returns, which `COUNT(*)` cannot answer: the deduplication
|
|
800
|
+
* happens after it counts, and a window function is no better - it counts before `DISTINCT` too.
|
|
801
|
+
* So the deduplicated set is made a derived table and its rows are counted. Every engine here
|
|
802
|
+
* supports one; MySQL is the reason it is aliased.
|
|
803
|
+
*
|
|
804
|
+
* The inner query takes the projection and the filter but never the page: the caller is asking how
|
|
805
|
+
* many rows there are beyond the page it already has.
|
|
806
|
+
*/
|
|
807
|
+
countDistinct(ctx, entity, q, opts) {
|
|
808
|
+
ctx.append(`SELECT COUNT(*) ${this.escapeId(COUNT_ALIAS, true)} FROM (`);
|
|
809
|
+
this.select(ctx, entity, q, opts);
|
|
810
|
+
this.search(ctx, entity, { $where: q.$where }, opts);
|
|
811
|
+
ctx.append(`) ${this.escapeId(DISTINCT_DERIVED_ALIAS, true)}`);
|
|
812
|
+
}
|
|
797
813
|
/**
|
|
798
814
|
* The statistic the engine already keeps, as a `count` column. Overridden by the dialects that
|
|
799
815
|
* keep one; the rest throw, because falling back to `COUNT(*)` would run exactly the scan the
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every identifier UQL invents for itself: a column a statement answers in, a derived table it wraps
|
|
3
|
+
* a set in, a temporary field a pipeline parks a value on.
|
|
4
|
+
*
|
|
5
|
+
* All of them share the `_uql` prefix, which is what keeps them off a user's own column or field, and
|
|
6
|
+
* all of them are declared here rather than beside the code that emits them: the end that writes one
|
|
7
|
+
* and the end that reads it back are usually in different modules, and a drift between the two fails
|
|
8
|
+
* silently - a count of zero, or an ordering that ranks everything equal. Collected in one file so
|
|
9
|
+
* the whole reserved namespace can be read at a glance before a new name is added to it.
|
|
10
|
+
*/
|
|
11
|
+
/** The column every internally-built count answers in: `COUNT(*)`, a grouped tally, a `$count` stage. */
|
|
12
|
+
export declare const COUNT_ALIAS = "_uql_count";
|
|
13
|
+
/** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
|
|
14
|
+
export declare const TOTAL_ALIAS = "_uql_total";
|
|
15
|
+
/** The derived table a `$distinct` count wraps its deduplicated set in. MySQL requires the alias. */
|
|
16
|
+
export declare const DISTINCT_DERIVED_ALIAS = "_uql_distinct";
|
|
17
|
+
/** Prefix for the alias an exploded JSON array element is read through. */
|
|
18
|
+
export declare const JSON_ELEM_ALIAS_PREFIX = "_uql_elem";
|
|
19
|
+
/** The alias a `$pull` reads its surviving elements through, kept distinct from {@link JSON_ELEM_ALIAS_PREFIX}. */
|
|
20
|
+
export declare const JSON_PULL_ALIAS = "_uql_pull";
|
|
21
|
+
/** Prefix for the field a MongoDB relation lookup parks its result on, one per condition. */
|
|
22
|
+
export declare const REL_TEMP_PREFIX = "_uql_rel_";
|
|
23
|
+
/** The field a ManyToMany lookup nests its target match under, inside the junction's own pipeline. */
|
|
24
|
+
export declare const REL_NESTED_KEY = "_uql_target";
|
|
25
|
+
/**
|
|
26
|
+
* Where a `$sort` by a relation's size parks its tally until the ordering has run. A function, so the
|
|
27
|
+
* `$sort` that names the field and the stage that produces it cannot spell it differently - MongoDB
|
|
28
|
+
* ranks a field that is not there as all-equal rather than failing, so a drift would go unnoticed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function sortCountField(relKey: string): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every identifier UQL invents for itself: a column a statement answers in, a derived table it wraps
|
|
3
|
+
* a set in, a temporary field a pipeline parks a value on.
|
|
4
|
+
*
|
|
5
|
+
* All of them share the `_uql` prefix, which is what keeps them off a user's own column or field, and
|
|
6
|
+
* all of them are declared here rather than beside the code that emits them: the end that writes one
|
|
7
|
+
* and the end that reads it back are usually in different modules, and a drift between the two fails
|
|
8
|
+
* silently - a count of zero, or an ordering that ranks everything equal. Collected in one file so
|
|
9
|
+
* the whole reserved namespace can be read at a glance before a new name is added to it.
|
|
10
|
+
*/
|
|
11
|
+
/** The column every internally-built count answers in: `COUNT(*)`, a grouped tally, a `$count` stage. */
|
|
12
|
+
export const COUNT_ALIAS = '_uql_count';
|
|
13
|
+
/** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
|
|
14
|
+
export const TOTAL_ALIAS = '_uql_total';
|
|
15
|
+
/** The derived table a `$distinct` count wraps its deduplicated set in. MySQL requires the alias. */
|
|
16
|
+
export const DISTINCT_DERIVED_ALIAS = '_uql_distinct';
|
|
17
|
+
/** Prefix for the alias an exploded JSON array element is read through. */
|
|
18
|
+
export const JSON_ELEM_ALIAS_PREFIX = '_uql_elem';
|
|
19
|
+
/** The alias a `$pull` reads its surviving elements through, kept distinct from {@link JSON_ELEM_ALIAS_PREFIX}. */
|
|
20
|
+
export const JSON_PULL_ALIAS = '_uql_pull';
|
|
21
|
+
/** Prefix for the field a MongoDB relation lookup parks its result on, one per condition. */
|
|
22
|
+
export const REL_TEMP_PREFIX = '_uql_rel_';
|
|
23
|
+
/** The field a ManyToMany lookup nests its target match under, inside the junction's own pipeline. */
|
|
24
|
+
export const REL_NESTED_KEY = '_uql_target';
|
|
25
|
+
/**
|
|
26
|
+
* Where a `$sort` by a relation's size parks its tally until the ordering has run. A function, so the
|
|
27
|
+
* `$sort` that names the field and the stage that produces it cannot spell it differently - MongoDB
|
|
28
|
+
* ranks a field that is not there as all-equal rather than failing, so a drift would go unnoticed.
|
|
29
|
+
*/
|
|
30
|
+
export function sortCountField(relKey) {
|
|
31
|
+
return `_uql_sort_count_${relKey}`;
|
|
32
|
+
}
|
|
@@ -1,24 +1,4 @@
|
|
|
1
1
|
import type { FieldOptions } from '../type/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* Alias prefix for the derived table a dialect explodes a JSON array into to test `$all`/
|
|
4
|
-
* `$elemMatch` (e.g. SQLite's `json_each(col) AS _uql_elem_1`). Passed to
|
|
5
|
-
* {@link QueryContext.nextAlias} for a fresh, uniquely-numbered name per call - `$elemMatch`/`$all`
|
|
6
|
-
* can recurse into this on a nested array, so a single fixed alias would let the inner occurrence
|
|
7
|
-
* shadow the outer one it needs to correlate against (confirmed on SQLite and MySQL: reusing one
|
|
8
|
-
* literal alias at two nesting depths silently returned zero rows instead of the matching ones).
|
|
9
|
-
* Leading underscore keeps it a valid unquoted identifier on every dialect (unlike a leading `$`,
|
|
10
|
-
* which Postgres/SQLite only allow after the first character) while staying an unlikely real
|
|
11
|
-
* column/relation/`$select` alias name.
|
|
12
|
-
*/
|
|
13
|
-
export declare const JSON_ELEM_ALIAS_PREFIX = "_uql_elem";
|
|
14
|
-
/**
|
|
15
|
-
* Alias for the derived table a dialect explodes a JSON array into to evaluate `$pull` (e.g.
|
|
16
|
-
* SQLite's `json_each(col) AS _uql_pull`). Kept distinct from {@link JSON_ELEM_ALIAS_PREFIX} since
|
|
17
|
-
* the two subqueries have different shapes (an `EXISTS` boolean vs. a `json_group_array` rebuild)
|
|
18
|
-
* and could in principle both be in scope if this ever supports nesting one inside the other's
|
|
19
|
-
* condition.
|
|
20
|
-
*/
|
|
21
|
-
export declare const JSON_PULL_ALIAS = "_uql_pull";
|
|
22
2
|
/**
|
|
23
3
|
* A `'$.a.b'` JSON path literal, each dot-separated segment escaped. `suffix` appends an accessor
|
|
24
4
|
* such as `[#]` or `[*]`. Shared across dialects unchanged: no dialect escapes a JSON path key
|
package/dist/dialect/jsonSql.js
CHANGED
|
@@ -1,24 +1,4 @@
|
|
|
1
1
|
import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
2
|
-
/**
|
|
3
|
-
* Alias prefix for the derived table a dialect explodes a JSON array into to test `$all`/
|
|
4
|
-
* `$elemMatch` (e.g. SQLite's `json_each(col) AS _uql_elem_1`). Passed to
|
|
5
|
-
* {@link QueryContext.nextAlias} for a fresh, uniquely-numbered name per call - `$elemMatch`/`$all`
|
|
6
|
-
* can recurse into this on a nested array, so a single fixed alias would let the inner occurrence
|
|
7
|
-
* shadow the outer one it needs to correlate against (confirmed on SQLite and MySQL: reusing one
|
|
8
|
-
* literal alias at two nesting depths silently returned zero rows instead of the matching ones).
|
|
9
|
-
* Leading underscore keeps it a valid unquoted identifier on every dialect (unlike a leading `$`,
|
|
10
|
-
* which Postgres/SQLite only allow after the first character) while staying an unlikely real
|
|
11
|
-
* column/relation/`$select` alias name.
|
|
12
|
-
*/
|
|
13
|
-
export const JSON_ELEM_ALIAS_PREFIX = '_uql_elem';
|
|
14
|
-
/**
|
|
15
|
-
* Alias for the derived table a dialect explodes a JSON array into to evaluate `$pull` (e.g.
|
|
16
|
-
* SQLite's `json_each(col) AS _uql_pull`). Kept distinct from {@link JSON_ELEM_ALIAS_PREFIX} since
|
|
17
|
-
* the two subqueries have different shapes (an `EXISTS` boolean vs. a `json_group_array` rebuild)
|
|
18
|
-
* and could in principle both be in scope if this ever supports nesting one inside the other's
|
|
19
|
-
* condition.
|
|
20
|
-
*/
|
|
21
|
-
export const JSON_PULL_ALIAS = '_uql_pull';
|
|
22
2
|
/**
|
|
23
3
|
* A `'$.a.b'` JSON path literal, each dot-separated segment escaped. `suffix` appends an accessor
|
|
24
4
|
* such as `[#]` or `[*]`. Shared across dialects unchanged: no dialect escapes a JSON path key
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { getMeta } from '../entity/index.js';
|
|
2
2
|
import { getFieldKeys } from '../util/index.js';
|
|
3
3
|
import { escapeMysqlSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
4
|
-
import { AbstractSqlDialect
|
|
5
|
-
import {
|
|
4
|
+
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
5
|
+
import { COUNT_ALIAS, JSON_PULL_ALIAS } from './aliases.js';
|
|
6
|
+
import { jsonAssignCall, jsonPath, jsonRemoveCall, jsonSetTarget } from './jsonSql.js';
|
|
6
7
|
/** The row count MySQL's manual gives for "all rows from the offset on": the largest `BIGINT UNSIGNED`. */
|
|
7
8
|
const MAX_LIMIT = BigInt.asUintN(64, -1n);
|
|
8
9
|
/**
|
|
@@ -10,6 +10,8 @@ import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
|
10
10
|
* syntax; CockroachDB's vector type and `CREATE VECTOR INDEX` syntax are both native).
|
|
11
11
|
*/
|
|
12
12
|
export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
13
|
+
/** `FOR UPDATE` and a window function cannot share a statement here. See the base declaration. */
|
|
14
|
+
readonly supportsWindowWithRowLock = false;
|
|
13
15
|
/** Default {@link DialectFeatures} for Postgres-wire dialects. */
|
|
14
16
|
protected readonly featureDefaults: DialectFeatures;
|
|
15
17
|
readonly escapeIdChar = "\"";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { QueryRaw, } from '../type/index.js';
|
|
2
2
|
import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
3
3
|
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
4
|
-
import { JSON_PULL_ALIAS
|
|
4
|
+
import { JSON_PULL_ALIAS } from './aliases.js';
|
|
5
|
+
import { jsonSetTarget } from './jsonSql.js';
|
|
5
6
|
import { resolveVectorCast, toSparsevecLiteral } from './vectorCast.js';
|
|
6
7
|
/**
|
|
7
8
|
* Shared AST/quoting/JSONB/full-text-search/vector-search implementation between Postgres and
|
|
@@ -13,6 +14,8 @@ import { resolveVectorCast, toSparsevecLiteral } from './vectorCast.js';
|
|
|
13
14
|
* syntax; CockroachDB's vector type and `CREATE VECTOR INDEX` syntax are both native).
|
|
14
15
|
*/
|
|
15
16
|
export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
17
|
+
/** `FOR UPDATE` and a window function cannot share a statement here. See the base declaration. */
|
|
18
|
+
supportsWindowWithRowLock = false;
|
|
16
19
|
/** Default {@link DialectFeatures} for Postgres-wire dialects. */
|
|
17
20
|
featureDefaults = {
|
|
18
21
|
explicitJsonCast: false,
|
|
@@ -18,15 +18,6 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
18
18
|
readonly dialectName = "mongodb";
|
|
19
19
|
readonly insertIdSource = "returning";
|
|
20
20
|
private static readonly ID_KEY;
|
|
21
|
-
/** Temporary lookup fields for relation conditions, dropped with `$unset` after the `$match`. */
|
|
22
|
-
private static readonly REL_TEMP_PREFIX;
|
|
23
|
-
/**
|
|
24
|
-
* Where a `$sort` by a relation's size parks its tally, until the ordering has run. One spelling
|
|
25
|
-
* for both ends: the `$sort` names this field and {@link sortCountStages} produces it, and MongoDB
|
|
26
|
-
* ranks a field that is not there as all-equal rather than failing, so a drift would go unnoticed.
|
|
27
|
-
*/
|
|
28
|
-
private static sortCountField;
|
|
29
|
-
private static readonly REL_NESTED_KEY;
|
|
30
21
|
private static readonly VECTOR_INDEX_TYPES;
|
|
31
22
|
/** Atlas rejects a `$vectorSearch` asking for more candidates than this. */
|
|
32
23
|
private static readonly MAX_NUM_CANDIDATES;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ObjectId } from 'mongodb';
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
|
+
import { COUNT_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, sortCountField } from '../dialect/aliases.js';
|
|
3
4
|
import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
|
|
4
5
|
import { getMeta } from '../entity/index.js';
|
|
5
6
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
6
|
-
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap,
|
|
7
|
+
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, fillOnFields, filterFieldKeys, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationSize, parseSortByCount, someKey, } from '../util/index.js';
|
|
7
8
|
/** Default {@link DialectFeatures} for MongoDB; shared by {@link MongoDialect} and its schema generator. */
|
|
8
9
|
export const mongoDialectFeatures = {
|
|
9
10
|
explicitJsonCast: false,
|
|
@@ -27,17 +28,6 @@ export class MongoDialect extends AbstractDialect {
|
|
|
27
28
|
// The MongoDB driver reports the exact `_id` of every inserted document (`insertedIds`).
|
|
28
29
|
insertIdSource = 'returning';
|
|
29
30
|
static ID_KEY = '_id';
|
|
30
|
-
/** Temporary lookup fields for relation conditions, dropped with `$unset` after the `$match`. */
|
|
31
|
-
static REL_TEMP_PREFIX = '__uql_rel_';
|
|
32
|
-
/**
|
|
33
|
-
* Where a `$sort` by a relation's size parks its tally, until the ordering has run. One spelling
|
|
34
|
-
* for both ends: the `$sort` names this field and {@link sortCountStages} produces it, and MongoDB
|
|
35
|
-
* ranks a field that is not there as all-equal rather than failing, so a drift would go unnoticed.
|
|
36
|
-
*/
|
|
37
|
-
static sortCountField(relKey) {
|
|
38
|
-
return `__uql_sort_count_${relKey}`;
|
|
39
|
-
}
|
|
40
|
-
static REL_NESTED_KEY = '__uql_target';
|
|
41
31
|
static VECTOR_INDEX_TYPES = new Set(['vectorSearch', 'hnsw', 'ivfflat', 'vector']);
|
|
42
32
|
/** Atlas rejects a `$vectorSearch` asking for more candidates than this. */
|
|
43
33
|
static MAX_NUM_CANDIDATES = 10_000;
|
|
@@ -149,10 +139,10 @@ export class MongoDialect extends AbstractDialect {
|
|
|
149
139
|
const relOpts = meta.relations[relKey];
|
|
150
140
|
const relEntity = relOpts.entity();
|
|
151
141
|
const relMeta = getMeta(relEntity);
|
|
152
|
-
const temp = `${
|
|
142
|
+
const temp = `${REL_TEMP_PREFIX}${lookups.temps.length}`;
|
|
153
143
|
const sizeVal = parseRelationSize(val);
|
|
154
144
|
// `$count` for a size test, `$limit: 1` for existence: neither returns the matched documents.
|
|
155
|
-
const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count:
|
|
145
|
+
const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count: COUNT_ALIAS }];
|
|
156
146
|
// Scope first, render once - merging the target's filters into an already-rendered filter would
|
|
157
147
|
// leave their own keys unmapped. The caller's filter bypass is deliberately *not* passed down:
|
|
158
148
|
// `withDeleted()` or `hardDelete` on the parent must not un-hide trashed rows of the target, the
|
|
@@ -190,7 +180,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
190
180
|
const throughEntity = relOpts.through();
|
|
191
181
|
const throughMeta = getMeta(throughEntity);
|
|
192
182
|
const junctionScope = this.renderFilter(throughEntity, this.scopedWhereMap(throughMeta, {}), opts);
|
|
193
|
-
const nested =
|
|
183
|
+
const nested = REL_NESTED_KEY;
|
|
194
184
|
return {
|
|
195
185
|
$lookup: {
|
|
196
186
|
from: this.resolveTableName(throughMeta),
|
|
@@ -219,7 +209,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
219
209
|
* the `$ifNull` fallback to 0, so `{ $size: 0 }` matches parents with no related row at all.
|
|
220
210
|
*/
|
|
221
211
|
compareRelationCount(temp, sizeVal) {
|
|
222
|
-
const count = { $ifNull: [{ $arrayElemAt: [`$${temp}.${
|
|
212
|
+
const count = { $ifNull: [{ $arrayElemAt: [`$${temp}.${COUNT_ALIAS}`, 0] }, 0] };
|
|
223
213
|
if (typeof sizeVal === 'number') {
|
|
224
214
|
return { $eq: [count, sizeVal] };
|
|
225
215
|
}
|
|
@@ -420,7 +410,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
420
410
|
if (path) {
|
|
421
411
|
throw new TypeError(`$sort by '${relPath}.$count' is only supported on the queried entity`);
|
|
422
412
|
}
|
|
423
|
-
out[
|
|
413
|
+
out[sortCountField(key)] = sortDirection(countDirection);
|
|
424
414
|
continue;
|
|
425
415
|
}
|
|
426
416
|
const { join, sort: relationSort } = resolveSortableJoin(relation, relPath, value, joins, `cannot $sort by relation '${relPath}' on MongoDB unless it is populated: only $populate adds its fields to the document`);
|
|
@@ -443,11 +433,11 @@ export class MongoDialect extends AbstractDialect {
|
|
|
443
433
|
}
|
|
444
434
|
const relEntity = relOpts.entity();
|
|
445
435
|
const relMeta = getMeta(relEntity);
|
|
446
|
-
const temp =
|
|
436
|
+
const temp = sortCountField(key);
|
|
447
437
|
const targetScope = this.renderFilter(relEntity, this.scopedWhereMap(relMeta, {}), opts);
|
|
448
|
-
const tail = [{ $count:
|
|
438
|
+
const tail = [{ $count: COUNT_ALIAS }];
|
|
449
439
|
stages.push(this.relationLookup(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts), {
|
|
450
|
-
$addFields: { [temp]: { $ifNull: [{ $arrayElemAt: [`$${temp}.${
|
|
440
|
+
$addFields: { [temp]: { $ifNull: [{ $arrayElemAt: [`$${temp}.${COUNT_ALIAS}`, 0] }, 0] } },
|
|
451
441
|
});
|
|
452
442
|
fields.push(temp);
|
|
453
443
|
}
|
|
@@ -23,9 +23,9 @@ export declare class MongodbQuerier extends AbstractQuerier {
|
|
|
23
23
|
private buildVectorPipeline;
|
|
24
24
|
protected internalAggregate<E extends Document, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
25
25
|
/**
|
|
26
|
-
* A `$required` relation drops parents that have no match,
|
|
27
|
-
*
|
|
28
|
-
* {@link internalCount}, which needs no pipeline of its own.
|
|
26
|
+
* A `$required` relation drops parents that have no match, and `$distinct` collapses them, so both
|
|
27
|
+
* totals have to be taken after the stage that does it - which only the read pipeline builds. Every
|
|
28
|
+
* other query counts through {@link internalCount}, which needs no pipeline of its own.
|
|
29
29
|
*/
|
|
30
30
|
protected internalFindManyAndCount<E extends Document>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
|
|
31
31
|
protected internalCount<E extends Document>(entity: Type<E>, qm?: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
1
2
|
import { hasRequiredJoin } from '../dialect/queryJoins.js';
|
|
2
3
|
import { getMeta } from '../entity/index.js';
|
|
3
4
|
import { AbstractQuerier, enrichError } from '../querier/index.js';
|
|
4
|
-
import {
|
|
5
|
+
import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
6
|
/**
|
|
6
7
|
* `$limit: 0` asks for no rows, the way it does on every SQL dialect - but MongoDB reads `limit(0)`
|
|
7
8
|
* as *unlimited*, so a read that passed it straight to the driver came back with the whole
|
|
@@ -151,31 +152,33 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
/**
|
|
154
|
-
* A `$required` relation drops parents that have no match,
|
|
155
|
-
*
|
|
156
|
-
* {@link internalCount}, which needs no pipeline of its own.
|
|
155
|
+
* A `$required` relation drops parents that have no match, and `$distinct` collapses them, so both
|
|
156
|
+
* totals have to be taken after the stage that does it - which only the read pipeline builds. Every
|
|
157
|
+
* other query counts through {@link internalCount}, which needs no pipeline of its own.
|
|
157
158
|
*/
|
|
158
159
|
async internalFindManyAndCount(entity, q, opts) {
|
|
159
|
-
if (!hasRequiredJoin(getMeta(entity), q)) {
|
|
160
|
+
if (!q.$distinct && !hasRequiredJoin(getMeta(entity), q)) {
|
|
160
161
|
return super.internalFindManyAndCount(entity, q, opts);
|
|
161
162
|
}
|
|
162
163
|
const { $sort: _sort, $skip: _skip, $limit: _limit, ...unpaged } = q;
|
|
163
164
|
const [founds, counted] = await Promise.all([
|
|
164
165
|
this.internalFindMany(entity, q, opts),
|
|
165
166
|
this.execute((session) => this.collection(entity)
|
|
166
|
-
.aggregate([...this.dialect.aggregationPipeline(entity, unpaged, opts), { $count:
|
|
167
|
+
.aggregate([...this.dialect.aggregationPipeline(entity, unpaged, opts), { $count: COUNT_ALIAS }], { session })
|
|
167
168
|
.toArray()),
|
|
168
169
|
]);
|
|
169
|
-
return [founds, counted[0]?.[
|
|
170
|
+
return [founds, counted[0]?.[COUNT_ALIAS] ?? 0];
|
|
170
171
|
}
|
|
171
172
|
async internalCount(entity, qm = {}, opts) {
|
|
172
173
|
return this.timed('internalCount', undefined, async () => {
|
|
173
174
|
if (this.dialect.constrainsRelations(entity, qm.$where)) {
|
|
174
175
|
const { stages, filter } = this.dialect.whereWithRelations(entity, qm.$where, opts);
|
|
175
176
|
const [counted] = await this.execute((session) => this.collection(entity)
|
|
176
|
-
.aggregate([...stages, { $match: filter }, { $count:
|
|
177
|
+
.aggregate([...stages, { $match: filter }, { $count: COUNT_ALIAS }], {
|
|
178
|
+
session,
|
|
179
|
+
})
|
|
177
180
|
.toArray());
|
|
178
|
-
return counted?.[
|
|
181
|
+
return counted?.[COUNT_ALIAS] ?? 0;
|
|
179
182
|
}
|
|
180
183
|
const filter = this.dialect.where(entity, qm.$where, opts);
|
|
181
184
|
return this.execute((session) => this.collection(entity).countDocuments(filter, {
|
|
@@ -218,13 +218,12 @@ export class AbstractQuerier {
|
|
|
218
218
|
const toInsert = [];
|
|
219
219
|
const toUpdate = [];
|
|
220
220
|
const existingIds = [];
|
|
221
|
-
const idKey = (meta.id ?? 'id');
|
|
222
221
|
for (const it of payload) {
|
|
223
|
-
const id = it[
|
|
222
|
+
const id = it[meta.id];
|
|
224
223
|
if (!id) {
|
|
225
224
|
toInsert.push(it);
|
|
226
225
|
}
|
|
227
|
-
else if (!someKey(it, (key) => key !==
|
|
226
|
+
else if (!someKey(it, (key) => key !== meta.id)) {
|
|
228
227
|
existingIds.push(id);
|
|
229
228
|
}
|
|
230
229
|
else {
|
|
@@ -234,9 +233,9 @@ export class AbstractQuerier {
|
|
|
234
233
|
const [insertedIds, updatedIds] = await Promise.all([
|
|
235
234
|
toInsert.length ? this.insertMany(entity, toInsert) : [],
|
|
236
235
|
Promise.all(toUpdate.map(async (it) => {
|
|
237
|
-
const id = it[
|
|
236
|
+
const id = it[meta.id];
|
|
238
237
|
const data = { ...it };
|
|
239
|
-
delete data[
|
|
238
|
+
delete data[meta.id];
|
|
240
239
|
await this.updateOneById(entity, id, data);
|
|
241
240
|
return id;
|
|
242
241
|
})),
|
|
@@ -52,8 +52,18 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
52
52
|
* the end, or a filter nothing matched.
|
|
53
53
|
*
|
|
54
54
|
* A `$required` relation needs no special case: the window counts what the INNER JOIN left, which
|
|
55
|
-
* is exactly the total a caller of a filtered read is asking for.
|
|
55
|
+
* is exactly the total a caller of a filtered read is asking for. A `$lock` is the one clause an
|
|
56
|
+
* engine may refuse to have in the same statement, which {@link AbstractSqlDialect.supportsWindowWithRowLock}
|
|
57
|
+
* answers; where it does, the total comes from a count of its own. A `$distinct` read needs one
|
|
58
|
+
* too, and a deduplicating one: see {@link AbstractSqlDialect.countDistinct}.
|
|
56
59
|
*/
|
|
60
|
+
/**
|
|
61
|
+
* How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
|
|
62
|
+
* `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
|
|
63
|
+
* before the deduplication and so overstates the page, and `$lock` on an engine that refuses the
|
|
64
|
+
* pair outright. Both then cost a second statement; only the counting differs.
|
|
65
|
+
*/
|
|
66
|
+
private countedSeparately;
|
|
57
67
|
protected internalFindManyAndCount<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
|
|
58
68
|
private selectRows;
|
|
59
69
|
private hydrateRows;
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import { COUNT_ALIAS } from '../dialect/
|
|
1
|
+
import { COUNT_ALIAS, TOTAL_ALIAS } from '../dialect/aliases.js';
|
|
2
2
|
import { decodeColumn } from '../dialect/hydrateColumn.js';
|
|
3
3
|
import { getMeta } from '../entity/index.js';
|
|
4
4
|
import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, getRelationRequestSummary, hasKeys, idOnlyQuery, isAutoIncrement, isPagedQuery, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
5
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
6
6
|
import { enrichError } from './queryError.js';
|
|
7
|
-
/**
|
|
8
|
-
* The column a paged read carries its unpaged total in. Prefixed the way the other internal aliases
|
|
9
|
-
* are, since it rides in the same row as the entity's own columns and must not shadow one.
|
|
10
|
-
*/
|
|
11
|
-
const TOTAL_ALIAS = '_uql_total';
|
|
12
7
|
export class AbstractSqlQuerier extends AbstractQuerier {
|
|
13
8
|
dialect;
|
|
14
9
|
extra;
|
|
@@ -90,9 +85,31 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
90
85
|
* the end, or a filter nothing matched.
|
|
91
86
|
*
|
|
92
87
|
* A `$required` relation needs no special case: the window counts what the INNER JOIN left, which
|
|
93
|
-
* is exactly the total a caller of a filtered read is asking for.
|
|
88
|
+
* is exactly the total a caller of a filtered read is asking for. A `$lock` is the one clause an
|
|
89
|
+
* engine may refuse to have in the same statement, which {@link AbstractSqlDialect.supportsWindowWithRowLock}
|
|
90
|
+
* answers; where it does, the total comes from a count of its own. A `$distinct` read needs one
|
|
91
|
+
* too, and a deduplicating one: see {@link AbstractSqlDialect.countDistinct}.
|
|
94
92
|
*/
|
|
93
|
+
/**
|
|
94
|
+
* How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
|
|
95
|
+
* `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
|
|
96
|
+
* before the deduplication and so overstates the page, and `$lock` on an engine that refuses the
|
|
97
|
+
* pair outright. Both then cost a second statement; only the counting differs.
|
|
98
|
+
*/
|
|
99
|
+
countedSeparately(entity, q, opts) {
|
|
100
|
+
if (q.$distinct) {
|
|
101
|
+
return (ctx) => this.dialect.countDistinct(ctx, entity, q, opts);
|
|
102
|
+
}
|
|
103
|
+
if (q.$lock && !this.dialect.supportsWindowWithRowLock) {
|
|
104
|
+
return (ctx) => this.dialect.count(ctx, entity, q, opts);
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
95
108
|
async internalFindManyAndCount(entity, q, opts) {
|
|
109
|
+
const separately = this.countedSeparately(entity, q, opts);
|
|
110
|
+
if (separately) {
|
|
111
|
+
return Promise.all([this.internalFindMany(entity, q, opts), this.runCount(separately)]);
|
|
112
|
+
}
|
|
96
113
|
const rows = await this.selectRows(entity, q, opts, TOTAL_ALIAS);
|
|
97
114
|
const total = rows.length ? Number(rows[0][TOTAL_ALIAS]) : await this.internalCount(entity, q, opts);
|
|
98
115
|
for (const row of rows) {
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
+
import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
1
2
|
import { getMeta } from '../entity/index.js';
|
|
2
3
|
import { COUNT_RESULT_KEY } from '../type/index.js';
|
|
3
|
-
import { asSelectMap,
|
|
4
|
-
/**
|
|
5
|
-
* The one place the counting path crosses from the caller's type to a structural one: a relation's
|
|
6
|
-
* entity comes from metadata, which names its columns only as strings. Everything past this is
|
|
7
|
-
* checked against {@link CountedRow} normally.
|
|
8
|
-
*/
|
|
9
|
-
function countable(entity) {
|
|
10
|
-
return entity;
|
|
11
|
-
}
|
|
4
|
+
import { asSelectMap, getKeys, parentKeyColumn, targetKeyColumn } from '../util/index.js';
|
|
12
5
|
/**
|
|
13
6
|
* A `$count` groups its tallies by the parent's id, so the id has to outlive the projection - the
|
|
14
7
|
* same reason populating a relation keeps it. A whitelisting `$select` gains the key and an
|
|
@@ -78,10 +71,10 @@ export async function fillRelationCounts(querier, entity, payload, count) {
|
|
|
78
71
|
async function countPerParent(querier, relOpts, ids, where) {
|
|
79
72
|
const through = relOpts.through;
|
|
80
73
|
if (through) {
|
|
81
|
-
return countThroughPerParent(querier, relOpts,
|
|
74
|
+
return countThroughPerParent(querier, relOpts, through(), ids, where);
|
|
82
75
|
}
|
|
83
76
|
const foreign = parentKeyColumn(relOpts);
|
|
84
|
-
return groupedCount(querier,
|
|
77
|
+
return groupedCount(querier, relOpts.entity(), foreign, { ...where, [foreign]: ids });
|
|
85
78
|
}
|
|
86
79
|
/**
|
|
87
80
|
* A many-to-many counts its junction rows, one per pairing. A filter names the target's columns,
|
|
@@ -92,7 +85,7 @@ async function countThroughPerParent(querier, relOpts, throughEntity, ids, where
|
|
|
92
85
|
const local = parentKeyColumn(relOpts);
|
|
93
86
|
const throughWhere = { [local]: ids };
|
|
94
87
|
if (where) {
|
|
95
|
-
const target =
|
|
88
|
+
const target = relOpts.entity();
|
|
96
89
|
const targetId = getMeta(target).id;
|
|
97
90
|
const targets = await querier.findMany(target, { $select: { [targetId]: true }, $where: where });
|
|
98
91
|
throughWhere[targetKeyColumn(relOpts)] = targets.map((it) => it[targetId]);
|
|
@@ -101,11 +94,11 @@ async function countThroughPerParent(querier, relOpts, throughEntity, ids, where
|
|
|
101
94
|
}
|
|
102
95
|
/** `SELECT <key>, COUNT(*) ... GROUP BY <key>`, as a lookup from parent key to tally. */
|
|
103
96
|
async function groupedCount(querier, entity, groupKey, where) {
|
|
104
|
-
const $agg = { [
|
|
97
|
+
const $agg = { [COUNT_ALIAS]: { $count: '*' } };
|
|
105
98
|
const rows = await querier.aggregate(entity, { $group: { [groupKey]: true }, $agg, $where: where });
|
|
106
99
|
const byParent = {};
|
|
107
100
|
for (const row of rows) {
|
|
108
|
-
byParent[String(row[groupKey])] = Number(row[
|
|
101
|
+
byParent[String(row[groupKey])] = Number(row[COUNT_ALIAS]);
|
|
109
102
|
}
|
|
110
103
|
return byParent;
|
|
111
104
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AbstractSqlDialect } from '../dialect/abstractSqlDialect.js';
|
|
2
|
-
import { JSON_ELEM_ALIAS_PREFIX, JSON_PULL_ALIAS
|
|
2
|
+
import { JSON_ELEM_ALIAS_PREFIX, JSON_PULL_ALIAS } from '../dialect/aliases.js';
|
|
3
|
+
import { jsonAssignCall, jsonElemExists, jsonPath, jsonRemoveCall, jsonSetTarget } from '../dialect/jsonSql.js';
|
|
3
4
|
export class SqliteDialect extends AbstractSqlDialect {
|
|
4
5
|
/** Default {@link DialectFeatures} for SQLite and SQLite-derived dialects. */
|
|
5
6
|
featureDefaults = {
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -22,6 +22,12 @@ export type QueryWhereOptions = QueryComparisonOptions & {
|
|
|
22
22
|
*/
|
|
23
23
|
clause?: 'WHERE' | 'AND' | false;
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Emits a statement, or a fragment of one, into the context it is handed. What a caller passes when
|
|
27
|
+
* it knows *what* to build but not *where*: the statement is assembled into whichever context the
|
|
28
|
+
* receiver opens, so the two ends cannot disagree about which one it went into.
|
|
29
|
+
*/
|
|
30
|
+
export type QueryBuildFn = (ctx: QueryContext) => void;
|
|
25
31
|
export interface QueryContext {
|
|
26
32
|
append(sql: string): this;
|
|
27
33
|
addValue(value: unknown): this;
|
|
@@ -1,14 +1,4 @@
|
|
|
1
1
|
import { type CascadeType, type EntityData, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QueryVectorSearch, type QueryWhere, type QueryWhereMap, type RelationKey } from '../type/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* The alias an internally-built count answers under - a batched relation tally, a MongoDB relation
|
|
4
|
-
* lookup, a total taken past a `$required` unwind. Every one of those picks the alias and reads it
|
|
5
|
-
* back a few lines later, so they share the spelling rather than each repeating it twice.
|
|
6
|
-
*
|
|
7
|
-
* Prefixed like every other internal alias here: a batched tally selects it alongside the column it
|
|
8
|
-
* groups by, so a bare name would collide with a real column of that name and answer the grouped
|
|
9
|
-
* value twice over, with no statement failing to say so.
|
|
10
|
-
*/
|
|
11
|
-
export declare const COUNT_AGG_ALIAS = "_uql_count";
|
|
12
2
|
export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
|
|
13
3
|
export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E>, callbackKey: CallbackKey): FieldKey<E>[];
|
|
14
4
|
/**
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import { getContext, UqlSecurityError } from '../context/context.js';
|
|
2
2
|
import { QueryRaw, resolveAggregateOp, } from '../type/index.js';
|
|
3
3
|
import { getFieldKeys, getKeys, hasKeys, someKey } from './object.util.js';
|
|
4
|
-
/**
|
|
5
|
-
* The alias an internally-built count answers under - a batched relation tally, a MongoDB relation
|
|
6
|
-
* lookup, a total taken past a `$required` unwind. Every one of those picks the alias and reads it
|
|
7
|
-
* back a few lines later, so they share the spelling rather than each repeating it twice.
|
|
8
|
-
*
|
|
9
|
-
* Prefixed like every other internal alias here: a batched tally selects it alongside the column it
|
|
10
|
-
* groups by, so a bare name would collide with a real column of that name and answer the grouped
|
|
11
|
-
* value twice over, with no statement failing to say so.
|
|
12
|
-
*/
|
|
13
|
-
export const COUNT_AGG_ALIAS = '_uql_count';
|
|
14
4
|
export function filterFieldKeys(meta, payload, callbackKey) {
|
|
15
5
|
return getKeys(payload).filter((key) => {
|
|
16
6
|
const fieldOpts = meta.fields[key];
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "JSON-native ORM for Node.js, Bun and Deno. 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.37.
|
|
6
|
+
"version": "0.37.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|