@atscript/db 0.1.145 → 0.1.147
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-CfsveE1w.mjs → aggregate-fns-C0BymEqa.mjs} +3 -1
- package/dist/{aggregate-fns-CGBv3E8S.cjs → aggregate-fns-CtsRbZn9.cjs} +8 -0
- package/dist/{buckets-DYFu0eZ8.d.cts → buckets-CjdPipCC.d.cts} +522 -17
- package/dist/{buckets-DRycmhOW.d.mts → buckets-DYopfd2Y.d.mts} +522 -17
- package/dist/{column-diff-e2oHc71_.mjs → column-diff-CO06iQvI.mjs} +862 -115
- package/dist/{column-diff-D_Kyuh0S.cjs → column-diff-Dt7EDJsA.cjs} +872 -113
- package/dist/{column-diff-Q9UmWn5x.d.mts → fk-diff-BtOmL4OW.d.mts} +48 -2
- package/dist/{column-diff-w-Mym_3w.d.cts → fk-diff-DLqwVkKG.d.cts} +48 -2
- package/dist/index.cjs +28 -33
- package/dist/index.d.cts +40 -15
- package/dist/index.d.mts +40 -15
- package/dist/index.mjs +5 -33
- package/dist/{nested-writer-CnOOAehr.mjs → nested-writer-D35UsDBT.mjs} +542 -2
- package/dist/{nested-writer-xfQwxplL.cjs → nested-writer-SqtYJHF0.cjs} +666 -0
- package/dist/plugin.cjs +266 -5
- package/dist/plugin.mjs +267 -6
- 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-B-0NRKat.d.mts → relation-helpers-C2bLtE-t.d.mts} +1 -1
- package/dist/{relation-helpers-BOMm_HUI.d.cts → relation-helpers-DH3LvwLV.d.cts} +1 -1
- package/dist/relation-loader-BFhVuJ-B.cjs +369 -0
- package/dist/relation-loader-C0qE8xgR.mjs +370 -0
- package/dist/shared.cjs +1 -1
- package/dist/shared.d.cts +4 -3
- package/dist/shared.d.mts +4 -3
- package/dist/shared.mjs +1 -1
- package/dist/sync.cjs +10 -6
- package/dist/sync.d.cts +2 -25
- package/dist/sync.d.mts +2 -25
- package/dist/sync.mjs +10 -6
- package/dist/{validation-utils-DOsB4e6G.cjs → validation-utils-Da2GjobR.cjs} +35 -24
- package/dist/{validation-utils-CMR4fe2M.mjs → validation-utils-Dq0uZ7ef.mjs} +35 -24
- package/dist/{validator-Bw6ks9Hy.d.mts → validator-CMnTI6r7.d.cts} +9 -6
- package/dist/{validator-Bw6ks9Hy.d.cts → validator-CMnTI6r7.d.mts} +9 -6
- package/dist/validator.d.cts +1 -1
- package/dist/validator.d.mts +1 -1
- package/package.json +8 -8
- package/dist/relation-loader-CBPY6kM7.cjs +0 -461
- package/dist/relation-loader-D9XuXaMv.mjs +0 -462
package/dist/plugin.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as SUPPORTED_AGGREGATE_FNS, i as NULL_WHEN_EMPTY_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-C0BymEqa.mjs";
|
|
2
2
|
import { n as DERIVED_INCOMPATIBLE, r as JSON_LEAF_TYPES, t as DB_ENTITY_ANNOTATIONS } from "./derived-rules-0sKn4f5C.mjs";
|
|
3
3
|
import "./consts-C_-5_pFq.mjs";
|
|
4
|
-
import { S as validateSiblingStringField, _ as getParentTypeName, a as hasAnyViewAnnotation, b as validateExclusiveWith, d as viewJoins, f as viewScopeTypes, g as getParentStruct, h as getNavTargetTypeName, l as validateQueryScope, m as getDbTableOwner, n as findFKFieldsPointingTo, o as isAliasDecl, p as getAnnotationAlias, r as findViewCycle, s as isDbSourceDecl, t as earlierJoinTargets, u as validateRefArgument, v as primitiveBaseType, x as validateFieldBaseType, y as refActionAnnotation } from "./validation-utils-
|
|
4
|
+
import { S as validateSiblingStringField, _ as getParentTypeName, a as hasAnyViewAnnotation, b as validateExclusiveWith, d as viewJoins, f as viewScopeTypes, g as getParentStruct, h as getNavTargetTypeName, i as forEachFieldRef, l as validateQueryScope, m as getDbTableOwner, n as findFKFieldsPointingTo, o as isAliasDecl, p as getAnnotationAlias, r as findViewCycle, s as isDbSourceDecl, t as earlierJoinTargets, u as validateRefArgument, v as primitiveBaseType, x as validateFieldBaseType, y as refActionAnnotation } from "./validation-utils-Dq0uZ7ef.mjs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { AnnotationSpec, DEFAULT_FORMAT, isArray, isInterface, isPrimitive, isProp, isRef, isStructure } from "@atscript/core";
|
|
7
|
+
import { AnnotationSpec, DEFAULT_FORMAT, getFieldsForType, isArray, isInterface, isPrimitive, isProp, isQueryComparison, isQueryLogical, isRef, isStructure } from "@atscript/core";
|
|
8
8
|
//#region src/plugin/manifest.ts
|
|
9
9
|
/**
|
|
10
10
|
* Renders the model-manifest module: an inventory of every exported
|
|
@@ -138,6 +138,27 @@ function viewHavingScope(view) {
|
|
|
138
138
|
unqualifiedTarget: view.id
|
|
139
139
|
} : void 0;
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* The ordering (4th argument) of a first-row `@db.view.joins`: the join
|
|
143
|
+
* target only — an unqualified key is a field of the target.
|
|
144
|
+
* @since 0.1.147
|
|
145
|
+
*/
|
|
146
|
+
function joinOrderScope(join) {
|
|
147
|
+
const target = join.args[0]?.text;
|
|
148
|
+
return target ? {
|
|
149
|
+
allowedTypes: [target],
|
|
150
|
+
unqualifiedTarget: target
|
|
151
|
+
} : void 0;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The expression of a `@db.compute` on a view field: the view's own fields,
|
|
155
|
+
* unqualified — the `@db.view.having` scope.
|
|
156
|
+
* @since 0.1.147
|
|
157
|
+
*/
|
|
158
|
+
function computeScope(propToken) {
|
|
159
|
+
const view = getDbTableOwner(propToken);
|
|
160
|
+
return view ? viewHavingScope(view) : void 0;
|
|
161
|
+
}
|
|
141
162
|
/** The condition of a `@db.agg.*` on a view field: the scope of the view's `@db.view.filter`. */
|
|
142
163
|
function aggConditionScope(propToken) {
|
|
143
164
|
const view = getDbTableOwner(propToken);
|
|
@@ -172,16 +193,25 @@ function relFilterScope(field) {
|
|
|
172
193
|
unqualifiedTarget: target
|
|
173
194
|
};
|
|
174
195
|
}
|
|
196
|
+
/** The `@db.view.joins` annotation one of whose arguments is `arg`. */
|
|
197
|
+
function joinOfArg(joins, arg) {
|
|
198
|
+
return joins.find((a) => a.args.includes(arg));
|
|
199
|
+
}
|
|
175
200
|
/** The `fieldScope` hooks (argument token → scope) the annotation specs declare. */
|
|
176
201
|
const fieldScopes = {
|
|
177
202
|
viewFilter: (arg) => arg.parentNode ? viewFilterScope(arg.parentNode) : void 0,
|
|
178
203
|
viewJoin: (arg) => {
|
|
179
204
|
const view = arg.parentNode;
|
|
180
205
|
const joins = view ? viewJoins(view) : [];
|
|
181
|
-
const join = joins
|
|
206
|
+
const join = joinOfArg(joins, arg);
|
|
182
207
|
return view && join ? viewJoinScope(view, join, earlierJoinTargets(joins, join)) : void 0;
|
|
183
208
|
},
|
|
209
|
+
joinOrder: (arg) => {
|
|
210
|
+
const join = arg.parentNode ? joinOfArg(viewJoins(arg.parentNode), arg) : void 0;
|
|
211
|
+
return join ? joinOrderScope(join) : void 0;
|
|
212
|
+
},
|
|
184
213
|
viewHaving: (arg) => arg.parentNode ? viewHavingScope(arg.parentNode) : void 0,
|
|
214
|
+
compute: computeScope,
|
|
185
215
|
aggCondition: aggConditionScope,
|
|
186
216
|
aggField: aggFieldScope,
|
|
187
217
|
relFilter: (arg) => arg.parentNode ? relFilterScope(arg.parentNode) : void 0
|
|
@@ -832,6 +862,127 @@ function columnCapability(capability, verb) {
|
|
|
832
862
|
});
|
|
833
863
|
}
|
|
834
864
|
//#endregion
|
|
865
|
+
//#region src/plugin/annotations/compute.ts
|
|
866
|
+
/** What a `@db.compute` field cannot also carry (VC1). */
|
|
867
|
+
const EXCLUSIVE_WITH = [
|
|
868
|
+
...AGG_ANNOTATIONS,
|
|
869
|
+
"db.json",
|
|
870
|
+
"db.ignore"
|
|
871
|
+
].map((key) => ({ key }));
|
|
872
|
+
/** Primitive tags of timestamps (MySQL may store them as native `TIMESTAMP`). */
|
|
873
|
+
const TIMESTAMP_TAGS = [
|
|
874
|
+
"timestamp",
|
|
875
|
+
"created",
|
|
876
|
+
"updated"
|
|
877
|
+
];
|
|
878
|
+
/** The `@db.compute` expression token of a prop, if any. */
|
|
879
|
+
function computeArg(prop) {
|
|
880
|
+
return prop?.annotations?.find((a) => a.name === "db.compute")?.args[0];
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Whether a prop is typed exactly `number` (a plain primitive ref — no chain
|
|
884
|
+
* ref, no `number.*` extension).
|
|
885
|
+
*/
|
|
886
|
+
function isPlainNumber(prop, doc) {
|
|
887
|
+
const def = prop.getDefinition();
|
|
888
|
+
if (!def || !isRef(def) || def.hasChain || def.id !== "number") return false;
|
|
889
|
+
return isPrimitive(doc.unwindType("number")?.def);
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Why an operand prop cannot be computed with, or `undefined` when it can:
|
|
893
|
+
* its resolved type must be a `number` primitive — not `decimal`, not a
|
|
894
|
+
* timestamp — and it must have storage (no `@db.ignore`).
|
|
895
|
+
*/
|
|
896
|
+
function operandProblem(prop, doc) {
|
|
897
|
+
if (prop.countAnnotations("db.ignore") > 0) return "is @db.ignore'd";
|
|
898
|
+
if (computeArg(prop)) return void 0;
|
|
899
|
+
const def = prop.getDefinition();
|
|
900
|
+
const leaf = def && isRef(def) ? doc.unwindType(def.id, def.chain)?.def : def;
|
|
901
|
+
const base = primitiveBaseType(leaf);
|
|
902
|
+
if (base === void 0) return void 0;
|
|
903
|
+
if (base !== "number") return `is ${base === "decimal" ? "a decimal" : `a ${base}`}`;
|
|
904
|
+
if (isPrimitive(leaf) && TIMESTAMP_TAGS.some((t) => leaf.tags.has(t))) return "is a timestamp";
|
|
905
|
+
}
|
|
906
|
+
/** VC6 — whether an expression may be NULL (`/` by zero, an optional operand). */
|
|
907
|
+
function exprNullable(node, props) {
|
|
908
|
+
if ("value" in node) return false;
|
|
909
|
+
if ("left" in node) return node.op === "/" || exprNullable(node.left, props) || exprNullable(node.right, props);
|
|
910
|
+
if ("operand" in node) return exprNullable(node.operand, props);
|
|
911
|
+
if ("args" in node) return node.args.every((a) => exprNullable(a, props));
|
|
912
|
+
return props.get(node.fieldRef.text)?.has("optional") ?? false;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* VC4 — the first `@db.compute` cycle through `start`, as field names
|
|
916
|
+
* (`rank → priority → rank`), or `undefined`.
|
|
917
|
+
*/
|
|
918
|
+
function findComputeCycle(start, props) {
|
|
919
|
+
const path = [];
|
|
920
|
+
const done = /* @__PURE__ */ new Set();
|
|
921
|
+
const visit = (name) => {
|
|
922
|
+
const at = path.indexOf(name);
|
|
923
|
+
if (at !== -1) return name === start ? [...path.slice(at), name] : void 0;
|
|
924
|
+
if (done.has(name)) return void 0;
|
|
925
|
+
const refs = computeArg(props.get(name))?.exprNode?.fieldRefs() ?? [];
|
|
926
|
+
path.push(name);
|
|
927
|
+
for (const ref of refs) {
|
|
928
|
+
if (ref.typeRef) continue;
|
|
929
|
+
const cycle = visit(ref.fieldRef.text);
|
|
930
|
+
if (cycle) return cycle;
|
|
931
|
+
}
|
|
932
|
+
path.pop();
|
|
933
|
+
done.add(name);
|
|
934
|
+
};
|
|
935
|
+
return visit(start);
|
|
936
|
+
}
|
|
937
|
+
function validateCompute(token, args, doc) {
|
|
938
|
+
const errors = [];
|
|
939
|
+
const prop = token.parentNode;
|
|
940
|
+
const field = prop.id ?? "field";
|
|
941
|
+
const view = getDbTableOwner(token);
|
|
942
|
+
const error = (message, range = token.range) => errors.push({
|
|
943
|
+
message,
|
|
944
|
+
severity: 1,
|
|
945
|
+
range
|
|
946
|
+
});
|
|
947
|
+
if (!view || view.countAnnotations("db.view.for") === 0) {
|
|
948
|
+
error("@db.compute is only valid on a field of a @db.view.for view");
|
|
949
|
+
return errors;
|
|
950
|
+
}
|
|
951
|
+
errors.push(...validateExclusiveWith(token, "@db.compute", EXCLUSIVE_WITH));
|
|
952
|
+
if (!isPlainNumber(prop, doc)) error(`Field "${field}" has a @db.compute and must be typed \`number\``);
|
|
953
|
+
const expr = args[0]?.exprNode;
|
|
954
|
+
if (!expr) return errors;
|
|
955
|
+
const refs = expr.fieldRefs();
|
|
956
|
+
const props = viewProps(view) ?? /* @__PURE__ */ new Map();
|
|
957
|
+
if (refs.length === 0) error("@db.compute needs at least one view field — a constant column is not supported", args[0].range);
|
|
958
|
+
const scope = viewHavingScope(view);
|
|
959
|
+
if (scope) errors.push(...validateQueryScope(args[0], scope, doc, "@db.compute references the view's own fields — declare it on the view first (`field: Type.field`)"));
|
|
960
|
+
for (const ref of refs) {
|
|
961
|
+
if (ref.typeRef) continue;
|
|
962
|
+
const operand = props.get(ref.fieldRef.text);
|
|
963
|
+
const why = operand && operandProblem(operand, doc);
|
|
964
|
+
if (why) error(`@db.compute operand '${ref.fieldRef.text}' ${why} — operands must be number fields`, ref.fieldRef.range);
|
|
965
|
+
}
|
|
966
|
+
if (prop.id) {
|
|
967
|
+
const cycle = findComputeCycle(prop.id, props);
|
|
968
|
+
if (cycle) error(`@db.compute of "${field}" depends on itself: ${cycle.join(" → ")}`, args[0].range);
|
|
969
|
+
}
|
|
970
|
+
if (!prop.has("optional") && exprNullable(expr.expression, props)) error(`Field "${field}" has a @db.compute that may be NULL and must be optional (${field}?: …) — it is NULL when an operand is NULL or a divisor is 0`);
|
|
971
|
+
return errors;
|
|
972
|
+
}
|
|
973
|
+
const dbComputeAnnotations = { compute: new AnnotationSpec({
|
|
974
|
+
description: "Declares a **computed view column** (since 0.1.147): closed arithmetic over the view's own fields — dimensions, aggregates, first-row-join fields and other computed fields — evaluated by the database, so the column sorts, filters and pages like any other.\n\nGrammar: `+ - * /`, unary `-`, parentheses, numeric literals, `coalesce(a, b, …)`. Every value is a double (`7 / 2 = 3.5`); a NULL operand yields NULL; division by zero yields NULL — so an expression with `/` or an optional operand needs an optional field.\n\n**Example:**\n```atscript\n@db.agg.count 'id', `Issue.status = 'open'`\nopenCount: Issue.id\n\n@db.compute `openCount * 10 + coalesce(oldestSeverity, 0)`\nrank: number\n```\n",
|
|
975
|
+
nodeType: ["prop"],
|
|
976
|
+
passedWhenReferred: false,
|
|
977
|
+
argument: {
|
|
978
|
+
name: "expression",
|
|
979
|
+
type: "expr",
|
|
980
|
+
description: "Arithmetic over the view's own fields (unqualified): `+ - * /`, unary `-`, parentheses, numbers, `coalesce(a, b, …)`.",
|
|
981
|
+
fieldScope: fieldScopes.compute
|
|
982
|
+
},
|
|
983
|
+
validate: validateCompute
|
|
984
|
+
}) };
|
|
985
|
+
//#endregion
|
|
835
986
|
//#region src/plugin/annotations/index-ann.ts
|
|
836
987
|
const dbIndexAnnotations = { index: {
|
|
837
988
|
plain: new AnnotationSpec({
|
|
@@ -1222,8 +1373,23 @@ const dbRelAnnotations = { rel: {
|
|
|
1222
1373
|
return errors;
|
|
1223
1374
|
}
|
|
1224
1375
|
}),
|
|
1376
|
+
filterable: new AnnotationSpec({
|
|
1377
|
+
description: "Lets HTTP clients filter the parent rows by this relation's related rows — `ticket=$some(status=open)` / `ticket=$none(...)` in a moost-db query (`{ ticket: { $some: { status: 'open' } } }`). Off by default: a client predicate filters the PARENT set and runs a correlated subquery. Server-side code (`transformFilter`, `actionRowScope`, direct table calls) never needs it. The related table's own field rules (visibility, encryption, write-only) apply inside the predicate.\n\n**Example:**\n```atscript\n@db.rel.to\n@db.rel.filterable\nticket?: Ticket\n```\n",
|
|
1378
|
+
nodeType: ["prop"],
|
|
1379
|
+
passedWhenReferred: false,
|
|
1380
|
+
validate(token) {
|
|
1381
|
+
const errors = [];
|
|
1382
|
+
const field = token.parentNode;
|
|
1383
|
+
if (!(field.countAnnotations("db.rel.to") > 0 || field.countAnnotations("db.rel.from") > 0 || field.countAnnotations("db.rel.via") > 0)) errors.push({
|
|
1384
|
+
message: "@db.rel.filterable is only valid on navigational fields (@db.rel.to, @db.rel.from, or @db.rel.via)",
|
|
1385
|
+
severity: 1,
|
|
1386
|
+
range: token.range
|
|
1387
|
+
});
|
|
1388
|
+
return errors;
|
|
1389
|
+
}
|
|
1390
|
+
}),
|
|
1225
1391
|
filter: new AnnotationSpec({
|
|
1226
|
-
description: "Applies a filter to a navigational property, restricting which related records are loaded.\n\n**Example:**\n```atscript\n@db.rel.from\n@db.rel.filter `Post.published = true`\npublishedPosts: Post[]\n```\n",
|
|
1392
|
+
description: "Applies a filter to a navigational property, restricting which related records are loaded (`$with`) and which count in relational predicates (`$some` / `$none`).\n\n**Example:**\n```atscript\n@db.rel.from\n@db.rel.filter `Post.published = true`\npublishedPosts: Post[]\n```\n",
|
|
1227
1393
|
nodeType: ["prop"],
|
|
1228
1394
|
passedWhenReferred: false,
|
|
1229
1395
|
argument: {
|
|
@@ -1249,10 +1415,40 @@ const dbRelAnnotations = { rel: {
|
|
|
1249
1415
|
if (!args[0]?.queryNode) return errors;
|
|
1250
1416
|
const scope = relFilterScope(field);
|
|
1251
1417
|
if (scope) errors.push(...validateQueryScope(args[0], scope, doc));
|
|
1418
|
+
const expression = args[0].queryNode.expression;
|
|
1419
|
+
forEachComparison(expression, (cmp) => {
|
|
1420
|
+
if (cmp.right && "fieldRef" in cmp.right) errors.push({
|
|
1421
|
+
message: "@db.rel.filter compares two fields — only field-to-value conditions are supported",
|
|
1422
|
+
severity: 1,
|
|
1423
|
+
range: cmp.right.fieldRef.range
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1426
|
+
const junction = hasVia ? getAnnotationAlias(field, "db.rel.via") : void 0;
|
|
1427
|
+
if (junction) {
|
|
1428
|
+
const conjuncts = isQueryLogical(expression) && expression.operator === "and" ? expression.operands : [expression];
|
|
1429
|
+
for (const conjunct of conjuncts) {
|
|
1430
|
+
let junctionRef;
|
|
1431
|
+
let targetRef = false;
|
|
1432
|
+
forEachFieldRef(conjunct, (ref) => {
|
|
1433
|
+
if (ref.typeRef?.text === junction) junctionRef ??= ref.typeRef.range;
|
|
1434
|
+
else targetRef = true;
|
|
1435
|
+
});
|
|
1436
|
+
if (junctionRef && targetRef) errors.push({
|
|
1437
|
+
message: `@db.rel.filter has a condition reading both the junction '${junction}' and the related type — split it into separate top-level "and" conditions`,
|
|
1438
|
+
severity: 1,
|
|
1439
|
+
range: junctionRef
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1252
1443
|
return errors;
|
|
1253
1444
|
}
|
|
1254
1445
|
})
|
|
1255
1446
|
} };
|
|
1447
|
+
/** Every comparison of a query expression (through and / or / not). */
|
|
1448
|
+
function forEachComparison(expr, fn) {
|
|
1449
|
+
if (isQueryLogical(expr)) for (const operand of expr.operands) forEachComparison(operand, fn);
|
|
1450
|
+
else if (isQueryComparison(expr)) fn(expr);
|
|
1451
|
+
}
|
|
1256
1452
|
//#endregion
|
|
1257
1453
|
//#region src/plugin/annotations/search.ts
|
|
1258
1454
|
const dbSearchAnnotations = { search: {
|
|
@@ -1602,6 +1798,61 @@ const dbUnitAnnotations = { unit: {
|
|
|
1602
1798
|
} };
|
|
1603
1799
|
//#endregion
|
|
1604
1800
|
//#region src/plugin/annotations/view.ts
|
|
1801
|
+
/**
|
|
1802
|
+
* VJ6–VJ8 — the ordering of a first-row join: every key is a scalar field of
|
|
1803
|
+
* the join target (no object, array, `@db.json`, `@db.encrypted`, `@db.writeOnly` or
|
|
1804
|
+
* navigation field), no key repeats, and the target (through `@db.alias`)
|
|
1805
|
+
* declares exactly one `@meta.id` field — the anchor of the join's
|
|
1806
|
+
* correlated subquery.
|
|
1807
|
+
*/
|
|
1808
|
+
function validateJoinOrder(orderToken, scope, doc) {
|
|
1809
|
+
const target = scope.unqualifiedTarget;
|
|
1810
|
+
const errors = validateQueryScope(orderToken, scope, doc, `a first-row join orders by fields of its target '${target}'`);
|
|
1811
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1812
|
+
for (const item of orderToken.orderNode.items) {
|
|
1813
|
+
const { ref } = item;
|
|
1814
|
+
if (ref.typeRef && ref.typeRef.text !== target) continue;
|
|
1815
|
+
const path = ref.fieldRef.text;
|
|
1816
|
+
if (seen.has(path)) {
|
|
1817
|
+
errors.push({
|
|
1818
|
+
message: `Order key '${path}' appears more than once`,
|
|
1819
|
+
severity: 1,
|
|
1820
|
+
range: ref.fieldRef.range
|
|
1821
|
+
});
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
seen.add(path);
|
|
1825
|
+
const why = orderKeyProblem(doc, target, path.split("."));
|
|
1826
|
+
if (why) errors.push({
|
|
1827
|
+
message: `Order key '${path}' ${why} — order by a scalar field of '${target}'`,
|
|
1828
|
+
severity: 1,
|
|
1829
|
+
range: ref.fieldRef.range
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
const ids = getFieldsForType(doc, target).filter((p) => p.countAnnotations("meta.id") > 0);
|
|
1833
|
+
if (ids.length !== 1) errors.push({
|
|
1834
|
+
message: `A first-row join needs a target with exactly one @meta.id field — '${target}' has ${ids.length === 0 ? "none" : `${ids.length} (a composite key)`}`,
|
|
1835
|
+
severity: 1,
|
|
1836
|
+
range: orderToken.range
|
|
1837
|
+
});
|
|
1838
|
+
return errors;
|
|
1839
|
+
}
|
|
1840
|
+
/** Why `path` of `target` cannot order a first-row join, or `undefined` when it can. */
|
|
1841
|
+
function orderKeyProblem(doc, target, path) {
|
|
1842
|
+
for (let i = 1; i <= path.length; i++) {
|
|
1843
|
+
const step = doc.unwindType(target, path.slice(0, i));
|
|
1844
|
+
if (!step) return void 0;
|
|
1845
|
+
const node = step.node;
|
|
1846
|
+
if (node && isProp(node)) {
|
|
1847
|
+
if (node.countAnnotations("db.json") > 0) return "reads a @db.json field";
|
|
1848
|
+
if (node.countAnnotations("db.encrypted") > 0) return "is @db.encrypted";
|
|
1849
|
+
if (node.countAnnotations("db.writeOnly") > 0) return "is @db.writeOnly";
|
|
1850
|
+
if (node.countAnnotations("db.ignore") > 0) return "is @db.ignore'd";
|
|
1851
|
+
}
|
|
1852
|
+
if (isArray(step.def)) return "is an array";
|
|
1853
|
+
if (i === path.length && !isPrimitive(step.def)) return "is not a scalar";
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1605
1856
|
const dbViewAnnotations = { view: {
|
|
1606
1857
|
$self: new AnnotationSpec({
|
|
1607
1858
|
description: "Marks an interface as a **database view**. Optionally takes a view name argument.\n\n**Example:**\n```atscript\n@db.view \"active_premium_users\"\n@db.view.for User\nexport interface ActivePremiumUser { ... }\n```\n",
|
|
@@ -1653,7 +1904,7 @@ const dbViewAnnotations = { view: {
|
|
|
1653
1904
|
}
|
|
1654
1905
|
}),
|
|
1655
1906
|
joins: new AnnotationSpec({
|
|
1656
|
-
description: "Declares an explicit join for a view. Joins are INNER by default — pass `'left'` as the third argument to keep entry rows without a match (fields read from a left-joined table must be optional). A join condition may reference the entry table and joins declared before it (chained joins). The target is a `@db.table`, a `@db.view` (since 0.1.141), or a `@db.alias` type — the way to join one table twice or to self-join the entry table: every scope name (entry + joins) must be unique.\n\n**Example:**\n```atscript\n@db.view.for Order\n@db.view.joins Customer, `Customer.id = Order.customerId`\n@db.view.joins Region, `Region.id = Customer.regionId`, 'left'\nexport interface OrderRegion { ... }\n```\n",
|
|
1907
|
+
description: "Declares an explicit join for a view. Joins are INNER by default — pass `'left'` as the third argument to keep entry rows without a match (fields read from a left-joined table must be optional). A join condition may reference the entry table and joins declared before it (chained joins). The target is a `@db.table`, a `@db.view` (since 0.1.141), or a `@db.alias` type — the way to join one table twice or to self-join the entry table: every scope name (entry + joins) must be unique.\n\n**Example:**\n```atscript\n@db.view.for Order\n@db.view.joins Customer, `Customer.id = Order.customerId`\n@db.view.joins Region, `Region.id = Customer.regionId`, 'left'\nexport interface OrderRegion { ... }\n```\n\n**First-row join** (since 0.1.147): a 4th argument — an ordering of the target's fields (`` `raisedAt, id` ``, `` `severity desc` ``) — joins only the FIRST matching target row by that ordering (the target's primary key is appended as the final tie-break), so every field read through the join comes from that one row. NULL sorts first in `asc`.\n```atscript\n@db.view.joins OldestOpenIssue, `OldestOpenIssue.ticketId = Ticket.id and OldestOpenIssue.status = 'open'`, 'left', `raisedAt`\n```\n",
|
|
1657
1908
|
nodeType: ["interface"],
|
|
1658
1909
|
passedWhenReferred: false,
|
|
1659
1910
|
multiple: true,
|
|
@@ -1677,6 +1928,13 @@ const dbViewAnnotations = { view: {
|
|
|
1677
1928
|
optional: true,
|
|
1678
1929
|
description: "`\"inner\"` (default) drops entry rows without a match; `\"left\"` keeps them with NULLs.",
|
|
1679
1930
|
values: ["inner", "left"]
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
name: "order",
|
|
1934
|
+
type: "order",
|
|
1935
|
+
optional: true,
|
|
1936
|
+
description: "Makes this a first-row join: of the target rows matching the condition only the first by this ordering joins (`` `raisedAt, id` ``, `` `severity desc` ``). Keys are scalar fields of the join target; its primary key is appended as the final tie-break. NULL is the smallest value (first in `asc`, last in `desc`).",
|
|
1937
|
+
fieldScope: fieldScopes.joinOrder
|
|
1680
1938
|
}
|
|
1681
1939
|
],
|
|
1682
1940
|
validate(token, args, doc) {
|
|
@@ -1725,6 +1983,8 @@ const dbViewAnnotations = { view: {
|
|
|
1725
1983
|
}
|
|
1726
1984
|
const scope = join && viewJoinScope(owner, join, earlier);
|
|
1727
1985
|
if (args[1]?.queryNode && scope) errors.push(...validateQueryScope(args[1], scope, doc, "a join may reference the entry table and joins declared before it"));
|
|
1986
|
+
const order = join && args[3]?.orderNode ? joinOrderScope(join) : void 0;
|
|
1987
|
+
if (order) errors.push(...validateJoinOrder(args[3], order, doc));
|
|
1728
1988
|
return errors;
|
|
1729
1989
|
}
|
|
1730
1990
|
}),
|
|
@@ -1854,6 +2114,7 @@ const dbPlugin = (options) => ({
|
|
|
1854
2114
|
view: dbViewAnnotations.view,
|
|
1855
2115
|
alias: dbAliasAnnotations.alias,
|
|
1856
2116
|
agg: dbAggAnnotations.agg,
|
|
2117
|
+
compute: dbComputeAnnotations.compute,
|
|
1857
2118
|
search: dbSearchAnnotations.search,
|
|
1858
2119
|
amount: dbAmountAnnotations.amount,
|
|
1859
2120
|
unit: dbUnitAnnotations.unit
|
package/dist/rel.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_nested_writer = require("./nested-writer-
|
|
3
|
-
const require_relation_loader = require("./relation-loader-
|
|
2
|
+
const require_nested_writer = require("./nested-writer-SqtYJHF0.cjs");
|
|
3
|
+
const require_relation_loader = require("./relation-loader-BFhVuJ-B.cjs");
|
|
4
4
|
exports.applyPatchNestedTo = require_nested_writer.applyPatchNestedTo;
|
|
5
5
|
exports.batchInsertNestedFrom = require_nested_writer.batchInsertNestedFrom;
|
|
6
6
|
exports.batchInsertNestedTo = require_nested_writer.batchInsertNestedTo;
|
package/dist/rel.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fn as TGenericLogger, H as TDbForeignKey, Nt as TWriteTableResolver, Ot as TTableResolver, Rt as TableMetadata, Ut as BaseDbAdapter, X as TDbRelation, m as AtscriptDbWritable, p as AtscriptDbTableLike } from "./buckets-CjdPipCC.cjs";
|
|
2
2
|
import { t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.cjs";
|
|
3
|
-
import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-
|
|
3
|
+
import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-DH3LvwLV.cjs";
|
|
4
4
|
import { FilterExpr, WithRelation } from "@uniqu/core";
|
|
5
5
|
import { Validator } from "@atscript/typescript/utils";
|
|
6
6
|
|
package/dist/rel.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fn as TGenericLogger, H as TDbForeignKey, Nt as TWriteTableResolver, Ot as TTableResolver, Rt as TableMetadata, Ut as BaseDbAdapter, X as TDbRelation, m as AtscriptDbWritable, p as AtscriptDbTableLike } from "./buckets-DYopfd2Y.mjs";
|
|
2
2
|
import { t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.mjs";
|
|
3
|
-
import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-
|
|
3
|
+
import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-C2bLtE-t.mjs";
|
|
4
4
|
import { Validator } from "@atscript/typescript/utils";
|
|
5
5
|
import { FilterExpr, WithRelation } from "@uniqu/core";
|
|
6
6
|
|
package/dist/rel.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as loadRelationsImpl } from "./relation-loader-
|
|
2
|
-
import {
|
|
1
|
+
import { t as loadRelationsImpl } from "./relation-loader-C0qE8xgR.mjs";
|
|
2
|
+
import { I as findFKForRelation, L as findRemoteFK, R as resolveRelationTargetTable, a as batchPatchNestedFrom, c as batchReplaceNestedFrom, d as checkDepthOverflow, f as planNestedFromVia, h as validateBatch, i as batchInsertNestedVia, l as batchReplaceNestedTo, m as preValidateNestedFrom, n as batchInsertNestedFrom, o as batchPatchNestedTo, p as planPatchNestedTo, r as batchInsertNestedTo, s as batchPatchNestedVia, t as applyPatchNestedTo, u as batchReplaceNestedVia } from "./nested-writer-D35UsDBT.mjs";
|
|
3
3
|
export { applyPatchNestedTo, batchInsertNestedFrom, batchInsertNestedTo, batchInsertNestedVia, batchPatchNestedFrom, batchPatchNestedTo, batchPatchNestedVia, batchReplaceNestedFrom, batchReplaceNestedTo, batchReplaceNestedVia, checkDepthOverflow, findFKForRelation, findRemoteFK, loadRelationsImpl, planNestedFromVia, planPatchNestedTo, preValidateNestedFrom, resolveRelationTargetTable, validateBatch };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { H as TDbForeignKey, X as TDbRelation } from "./buckets-DYopfd2Y.mjs";
|
|
2
2
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
3
3
|
|
|
4
4
|
//#region src/rel/relation-helpers.d.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { H as TDbForeignKey, X as TDbRelation } from "./buckets-CjdPipCC.cjs";
|
|
2
2
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
3
3
|
|
|
4
4
|
//#region src/rel/relation-helpers.d.ts
|