uql-orm 0.36.1 → 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/browser/http/http.d.ts +1 -1
- package/dist/browser/querier/httpQuerier.d.ts +12 -7
- package/dist/browser/querier/httpQuerier.js +6 -1
- package/dist/browser/type/clientQuerier.d.ts +10 -23
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +7 -7
- package/dist/cockroachdb/cockroachDialect.d.ts +8 -1
- package/dist/cockroachdb/cockroachDialect.js +12 -0
- package/dist/dialect/abstractSqlDialect.d.ts +31 -4
- package/dist/dialect/abstractSqlDialect.js +64 -15
- 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.d.ts +6 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +20 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +2 -0
- package/dist/dialect/pgLikeSqlDialect.js +4 -1
- package/dist/dialect/queryJoins.d.ts +6 -0
- package/dist/dialect/queryJoins.js +13 -0
- package/dist/http/contract.d.ts +0 -7
- package/dist/http/handler.d.ts +2 -2
- package/dist/http/handler.js +1 -1
- package/dist/http/query.js +3 -2
- package/dist/mongo/mongoDialect.d.ts +15 -4
- package/dist/mongo/mongoDialect.js +65 -16
- package/dist/mongo/mongodbQuerier.d.ts +13 -2
- package/dist/mongo/mongodbQuerier.js +46 -2
- package/dist/postgres/postgresDialect.d.ts +7 -0
- package/dist/postgres/postgresDialect.js +13 -0
- package/dist/querier/abstractQuerier.d.ts +34 -18
- package/dist/querier/abstractQuerier.js +43 -30
- package/dist/querier/abstractQuerierPool.d.ts +9 -7
- package/dist/querier/abstractQuerierPool.js +6 -0
- package/dist/querier/abstractSqlQuerier.d.ts +30 -2
- package/dist/querier/abstractSqlQuerier.js +61 -8
- package/dist/querier/relationCount.d.ts +16 -0
- package/dist/querier/relationCount.js +104 -0
- package/dist/sqlite/sqliteDialect.d.ts +6 -1
- package/dist/sqlite/sqliteDialect.js +12 -1
- package/dist/type/dialect.d.ts +10 -3
- package/dist/type/index.d.ts +1 -0
- package/dist/type/index.js +1 -0
- package/dist/type/querier.d.ts +21 -14
- package/dist/type/query.d.ts +77 -16
- package/dist/type/query.js +17 -0
- package/dist/type/universalQuerier.d.ts +85 -48
- package/dist/type/wire.d.ts +37 -0
- package/dist/type/wire.js +1 -0
- package/dist/util/dialect.util.d.ts +6 -0
- package/dist/util/dialect.util.js +15 -0
- package/dist/util/relationQuery.util.d.ts +9 -0
- package/dist/util/relationQuery.util.js +13 -0
- package/package.json +4 -4
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { getMeta } from '../entity/index.js';
|
|
2
2
|
import { parseQueryLock, QueryRaw, RAW_ALIAS, RAW_VALUE, } from '../type/index.js';
|
|
3
|
-
import { asSelectMap, assertNonNegativeInteger, buildQueryWhereAsMap, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getSoftDeleteValue, hasKeys, isBooleanType, isJsonType, isJsonUpdateOp, isNumericType, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationSize, populatesRelations, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
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';
|
|
@@ -91,8 +92,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
91
92
|
}
|
|
92
93
|
returningId(entity) {
|
|
93
94
|
const meta = getMeta(entity);
|
|
94
|
-
const
|
|
95
|
-
const idName = this.columnOf(meta, idKey);
|
|
95
|
+
const idName = this.columnOf(meta, meta.id);
|
|
96
96
|
return `RETURNING ${this.escapeId(idName)} ${this.escapeId('id')}`;
|
|
97
97
|
}
|
|
98
98
|
search(ctx, entity, q = {}, opts = {}, joins = NO_JOINS) {
|
|
@@ -171,7 +171,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
171
171
|
appendTextSearch(_ctx, _entity, _meta, _search) {
|
|
172
172
|
throw new TypeError(`${this.dialectName} does not support $text full-text search`);
|
|
173
173
|
}
|
|
174
|
-
select(ctx, entity, q, opts = {}, joins = NO_JOINS) {
|
|
174
|
+
select(ctx, entity, q, opts = {}, joins = NO_JOINS, totalAlias) {
|
|
175
175
|
const meta = getMeta(entity);
|
|
176
176
|
const { alias, ref } = this.tableRef(meta);
|
|
177
177
|
const prefix = this.resolveRelationAwarePrefix(alias, meta, opts, q.$populate, joins);
|
|
@@ -186,6 +186,9 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
186
186
|
this.appendVectorProjection(ctx, meta, key, val);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
+
if (totalAlias) {
|
|
190
|
+
ctx.append(`, ${this.totalOverExpr} ${this.escapeId(totalAlias, true)}`);
|
|
191
|
+
}
|
|
189
192
|
ctx.append(` FROM ${ref}`);
|
|
190
193
|
// Add JOINs AFTER FROM clause
|
|
191
194
|
this.selectRelationJoins(ctx, meta, alias, joins);
|
|
@@ -691,6 +694,16 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
691
694
|
const relation = meta.relations[key];
|
|
692
695
|
if (relation) {
|
|
693
696
|
const relPath = path ? `${path}.${key}` : key;
|
|
697
|
+
const countDirection = parseSortByCount(value);
|
|
698
|
+
if (countDirection !== undefined) {
|
|
699
|
+
// A correlated count, not a join: a parent has many of these, so what is being ordered by
|
|
700
|
+
// is how many, and `SELECT DISTINCT` cannot order by an expression it did not select.
|
|
701
|
+
if (opts.distinct) {
|
|
702
|
+
throw new TypeError(`cannot $sort by '${relPath}.$count' with $distinct: it is not a selected column`);
|
|
703
|
+
}
|
|
704
|
+
columns.push(this.buildFragment(ctx, (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, meta, relation, { prefix }, 'COUNT(*)', {})) + this.resolveSortDirection(countDirection));
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
694
707
|
const { join, sort: relationSort } = resolveSortableJoin(relation, relPath, value, opts.joins ?? NO_JOINS, `cannot $sort by relation '${relPath}': this statement joins no relations`);
|
|
695
708
|
// `SELECT DISTINCT` can only order by what it selected, on every engine here, so a join
|
|
696
709
|
// brought in for the sort alone has nothing to order by. Populating it selects its columns.
|
|
@@ -735,6 +748,12 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
735
748
|
}
|
|
736
749
|
/** Whether this engine has row locks at all. The SQLite family locks the database instead. */
|
|
737
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;
|
|
738
757
|
/** MariaDB is the one engine here that cannot narrow a lock to one table of a join. */
|
|
739
758
|
supportsLockOf = true;
|
|
740
759
|
/** Validated before the querier checks for a transaction, so the clearer error wins. */
|
|
@@ -767,11 +786,37 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
767
786
|
const suffix = wait === 'skip' ? ' SKIP LOCKED' : wait === 'nowait' ? ' NOWAIT' : '';
|
|
768
787
|
ctx.append(` FOR UPDATE${target}${suffix}`);
|
|
769
788
|
}
|
|
770
|
-
// `QueryFilter`, not `
|
|
771
|
-
// would push its single row out of the result set. The
|
|
789
|
+
// `QueryFilter`, not `QueryPage`: a count orders nothing and pages nothing, and an `OFFSET`
|
|
790
|
+
// would push its single row out of the result set. The type only stops a statically-checked
|
|
791
|
+
// caller though - the HTTP handler hands this `q` straight from the wire - so `$where` is read
|
|
792
|
+
// off it explicitly rather than forwarding `q` itself into `search()`, which would honor a
|
|
793
|
+
// `$sort`/`$skip`/`$limit` an untyped caller snuck in regardless of what TypeScript allowed them.
|
|
772
794
|
count(ctx, entity, q, opts) {
|
|
773
|
-
this.select(ctx, entity, { $select: [raw('COUNT(*)',
|
|
774
|
-
this.search(ctx, entity, q, opts);
|
|
795
|
+
this.select(ctx, entity, { $select: [raw('COUNT(*)', COUNT_ALIAS)] });
|
|
796
|
+
this.search(ctx, entity, { $where: q.$where }, opts);
|
|
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
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* The statistic the engine already keeps, as a `count` column. Overridden by the dialects that
|
|
815
|
+
* keep one; the rest throw, because falling back to `COUNT(*)` would run exactly the scan the
|
|
816
|
+
* caller reached for this to avoid, and only say so by taking a long time.
|
|
817
|
+
*/
|
|
818
|
+
estimatedCount(_ctx, _entity) {
|
|
819
|
+
throw new TypeError(`${this.dialectName} does not support estimatedCount`);
|
|
775
820
|
}
|
|
776
821
|
/** `$group` aggregate operator → SQL function name. An allowlist, not a formatter: the op key
|
|
777
822
|
* comes from query data, so anything outside this map must be rejected rather than passed
|
|
@@ -882,11 +927,16 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
882
927
|
}
|
|
883
928
|
});
|
|
884
929
|
}
|
|
885
|
-
|
|
930
|
+
/**
|
|
931
|
+
* How many rows the filter matched, on every row of the page. A window function runs before
|
|
932
|
+
* LIMIT/OFFSET, so what it counts is the whole match rather than the page cut out of it.
|
|
933
|
+
*/
|
|
934
|
+
totalOverExpr = 'COUNT(*) OVER ()';
|
|
935
|
+
find(ctx, entity, q = {}, opts, totalAlias) {
|
|
886
936
|
// The one statement that can join, so the one that resolves the join set; everything else renders
|
|
887
937
|
// against `NO_JOINS` and rejects a `$sort` that would need one.
|
|
888
938
|
const joins = resolveQueryJoins(getMeta(entity), q);
|
|
889
|
-
this.select(ctx, entity, q, opts, joins);
|
|
939
|
+
this.select(ctx, entity, q, opts, joins, totalAlias);
|
|
890
940
|
this.search(ctx, entity, q, opts, joins);
|
|
891
941
|
// Appended here rather than in `search`, which `count`/`update`/`delete` share: a lock belongs
|
|
892
942
|
// to a SELECT alone. Every engine spells it after LIMIT/OFFSET, so it goes last.
|
|
@@ -1388,8 +1438,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1388
1438
|
* invisible here just as it is to a joined `$populate`. The caller's filter bypass is deliberately
|
|
1389
1439
|
* not propagated (`withDeleted()` does not reach into relations), matching `selectRelationJoins`.
|
|
1390
1440
|
*/
|
|
1391
|
-
appendRelationSubquery(ctx,
|
|
1392
|
-
const meta = getMeta(entity);
|
|
1441
|
+
appendRelationSubquery(ctx, meta, rel, opts, projection, val) {
|
|
1393
1442
|
// Aliases, not paths, everywhere a column is prefixed; `tableRef` declares them in the FROM.
|
|
1394
1443
|
const parentAlias = this.resolveTableAlias(meta);
|
|
1395
1444
|
const references = rel.references;
|
|
@@ -1429,11 +1478,11 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1429
1478
|
/** Filter by relation: a parent matches when {@link appendRelationSubquery} finds one target row. */
|
|
1430
1479
|
compareRelation(ctx, entity, val, rel, opts) {
|
|
1431
1480
|
ctx.append('EXISTS ');
|
|
1432
|
-
this.appendRelationSubquery(ctx, entity, rel, opts, '1', val);
|
|
1481
|
+
this.appendRelationSubquery(ctx, getMeta(entity), rel, opts, '1', val);
|
|
1433
1482
|
}
|
|
1434
1483
|
/** Filter by relation size: the same subquery, counting instead of testing for existence. */
|
|
1435
1484
|
compareRelationSize(ctx, entity, sizeVal, rel, opts) {
|
|
1436
|
-
this.buildSizeComparison(ctx, () => this.appendRelationSubquery(ctx, entity, rel, opts, 'COUNT(*)', {}), sizeVal);
|
|
1485
|
+
this.buildSizeComparison(ctx, () => this.appendRelationSubquery(ctx, getMeta(entity), rel, opts, 'COUNT(*)', {}), sizeVal);
|
|
1437
1486
|
}
|
|
1438
1487
|
/**
|
|
1439
1488
|
* Build a complete `$size` comparison expression.
|
|
@@ -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
|
|
@@ -13,6 +13,12 @@ import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
|
13
13
|
export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
14
14
|
/** Default {@link DialectFeatures} for MySQL-compatible SQL dialects. */
|
|
15
15
|
protected readonly featureDefaults: DialectFeatures;
|
|
16
|
+
/**
|
|
17
|
+
* `information_schema` keeps InnoDB's own row estimate, which is live enough to answer before
|
|
18
|
+
* anything has been analyzed. `DATABASE()` where the entity names no schema, so the estimate comes
|
|
19
|
+
* from the connection's own database rather than a same-named table in another one.
|
|
20
|
+
*/
|
|
21
|
+
estimatedCount<E>(ctx: QueryContext, entity: Type<E>): void;
|
|
16
22
|
/** `OFFSET` is only legal after a `LIMIT` here, so a bare `$skip` needs one. */
|
|
17
23
|
pager(ctx: QueryContext, opts: QueryPager): void;
|
|
18
24
|
readonly serialPrimaryKey = "BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY";
|
|
@@ -2,7 +2,8 @@ import { getMeta } from '../entity/index.js';
|
|
|
2
2
|
import { getFieldKeys } from '../util/index.js';
|
|
3
3
|
import { escapeMysqlSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
4
4
|
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
5
|
-
import {
|
|
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
|
/**
|
|
@@ -33,6 +34,24 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
33
34
|
supportsTimestamptz: false,
|
|
34
35
|
defaultStringAsText: false,
|
|
35
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* `information_schema` keeps InnoDB's own row estimate, which is live enough to answer before
|
|
39
|
+
* anything has been analyzed. `DATABASE()` where the entity names no schema, so the estimate comes
|
|
40
|
+
* from the connection's own database rather than a same-named table in another one.
|
|
41
|
+
*/
|
|
42
|
+
estimatedCount(ctx, entity) {
|
|
43
|
+
const meta = getMeta(entity);
|
|
44
|
+
const schema = this.resolveSchema(meta);
|
|
45
|
+
ctx.append(`SELECT TABLE_ROWS ${this.escapeId(COUNT_ALIAS, true)} FROM information_schema.TABLES WHERE TABLE_SCHEMA = `);
|
|
46
|
+
if (schema) {
|
|
47
|
+
ctx.addValue(schema);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
ctx.append('DATABASE()');
|
|
51
|
+
}
|
|
52
|
+
ctx.append(' AND TABLE_NAME = ');
|
|
53
|
+
ctx.addValue(this.resolveTableAlias(meta));
|
|
54
|
+
}
|
|
36
55
|
/** `OFFSET` is only legal after a `LIMIT` here, so a bare `$skip` needs one. */
|
|
37
56
|
pager(ctx, opts) {
|
|
38
57
|
if (opts.$limit === undefined && opts.$skip !== undefined) {
|
|
@@ -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,
|
|
@@ -40,6 +40,12 @@ export type QuerySortOptions = {
|
|
|
40
40
|
* statement. `$sort` contributes to-one relations only; the rest is rejected where it is rendered.
|
|
41
41
|
*/
|
|
42
42
|
export declare function resolveQueryJoins<E>(meta: EntityMeta<E>, q: Query<E>): QueryJoins;
|
|
43
|
+
/**
|
|
44
|
+
* Whether a join drops parents that have no match, which is the one thing a join does to *how many*
|
|
45
|
+
* rows a read returns rather than how wide they are. A count that skips the joins has to be told, or
|
|
46
|
+
* it counts the parents the read will never hand back.
|
|
47
|
+
*/
|
|
48
|
+
export declare function hasRequiredJoin<E>(meta: EntityMeta<E>, q: Query<E>): boolean;
|
|
43
49
|
/**
|
|
44
50
|
* The join an ordering may address at `path`, with the relation's own sort map, or why it may not.
|
|
45
51
|
* Every backend answers this the same way - a to-many has no single value to order by, a relation
|
|
@@ -16,6 +16,19 @@ export function resolveQueryJoins(meta, q) {
|
|
|
16
16
|
addSortJoins(joins, meta, q.$sort);
|
|
17
17
|
return joins;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether a join drops parents that have no match, which is the one thing a join does to *how many*
|
|
21
|
+
* rows a read returns rather than how wide they are. A count that skips the joins has to be told, or
|
|
22
|
+
* it counts the parents the read will never hand back.
|
|
23
|
+
*/
|
|
24
|
+
export function hasRequiredJoin(meta, q) {
|
|
25
|
+
for (const join of resolveQueryJoins(meta, q).values()) {
|
|
26
|
+
if (join.required) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
19
32
|
function addJoin(joins, parent, key, relation, query, required, projected) {
|
|
20
33
|
const path = parent ? `${parent.path}.${key}` : key;
|
|
21
34
|
const existing = joins.get(path);
|
package/dist/http/contract.d.ts
CHANGED
|
@@ -78,13 +78,6 @@ export type RouteMatch = {
|
|
|
78
78
|
* Resolve a (method, sub-path) pair to a CRUD operation. Literal sub-paths win over `:id`.
|
|
79
79
|
*/
|
|
80
80
|
export declare function matchRoute(method: string, subPath: string | undefined): RouteMatch | undefined;
|
|
81
|
-
export type RequestSuccessResponse<E> = {
|
|
82
|
-
data: E;
|
|
83
|
-
count?: number;
|
|
84
|
-
};
|
|
85
|
-
export type RequestCountedSuccessResponse<E> = RequestSuccessResponse<E> & {
|
|
86
|
-
count: number;
|
|
87
|
-
};
|
|
88
81
|
export type RequestErrorResponse = {
|
|
89
82
|
readonly error: {
|
|
90
83
|
readonly message: string;
|
package/dist/http/handler.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { EntityMeta, QuerierPool, Query, Type, UqlContext } from '../type/index.js';
|
|
2
|
-
import { type CrudOperation, type HttpMethod
|
|
1
|
+
import type { EntityMeta, QuerierPool, Query, RequestSuccessResponse, Type, UqlContext } from '../type/index.js';
|
|
2
|
+
import { type CrudOperation, type HttpMethod } from './contract.js';
|
|
3
3
|
/**
|
|
4
4
|
* Framework-normalized request: adapters (express, fetch, ...) reduce their native
|
|
5
5
|
* request to this shape and get back a status + JSON body.
|
package/dist/http/handler.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { withContext } from '../context/context.js';
|
|
2
2
|
import { getEntities, getMeta } from '../entity/index.js';
|
|
3
|
-
import { entityPath, matchRoute
|
|
3
|
+
import { entityPath, matchRoute } from './contract.js';
|
|
4
4
|
import { parseQueryParams } from './query.js';
|
|
5
5
|
/** `Company (crm.Company)`: the class, and the table it maps, which is what tells two apart. */
|
|
6
6
|
function tableOf(entity) {
|
package/dist/http/query.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// the clause lists themselves, not the barrel: this module is in the browser bundle's graph
|
|
2
|
-
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES } from '../type/query.js';
|
|
2
|
+
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_ROOT_OBJECT_CLAUSES, } from '../type/query.js';
|
|
3
3
|
// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata
|
|
4
4
|
import { getKeys } from '../util/object.util.js';
|
|
5
5
|
/**
|
|
@@ -10,6 +10,7 @@ import { getKeys } from '../util/object.util.js';
|
|
|
10
10
|
*/
|
|
11
11
|
const ALLOWED_QUERY_KEYS = new Set([
|
|
12
12
|
...QUERY_OBJECT_CLAUSES,
|
|
13
|
+
...QUERY_ROOT_OBJECT_CLAUSES,
|
|
13
14
|
...QUERY_NUMBER_CLAUSES,
|
|
14
15
|
...QUERY_BOOLEAN_CLAUSES,
|
|
15
16
|
'hardDelete',
|
|
@@ -36,7 +37,7 @@ export function parseQueryParams(params = {}) {
|
|
|
36
37
|
query[key] = params[key];
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
for (const key of QUERY_OBJECT_CLAUSES) {
|
|
40
|
+
for (const key of [...QUERY_OBJECT_CLAUSES, ...QUERY_ROOT_OBJECT_CLAUSES]) {
|
|
40
41
|
const value = query[key];
|
|
41
42
|
if (typeof value === 'string') {
|
|
42
43
|
try {
|
|
@@ -18,10 +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
|
-
private static readonly REL_COUNT_KEY;
|
|
24
|
-
private static readonly REL_NESTED_KEY;
|
|
25
21
|
private static readonly VECTOR_INDEX_TYPES;
|
|
26
22
|
/** Atlas rejects a `$vectorSearch` asking for more candidates than this. */
|
|
27
23
|
private static readonly MAX_NUM_CANDIDATES;
|
|
@@ -60,6 +56,12 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
60
56
|
* relation subquery can no more read out-of-scope rows than a direct query on the target can.
|
|
61
57
|
*/
|
|
62
58
|
private appendRelationLookup;
|
|
59
|
+
/**
|
|
60
|
+
* The correlated `$lookup` for one relation, as `temp`: through its junction for a ManyToMany, or
|
|
61
|
+
* straight at the target otherwise. `tail` decides what the lookup leaves behind - a row to test
|
|
62
|
+
* for existence, or a `$count` - so a filter and an ordering build the same stage.
|
|
63
|
+
*/
|
|
64
|
+
private relationLookup;
|
|
63
65
|
/**
|
|
64
66
|
* ManyToMany counts/tests junction rows, so the target is reached from inside the junction's own
|
|
65
67
|
* lookup - the junction's filters apply too, since a soft-deleted link is not a link.
|
|
@@ -112,6 +114,15 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
112
114
|
sort<E extends Document>(entity: Type<E>, sort?: QuerySortMap<E>, populate?: QueryPopulate<E>): Sort;
|
|
113
115
|
/** Walks `$sort` against the metadata of the entity each level addresses, as the SQL dialects do. */
|
|
114
116
|
private collectSort;
|
|
117
|
+
/**
|
|
118
|
+
* The stages a `$sort` by a relation's size needs: one correlated `$lookup` tallying the relation
|
|
119
|
+
* per parent, and the `$set` that lifts the tally onto the document as the field the `$sort` then
|
|
120
|
+
* orders by. A parent with no related row gets no lookup result at all, which is a zero.
|
|
121
|
+
*/
|
|
122
|
+
sortCountStages<E extends Document>(entity: Type<E>, sort: QuerySortMap<E> | undefined, opts?: QueryOptions): {
|
|
123
|
+
readonly stages: MongoAggregationPipelineEntry<Document>[];
|
|
124
|
+
readonly fields: string[];
|
|
125
|
+
};
|
|
115
126
|
/** Whether a `$sort` reads a relation, which is what forces the lookups to run before it. */
|
|
116
127
|
sortsRelations<E extends Document>(entity: Type<E>, sort: QuerySortMap<E> | undefined): boolean;
|
|
117
128
|
/**
|