@metaobjectsdev/metadata 0.16.0 → 0.17.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/attr/attr-constants.d.ts +2 -1
- package/dist/core/attr/attr-constants.d.ts.map +1 -1
- package/dist/core/attr/attr-constants.js +4 -0
- package/dist/core/attr/attr-constants.js.map +1 -1
- package/dist/core/attr/attr-definition.embedded.d.ts.map +1 -1
- package/dist/core/attr/attr-definition.embedded.js +6 -0
- package/dist/core/attr/attr-definition.embedded.js.map +1 -1
- package/dist/core/attr/meta-attr-expression.d.ts +62 -0
- package/dist/core/attr/meta-attr-expression.d.ts.map +1 -0
- package/dist/core/attr/meta-attr-expression.js +203 -0
- package/dist/core/attr/meta-attr-expression.js.map +1 -0
- package/dist/core/field/meta-field.d.ts +8 -0
- package/dist/core/field/meta-field.d.ts.map +1 -1
- package/dist/core/field/meta-field.js +12 -1
- package/dist/core/field/meta-field.js.map +1 -1
- package/dist/core/object/object-constants.d.ts +6 -0
- package/dist/core/object/object-constants.d.ts.map +1 -1
- package/dist/core/object/object-constants.js +9 -0
- package/dist/core/object/object-constants.js.map +1 -1
- package/dist/core/object/object-definition.embedded.d.ts.map +1 -1
- package/dist/core/object/object-definition.embedded.js +11 -1
- package/dist/core/object/object-definition.embedded.js.map +1 -1
- package/dist/core-types.d.ts +1 -0
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +5 -2
- package/dist/core-types.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +12 -1
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/validation-passes.d.ts +1 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +264 -13
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/persistence/db/db-definition.embedded.d.ts.map +1 -1
- package/dist/persistence/db/db-definition.embedded.js +16 -0
- package/dist/persistence/db/db-definition.embedded.js.map +1 -1
- package/dist/persistence/origin/meta-origin.d.ts +30 -3
- package/dist/persistence/origin/meta-origin.d.ts.map +1 -1
- package/dist/persistence/origin/meta-origin.js +44 -2
- package/dist/persistence/origin/meta-origin.js.map +1 -1
- package/dist/persistence/origin/origin-constants.d.ts +15 -1
- package/dist/persistence/origin/origin-constants.d.ts.map +1 -1
- package/dist/persistence/origin/origin-constants.js +40 -3
- package/dist/persistence/origin/origin-constants.js.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.d.ts.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.js +86 -8
- package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
- package/dist/persistence/source/meta-source.d.ts +12 -0
- package/dist/persistence/source/meta-source.d.ts.map +1 -1
- package/dist/persistence/source/meta-source.js +20 -1
- package/dist/persistence/source/meta-source.js.map +1 -1
- package/dist/persistence/source/source-constants.d.ts +9 -0
- package/dist/persistence/source/source-constants.d.ts.map +1 -1
- package/dist/persistence/source/source-constants.js +9 -0
- package/dist/persistence/source/source-constants.js.map +1 -1
- package/dist/persistence/source/validate-source-escapes.d.ts +9 -0
- package/dist/persistence/source/validate-source-escapes.d.ts.map +1 -0
- package/dist/persistence/source/validate-source-escapes.js +115 -0
- package/dist/persistence/source/validate-source-escapes.js.map +1 -0
- package/package.json +1 -1
- package/src/core/attr/attr-constants.ts +4 -0
- package/src/core/attr/attr-definition.embedded.ts +6 -0
- package/src/core/attr/meta-attr-expression.ts +230 -0
- package/src/core/field/meta-field.ts +13 -1
- package/src/core/object/object-constants.ts +10 -0
- package/src/core/object/object-definition.embedded.ts +11 -1
- package/src/core-types.ts +6 -1
- package/src/errors.ts +28 -0
- package/src/index.ts +2 -0
- package/src/loader/meta-data-loader.ts +14 -1
- package/src/loader/validation-passes.ts +370 -15
- package/src/persistence/db/db-definition.embedded.ts +16 -0
- package/src/persistence/origin/meta-origin.ts +55 -4
- package/src/persistence/origin/origin-constants.ts +45 -3
- package/src/persistence/origin/origin-definition.embedded.ts +86 -8
- package/src/persistence/source/meta-source.ts +23 -0
- package/src/persistence/source/source-constants.ts +10 -0
- package/src/persistence/source/validate-source-escapes.ts +158 -0
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
OBJECT_SUBTYPE_ENTITY,
|
|
50
50
|
OBJECT_SUBTYPE_VALUE,
|
|
51
51
|
OBJECT_SUBTYPE_PROJECTION,
|
|
52
|
+
OBJECT_PROJECTION_ATTR_FILTER,
|
|
52
53
|
} from "../core/object/object-constants.js";
|
|
53
54
|
import { MetaSource } from "../persistence/source/meta-source.js";
|
|
54
55
|
import {
|
|
@@ -72,6 +73,7 @@ import {
|
|
|
72
73
|
FIELD_SUBTYPE_ENUM,
|
|
73
74
|
FIELD_SUBTYPE_OBJECT,
|
|
74
75
|
FIELD_SUBTYPE_MAP,
|
|
76
|
+
FIELD_ATTR_REQUIRED,
|
|
75
77
|
FIELD_ATTR_VALUE_TYPE,
|
|
76
78
|
FIELD_SUBTYPE_STRING,
|
|
77
79
|
FIELD_SUBTYPE_DATE,
|
|
@@ -88,12 +90,31 @@ import {
|
|
|
88
90
|
import {
|
|
89
91
|
ORIGIN_SUBTYPE_PASSTHROUGH,
|
|
90
92
|
ORIGIN_SUBTYPE_AGGREGATE,
|
|
93
|
+
ORIGIN_SUBTYPE_COMPUTED,
|
|
94
|
+
ORIGIN_SUBTYPE_FIRST,
|
|
91
95
|
ORIGIN_PASSTHROUGH_ATTR_FROM,
|
|
92
96
|
ORIGIN_PASSTHROUGH_ATTR_VIA,
|
|
93
97
|
ORIGIN_PASSTHROUGH_ATTR_CONVERT,
|
|
98
|
+
ORIGIN_AGGREGATE_ATTR_AGG,
|
|
94
99
|
ORIGIN_AGGREGATE_ATTR_OF,
|
|
95
100
|
ORIGIN_AGGREGATE_ATTR_VIA,
|
|
101
|
+
ORIGIN_AGGREGATE_ATTR_FILTER,
|
|
102
|
+
ORIGIN_ATTR_DISTINCT,
|
|
103
|
+
ORIGIN_ATTR_ORDER_BY,
|
|
104
|
+
ORIGIN_COMPUTED_ATTR_EXPR,
|
|
105
|
+
ORIGIN_FIRST_ATTR_OF,
|
|
106
|
+
ORIGIN_FIRST_ATTR_VIA,
|
|
107
|
+
ORIGIN_FIRST_ATTR_FILTER,
|
|
108
|
+
AGG_ANY,
|
|
109
|
+
AGG_ALL,
|
|
110
|
+
AGG_COLLECT,
|
|
96
111
|
} from "../persistence/origin/origin-constants.js";
|
|
112
|
+
import {
|
|
113
|
+
inferExprType,
|
|
114
|
+
validateExprNode,
|
|
115
|
+
type ExprNode,
|
|
116
|
+
} from "../core/attr/meta-attr-expression.js";
|
|
117
|
+
import { SORT_ORDER_VALUES } from "../core/query/query-constants.js";
|
|
97
118
|
import {
|
|
98
119
|
RELATIONSHIP_ATTR_OBJECT_REF,
|
|
99
120
|
RELATIONSHIP_ATTR_CARDINALITY,
|
|
@@ -959,6 +980,49 @@ function _checkPassthroughType(
|
|
|
959
980
|
);
|
|
960
981
|
}
|
|
961
982
|
|
|
983
|
+
/**
|
|
984
|
+
* #195 — validate `@orderBy` keys ('field[:asc|desc]') resolve against the
|
|
985
|
+
* RELATED entity's effective fields (the entity reached via `@via`/`@of`), and
|
|
986
|
+
* that any direction suffix is `asc`/`desc`. Null placement is pinned (nulls-last)
|
|
987
|
+
* and carries no vocabulary. Shared by `@agg:collect` (element order) and
|
|
988
|
+
* `origin.first` (row selection). A missing related entity means a prior error
|
|
989
|
+
* already fired — skip silently.
|
|
990
|
+
*/
|
|
991
|
+
function _validateOrderByKeys(
|
|
992
|
+
orderBy: unknown,
|
|
993
|
+
relatedEntity: MetaData | undefined,
|
|
994
|
+
obj: MetaData,
|
|
995
|
+
fieldName: string,
|
|
996
|
+
label: string,
|
|
997
|
+
originSource: ErrorSource,
|
|
998
|
+
errors: ParseError[],
|
|
999
|
+
): void {
|
|
1000
|
+
if (!Array.isArray(orderBy) || relatedEntity === undefined) return;
|
|
1001
|
+
for (const raw of orderBy) {
|
|
1002
|
+
if (typeof raw !== "string") continue;
|
|
1003
|
+
const colonIdx = raw.indexOf(":");
|
|
1004
|
+
const key = colonIdx === -1 ? raw : raw.slice(0, colonIdx);
|
|
1005
|
+
const dir = colonIdx === -1 ? undefined : raw.slice(colonIdx + 1);
|
|
1006
|
+
// ADR-0039: resolving — an ordering key may target an inherited field.
|
|
1007
|
+
const target = relatedEntity.children().find((f) => f.type === TYPE_FIELD && f.name === key);
|
|
1008
|
+
if (target === undefined) {
|
|
1009
|
+
errors.push(
|
|
1010
|
+
new ParseError(
|
|
1011
|
+
`${label} on ${obj.name}.${fieldName}: @orderBy key "${raw}" — no such field "${key}" on ${relatedEntity.name}.`,
|
|
1012
|
+
{ code: "ERR_INVALID_ORIGIN", source: originSource },
|
|
1013
|
+
),
|
|
1014
|
+
);
|
|
1015
|
+
} else if (dir !== undefined && !(SORT_ORDER_VALUES as readonly string[]).includes(dir)) {
|
|
1016
|
+
errors.push(
|
|
1017
|
+
new ParseError(
|
|
1018
|
+
`${label} on ${obj.name}.${fieldName}: @orderBy key "${raw}" — direction must be one of ${SORT_ORDER_VALUES.join("|")}.`,
|
|
1019
|
+
{ code: "ERR_INVALID_ORIGIN", source: originSource },
|
|
1020
|
+
),
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
962
1026
|
export function validateOriginPaths(root: MetaData): ParseError[] {
|
|
963
1027
|
const errors: ParseError[] = [];
|
|
964
1028
|
// ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
|
|
@@ -1020,26 +1084,106 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
|
|
|
1020
1084
|
}
|
|
1021
1085
|
} else if (origin.subType === ORIGIN_SUBTYPE_AGGREGATE) {
|
|
1022
1086
|
// ADR-0039: own — origin.* never inherits (ADR-0029).
|
|
1087
|
+
const src = origin.source;
|
|
1088
|
+
const agg = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_AGG);
|
|
1023
1089
|
const of_ = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_OF);
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1090
|
+
const ofPresent = typeof of_ === "string" && of_ !== "";
|
|
1091
|
+
const hasFilter = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_FILTER) !== undefined;
|
|
1092
|
+
const hasDistinct = origin.ownAttr(ORIGIN_ATTR_DISTINCT) !== undefined;
|
|
1093
|
+
const orderBy = origin.ownAttr(ORIGIN_ATTR_ORDER_BY);
|
|
1094
|
+
const hasOrderBy = orderBy !== undefined;
|
|
1095
|
+
const isPredicate = agg === AGG_ANY || agg === AGG_ALL;
|
|
1096
|
+
const isCollect = agg === AGG_COLLECT;
|
|
1097
|
+
|
|
1098
|
+
// --- #195 field-shape rules ---
|
|
1099
|
+
// collect ⇒ the carrying field is an array (it produces a list); every
|
|
1100
|
+
// other @agg reduces to a scalar (the inverse rule closes a latent hole).
|
|
1101
|
+
if (isCollect && !field.resolvedIsArray()) {
|
|
1102
|
+
errors.push(new ParseError(
|
|
1103
|
+
`origin.aggregate @agg:collect on ${obj.name}.${field.name}: the carrying field must be isArray:true (collect produces a list).`,
|
|
1104
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1105
|
+
} else if (!isCollect && field.resolvedIsArray()) {
|
|
1106
|
+
errors.push(new ParseError(
|
|
1107
|
+
`origin.aggregate @agg:${String(agg)} on ${obj.name}.${field.name}: a non-collect aggregate reduces to a scalar — the field must be isArray:false.`,
|
|
1108
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1109
|
+
}
|
|
1110
|
+
// any/all yield a boolean.
|
|
1111
|
+
if (isPredicate && field.subType !== FIELD_SUBTYPE_BOOLEAN) {
|
|
1112
|
+
errors.push(new ParseError(
|
|
1113
|
+
`origin.aggregate @agg:${String(agg)} on ${obj.name}.${field.name}: a predicate quantifier yields a boolean — the field must be field.boolean.`,
|
|
1114
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// --- #195 attr-presence rules ---
|
|
1118
|
+
if (hasDistinct && !isCollect) {
|
|
1119
|
+
errors.push(new ParseError(
|
|
1120
|
+
`origin.aggregate on ${obj.name}.${field.name}: @distinct is valid only on @agg:collect.`,
|
|
1121
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1122
|
+
}
|
|
1123
|
+
if (hasOrderBy && !isCollect) {
|
|
1124
|
+
errors.push(new ParseError(
|
|
1125
|
+
`origin.aggregate on ${obj.name}.${field.name}: @orderBy is valid only on @agg:collect.`,
|
|
1126
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1127
|
+
}
|
|
1128
|
+
if (isCollect && hasDistinct && hasOrderBy) {
|
|
1129
|
+
errors.push(new ParseError(
|
|
1130
|
+
`origin.aggregate @agg:collect on ${obj.name}.${field.name}: @orderBy and @distinct are mutually exclusive — a distinct collect uses value-ascending order (explicit element order is meaningful only without dedupe).`,
|
|
1131
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
if (isPredicate) {
|
|
1135
|
+
// --- any/all: @filter REQUIRED, @of FORBIDDEN, @via REQUIRED (no @of
|
|
1136
|
+
// to infer the path from) + must be to-many. ---
|
|
1137
|
+
if (!hasFilter) {
|
|
1138
|
+
errors.push(new ParseError(
|
|
1139
|
+
`origin.aggregate @agg:${String(agg)} on ${obj.name}.${field.name}: a predicate quantifier requires @filter (the quantified predicate); "does any related row exist" is @agg:count.`,
|
|
1140
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1141
|
+
}
|
|
1142
|
+
if (ofPresent) {
|
|
1143
|
+
errors.push(new ParseError(
|
|
1144
|
+
`origin.aggregate @agg:${String(agg)} on ${obj.name}.${field.name}: @of is forbidden — a quantifier ranges over rows, not a column (the predicate is @filter).`,
|
|
1145
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1146
|
+
}
|
|
1147
|
+
const via = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_VIA);
|
|
1148
|
+
if (typeof via !== "string" || via === "") {
|
|
1149
|
+
errors.push(new ParseError(
|
|
1150
|
+
`origin.aggregate @agg:${String(agg)} on ${obj.name}.${field.name}: requires an explicit @via (a quantifier has no @of to infer the path from).`,
|
|
1151
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1152
|
+
} else {
|
|
1153
|
+
const hops = _validateViaPath(via, root, obj, field.name, src, errors);
|
|
1154
|
+
if (hops !== undefined) _checkAggregateCardinality(hops, obj, field.name, src, errors);
|
|
1155
|
+
}
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// --- count/sum/avg/min/max/collect: @of REQUIRED ---
|
|
1160
|
+
if (!ofPresent) {
|
|
1161
|
+
errors.push(new ParseError(
|
|
1162
|
+
`origin.aggregate on ${obj.name}.${field.name}: missing @of.`,
|
|
1163
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1031
1164
|
continue;
|
|
1032
1165
|
}
|
|
1033
1166
|
// NOTE (FR-024 B6): NO extends/origin agreement check on aggregates —
|
|
1034
1167
|
// an aggregate computes something new (count/sum/…); spec §4 defines
|
|
1035
1168
|
// agreement for passthrough only.
|
|
1036
|
-
const ofTarget = _validateFromPath(of_, root, obj, field.name,
|
|
1169
|
+
const ofTarget = _validateFromPath(of_, root, obj, field.name, src, errors, "origin.aggregate.@of");
|
|
1170
|
+
// #195 — collect preserves the element type: the array field's own subType
|
|
1171
|
+
// must equal the @of column's subType (the #185 doctrine on the element).
|
|
1172
|
+
if (isCollect && ofTarget !== undefined && field.subType !== ofTarget.field.subType) {
|
|
1173
|
+
errors.push(new ParseError(
|
|
1174
|
+
`origin.aggregate @agg:collect on ${obj.name}.${field.name}: field element type field.${field.subType} does not match the @of column type field.${ofTarget.field.subType} — collect preserves the element type.`,
|
|
1175
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1176
|
+
}
|
|
1177
|
+
// @orderBy keys (collect only, non-distinct) resolve against the @of entity.
|
|
1178
|
+
if (isCollect && hasOrderBy && !hasDistinct) {
|
|
1179
|
+
_validateOrderByKeys(orderBy, ofTarget?.entity, obj, field.name, "origin.aggregate @agg:collect", src, errors);
|
|
1180
|
+
}
|
|
1037
1181
|
// ADR-0039: own — origin.* never inherits (ADR-0029).
|
|
1038
1182
|
const via = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_VIA);
|
|
1039
1183
|
if (typeof via === "string" && via !== "") {
|
|
1040
|
-
const hops = _validateViaPath(via, root, obj, field.name,
|
|
1184
|
+
const hops = _validateViaPath(via, root, obj, field.name, src, errors);
|
|
1041
1185
|
if (hops !== undefined) {
|
|
1042
|
-
_checkAggregateCardinality(hops, obj, field.name,
|
|
1186
|
+
_checkAggregateCardinality(hops, obj, field.name, src, errors);
|
|
1043
1187
|
}
|
|
1044
1188
|
continue;
|
|
1045
1189
|
}
|
|
@@ -1051,29 +1195,110 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
|
|
|
1051
1195
|
errors.push(
|
|
1052
1196
|
new ParseError(
|
|
1053
1197
|
`origin.aggregate on ${obj.name}.${field.name}: missing @via (aggregates require a relationship path).`,
|
|
1054
|
-
{ code: "ERR_INVALID_ORIGIN", source:
|
|
1198
|
+
{ code: "ERR_INVALID_ORIGIN", source: src },
|
|
1055
1199
|
),
|
|
1056
1200
|
);
|
|
1057
1201
|
continue;
|
|
1058
1202
|
}
|
|
1059
|
-
const base = _deriveBaseEntity(obj, root, field.name,
|
|
1203
|
+
const base = _deriveBaseEntity(obj, root, field.name, src, errors);
|
|
1060
1204
|
if (base === undefined) continue; // base underivable — error already pushed
|
|
1061
1205
|
if (_isBaseRelationTarget(ofTarget.entity, base, obj)) {
|
|
1062
1206
|
errors.push(
|
|
1063
1207
|
new ParseError(
|
|
1064
1208
|
`origin.aggregate on ${obj.name}.${field.name}: missing @via (aggregates require a relationship path).`,
|
|
1065
|
-
{ code: "ERR_INVALID_ORIGIN", source:
|
|
1209
|
+
{ code: "ERR_INVALID_ORIGIN", source: src },
|
|
1066
1210
|
),
|
|
1067
1211
|
);
|
|
1068
1212
|
continue;
|
|
1069
1213
|
}
|
|
1070
1214
|
const hops = _inferViaSingleHop(
|
|
1071
1215
|
base, ofTarget.entity, obj, field.name, of_,
|
|
1072
|
-
"origin.aggregate.@of",
|
|
1216
|
+
"origin.aggregate.@of", src, errors,
|
|
1073
1217
|
);
|
|
1074
1218
|
if (hops !== undefined) {
|
|
1075
|
-
_checkAggregateCardinality(hops, obj, field.name,
|
|
1219
|
+
_checkAggregateCardinality(hops, obj, field.name, src, errors);
|
|
1220
|
+
}
|
|
1221
|
+
} else if (origin.subType === ORIGIN_SUBTYPE_COMPUTED) {
|
|
1222
|
+
// #195 — a row-level expression over the base entity's OWN fields. No
|
|
1223
|
+
// @via/@of (strict scoping already rejects them as ERR_UNKNOWN_ATTR).
|
|
1224
|
+
const src = origin.source;
|
|
1225
|
+
const expr = origin.ownAttr(ORIGIN_COMPUTED_ATTR_EXPR);
|
|
1226
|
+
if (typeof expr !== "object" || expr === null) continue; // schema requires @expr (ERR_MISSING_REQUIRED_ATTR)
|
|
1227
|
+
// Structural grammar (fail-closed unknown node) is validated HERE, not in
|
|
1228
|
+
// the attr class, so every port validates the closed grammar identically
|
|
1229
|
+
// (the other ports store @expr verbatim; C#/Python/Java mirror this pass).
|
|
1230
|
+
const structural = validateExprNode(expr as ExprNode);
|
|
1231
|
+
if (structural.length > 0) {
|
|
1232
|
+
for (const m of structural) {
|
|
1233
|
+
errors.push(new ParseError(
|
|
1234
|
+
`origin.computed on ${obj.name}.${field.name}: ${m}`,
|
|
1235
|
+
{ code: "ERR_UNKNOWN_EXPR_NODE", source: src }));
|
|
1236
|
+
}
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
// Type inference against the base entity's EFFECTIVE fields (ADR-0039).
|
|
1240
|
+
const base = _deriveBaseEntity(obj, root, field.name, src, errors);
|
|
1241
|
+
if (base === undefined) continue;
|
|
1242
|
+
const resolveField = (name: string): string | undefined =>
|
|
1243
|
+
base.children().find((f) => f.type === TYPE_FIELD && f.name === name)?.subType;
|
|
1244
|
+
const inferred = inferExprType(expr as ExprNode, resolveField);
|
|
1245
|
+
if (inferred.errors.length > 0) {
|
|
1246
|
+
for (const m of inferred.errors) {
|
|
1247
|
+
errors.push(new ParseError(
|
|
1248
|
+
`origin.computed on ${obj.name}.${field.name}: ${m}`,
|
|
1249
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1250
|
+
}
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
if (inferred.type !== undefined && inferred.type !== field.subType) {
|
|
1254
|
+
errors.push(new ParseError(
|
|
1255
|
+
`origin.computed on ${obj.name}.${field.name}: @expr infers field.${inferred.type} but the field is declared field.${field.subType} — a computed column's type is derived from its expression and must match (no @convert escape).`,
|
|
1256
|
+
{ code: "ERR_COMPUTED_TYPE_MISMATCH", source: src }));
|
|
1076
1257
|
}
|
|
1258
|
+
} else if (origin.subType === ORIGIN_SUBTYPE_FIRST) {
|
|
1259
|
+
// #195 — pick one related row by @orderBy along @via, project @of.
|
|
1260
|
+
const src = origin.source;
|
|
1261
|
+
const of_ = origin.ownAttr(ORIGIN_FIRST_ATTR_OF);
|
|
1262
|
+
const ofPresent = typeof of_ === "string" && of_ !== "";
|
|
1263
|
+
if (!ofPresent) {
|
|
1264
|
+
errors.push(new ParseError(
|
|
1265
|
+
`origin.first on ${obj.name}.${field.name}: missing @of.`,
|
|
1266
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
// The carrying field must NOT be @required — an empty related set (after
|
|
1270
|
+
// @filter) selects no row, so the value is null. ADR-0039: resolving.
|
|
1271
|
+
if (field.attr(FIELD_ATTR_REQUIRED) === true) {
|
|
1272
|
+
errors.push(new ParseError(
|
|
1273
|
+
`origin.first on ${obj.name}.${field.name}: the field must not be @required — an empty related set (after @filter) yields null.`,
|
|
1274
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1275
|
+
}
|
|
1276
|
+
const ofTarget = _validateFromPath(of_, root, obj, field.name, src, errors, "origin.first.@of");
|
|
1277
|
+
// #185 type-preservation: first projects the @of column unchanged, so the
|
|
1278
|
+
// field's subType must equal the @of column's subType (first is scalar).
|
|
1279
|
+
if (ofTarget !== undefined && field.subType !== ofTarget.field.subType) {
|
|
1280
|
+
errors.push(new ParseError(
|
|
1281
|
+
`origin.first on ${obj.name}.${field.name}: field field.${field.subType} does not match the @of column field.${ofTarget.field.subType} — first projects the column unchanged, so the types must match.`,
|
|
1282
|
+
{ code: "ERR_INVALID_ORIGIN", source: src }));
|
|
1283
|
+
}
|
|
1284
|
+
// @via — explicit (validated + cardinality) or single-hop-unique inferred.
|
|
1285
|
+
const via = origin.ownAttr(ORIGIN_FIRST_ATTR_VIA);
|
|
1286
|
+
if (typeof via === "string" && via !== "") {
|
|
1287
|
+
const hops = _validateViaPath(via, root, obj, field.name, src, errors);
|
|
1288
|
+
if (hops !== undefined) _checkAggregateCardinality(hops, obj, field.name, src, errors);
|
|
1289
|
+
} else if (ofTarget !== undefined && !isValueHost) {
|
|
1290
|
+
const base = _deriveBaseEntity(obj, root, field.name, src, errors);
|
|
1291
|
+
if (base !== undefined && !_isBaseRelationTarget(ofTarget.entity, base, obj)) {
|
|
1292
|
+
const hops = _inferViaSingleHop(
|
|
1293
|
+
base, ofTarget.entity, obj, field.name, of_,
|
|
1294
|
+
"origin.first.@of", src, errors);
|
|
1295
|
+
if (hops !== undefined) _checkAggregateCardinality(hops, obj, field.name, src, errors);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
// @orderBy keys resolve against the related (@of) entity.
|
|
1299
|
+
_validateOrderByKeys(
|
|
1300
|
+
origin.ownAttr(ORIGIN_ATTR_ORDER_BY), ofTarget?.entity,
|
|
1301
|
+
obj, field.name, "origin.first", src, errors);
|
|
1077
1302
|
}
|
|
1078
1303
|
}
|
|
1079
1304
|
}
|
|
@@ -1658,3 +1883,133 @@ function checkFilterClauses(
|
|
|
1658
1883
|
}
|
|
1659
1884
|
}
|
|
1660
1885
|
}
|
|
1886
|
+
|
|
1887
|
+
// ---------------------------------------------------------------------------
|
|
1888
|
+
// #207 — projection row-scope @filter (view-level WHERE) reference validation
|
|
1889
|
+
//
|
|
1890
|
+
// A projection's @filter (a portable attr.filter object) scopes which rows the
|
|
1891
|
+
// derived view returns. Its field refs must name the projection's OWN declared
|
|
1892
|
+
// fields, and each must be ADDRESSABLE in a WHERE:
|
|
1893
|
+
// - a plain (extends-bound / no-origin) or origin.passthrough or origin.computed
|
|
1894
|
+
// field → addressable (a base/joined column, or an inlined row-level expression).
|
|
1895
|
+
// - an aggregate-derived field (origin.aggregate — count/sum/…/any/all/collect —
|
|
1896
|
+
// or origin.first / origin.collection) → NOT addressable: a WHERE runs before
|
|
1897
|
+
// aggregation, so it cannot see an aggregate (post-aggregate filtering is HAVING,
|
|
1898
|
+
// a separate later extension). Fail-closed → ERR_BAD_ATTR_FILTER.
|
|
1899
|
+
// - a ref naming no declared field → dangling → ERR_BAD_ATTR_FILTER.
|
|
1900
|
+
//
|
|
1901
|
+
// Own-attrs only: the @filter is declared locally, and is registered on
|
|
1902
|
+
// object.projection alone — a write-through entity read-view (object.entity,
|
|
1903
|
+
// isWriteThrough) can never carry one, so filtered replicas (which would break
|
|
1904
|
+
// read-your-writes totality) are excluded by construction (v1 scope).
|
|
1905
|
+
// ---------------------------------------------------------------------------
|
|
1906
|
+
|
|
1907
|
+
/** A projection field addressable by a @filter: its aggregate-derived-ness + its
|
|
1908
|
+
* op-band (by declared subType). Mirrors the dataGrid allowlist shape. */
|
|
1909
|
+
interface ProjectionFilterField {
|
|
1910
|
+
readonly derived: boolean;
|
|
1911
|
+
readonly ops: readonly string[];
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
export function validateProjectionFilter(root: MetaData): ParseError[] {
|
|
1915
|
+
const errors: ParseError[] = [];
|
|
1916
|
+
// ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
|
|
1917
|
+
for (const obj of root
|
|
1918
|
+
.children()
|
|
1919
|
+
.filter((c) => c.type === TYPE_OBJECT && c.subType === OBJECT_SUBTYPE_PROJECTION)) {
|
|
1920
|
+
// ADR-0039: own — the @filter is declared locally on this projection.
|
|
1921
|
+
const filter = obj.ownAttr(OBJECT_PROJECTION_ATTR_FILTER);
|
|
1922
|
+
// Non-object shapes are rejected by the attr schema check (FilterAttr.validateValue).
|
|
1923
|
+
if (typeof filter !== "object" || filter === null || Array.isArray(filter)) continue;
|
|
1924
|
+
|
|
1925
|
+
// Classify the projection's OWN fields (the declared set IS the exposure —
|
|
1926
|
+
// FR-024/ADR-0028): aggregate-derived-ness + the op-band legal for the field's
|
|
1927
|
+
// declared subType. origin.* never inherits (ADR-0029), so the origin reads
|
|
1928
|
+
// below are own (category 4).
|
|
1929
|
+
const fields = new Map<string, ProjectionFilterField>();
|
|
1930
|
+
for (const f of obj.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
|
|
1931
|
+
const origin = f.ownChildren().find((c) => c.type === TYPE_ORIGIN);
|
|
1932
|
+
const derived =
|
|
1933
|
+
origin !== undefined &&
|
|
1934
|
+
origin.subType !== ORIGIN_SUBTYPE_PASSTHROUGH &&
|
|
1935
|
+
origin.subType !== ORIGIN_SUBTYPE_COMPUTED;
|
|
1936
|
+
fields.set(f.name, { derived, ops: opsForSubType(f.subType) });
|
|
1937
|
+
}
|
|
1938
|
+
checkProjectionFilterRefs(filter as Record<string, unknown>, fields, obj.name, obj.source, errors);
|
|
1939
|
+
}
|
|
1940
|
+
return errors;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// Validates a projection @filter fail-closed at load — mirrors the dataGrid sibling
|
|
1944
|
+
// checkFilterClauses (which validates both field refs AND ops). A malformation that
|
|
1945
|
+
// slips through here lowers to a silently-wrong or no WHERE, or crashes `meta migrate`
|
|
1946
|
+
// / emits invalid CREATE VIEW SQL, so every branch that isn't a valid clause errors.
|
|
1947
|
+
function checkProjectionFilterRefs(
|
|
1948
|
+
filter: Record<string, unknown>,
|
|
1949
|
+
fields: ReadonlyMap<string, ProjectionFilterField>,
|
|
1950
|
+
projectionName: string,
|
|
1951
|
+
source: ErrorSource,
|
|
1952
|
+
errors: ParseError[],
|
|
1953
|
+
): void {
|
|
1954
|
+
const err = (message: string): void => {
|
|
1955
|
+
errors.push(new ParseError(message, { code: "ERR_BAD_ATTR_FILTER", source }));
|
|
1956
|
+
};
|
|
1957
|
+
for (const [key, clause] of Object.entries(filter)) {
|
|
1958
|
+
if (key === FILTER_COMPOSE_OR || key === FILTER_COMPOSE_AND) {
|
|
1959
|
+
// Fail-closed: a compose key MUST hold an ARRAY of sub-clause objects. A non-array
|
|
1960
|
+
// (a common object-vs-array authoring slip, e.g. `and: { … }`) would otherwise
|
|
1961
|
+
// collapse to no WHERE at lowering — a soft-delete filter silently returning every
|
|
1962
|
+
// row. Reject it, and any non-object element, here.
|
|
1963
|
+
if (!Array.isArray(clause)) {
|
|
1964
|
+
err(`projection "${projectionName}" @filter "${key}" must be an array of sub-clauses.`);
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
for (const sub of clause) {
|
|
1968
|
+
if (typeof sub === "object" && sub !== null && !Array.isArray(sub)) {
|
|
1969
|
+
checkProjectionFilterRefs(sub as Record<string, unknown>, fields, projectionName, source, errors);
|
|
1970
|
+
} else {
|
|
1971
|
+
err(`projection "${projectionName}" @filter "${key}" contains a non-object sub-clause.`);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
const field = fields.get(key);
|
|
1977
|
+
if (field === undefined) {
|
|
1978
|
+
err(
|
|
1979
|
+
`projection "${projectionName}" @filter references "${key}", which is not a declared field of the ` +
|
|
1980
|
+
`projection. A view-level @filter may only reference the projection's own declared fields.`,
|
|
1981
|
+
);
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
if (field.derived) {
|
|
1985
|
+
err(
|
|
1986
|
+
`projection "${projectionName}" @filter references "${key}", an aggregate-derived field. A view-level ` +
|
|
1987
|
+
`WHERE runs before aggregation, so it cannot filter on an aggregate (post-aggregate filtering is a ` +
|
|
1988
|
+
`separate HAVING extension). Filter on a passthrough or computed field instead.`,
|
|
1989
|
+
);
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
// After parse-time desugaring (FilterAttr.desugar) every field clause is a canonical
|
|
1993
|
+
// { op: value } object; a non-object or empty op-set would silently drop the predicate.
|
|
1994
|
+
if (typeof clause !== "object" || clause === null || Array.isArray(clause)) {
|
|
1995
|
+
err(`projection "${projectionName}" @filter on "${key}" must be an { op: value } object.`);
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
const ops = Object.keys(clause as Record<string, unknown>);
|
|
1999
|
+
if (ops.length === 0) {
|
|
2000
|
+
err(`projection "${projectionName}" @filter on "${key}" declares no operator.`);
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
// Every op must be legal for the field's subType — an unknown op (e.g. a typo
|
|
2004
|
+
// `contains`) or a subtype-illegal op (e.g. `like` on a boolean) would otherwise
|
|
2005
|
+
// crash the view synthesizer or emit invalid SQL at CREATE VIEW.
|
|
2006
|
+
for (const op of ops) {
|
|
2007
|
+
if (!field.ops.includes(op)) {
|
|
2008
|
+
err(
|
|
2009
|
+
`projection "${projectionName}" @filter on "${key}" uses op "${op}", which is not allowed for its ` +
|
|
2010
|
+
`type. Allowed ops: ${field.ops.join(", ") || "(none)"}.`,
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
@@ -181,6 +181,22 @@ export const DB_DEFINITION: ProviderDefinition = {
|
|
|
181
181
|
"max": 1,
|
|
182
182
|
"description": "Optional database schema name (e.g. 'catalog', 'public'). Postgres defaults to 'public'; SQLite rejects any non-default value."
|
|
183
183
|
},
|
|
184
|
+
{
|
|
185
|
+
"type": "attr",
|
|
186
|
+
"subType": "string",
|
|
187
|
+
"name": "sql",
|
|
188
|
+
"min": 0,
|
|
189
|
+
"max": 1,
|
|
190
|
+
"description": "FR-024/#208 escape valve — a hand-written SQL body the tool REGISTERS + fingerprints + drift-checks but never authors or parses. The body goes INSIDE `CREATE <kind> <physicalName> AS …` (never the CREATE wrapper, never the object name). Legal only on a read-only kind (not @kind: table); migrate lowers it on @kind: view (matview/proc/tableFunction: registered but not yet migrate-managed). Mutually exclusive with @unmanaged; forbids origin.* children (two sources of truth)."
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"type": "attr",
|
|
194
|
+
"subType": "boolean",
|
|
195
|
+
"name": "unmanaged",
|
|
196
|
+
"min": 0,
|
|
197
|
+
"max": 1,
|
|
198
|
+
"description": "FR-024/#208 escape valve — this DB object is managed elsewhere (Flyway / a hand-migration owns its DDL). meta migrate does NOT create, drop, or drift-check it; verify --db reports it as external (declared). Legal on any @kind including table (the externally-managed-entity case). Mutually exclusive with @sql."
|
|
199
|
+
},
|
|
184
200
|
{
|
|
185
201
|
"type": "attr",
|
|
186
202
|
"subType": "string",
|
|
@@ -15,8 +15,13 @@ import {
|
|
|
15
15
|
ORIGIN_AGGREGATE_ATTR_OF,
|
|
16
16
|
ORIGIN_AGGREGATE_ATTR_VIA,
|
|
17
17
|
ORIGIN_COLLECTION_ATTR_VIA,
|
|
18
|
-
|
|
18
|
+
ORIGIN_COMPUTED_ATTR_EXPR,
|
|
19
|
+
ORIGIN_FIRST_ATTR_OF,
|
|
20
|
+
ORIGIN_FIRST_ATTR_VIA,
|
|
21
|
+
ORIGIN_ATTR_ORDER_BY,
|
|
22
|
+
type OriginAggValue,
|
|
19
23
|
} from "./origin-constants.js";
|
|
24
|
+
import type { ExprNode } from "../../core/attr/meta-attr-expression.js";
|
|
20
25
|
|
|
21
26
|
export class MetaOrigin extends MetaData {}
|
|
22
27
|
|
|
@@ -50,11 +55,11 @@ export class MetaPassthroughOrigin extends MetaOrigin {
|
|
|
50
55
|
* Carries `@agg`, `@of`, and `@via` (all required).
|
|
51
56
|
*/
|
|
52
57
|
export class MetaAggregateOrigin extends MetaOrigin {
|
|
53
|
-
/** The
|
|
54
|
-
get agg():
|
|
58
|
+
/** The reducing function (count | sum | avg | min | max | any | all | collect). */
|
|
59
|
+
get agg(): OriginAggValue | undefined {
|
|
55
60
|
// ADR-0039: own — origin.* never inherits (ADR-0029), so own is correct.
|
|
56
61
|
const v = this.ownAttr(ORIGIN_AGGREGATE_ATTR_AGG);
|
|
57
|
-
return typeof v === "string" ? (v as
|
|
62
|
+
return typeof v === "string" ? (v as OriginAggValue) : undefined;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
/** The dotted-path target of the aggregate (e.g. "Week.id"). */
|
|
@@ -86,3 +91,49 @@ export class MetaCollectionOrigin extends MetaOrigin {
|
|
|
86
91
|
return typeof v === "string" ? v : undefined;
|
|
87
92
|
}
|
|
88
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Computed origin (#195) — a row-level value computed from the base entity's own
|
|
97
|
+
* fields via a structured expression tree (`@expr`). No related rows, no `@via`.
|
|
98
|
+
* The expression's inferred type must equal the carrying field's declared subType
|
|
99
|
+
* (ERR_COMPUTED_TYPE_MISMATCH).
|
|
100
|
+
*/
|
|
101
|
+
export class MetaComputedOrigin extends MetaOrigin {
|
|
102
|
+
/** The structured expression tree computing this field's value. */
|
|
103
|
+
get expr(): ExprNode | undefined {
|
|
104
|
+
// ADR-0039: own — origin.* never inherits (ADR-0029), so own is correct.
|
|
105
|
+
const v = this.ownAttr(ORIGIN_COMPUTED_ATTR_EXPR);
|
|
106
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? (v as ExprNode) : undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* First origin (#195) — the single related row selected by `@orderBy` along
|
|
112
|
+
* `@via`, projecting its `@of` column (argmax-then-project). Empty related set
|
|
113
|
+
* (after `@filter`) → null, so the carrying field must not be `@required`.
|
|
114
|
+
*
|
|
115
|
+
* Carries `@of` (required, type-preserving), `@via` (optional; single-hop-unique
|
|
116
|
+
* inference), `@orderBy` (required), and an optional `@filter`.
|
|
117
|
+
*/
|
|
118
|
+
export class MetaFirstOrigin extends MetaOrigin {
|
|
119
|
+
/** The dotted Entity.field reference projected from the selected row (e.g. "ChildA.label"). */
|
|
120
|
+
get of(): string | undefined {
|
|
121
|
+
// ADR-0039: own — origin.* never inherits (ADR-0029), so own is correct.
|
|
122
|
+
const v = this.ownAttr(ORIGIN_FIRST_ATTR_OF);
|
|
123
|
+
return typeof v === "string" ? v : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The dotted relationship path to the related rows (e.g. "Parent.childAs"). */
|
|
127
|
+
get via(): string | undefined {
|
|
128
|
+
// ADR-0039: own — origin.* never inherits (ADR-0029), so own is correct.
|
|
129
|
+
const v = this.ownAttr(ORIGIN_FIRST_ATTR_VIA);
|
|
130
|
+
return typeof v === "string" ? v : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The ordering keys ('field[:asc|desc]') selecting the single row. */
|
|
134
|
+
get orderBy(): string[] | undefined {
|
|
135
|
+
// ADR-0039: own — origin.* never inherits (ADR-0029), so own is correct.
|
|
136
|
+
const v = this.ownAttr(ORIGIN_ATTR_ORDER_BY);
|
|
137
|
+
return Array.isArray(v) ? (v as string[]) : undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -14,12 +14,19 @@ import { SUBTYPE_BASE } from "../../shared/base-types.js";
|
|
|
14
14
|
export const ORIGIN_SUBTYPE_PASSTHROUGH = "passthrough";
|
|
15
15
|
export const ORIGIN_SUBTYPE_AGGREGATE = "aggregate";
|
|
16
16
|
export const ORIGIN_SUBTYPE_COLLECTION = "collection";
|
|
17
|
+
// #195 — computed: a row-level value from the base entity's own fields via a
|
|
18
|
+
// structured @expr tree (no related rows). first: the single related row picked
|
|
19
|
+
// by @orderBy along @via, projecting @of (argmax-then-project).
|
|
20
|
+
export const ORIGIN_SUBTYPE_COMPUTED = "computed";
|
|
21
|
+
export const ORIGIN_SUBTYPE_FIRST = "first";
|
|
17
22
|
|
|
18
23
|
export const ORIGIN_SUBTYPES = [
|
|
19
24
|
SUBTYPE_BASE,
|
|
20
25
|
ORIGIN_SUBTYPE_PASSTHROUGH,
|
|
21
26
|
ORIGIN_SUBTYPE_AGGREGATE,
|
|
22
27
|
ORIGIN_SUBTYPE_COLLECTION,
|
|
28
|
+
ORIGIN_SUBTYPE_COMPUTED,
|
|
29
|
+
ORIGIN_SUBTYPE_FIRST,
|
|
23
30
|
] as const;
|
|
24
31
|
export type OriginSubType = (typeof ORIGIN_SUBTYPES)[number];
|
|
25
32
|
|
|
@@ -28,7 +35,7 @@ export const ORIGIN_PASSTHROUGH_ATTR_FROM = "from";
|
|
|
28
35
|
export const ORIGIN_PASSTHROUGH_ATTR_VIA = "via";
|
|
29
36
|
// @convert (boolean): acknowledges a deliberate type change vs the @from source
|
|
30
37
|
// (#185). Suppresses ERR_PASSTHROUGH_TYPE_MISMATCH. Acknowledgement only — it
|
|
31
|
-
// does NOT generate a cast; real type-converting projections are origin.
|
|
38
|
+
// does NOT generate a cast; real type-converting projections are origin.computed (#195/#159).
|
|
32
39
|
export const ORIGIN_PASSTHROUGH_ATTR_CONVERT = "convert";
|
|
33
40
|
|
|
34
41
|
// collection attrs — a relationship-derived array of nested view-objects
|
|
@@ -43,9 +50,44 @@ export const ORIGIN_AGGREGATE_ATTR_VIA = "via";
|
|
|
43
50
|
// @filter (attr.filter object): an optional PORTABLE structured predicate scoping
|
|
44
51
|
// which related rows the aggregate spans (same shape as a preset filter). Codegen
|
|
45
52
|
// renders it per target — the projection view emitter turns it into SQL
|
|
46
|
-
// FILTER (WHERE …) / SQLite CASE WHEN.
|
|
53
|
+
// FILTER (WHERE …) / SQLite CASE WHEN. On @agg:any|all it is the QUANTIFIED
|
|
54
|
+
// predicate (required there); on origin.first it scopes the eligible rows.
|
|
47
55
|
export const ORIGIN_AGGREGATE_ATTR_FILTER = "filter";
|
|
56
|
+
// #195 — shared origin attrs. @distinct (collect-only) applies set semantics;
|
|
57
|
+
// @orderBy carries ordering keys ('field[:asc|desc]', nulls-last) used by
|
|
58
|
+
// @agg:collect (element order) and required by origin.first (row selection).
|
|
59
|
+
export const ORIGIN_ATTR_DISTINCT = "distinct";
|
|
60
|
+
export const ORIGIN_ATTR_ORDER_BY = "orderBy";
|
|
48
61
|
|
|
49
|
-
//
|
|
62
|
+
// computed attrs — @expr is the structured expression tree (attr.expression).
|
|
63
|
+
export const ORIGIN_COMPUTED_ATTR_EXPR = "expr";
|
|
64
|
+
|
|
65
|
+
// first attrs — reuse the same physical names as aggregate (@of/@via/@filter)
|
|
66
|
+
// plus the required @orderBy above.
|
|
67
|
+
export const ORIGIN_FIRST_ATTR_OF = "of";
|
|
68
|
+
export const ORIGIN_FIRST_ATTR_VIA = "via";
|
|
69
|
+
export const ORIGIN_FIRST_ATTR_FILTER = "filter";
|
|
70
|
+
|
|
71
|
+
// aggregate function vocabulary.
|
|
72
|
+
//
|
|
73
|
+
// AGGREGATE_FUNCTIONS is the NUMERIC/ORDINAL reduce set — each maps directly to a
|
|
74
|
+
// SQL aggregate over the @of column and to the codegen `aggregate` SelectColumn
|
|
75
|
+
// kind. The predicate quantifiers (any/all) and the array rollup (collect) are
|
|
76
|
+
// distinct reducing behaviors handled by their own view-synthesis column kinds,
|
|
77
|
+
// so they are NOT in this set; the full @agg vocabulary is ORIGIN_AGG_VALUES.
|
|
50
78
|
export const AGGREGATE_FUNCTIONS = ["count", "sum", "avg", "min", "max"] as const;
|
|
51
79
|
export type AggregateFunction = (typeof AGGREGATE_FUNCTIONS)[number];
|
|
80
|
+
|
|
81
|
+
// #195 — the predicate-quantifier + array-rollup @agg values.
|
|
82
|
+
export const AGG_ANY = "any";
|
|
83
|
+
export const AGG_ALL = "all";
|
|
84
|
+
export const AGG_COLLECT = "collect";
|
|
85
|
+
|
|
86
|
+
/** The full @agg attribute vocabulary (matches origin.json allowedValues). */
|
|
87
|
+
export const ORIGIN_AGG_VALUES = [
|
|
88
|
+
...AGGREGATE_FUNCTIONS,
|
|
89
|
+
AGG_ANY,
|
|
90
|
+
AGG_ALL,
|
|
91
|
+
AGG_COLLECT,
|
|
92
|
+
] as const;
|
|
93
|
+
export type OriginAggValue = (typeof ORIGIN_AGG_VALUES)[number];
|