@atscript/db 0.1.152 → 0.1.154
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-CnyLjbQ8.mjs → column-diff-DAIAq6XR.mjs} +728 -29
- package/dist/{column-diff-CU7KHj9-.cjs → column-diff-qnzx1o3c.cjs} +727 -28
- 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,35 @@ 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 target = flat.metadata;
|
|
682
|
+
target.clear();
|
|
683
|
+
for (const entry of entries) {
|
|
684
|
+
if (entry.member && isInlinePrimitive(entry.type)) continue;
|
|
685
|
+
for (const [key, value] of entry.metadata) if (!entry.member || !key.startsWith("db.")) target.set(key, value);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
628
690
|
* Applies adapter-provided metadata overrides atomically.
|
|
629
691
|
* Processing order: injectFields → removePrimaryKeys → addPrimaryKeys → addUniqueFields.
|
|
630
692
|
*/
|
|
@@ -638,6 +700,7 @@ var TableMetadata = class {
|
|
|
638
700
|
for (const field of overrides.addPrimaryKeys) if (!this.primaryKeys.includes(field)) this.primaryKeys.push(field);
|
|
639
701
|
}
|
|
640
702
|
if (overrides.addUniqueFields) for (const field of overrides.addUniqueFields) this.uniqueProps.add(field);
|
|
703
|
+
if (overrides.addUniqueKeys) this.uniqueKeys.push(...overrides.addUniqueKeys.map((key) => [...key]));
|
|
641
704
|
}
|
|
642
705
|
/**
|
|
643
706
|
* Scans `@db.*` and `@meta.id` annotations on a field during flattening.
|
|
@@ -992,7 +1055,11 @@ var TableMetadata = class {
|
|
|
992
1055
|
_buildFieldDescriptors(adapter, rootType) {
|
|
993
1056
|
const descriptors = [];
|
|
994
1057
|
const skipFlattening = this.nestedObjects;
|
|
995
|
-
const indexedFields = new Set([
|
|
1058
|
+
const indexedFields = new Set([
|
|
1059
|
+
...this.primaryKeys,
|
|
1060
|
+
...this.uniqueProps,
|
|
1061
|
+
...this.uniqueKeys.flat()
|
|
1062
|
+
]);
|
|
996
1063
|
for (const index of this.indexes.values()) for (const f of index.fields) indexedFields.add(f.name);
|
|
997
1064
|
for (const [path, type] of this.flatMap.entries()) {
|
|
998
1065
|
if (!path) continue;
|
|
@@ -1187,6 +1254,8 @@ var TableMetadata = class {
|
|
|
1187
1254
|
* overrides (`addUniqueFields`); these aren't reflected in
|
|
1188
1255
|
* `this.indexes` but still legitimate addressing identifications.
|
|
1189
1256
|
* Deduped against any single-field unique index already captured.
|
|
1257
|
+
* - `this.uniqueKeys` — multi-field uniques contributed by adapter
|
|
1258
|
+
* overrides (`addUniqueKeys`).
|
|
1190
1259
|
*/
|
|
1191
1260
|
_buildIdentifications() {
|
|
1192
1261
|
const out = [];
|
|
@@ -1211,6 +1280,10 @@ var TableMetadata = class {
|
|
|
1211
1280
|
source: `unique:${field}`
|
|
1212
1281
|
});
|
|
1213
1282
|
}
|
|
1283
|
+
for (const key of this.uniqueKeys) out.push({
|
|
1284
|
+
fields: [...key],
|
|
1285
|
+
source: `unique:${key.join("+")}`
|
|
1286
|
+
});
|
|
1214
1287
|
this._identifications = out;
|
|
1215
1288
|
}
|
|
1216
1289
|
/** Legitimate row-identifier shapes — primary key first, then each unique index. */
|
|
@@ -1575,6 +1648,18 @@ function rewriteIntegerRegex(filter, meta) {
|
|
|
1575
1648
|
}
|
|
1576
1649
|
//#endregion
|
|
1577
1650
|
//#region src/query/aggregate-expr.ts
|
|
1651
|
+
/**
|
|
1652
|
+
* Output aliases of the computed `$select` entries of a grouped query —
|
|
1653
|
+
* aggregates, expressions and calendar buckets (`$as`, else uniqu's
|
|
1654
|
+
* `resolveAlias`).
|
|
1655
|
+
*/
|
|
1656
|
+
function computedAliases(select) {
|
|
1657
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
1658
|
+
if (!Array.isArray(select)) return aliases;
|
|
1659
|
+
for (const item of select) if (isAggregateExpr(item) || isBucketExpr(item)) aliases.add(resolveAlias(item));
|
|
1660
|
+
else if (isAggregateOfExpr(item) || isSelectArithExpr(item)) aliases.add(item.$as);
|
|
1661
|
+
return aliases;
|
|
1662
|
+
}
|
|
1578
1663
|
/** Most nodes a group-level expression may have once the aliases it names are inlined (SQL repeats them). */
|
|
1579
1664
|
const EXPANDED_MAX_NODES = 4 * ARITH_MAX_NODES;
|
|
1580
1665
|
/** Most expanded nodes ALL the expression entries of one query may have together. */
|
|
@@ -2059,7 +2144,7 @@ var FieldMappingStrategy = class {
|
|
|
2059
2144
|
*/
|
|
2060
2145
|
translateAggregateQuery(query, meta, buckets, exprs = [], rowOrder) {
|
|
2061
2146
|
const controls = query.controls;
|
|
2062
|
-
const aliases =
|
|
2147
|
+
const aliases = computedAliases(controls.$select);
|
|
2063
2148
|
const physicalBuckets = buckets.map((b) => ({
|
|
2064
2149
|
...b,
|
|
2065
2150
|
field: this.physicalPath(b.field, meta),
|
|
@@ -2076,6 +2161,7 @@ var FieldMappingStrategy = class {
|
|
|
2076
2161
|
$groupBy: this.renamesPaths(meta) ? controls.$groupBy.map((key) => aliases.has(key) ? key : this.physicalPath(key, meta)) : controls.$groupBy,
|
|
2077
2162
|
$select: select ? new UniquSelect(select, meta.allPhysicalFields, physicalBuckets, computed) : void 0,
|
|
2078
2163
|
$sort: controls.$sort && this.physicalSort(controls.$sort, meta, aliases),
|
|
2164
|
+
$nulls: controls.$nulls && this.physicalNulls(controls.$nulls, meta, aliases),
|
|
2079
2165
|
$having: controls.$having ? this.translateHaving(controls.$having, meta, aliases) : void 0
|
|
2080
2166
|
},
|
|
2081
2167
|
insights: query.insights
|
|
@@ -2099,13 +2185,6 @@ var FieldMappingStrategy = class {
|
|
|
2099
2185
|
...this.translateFilter(fields, meta)
|
|
2100
2186
|
} : out;
|
|
2101
2187
|
}
|
|
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
2188
|
/**
|
|
2110
2189
|
* The arithmetic and `$rowOrder` parts of a grouped query with PHYSICAL
|
|
2111
2190
|
* names: a row-level operand is a column; a group-level operand stays an
|
|
@@ -2144,7 +2223,8 @@ var FieldMappingStrategy = class {
|
|
|
2144
2223
|
if (rowOrder?.length) {
|
|
2145
2224
|
order = rowOrder.map((k) => ({
|
|
2146
2225
|
column: this.physicalPath(k.field, meta),
|
|
2147
|
-
desc: k.desc
|
|
2226
|
+
desc: k.desc,
|
|
2227
|
+
...k.nulls && { nulls: k.nulls }
|
|
2148
2228
|
}));
|
|
2149
2229
|
for (const pk of meta.primaryKeys) {
|
|
2150
2230
|
const column = this.physicalPath(pk, meta);
|
|
@@ -2194,9 +2274,20 @@ var FieldMappingStrategy = class {
|
|
|
2194
2274
|
}
|
|
2195
2275
|
/** `$sort` with physical keys; computed `aliases` (grouped queries) pass through. */
|
|
2196
2276
|
physicalSort(sort, meta, aliases) {
|
|
2197
|
-
|
|
2277
|
+
return this.physicalKeys(sort, meta, aliases);
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* `$nulls` with physical keys, like {@link physicalSort}. The core hands
|
|
2281
|
+
* over only the resolved entries (since 0.1.153).
|
|
2282
|
+
*/
|
|
2283
|
+
physicalNulls(nulls, meta, aliases) {
|
|
2284
|
+
return this.physicalKeys(nulls, meta, aliases);
|
|
2285
|
+
}
|
|
2286
|
+
/** `map` with its field-path keys made physical; computed `aliases` pass through. */
|
|
2287
|
+
physicalKeys(map, meta, aliases) {
|
|
2288
|
+
if (!this.renamesPaths(meta)) return map;
|
|
2198
2289
|
const translated = {};
|
|
2199
|
-
for (const [key,
|
|
2290
|
+
for (const [key, value] of Object.entries(map)) translated[aliases?.has(key) ? key : this.physicalPath(key, meta)] = value;
|
|
2200
2291
|
return translated;
|
|
2201
2292
|
}
|
|
2202
2293
|
/**
|
|
@@ -2472,7 +2563,8 @@ var DocumentFieldMapper = class extends FieldMappingStrategy {
|
|
|
2472
2563
|
...controls,
|
|
2473
2564
|
$with: void 0,
|
|
2474
2565
|
$select: select ? new UniquSelect(select, meta.allPhysicalFields) : void 0,
|
|
2475
|
-
$sort: controls?.$sort && this.physicalSort(controls.$sort, meta)
|
|
2566
|
+
$sort: controls?.$sort && this.physicalSort(controls.$sort, meta),
|
|
2567
|
+
$nulls: controls?.$nulls && this.physicalNulls(controls.$nulls, meta)
|
|
2476
2568
|
},
|
|
2477
2569
|
insights: query.insights
|
|
2478
2570
|
};
|
|
@@ -2663,6 +2755,7 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
2663
2755
|
}
|
|
2664
2756
|
result.$sort = translated;
|
|
2665
2757
|
}
|
|
2758
|
+
if (controls.$nulls) result.$nulls = this.physicalNulls(controls.$nulls, meta);
|
|
2666
2759
|
if (controls.$select) {
|
|
2667
2760
|
let translatedRaw;
|
|
2668
2761
|
if (Array.isArray(controls.$select)) {
|
|
@@ -3244,6 +3337,10 @@ function collectQueryPaths(query, aggregate) {
|
|
|
3244
3337
|
refs.groupBy = bucketAliases.size > 0 ? groupBy.filter((name) => !bucketAliases.has(name)) : groupBy;
|
|
3245
3338
|
for (const name of sortFieldNames(controls.$sort)) if (!aliases.has(name)) refs.sort.push(name);
|
|
3246
3339
|
if (refs.aggregateMode) refs.sort.push(...rowOrderKeys(controls.$rowOrder));
|
|
3340
|
+
const nulls = controls.$nulls;
|
|
3341
|
+
if (isPlainObject(nulls)) {
|
|
3342
|
+
for (const name of Object.keys(nulls)) if (!aliases.has(name) && !refs.sort.includes(name)) refs.sort.push(name);
|
|
3343
|
+
}
|
|
3247
3344
|
if (refs.aggregateMode) collectFilterKeys(controls.$having, (path) => refs.having.push(path), aliases, refs);
|
|
3248
3345
|
return refs;
|
|
3249
3346
|
}
|
|
@@ -3515,6 +3612,130 @@ function guardBucketUnits(adapter, buckets) {
|
|
|
3515
3612
|
}]);
|
|
3516
3613
|
}
|
|
3517
3614
|
//#endregion
|
|
3615
|
+
//#region src/query/nulls.ts
|
|
3616
|
+
/** Annotation key of a field's default NULL placement (`@db.sort.nulls`). */
|
|
3617
|
+
const SORT_NULLS_ANNOTATION = "db.sort.nulls";
|
|
3618
|
+
/**
|
|
3619
|
+
* Per-table NULL-placement resolution (since 0.1.153): built once per
|
|
3620
|
+
* readable, memoizing the per-path answers every sorted read asks for.
|
|
3621
|
+
*/
|
|
3622
|
+
var NullsSource = class {
|
|
3623
|
+
_init;
|
|
3624
|
+
_nullable = /* @__PURE__ */ new Map();
|
|
3625
|
+
_hasDefaults;
|
|
3626
|
+
constructor(_init) {
|
|
3627
|
+
this._init = _init;
|
|
3628
|
+
}
|
|
3629
|
+
/** Whether any field carries a `@db.sort.nulls` default. */
|
|
3630
|
+
hasDefaults() {
|
|
3631
|
+
return this._hasDefaults ??= [...this._init.meta.flatMap.keys()].some((path) => this.defaultOf(path) !== void 0);
|
|
3632
|
+
}
|
|
3633
|
+
/** The `@db.sort.nulls` default of the LOGICAL field `path`, if any. */
|
|
3634
|
+
defaultOf(path) {
|
|
3635
|
+
const value = this._init.meta.flatMap.get(path)?.metadata.get(SORT_NULLS_ANNOTATION);
|
|
3636
|
+
return isNullsPlacement(value) ? value : void 0;
|
|
3637
|
+
}
|
|
3638
|
+
/**
|
|
3639
|
+
* Whether ordering by the LOGICAL field `path` can meet NULL (or a missing
|
|
3640
|
+
* value): the field or one of its parent objects is optional, the field is
|
|
3641
|
+
* a derived column, it is not a known field, or the source is a view. A
|
|
3642
|
+
* `$nulls` entry on any other field changes nothing and is dropped, so the
|
|
3643
|
+
* plain `ORDER BY` keeps using the index.
|
|
3644
|
+
*/
|
|
3645
|
+
nullable(path) {
|
|
3646
|
+
let answer = this._nullable.get(path);
|
|
3647
|
+
if (answer === void 0) {
|
|
3648
|
+
answer = this._init.isView || this._nullableField(path);
|
|
3649
|
+
this._nullable.set(path, answer);
|
|
3650
|
+
}
|
|
3651
|
+
return answer;
|
|
3652
|
+
}
|
|
3653
|
+
_nullableField(path) {
|
|
3654
|
+
const { meta } = this._init;
|
|
3655
|
+
const fd = meta.descriptorByPath.get(path);
|
|
3656
|
+
if (!fd || fd.derived) return true;
|
|
3657
|
+
for (let p = path;;) {
|
|
3658
|
+
if (meta.flatMap.get(p)?.optional === true) return true;
|
|
3659
|
+
const dot = p.lastIndexOf(".");
|
|
3660
|
+
if (dot === -1) return false;
|
|
3661
|
+
p = p.slice(0, dot);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
/**
|
|
3665
|
+
* The effective `$nulls` of the `$sort` keys of one read: the request's
|
|
3666
|
+
* entry, else the field's `@db.sort.nulls` default; entries on fields that
|
|
3667
|
+
* cannot be NULL ({@link nullable}) are dropped. `aliases` are the computed
|
|
3668
|
+
* output names of a grouped read — they take a request entry only and are
|
|
3669
|
+
* never dropped. Entries for keys that are not ordered by are ignored. On
|
|
3670
|
+
* an adapter without support a request entry is refused and defaults are
|
|
3671
|
+
* dropped.
|
|
3672
|
+
*
|
|
3673
|
+
* @returns LOGICAL keys (or aliases) → placement; `undefined` when none.
|
|
3674
|
+
* @throws DbError `INVALID_QUERY` on a malformed `$nulls`, or a request
|
|
3675
|
+
* entry the adapter cannot honour.
|
|
3676
|
+
*/
|
|
3677
|
+
resolveSort(sort, rawNulls, aliases) {
|
|
3678
|
+
const requested = checkNullsControl(rawNulls);
|
|
3679
|
+
if (!isPlainObject(sort)) return void 0;
|
|
3680
|
+
let nulls;
|
|
3681
|
+
for (const key of Object.keys(sort)) {
|
|
3682
|
+
const asked = requested && Object.hasOwn(requested, key) ? requested[key] : void 0;
|
|
3683
|
+
const placement = aliases?.has(key) ? asked : this._placement(key, asked);
|
|
3684
|
+
if (placement && this._allowed(asked)) (nulls ??= {})[key] = placement;
|
|
3685
|
+
}
|
|
3686
|
+
return nulls;
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* `$rowOrder` keys (as uniqu resolved them, `nulls` from the request) under
|
|
3690
|
+
* the rules of {@link resolveSort}: defaults filled in, placements on
|
|
3691
|
+
* fields that cannot be NULL (or on an unsupporting adapter) dropped.
|
|
3692
|
+
*/
|
|
3693
|
+
resolveRowOrder(rowOrder) {
|
|
3694
|
+
return rowOrder?.map((key) => {
|
|
3695
|
+
const placement = this._placement(key.field, key.nulls);
|
|
3696
|
+
const nulls = placement && this._allowed(key.nulls) ? placement : void 0;
|
|
3697
|
+
if (nulls === key.nulls) return key;
|
|
3698
|
+
return nulls ? {
|
|
3699
|
+
...key,
|
|
3700
|
+
nulls
|
|
3701
|
+
} : {
|
|
3702
|
+
field: key.field,
|
|
3703
|
+
desc: key.desc
|
|
3704
|
+
};
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
/** The placement of field `path` — the request's `asked`, else its default — or `undefined` when it cannot be NULL. */
|
|
3708
|
+
_placement(path, asked) {
|
|
3709
|
+
return this.nullable(path) ? asked ?? this.defaultOf(path) : void 0;
|
|
3710
|
+
}
|
|
3711
|
+
/** Whether a placement may be forwarded; a requested one on an unsupporting adapter is refused. */
|
|
3712
|
+
_allowed(asked) {
|
|
3713
|
+
if (this._init.supported) return true;
|
|
3714
|
+
if (asked) throw new DbError("INVALID_QUERY", [{
|
|
3715
|
+
path: "$nulls",
|
|
3716
|
+
message: `NULL placement ($nulls / :first / :last) is not supported by the adapter of "${this._init.tableName}"`
|
|
3717
|
+
}]);
|
|
3718
|
+
return false;
|
|
3719
|
+
}
|
|
3720
|
+
};
|
|
3721
|
+
/**
|
|
3722
|
+
* The raw `$nulls` control as a checked map, or `undefined` when absent.
|
|
3723
|
+
* @throws DbError `INVALID_QUERY` (`path: "$nulls"`) on a non-object or a
|
|
3724
|
+
* value other than `'first'` / `'last'`.
|
|
3725
|
+
*/
|
|
3726
|
+
function checkNullsControl(raw) {
|
|
3727
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
3728
|
+
if (!isPlainObject(raw)) throw new DbError("INVALID_QUERY", [{
|
|
3729
|
+
path: "$nulls",
|
|
3730
|
+
message: "$nulls must be an object of field → 'first' | 'last'"
|
|
3731
|
+
}]);
|
|
3732
|
+
for (const [field, value] of Object.entries(raw)) if (!isNullsPlacement(value)) throw new DbError("INVALID_QUERY", [{
|
|
3733
|
+
path: "$nulls",
|
|
3734
|
+
message: `$nulls "${field}" must be 'first' or 'last'`
|
|
3735
|
+
}]);
|
|
3736
|
+
return raw;
|
|
3737
|
+
}
|
|
3738
|
+
//#endregion
|
|
3518
3739
|
//#region src/shared/index-messages.ts
|
|
3519
3740
|
/**
|
|
3520
3741
|
* Wording of the "index not found" errors — one source for the core, every
|
|
@@ -3598,6 +3819,26 @@ function resolveDefaultFromMetadata(metadata) {
|
|
|
3598
3819
|
};
|
|
3599
3820
|
}
|
|
3600
3821
|
/**
|
|
3822
|
+
* `query` with `$groupBy` defaulted to `[]` — an aggregate without one is the
|
|
3823
|
+
* ungrouped query (one group), as `$groupBy: []` documents it.
|
|
3824
|
+
* @throws DbError `INVALID_QUERY` when `$groupBy` is present but not an array.
|
|
3825
|
+
*/
|
|
3826
|
+
function withGroupBy(query) {
|
|
3827
|
+
const controls = query.controls ?? {};
|
|
3828
|
+
if (Array.isArray(controls.$groupBy)) return query;
|
|
3829
|
+
if (controls.$groupBy !== void 0 && controls.$groupBy !== null) throw new DbError("INVALID_QUERY", [{
|
|
3830
|
+
path: "$groupBy",
|
|
3831
|
+
message: "$groupBy must be an array of field names"
|
|
3832
|
+
}]);
|
|
3833
|
+
return {
|
|
3834
|
+
...query,
|
|
3835
|
+
controls: {
|
|
3836
|
+
...controls,
|
|
3837
|
+
$groupBy: []
|
|
3838
|
+
}
|
|
3839
|
+
};
|
|
3840
|
+
}
|
|
3841
|
+
/**
|
|
3601
3842
|
* Checks whether an id value is type-compatible with a field's design type.
|
|
3602
3843
|
* Used by `findById` to skip primary-key lookup when the id clearly can't match,
|
|
3603
3844
|
* falling through to unique-property search instead.
|
|
@@ -3739,7 +3980,7 @@ var AtscriptDbReadable = class {
|
|
|
3739
3980
|
_translateForAdapter(query) {
|
|
3740
3981
|
this._ensureBuilt();
|
|
3741
3982
|
this._guardQuery(query);
|
|
3742
|
-
return this._fieldMapper.translateQuery(query, this._meta);
|
|
3983
|
+
return this._fieldMapper.translateQuery(this._withStableSort(query), this._meta);
|
|
3743
3984
|
}
|
|
3744
3985
|
/**
|
|
3745
3986
|
* Physical rows of THIS table → logical rows (field mapping, value
|
|
@@ -3839,7 +4080,8 @@ var AtscriptDbReadable = class {
|
|
|
3839
4080
|
}
|
|
3840
4081
|
/**
|
|
3841
4082
|
* 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
|
|
4083
|
+
* followed by every unique index and every adapter-contributed unique field / key. Used by
|
|
4084
|
+
* conflict-ignoring inserts and view read pruning.
|
|
3843
4085
|
* @since 0.1.148
|
|
3844
4086
|
*/
|
|
3845
4087
|
get uniqueKeySets() {
|
|
@@ -3856,6 +4098,7 @@ var AtscriptDbReadable = class {
|
|
|
3856
4098
|
const column = this._meta.physicalPath(prop);
|
|
3857
4099
|
if (!sets.some((s) => s.length === 1 && s[0] === column)) sets.push([column]);
|
|
3858
4100
|
}
|
|
4101
|
+
for (const key of this._meta.uniqueKeys) sets.push(key.map((f) => this._meta.physicalPath(f)));
|
|
3859
4102
|
for (const set of sets) Object.freeze(set);
|
|
3860
4103
|
return Object.freeze(sets);
|
|
3861
4104
|
}
|
|
@@ -4100,6 +4343,7 @@ var AtscriptDbReadable = class {
|
|
|
4100
4343
|
widened = widen.added;
|
|
4101
4344
|
}
|
|
4102
4345
|
}
|
|
4346
|
+
readQuery = this._withStableSort(readQuery);
|
|
4103
4347
|
return {
|
|
4104
4348
|
translated: this._fieldMapper.translateQuery(readQuery, this._meta),
|
|
4105
4349
|
controls: readQuery.controls,
|
|
@@ -4121,6 +4365,99 @@ var AtscriptDbReadable = class {
|
|
|
4121
4365
|
}
|
|
4122
4366
|
return rows;
|
|
4123
4367
|
}
|
|
4368
|
+
/**
|
|
4369
|
+
* `query` with {@link _withPkTieBreak} applied and its `$nulls` resolved
|
|
4370
|
+
* ({@link NullsSource.resolveSort}) — a copy when either changes.
|
|
4371
|
+
*/
|
|
4372
|
+
_withStableSort(query) {
|
|
4373
|
+
const controls = query.controls;
|
|
4374
|
+
const raw = controls?.$nulls;
|
|
4375
|
+
const source = this._nulls;
|
|
4376
|
+
const nulls = raw !== void 0 || source.hasDefaults() ? source.resolveSort(controls?.$sort, raw) : void 0;
|
|
4377
|
+
const sort = this._withPkTieBreak(controls);
|
|
4378
|
+
if (!sort && nulls === raw) return query;
|
|
4379
|
+
const next = { ...controls };
|
|
4380
|
+
if (sort) next.$sort = sort;
|
|
4381
|
+
if (nulls) next.$nulls = nulls;
|
|
4382
|
+
else delete next.$nulls;
|
|
4383
|
+
return {
|
|
4384
|
+
...query,
|
|
4385
|
+
controls: next
|
|
4386
|
+
};
|
|
4387
|
+
}
|
|
4388
|
+
_nullsCache;
|
|
4389
|
+
/** NULL-placement resolution for this table (`$nulls`, `@db.sort.nulls`; since 0.1.153). */
|
|
4390
|
+
get _nulls() {
|
|
4391
|
+
return this._nullsCache ??= new NullsSource({
|
|
4392
|
+
meta: this._meta,
|
|
4393
|
+
tableName: this.tableName,
|
|
4394
|
+
isView: this.isView,
|
|
4395
|
+
supported: this.adapter.supportsNullsPlacement()
|
|
4396
|
+
});
|
|
4397
|
+
}
|
|
4398
|
+
/**
|
|
4399
|
+
* `$sort` with the primary-key tie-breaker appended (since 0.1.153), or
|
|
4400
|
+
* `undefined` when the read keeps its `$sort` as written. Rows that tie on
|
|
4401
|
+
* every `$sort` key otherwise come back in an engine-chosen order that can
|
|
4402
|
+
* differ between two reads, so offset pages overlap or skip rows. The
|
|
4403
|
+
* primary-key fields the `$sort` leaves out are appended in declaration
|
|
4404
|
+
* order, in the direction of the LAST `$sort` key (a backward index scan
|
|
4405
|
+
* serves `amount DESC, id DESC`).
|
|
4406
|
+
*
|
|
4407
|
+
* Unchanged when there is no `$sort` (no order is imposed), when the
|
|
4408
|
+
* `$sort` already names every field of the primary key or of a unique key
|
|
4409
|
+
* over non-nullable fields ({@link _totalOrderKeys}), when the table has no
|
|
4410
|
+
* primary key (a view without `@meta.id`), and on grouped / aggregate /
|
|
4411
|
+
* `$count` reads. Search, vector and geo relevance is never touched: the
|
|
4412
|
+
* key is appended only to an explicit `$sort`. Every read path applies it
|
|
4413
|
+
* ({@link _translateRead}, and {@link _translateForAdapter} for a native
|
|
4414
|
+
* `$with` loader), so nested relation rows get it too.
|
|
4415
|
+
*/
|
|
4416
|
+
_withPkTieBreak(controls) {
|
|
4417
|
+
const sort = controls?.$sort;
|
|
4418
|
+
if (!sort || typeof sort !== "object") return void 0;
|
|
4419
|
+
const keys = Object.keys(sort);
|
|
4420
|
+
if (keys.length === 0 || controls.$count) return void 0;
|
|
4421
|
+
if (Array.isArray(controls.$groupBy) && controls.$groupBy.length > 0) return void 0;
|
|
4422
|
+
const select = controls.$select;
|
|
4423
|
+
if (Array.isArray(select) && select.some((item) => isAggregateExpr(item) || isAggregateOfExpr(item))) return;
|
|
4424
|
+
const pk = this._meta.primaryKeys;
|
|
4425
|
+
if (pk.length === 0) return void 0;
|
|
4426
|
+
const sorted = new Set(keys.map((key) => this._meta.physicalPath(key)));
|
|
4427
|
+
if (this._totalOrderKeys().some((set) => set.every((column) => sorted.has(column)))) return;
|
|
4428
|
+
const dir = sort[keys[keys.length - 1]];
|
|
4429
|
+
const next = { ...sort };
|
|
4430
|
+
for (const field of pk) if (!sorted.has(this._meta.physicalPath(field))) next[field] = dir;
|
|
4431
|
+
return next;
|
|
4432
|
+
}
|
|
4433
|
+
_totalOrderKeysCache;
|
|
4434
|
+
/**
|
|
4435
|
+
* Physical column sets that order rows totally: the primary key, then
|
|
4436
|
+
* every unique index (and adapter-contributed unique field) whose fields
|
|
4437
|
+
* are all non-nullable — a unique key over a nullable column admits
|
|
4438
|
+
* several NULLs, so it does not.
|
|
4439
|
+
*/
|
|
4440
|
+
_totalOrderKeys() {
|
|
4441
|
+
if (this._totalOrderKeysCache) return this._totalOrderKeysCache;
|
|
4442
|
+
const meta = this._meta;
|
|
4443
|
+
const nullable = (logical) => {
|
|
4444
|
+
for (let path = logical;;) {
|
|
4445
|
+
if (meta.flatMap.get(path)?.optional === true) return true;
|
|
4446
|
+
const dot = path.lastIndexOf(".");
|
|
4447
|
+
if (dot === -1) return false;
|
|
4448
|
+
path = path.slice(0, dot);
|
|
4449
|
+
}
|
|
4450
|
+
};
|
|
4451
|
+
const sets = [];
|
|
4452
|
+
if (meta.primaryKeys.length > 0) sets.push(meta.primaryKeys.map((f) => meta.physicalPath(f)));
|
|
4453
|
+
for (const index of meta.indexes.values()) {
|
|
4454
|
+
if (index.type !== "unique") continue;
|
|
4455
|
+
if (index.fields.every((f) => f.optional !== true && !nullable(meta.physicalToPath.get(f.name) ?? f.name))) sets.push(index.fields.map((f) => f.name));
|
|
4456
|
+
}
|
|
4457
|
+
for (const prop of meta.uniqueProps) if (!nullable(prop)) sets.push([meta.physicalPath(prop)]);
|
|
4458
|
+
this._totalOrderKeysCache = sets;
|
|
4459
|
+
return sets;
|
|
4460
|
+
}
|
|
4124
4461
|
/** `$select` plus the join keys of `withRelations` it leaves out — `undefined` when none is missing. */
|
|
4125
4462
|
_widenSelectForWith(select, withRelations) {
|
|
4126
4463
|
const keys = /* @__PURE__ */ new Set();
|
|
@@ -4331,6 +4668,7 @@ var AtscriptDbReadable = class {
|
|
|
4331
4668
|
*/
|
|
4332
4669
|
async aggregate(query) {
|
|
4333
4670
|
this._ensureBuilt();
|
|
4671
|
+
query = withGroupBy(query);
|
|
4334
4672
|
const { $groupBy, $select } = query.controls;
|
|
4335
4673
|
const computed = resolveComputedSelect(query.controls, this._meta, true);
|
|
4336
4674
|
const { buckets } = computed;
|
|
@@ -4389,11 +4727,36 @@ var AtscriptDbReadable = class {
|
|
|
4389
4727
|
if (!searchTerm.trim()) return query.controls.$count ? [{ count: 0 }] : [];
|
|
4390
4728
|
}
|
|
4391
4729
|
guardAggregate(this._meta, this.adapter, query, computed);
|
|
4392
|
-
const
|
|
4730
|
+
const ordered = this._aggregateNulls(query, computed.rowOrder);
|
|
4731
|
+
const dbQuery = this._fieldMapper.translateAggregateQuery(ordered.query, this._meta, buckets, computed.exprs, ordered.rowOrder);
|
|
4393
4732
|
const results = await this.adapter.aggregate(dbQuery);
|
|
4394
4733
|
return this._fromRead(results, query.controls, this._aliasFields($select));
|
|
4395
4734
|
}
|
|
4396
4735
|
/**
|
|
4736
|
+
* A grouped read's `$nulls` resolved for its `$sort` keys (computed aliases
|
|
4737
|
+
* take a request entry only) and its `$rowOrder` keys — see
|
|
4738
|
+
* {@link NullsSource}.
|
|
4739
|
+
*/
|
|
4740
|
+
_aggregateNulls(query, rowOrder) {
|
|
4741
|
+
const controls = query.controls;
|
|
4742
|
+
const source = this._nulls;
|
|
4743
|
+
if (controls.$nulls === void 0 && !source.hasDefaults()) return {
|
|
4744
|
+
query,
|
|
4745
|
+
rowOrder
|
|
4746
|
+
};
|
|
4747
|
+
const nulls = source.resolveSort(controls.$sort, controls.$nulls, computedAliases(controls.$select));
|
|
4748
|
+
const next = { ...controls };
|
|
4749
|
+
if (nulls) next.$nulls = nulls;
|
|
4750
|
+
else delete next.$nulls;
|
|
4751
|
+
return {
|
|
4752
|
+
query: {
|
|
4753
|
+
...query,
|
|
4754
|
+
controls: next
|
|
4755
|
+
},
|
|
4756
|
+
rowOrder: source.resolveRowOrder(rowOrder)
|
|
4757
|
+
};
|
|
4758
|
+
}
|
|
4759
|
+
/**
|
|
4397
4760
|
* The computed aliases of one source field that is a boolean or a decimal —
|
|
4398
4761
|
* `min` / `max` / `first` / `last` — with that field's descriptor: the
|
|
4399
4762
|
* aggregate row's value is coerced like the column's own on read.
|
|
@@ -4427,6 +4790,14 @@ var AtscriptDbReadable = class {
|
|
|
4427
4790
|
aggregateFns() {
|
|
4428
4791
|
return this.adapter.aggregateFns();
|
|
4429
4792
|
}
|
|
4793
|
+
/**
|
|
4794
|
+
* Whether the adapter honours a requested NULL placement (`$nulls`,
|
|
4795
|
+
* `@db.sort.nulls`) — proxies adapter capability.
|
|
4796
|
+
* @since 0.1.153
|
|
4797
|
+
*/
|
|
4798
|
+
supportsNullsPlacement() {
|
|
4799
|
+
return this.adapter.supportsNullsPlacement();
|
|
4800
|
+
}
|
|
4430
4801
|
/** Whether the adapter can sort by a given field (proxies adapter capability). */
|
|
4431
4802
|
canSortField(fd) {
|
|
4432
4803
|
return this.adapter.canSortField(fd);
|
|
@@ -4831,7 +5202,7 @@ var AtscriptDbReadable = class {
|
|
|
4831
5202
|
* Public entry point for relation loading. Used by adapters for nested $with delegation.
|
|
4832
5203
|
*/
|
|
4833
5204
|
async loadRelations(rows, withRelations) {
|
|
4834
|
-
const { loadRelationsImpl } = await import("./relation-loader-
|
|
5205
|
+
const { loadRelationsImpl } = await import("./relation-loader-CNBMPfRr.mjs").then((n) => n.n);
|
|
4835
5206
|
return loadRelationsImpl(rows, withRelations, this);
|
|
4836
5207
|
}
|
|
4837
5208
|
/**
|
|
@@ -5217,6 +5588,24 @@ var BaseDbAdapter = class {
|
|
|
5217
5588
|
return false;
|
|
5218
5589
|
}
|
|
5219
5590
|
/**
|
|
5591
|
+
* Whether this adapter honours a requested NULL placement: `controls.$nulls`
|
|
5592
|
+
* (PHYSICAL `$sort` keys → `'first'` / `'last'`, or computed aliases on a
|
|
5593
|
+
* grouped read) and `TRowOrderKey.nulls` on the `first` / `last` row
|
|
5594
|
+
* order. NULL (and, on document stores, a missing value) goes before or
|
|
5595
|
+
* after every non-null value whatever the sort direction. The core only
|
|
5596
|
+
* forwards entries for keys that can be NULL; a key without an entry keeps
|
|
5597
|
+
* the adapter's native order (`$sort`) or "NULL is the smallest value"
|
|
5598
|
+
* (grouped / row order). Default `false` (fail-closed): the core refuses a
|
|
5599
|
+
* request naming a placement with `INVALID_QUERY` and drops
|
|
5600
|
+
* `@db.sort.nulls` defaults. moost-db advertises it as
|
|
5601
|
+
* `/meta.nullsPlacement`.
|
|
5602
|
+
*
|
|
5603
|
+
* @since 0.1.153
|
|
5604
|
+
*/
|
|
5605
|
+
supportsNullsPlacement() {
|
|
5606
|
+
return false;
|
|
5607
|
+
}
|
|
5608
|
+
/**
|
|
5220
5609
|
* Revision of how this adapter renders a managed view (its SQL / pipeline)
|
|
5221
5610
|
* from an unchanged view definition. Stored in each managed view's sync
|
|
5222
5611
|
* snapshot when defined, so bumping it recreates every managed view of the
|
|
@@ -5240,6 +5629,18 @@ var BaseDbAdapter = class {
|
|
|
5240
5629
|
return NO_VIEW_CAPABILITIES;
|
|
5241
5630
|
}
|
|
5242
5631
|
/**
|
|
5632
|
+
* Whether reads of a managed view drop the LEFT joins the query does not
|
|
5633
|
+
* need (`AtscriptDbView.readPlan`) — reading through an inline, pruned
|
|
5634
|
+
* definition instead of the stored view. Only adapters that implement it
|
|
5635
|
+
* honour it; each sets its own default from its constructor options (the
|
|
5636
|
+
* bundled MySQL, SQLite and MongoDB adapters: on; PostgreSQL removes such
|
|
5637
|
+
* joins itself). Set it to `false` on one view's adapter
|
|
5638
|
+
* (`space.getAdapter(View)`) to read that view by name again.
|
|
5639
|
+
*
|
|
5640
|
+
* @since 0.1.153
|
|
5641
|
+
*/
|
|
5642
|
+
viewJoinPruning = false;
|
|
5643
|
+
/**
|
|
5243
5644
|
* Whether this adapter enforces foreign key constraints natively.
|
|
5244
5645
|
* When `true`, the generic layer skips application-level cascade/setNull
|
|
5245
5646
|
* on delete — the DB engine handles it (e.g. SQLite `ON DELETE CASCADE`).
|
|
@@ -5617,10 +6018,13 @@ var BaseDbAdapter = class {
|
|
|
5617
6018
|
* and every other error must throw — and a skipped row must never abort the
|
|
5618
6019
|
* surrounding transaction. Fail-closed by default: throws
|
|
5619
6020
|
* `DbError("ON_CONFLICT_NOT_SUPPORTED")`; also override
|
|
5620
|
-
* {@link supportsInsertIgnore}.
|
|
6021
|
+
* {@link supportsInsertIgnore}. `opts` (since 0.1.153) tells whether the
|
|
6022
|
+
* call joins a caller's transaction and whether to lock the stored rows the
|
|
6023
|
+
* batch collides with — see {@link TDbInsertIgnoreOptions}; an adapter
|
|
6024
|
+
* without row locks ignores it.
|
|
5621
6025
|
* @since 0.1.148
|
|
5622
6026
|
*/
|
|
5623
|
-
insertManyIgnore(_data) {
|
|
6027
|
+
insertManyIgnore(_data, _opts) {
|
|
5624
6028
|
return Promise.reject(new DbError("ON_CONFLICT_NOT_SUPPORTED", [{
|
|
5625
6029
|
path: "",
|
|
5626
6030
|
message: `The ${this.constructor.name} adapter does not support onConflict "ignore"`
|
|
@@ -6481,12 +6885,18 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
6481
6885
|
}
|
|
6482
6886
|
async _insertMany(payloads, opts) {
|
|
6483
6887
|
this._ensureBuilt();
|
|
6484
|
-
const { _depth, _action, maxDepth: userMax, guard, check, onConflict } = opts ?? {};
|
|
6888
|
+
const { _depth, _action, maxDepth: userMax, guard, check, onConflict, lockConflicts } = opts ?? {};
|
|
6485
6889
|
if (onConflict !== void 0 && onConflict !== "error" && onConflict !== "ignore") throw new DbError("INVALID_QUERY", [{
|
|
6486
6890
|
path: "onConflict",
|
|
6487
6891
|
message: `Unknown onConflict mode "${String(onConflict)}"`
|
|
6488
6892
|
}]);
|
|
6489
6893
|
const ignore = onConflict === "ignore";
|
|
6894
|
+
if (lockConflicts && !ignore) throw new DbError("INVALID_QUERY", [{
|
|
6895
|
+
path: "lockConflicts",
|
|
6896
|
+
message: "lockConflicts requires onConflict \"ignore\""
|
|
6897
|
+
}]);
|
|
6898
|
+
const ignoreOpts = ignore ? { inCallerTransaction: this.adapter.isInTransaction() } : void 0;
|
|
6899
|
+
if (ignoreOpts?.inCallerTransaction && lockConflicts) ignoreOpts.lockConflicts = true;
|
|
6490
6900
|
const maxDepth = userMax ?? 3;
|
|
6491
6901
|
const depth = _depth ?? 0;
|
|
6492
6902
|
const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
|
|
@@ -6521,7 +6931,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
6521
6931
|
if (canNest) await preValidateNestedFrom(host, originals);
|
|
6522
6932
|
let ignored;
|
|
6523
6933
|
if (ignore) {
|
|
6524
|
-
ignored = await this._insertIgnoring(prepared);
|
|
6934
|
+
ignored = await this._insertIgnoring(prepared, ignoreOpts);
|
|
6525
6935
|
const keep = ignored.inserted;
|
|
6526
6936
|
items = keep.map((i) => items[i]);
|
|
6527
6937
|
originals = canNest ? keep.map((i) => originals[i]) : originals;
|
|
@@ -7170,7 +7580,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7170
7580
|
* primary / unique key tuple (NULL components never collide), sends the rest
|
|
7171
7581
|
* to the adapter and assembles one slot per input row.
|
|
7172
7582
|
*/
|
|
7173
|
-
async _insertIgnoring(prepared) {
|
|
7583
|
+
async _insertIgnoring(prepared, opts) {
|
|
7174
7584
|
const keySets = this.uniqueKeySets;
|
|
7175
7585
|
const seen = keySets.map(() => /* @__PURE__ */ new Set());
|
|
7176
7586
|
const conflictAt = /* @__PURE__ */ new Set();
|
|
@@ -7187,7 +7597,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7187
7597
|
});
|
|
7188
7598
|
send.push(i);
|
|
7189
7599
|
}
|
|
7190
|
-
const slots = send.length ? await this.adapter.insertManyIgnore(send.map((i) => prepared[i])) : [];
|
|
7600
|
+
const slots = send.length ? await this.adapter.insertManyIgnore(send.map((i) => prepared[i]), opts) : [];
|
|
7191
7601
|
if (slots.length !== send.length) throw new DbError("INVALID_QUERY", [{
|
|
7192
7602
|
path: "",
|
|
7193
7603
|
message: "Adapter insertManyIgnore must return one slot per input row"
|
|
@@ -7448,6 +7858,231 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7448
7858
|
}
|
|
7449
7859
|
};
|
|
7450
7860
|
//#endregion
|
|
7861
|
+
//#region src/table/view-read-plan.ts
|
|
7862
|
+
/** Design types whose `=` compares one scalar to one scalar on every engine. */
|
|
7863
|
+
const SCALAR_TYPES = new Set([
|
|
7864
|
+
"string",
|
|
7865
|
+
"number",
|
|
7866
|
+
"boolean",
|
|
7867
|
+
"decimal",
|
|
7868
|
+
"bigint"
|
|
7869
|
+
]);
|
|
7870
|
+
/** The comparisons of a pure conjunction (nested `$and`s flattened), or `undefined` for any `$or` / `$not`. */
|
|
7871
|
+
function conjuncts(node, out = []) {
|
|
7872
|
+
if ("$and" in node) {
|
|
7873
|
+
for (const child of node.$and) if (!conjuncts(child, out)) return void 0;
|
|
7874
|
+
return out;
|
|
7875
|
+
}
|
|
7876
|
+
if ("$or" in node || "$not" in node) return void 0;
|
|
7877
|
+
out.push(node);
|
|
7878
|
+
return out;
|
|
7879
|
+
}
|
|
7880
|
+
/** Every scope a predicate references (its field refs' tables). */
|
|
7881
|
+
function predicateScopes(host, node, out) {
|
|
7882
|
+
if ("$and" in node || "$or" in node) {
|
|
7883
|
+
const children = node;
|
|
7884
|
+
for (const child of children.$and ?? children.$or ?? []) predicateScopes(host, child, out);
|
|
7885
|
+
return out;
|
|
7886
|
+
}
|
|
7887
|
+
if ("$not" in node) return predicateScopes(host, node.$not, out);
|
|
7888
|
+
const comp = node;
|
|
7889
|
+
out.add(host.resolveRefSource(comp.left).table);
|
|
7890
|
+
if (isFieldRef(comp.right)) out.add(host.resolveRefSource(comp.right).table);
|
|
7891
|
+
return out;
|
|
7892
|
+
}
|
|
7893
|
+
/** Whether a literal `value` compares as the scalar design type `designType` (no implicit casts). */
|
|
7894
|
+
function literalMatches(value, designType) {
|
|
7895
|
+
switch (typeof value) {
|
|
7896
|
+
case "string": return designType === "string";
|
|
7897
|
+
case "number": return designType === "number" || designType === "decimal";
|
|
7898
|
+
case "boolean": return designType === "boolean";
|
|
7899
|
+
default: return false;
|
|
7900
|
+
}
|
|
7901
|
+
}
|
|
7902
|
+
/**
|
|
7903
|
+
* Whether the left join `join` matches AT MOST ONE target row per input row,
|
|
7904
|
+
* so dropping it (when nothing reads it) leaves every result row in place:
|
|
7905
|
+
*
|
|
7906
|
+
* - a first-row join (its ON pins the target's primary key to one value);
|
|
7907
|
+
* - otherwise its condition is a conjunction whose `=` comparisons pin EVERY
|
|
7908
|
+
* column of one of the target's unique keys (primary key included) to a
|
|
7909
|
+
* value that does not depend on the target — another scope's field of the
|
|
7910
|
+
* same scalar design type that compares alike (collation, type overrides,
|
|
7911
|
+
* table charset), or a literal of that type.
|
|
7912
|
+
* Other conjuncts only narrow the match.
|
|
7913
|
+
*
|
|
7914
|
+
* Never for an inner join (it filters), a target that is a view (its
|
|
7915
|
+
* uniqueness is unknown), or a predicate with `$or` / `$not`. On a document
|
|
7916
|
+
* store every pinned key field must also be required: a unique index over an
|
|
7917
|
+
* optional field is partial there, and a missing value would match many.
|
|
7918
|
+
*/
|
|
7919
|
+
function atMostOneMatch(host, join) {
|
|
7920
|
+
if (join.kind !== "left") return false;
|
|
7921
|
+
const keySets = host.uniqueKeySets(join.targetType());
|
|
7922
|
+
if (!keySets) return false;
|
|
7923
|
+
if (join.first) return true;
|
|
7924
|
+
const comparisons = conjuncts(join.condition);
|
|
7925
|
+
if (!comparisons) return false;
|
|
7926
|
+
const pinned = /* @__PURE__ */ new Set();
|
|
7927
|
+
for (const node of comparisons) {
|
|
7928
|
+
const comp = node;
|
|
7929
|
+
if (comp.op !== "$eq" || comp.right === null || comp.right === void 0) continue;
|
|
7930
|
+
const left = host.resolveRefSource(comp.left);
|
|
7931
|
+
const rightRef = isFieldRef(comp.right) ? comp.right : void 0;
|
|
7932
|
+
const right = rightRef ? host.resolveRefSource(rightRef) : void 0;
|
|
7933
|
+
let target;
|
|
7934
|
+
let other;
|
|
7935
|
+
if (left.table === join.scope && right?.table !== join.scope) {
|
|
7936
|
+
target = {
|
|
7937
|
+
ref: comp.left,
|
|
7938
|
+
source: left.source
|
|
7939
|
+
};
|
|
7940
|
+
other = right && rightRef ? {
|
|
7941
|
+
ref: rightRef,
|
|
7942
|
+
source: right.source
|
|
7943
|
+
} : void 0;
|
|
7944
|
+
} else if (right && rightRef && right.table === join.scope && left.table !== join.scope) {
|
|
7945
|
+
target = {
|
|
7946
|
+
ref: rightRef,
|
|
7947
|
+
source: right.source
|
|
7948
|
+
};
|
|
7949
|
+
other = {
|
|
7950
|
+
ref: comp.left,
|
|
7951
|
+
source: left.source
|
|
7952
|
+
};
|
|
7953
|
+
} else continue;
|
|
7954
|
+
const { source } = target;
|
|
7955
|
+
if (source.jsonPath || source.flattened || !SCALAR_TYPES.has(source.designType)) continue;
|
|
7956
|
+
if (host.nested && source.optional) continue;
|
|
7957
|
+
const traits = host.fieldTraits(target.ref);
|
|
7958
|
+
if (traits.encrypted) continue;
|
|
7959
|
+
if (other) {
|
|
7960
|
+
const o = other.source;
|
|
7961
|
+
if (o.jsonPath || o.flattened || o.designType !== source.designType) continue;
|
|
7962
|
+
const otherTraits = host.fieldTraits(other.ref);
|
|
7963
|
+
if (otherTraits.encrypted || otherTraits.compare !== traits.compare) continue;
|
|
7964
|
+
} else if (!literalMatches(comp.right, source.designType)) continue;
|
|
7965
|
+
pinned.add(source.column);
|
|
7966
|
+
}
|
|
7967
|
+
return keySets.some((set) => set.length > 0 && set.every((column) => pinned.has(column)));
|
|
7968
|
+
}
|
|
7969
|
+
function prunerState(host) {
|
|
7970
|
+
const plan = host.viewPlan;
|
|
7971
|
+
if (plan.materialized || !plan.joins.some((j) => j.kind === "left")) return void 0;
|
|
7972
|
+
const columns = host.getViewColumnMappings();
|
|
7973
|
+
if (columns.some((c) => c.aggFn)) return void 0;
|
|
7974
|
+
const joins = plan.joins.map((join) => {
|
|
7975
|
+
const refs = predicateScopes(host, join.condition, /* @__PURE__ */ new Set());
|
|
7976
|
+
for (const { ref } of join.first?.order ?? []) refs.add(host.resolveRefSource(ref).table);
|
|
7977
|
+
refs.delete(join.scope);
|
|
7978
|
+
return {
|
|
7979
|
+
join,
|
|
7980
|
+
droppable: atMostOneMatch(host, join),
|
|
7981
|
+
refs
|
|
7982
|
+
};
|
|
7983
|
+
});
|
|
7984
|
+
if (!joins.some((j) => j.droppable)) return void 0;
|
|
7985
|
+
const base = new Set([plan.entryTable]);
|
|
7986
|
+
if (plan.filter) predicateScopes(host, plan.filter, base);
|
|
7987
|
+
for (const j of joins) if (!j.droppable) base.add(j.join.scope);
|
|
7988
|
+
const byPath = new Map(columns.map((c) => [c.viewPath, c]));
|
|
7989
|
+
const columnScopes = /* @__PURE__ */ new Map();
|
|
7990
|
+
const scopesOf = (c) => {
|
|
7991
|
+
let scopes = columnScopes.get(c);
|
|
7992
|
+
if (scopes) return scopes;
|
|
7993
|
+
scopes = /* @__PURE__ */ new Set();
|
|
7994
|
+
columnScopes.set(c, scopes);
|
|
7995
|
+
if (c.expr === void 0) scopes.add(c.sourceTable);
|
|
7996
|
+
else {
|
|
7997
|
+
const out = scopes;
|
|
7998
|
+
walkViewExpr(c.expr, (path) => {
|
|
7999
|
+
const leaf = byPath.get(path);
|
|
8000
|
+
if (!leaf) {
|
|
8001
|
+
for (const j of joins) out.add(j.join.scope);
|
|
8002
|
+
return;
|
|
8003
|
+
}
|
|
8004
|
+
for (const scope of scopesOf(leaf)) out.add(scope);
|
|
8005
|
+
});
|
|
8006
|
+
}
|
|
8007
|
+
return scopes;
|
|
8008
|
+
};
|
|
8009
|
+
for (const c of columns) scopesOf(c);
|
|
8010
|
+
return {
|
|
8011
|
+
base,
|
|
8012
|
+
joins,
|
|
8013
|
+
columnScopes,
|
|
8014
|
+
byColumn: new Map(columns.map((c) => [c.viewColumn, c])),
|
|
8015
|
+
columns
|
|
8016
|
+
};
|
|
8017
|
+
}
|
|
8018
|
+
/**
|
|
8019
|
+
* The read planner of one managed view: memoised per dropped-join set.
|
|
8020
|
+
* @since 0.1.153
|
|
8021
|
+
*/
|
|
8022
|
+
var ViewReadPlanner = class {
|
|
8023
|
+
_host;
|
|
8024
|
+
_state;
|
|
8025
|
+
_variants = /* @__PURE__ */ new Map();
|
|
8026
|
+
constructor(_host) {
|
|
8027
|
+
this._host = _host;
|
|
8028
|
+
}
|
|
8029
|
+
/** Whether any join of the view can ever be dropped. */
|
|
8030
|
+
get prunable() {
|
|
8031
|
+
return this._getState() !== void 0;
|
|
8032
|
+
}
|
|
8033
|
+
_getState() {
|
|
8034
|
+
if (this._state === void 0) try {
|
|
8035
|
+
this._state = prunerState(this._host) ?? null;
|
|
8036
|
+
} catch {
|
|
8037
|
+
this._state = null;
|
|
8038
|
+
}
|
|
8039
|
+
return this._state ?? void 0;
|
|
8040
|
+
}
|
|
8041
|
+
/** The mappings a needed (physical) name reads: exact, or a document path above / below it. */
|
|
8042
|
+
_matching(state, name) {
|
|
8043
|
+
const exact = state.byColumn.get(name);
|
|
8044
|
+
if (exact) return [exact];
|
|
8045
|
+
return state.columns.filter((c) => name.startsWith(`${c.viewColumn}.`) || c.viewColumn.startsWith(`${name}.`));
|
|
8046
|
+
}
|
|
8047
|
+
plan(needed) {
|
|
8048
|
+
const state = this._getState();
|
|
8049
|
+
if (!state) return void 0;
|
|
8050
|
+
const keep = new Set(state.base);
|
|
8051
|
+
const read = needed === void 0 ? state.columns : [...needed].flatMap((n) => this._matching(state, n));
|
|
8052
|
+
for (const c of read) for (const scope of state.columnScopes.get(c)) keep.add(scope);
|
|
8053
|
+
const dropped = [];
|
|
8054
|
+
for (let i = state.joins.length - 1; i >= 0; i--) {
|
|
8055
|
+
const { join, droppable, refs } = state.joins[i];
|
|
8056
|
+
if (droppable && !keep.has(join.scope)) dropped.push(join.scope);
|
|
8057
|
+
else for (const scope of refs) keep.add(scope);
|
|
8058
|
+
}
|
|
8059
|
+
if (dropped.length === 0) return void 0;
|
|
8060
|
+
dropped.reverse();
|
|
8061
|
+
const key = dropped.join("\0");
|
|
8062
|
+
let variant = this._variants.get(key);
|
|
8063
|
+
if (!variant) {
|
|
8064
|
+
const gone = new Set(dropped);
|
|
8065
|
+
const plan = this._host.viewPlan;
|
|
8066
|
+
const columns = [];
|
|
8067
|
+
const droppedColumns = [];
|
|
8068
|
+
for (const c of state.columns) if ([...state.columnScopes.get(c)].some((scope) => gone.has(scope))) droppedColumns.push(c.viewColumn);
|
|
8069
|
+
else columns.push(c);
|
|
8070
|
+
variant = {
|
|
8071
|
+
plan: {
|
|
8072
|
+
...plan,
|
|
8073
|
+
joins: plan.joins.filter((j) => !gone.has(j.scope))
|
|
8074
|
+
},
|
|
8075
|
+
columns,
|
|
8076
|
+
dropped,
|
|
8077
|
+
droppedColumns,
|
|
8078
|
+
key
|
|
8079
|
+
};
|
|
8080
|
+
this._variants.set(key, variant);
|
|
8081
|
+
}
|
|
8082
|
+
return variant;
|
|
8083
|
+
}
|
|
8084
|
+
};
|
|
8085
|
+
//#endregion
|
|
7451
8086
|
//#region src/table/db-view.ts
|
|
7452
8087
|
/**
|
|
7453
8088
|
* The single `@meta.id` field of a first-row join target (its source type,
|
|
@@ -7511,6 +8146,22 @@ function viewFieldSource(fieldName, fieldType, entryType) {
|
|
|
7511
8146
|
sourcePath: aggField && aggField !== "*" ? aggField : fieldName
|
|
7512
8147
|
};
|
|
7513
8148
|
}
|
|
8149
|
+
/**
|
|
8150
|
+
* Field annotations that change how a column compares (`=`): its collation
|
|
8151
|
+
* and engine-type overrides. Two columns of a join condition must agree on
|
|
8152
|
+
* them (and on {@link COMPARE_TABLE_KEYS}) for the condition to prove a
|
|
8153
|
+
* unique-key match (`AtscriptDbView.readPlan`).
|
|
8154
|
+
*/
|
|
8155
|
+
const COMPARE_FIELD_KEYS = [
|
|
8156
|
+
"db.column.collate",
|
|
8157
|
+
"db.mysql.type",
|
|
8158
|
+
"db.mysql.collate",
|
|
8159
|
+
"db.mysql.charset",
|
|
8160
|
+
"db.pg.type",
|
|
8161
|
+
"db.pg.collate"
|
|
8162
|
+
];
|
|
8163
|
+
/** Table annotations that set its columns' default charset / collation. */
|
|
8164
|
+
const COMPARE_TABLE_KEYS = ["db.mysql.charset", "db.mysql.collate"];
|
|
7514
8165
|
/** View types whose fields already carry their inherited seals. */
|
|
7515
8166
|
const sealedViews = /* @__PURE__ */ new WeakSet();
|
|
7516
8167
|
/**
|
|
@@ -7586,6 +8237,7 @@ function inheritViewFieldSeals(viewType) {
|
|
|
7586
8237
|
var AtscriptDbView = class extends AtscriptDbReadable {
|
|
7587
8238
|
_viewPlan;
|
|
7588
8239
|
_columnMappings;
|
|
8240
|
+
_readPlanner;
|
|
7589
8241
|
get isView() {
|
|
7590
8242
|
return true;
|
|
7591
8243
|
}
|
|
@@ -7737,6 +8389,53 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
7737
8389
|
return this._columnMappings;
|
|
7738
8390
|
}
|
|
7739
8391
|
/**
|
|
8392
|
+
* The read variant of this managed view for a query that reads only the
|
|
8393
|
+
* `needed` PHYSICAL view columns (`undefined`: every column) — the view
|
|
8394
|
+
* plan with every LEFT join removed that (a) matches at most one row per
|
|
8395
|
+
* input row and (b) feeds no needed column, no other kept join's ON clause
|
|
8396
|
+
* and no `@db.view.filter`. Dropping such a join leaves every row of the
|
|
8397
|
+
* view in place, so a query over the variant returns exactly what it
|
|
8398
|
+
* returns over the view. `undefined` when nothing can be dropped.
|
|
8399
|
+
*
|
|
8400
|
+
* At most one match is proven from the model: a first-row join, or an ON
|
|
8401
|
+
* clause whose `=` conjuncts pin every column of one of the target's unique
|
|
8402
|
+
* keys (primary key included) — see the views guide. Inner joins, joins to
|
|
8403
|
+
* a view, grouped (`@db.agg.*`), materialized and external views are never
|
|
8404
|
+
* pruned. A variant's {@link TViewReadPlan.columns} omit the columns of the
|
|
8405
|
+
* dropped joins, so a column reference the caller failed to declare fails
|
|
8406
|
+
* loudly instead of reading wrong data.
|
|
8407
|
+
*
|
|
8408
|
+
* Memoised per dropped-join set. Adapters use it to read through an
|
|
8409
|
+
* inline, pruned definition instead of the stored view.
|
|
8410
|
+
* @since 0.1.153
|
|
8411
|
+
*/
|
|
8412
|
+
readPlan(needed) {
|
|
8413
|
+
if (this.isExternal) return void 0;
|
|
8414
|
+
this._readPlanner ??= new ViewReadPlanner({
|
|
8415
|
+
viewPlan: this.viewPlan,
|
|
8416
|
+
nested: this._nested,
|
|
8417
|
+
getViewColumnMappings: () => this.getViewColumnMappings(),
|
|
8418
|
+
resolveRefSource: (ref) => this.resolveRefSource(ref),
|
|
8419
|
+
uniqueKeySets: (targetType) => {
|
|
8420
|
+
const source = viewSourceOf(targetType).type;
|
|
8421
|
+
if (isViewType(source) || !this._tableResolver) return void 0;
|
|
8422
|
+
const target = this._tableResolver(source);
|
|
8423
|
+
return target && !target.isView ? target.uniqueKeySets : void 0;
|
|
8424
|
+
},
|
|
8425
|
+
fieldTraits: (ref) => {
|
|
8426
|
+
const source = viewSourceOf(ref.type ? ref.type() : this.viewPlan.entryType()).type;
|
|
8427
|
+
const field = sourceFieldMetadata(source, ref.field);
|
|
8428
|
+
const table = source.metadata;
|
|
8429
|
+
const column = field;
|
|
8430
|
+
return {
|
|
8431
|
+
compare: JSON.stringify([...COMPARE_FIELD_KEYS.map((key) => column?.get(key) ?? null), ...COMPARE_TABLE_KEYS.map((key) => table.get(key) ?? null)]),
|
|
8432
|
+
encrypted: field?.has("db.encrypted") === true
|
|
8433
|
+
};
|
|
8434
|
+
}
|
|
8435
|
+
});
|
|
8436
|
+
return this._readPlanner.plan(needed);
|
|
8437
|
+
}
|
|
8438
|
+
/**
|
|
7740
8439
|
* Why the bound adapter cannot render this managed view: one message per
|
|
7741
8440
|
* computed column / first-row join whose feature its `viewCapabilities()`
|
|
7742
8441
|
* does not list. Empty for an external view or when every feature is
|