@atscript/db 0.1.151 → 0.1.153
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/agg.cjs +1 -1
- package/dist/agg.d.cts +1 -1
- package/dist/agg.d.mts +1 -1
- package/dist/agg.mjs +1 -1
- package/dist/{aggregate-fns-CyaZyb9I.mjs → aggregate-fns-B_sgl4mB.mjs} +1 -1
- package/dist/{aggregate-fns-C-UJRobm.cjs → aggregate-fns-DFtKAv7Q.cjs} +1 -1
- package/dist/{buckets-Cb0zmhDc.d.mts → buckets-CBKL_ZWE.d.mts} +205 -7
- package/dist/{buckets-tzd8iIM2.d.cts → buckets-jy8BfswO.d.cts} +205 -7
- package/dist/{column-diff-CU7KHj9-.cjs → column-diff-CbdWi_uZ.cjs} +729 -28
- package/dist/{column-diff-CnyLjbQ8.mjs → column-diff-DrE4ho_m.mjs} +730 -29
- package/dist/{db-error-DRQH4sLY.cjs → db-error-JcfJqqpk.cjs} +29 -0
- package/dist/{db-error-Az85UhTX.mjs → db-error-eSsQX7rt.mjs} +24 -1
- package/dist/{fk-diff-Bw26lwSc.d.cts → fk-diff-BIJ8l_hF.d.mts} +1 -1
- package/dist/{fk-diff-lcAnwvm9.d.mts → fk-diff-D1mrLMjU.d.cts} +1 -1
- package/dist/index.cjs +93 -5
- package/dist/index.d.cts +29 -5
- package/dist/index.d.mts +29 -5
- package/dist/index.mjs +92 -6
- package/dist/{nested-writer-cvbVAkNo.mjs → nested-writer-BOj_c_lI.mjs} +1 -1
- package/dist/{nested-writer-BzbOPSUp.cjs → nested-writer-Bl1SZS7x.cjs} +1 -1
- package/dist/ops.cjs +1 -1
- package/dist/ops.mjs +1 -1
- package/dist/plugin.cjs +48 -1
- package/dist/plugin.mjs +48 -1
- package/dist/rel.cjs +2 -2
- package/dist/rel.d.cts +2 -2
- package/dist/rel.d.mts +2 -2
- package/dist/rel.mjs +2 -2
- package/dist/{relation-helpers-Bdo4NHPC.d.mts → relation-helpers-BUsxNNGU.d.mts} +1 -1
- package/dist/{relation-helpers-DxIZKvnJ.d.cts → relation-helpers-D_8bydc2.d.cts} +1 -1
- package/dist/{relation-loader-DYab07lw.mjs → relation-loader-CNBMPfRr.mjs} +2 -1
- package/dist/{relation-loader-fNjHgBMs.cjs → relation-loader-DeulLrAw.cjs} +2 -1
- package/dist/sync.cjs +2 -2
- package/dist/sync.d.cts +2 -2
- package/dist/sync.d.mts +2 -2
- package/dist/sync.mjs +2 -2
- package/dist/{validator-tBNvM1qc.d.cts → validator-B3xlbrWf.d.cts} +36 -2
- package/dist/{validator-tBNvM1qc.d.mts → validator-B3xlbrWf.d.mts} +36 -2
- package/dist/{validator-BTiIOTKP.mjs → validator-BACewoRj.mjs} +51 -7
- package/dist/{validator-UcuJxHNT.cjs → validator-BxWwh3wf.cjs} +50 -6
- package/dist/validator.cjs +2 -2
- package/dist/validator.d.cts +1 -1
- package/dist/validator.d.mts +1 -1
- package/dist/validator.mjs +2 -2
- package/package.json +8 -8
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { a as aggregateExpressionsNotSupported,
|
|
2
|
-
import { a as SOURCE_VALUE_FNS, c as isFirstLast, o as SUPPORTED_AGGREGATE_FNS, r as BASE_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-
|
|
3
|
-
import { A as hasRelationOp, D as containsRelationFilter, F as resolveRelationFilterTree, H as tableNameOf, I as compositeKey, J as viewExprNullable, L as findFKEntryForRelation, M as noteRelationFilter, N as relGuardState, O as createRelationFilterHost, R as findFKForRelation, S as uniqueKeyTuple, U as computeOf, V as slicePerGroup, W as computedOperands, Y as walkViewExpr, _ as remapDeleteFkViolation, a as batchPatchNestedFrom, b as rowMatchesKey, c as batchReplaceNestedFrom, d as checkDepthOverflow, f as planNestedFromVia, g as enrichFkViolation, h as validateBatch, i as batchInsertNestedVia, l as batchReplaceNestedTo, m as preValidateNestedFrom, n as batchInsertNestedFrom, p as planPatchNestedTo, r as batchInsertNestedTo, s as batchPatchNestedVia, t as applyPatchNestedTo, u as batchReplaceNestedVia, v as findRowsByKeys, x as sameKey, y as pkTupleKey, z as findRemoteFK } from "./nested-writer-
|
|
1
|
+
import { a as aggregateExpressionsNotSupported, n as CasMismatchError, r as DbError, u as spaceClosedError } from "./db-error-eSsQX7rt.mjs";
|
|
2
|
+
import { a as SOURCE_VALUE_FNS, c as isFirstLast, o as SUPPORTED_AGGREGATE_FNS, r as BASE_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-B_sgl4mB.mjs";
|
|
3
|
+
import { A as hasRelationOp, D as containsRelationFilter, F as resolveRelationFilterTree, H as tableNameOf, I as compositeKey, J as viewExprNullable, K as isFieldRef, L as findFKEntryForRelation, M as noteRelationFilter, N as relGuardState, O as createRelationFilterHost, R as findFKForRelation, S as uniqueKeyTuple, U as computeOf, V as slicePerGroup, W as computedOperands, Y as walkViewExpr, _ as remapDeleteFkViolation, a as batchPatchNestedFrom, b as rowMatchesKey, c as batchReplaceNestedFrom, d as checkDepthOverflow, f as planNestedFromVia, g as enrichFkViolation, h as validateBatch, i as batchInsertNestedVia, l as batchReplaceNestedTo, m as preValidateNestedFrom, n as batchInsertNestedFrom, p as planPatchNestedTo, r as batchInsertNestedTo, s as batchPatchNestedVia, t as applyPatchNestedTo, u as batchReplaceNestedVia, v as findRowsByKeys, x as sameKey, y as pkTupleKey, z as findRemoteFK } from "./nested-writer-BOj_c_lI.mjs";
|
|
4
4
|
import { a as DERIVED_INCOMPATIBLE, n as searchMemberKind, r as numericTypeProblem, s as isJsonLeafType } from "./search-fields-DBIZKwyb.mjs";
|
|
5
5
|
import { a as isPlainObject, i as isEmptyObject, n as findAncestorInSet, o as selfOrAncestor, r as getPath, t as deletePath } from "./object-TkiJQ-Dp.mjs";
|
|
6
6
|
import { resolveAlias as resolveAlias$1 } from "./agg.mjs";
|
|
7
7
|
import { t as NOT_DIMENSION_REASON } from "./aggregate-rules-jdPrxqWa.mjs";
|
|
8
8
|
import { isDbFieldOp, separateCas, separateFieldOps } from "./ops.mjs";
|
|
9
|
-
import { a as forceNavNonOptional, c as getKeyProps, i as dbPlugin, n as buildPatchPartial, t as buildDbValidator } from "./validator-
|
|
9
|
+
import { a as forceNavNonOptional, c as getKeyProps, i as dbPlugin, n as buildPatchPartial, t as buildDbValidator } from "./validator-BACewoRj.mjs";
|
|
10
10
|
import { flattenAnnotatedType, isAnnotatedType } from "@atscript/typescript/utils";
|
|
11
|
-
import { ARITH_MAX_NODES, BUCKET_UNITS, isAggregateExpr, isAggregateOfExpr, isBucketExpr, isPrimitive, isRelationOp, isSelectArithExpr, resolveAlias, resolveBuckets } from "@uniqu/core";
|
|
11
|
+
import { ARITH_MAX_NODES, BUCKET_UNITS, isAggregateExpr, isAggregateOfExpr, isBucketExpr, isNullsPlacement, isPrimitive, isRelationOp, isSelectArithExpr, resolveAlias, resolveBuckets } from "@uniqu/core";
|
|
12
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
13
|
//#region src/logger.ts
|
|
14
14
|
const NoopLogger = {
|
|
@@ -252,6 +252,14 @@ function sourceFieldSeals(sourceType, logicalPath) {
|
|
|
252
252
|
return seals;
|
|
253
253
|
}
|
|
254
254
|
/**
|
|
255
|
+
* The annotations of one LOGICAL path of a source table (`undefined` for a
|
|
256
|
+
* path the type does not declare).
|
|
257
|
+
* @since 0.1.153
|
|
258
|
+
*/
|
|
259
|
+
function sourceFieldMetadata(sourceType, logicalPath) {
|
|
260
|
+
return sourceIndex(sourceType).flatMap.get(logicalPath)?.metadata;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
255
263
|
* Resolves a LOGICAL path of a source table (a view field's chain ref, an
|
|
256
264
|
* aggregate's field, a predicate operand) to where it is physically stored.
|
|
257
265
|
* Internal — `AtscriptDbView.resolveRefSource` is the public entry.
|
|
@@ -400,6 +408,26 @@ function rejectVersionExempt(path, why) {
|
|
|
400
408
|
throw new Error(`@db.column.version.exempt on "${path}": ${why}`);
|
|
401
409
|
}
|
|
402
410
|
/**
|
|
411
|
+
* When `fieldType` is a union / tuple / intersection, marks the entries its
|
|
412
|
+
* items already fired at the same `path` (onField is post-order) as members.
|
|
413
|
+
*/
|
|
414
|
+
function markComplexMembers(collected, path, fieldType) {
|
|
415
|
+
const kind = fieldType.type.kind;
|
|
416
|
+
if (kind !== "union" && kind !== "tuple" && kind !== "intersection") return;
|
|
417
|
+
const items = new Set(fieldType.type.items);
|
|
418
|
+
const childPrefix = `${path}.`;
|
|
419
|
+
for (let i = collected.length - 1; i >= 0; i--) {
|
|
420
|
+
const entry = collected[i];
|
|
421
|
+
if (entry.path === path) {
|
|
422
|
+
if (items.has(entry.type)) entry.member = true;
|
|
423
|
+
} else if (!entry.path.startsWith(childPrefix)) break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/** A built-in primitive inlined as a member (`number.int | null`), not a named alias. */
|
|
427
|
+
function isInlinePrimitive(type) {
|
|
428
|
+
return type.type.kind === "" && type.ref === void 0;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
403
431
|
* Computed metadata for a database table or view.
|
|
404
432
|
*
|
|
405
433
|
* Contains all field metadata, physical mapping indexes, relation definitions,
|
|
@@ -439,6 +467,8 @@ var TableMetadata = class {
|
|
|
439
467
|
navFields = /* @__PURE__ */ new Set();
|
|
440
468
|
ignoredFields = /* @__PURE__ */ new Set();
|
|
441
469
|
uniqueProps = /* @__PURE__ */ new Set();
|
|
470
|
+
/** Adapter-contributed multi-field unique keys (logical paths) — see `TMetadataOverrides.addUniqueKeys`. */
|
|
471
|
+
uniqueKeys = [];
|
|
442
472
|
defaults = /* @__PURE__ */ new Map();
|
|
443
473
|
/** Logical path → `@db.column` override (top-level keys only on document storage). */
|
|
444
474
|
columnMap = /* @__PURE__ */ new Map();
|
|
@@ -579,7 +609,7 @@ var TableMetadata = class {
|
|
|
579
609
|
topLevelArrayTag: adapter.getTopLevelArrayTag?.() ?? "db.__topLevelArray",
|
|
580
610
|
excludePhantomTypes: true,
|
|
581
611
|
onField: (path, fieldType, metadata) => {
|
|
582
|
-
|
|
612
|
+
markComplexMembers(collected, path, fieldType);
|
|
583
613
|
collected.push({
|
|
584
614
|
path,
|
|
585
615
|
type: fieldType,
|
|
@@ -587,11 +617,14 @@ var TableMetadata = class {
|
|
|
587
617
|
});
|
|
588
618
|
}
|
|
589
619
|
});
|
|
620
|
+
for (const entry of collected) if (!entry.member && isNavRelation(entry.metadata)) this.navFields.add(entry.path);
|
|
621
|
+
this._dropMemberMetadata(collected);
|
|
590
622
|
for (const entry of collected) {
|
|
591
623
|
if (findAncestorInSet(entry.path, this.navFields) !== void 0) {
|
|
592
624
|
this.ignoredFields.add(entry.path);
|
|
593
625
|
continue;
|
|
594
626
|
}
|
|
627
|
+
if (entry.member) continue;
|
|
595
628
|
this._scanGenericAnnotations(entry.path, entry.type, entry.metadata, logger);
|
|
596
629
|
if (entry.metadata.has("db.column.derived")) this.derivedFields.set(entry.path, this._validateDerivedField(entry.path, entry.type, entry.metadata, type));
|
|
597
630
|
adapter.onFieldScanned?.(entry.path, entry.type, entry.metadata);
|
|
@@ -625,6 +658,37 @@ var TableMetadata = class {
|
|
|
625
658
|
}
|
|
626
659
|
}
|
|
627
660
|
/**
|
|
661
|
+
* `flattenAnnotatedType` merges the metadata of every union / tuple member
|
|
662
|
+
* into the synthetic flat entry of the prop's path, so a member's
|
|
663
|
+
* annotations would reach the column (DDL type, size, default). Since
|
|
664
|
+
* atscript 0.1.103 a built-in primitive member carries its built-in
|
|
665
|
+
* annotations (`number.timestamp.created | null` → `@db.default.now`,
|
|
666
|
+
* `string.char` → `@expect.maxLength 1`). Rebuild such an entry from the
|
|
667
|
+
* prop's own annotations plus the non-`db.*` annotations of named members
|
|
668
|
+
* (aliases, interfaces), so the column is described as before 0.1.103.
|
|
669
|
+
*/
|
|
670
|
+
_dropMemberMetadata(collected) {
|
|
671
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
672
|
+
for (const entry of collected) {
|
|
673
|
+
const list = byPath.get(entry.path);
|
|
674
|
+
if (list) list.push(entry);
|
|
675
|
+
else byPath.set(entry.path, [entry]);
|
|
676
|
+
}
|
|
677
|
+
for (const [path, entries] of byPath) {
|
|
678
|
+
if (!entries.some((e) => e.member)) continue;
|
|
679
|
+
const flat = this.flatMap.get(path);
|
|
680
|
+
if (!flat?.__flat_union) continue;
|
|
681
|
+
const metadata = /* @__PURE__ */ new Map();
|
|
682
|
+
for (const entry of entries) {
|
|
683
|
+
if (entry.member && isInlinePrimitive(entry.type)) continue;
|
|
684
|
+
for (const [key, value] of entry.metadata) if (!entry.member || !key.startsWith("db.")) metadata.set(key, value);
|
|
685
|
+
}
|
|
686
|
+
const target = flat.metadata;
|
|
687
|
+
target.clear();
|
|
688
|
+
for (const [key, value] of metadata) target.set(key, value);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
628
692
|
* Applies adapter-provided metadata overrides atomically.
|
|
629
693
|
* Processing order: injectFields → removePrimaryKeys → addPrimaryKeys → addUniqueFields.
|
|
630
694
|
*/
|
|
@@ -638,6 +702,7 @@ var TableMetadata = class {
|
|
|
638
702
|
for (const field of overrides.addPrimaryKeys) if (!this.primaryKeys.includes(field)) this.primaryKeys.push(field);
|
|
639
703
|
}
|
|
640
704
|
if (overrides.addUniqueFields) for (const field of overrides.addUniqueFields) this.uniqueProps.add(field);
|
|
705
|
+
if (overrides.addUniqueKeys) this.uniqueKeys.push(...overrides.addUniqueKeys.map((key) => [...key]));
|
|
641
706
|
}
|
|
642
707
|
/**
|
|
643
708
|
* Scans `@db.*` and `@meta.id` annotations on a field during flattening.
|
|
@@ -992,7 +1057,11 @@ var TableMetadata = class {
|
|
|
992
1057
|
_buildFieldDescriptors(adapter, rootType) {
|
|
993
1058
|
const descriptors = [];
|
|
994
1059
|
const skipFlattening = this.nestedObjects;
|
|
995
|
-
const indexedFields = new Set([
|
|
1060
|
+
const indexedFields = new Set([
|
|
1061
|
+
...this.primaryKeys,
|
|
1062
|
+
...this.uniqueProps,
|
|
1063
|
+
...this.uniqueKeys.flat()
|
|
1064
|
+
]);
|
|
996
1065
|
for (const index of this.indexes.values()) for (const f of index.fields) indexedFields.add(f.name);
|
|
997
1066
|
for (const [path, type] of this.flatMap.entries()) {
|
|
998
1067
|
if (!path) continue;
|
|
@@ -1187,6 +1256,8 @@ var TableMetadata = class {
|
|
|
1187
1256
|
* overrides (`addUniqueFields`); these aren't reflected in
|
|
1188
1257
|
* `this.indexes` but still legitimate addressing identifications.
|
|
1189
1258
|
* Deduped against any single-field unique index already captured.
|
|
1259
|
+
* - `this.uniqueKeys` — multi-field uniques contributed by adapter
|
|
1260
|
+
* overrides (`addUniqueKeys`).
|
|
1190
1261
|
*/
|
|
1191
1262
|
_buildIdentifications() {
|
|
1192
1263
|
const out = [];
|
|
@@ -1211,6 +1282,10 @@ var TableMetadata = class {
|
|
|
1211
1282
|
source: `unique:${field}`
|
|
1212
1283
|
});
|
|
1213
1284
|
}
|
|
1285
|
+
for (const key of this.uniqueKeys) out.push({
|
|
1286
|
+
fields: [...key],
|
|
1287
|
+
source: `unique:${key.join("+")}`
|
|
1288
|
+
});
|
|
1214
1289
|
this._identifications = out;
|
|
1215
1290
|
}
|
|
1216
1291
|
/** Legitimate row-identifier shapes — primary key first, then each unique index. */
|
|
@@ -1575,6 +1650,18 @@ function rewriteIntegerRegex(filter, meta) {
|
|
|
1575
1650
|
}
|
|
1576
1651
|
//#endregion
|
|
1577
1652
|
//#region src/query/aggregate-expr.ts
|
|
1653
|
+
/**
|
|
1654
|
+
* Output aliases of the computed `$select` entries of a grouped query —
|
|
1655
|
+
* aggregates, expressions and calendar buckets (`$as`, else uniqu's
|
|
1656
|
+
* `resolveAlias`).
|
|
1657
|
+
*/
|
|
1658
|
+
function computedAliases(select) {
|
|
1659
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
1660
|
+
if (!Array.isArray(select)) return aliases;
|
|
1661
|
+
for (const item of select) if (isAggregateExpr(item) || isBucketExpr(item)) aliases.add(resolveAlias(item));
|
|
1662
|
+
else if (isAggregateOfExpr(item) || isSelectArithExpr(item)) aliases.add(item.$as);
|
|
1663
|
+
return aliases;
|
|
1664
|
+
}
|
|
1578
1665
|
/** Most nodes a group-level expression may have once the aliases it names are inlined (SQL repeats them). */
|
|
1579
1666
|
const EXPANDED_MAX_NODES = 4 * ARITH_MAX_NODES;
|
|
1580
1667
|
/** Most expanded nodes ALL the expression entries of one query may have together. */
|
|
@@ -2059,7 +2146,7 @@ var FieldMappingStrategy = class {
|
|
|
2059
2146
|
*/
|
|
2060
2147
|
translateAggregateQuery(query, meta, buckets, exprs = [], rowOrder) {
|
|
2061
2148
|
const controls = query.controls;
|
|
2062
|
-
const aliases =
|
|
2149
|
+
const aliases = computedAliases(controls.$select);
|
|
2063
2150
|
const physicalBuckets = buckets.map((b) => ({
|
|
2064
2151
|
...b,
|
|
2065
2152
|
field: this.physicalPath(b.field, meta),
|
|
@@ -2076,6 +2163,7 @@ var FieldMappingStrategy = class {
|
|
|
2076
2163
|
$groupBy: this.renamesPaths(meta) ? controls.$groupBy.map((key) => aliases.has(key) ? key : this.physicalPath(key, meta)) : controls.$groupBy,
|
|
2077
2164
|
$select: select ? new UniquSelect(select, meta.allPhysicalFields, physicalBuckets, computed) : void 0,
|
|
2078
2165
|
$sort: controls.$sort && this.physicalSort(controls.$sort, meta, aliases),
|
|
2166
|
+
$nulls: controls.$nulls && this.physicalNulls(controls.$nulls, meta, aliases),
|
|
2079
2167
|
$having: controls.$having ? this.translateHaving(controls.$having, meta, aliases) : void 0
|
|
2080
2168
|
},
|
|
2081
2169
|
insights: query.insights
|
|
@@ -2099,13 +2187,6 @@ var FieldMappingStrategy = class {
|
|
|
2099
2187
|
...this.translateFilter(fields, meta)
|
|
2100
2188
|
} : out;
|
|
2101
2189
|
}
|
|
2102
|
-
/** Output aliases of the computed `$select` entries (aggregates, expressions and calendar buckets). */
|
|
2103
|
-
computedAliasSet(select) {
|
|
2104
|
-
const aliases = /* @__PURE__ */ new Set();
|
|
2105
|
-
for (const item of select ?? []) if (isAggregateExpr(item) || isBucketExpr(item)) aliases.add(resolveAlias$1(item));
|
|
2106
|
-
else if (isAggregateOfExpr(item) || isSelectArithExpr(item)) aliases.add(item.$as);
|
|
2107
|
-
return aliases;
|
|
2108
|
-
}
|
|
2109
2190
|
/**
|
|
2110
2191
|
* The arithmetic and `$rowOrder` parts of a grouped query with PHYSICAL
|
|
2111
2192
|
* names: a row-level operand is a column; a group-level operand stays an
|
|
@@ -2144,7 +2225,8 @@ var FieldMappingStrategy = class {
|
|
|
2144
2225
|
if (rowOrder?.length) {
|
|
2145
2226
|
order = rowOrder.map((k) => ({
|
|
2146
2227
|
column: this.physicalPath(k.field, meta),
|
|
2147
|
-
desc: k.desc
|
|
2228
|
+
desc: k.desc,
|
|
2229
|
+
...k.nulls && { nulls: k.nulls }
|
|
2148
2230
|
}));
|
|
2149
2231
|
for (const pk of meta.primaryKeys) {
|
|
2150
2232
|
const column = this.physicalPath(pk, meta);
|
|
@@ -2194,9 +2276,20 @@ var FieldMappingStrategy = class {
|
|
|
2194
2276
|
}
|
|
2195
2277
|
/** `$sort` with physical keys; computed `aliases` (grouped queries) pass through. */
|
|
2196
2278
|
physicalSort(sort, meta, aliases) {
|
|
2197
|
-
|
|
2279
|
+
return this.physicalKeys(sort, meta, aliases);
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* `$nulls` with physical keys, like {@link physicalSort}. The core hands
|
|
2283
|
+
* over only the resolved entries (since 0.1.153).
|
|
2284
|
+
*/
|
|
2285
|
+
physicalNulls(nulls, meta, aliases) {
|
|
2286
|
+
return this.physicalKeys(nulls, meta, aliases);
|
|
2287
|
+
}
|
|
2288
|
+
/** `map` with its field-path keys made physical; computed `aliases` pass through. */
|
|
2289
|
+
physicalKeys(map, meta, aliases) {
|
|
2290
|
+
if (!this.renamesPaths(meta)) return map;
|
|
2198
2291
|
const translated = {};
|
|
2199
|
-
for (const [key,
|
|
2292
|
+
for (const [key, value] of Object.entries(map)) translated[aliases?.has(key) ? key : this.physicalPath(key, meta)] = value;
|
|
2200
2293
|
return translated;
|
|
2201
2294
|
}
|
|
2202
2295
|
/**
|
|
@@ -2472,7 +2565,8 @@ var DocumentFieldMapper = class extends FieldMappingStrategy {
|
|
|
2472
2565
|
...controls,
|
|
2473
2566
|
$with: void 0,
|
|
2474
2567
|
$select: select ? new UniquSelect(select, meta.allPhysicalFields) : void 0,
|
|
2475
|
-
$sort: controls?.$sort && this.physicalSort(controls.$sort, meta)
|
|
2568
|
+
$sort: controls?.$sort && this.physicalSort(controls.$sort, meta),
|
|
2569
|
+
$nulls: controls?.$nulls && this.physicalNulls(controls.$nulls, meta)
|
|
2476
2570
|
},
|
|
2477
2571
|
insights: query.insights
|
|
2478
2572
|
};
|
|
@@ -2663,6 +2757,7 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
2663
2757
|
}
|
|
2664
2758
|
result.$sort = translated;
|
|
2665
2759
|
}
|
|
2760
|
+
if (controls.$nulls) result.$nulls = this.physicalNulls(controls.$nulls, meta);
|
|
2666
2761
|
if (controls.$select) {
|
|
2667
2762
|
let translatedRaw;
|
|
2668
2763
|
if (Array.isArray(controls.$select)) {
|
|
@@ -3244,6 +3339,10 @@ function collectQueryPaths(query, aggregate) {
|
|
|
3244
3339
|
refs.groupBy = bucketAliases.size > 0 ? groupBy.filter((name) => !bucketAliases.has(name)) : groupBy;
|
|
3245
3340
|
for (const name of sortFieldNames(controls.$sort)) if (!aliases.has(name)) refs.sort.push(name);
|
|
3246
3341
|
if (refs.aggregateMode) refs.sort.push(...rowOrderKeys(controls.$rowOrder));
|
|
3342
|
+
const nulls = controls.$nulls;
|
|
3343
|
+
if (isPlainObject(nulls)) {
|
|
3344
|
+
for (const name of Object.keys(nulls)) if (!aliases.has(name) && !refs.sort.includes(name)) refs.sort.push(name);
|
|
3345
|
+
}
|
|
3247
3346
|
if (refs.aggregateMode) collectFilterKeys(controls.$having, (path) => refs.having.push(path), aliases, refs);
|
|
3248
3347
|
return refs;
|
|
3249
3348
|
}
|
|
@@ -3515,6 +3614,130 @@ function guardBucketUnits(adapter, buckets) {
|
|
|
3515
3614
|
}]);
|
|
3516
3615
|
}
|
|
3517
3616
|
//#endregion
|
|
3617
|
+
//#region src/query/nulls.ts
|
|
3618
|
+
/** Annotation key of a field's default NULL placement (`@db.sort.nulls`). */
|
|
3619
|
+
const SORT_NULLS_ANNOTATION = "db.sort.nulls";
|
|
3620
|
+
/**
|
|
3621
|
+
* Per-table NULL-placement resolution (since 0.1.153): built once per
|
|
3622
|
+
* readable, memoizing the per-path answers every sorted read asks for.
|
|
3623
|
+
*/
|
|
3624
|
+
var NullsSource = class {
|
|
3625
|
+
_init;
|
|
3626
|
+
_nullable = /* @__PURE__ */ new Map();
|
|
3627
|
+
_hasDefaults;
|
|
3628
|
+
constructor(_init) {
|
|
3629
|
+
this._init = _init;
|
|
3630
|
+
}
|
|
3631
|
+
/** Whether any field carries a `@db.sort.nulls` default. */
|
|
3632
|
+
hasDefaults() {
|
|
3633
|
+
return this._hasDefaults ??= [...this._init.meta.flatMap.keys()].some((path) => this.defaultOf(path) !== void 0);
|
|
3634
|
+
}
|
|
3635
|
+
/** The `@db.sort.nulls` default of the LOGICAL field `path`, if any. */
|
|
3636
|
+
defaultOf(path) {
|
|
3637
|
+
const value = this._init.meta.flatMap.get(path)?.metadata.get(SORT_NULLS_ANNOTATION);
|
|
3638
|
+
return isNullsPlacement(value) ? value : void 0;
|
|
3639
|
+
}
|
|
3640
|
+
/**
|
|
3641
|
+
* Whether ordering by the LOGICAL field `path` can meet NULL (or a missing
|
|
3642
|
+
* value): the field or one of its parent objects is optional, the field is
|
|
3643
|
+
* a derived column, it is not a known field, or the source is a view. A
|
|
3644
|
+
* `$nulls` entry on any other field changes nothing and is dropped, so the
|
|
3645
|
+
* plain `ORDER BY` keeps using the index.
|
|
3646
|
+
*/
|
|
3647
|
+
nullable(path) {
|
|
3648
|
+
let answer = this._nullable.get(path);
|
|
3649
|
+
if (answer === void 0) {
|
|
3650
|
+
answer = this._init.isView || this._nullableField(path);
|
|
3651
|
+
this._nullable.set(path, answer);
|
|
3652
|
+
}
|
|
3653
|
+
return answer;
|
|
3654
|
+
}
|
|
3655
|
+
_nullableField(path) {
|
|
3656
|
+
const { meta } = this._init;
|
|
3657
|
+
const fd = meta.descriptorByPath.get(path);
|
|
3658
|
+
if (!fd || fd.derived) return true;
|
|
3659
|
+
for (let p = path;;) {
|
|
3660
|
+
if (meta.flatMap.get(p)?.optional === true) return true;
|
|
3661
|
+
const dot = p.lastIndexOf(".");
|
|
3662
|
+
if (dot === -1) return false;
|
|
3663
|
+
p = p.slice(0, dot);
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3667
|
+
* The effective `$nulls` of the `$sort` keys of one read: the request's
|
|
3668
|
+
* entry, else the field's `@db.sort.nulls` default; entries on fields that
|
|
3669
|
+
* cannot be NULL ({@link nullable}) are dropped. `aliases` are the computed
|
|
3670
|
+
* output names of a grouped read — they take a request entry only and are
|
|
3671
|
+
* never dropped. Entries for keys that are not ordered by are ignored. On
|
|
3672
|
+
* an adapter without support a request entry is refused and defaults are
|
|
3673
|
+
* dropped.
|
|
3674
|
+
*
|
|
3675
|
+
* @returns LOGICAL keys (or aliases) → placement; `undefined` when none.
|
|
3676
|
+
* @throws DbError `INVALID_QUERY` on a malformed `$nulls`, or a request
|
|
3677
|
+
* entry the adapter cannot honour.
|
|
3678
|
+
*/
|
|
3679
|
+
resolveSort(sort, rawNulls, aliases) {
|
|
3680
|
+
const requested = checkNullsControl(rawNulls);
|
|
3681
|
+
if (!isPlainObject(sort)) return void 0;
|
|
3682
|
+
let nulls;
|
|
3683
|
+
for (const key of Object.keys(sort)) {
|
|
3684
|
+
const asked = requested && Object.hasOwn(requested, key) ? requested[key] : void 0;
|
|
3685
|
+
const placement = aliases?.has(key) ? asked : this._placement(key, asked);
|
|
3686
|
+
if (placement && this._allowed(asked)) (nulls ??= {})[key] = placement;
|
|
3687
|
+
}
|
|
3688
|
+
return nulls;
|
|
3689
|
+
}
|
|
3690
|
+
/**
|
|
3691
|
+
* `$rowOrder` keys (as uniqu resolved them, `nulls` from the request) under
|
|
3692
|
+
* the rules of {@link resolveSort}: defaults filled in, placements on
|
|
3693
|
+
* fields that cannot be NULL (or on an unsupporting adapter) dropped.
|
|
3694
|
+
*/
|
|
3695
|
+
resolveRowOrder(rowOrder) {
|
|
3696
|
+
return rowOrder?.map((key) => {
|
|
3697
|
+
const placement = this._placement(key.field, key.nulls);
|
|
3698
|
+
const nulls = placement && this._allowed(key.nulls) ? placement : void 0;
|
|
3699
|
+
if (nulls === key.nulls) return key;
|
|
3700
|
+
return nulls ? {
|
|
3701
|
+
...key,
|
|
3702
|
+
nulls
|
|
3703
|
+
} : {
|
|
3704
|
+
field: key.field,
|
|
3705
|
+
desc: key.desc
|
|
3706
|
+
};
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3709
|
+
/** The placement of field `path` — the request's `asked`, else its default — or `undefined` when it cannot be NULL. */
|
|
3710
|
+
_placement(path, asked) {
|
|
3711
|
+
return this.nullable(path) ? asked ?? this.defaultOf(path) : void 0;
|
|
3712
|
+
}
|
|
3713
|
+
/** Whether a placement may be forwarded; a requested one on an unsupporting adapter is refused. */
|
|
3714
|
+
_allowed(asked) {
|
|
3715
|
+
if (this._init.supported) return true;
|
|
3716
|
+
if (asked) throw new DbError("INVALID_QUERY", [{
|
|
3717
|
+
path: "$nulls",
|
|
3718
|
+
message: `NULL placement ($nulls / :first / :last) is not supported by the adapter of "${this._init.tableName}"`
|
|
3719
|
+
}]);
|
|
3720
|
+
return false;
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
/**
|
|
3724
|
+
* The raw `$nulls` control as a checked map, or `undefined` when absent.
|
|
3725
|
+
* @throws DbError `INVALID_QUERY` (`path: "$nulls"`) on a non-object or a
|
|
3726
|
+
* value other than `'first'` / `'last'`.
|
|
3727
|
+
*/
|
|
3728
|
+
function checkNullsControl(raw) {
|
|
3729
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
3730
|
+
if (!isPlainObject(raw)) throw new DbError("INVALID_QUERY", [{
|
|
3731
|
+
path: "$nulls",
|
|
3732
|
+
message: "$nulls must be an object of field → 'first' | 'last'"
|
|
3733
|
+
}]);
|
|
3734
|
+
for (const [field, value] of Object.entries(raw)) if (!isNullsPlacement(value)) throw new DbError("INVALID_QUERY", [{
|
|
3735
|
+
path: "$nulls",
|
|
3736
|
+
message: `$nulls "${field}" must be 'first' or 'last'`
|
|
3737
|
+
}]);
|
|
3738
|
+
return raw;
|
|
3739
|
+
}
|
|
3740
|
+
//#endregion
|
|
3518
3741
|
//#region src/shared/index-messages.ts
|
|
3519
3742
|
/**
|
|
3520
3743
|
* Wording of the "index not found" errors — one source for the core, every
|
|
@@ -3598,6 +3821,26 @@ function resolveDefaultFromMetadata(metadata) {
|
|
|
3598
3821
|
};
|
|
3599
3822
|
}
|
|
3600
3823
|
/**
|
|
3824
|
+
* `query` with `$groupBy` defaulted to `[]` — an aggregate without one is the
|
|
3825
|
+
* ungrouped query (one group), as `$groupBy: []` documents it.
|
|
3826
|
+
* @throws DbError `INVALID_QUERY` when `$groupBy` is present but not an array.
|
|
3827
|
+
*/
|
|
3828
|
+
function withGroupBy(query) {
|
|
3829
|
+
const controls = query.controls ?? {};
|
|
3830
|
+
if (Array.isArray(controls.$groupBy)) return query;
|
|
3831
|
+
if (controls.$groupBy !== void 0 && controls.$groupBy !== null) throw new DbError("INVALID_QUERY", [{
|
|
3832
|
+
path: "$groupBy",
|
|
3833
|
+
message: "$groupBy must be an array of field names"
|
|
3834
|
+
}]);
|
|
3835
|
+
return {
|
|
3836
|
+
...query,
|
|
3837
|
+
controls: {
|
|
3838
|
+
...controls,
|
|
3839
|
+
$groupBy: []
|
|
3840
|
+
}
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
/**
|
|
3601
3844
|
* Checks whether an id value is type-compatible with a field's design type.
|
|
3602
3845
|
* Used by `findById` to skip primary-key lookup when the id clearly can't match,
|
|
3603
3846
|
* falling through to unique-property search instead.
|
|
@@ -3739,7 +3982,7 @@ var AtscriptDbReadable = class {
|
|
|
3739
3982
|
_translateForAdapter(query) {
|
|
3740
3983
|
this._ensureBuilt();
|
|
3741
3984
|
this._guardQuery(query);
|
|
3742
|
-
return this._fieldMapper.translateQuery(query, this._meta);
|
|
3985
|
+
return this._fieldMapper.translateQuery(this._withStableSort(query), this._meta);
|
|
3743
3986
|
}
|
|
3744
3987
|
/**
|
|
3745
3988
|
* Physical rows of THIS table → logical rows (field mapping, value
|
|
@@ -3839,7 +4082,8 @@ var AtscriptDbReadable = class {
|
|
|
3839
4082
|
}
|
|
3840
4083
|
/**
|
|
3841
4084
|
* Physical column lists that must be unique: the primary key (when declared)
|
|
3842
|
-
* followed by every unique index and every adapter-contributed unique field. Used by
|
|
4085
|
+
* followed by every unique index and every adapter-contributed unique field / key. Used by
|
|
4086
|
+
* conflict-ignoring inserts and view read pruning.
|
|
3843
4087
|
* @since 0.1.148
|
|
3844
4088
|
*/
|
|
3845
4089
|
get uniqueKeySets() {
|
|
@@ -3856,6 +4100,7 @@ var AtscriptDbReadable = class {
|
|
|
3856
4100
|
const column = this._meta.physicalPath(prop);
|
|
3857
4101
|
if (!sets.some((s) => s.length === 1 && s[0] === column)) sets.push([column]);
|
|
3858
4102
|
}
|
|
4103
|
+
for (const key of this._meta.uniqueKeys) sets.push(key.map((f) => this._meta.physicalPath(f)));
|
|
3859
4104
|
for (const set of sets) Object.freeze(set);
|
|
3860
4105
|
return Object.freeze(sets);
|
|
3861
4106
|
}
|
|
@@ -4100,6 +4345,7 @@ var AtscriptDbReadable = class {
|
|
|
4100
4345
|
widened = widen.added;
|
|
4101
4346
|
}
|
|
4102
4347
|
}
|
|
4348
|
+
readQuery = this._withStableSort(readQuery);
|
|
4103
4349
|
return {
|
|
4104
4350
|
translated: this._fieldMapper.translateQuery(readQuery, this._meta),
|
|
4105
4351
|
controls: readQuery.controls,
|
|
@@ -4121,6 +4367,99 @@ var AtscriptDbReadable = class {
|
|
|
4121
4367
|
}
|
|
4122
4368
|
return rows;
|
|
4123
4369
|
}
|
|
4370
|
+
/**
|
|
4371
|
+
* `query` with {@link _withPkTieBreak} applied and its `$nulls` resolved
|
|
4372
|
+
* ({@link NullsSource.resolveSort}) — a copy when either changes.
|
|
4373
|
+
*/
|
|
4374
|
+
_withStableSort(query) {
|
|
4375
|
+
const controls = query.controls;
|
|
4376
|
+
const raw = controls?.$nulls;
|
|
4377
|
+
const source = this._nulls;
|
|
4378
|
+
const nulls = raw !== void 0 || source.hasDefaults() ? source.resolveSort(controls?.$sort, raw) : void 0;
|
|
4379
|
+
const sort = this._withPkTieBreak(controls);
|
|
4380
|
+
if (!sort && nulls === raw) return query;
|
|
4381
|
+
const next = { ...controls };
|
|
4382
|
+
if (sort) next.$sort = sort;
|
|
4383
|
+
if (nulls) next.$nulls = nulls;
|
|
4384
|
+
else delete next.$nulls;
|
|
4385
|
+
return {
|
|
4386
|
+
...query,
|
|
4387
|
+
controls: next
|
|
4388
|
+
};
|
|
4389
|
+
}
|
|
4390
|
+
_nullsCache;
|
|
4391
|
+
/** NULL-placement resolution for this table (`$nulls`, `@db.sort.nulls`; since 0.1.153). */
|
|
4392
|
+
get _nulls() {
|
|
4393
|
+
return this._nullsCache ??= new NullsSource({
|
|
4394
|
+
meta: this._meta,
|
|
4395
|
+
tableName: this.tableName,
|
|
4396
|
+
isView: this.isView,
|
|
4397
|
+
supported: this.adapter.supportsNullsPlacement()
|
|
4398
|
+
});
|
|
4399
|
+
}
|
|
4400
|
+
/**
|
|
4401
|
+
* `$sort` with the primary-key tie-breaker appended (since 0.1.153), or
|
|
4402
|
+
* `undefined` when the read keeps its `$sort` as written. Rows that tie on
|
|
4403
|
+
* every `$sort` key otherwise come back in an engine-chosen order that can
|
|
4404
|
+
* differ between two reads, so offset pages overlap or skip rows. The
|
|
4405
|
+
* primary-key fields the `$sort` leaves out are appended in declaration
|
|
4406
|
+
* order, in the direction of the LAST `$sort` key (a backward index scan
|
|
4407
|
+
* serves `amount DESC, id DESC`).
|
|
4408
|
+
*
|
|
4409
|
+
* Unchanged when there is no `$sort` (no order is imposed), when the
|
|
4410
|
+
* `$sort` already names every field of the primary key or of a unique key
|
|
4411
|
+
* over non-nullable fields ({@link _totalOrderKeys}), when the table has no
|
|
4412
|
+
* primary key (a view without `@meta.id`), and on grouped / aggregate /
|
|
4413
|
+
* `$count` reads. Search, vector and geo relevance is never touched: the
|
|
4414
|
+
* key is appended only to an explicit `$sort`. Every read path applies it
|
|
4415
|
+
* ({@link _translateRead}, and {@link _translateForAdapter} for a native
|
|
4416
|
+
* `$with` loader), so nested relation rows get it too.
|
|
4417
|
+
*/
|
|
4418
|
+
_withPkTieBreak(controls) {
|
|
4419
|
+
const sort = controls?.$sort;
|
|
4420
|
+
if (!sort || typeof sort !== "object") return void 0;
|
|
4421
|
+
const keys = Object.keys(sort);
|
|
4422
|
+
if (keys.length === 0 || controls.$count) return void 0;
|
|
4423
|
+
if (Array.isArray(controls.$groupBy) && controls.$groupBy.length > 0) return void 0;
|
|
4424
|
+
const select = controls.$select;
|
|
4425
|
+
if (Array.isArray(select) && select.some((item) => isAggregateExpr(item) || isAggregateOfExpr(item))) return;
|
|
4426
|
+
const pk = this._meta.primaryKeys;
|
|
4427
|
+
if (pk.length === 0) return void 0;
|
|
4428
|
+
const sorted = new Set(keys.map((key) => this._meta.physicalPath(key)));
|
|
4429
|
+
if (this._totalOrderKeys().some((set) => set.every((column) => sorted.has(column)))) return;
|
|
4430
|
+
const dir = sort[keys[keys.length - 1]];
|
|
4431
|
+
const next = { ...sort };
|
|
4432
|
+
for (const field of pk) if (!sorted.has(this._meta.physicalPath(field))) next[field] = dir;
|
|
4433
|
+
return next;
|
|
4434
|
+
}
|
|
4435
|
+
_totalOrderKeysCache;
|
|
4436
|
+
/**
|
|
4437
|
+
* Physical column sets that order rows totally: the primary key, then
|
|
4438
|
+
* every unique index (and adapter-contributed unique field) whose fields
|
|
4439
|
+
* are all non-nullable — a unique key over a nullable column admits
|
|
4440
|
+
* several NULLs, so it does not.
|
|
4441
|
+
*/
|
|
4442
|
+
_totalOrderKeys() {
|
|
4443
|
+
if (this._totalOrderKeysCache) return this._totalOrderKeysCache;
|
|
4444
|
+
const meta = this._meta;
|
|
4445
|
+
const nullable = (logical) => {
|
|
4446
|
+
for (let path = logical;;) {
|
|
4447
|
+
if (meta.flatMap.get(path)?.optional === true) return true;
|
|
4448
|
+
const dot = path.lastIndexOf(".");
|
|
4449
|
+
if (dot === -1) return false;
|
|
4450
|
+
path = path.slice(0, dot);
|
|
4451
|
+
}
|
|
4452
|
+
};
|
|
4453
|
+
const sets = [];
|
|
4454
|
+
if (meta.primaryKeys.length > 0) sets.push(meta.primaryKeys.map((f) => meta.physicalPath(f)));
|
|
4455
|
+
for (const index of meta.indexes.values()) {
|
|
4456
|
+
if (index.type !== "unique") continue;
|
|
4457
|
+
if (index.fields.every((f) => f.optional !== true && !nullable(meta.physicalToPath.get(f.name) ?? f.name))) sets.push(index.fields.map((f) => f.name));
|
|
4458
|
+
}
|
|
4459
|
+
for (const prop of meta.uniqueProps) if (!nullable(prop)) sets.push([meta.physicalPath(prop)]);
|
|
4460
|
+
this._totalOrderKeysCache = sets;
|
|
4461
|
+
return sets;
|
|
4462
|
+
}
|
|
4124
4463
|
/** `$select` plus the join keys of `withRelations` it leaves out — `undefined` when none is missing. */
|
|
4125
4464
|
_widenSelectForWith(select, withRelations) {
|
|
4126
4465
|
const keys = /* @__PURE__ */ new Set();
|
|
@@ -4331,6 +4670,7 @@ var AtscriptDbReadable = class {
|
|
|
4331
4670
|
*/
|
|
4332
4671
|
async aggregate(query) {
|
|
4333
4672
|
this._ensureBuilt();
|
|
4673
|
+
query = withGroupBy(query);
|
|
4334
4674
|
const { $groupBy, $select } = query.controls;
|
|
4335
4675
|
const computed = resolveComputedSelect(query.controls, this._meta, true);
|
|
4336
4676
|
const { buckets } = computed;
|
|
@@ -4389,11 +4729,36 @@ var AtscriptDbReadable = class {
|
|
|
4389
4729
|
if (!searchTerm.trim()) return query.controls.$count ? [{ count: 0 }] : [];
|
|
4390
4730
|
}
|
|
4391
4731
|
guardAggregate(this._meta, this.adapter, query, computed);
|
|
4392
|
-
const
|
|
4732
|
+
const ordered = this._aggregateNulls(query, computed.rowOrder);
|
|
4733
|
+
const dbQuery = this._fieldMapper.translateAggregateQuery(ordered.query, this._meta, buckets, computed.exprs, ordered.rowOrder);
|
|
4393
4734
|
const results = await this.adapter.aggregate(dbQuery);
|
|
4394
4735
|
return this._fromRead(results, query.controls, this._aliasFields($select));
|
|
4395
4736
|
}
|
|
4396
4737
|
/**
|
|
4738
|
+
* A grouped read's `$nulls` resolved for its `$sort` keys (computed aliases
|
|
4739
|
+
* take a request entry only) and its `$rowOrder` keys — see
|
|
4740
|
+
* {@link NullsSource}.
|
|
4741
|
+
*/
|
|
4742
|
+
_aggregateNulls(query, rowOrder) {
|
|
4743
|
+
const controls = query.controls;
|
|
4744
|
+
const source = this._nulls;
|
|
4745
|
+
if (controls.$nulls === void 0 && !source.hasDefaults()) return {
|
|
4746
|
+
query,
|
|
4747
|
+
rowOrder
|
|
4748
|
+
};
|
|
4749
|
+
const nulls = source.resolveSort(controls.$sort, controls.$nulls, computedAliases(controls.$select));
|
|
4750
|
+
const next = { ...controls };
|
|
4751
|
+
if (nulls) next.$nulls = nulls;
|
|
4752
|
+
else delete next.$nulls;
|
|
4753
|
+
return {
|
|
4754
|
+
query: {
|
|
4755
|
+
...query,
|
|
4756
|
+
controls: next
|
|
4757
|
+
},
|
|
4758
|
+
rowOrder: source.resolveRowOrder(rowOrder)
|
|
4759
|
+
};
|
|
4760
|
+
}
|
|
4761
|
+
/**
|
|
4397
4762
|
* The computed aliases of one source field that is a boolean or a decimal —
|
|
4398
4763
|
* `min` / `max` / `first` / `last` — with that field's descriptor: the
|
|
4399
4764
|
* aggregate row's value is coerced like the column's own on read.
|
|
@@ -4427,6 +4792,14 @@ var AtscriptDbReadable = class {
|
|
|
4427
4792
|
aggregateFns() {
|
|
4428
4793
|
return this.adapter.aggregateFns();
|
|
4429
4794
|
}
|
|
4795
|
+
/**
|
|
4796
|
+
* Whether the adapter honours a requested NULL placement (`$nulls`,
|
|
4797
|
+
* `@db.sort.nulls`) — proxies adapter capability.
|
|
4798
|
+
* @since 0.1.153
|
|
4799
|
+
*/
|
|
4800
|
+
supportsNullsPlacement() {
|
|
4801
|
+
return this.adapter.supportsNullsPlacement();
|
|
4802
|
+
}
|
|
4430
4803
|
/** Whether the adapter can sort by a given field (proxies adapter capability). */
|
|
4431
4804
|
canSortField(fd) {
|
|
4432
4805
|
return this.adapter.canSortField(fd);
|
|
@@ -4831,7 +5204,7 @@ var AtscriptDbReadable = class {
|
|
|
4831
5204
|
* Public entry point for relation loading. Used by adapters for nested $with delegation.
|
|
4832
5205
|
*/
|
|
4833
5206
|
async loadRelations(rows, withRelations) {
|
|
4834
|
-
const { loadRelationsImpl } = await import("./relation-loader-
|
|
5207
|
+
const { loadRelationsImpl } = await import("./relation-loader-CNBMPfRr.mjs").then((n) => n.n);
|
|
4835
5208
|
return loadRelationsImpl(rows, withRelations, this);
|
|
4836
5209
|
}
|
|
4837
5210
|
/**
|
|
@@ -5217,6 +5590,24 @@ var BaseDbAdapter = class {
|
|
|
5217
5590
|
return false;
|
|
5218
5591
|
}
|
|
5219
5592
|
/**
|
|
5593
|
+
* Whether this adapter honours a requested NULL placement: `controls.$nulls`
|
|
5594
|
+
* (PHYSICAL `$sort` keys → `'first'` / `'last'`, or computed aliases on a
|
|
5595
|
+
* grouped read) and `TRowOrderKey.nulls` on the `first` / `last` row
|
|
5596
|
+
* order. NULL (and, on document stores, a missing value) goes before or
|
|
5597
|
+
* after every non-null value whatever the sort direction. The core only
|
|
5598
|
+
* forwards entries for keys that can be NULL; a key without an entry keeps
|
|
5599
|
+
* the adapter's native order (`$sort`) or "NULL is the smallest value"
|
|
5600
|
+
* (grouped / row order). Default `false` (fail-closed): the core refuses a
|
|
5601
|
+
* request naming a placement with `INVALID_QUERY` and drops
|
|
5602
|
+
* `@db.sort.nulls` defaults. moost-db advertises it as
|
|
5603
|
+
* `/meta.nullsPlacement`.
|
|
5604
|
+
*
|
|
5605
|
+
* @since 0.1.153
|
|
5606
|
+
*/
|
|
5607
|
+
supportsNullsPlacement() {
|
|
5608
|
+
return false;
|
|
5609
|
+
}
|
|
5610
|
+
/**
|
|
5220
5611
|
* Revision of how this adapter renders a managed view (its SQL / pipeline)
|
|
5221
5612
|
* from an unchanged view definition. Stored in each managed view's sync
|
|
5222
5613
|
* snapshot when defined, so bumping it recreates every managed view of the
|
|
@@ -5240,6 +5631,18 @@ var BaseDbAdapter = class {
|
|
|
5240
5631
|
return NO_VIEW_CAPABILITIES;
|
|
5241
5632
|
}
|
|
5242
5633
|
/**
|
|
5634
|
+
* Whether reads of a managed view drop the LEFT joins the query does not
|
|
5635
|
+
* need (`AtscriptDbView.readPlan`) — reading through an inline, pruned
|
|
5636
|
+
* definition instead of the stored view. Only adapters that implement it
|
|
5637
|
+
* honour it; each sets its own default from its constructor options (the
|
|
5638
|
+
* bundled MySQL, SQLite and MongoDB adapters: on; PostgreSQL removes such
|
|
5639
|
+
* joins itself). Set it to `false` on one view's adapter
|
|
5640
|
+
* (`space.getAdapter(View)`) to read that view by name again.
|
|
5641
|
+
*
|
|
5642
|
+
* @since 0.1.153
|
|
5643
|
+
*/
|
|
5644
|
+
viewJoinPruning = false;
|
|
5645
|
+
/**
|
|
5243
5646
|
* Whether this adapter enforces foreign key constraints natively.
|
|
5244
5647
|
* When `true`, the generic layer skips application-level cascade/setNull
|
|
5245
5648
|
* on delete — the DB engine handles it (e.g. SQLite `ON DELETE CASCADE`).
|
|
@@ -5617,10 +6020,13 @@ var BaseDbAdapter = class {
|
|
|
5617
6020
|
* and every other error must throw — and a skipped row must never abort the
|
|
5618
6021
|
* surrounding transaction. Fail-closed by default: throws
|
|
5619
6022
|
* `DbError("ON_CONFLICT_NOT_SUPPORTED")`; also override
|
|
5620
|
-
* {@link supportsInsertIgnore}.
|
|
6023
|
+
* {@link supportsInsertIgnore}. `opts` (since 0.1.153) tells whether the
|
|
6024
|
+
* call joins a caller's transaction and whether to lock the stored rows the
|
|
6025
|
+
* batch collides with — see {@link TDbInsertIgnoreOptions}; an adapter
|
|
6026
|
+
* without row locks ignores it.
|
|
5621
6027
|
* @since 0.1.148
|
|
5622
6028
|
*/
|
|
5623
|
-
insertManyIgnore(_data) {
|
|
6029
|
+
insertManyIgnore(_data, _opts) {
|
|
5624
6030
|
return Promise.reject(new DbError("ON_CONFLICT_NOT_SUPPORTED", [{
|
|
5625
6031
|
path: "",
|
|
5626
6032
|
message: `The ${this.constructor.name} adapter does not support onConflict "ignore"`
|
|
@@ -6481,12 +6887,18 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
6481
6887
|
}
|
|
6482
6888
|
async _insertMany(payloads, opts) {
|
|
6483
6889
|
this._ensureBuilt();
|
|
6484
|
-
const { _depth, _action, maxDepth: userMax, guard, check, onConflict } = opts ?? {};
|
|
6890
|
+
const { _depth, _action, maxDepth: userMax, guard, check, onConflict, lockConflicts } = opts ?? {};
|
|
6485
6891
|
if (onConflict !== void 0 && onConflict !== "error" && onConflict !== "ignore") throw new DbError("INVALID_QUERY", [{
|
|
6486
6892
|
path: "onConflict",
|
|
6487
6893
|
message: `Unknown onConflict mode "${String(onConflict)}"`
|
|
6488
6894
|
}]);
|
|
6489
6895
|
const ignore = onConflict === "ignore";
|
|
6896
|
+
if (lockConflicts && !ignore) throw new DbError("INVALID_QUERY", [{
|
|
6897
|
+
path: "lockConflicts",
|
|
6898
|
+
message: "lockConflicts requires onConflict \"ignore\""
|
|
6899
|
+
}]);
|
|
6900
|
+
const ignoreOpts = ignore ? { inCallerTransaction: this.adapter.isInTransaction() } : void 0;
|
|
6901
|
+
if (ignoreOpts?.inCallerTransaction && lockConflicts) ignoreOpts.lockConflicts = true;
|
|
6490
6902
|
const maxDepth = userMax ?? 3;
|
|
6491
6903
|
const depth = _depth ?? 0;
|
|
6492
6904
|
const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
|
|
@@ -6521,7 +6933,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
6521
6933
|
if (canNest) await preValidateNestedFrom(host, originals);
|
|
6522
6934
|
let ignored;
|
|
6523
6935
|
if (ignore) {
|
|
6524
|
-
ignored = await this._insertIgnoring(prepared);
|
|
6936
|
+
ignored = await this._insertIgnoring(prepared, ignoreOpts);
|
|
6525
6937
|
const keep = ignored.inserted;
|
|
6526
6938
|
items = keep.map((i) => items[i]);
|
|
6527
6939
|
originals = canNest ? keep.map((i) => originals[i]) : originals;
|
|
@@ -7170,7 +7582,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7170
7582
|
* primary / unique key tuple (NULL components never collide), sends the rest
|
|
7171
7583
|
* to the adapter and assembles one slot per input row.
|
|
7172
7584
|
*/
|
|
7173
|
-
async _insertIgnoring(prepared) {
|
|
7585
|
+
async _insertIgnoring(prepared, opts) {
|
|
7174
7586
|
const keySets = this.uniqueKeySets;
|
|
7175
7587
|
const seen = keySets.map(() => /* @__PURE__ */ new Set());
|
|
7176
7588
|
const conflictAt = /* @__PURE__ */ new Set();
|
|
@@ -7187,7 +7599,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7187
7599
|
});
|
|
7188
7600
|
send.push(i);
|
|
7189
7601
|
}
|
|
7190
|
-
const slots = send.length ? await this.adapter.insertManyIgnore(send.map((i) => prepared[i])) : [];
|
|
7602
|
+
const slots = send.length ? await this.adapter.insertManyIgnore(send.map((i) => prepared[i]), opts) : [];
|
|
7191
7603
|
if (slots.length !== send.length) throw new DbError("INVALID_QUERY", [{
|
|
7192
7604
|
path: "",
|
|
7193
7605
|
message: "Adapter insertManyIgnore must return one slot per input row"
|
|
@@ -7448,6 +7860,231 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7448
7860
|
}
|
|
7449
7861
|
};
|
|
7450
7862
|
//#endregion
|
|
7863
|
+
//#region src/table/view-read-plan.ts
|
|
7864
|
+
/** Design types whose `=` compares one scalar to one scalar on every engine. */
|
|
7865
|
+
const SCALAR_TYPES = new Set([
|
|
7866
|
+
"string",
|
|
7867
|
+
"number",
|
|
7868
|
+
"boolean",
|
|
7869
|
+
"decimal",
|
|
7870
|
+
"bigint"
|
|
7871
|
+
]);
|
|
7872
|
+
/** The comparisons of a pure conjunction (nested `$and`s flattened), or `undefined` for any `$or` / `$not`. */
|
|
7873
|
+
function conjuncts(node, out = []) {
|
|
7874
|
+
if ("$and" in node) {
|
|
7875
|
+
for (const child of node.$and) if (!conjuncts(child, out)) return void 0;
|
|
7876
|
+
return out;
|
|
7877
|
+
}
|
|
7878
|
+
if ("$or" in node || "$not" in node) return void 0;
|
|
7879
|
+
out.push(node);
|
|
7880
|
+
return out;
|
|
7881
|
+
}
|
|
7882
|
+
/** Every scope a predicate references (its field refs' tables). */
|
|
7883
|
+
function predicateScopes(host, node, out) {
|
|
7884
|
+
if ("$and" in node || "$or" in node) {
|
|
7885
|
+
const children = node;
|
|
7886
|
+
for (const child of children.$and ?? children.$or ?? []) predicateScopes(host, child, out);
|
|
7887
|
+
return out;
|
|
7888
|
+
}
|
|
7889
|
+
if ("$not" in node) return predicateScopes(host, node.$not, out);
|
|
7890
|
+
const comp = node;
|
|
7891
|
+
out.add(host.resolveRefSource(comp.left).table);
|
|
7892
|
+
if (isFieldRef(comp.right)) out.add(host.resolveRefSource(comp.right).table);
|
|
7893
|
+
return out;
|
|
7894
|
+
}
|
|
7895
|
+
/** Whether a literal `value` compares as the scalar design type `designType` (no implicit casts). */
|
|
7896
|
+
function literalMatches(value, designType) {
|
|
7897
|
+
switch (typeof value) {
|
|
7898
|
+
case "string": return designType === "string";
|
|
7899
|
+
case "number": return designType === "number" || designType === "decimal";
|
|
7900
|
+
case "boolean": return designType === "boolean";
|
|
7901
|
+
default: return false;
|
|
7902
|
+
}
|
|
7903
|
+
}
|
|
7904
|
+
/**
|
|
7905
|
+
* Whether the left join `join` matches AT MOST ONE target row per input row,
|
|
7906
|
+
* so dropping it (when nothing reads it) leaves every result row in place:
|
|
7907
|
+
*
|
|
7908
|
+
* - a first-row join (its ON pins the target's primary key to one value);
|
|
7909
|
+
* - otherwise its condition is a conjunction whose `=` comparisons pin EVERY
|
|
7910
|
+
* column of one of the target's unique keys (primary key included) to a
|
|
7911
|
+
* value that does not depend on the target — another scope's field of the
|
|
7912
|
+
* same scalar design type that compares alike (collation, type overrides,
|
|
7913
|
+
* table charset), or a literal of that type.
|
|
7914
|
+
* Other conjuncts only narrow the match.
|
|
7915
|
+
*
|
|
7916
|
+
* Never for an inner join (it filters), a target that is a view (its
|
|
7917
|
+
* uniqueness is unknown), or a predicate with `$or` / `$not`. On a document
|
|
7918
|
+
* store every pinned key field must also be required: a unique index over an
|
|
7919
|
+
* optional field is partial there, and a missing value would match many.
|
|
7920
|
+
*/
|
|
7921
|
+
function atMostOneMatch(host, join) {
|
|
7922
|
+
if (join.kind !== "left") return false;
|
|
7923
|
+
const keySets = host.uniqueKeySets(join.targetType());
|
|
7924
|
+
if (!keySets) return false;
|
|
7925
|
+
if (join.first) return true;
|
|
7926
|
+
const comparisons = conjuncts(join.condition);
|
|
7927
|
+
if (!comparisons) return false;
|
|
7928
|
+
const pinned = /* @__PURE__ */ new Set();
|
|
7929
|
+
for (const node of comparisons) {
|
|
7930
|
+
const comp = node;
|
|
7931
|
+
if (comp.op !== "$eq" || comp.right === null || comp.right === void 0) continue;
|
|
7932
|
+
const left = host.resolveRefSource(comp.left);
|
|
7933
|
+
const rightRef = isFieldRef(comp.right) ? comp.right : void 0;
|
|
7934
|
+
const right = rightRef ? host.resolveRefSource(rightRef) : void 0;
|
|
7935
|
+
let target;
|
|
7936
|
+
let other;
|
|
7937
|
+
if (left.table === join.scope && right?.table !== join.scope) {
|
|
7938
|
+
target = {
|
|
7939
|
+
ref: comp.left,
|
|
7940
|
+
source: left.source
|
|
7941
|
+
};
|
|
7942
|
+
other = right && rightRef ? {
|
|
7943
|
+
ref: rightRef,
|
|
7944
|
+
source: right.source
|
|
7945
|
+
} : void 0;
|
|
7946
|
+
} else if (right && rightRef && right.table === join.scope && left.table !== join.scope) {
|
|
7947
|
+
target = {
|
|
7948
|
+
ref: rightRef,
|
|
7949
|
+
source: right.source
|
|
7950
|
+
};
|
|
7951
|
+
other = {
|
|
7952
|
+
ref: comp.left,
|
|
7953
|
+
source: left.source
|
|
7954
|
+
};
|
|
7955
|
+
} else continue;
|
|
7956
|
+
const { source } = target;
|
|
7957
|
+
if (source.jsonPath || source.flattened || !SCALAR_TYPES.has(source.designType)) continue;
|
|
7958
|
+
if (host.nested && source.optional) continue;
|
|
7959
|
+
const traits = host.fieldTraits(target.ref);
|
|
7960
|
+
if (traits.encrypted) continue;
|
|
7961
|
+
if (other) {
|
|
7962
|
+
const o = other.source;
|
|
7963
|
+
if (o.jsonPath || o.flattened || o.designType !== source.designType) continue;
|
|
7964
|
+
const otherTraits = host.fieldTraits(other.ref);
|
|
7965
|
+
if (otherTraits.encrypted || otherTraits.compare !== traits.compare) continue;
|
|
7966
|
+
} else if (!literalMatches(comp.right, source.designType)) continue;
|
|
7967
|
+
pinned.add(source.column);
|
|
7968
|
+
}
|
|
7969
|
+
return keySets.some((set) => set.length > 0 && set.every((column) => pinned.has(column)));
|
|
7970
|
+
}
|
|
7971
|
+
function prunerState(host) {
|
|
7972
|
+
const plan = host.viewPlan;
|
|
7973
|
+
if (plan.materialized || !plan.joins.some((j) => j.kind === "left")) return void 0;
|
|
7974
|
+
const columns = host.getViewColumnMappings();
|
|
7975
|
+
if (columns.some((c) => c.aggFn)) return void 0;
|
|
7976
|
+
const joins = plan.joins.map((join) => {
|
|
7977
|
+
const refs = predicateScopes(host, join.condition, /* @__PURE__ */ new Set());
|
|
7978
|
+
for (const { ref } of join.first?.order ?? []) refs.add(host.resolveRefSource(ref).table);
|
|
7979
|
+
refs.delete(join.scope);
|
|
7980
|
+
return {
|
|
7981
|
+
join,
|
|
7982
|
+
droppable: atMostOneMatch(host, join),
|
|
7983
|
+
refs
|
|
7984
|
+
};
|
|
7985
|
+
});
|
|
7986
|
+
if (!joins.some((j) => j.droppable)) return void 0;
|
|
7987
|
+
const base = new Set([plan.entryTable]);
|
|
7988
|
+
if (plan.filter) predicateScopes(host, plan.filter, base);
|
|
7989
|
+
for (const j of joins) if (!j.droppable) base.add(j.join.scope);
|
|
7990
|
+
const byPath = new Map(columns.map((c) => [c.viewPath, c]));
|
|
7991
|
+
const columnScopes = /* @__PURE__ */ new Map();
|
|
7992
|
+
const scopesOf = (c) => {
|
|
7993
|
+
let scopes = columnScopes.get(c);
|
|
7994
|
+
if (scopes) return scopes;
|
|
7995
|
+
scopes = /* @__PURE__ */ new Set();
|
|
7996
|
+
columnScopes.set(c, scopes);
|
|
7997
|
+
if (c.expr === void 0) scopes.add(c.sourceTable);
|
|
7998
|
+
else {
|
|
7999
|
+
const out = scopes;
|
|
8000
|
+
walkViewExpr(c.expr, (path) => {
|
|
8001
|
+
const leaf = byPath.get(path);
|
|
8002
|
+
if (!leaf) {
|
|
8003
|
+
for (const j of joins) out.add(j.join.scope);
|
|
8004
|
+
return;
|
|
8005
|
+
}
|
|
8006
|
+
for (const scope of scopesOf(leaf)) out.add(scope);
|
|
8007
|
+
});
|
|
8008
|
+
}
|
|
8009
|
+
return scopes;
|
|
8010
|
+
};
|
|
8011
|
+
for (const c of columns) scopesOf(c);
|
|
8012
|
+
return {
|
|
8013
|
+
base,
|
|
8014
|
+
joins,
|
|
8015
|
+
columnScopes,
|
|
8016
|
+
byColumn: new Map(columns.map((c) => [c.viewColumn, c])),
|
|
8017
|
+
columns
|
|
8018
|
+
};
|
|
8019
|
+
}
|
|
8020
|
+
/**
|
|
8021
|
+
* The read planner of one managed view: memoised per dropped-join set.
|
|
8022
|
+
* @since 0.1.153
|
|
8023
|
+
*/
|
|
8024
|
+
var ViewReadPlanner = class {
|
|
8025
|
+
_host;
|
|
8026
|
+
_state;
|
|
8027
|
+
_variants = /* @__PURE__ */ new Map();
|
|
8028
|
+
constructor(_host) {
|
|
8029
|
+
this._host = _host;
|
|
8030
|
+
}
|
|
8031
|
+
/** Whether any join of the view can ever be dropped. */
|
|
8032
|
+
get prunable() {
|
|
8033
|
+
return this._getState() !== void 0;
|
|
8034
|
+
}
|
|
8035
|
+
_getState() {
|
|
8036
|
+
if (this._state === void 0) try {
|
|
8037
|
+
this._state = prunerState(this._host) ?? null;
|
|
8038
|
+
} catch {
|
|
8039
|
+
this._state = null;
|
|
8040
|
+
}
|
|
8041
|
+
return this._state ?? void 0;
|
|
8042
|
+
}
|
|
8043
|
+
/** The mappings a needed (physical) name reads: exact, or a document path above / below it. */
|
|
8044
|
+
_matching(state, name) {
|
|
8045
|
+
const exact = state.byColumn.get(name);
|
|
8046
|
+
if (exact) return [exact];
|
|
8047
|
+
return state.columns.filter((c) => name.startsWith(`${c.viewColumn}.`) || c.viewColumn.startsWith(`${name}.`));
|
|
8048
|
+
}
|
|
8049
|
+
plan(needed) {
|
|
8050
|
+
const state = this._getState();
|
|
8051
|
+
if (!state) return void 0;
|
|
8052
|
+
const keep = new Set(state.base);
|
|
8053
|
+
const read = needed === void 0 ? state.columns : [...needed].flatMap((n) => this._matching(state, n));
|
|
8054
|
+
for (const c of read) for (const scope of state.columnScopes.get(c)) keep.add(scope);
|
|
8055
|
+
const dropped = [];
|
|
8056
|
+
for (let i = state.joins.length - 1; i >= 0; i--) {
|
|
8057
|
+
const { join, droppable, refs } = state.joins[i];
|
|
8058
|
+
if (droppable && !keep.has(join.scope)) dropped.push(join.scope);
|
|
8059
|
+
else for (const scope of refs) keep.add(scope);
|
|
8060
|
+
}
|
|
8061
|
+
if (dropped.length === 0) return void 0;
|
|
8062
|
+
dropped.reverse();
|
|
8063
|
+
const key = dropped.join("\0");
|
|
8064
|
+
let variant = this._variants.get(key);
|
|
8065
|
+
if (!variant) {
|
|
8066
|
+
const gone = new Set(dropped);
|
|
8067
|
+
const plan = this._host.viewPlan;
|
|
8068
|
+
const columns = [];
|
|
8069
|
+
const droppedColumns = [];
|
|
8070
|
+
for (const c of state.columns) if ([...state.columnScopes.get(c)].some((scope) => gone.has(scope))) droppedColumns.push(c.viewColumn);
|
|
8071
|
+
else columns.push(c);
|
|
8072
|
+
variant = {
|
|
8073
|
+
plan: {
|
|
8074
|
+
...plan,
|
|
8075
|
+
joins: plan.joins.filter((j) => !gone.has(j.scope))
|
|
8076
|
+
},
|
|
8077
|
+
columns,
|
|
8078
|
+
dropped,
|
|
8079
|
+
droppedColumns,
|
|
8080
|
+
key
|
|
8081
|
+
};
|
|
8082
|
+
this._variants.set(key, variant);
|
|
8083
|
+
}
|
|
8084
|
+
return variant;
|
|
8085
|
+
}
|
|
8086
|
+
};
|
|
8087
|
+
//#endregion
|
|
7451
8088
|
//#region src/table/db-view.ts
|
|
7452
8089
|
/**
|
|
7453
8090
|
* The single `@meta.id` field of a first-row join target (its source type,
|
|
@@ -7511,6 +8148,22 @@ function viewFieldSource(fieldName, fieldType, entryType) {
|
|
|
7511
8148
|
sourcePath: aggField && aggField !== "*" ? aggField : fieldName
|
|
7512
8149
|
};
|
|
7513
8150
|
}
|
|
8151
|
+
/**
|
|
8152
|
+
* Field annotations that change how a column compares (`=`): its collation
|
|
8153
|
+
* and engine-type overrides. Two columns of a join condition must agree on
|
|
8154
|
+
* them (and on {@link COMPARE_TABLE_KEYS}) for the condition to prove a
|
|
8155
|
+
* unique-key match (`AtscriptDbView.readPlan`).
|
|
8156
|
+
*/
|
|
8157
|
+
const COMPARE_FIELD_KEYS = [
|
|
8158
|
+
"db.column.collate",
|
|
8159
|
+
"db.mysql.type",
|
|
8160
|
+
"db.mysql.collate",
|
|
8161
|
+
"db.mysql.charset",
|
|
8162
|
+
"db.pg.type",
|
|
8163
|
+
"db.pg.collate"
|
|
8164
|
+
];
|
|
8165
|
+
/** Table annotations that set its columns' default charset / collation. */
|
|
8166
|
+
const COMPARE_TABLE_KEYS = ["db.mysql.charset", "db.mysql.collate"];
|
|
7514
8167
|
/** View types whose fields already carry their inherited seals. */
|
|
7515
8168
|
const sealedViews = /* @__PURE__ */ new WeakSet();
|
|
7516
8169
|
/**
|
|
@@ -7586,6 +8239,7 @@ function inheritViewFieldSeals(viewType) {
|
|
|
7586
8239
|
var AtscriptDbView = class extends AtscriptDbReadable {
|
|
7587
8240
|
_viewPlan;
|
|
7588
8241
|
_columnMappings;
|
|
8242
|
+
_readPlanner;
|
|
7589
8243
|
get isView() {
|
|
7590
8244
|
return true;
|
|
7591
8245
|
}
|
|
@@ -7737,6 +8391,53 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
7737
8391
|
return this._columnMappings;
|
|
7738
8392
|
}
|
|
7739
8393
|
/**
|
|
8394
|
+
* The read variant of this managed view for a query that reads only the
|
|
8395
|
+
* `needed` PHYSICAL view columns (`undefined`: every column) — the view
|
|
8396
|
+
* plan with every LEFT join removed that (a) matches at most one row per
|
|
8397
|
+
* input row and (b) feeds no needed column, no other kept join's ON clause
|
|
8398
|
+
* and no `@db.view.filter`. Dropping such a join leaves every row of the
|
|
8399
|
+
* view in place, so a query over the variant returns exactly what it
|
|
8400
|
+
* returns over the view. `undefined` when nothing can be dropped.
|
|
8401
|
+
*
|
|
8402
|
+
* At most one match is proven from the model: a first-row join, or an ON
|
|
8403
|
+
* clause whose `=` conjuncts pin every column of one of the target's unique
|
|
8404
|
+
* keys (primary key included) — see the views guide. Inner joins, joins to
|
|
8405
|
+
* a view, grouped (`@db.agg.*`), materialized and external views are never
|
|
8406
|
+
* pruned. A variant's {@link TViewReadPlan.columns} omit the columns of the
|
|
8407
|
+
* dropped joins, so a column reference the caller failed to declare fails
|
|
8408
|
+
* loudly instead of reading wrong data.
|
|
8409
|
+
*
|
|
8410
|
+
* Memoised per dropped-join set. Adapters use it to read through an
|
|
8411
|
+
* inline, pruned definition instead of the stored view.
|
|
8412
|
+
* @since 0.1.153
|
|
8413
|
+
*/
|
|
8414
|
+
readPlan(needed) {
|
|
8415
|
+
if (this.isExternal) return void 0;
|
|
8416
|
+
this._readPlanner ??= new ViewReadPlanner({
|
|
8417
|
+
viewPlan: this.viewPlan,
|
|
8418
|
+
nested: this._nested,
|
|
8419
|
+
getViewColumnMappings: () => this.getViewColumnMappings(),
|
|
8420
|
+
resolveRefSource: (ref) => this.resolveRefSource(ref),
|
|
8421
|
+
uniqueKeySets: (targetType) => {
|
|
8422
|
+
const source = viewSourceOf(targetType).type;
|
|
8423
|
+
if (isViewType(source) || !this._tableResolver) return void 0;
|
|
8424
|
+
const target = this._tableResolver(source);
|
|
8425
|
+
return target && !target.isView ? target.uniqueKeySets : void 0;
|
|
8426
|
+
},
|
|
8427
|
+
fieldTraits: (ref) => {
|
|
8428
|
+
const source = viewSourceOf(ref.type ? ref.type() : this.viewPlan.entryType()).type;
|
|
8429
|
+
const field = sourceFieldMetadata(source, ref.field);
|
|
8430
|
+
const table = source.metadata;
|
|
8431
|
+
const column = field;
|
|
8432
|
+
return {
|
|
8433
|
+
compare: JSON.stringify([...COMPARE_FIELD_KEYS.map((key) => column?.get(key) ?? null), ...COMPARE_TABLE_KEYS.map((key) => table.get(key) ?? null)]),
|
|
8434
|
+
encrypted: field?.has("db.encrypted") === true
|
|
8435
|
+
};
|
|
8436
|
+
}
|
|
8437
|
+
});
|
|
8438
|
+
return this._readPlanner.plan(needed);
|
|
8439
|
+
}
|
|
8440
|
+
/**
|
|
7740
8441
|
* Why the bound adapter cannot render this managed view: one message per
|
|
7741
8442
|
* computed column / first-row join whose feature its `viewCapabilities()`
|
|
7742
8443
|
* does not list. Empty for an external view or when every feature is
|