@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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Validation pass: DDL-ownership escape valves (#208) — @sql / @unmanaged on
|
|
2
|
+
// source.rdb. Sibling of validate-source-roles.ts / validate-source-physical-names.ts.
|
|
3
|
+
//
|
|
4
|
+
// #208 gives source.rdb two mutually exclusive, non-default DDL-ownership
|
|
5
|
+
// markers: @sql (a hand-written body the tool registers/fingerprints/drift-
|
|
6
|
+
// checks but never authors) and @unmanaged (this object's DDL is owned
|
|
7
|
+
// elsewhere — the tool never touches it). This pass enforces the six
|
|
8
|
+
// fail-closed rules from the design doc (§5):
|
|
9
|
+
//
|
|
10
|
+
// R1 @sql AND @unmanaged on the SAME source → ERR_SQL_BODY_WITH_UNMANAGED
|
|
11
|
+
// R2 @sql on a writable @kind ("table", the default) → ERR_SQL_BODY_ON_WRITABLE_KIND
|
|
12
|
+
// R3 @sql present but empty / whitespace-only → ERR_BAD_ATTR_VALUE
|
|
13
|
+
// R4 origin.*-bearing field under an @sql host → ERR_ORIGIN_UNDER_SQL_BODY
|
|
14
|
+
// R5 object.projection @filter (#207) + @sql host → ERR_ORIGIN_UNDER_SQL_BODY
|
|
15
|
+
// R6 origin.*-bearing field under an @unmanaged host → WARN_ORIGIN_UNDER_UNMANAGED
|
|
16
|
+
//
|
|
17
|
+
// R1–R3 are per-source (declaration-layer). R4–R6 are per-host-object: a host
|
|
18
|
+
// with ANY own source.rdb carrying @sql/@unmanaged is judged against its own
|
|
19
|
+
// fields (and, for R5, its own @filter). The asymmetry between R4 (hard error)
|
|
20
|
+
// and R6 (warn) is deliberate (design doc §5.6): @sql is a second body (two
|
|
21
|
+
// sources of truth for the SAME data); @unmanaged acts on nothing, so a
|
|
22
|
+
// documented-but-unacted-on lineage is benign.
|
|
23
|
+
import { ParseError } from "../../errors.js";
|
|
24
|
+
import { TYPE_OBJECT, TYPE_SOURCE, TYPE_FIELD } from "../../shared/base-types.js";
|
|
25
|
+
import { OBJECT_SUBTYPE_PROJECTION, OBJECT_PROJECTION_ATTR_FILTER } from "../../core/object/object-constants.js";
|
|
26
|
+
import { MetaSource } from "./meta-source.js";
|
|
27
|
+
import { MetaField } from "../../core/field/meta-field.js";
|
|
28
|
+
import { SOURCE_ATTR_SQL, SOURCE_SUBTYPE_RDB } from "./source-constants.js";
|
|
29
|
+
export function validateSourceEscapes(root) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
const warnings = [];
|
|
32
|
+
// ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
|
|
33
|
+
for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
|
|
34
|
+
// ADR-0039: own — declaration-layer source iteration (mirrors validateSourceRoles /
|
|
35
|
+
// validateSourceParameterRef): R1–R3 judge markers DECLARED on this object's own sources.
|
|
36
|
+
const sources = obj
|
|
37
|
+
.ownChildren()
|
|
38
|
+
.filter((c) => c.type === TYPE_SOURCE && c.subType === SOURCE_SUBTYPE_RDB && c instanceof MetaSource);
|
|
39
|
+
let hasSqlHost = false;
|
|
40
|
+
let hasUnmanagedHost = false;
|
|
41
|
+
for (const source of sources) {
|
|
42
|
+
// ADR-0039: resolving — @sql/@unmanaged are inheritable (follow the
|
|
43
|
+
// @role/effectiveKind precedent — sources are inheritable, NOT the
|
|
44
|
+
// @dbColumnType own-only exception).
|
|
45
|
+
const sqlSet = source.sqlBody !== undefined;
|
|
46
|
+
const unmanagedSet = source.isUnmanaged;
|
|
47
|
+
// R1 — contradictory DDL owners on the same source.
|
|
48
|
+
if (sqlSet && unmanagedSet) {
|
|
49
|
+
errors.push(new ParseError(`source.rdb on object "${obj.name}" declares both @sql and @unmanaged — these are the ` +
|
|
50
|
+
`mutually exclusive non-default states of one DDL-ownership axis (an author-supplied body ` +
|
|
51
|
+
`contradicts "someone else owns this DDL")`, { code: "ERR_SQL_BODY_WITH_UNMANAGED", source: source.source }));
|
|
52
|
+
}
|
|
53
|
+
// R2 — @sql on a writable kind would bypass the column-diff machinery;
|
|
54
|
+
// tables are fully modeled or @unmanaged, never opaque-bodied.
|
|
55
|
+
if (sqlSet && source.isWritable()) {
|
|
56
|
+
errors.push(new ParseError(`source.rdb on object "${obj.name}" declares @sql with a writable @kind ("${source.effectiveKind}") — ` +
|
|
57
|
+
`@sql is legal only on a read-only kind (view/materializedView/storedProc/tableFunction); ` +
|
|
58
|
+
`a writable table is either fully modeled or marked @unmanaged, never opaque-bodied`, { code: "ERR_SQL_BODY_ON_WRITABLE_KIND", source: source.source }));
|
|
59
|
+
}
|
|
60
|
+
// R3 — @sql present but empty/whitespace. MUST read the raw attr, not
|
|
61
|
+
// the sqlBody accessor: sqlBody already narrows an empty string to
|
|
62
|
+
// undefined, which would make a present-but-empty @sql indistinguishable
|
|
63
|
+
// from an absent one.
|
|
64
|
+
const rawSql = source.attr(SOURCE_ATTR_SQL);
|
|
65
|
+
if (rawSql !== undefined && (typeof rawSql !== "string" || rawSql.trim() === "")) {
|
|
66
|
+
errors.push(new ParseError(`source.rdb on object "${obj.name}" sets @sql to an empty/whitespace-only value; ` +
|
|
67
|
+
`@sql requires a non-empty SQL body`, { code: "ERR_BAD_ATTR_VALUE", source: source.source }));
|
|
68
|
+
}
|
|
69
|
+
if (sqlSet)
|
|
70
|
+
hasSqlHost = true;
|
|
71
|
+
if (unmanagedSet)
|
|
72
|
+
hasUnmanagedHost = true;
|
|
73
|
+
}
|
|
74
|
+
if (!hasSqlHost && !hasUnmanagedHost)
|
|
75
|
+
continue;
|
|
76
|
+
// R4 / R6 — origin.*-bearing (derived) own fields under an @sql / @unmanaged
|
|
77
|
+
// host. ADR-0039: own — origin.* never inherits (ADR-0029), so isDerived()
|
|
78
|
+
// is own-only by policy (mirrors validateDerivedFieldProvidability).
|
|
79
|
+
// @sql (hard error) takes priority over @unmanaged (warn) when a host
|
|
80
|
+
// happens to declare both markers across different sources.
|
|
81
|
+
for (const field of obj.ownChildren().filter((c) => c.type === TYPE_FIELD && c instanceof MetaField)) {
|
|
82
|
+
if (!field.isDerived())
|
|
83
|
+
continue;
|
|
84
|
+
if (hasSqlHost) {
|
|
85
|
+
errors.push(new ParseError(`field "${obj.name}.${field.name}" carries an origin.* (derived) child, but "${obj.name}" has a ` +
|
|
86
|
+
`read source carrying @sql — the synthesized derivation and the author's verbatim SQL are two ` +
|
|
87
|
+
`sources of truth for the same body`, { code: "ERR_ORIGIN_UNDER_SQL_BODY", source: field.source }));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
warnings.push({
|
|
91
|
+
code: "WARN_ORIGIN_UNDER_UNMANAGED",
|
|
92
|
+
message: `field "${obj.name}.${field.name}" carries an origin.* (derived) child, but "${obj.name}" has a ` +
|
|
93
|
+
`source marked @unmanaged — the tool never touches this object's DDL, so the derivation is ` +
|
|
94
|
+
`documented lineage only (not acted on); this is informational, not an error`,
|
|
95
|
+
source: field.source,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// R5 — a projection's row-scope @filter (#207) lowers to the outer WHERE
|
|
100
|
+
// of a TOOL-SYNTHESIZED body; with @sql the author owns the body (and its
|
|
101
|
+
// WHERE), so wrapping it is deferred cleverness (design doc D5) — reject.
|
|
102
|
+
if (hasSqlHost && obj.subType === OBJECT_SUBTYPE_PROJECTION) {
|
|
103
|
+
// ADR-0039: own — the @filter is declared locally on this projection
|
|
104
|
+
// (mirrors validateProjectionFilter).
|
|
105
|
+
const filter = obj.ownAttr(OBJECT_PROJECTION_ATTR_FILTER);
|
|
106
|
+
if (filter !== undefined) {
|
|
107
|
+
errors.push(new ParseError(`projection "${obj.name}" declares both @filter and an @sql read source — a view-level @filter ` +
|
|
108
|
+
`lowers to the outer WHERE of a synthesized body; with @sql the author owns the body (and its ` +
|
|
109
|
+
`WHERE), so the two cannot be combined`, { code: "ERR_ORIGIN_UNDER_SQL_BODY", source: obj.source }));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { errors, warnings };
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=validate-source-escapes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-source-escapes.js","sourceRoot":"","sources":["../../../src/persistence/source/validate-source-escapes.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -14,6 +14,9 @@ export const ATTR_SUBTYPE_BOOLEAN = "boolean";
|
|
|
14
14
|
export const ATTR_SUBTYPE_CLASS = "class";
|
|
15
15
|
export const ATTR_SUBTYPE_PROPERTIES = "properties";
|
|
16
16
|
export const ATTR_SUBTYPE_FILTER = "filter";
|
|
17
|
+
// #195 — a structured expression tree over a base entity's own fields (backs
|
|
18
|
+
// origin.computed). Object-shaped; a closed node grammar (see meta-attr-expression.ts).
|
|
19
|
+
export const ATTR_SUBTYPE_EXPRESSION = "expression";
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* The retired `stringarray` array attr subtype. It is NO LONGER a registered
|
|
@@ -37,6 +40,7 @@ export const ATTR_SUBTYPES = [
|
|
|
37
40
|
ATTR_SUBTYPE_CLASS,
|
|
38
41
|
ATTR_SUBTYPE_PROPERTIES,
|
|
39
42
|
ATTR_SUBTYPE_FILTER,
|
|
43
|
+
ATTR_SUBTYPE_EXPRESSION,
|
|
40
44
|
] as const;
|
|
41
45
|
export type AttrSubType =
|
|
42
46
|
| (typeof ATTR_SUBTYPES)[number]
|
|
@@ -62,6 +62,12 @@ export const ATTR_DEFINITION: ProviderDefinition = {
|
|
|
62
62
|
"subType": "filter",
|
|
63
63
|
"dataType": "object",
|
|
64
64
|
"description": "A filter-expression-valued metadata attribute. Object-shaped value that desugars a preset filter to the canonical { field: { op: value } } form (scalar→eq, array→in, null→isNull; or/and recurse)."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"type": "attr",
|
|
68
|
+
"subType": "expression",
|
|
69
|
+
"dataType": "object",
|
|
70
|
+
"description": "A structured expression tree over a base entity's own fields (closed node grammar: field/value refs, comparisons sharing the filter op vocabulary, isNull/isNotNull, and/or/not, coalesce). Backs origin.computed; a filter object embeds canonically. Additive node kinds (arithmetic/case/via-joined refs) are #159."
|
|
65
71
|
}
|
|
66
72
|
]
|
|
67
73
|
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// attr.expression — the closed structured-expression node grammar backing
|
|
2
|
+
// origin.computed (#195). A row-level value computed from a base entity's own
|
|
3
|
+
// fields. Deliberately NOT a raw-SQL string: a declarative, backend-agnostic
|
|
4
|
+
// tree that any lowering (SQL view DDL, an in-process evaluator, a document
|
|
5
|
+
// store) can walk.
|
|
6
|
+
//
|
|
7
|
+
// It shares the FILTER op vocabulary and per-subtype legality bands
|
|
8
|
+
// (query-constants.ts) so the two languages stay ONE vocabulary, and a canonical
|
|
9
|
+
// filter object embeds into it (`filterToExpr`) — so each port implements one
|
|
10
|
+
// expression engine and the filter renderers become thin adapters. The filter's
|
|
11
|
+
// leaves are `field → {op: literal}` and cannot hold a value tree (field-vs-field,
|
|
12
|
+
// coalesce), which is why the expression tree is its own grammar rather than a
|
|
13
|
+
// reused filter object.
|
|
14
|
+
//
|
|
15
|
+
// This module exposes the STANDALONE grammar (validate / infer / embed) AND the
|
|
16
|
+
// `attr.expression` MetaAttr registration. The registration is only live once the
|
|
17
|
+
// subtype is in ATTR_SUBTYPES + the canonical spec (a COORDINATED cross-port step,
|
|
18
|
+
// since the registry manifest is a single shared byte-matched file) — #195.
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
FILTER_OP_EQ, FILTER_OP_NE, FILTER_OP_GT, FILTER_OP_GTE, FILTER_OP_LT, FILTER_OP_LTE,
|
|
22
|
+
FILTER_OP_IS_NULL, FILTER_COMPOSE_AND, FILTER_COMPOSE_OR,
|
|
23
|
+
opsForSubType, type FilterOp,
|
|
24
|
+
} from "../query/query-constants.js";
|
|
25
|
+
import {
|
|
26
|
+
FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_BOOLEAN,
|
|
27
|
+
} from "../field/field-constants.js";
|
|
28
|
+
import { MetaAttr, type ValueError, runtimeTypeName } from "./meta-attr.js";
|
|
29
|
+
import { type AttrValue } from "../../shared/meta-data.js";
|
|
30
|
+
import { DATA_TYPE_OBJECT, type DataType } from "../../data-type.js";
|
|
31
|
+
import { ATTR_SUBTYPE_EXPRESSION } from "./attr-constants.js";
|
|
32
|
+
import { registerAttrClass } from "../../attr-class-map.js";
|
|
33
|
+
|
|
34
|
+
// --- Expression-only op/fn names (the comparison + and/or/isNull names are the
|
|
35
|
+
// --- shared filter vocabulary above; these three are new). ---
|
|
36
|
+
export const EXPR_OP_IS_NOT_NULL = "isNotNull";
|
|
37
|
+
export const EXPR_OP_NOT = "not";
|
|
38
|
+
export const EXPR_FN_COALESCE = "coalesce";
|
|
39
|
+
|
|
40
|
+
const COMPARISON_OPS: ReadonlySet<string> = new Set([
|
|
41
|
+
FILTER_OP_EQ, FILTER_OP_NE, FILTER_OP_GT, FILTER_OP_GTE, FILTER_OP_LT, FILTER_OP_LTE,
|
|
42
|
+
]);
|
|
43
|
+
const NULL_OPS: ReadonlySet<string> = new Set([FILTER_OP_IS_NULL, EXPR_OP_IS_NOT_NULL]);
|
|
44
|
+
const VARIADIC_LOGIC_OPS: ReadonlySet<string> = new Set([FILTER_COMPOSE_AND, FILTER_COMPOSE_OR]);
|
|
45
|
+
|
|
46
|
+
export type ExprLiteral = string | number | boolean | null;
|
|
47
|
+
export type ExprNode =
|
|
48
|
+
| { field: string }
|
|
49
|
+
| { value: ExprLiteral }
|
|
50
|
+
| { op: string; left: ExprNode; right: ExprNode } // comparison
|
|
51
|
+
| { op: string; arg: ExprNode } // isNull / isNotNull / not
|
|
52
|
+
| { op: string; args: ExprNode[] } // and / or
|
|
53
|
+
| { fn: string; args: ExprNode[] }; // coalesce
|
|
54
|
+
|
|
55
|
+
function isObj(v: unknown): v is Record<string, unknown> {
|
|
56
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Structural well-formedness of an expression tree (known node kinds, ops, arity).
|
|
60
|
+
* Returns [] when valid. No entity/type context — see `inferExprType` for typing. */
|
|
61
|
+
export function validateExprNode(node: unknown): string[] {
|
|
62
|
+
if (!isObj(node)) return [`expression node must be an object, got ${node === null ? "null" : Array.isArray(node) ? "array" : typeof node}`];
|
|
63
|
+
if ("field" in node) {
|
|
64
|
+
return typeof node.field === "string" && node.field.length > 0 ? [] : ["expression 'field' must be a non-empty string"];
|
|
65
|
+
}
|
|
66
|
+
if ("value" in node) {
|
|
67
|
+
const v = node.value;
|
|
68
|
+
return v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean"
|
|
69
|
+
? [] : ["expression 'value' must be a scalar literal (string/number/boolean/null)"];
|
|
70
|
+
}
|
|
71
|
+
if ("fn" in node) {
|
|
72
|
+
if (node.fn !== EXPR_FN_COALESCE) return [`unknown expression fn '${String(node.fn)}'`];
|
|
73
|
+
return Array.isArray(node.args) && node.args.length >= 1
|
|
74
|
+
? node.args.flatMap(validateExprNode) : [`'${EXPR_FN_COALESCE}' requires a non-empty args array`];
|
|
75
|
+
}
|
|
76
|
+
if ("op" in node) {
|
|
77
|
+
const op = node.op;
|
|
78
|
+
if (typeof op !== "string") return ["expression 'op' must be a string"];
|
|
79
|
+
if (COMPARISON_OPS.has(op)) {
|
|
80
|
+
return "left" in node && "right" in node
|
|
81
|
+
? [...validateExprNode(node.left), ...validateExprNode(node.right)]
|
|
82
|
+
: [`comparison op '${op}' requires 'left' and 'right'`];
|
|
83
|
+
}
|
|
84
|
+
if (NULL_OPS.has(op) || op === EXPR_OP_NOT) {
|
|
85
|
+
return "arg" in node ? validateExprNode(node.arg) : [`op '${op}' requires 'arg'`];
|
|
86
|
+
}
|
|
87
|
+
if (VARIADIC_LOGIC_OPS.has(op)) {
|
|
88
|
+
return Array.isArray(node.args) && node.args.length >= 1
|
|
89
|
+
? node.args.flatMap(validateExprNode) : [`op '${op}' requires a non-empty args array`];
|
|
90
|
+
}
|
|
91
|
+
return [`unknown expression op '${op}'`];
|
|
92
|
+
}
|
|
93
|
+
return ["expression node must be one of {field}, {value}, {op,…}, {fn,…}"];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The subtype of a scalar literal (coarse: an integer number → int, else double). */
|
|
97
|
+
function literalType(v: ExprLiteral): string | undefined {
|
|
98
|
+
if (typeof v === "string") return FIELD_SUBTYPE_STRING;
|
|
99
|
+
if (typeof v === "boolean") return FIELD_SUBTYPE_BOOLEAN;
|
|
100
|
+
if (typeof v === "number") return Number.isInteger(v) ? FIELD_SUBTYPE_INT : FIELD_SUBTYPE_DOUBLE;
|
|
101
|
+
return undefined; // null literal
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface InferResult {
|
|
105
|
+
/** The inferred field subType of the expression's root, or undefined if untypable. */
|
|
106
|
+
type: string | undefined;
|
|
107
|
+
/** Type/resolution/op-legality errors (empty when the tree types cleanly). */
|
|
108
|
+
errors: string[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Bottom-up type inference for a computed expression. `resolveField(name)` returns
|
|
113
|
+
* the subType of a base-entity field ref (undefined = unresolvable). Comparisons /
|
|
114
|
+
* null-tests / logic → boolean; a field ref → its subType; coalesce → the unified
|
|
115
|
+
* arg subType. Also enforces per-operand op legality against `OPS_BY_SUBTYPE`
|
|
116
|
+
* (the same bands filters use), so e.g. `gt` on a boolean operand is rejected.
|
|
117
|
+
*/
|
|
118
|
+
export function inferExprType(node: ExprNode, resolveField: (name: string) => string | undefined): InferResult {
|
|
119
|
+
const structural = validateExprNode(node);
|
|
120
|
+
if (structural.length) return { type: undefined, errors: structural };
|
|
121
|
+
const n = node as Record<string, unknown>;
|
|
122
|
+
|
|
123
|
+
if ("field" in n) {
|
|
124
|
+
const t = resolveField(n.field as string);
|
|
125
|
+
return t === undefined
|
|
126
|
+
? { type: undefined, errors: [`expression field '${String(n.field)}' does not resolve to a base entity field`] }
|
|
127
|
+
: { type: t, errors: [] };
|
|
128
|
+
}
|
|
129
|
+
if ("value" in n) return { type: literalType(n.value as ExprLiteral), errors: [] };
|
|
130
|
+
|
|
131
|
+
if ("fn" in n) { // coalesce
|
|
132
|
+
const argResults = (n.args as ExprNode[]).map((a) => inferExprType(a, resolveField));
|
|
133
|
+
const errors = argResults.flatMap((a) => a.errors);
|
|
134
|
+
const argTypes = argResults.map((a) => a.type).filter((t): t is string => t !== undefined);
|
|
135
|
+
const unified = argTypes[0];
|
|
136
|
+
if (unified !== undefined && argTypes.some((t) => t !== unified)) {
|
|
137
|
+
errors.push(`'${EXPR_FN_COALESCE}' arguments have differing types (${[...new Set(argTypes)].join(", ")})`);
|
|
138
|
+
}
|
|
139
|
+
return { type: unified, errors };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const op = n.op as string;
|
|
143
|
+
if (COMPARISON_OPS.has(op)) {
|
|
144
|
+
const l = inferExprType(n.left as ExprNode, resolveField);
|
|
145
|
+
const r = inferExprType(n.right as ExprNode, resolveField);
|
|
146
|
+
const errors = [...l.errors, ...r.errors];
|
|
147
|
+
if (l.type !== undefined && !opsForSubType(l.type).includes(op as FilterOp)) {
|
|
148
|
+
errors.push(`op '${op}' is not legal for a ${l.type} operand`);
|
|
149
|
+
}
|
|
150
|
+
return { type: FIELD_SUBTYPE_BOOLEAN, errors };
|
|
151
|
+
}
|
|
152
|
+
if (NULL_OPS.has(op)) {
|
|
153
|
+
return { type: FIELD_SUBTYPE_BOOLEAN, errors: inferExprType(n.arg as ExprNode, resolveField).errors };
|
|
154
|
+
}
|
|
155
|
+
if (op === EXPR_OP_NOT || VARIADIC_LOGIC_OPS.has(op)) {
|
|
156
|
+
const kids = op === EXPR_OP_NOT ? [n.arg as ExprNode] : (n.args as ExprNode[]);
|
|
157
|
+
const errors: string[] = [];
|
|
158
|
+
for (const k of kids) {
|
|
159
|
+
const kt = inferExprType(k, resolveField);
|
|
160
|
+
errors.push(...kt.errors);
|
|
161
|
+
if (kt.type !== undefined && kt.type !== FIELD_SUBTYPE_BOOLEAN) {
|
|
162
|
+
errors.push(`op '${op}' requires boolean operands, got ${kt.type}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { type: FIELD_SUBTYPE_BOOLEAN, errors };
|
|
166
|
+
}
|
|
167
|
+
return { type: undefined, errors: [`unknown expression op '${op}'`] };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Embed a CANONICAL filter object (`{ field: { op: value } }` with `and`/`or`
|
|
172
|
+
* composition — the desugared `attr.filter` form) into an expression tree, so the
|
|
173
|
+
* two languages share one evaluator. `isNull: false` becomes `isNotNull`.
|
|
174
|
+
*/
|
|
175
|
+
export function filterToExpr(filter: Record<string, unknown>): ExprNode {
|
|
176
|
+
const clauses: ExprNode[] = [];
|
|
177
|
+
for (const [key, raw] of Object.entries(filter)) {
|
|
178
|
+
if (key === FILTER_COMPOSE_AND || key === FILTER_COMPOSE_OR) {
|
|
179
|
+
const subs = (raw as Record<string, unknown>[]).map(filterToExpr);
|
|
180
|
+
clauses.push({ op: key, args: subs });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const opObj = raw as Record<string, ExprLiteral>;
|
|
184
|
+
for (const [op, val] of Object.entries(opObj)) {
|
|
185
|
+
if (op === FILTER_OP_IS_NULL) {
|
|
186
|
+
clauses.push(val === false
|
|
187
|
+
? { op: EXPR_OP_IS_NOT_NULL, arg: { field: key } }
|
|
188
|
+
: { op: FILTER_OP_IS_NULL, arg: { field: key } });
|
|
189
|
+
} else {
|
|
190
|
+
clauses.push({ op, left: { field: key }, right: { value: val } });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const [only] = clauses;
|
|
195
|
+
return clauses.length === 1 && only !== undefined ? only : { op: FILTER_COMPOSE_AND, args: clauses };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* ExpressionAttr — attr subtype `expression`. Object-shaped value holding a
|
|
200
|
+
* closed expression tree (`@expr` on origin.computed). Stored verbatim (no
|
|
201
|
+
* desugar); `validateValue` enforces STRUCTURAL well-formedness of the node
|
|
202
|
+
* grammar (a fail-closed unknown op/fn/kind is a load error, ADR-0023). Entity-
|
|
203
|
+
* aware TYPE inference (`inferExprType` → ERR_COMPUTED_TYPE_MISMATCH) runs later
|
|
204
|
+
* in the projection validation pass, where the base entity's fields are known.
|
|
205
|
+
*/
|
|
206
|
+
export class ExpressionAttr extends MetaAttr {
|
|
207
|
+
override get dataType(): DataType {
|
|
208
|
+
return DATA_TYPE_OBJECT;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
override coerce(raw: unknown): AttrValue {
|
|
212
|
+
// The expression tree is the canonical shape; pass a non-object through so
|
|
213
|
+
// validateValue rejects it.
|
|
214
|
+
return raw as AttrValue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
override validateValue(value: AttrValue): ValueError[] {
|
|
218
|
+
// Only shape (must be an object) is checked here — the CLOSED node grammar
|
|
219
|
+
// (fail-closed unknown op/fn/kind → ERR_UNKNOWN_EXPR_NODE) and entity-aware
|
|
220
|
+
// type inference are validated in the origin.computed validation pass, where
|
|
221
|
+
// the base entity's fields are known. Keeping the deep check out of the attr
|
|
222
|
+
// class matches the cross-port contract (C#/Java/Python store @expr verbatim)
|
|
223
|
+
// and mirrors FilterAttr, which likewise validates only shape.
|
|
224
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
225
|
+
? []
|
|
226
|
+
: [{ message: `attribute '@${this.name}' must be of type 'expression' but got ${runtimeTypeName(value)}` }];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
registerAttrClass(ATTR_SUBTYPE_EXPRESSION, ExpressionAttr);
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
DATA_TYPE_OBJECT,
|
|
17
17
|
} from "../../data-type.js";
|
|
18
18
|
import { convertToDataType } from "../../data-converter.js";
|
|
19
|
-
import { TYPE_VALIDATOR, TYPE_VIEW, SUBTYPE_BASE } from "../../shared/base-types.js";
|
|
19
|
+
import { TYPE_VALIDATOR, TYPE_VIEW, TYPE_ORIGIN, SUBTYPE_BASE } from "../../shared/base-types.js";
|
|
20
20
|
import {
|
|
21
21
|
FIELD_SUBTYPE_STRING,
|
|
22
22
|
FIELD_SUBTYPE_INT,
|
|
@@ -196,6 +196,18 @@ export class MetaField extends MetaData implements DataTypeAware {
|
|
|
196
196
|
);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* True when this field carries an `origin.*` child (passthrough / aggregate /
|
|
201
|
+
* computed / first): its value is DERIVED from other data, not stored in the
|
|
202
|
+
* object's own writable table. A derived field is read-only wherever it lives
|
|
203
|
+
* (ADR-0028) — excluded from the write-table DDL, the ORM table def, and the
|
|
204
|
+
* Insert/Update codecs; it exists only on the read (view) side. `origin.*` never
|
|
205
|
+
* inherits (ADR-0029), so the check is own-only (category 4). */
|
|
206
|
+
isDerived(): boolean {
|
|
207
|
+
// ADR-0039 own (category 4): origin.* never inherits (ADR-0029) — own by policy.
|
|
208
|
+
return this.ownChildren().some((c) => c.type === TYPE_ORIGIN);
|
|
209
|
+
}
|
|
210
|
+
|
|
199
211
|
/** The typed supertype field if `extends:` resolved, else undefined. */
|
|
200
212
|
resolveSuper(): MetaField | undefined {
|
|
201
213
|
return this.superData as MetaField | undefined;
|
|
@@ -39,3 +39,13 @@ export const OBJECT_ATTR_DISCRIMINATOR = "discriminator";
|
|
|
39
39
|
/** FR-014: on a subtype of an entity with @discriminator: the value that
|
|
40
40
|
* identifies rows of this subtype in the shared discriminator field. */
|
|
41
41
|
export const OBJECT_ATTR_DISCRIMINATOR_VALUE = "discriminatorValue";
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// #207 — projection row-scope @filter (view-level WHERE)
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/** #207: an optional row-scope predicate on an object.projection (an attr.filter
|
|
47
|
+
* object) selecting which rows the derived view returns — lowered to an outer
|
|
48
|
+
* SQL WHERE. Mirrors origin.aggregate's @filter placement (attr.filter subType),
|
|
49
|
+
* but resolves against the projection's OWN declared fields rather than one
|
|
50
|
+
* aggregated entity. An aggregate-derived field is not addressable (fail-closed). */
|
|
51
|
+
export const OBJECT_PROJECTION_ATTR_FILTER = "filter";
|
|
@@ -121,7 +121,17 @@ export const OBJECT_DEFINITION: ProviderDefinition = {
|
|
|
121
121
|
"extendsBase": true,
|
|
122
122
|
"description": "A derived read-only representation of entities. Its fields are extends-bound / origin-derived / self-declared-under-external-assembly, all read-only at the subtype level. Identity is optional and MUST extend an entity identity; sources are restricted to read-only @kinds. The declared field set IS the exposure (inclusive, fail-closed).",
|
|
123
123
|
"whenToUse": "You need a derived, read-only view over entities — a report, summary, or joined/aggregated read model. Declare it (with origin.* children) instead of hand-writing the SELECT/joins; it stays read-only and regenerates.",
|
|
124
|
-
"rules": "object.projection is a derived read-only representation: every field is extends-bound, origin-derived, or self-declared-under-external-assembly, and all are read-only at the subtype level. Identity is optional and, when present, MUST extend an entity identity. Sources are restricted to read-only @kinds. The declared field set IS the exposure — an inclusive list, fail-closed. A projection NEVER declares relationships (derivation is expressed via @via, not a relationship child) and NEVER co-locates templates — hence its child set omits both relationship and template. See ADR-0028 (object taxonomy, projection)."
|
|
124
|
+
"rules": "object.projection is a derived read-only representation: every field is extends-bound, origin-derived, or self-declared-under-external-assembly, and all are read-only at the subtype level. Identity is optional and, when present, MUST extend an entity identity. Sources are restricted to read-only @kinds. The declared field set IS the exposure — an inclusive list, fail-closed. A projection NEVER declares relationships (derivation is expressed via @via, not a relationship child) and NEVER co-locates templates — hence its child set omits both relationship and template. See ADR-0028 (object taxonomy, projection).",
|
|
125
|
+
"children": [
|
|
126
|
+
{
|
|
127
|
+
"type": "attr",
|
|
128
|
+
"subType": "filter",
|
|
129
|
+
"name": "filter",
|
|
130
|
+
"min": 0,
|
|
131
|
+
"max": 1,
|
|
132
|
+
"description": "Optional row-scope predicate (a portable attr.filter object: eq/ne/gt/gte/lt/lte/like/in/isNull with and/or, desugared to { field: { op: value } } at parse time) selecting which rows the view returns — lowered to an outer SQL WHERE. Resolves against the projection's own declared fields; an aggregate-derived field is not addressable (fail-closed)."
|
|
133
|
+
}
|
|
134
|
+
]
|
|
125
135
|
}
|
|
126
136
|
]
|
|
127
137
|
};
|
package/src/core-types.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { attrClassFor, type NodeConstructor } from "./attr-class-map.js";
|
|
|
18
18
|
import "./core/attr/meta-attr-stringarray.js";
|
|
19
19
|
import "./core/attr/meta-attr-filter.js";
|
|
20
20
|
import "./core/attr/meta-attr-properties.js";
|
|
21
|
+
import "./core/attr/meta-attr-expression.js";
|
|
21
22
|
import {
|
|
22
23
|
MetaValidator,
|
|
23
24
|
MetaRequiredValidator,
|
|
@@ -36,7 +37,7 @@ import {
|
|
|
36
37
|
import { MetaRelationship } from "./core/relationship/meta-relationship.js";
|
|
37
38
|
import { MetaLayout } from "./presentation/layout/meta-layout.js";
|
|
38
39
|
import { MetaSource } from "./persistence/source/meta-source.js";
|
|
39
|
-
import { MetaOrigin, MetaPassthroughOrigin, MetaAggregateOrigin, MetaCollectionOrigin } from "./persistence/origin/meta-origin.js";
|
|
40
|
+
import { MetaOrigin, MetaPassthroughOrigin, MetaAggregateOrigin, MetaCollectionOrigin, MetaComputedOrigin, MetaFirstOrigin } from "./persistence/origin/meta-origin.js";
|
|
40
41
|
import { defineProviderFromData, type FactoryMap } from "./provider-data.js";
|
|
41
42
|
import { FIELD_DEFINITION } from "./core/field/field-definition.embedded.js";
|
|
42
43
|
import { OBJECT_DEFINITION } from "./core/object/object-definition.embedded.js";
|
|
@@ -98,6 +99,8 @@ import {
|
|
|
98
99
|
ORIGIN_SUBTYPE_PASSTHROUGH,
|
|
99
100
|
ORIGIN_SUBTYPE_AGGREGATE,
|
|
100
101
|
ORIGIN_SUBTYPE_COLLECTION,
|
|
102
|
+
ORIGIN_SUBTYPE_COMPUTED,
|
|
103
|
+
ORIGIN_SUBTYPE_FIRST,
|
|
101
104
|
} from "./persistence/origin/origin-constants.js";
|
|
102
105
|
|
|
103
106
|
// ---------------------------------------------------------------------------
|
|
@@ -161,6 +164,8 @@ const ORIGIN_CLASS_MAP = new Map<string, NodeConstructor>([
|
|
|
161
164
|
[ORIGIN_SUBTYPE_PASSTHROUGH, MetaPassthroughOrigin],
|
|
162
165
|
[ORIGIN_SUBTYPE_AGGREGATE, MetaAggregateOrigin],
|
|
163
166
|
[ORIGIN_SUBTYPE_COLLECTION, MetaCollectionOrigin],
|
|
167
|
+
[ORIGIN_SUBTYPE_COMPUTED, MetaComputedOrigin],
|
|
168
|
+
[ORIGIN_SUBTYPE_FIRST, MetaFirstOrigin],
|
|
164
169
|
]);
|
|
165
170
|
|
|
166
171
|
// ATTR_CLASS_MAP + attrClassFor live in the leaf module ./attr-class-map.ts
|
package/src/errors.ts
CHANGED
|
@@ -170,6 +170,27 @@ export const ERROR_CODES = [
|
|
|
170
170
|
// index.lookup field-resolution: @fields is empty or names a field that does
|
|
171
171
|
// not exist on the owning entity's effective (resolved via extends) field set.
|
|
172
172
|
"ERR_INVALID_INDEX",
|
|
173
|
+
// #195 — origin.computed @expr: the expression tree's inferred root type does
|
|
174
|
+
// not equal the carrying field's declared field.<subType>. A computed column's
|
|
175
|
+
// type is DERIVED from its expression, never asserted (no @convert escape),
|
|
176
|
+
// so a mismatch is a hard load error (sibling of ERR_PASSTHROUGH_TYPE_MISMATCH).
|
|
177
|
+
"ERR_COMPUTED_TYPE_MISMATCH",
|
|
178
|
+
// #195 — origin.computed @expr contains a node whose kind/op/fn is not in the
|
|
179
|
+
// closed expression grammar (ADR-0023 fail-closed). Detail names the bad token.
|
|
180
|
+
"ERR_UNKNOWN_EXPR_NODE",
|
|
181
|
+
// #208 — a source.rdb declares BOTH @sql (author-supplied body) and
|
|
182
|
+
// @unmanaged (DDL owned elsewhere). The two markers are the mutually
|
|
183
|
+
// exclusive non-default states of one "who owns this DDL" axis.
|
|
184
|
+
"ERR_SQL_BODY_WITH_UNMANAGED",
|
|
185
|
+
// #208 — a source.rdb declares @sql with a writable @kind ("table", the
|
|
186
|
+
// default). A hand-written CREATE TABLE would bypass the column-diff
|
|
187
|
+
// machinery; tables are fully modeled or @unmanaged, never opaque-bodied.
|
|
188
|
+
"ERR_SQL_BODY_ON_WRITABLE_KIND",
|
|
189
|
+
// #208 — a host object whose read source carries @sql also declares an
|
|
190
|
+
// origin.*-bearing (derived) field, or (on object.projection) an @filter —
|
|
191
|
+
// two sources of truth for the same body (the synthesized derivation/WHERE
|
|
192
|
+
// vs. the author's verbatim SQL). Fail-closed.
|
|
193
|
+
"ERR_ORIGIN_UNDER_SQL_BODY",
|
|
173
194
|
"ERR_UNKNOWN",
|
|
174
195
|
] as const;
|
|
175
196
|
|
|
@@ -189,6 +210,13 @@ export const WARNING_CODES = [
|
|
|
189
210
|
// implication does not apply to value-objects; the attr is retained for
|
|
190
211
|
// language-specific record/struct treatment (e.g. Kotlin `val` vs `var`).
|
|
191
212
|
"WARN_READONLY_VALUE_OBJECT",
|
|
213
|
+
// #208 — a host object whose source carries @unmanaged also declares an
|
|
214
|
+
// origin.*-bearing (derived) field. Deliberate asymmetry with
|
|
215
|
+
// ERR_ORIGIN_UNDER_SQL_BODY: @unmanaged acts on nothing (the tool never
|
|
216
|
+
// touches the object's DDL), so a documented-but-unacted-on lineage is
|
|
217
|
+
// benign — a WARN preserves the matview symmetry and lets an author
|
|
218
|
+
// document lineage for future in-process evaluation (#211).
|
|
219
|
+
"WARN_ORIGIN_UNDER_UNMANAGED",
|
|
192
220
|
] as const;
|
|
193
221
|
export type WarningCode = (typeof WARNING_CODES)[number];
|
|
194
222
|
|
package/src/index.ts
CHANGED
|
@@ -107,6 +107,8 @@ export {
|
|
|
107
107
|
MetaPassthroughOrigin,
|
|
108
108
|
MetaAggregateOrigin,
|
|
109
109
|
MetaCollectionOrigin,
|
|
110
|
+
MetaComputedOrigin,
|
|
111
|
+
MetaFirstOrigin,
|
|
110
112
|
} from "./persistence/origin/meta-origin.js";
|
|
111
113
|
// Template: single class backs both subtypes (FR-004)
|
|
112
114
|
export { MetaTemplate } from "./template/meta-template.js";
|
|
@@ -18,9 +18,10 @@ import { ParseError } from "../errors.js";
|
|
|
18
18
|
import type { LoaderWarning } from "../source.js";
|
|
19
19
|
import { codeSource, resolvedSource } from "../source.js";
|
|
20
20
|
import { parseJson } from "../parser-json.js";
|
|
21
|
-
import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateFieldMap, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships, validateIndexLookupFields } from "./validation-passes.js";
|
|
21
|
+
import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateFieldMap, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships, validateIndexLookupFields, validateProjectionFilter } from "./validation-passes.js";
|
|
22
22
|
import { runRegisteredValidation } from "./validation-registry.js";
|
|
23
23
|
import { validateSourceRoles } from "../persistence/source/validate-source-roles.js";
|
|
24
|
+
import { validateSourceEscapes } from "../persistence/source/validate-source-escapes.js";
|
|
24
25
|
import { validateSourcePhysicalNames } from "../persistence/source/validate-source-physical-names.js";
|
|
25
26
|
import { validateSourceParameterRef } from "../persistence/source/validate-source-parameter-ref.js";
|
|
26
27
|
import { validateFieldReadOnly } from "../core/field/validate-field-readonly.js";
|
|
@@ -589,6 +590,11 @@ export class MetaDataLoader {
|
|
|
589
590
|
// Seventh pass: @filter value validation — fields filterable + ops allowed per subtype.
|
|
590
591
|
errors.push(...validateDataGridFilterValues(root));
|
|
591
592
|
|
|
593
|
+
// #207 — a projection's row-scope @filter (view-level WHERE) must reference the
|
|
594
|
+
// projection's own declared, WHERE-addressable fields (passthrough/computed/plain);
|
|
595
|
+
// an aggregate-derived or dangling ref is fail-closed (ERR_BAD_ATTR_FILTER).
|
|
596
|
+
errors.push(...validateProjectionFilter(root));
|
|
597
|
+
|
|
592
598
|
// FR-017 — M:N relationship validation (deferred-resolution): @through names a
|
|
593
599
|
// junction declaring two identity.reference children; @sourceRefField matches one;
|
|
594
600
|
// @symmetric is self-join-only + mutually exclusive with @sourceRefField; M:N attrs
|
|
@@ -630,6 +636,13 @@ export class MetaDataLoader {
|
|
|
630
636
|
// ERR_SOURCE_MULTIPLE_PRIMARY).
|
|
631
637
|
errors.push(...validateSourceRoles(root));
|
|
632
638
|
|
|
639
|
+
// #208 — DDL-ownership escape valves: source.rdb @sql / @unmanaged
|
|
640
|
+
// fail-closed rules (ERR_SQL_BODY_WITH_UNMANAGED / ERR_SQL_BODY_ON_WRITABLE_KIND /
|
|
641
|
+
// ERR_BAD_ATTR_VALUE / ERR_ORIGIN_UNDER_SQL_BODY / WARN_ORIGIN_UNDER_UNMANAGED).
|
|
642
|
+
const sourceEscapeResult = validateSourceEscapes(root);
|
|
643
|
+
errors.push(...sourceEscapeResult.errors);
|
|
644
|
+
envelopeWarnings.push(...sourceEscapeResult.warnings);
|
|
645
|
+
|
|
633
646
|
// FR-016 / ADR-0018 — per-kind physical-name alias validation on
|
|
634
647
|
// source.rdb (kind-matching alias, no multiples, legacy @table warning).
|
|
635
648
|
const physicalNameResult = validateSourcePhysicalNames(root);
|