uql-orm 0.69.0 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/browser/querier/httpQuerier.d.ts +7 -7
- package/dist/browser/type/clientQuerier.d.ts +5 -5
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/cockroachdb/cockroachDialect.d.ts +6 -0
- package/dist/cockroachdb/cockroachDialect.js +10 -2
- package/dist/d1/d1SqliteDialect.d.ts +3 -0
- package/dist/d1/d1SqliteDialect.js +3 -1
- package/dist/dialect/abstractDialect.d.ts +8 -2
- package/dist/dialect/abstractDialect.js +17 -1
- package/dist/dialect/abstractSqlDialect.d.ts +21 -5
- package/dist/dialect/abstractSqlDialect.js +91 -46
- package/dist/dialect/aliases.d.ts +10 -7
- package/dist/dialect/aliases.js +10 -7
- package/dist/dialect/hydrateColumn.d.ts +3 -2
- package/dist/dialect/hydrateColumn.js +10 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +5 -3
- package/dist/dialect/pgLikeSqlDialect.d.ts +17 -5
- package/dist/dialect/pgLikeSqlDialect.js +34 -15
- package/dist/dialect/queryJoins.d.ts +19 -1
- package/dist/dialect/queryJoins.js +54 -11
- package/dist/dialect/vectorCast.d.ts +2 -0
- package/dist/dialect/vectorCast.js +7 -0
- package/dist/dialect/vectorSqlDialect.d.ts +7 -3
- package/dist/dialect/vectorSqlDialect.js +15 -10
- package/dist/entity/decorator/members.d.ts +25 -11
- package/dist/entity/metadata/definition.d.ts +8 -3
- package/dist/libsql/libsqlDialect.d.ts +1 -1
- package/dist/libsql/libsqlDialect.js +3 -3
- package/dist/maria/mariaDialect.d.ts +3 -9
- package/dist/maria/mariaDialect.js +4 -13
- package/dist/maria/mariadbQuerier.js +9 -3
- package/dist/migrate/codegen/entityCodeGenerator.js +4 -2
- package/dist/migrate/ddl/index.d.ts +1 -0
- package/dist/migrate/ddl/index.js +4 -2
- package/dist/migrate/ddl/indexDdl.d.ts +2 -0
- package/dist/migrate/ddl/indexDdl.js +10 -2
- package/dist/migrate/ddl/pgIndexDdl.d.ts +3 -4
- package/dist/migrate/ddl/pgIndexDdl.js +11 -11
- package/dist/migrate/ddl/sqliteIndexDdl.d.ts +11 -0
- package/dist/migrate/ddl/sqliteIndexDdl.js +38 -0
- package/dist/migrate/generator/mongoCommand.d.ts +28 -0
- package/dist/migrate/generator/mongoCommand.js +8 -0
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +2 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +50 -6
- package/dist/migrate/introspection/mongoIntrospector.js +33 -7
- package/dist/migrate/introspection/postgresIntrospector.js +19 -0
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +6 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +29 -3
- package/dist/migrate/migrator.d.ts +2 -1
- package/dist/migrate/migrator.js +5 -3
- package/dist/mongo/mongoDialect.d.ts +33 -27
- package/dist/mongo/mongoDialect.js +178 -114
- package/dist/mongo/mongodbQuerier.d.ts +0 -2
- package/dist/mongo/mongodbQuerier.js +14 -13
- package/dist/mssql/mssqlDialect.js +4 -2
- package/dist/postgres/postgresDialect.js +2 -2
- package/dist/querier/abstractQuerier.d.ts +16 -11
- package/dist/querier/abstractQuerier.js +28 -8
- package/dist/querier/abstractQuerierPool.d.ts +9 -9
- package/dist/querier/abstractSqlQuerier.d.ts +1 -1
- package/dist/querier/abstractSqlQuerier.js +3 -3
- package/dist/schema/canonicalType.js +10 -4
- package/dist/schema/indexDifferences.d.ts +5 -2
- package/dist/schema/indexDifferences.js +5 -0
- package/dist/schema/schemaASTBuilder.js +3 -2
- package/dist/sqlite/localSqliteQuerierPool.d.ts +10 -11
- package/dist/sqlite/localSqliteQuerierPool.js +8 -11
- package/dist/sqlite/nodeSqliteQuerierPool.d.ts +4 -2
- package/dist/sqlite/nodeSqliteQuerierPool.js +7 -4
- package/dist/sqlite/sqliteDialect.d.ts +9 -1
- package/dist/sqlite/sqliteDialect.js +43 -3
- package/dist/sqlite/sqliteQuerierPool.d.ts +6 -5
- package/dist/sqlite/sqliteQuerierPool.js +10 -7
- package/dist/turso/tursoDialect.d.ts +1 -1
- package/dist/turso/tursoDialect.js +6 -2
- package/dist/turso/tursoLocalDialect.d.ts +1 -1
- package/dist/turso/tursoLocalDialect.js +1 -1
- package/dist/turso/tursoLocalQuerierPool.d.ts +4 -5
- package/dist/turso/tursoLocalQuerierPool.js +3 -10
- package/dist/type/dialect.d.ts +11 -0
- package/dist/type/entity.d.ts +45 -5
- package/dist/type/migration.d.ts +14 -9
- package/dist/type/query.d.ts +6 -0
- package/dist/type/queryAggregate.d.ts +73 -42
- package/dist/type/queryAggregate.js +4 -21
- package/dist/type/universalQuerier.d.ts +9 -9
- package/dist/type/vector.d.ts +17 -0
- package/dist/type/vector.js +6 -0
- package/dist/util/ddlExpression.util.d.ts +2 -0
- package/dist/util/ddlExpression.util.js +5 -0
- package/dist/util/dialect.util.d.ts +17 -3
- package/dist/util/dialect.util.js +40 -6
- package/package.json +5 -3
- package/skills/uql-orm/SKILL.md +142 -0
|
@@ -3,10 +3,10 @@ import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, VECTOR_QUERY_KEY
|
|
|
3
3
|
import { isInlinedExpression } from '../util/field.util.js';
|
|
4
4
|
import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
5
|
import { escapeAnsiSqlLiteral } from '../util/sqlLiteral.js';
|
|
6
|
-
import { AGGREGATE_PAGE_ALIAS, AGGREGATE_VALUE_ALIAS,
|
|
6
|
+
import { AGGREGATE_PAGE_ALIAS, AGGREGATE_VALUE_ALIAS, ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn, } from './aliases.js';
|
|
7
7
|
import { holdsOperator, isJsonScalar, jsonCompareMode, jsonElemExists, jsonPath, } from './jsonSql.js';
|
|
8
8
|
import { SqlQueryContext } from './queryContext.js';
|
|
9
|
-
import { NO_JOINS, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
|
|
9
|
+
import { groupPathField, NO_JOINS, resolveGroupJoins, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
|
|
10
10
|
import { resolveVectorCast } from './vectorCast.js';
|
|
11
11
|
import { VectorSqlDialect } from './vectorSqlDialect.js';
|
|
12
12
|
/** The key a term answers under in a populated relation's row, which a raw expression has only once aliased. */
|
|
@@ -150,7 +150,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
150
150
|
if (opts.prefix !== prefix) {
|
|
151
151
|
opts = { ...opts, prefix };
|
|
152
152
|
}
|
|
153
|
-
this.where(ctx, entity, q
|
|
153
|
+
this.where(ctx, entity, this.rankedWhere(meta, q, prefix), opts);
|
|
154
154
|
const sorted = order
|
|
155
155
|
? this.orderCarried(ctx, q, order)
|
|
156
156
|
: this.sort(ctx, entity, q.$sort, { prefix, joins, distinct: q.$distinct });
|
|
@@ -916,7 +916,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
916
916
|
count(ctx, entity, q, opts) {
|
|
917
917
|
const { $where, $skip, $limit } = q;
|
|
918
918
|
if ($skip === undefined && $limit === undefined) {
|
|
919
|
-
this.select(ctx, entity, { $select: [raw `COUNT(*)`.as(
|
|
919
|
+
this.select(ctx, entity, { $select: [raw `COUNT(*)`.as(AGGREGATE_VALUE_ALIAS)] });
|
|
920
920
|
this.search(ctx, entity, { $where }, opts);
|
|
921
921
|
return;
|
|
922
922
|
}
|
|
@@ -936,9 +936,9 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
936
936
|
}
|
|
937
937
|
/** `SELECT COUNT(*)` over the rows `rows` appends, as a derived table. */
|
|
938
938
|
countRows(ctx, rows) {
|
|
939
|
-
ctx.append(`SELECT COUNT(*) ${this.escapeId(
|
|
939
|
+
ctx.append(`SELECT COUNT(*) ${this.escapeId(AGGREGATE_VALUE_ALIAS, true)} FROM (`);
|
|
940
940
|
rows();
|
|
941
|
-
ctx.append(`) ${this.escapeId(
|
|
941
|
+
ctx.append(`) ${this.escapeId(ROWS_ALIAS, true)}`);
|
|
942
942
|
}
|
|
943
943
|
/**
|
|
944
944
|
* The statistic the engine already keeps, as a `count` column. Overridden by the dialects that
|
|
@@ -958,35 +958,40 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
958
958
|
};
|
|
959
959
|
aggregate(ctx, entity, q, opts = {}) {
|
|
960
960
|
const meta = getMeta(entity);
|
|
961
|
-
const
|
|
961
|
+
const entries = parseGroupMap(q.$group, q.$select);
|
|
962
|
+
if (!entries.length) {
|
|
963
|
+
throw new TypeError('aggregate requires at least one $group column or $select function');
|
|
964
|
+
}
|
|
965
|
+
const table = this.tableRef(meta, this.readOptions(ctx, meta).alias);
|
|
966
|
+
const { joins, where } = resolveGroupJoins(meta, q, (path) => ctx.claimAlias(path));
|
|
967
|
+
const prefix = joins.size ? table.alias : undefined;
|
|
968
|
+
const reads = entries.map((entry) => ({ entry, value: this.aggregateValue(ctx, entity, entry, joins, prefix) }));
|
|
969
|
+
// Only bare columns are read inline: SQL Server refuses a subquery inside an aggregate or a
|
|
970
|
+
// `GROUP BY`, and a filter's bound values would repeat in `HAVING`. A derived table answers by alias.
|
|
971
|
+
const derived = reads.some(({ value }) => !value.bare);
|
|
972
|
+
const named = (sql, alias) => sql === this.escapeId(alias) ? sql : `${sql} ${this.escapeId(alias)}`;
|
|
962
973
|
const groupKeys = [];
|
|
963
974
|
const selectParts = [];
|
|
964
975
|
// Every column the statement emits, mapped to the SQL that references it. `$having` and `$sort`
|
|
965
976
|
// may name these and nothing else, so one map answers both "is this legal?" and "what do I
|
|
966
977
|
// emit for it?".
|
|
967
978
|
const emittedColumns = {};
|
|
968
|
-
for (const entry of
|
|
979
|
+
for (const { entry, value } of reads) {
|
|
980
|
+
const column = derived ? this.escapeId(entry.alias) : value.sql;
|
|
981
|
+
const expr = entry.kind === 'key' ? column : this.aggregateFn(entry.op, value.sql === '*' ? '*' : column, entry.distinct);
|
|
969
982
|
if (entry.kind === 'key') {
|
|
970
|
-
|
|
971
|
-
const columnName = this.resolveColumnName(entry.alias, field);
|
|
972
|
-
const escaped = this.escapeId(columnName);
|
|
973
|
-
groupKeys.push(escaped);
|
|
974
|
-
emittedColumns[entry.alias] = escaped;
|
|
975
|
-
selectParts.push(columnName !== entry.alias ? `${escaped} ${this.escapeId(entry.alias)}` : escaped);
|
|
976
|
-
}
|
|
977
|
-
else {
|
|
978
|
-
const sqlFn = AbstractSqlDialect.AGGREGATE_FN[entry.op];
|
|
979
|
-
const sqlArg = entry.fieldRef === '*' ? '*' : this.escapeId(this.columnOf(meta, entry.fieldRef));
|
|
980
|
-
const expr = `${sqlFn}(${entry.distinct ? 'DISTINCT ' : ''}${sqlArg})`;
|
|
981
|
-
emittedColumns[entry.alias] = expr;
|
|
982
|
-
selectParts.push(`${expr} ${this.escapeId(entry.alias)}`);
|
|
983
|
+
groupKeys.push(expr);
|
|
983
984
|
}
|
|
985
|
+
emittedColumns[entry.alias] = expr;
|
|
986
|
+
selectParts.push(named(expr, entry.alias));
|
|
984
987
|
}
|
|
985
|
-
|
|
986
|
-
|
|
988
|
+
const columns = reads.flatMap(({ entry, value }) => (value.sql === '*' ? [] : [named(value.sql, entry.alias)]));
|
|
989
|
+
ctx.append(`SELECT ${selectParts.join(', ')} FROM ${derived ? `(SELECT ${columns.join(', ')} FROM ` : ''}${table.ref}`);
|
|
990
|
+
this.selectRelationJoins(ctx, meta, table.alias, joins);
|
|
991
|
+
this.where(ctx, entity, where, { ...opts, prefix });
|
|
992
|
+
if (derived) {
|
|
993
|
+
ctx.append(`) ${this.escapeId(ROWS_ALIAS, true)}`);
|
|
987
994
|
}
|
|
988
|
-
ctx.append(`SELECT ${selectParts.join(', ')} FROM ${tableName}`);
|
|
989
|
-
this.where(ctx, entity, q.$where, opts);
|
|
990
995
|
if (groupKeys.length) {
|
|
991
996
|
ctx.append(` GROUP BY ${groupKeys.join(', ')}`);
|
|
992
997
|
}
|
|
@@ -996,6 +1001,34 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
996
1001
|
const sorted = this.aggregateSort(ctx, q.$sort, emittedColumns);
|
|
997
1002
|
this.pager(ctx, q, sorted);
|
|
998
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* What one entry reads: a grouped field, through the join its path passes, or an aggregate's argument,
|
|
1006
|
+
* narrowed by its own `$where` to `CASE WHEN … THEN … END`. Bare where it is a column or `'*'`.
|
|
1007
|
+
*/
|
|
1008
|
+
aggregateValue(ctx, entity, entry, joins, prefix) {
|
|
1009
|
+
if (entry.kind === 'key') {
|
|
1010
|
+
const { key, join } = groupPathField(joins, entry.path);
|
|
1011
|
+
return join
|
|
1012
|
+
? this.aggregateOperand(ctx, join.entity, key, join.alias)
|
|
1013
|
+
: this.aggregateOperand(ctx, entity, key, prefix);
|
|
1014
|
+
}
|
|
1015
|
+
const arg = entry.fieldRef === '*' ? undefined : this.aggregateOperand(ctx, entity, entry.fieldRef, prefix);
|
|
1016
|
+
const { where } = entry;
|
|
1017
|
+
const condition = where &&
|
|
1018
|
+
this.buildFragment(ctx, (fragment) => this.renderWhere(fragment, entity, where, { clause: false, prefix }));
|
|
1019
|
+
if (!condition) {
|
|
1020
|
+
return arg ?? { sql: '*', bare: true };
|
|
1021
|
+
}
|
|
1022
|
+
return { sql: `CASE WHEN ${condition} THEN ${arg?.sql ?? '1'} END`, bare: false };
|
|
1023
|
+
}
|
|
1024
|
+
/** A field as an aggregate reads it: its column, or the expression an inlined one stands for. */
|
|
1025
|
+
aggregateOperand(ctx, entity, key, prefix) {
|
|
1026
|
+
const field = getMeta(entity).fields[key];
|
|
1027
|
+
return {
|
|
1028
|
+
sql: this.resolveOperandField(ctx, entity, key, { prefix }),
|
|
1029
|
+
bare: field === undefined || !isInlinedExpression(field),
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
999
1032
|
/**
|
|
1000
1033
|
* ORDER BY for aggregate queries - handles both entity-field and alias references. A grouped
|
|
1001
1034
|
* statement has no joins to address, so a relation key is rejected rather than emitted as an alias
|
|
@@ -1094,6 +1127,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1094
1127
|
ctx.append(` ${returning}`);
|
|
1095
1128
|
}
|
|
1096
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* What a text search matches and a fulltext index covers, which have to agree for the index to serve the
|
|
1132
|
+
* search: the columns themselves, where the engine indexes them as they are (MySQL's `MATCH (a, b)`).
|
|
1133
|
+
*/
|
|
1134
|
+
textSearchTarget(columns, _config) {
|
|
1135
|
+
return columns.join(', ');
|
|
1136
|
+
}
|
|
1097
1137
|
/** Where an insert's id clause goes: `RETURNING` at the end, or SQL Server's `OUTPUT` before `VALUES`. */
|
|
1098
1138
|
returningPosition = 'suffix';
|
|
1099
1139
|
/**
|
|
@@ -1296,7 +1336,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1296
1336
|
}
|
|
1297
1337
|
const decoded = [];
|
|
1298
1338
|
for (const [key, field] of Object.entries(meta.fields)) {
|
|
1299
|
-
const kind = this.
|
|
1339
|
+
const kind = this.fieldKind(meta, field);
|
|
1300
1340
|
if (kind) {
|
|
1301
1341
|
decoded.push([key, kind]);
|
|
1302
1342
|
}
|
|
@@ -1306,13 +1346,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1306
1346
|
}
|
|
1307
1347
|
/** The same for an aggregate's row, per query: each alias decodes by {@link aggregateKind}. */
|
|
1308
1348
|
hydratableAggregates(entity, q) {
|
|
1309
|
-
const
|
|
1349
|
+
const meta = getMeta(entity);
|
|
1350
|
+
const { joins } = resolveGroupJoins(meta, q);
|
|
1310
1351
|
const decoded = [];
|
|
1311
1352
|
for (const entry of parseGroupMap(q.$group, q.$select)) {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
const kind = entry.kind === 'fn' ? this.aggregateKind(entry.op, field) : this.hydrateKind(field);
|
|
1353
|
+
const kind = entry.kind === 'fn'
|
|
1354
|
+
? this.aggregateKind(entry.op, this.fieldKind(meta, meta.fields[entry.fieldRef]))
|
|
1355
|
+
: this.groupedKind(meta, joins, entry.path);
|
|
1316
1356
|
if (kind) {
|
|
1317
1357
|
decoded.push([entry.alias, kind]);
|
|
1318
1358
|
}
|
|
@@ -1327,17 +1367,22 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1327
1367
|
*
|
|
1328
1368
|
* One rule for both readers: a relation aggregate a field declares, and an aggregate a query names.
|
|
1329
1369
|
*/
|
|
1330
|
-
aggregateKind(op,
|
|
1331
|
-
return op === '$count' || op === '$avg' ? 'number' :
|
|
1370
|
+
aggregateKind(op, fieldKind) {
|
|
1371
|
+
return op === '$count' || op === '$avg' ? 'number' : fieldKind;
|
|
1372
|
+
}
|
|
1373
|
+
/** What a grouped path decodes as: the kind of the field it reaches, through its join or on the entity. */
|
|
1374
|
+
groupedKind(meta, joins, path) {
|
|
1375
|
+
const { key, join } = groupPathField(joins, path);
|
|
1376
|
+
return join ? this.fieldKind(join.meta, join.meta.fields[key]) : this.fieldKind(meta, meta.fields[key]);
|
|
1332
1377
|
}
|
|
1333
|
-
/** What a
|
|
1334
|
-
|
|
1378
|
+
/** What a field decodes as: its column's kind, or a relation aggregate's {@link aggregateKind} over the target's column. */
|
|
1379
|
+
fieldKind(meta, field) {
|
|
1335
1380
|
const spec = aggregateOf(field);
|
|
1336
1381
|
if (!spec) {
|
|
1337
|
-
return
|
|
1382
|
+
return this.hydrateKind(field);
|
|
1338
1383
|
}
|
|
1339
1384
|
const target = getMeta(relationOf(meta, spec.relation).entity());
|
|
1340
|
-
return this.aggregateKind(spec.op, spec.field ? target.fields[spec.field] : undefined);
|
|
1385
|
+
return this.aggregateKind(spec.op, spec.field ? this.fieldKind(target, target.fields[spec.field]) : undefined);
|
|
1341
1386
|
}
|
|
1342
1387
|
/** What one column decodes as, the inverse of {@link persistKind}. `BigInt` first, since it shares the numeric family. */
|
|
1343
1388
|
hydrateKind(field) {
|
|
@@ -1349,7 +1394,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1349
1394
|
case 'json':
|
|
1350
1395
|
return 'json';
|
|
1351
1396
|
case 'vector':
|
|
1352
|
-
return this.supportedVectorType(resolveVectorCast(field));
|
|
1397
|
+
return this.features.vectorBytes ? 'float32' : this.supportedVectorType(resolveVectorCast(field));
|
|
1353
1398
|
case 'boolean':
|
|
1354
1399
|
return 'boolean';
|
|
1355
1400
|
case 'numeric':
|
|
@@ -1395,8 +1440,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1395
1440
|
* `$pull` reads the column and every later one its expression once, so no value binds twice.
|
|
1396
1441
|
*/
|
|
1397
1442
|
formatJsonUpdate(ctx, escapedCol, value, field) {
|
|
1398
|
-
// Centralizes the one narrowing cast: the payload's keys are typed against the entity's JSON
|
|
1399
|
-
// payload, which the dialects do not need - they only build SQL from keys and values.
|
|
1400
1443
|
const { $pull, $set, $push, $unset } = value;
|
|
1401
1444
|
let expr = escapedCol;
|
|
1402
1445
|
if (hasKeys($pull)) {
|
|
@@ -1584,7 +1627,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1584
1627
|
const meta = getMeta(entity);
|
|
1585
1628
|
const rel = relationOf(meta, aggregate.relation);
|
|
1586
1629
|
const parent = prefix || this.resolveTableAlias(meta);
|
|
1587
|
-
|
|
1630
|
+
const { $limit, $skip } = aggregate.query ?? {};
|
|
1631
|
+
if ($limit === undefined && $skip === undefined) {
|
|
1588
1632
|
this.appendRelationSubquery(ctx, meta, aggregate.relation, rel, { prefix: parent }, aggregate);
|
|
1589
1633
|
return;
|
|
1590
1634
|
}
|
|
@@ -1616,14 +1660,15 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1616
1660
|
const joins = resolveQueryJoins(getMeta(entity), query, (path) => ctx.claimAlias(path));
|
|
1617
1661
|
this.read(ctx, entity, query, { alias }, joins);
|
|
1618
1662
|
}
|
|
1619
|
-
/**
|
|
1663
|
+
/** A relation aggregate over an operand, reading `0` on a parent with no rows where its type says so. */
|
|
1620
1664
|
aggregateCall(op, operand) {
|
|
1621
|
-
|
|
1622
|
-
return 'COUNT(*)';
|
|
1623
|
-
}
|
|
1624
|
-
const call = `${AbstractSqlDialect.AGGREGATE_FN[op]}(${operand})`;
|
|
1665
|
+
const call = this.aggregateFn(op, op === '$count' ? '*' : operand);
|
|
1625
1666
|
return op === '$sum' ? `COALESCE(${call}, 0)` : call;
|
|
1626
1667
|
}
|
|
1668
|
+
/** One aggregate function call, the one spelling every statement that aggregates writes. */
|
|
1669
|
+
aggregateFn(op, operand, distinct) {
|
|
1670
|
+
return `${AbstractSqlDialect.AGGREGATE_FN[op]}(${distinct ? 'DISTINCT ' : ''}${operand})`;
|
|
1671
|
+
}
|
|
1627
1672
|
/**
|
|
1628
1673
|
* One equality per key of the parent, anded: a composite correlates on every column, and matching on
|
|
1629
1674
|
* part of one would find the rows of a different parent. `parentJoins` keeps the two ends the right
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
/** The column every internally-built count answers in: `COUNT(*)`, a grouped tally, a `$count` stage. */
|
|
2
|
-
export declare const COUNT_ALIAS = "_uql_count";
|
|
3
1
|
/** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
|
|
4
2
|
export declare const TOTAL_ALIAS = "_uql_total";
|
|
5
|
-
/**
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The derived table a statement wraps the rows it counts or aggregates in: a page, a `$distinct` set, or
|
|
5
|
+
* rows computing an inlined field an aggregate names. MySQL requires the alias.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ROWS_ALIAS = "_uql_rows";
|
|
7
8
|
/** The derived table a capped relation aggregate reads: the page is taken first, then aggregated over. */
|
|
8
9
|
export declare const AGGREGATE_PAGE_ALIAS = "_uql_page";
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
* and the field a MongoDB `$group` or `$count`
|
|
12
|
-
* are written and read here.
|
|
11
|
+
* The one value a statement that aggregates answers under: a `COUNT(*)` or an estimate, the column a
|
|
12
|
+
* capped page carries out for the aggregate wrapping it, and the field a MongoDB `$group` or `$count`
|
|
13
|
+
* leaves its result in. One name, since both ends of each are written and read here.
|
|
13
14
|
*/
|
|
14
15
|
export declare const AGGREGATE_VALUE_ALIAS = "_uql_value";
|
|
16
|
+
/** Beside each MongoDB `$sum`, how many values it read: `$sum` answers 0 over none, where SQL answers null. */
|
|
17
|
+
export declare const SUM_COUNT_ALIAS = "_uql_count";
|
|
15
18
|
/** The row a Postgres relation aggregates whole: a LATERAL projection of the columns it answers under. */
|
|
16
19
|
export declare const RELATION_ROW_ALIAS = "_uql_row";
|
|
17
20
|
/** The alias an exploded JSON array element is read through, `_uql_elem_2` and on where one nests in another. */
|
package/dist/dialect/aliases.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
// Every identifier UQL invents, `_uql`-prefixed to stay off a user's own, collected in one place:
|
|
2
2
|
// the ends writing and reading one sit in different modules, and a drift between them fails silently.
|
|
3
|
-
/** The column every internally-built count answers in: `COUNT(*)`, a grouped tally, a `$count` stage. */
|
|
4
|
-
export const COUNT_ALIAS = '_uql_count';
|
|
5
3
|
/** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
|
|
6
4
|
export const TOTAL_ALIAS = '_uql_total';
|
|
7
|
-
/**
|
|
8
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The derived table a statement wraps the rows it counts or aggregates in: a page, a `$distinct` set, or
|
|
7
|
+
* rows computing an inlined field an aggregate names. MySQL requires the alias.
|
|
8
|
+
*/
|
|
9
|
+
export const ROWS_ALIAS = '_uql_rows';
|
|
9
10
|
/** The derived table a capped relation aggregate reads: the page is taken first, then aggregated over. */
|
|
10
11
|
export const AGGREGATE_PAGE_ALIAS = '_uql_page';
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
* and the field a MongoDB `$group` or `$count`
|
|
14
|
-
* are written and read here.
|
|
13
|
+
* The one value a statement that aggregates answers under: a `COUNT(*)` or an estimate, the column a
|
|
14
|
+
* capped page carries out for the aggregate wrapping it, and the field a MongoDB `$group` or `$count`
|
|
15
|
+
* leaves its result in. One name, since both ends of each are written and read here.
|
|
15
16
|
*/
|
|
16
17
|
export const AGGREGATE_VALUE_ALIAS = '_uql_value';
|
|
18
|
+
/** Beside each MongoDB `$sum`, how many values it read: `$sum` answers 0 over none, where SQL answers null. */
|
|
19
|
+
export const SUM_COUNT_ALIAS = '_uql_count';
|
|
17
20
|
/** The row a Postgres relation aggregates whole: a LATERAL projection of the columns it answers under. */
|
|
18
21
|
export const RELATION_ROW_ALIAS = '_uql_row';
|
|
19
22
|
/** The alias an exploded JSON array element is read through, `_uql_elem_2` and on where one nests in another. */
|
|
@@ -3,9 +3,10 @@ import { type VectorCast } from './vectorCast.js';
|
|
|
3
3
|
* How a stored column is decoded on read: the inverse of `AbstractSqlDialect.persistKind`. `json`
|
|
4
4
|
* parses; a {@link VectorCast} says which literal; `boolean` undoes an engine with no boolean type,
|
|
5
5
|
* `number` and `bigint` a driver that hands a wide integer or a decimal back as text, and `date` and
|
|
6
|
-
* `bytes` a row that crossed JSON inside its parent's statement, which spells both as text.
|
|
6
|
+
* `bytes` a row that crossed JSON inside its parent's statement, which spells both as text. `float32` is
|
|
7
|
+
* a vector bound as bytes (`DialectFeatures.vectorBytes`).
|
|
7
8
|
*/
|
|
8
|
-
export type HydrateKind = 'json' | 'boolean' | 'number' | 'bigint' | 'date' | 'bytes' | VectorCast;
|
|
9
|
+
export type HydrateKind = 'json' | 'boolean' | 'number' | 'bigint' | 'date' | 'bytes' | 'float32' | VectorCast;
|
|
9
10
|
/**
|
|
10
11
|
* Decodes one non-null cell. A no-op where the driver already decoded it, since that varies per driver,
|
|
11
12
|
* and untouched where it does not match its column's format.
|
|
@@ -20,6 +20,14 @@ function vectorDecoder(cast) {
|
|
|
20
20
|
? decodeFloat32s(hexBytes(text.slice(BYTES_PREFIX.length)))
|
|
21
21
|
: (parseVectorLiteral(text, cast) ?? value));
|
|
22
22
|
}
|
|
23
|
+
const denseVector = vectorDecoder('vector');
|
|
24
|
+
/** A vector bound as bytes: packed float32s as a driver returns them, else hex or text, as JSON or an older row carries it. */
|
|
25
|
+
const float32Decoder = (value) => {
|
|
26
|
+
if (value instanceof ArrayBuffer) {
|
|
27
|
+
return decodeFloat32s(new Uint8Array(value));
|
|
28
|
+
}
|
|
29
|
+
return value instanceof Uint8Array ? decodeFloat32s(value) : denseVector(value);
|
|
30
|
+
};
|
|
23
31
|
const DECODERS = {
|
|
24
32
|
// 0/1 from SQLite's INTEGER or MySQL's TINYINT(1). Already a boolean on Postgres.
|
|
25
33
|
boolean: (value) => (typeof value === 'boolean' ? value : Boolean(value)),
|
|
@@ -48,7 +56,8 @@ const DECODERS = {
|
|
|
48
56
|
return value;
|
|
49
57
|
}
|
|
50
58
|
}),
|
|
51
|
-
|
|
59
|
+
float32: float32Decoder,
|
|
60
|
+
vector: denseVector,
|
|
52
61
|
halfvec: vectorDecoder('halfvec'),
|
|
53
62
|
sparsevec: vectorDecoder('sparsevec'),
|
|
54
63
|
};
|
|
@@ -2,7 +2,7 @@ import { getMeta } from '../entity/index.js';
|
|
|
2
2
|
import { textSearchFields } from '../util/index.js';
|
|
3
3
|
import { escapeMysqlSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
4
4
|
import { AbstractSqlDialect, } from './abstractSqlDialect.js';
|
|
5
|
-
import {
|
|
5
|
+
import { AGGREGATE_VALUE_ALIAS } from './aliases.js';
|
|
6
6
|
import { BYTES_PREFIX } from './hydrateColumn.js';
|
|
7
7
|
import { jsonSetCall, jsonPath, jsonRemoveCall, jsonSetTarget } from './jsonSql.js';
|
|
8
8
|
import { aggregatesRelations } from './queryJoins.js';
|
|
@@ -23,10 +23,12 @@ export const MYSQL_FEATURES = {
|
|
|
23
23
|
commentSyntax: 'inline',
|
|
24
24
|
vectorIndexRequiresNotNull: false,
|
|
25
25
|
vectorSupportsLength: false,
|
|
26
|
+
vectorBytes: false,
|
|
26
27
|
supportsTimestamptz: false,
|
|
27
28
|
stringSizing: 'varchar',
|
|
28
29
|
supportsUnsigned: true,
|
|
29
30
|
serverSideCursors: false,
|
|
31
|
+
correlatedWrites: true,
|
|
30
32
|
rowLocks: true,
|
|
31
33
|
rowLockWithWindow: true,
|
|
32
34
|
rowLockOf: true,
|
|
@@ -49,7 +51,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
49
51
|
estimatedCount(ctx, entity) {
|
|
50
52
|
const meta = getMeta(entity);
|
|
51
53
|
const schema = this.resolveSchema(meta);
|
|
52
|
-
ctx.append(`SELECT TABLE_ROWS ${this.escapeId(
|
|
54
|
+
ctx.append(`SELECT TABLE_ROWS ${this.escapeId(AGGREGATE_VALUE_ALIAS, true)} FROM information_schema.TABLES WHERE TABLE_SCHEMA = `);
|
|
53
55
|
if (schema) {
|
|
54
56
|
ctx.addValue(schema);
|
|
55
57
|
}
|
|
@@ -177,7 +179,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
177
179
|
*/
|
|
178
180
|
appendTextSearch(ctx, _entity, meta, search) {
|
|
179
181
|
const columns = textSearchFields(meta, search).map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
|
|
180
|
-
ctx.append(`MATCH(${
|
|
182
|
+
ctx.append(`MATCH(${this.textSearchTarget(columns)}) AGAINST(`);
|
|
181
183
|
ctx.addValue(search.$value);
|
|
182
184
|
ctx.append(')');
|
|
183
185
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IndexType } from '../schema/types.js';
|
|
1
2
|
import { type DriverCapabilities, type EntityMeta, type FieldOptions, type JsonColumnType, type Query, type QueryContext, type QueryTextSearchOptions, type SqlDialectFeatures, type Type, type VectorDistance, type VectorMetric } from '../type/index.js';
|
|
2
3
|
import type { DialectOptions } from './abstractDialect.js';
|
|
3
4
|
import { AbstractSqlDialect, type RelationRows } from './abstractSqlDialect.js';
|
|
@@ -45,11 +46,11 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
45
46
|
readonly maxBindValues: number;
|
|
46
47
|
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
47
48
|
/**
|
|
48
|
-
* The
|
|
49
|
+
* The setting each vector index type reads for "how much of the index to explore". They are not the
|
|
49
50
|
* same quantity - `ef_search` is a candidate-list size, `probes` a count of lists - which is why
|
|
50
51
|
* `$candidates` is documented in the index's own units rather than as a portable number.
|
|
51
52
|
*/
|
|
52
|
-
|
|
53
|
+
protected readonly annSettings: ReadonlyMap<IndexType, string>;
|
|
53
54
|
/**
|
|
54
55
|
* `SET LOCAL hnsw.ef_search = N`, plus `hnsw.iterative_scan = strict_order` where the query also filters
|
|
55
56
|
* by distance, which would otherwise drop rows the candidate list missed.
|
|
@@ -58,11 +59,22 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
58
59
|
normalizeValue(value: unknown): unknown;
|
|
59
60
|
placeholder(index: number): string;
|
|
60
61
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
62
|
+
* `<document> @@ WEBSEARCH_TO_TSQUERY(...)`, under the `$config` asked for, else that of the fulltext
|
|
63
|
+
* index over these fields, which the planner serves it from. `WEBSEARCH_TO_TSQUERY` takes free-form
|
|
64
|
+
* input (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `TO_TSQUERY`.
|
|
64
65
|
*/
|
|
65
66
|
protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
|
|
67
|
+
/**
|
|
68
|
+
* The document, `TO_TSVECTOR('english'::regconfig, COALESCE("a", '') || ' ' || COALESCE("b", ''))`, a
|
|
69
|
+
* `NULL` column read as empty rather than emptying it all. The config is a literal: an index is built
|
|
70
|
+
* over one, and a bound one reaches it only where the driver leaves the parameter untyped.
|
|
71
|
+
*/
|
|
72
|
+
textSearchTarget(columns: readonly string[], config?: string): string;
|
|
73
|
+
/** A config as the text-search functions' first argument, or nothing, for the server's default. */
|
|
74
|
+
private textConfigArg;
|
|
75
|
+
/** How a config literal is typed, and the function reading a search's text: CockroachDB lacks both of these. */
|
|
76
|
+
protected readonly textConfigCast: string;
|
|
77
|
+
protected readonly textQueryFn: string;
|
|
66
78
|
protected jsonContains(ctx: QueryContext, slot: JsonSlot, values: readonly unknown[]): string;
|
|
67
79
|
protected jsonLength(slot: JsonSlot): string;
|
|
68
80
|
/** Each element stays `jsonb`, so it is read as any path is: `->` for the value, `->>` for its text. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QueryRaw, } from '../type/index.js';
|
|
2
|
-
import { hasVectorNear, textSearchFields } from '../util/dialect.util.js';
|
|
2
|
+
import { fulltextConfig, fulltextIndexOver, hasVectorNear, textSearchFields } from '../util/dialect.util.js';
|
|
3
3
|
import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
4
4
|
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
5
5
|
import { JSON_PULL_ALIAS, RELATION_ROW_ALIAS } from './aliases.js';
|
|
@@ -13,6 +13,8 @@ export const PG_VECTOR_METRICS = new Map([
|
|
|
13
13
|
['inner', { op: '<#>', index: 'ip' }],
|
|
14
14
|
['l1', { op: '<+>', index: 'l1' }],
|
|
15
15
|
]);
|
|
16
|
+
/** pgvector's HNSW candidate list, the one setting an iterative scan goes with. */
|
|
17
|
+
const HNSW_EF_SEARCH = 'hnsw.ef_search';
|
|
16
18
|
/** What the Postgres-wire engines have. */
|
|
17
19
|
export const PG_FEATURES = {
|
|
18
20
|
ifNotExists: true,
|
|
@@ -25,10 +27,12 @@ export const PG_FEATURES = {
|
|
|
25
27
|
commentSyntax: 'statement',
|
|
26
28
|
vectorIndexRequiresNotNull: false,
|
|
27
29
|
vectorSupportsLength: true,
|
|
30
|
+
vectorBytes: false,
|
|
28
31
|
supportsTimestamptz: true,
|
|
29
32
|
stringSizing: 'bounded-text',
|
|
30
33
|
supportsUnsigned: false,
|
|
31
34
|
serverSideCursors: true,
|
|
35
|
+
correlatedWrites: true,
|
|
32
36
|
rowLocks: true,
|
|
33
37
|
rowLockWithWindow: false,
|
|
34
38
|
rowLockOf: true,
|
|
@@ -86,12 +90,12 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
86
90
|
maxBindValues = 65535;
|
|
87
91
|
vectorMetrics = PG_VECTOR_METRICS;
|
|
88
92
|
/**
|
|
89
|
-
* The
|
|
93
|
+
* The setting each vector index type reads for "how much of the index to explore". They are not the
|
|
90
94
|
* same quantity - `ef_search` is a candidate-list size, `probes` a count of lists - which is why
|
|
91
95
|
* `$candidates` is documented in the index's own units rather than as a portable number.
|
|
92
96
|
*/
|
|
93
|
-
|
|
94
|
-
['hnsw',
|
|
97
|
+
annSettings = new Map([
|
|
98
|
+
['hnsw', HNSW_EF_SEARCH],
|
|
95
99
|
['ivfflat', 'ivfflat.probes'],
|
|
96
100
|
]);
|
|
97
101
|
/**
|
|
@@ -100,12 +104,12 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
100
104
|
*/
|
|
101
105
|
vectorTuningStatements(meta, q) {
|
|
102
106
|
const indexType = this.tunedVectorIndex(meta, q)?.type;
|
|
103
|
-
const setting = indexType ?
|
|
107
|
+
const setting = indexType ? this.annSettings.get(indexType) : undefined;
|
|
104
108
|
if (!setting) {
|
|
105
109
|
return [];
|
|
106
110
|
}
|
|
107
111
|
const statements = [`SET LOCAL ${setting} = ${q.$candidates}`];
|
|
108
|
-
if (
|
|
112
|
+
if (setting === HNSW_EF_SEARCH && hasVectorNear(q.$where)) {
|
|
109
113
|
statements.push('SET LOCAL hnsw.iterative_scan = strict_order');
|
|
110
114
|
}
|
|
111
115
|
return statements;
|
|
@@ -120,20 +124,35 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
120
124
|
return `$${index}`;
|
|
121
125
|
}
|
|
122
126
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
127
|
+
* `<document> @@ WEBSEARCH_TO_TSQUERY(...)`, under the `$config` asked for, else that of the fulltext
|
|
128
|
+
* index over these fields, which the planner serves it from. `WEBSEARCH_TO_TSQUERY` takes free-form
|
|
129
|
+
* input (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `TO_TSQUERY`.
|
|
126
130
|
*/
|
|
127
131
|
appendTextSearch(ctx, _entity, meta, search) {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
ctx.append(`TO_TSVECTOR(${config}${fields}) @@ WEBSEARCH_TO_TSQUERY(${config}`);
|
|
132
|
+
const keys = textSearchFields(meta, search);
|
|
133
|
+
const index = fulltextIndexOver(meta, keys);
|
|
134
|
+
const config = search.$config ?? (index && fulltextConfig(index));
|
|
135
|
+
const columns = keys.map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
|
|
136
|
+
ctx.append(`${this.textSearchTarget(columns, config)} @@ ${this.textQueryFn}(${this.textConfigArg(config)}`);
|
|
134
137
|
ctx.addValue(search.$value);
|
|
135
138
|
ctx.append(')');
|
|
136
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* The document, `TO_TSVECTOR('english'::regconfig, COALESCE("a", '') || ' ' || COALESCE("b", ''))`, a
|
|
142
|
+
* `NULL` column read as empty rather than emptying it all. The config is a literal: an index is built
|
|
143
|
+
* over one, and a bound one reaches it only where the driver leaves the parameter untyped.
|
|
144
|
+
*/
|
|
145
|
+
textSearchTarget(columns, config) {
|
|
146
|
+
const document = columns.map((column) => `COALESCE(${column}, '')`).join(` || ' ' || `);
|
|
147
|
+
return `TO_TSVECTOR(${this.textConfigArg(config)}${document})`;
|
|
148
|
+
}
|
|
149
|
+
/** A config as the text-search functions' first argument, or nothing, for the server's default. */
|
|
150
|
+
textConfigArg(config) {
|
|
151
|
+
return config === undefined ? '' : `${this.escape(config)}${this.textConfigCast}, `;
|
|
152
|
+
}
|
|
153
|
+
/** How a config literal is typed, and the function reading a search's text: CockroachDB lacks both of these. */
|
|
154
|
+
textConfigCast = '::regconfig';
|
|
155
|
+
textQueryFn = 'WEBSEARCH_TO_TSQUERY';
|
|
137
156
|
jsonContains(ctx, slot, values) {
|
|
138
157
|
return `${this.jsonValue(slot)} @> ${this.jsonVal(ctx, values)}`;
|
|
139
158
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityMeta, Query, QuerySortMap, RelationMeta, RelationQuery, Type } from '../type/index.js';
|
|
1
|
+
import type { EntityMeta, Query, QueryGroupMap, QuerySortMap, QueryWhere, RelationMeta, RelationQuery, Type } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* One relation a statement joins, keyed by the alias its columns are addressed by (`tax`,
|
|
4
4
|
* `tax.category`). `projected` tells a `$populate` join, whose columns are selected, from one only
|
|
@@ -39,6 +39,24 @@ export type QuerySortOptions = {
|
|
|
39
39
|
* columns, the `ORDER BY` and the lock agree. `claimAlias` names each join's table, parents first.
|
|
40
40
|
*/
|
|
41
41
|
export declare function resolveQueryJoins<E>(meta: EntityMeta<E>, q: Query<E>, claimAlias?: (path: string) => string): QueryJoins;
|
|
42
|
+
/**
|
|
43
|
+
* What an aggregate joins, and the `$where` left to it. Each to-one relation a `$group` path passes through
|
|
44
|
+
* is an `INNER` join, since a group of a path names a related row; a filter on one of them, keyed at the top
|
|
45
|
+
* of the `$where` where an `AND` joins it, moves into that join rather than reading its table again. Under a
|
|
46
|
+
* `$not` it could not: the join would drop the rows the negation keeps.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveGroupJoins<E>(meta: EntityMeta<E>, q: {
|
|
49
|
+
readonly $group?: QueryGroupMap<E>;
|
|
50
|
+
readonly $where?: QueryWhere<E>;
|
|
51
|
+
}, claimAlias?: (path: string) => string): {
|
|
52
|
+
readonly joins: QueryJoins;
|
|
53
|
+
readonly where: QueryWhere<E> | undefined;
|
|
54
|
+
};
|
|
55
|
+
/** The field a grouped `path` reads, and the join it reads it through: none for the entity's own. */
|
|
56
|
+
export declare function groupPathField(joins: QueryJoins, path: readonly string[]): {
|
|
57
|
+
readonly key: string;
|
|
58
|
+
readonly join: QueryJoin | undefined;
|
|
59
|
+
};
|
|
42
60
|
/**
|
|
43
61
|
* Whether a join drops parents that have no match, which is the one thing a join does to *how many*
|
|
44
62
|
* rows a read returns rather than how wide they are. A count that skips the joins has to be told, or
|