@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.
Files changed (86) hide show
  1. package/dist/core/attr/attr-constants.d.ts +2 -1
  2. package/dist/core/attr/attr-constants.d.ts.map +1 -1
  3. package/dist/core/attr/attr-constants.js +4 -0
  4. package/dist/core/attr/attr-constants.js.map +1 -1
  5. package/dist/core/attr/attr-definition.embedded.d.ts.map +1 -1
  6. package/dist/core/attr/attr-definition.embedded.js +6 -0
  7. package/dist/core/attr/attr-definition.embedded.js.map +1 -1
  8. package/dist/core/attr/meta-attr-expression.d.ts +62 -0
  9. package/dist/core/attr/meta-attr-expression.d.ts.map +1 -0
  10. package/dist/core/attr/meta-attr-expression.js +203 -0
  11. package/dist/core/attr/meta-attr-expression.js.map +1 -0
  12. package/dist/core/field/meta-field.d.ts +8 -0
  13. package/dist/core/field/meta-field.d.ts.map +1 -1
  14. package/dist/core/field/meta-field.js +12 -1
  15. package/dist/core/field/meta-field.js.map +1 -1
  16. package/dist/core/object/object-constants.d.ts +6 -0
  17. package/dist/core/object/object-constants.d.ts.map +1 -1
  18. package/dist/core/object/object-constants.js +9 -0
  19. package/dist/core/object/object-constants.js.map +1 -1
  20. package/dist/core/object/object-definition.embedded.d.ts.map +1 -1
  21. package/dist/core/object/object-definition.embedded.js +11 -1
  22. package/dist/core/object/object-definition.embedded.js.map +1 -1
  23. package/dist/core-types.d.ts +1 -0
  24. package/dist/core-types.d.ts.map +1 -1
  25. package/dist/core-types.js +5 -2
  26. package/dist/core-types.js.map +1 -1
  27. package/dist/errors.d.ts +2 -2
  28. package/dist/errors.d.ts.map +1 -1
  29. package/dist/errors.js +28 -0
  30. package/dist/errors.js.map +1 -1
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  36. package/dist/loader/meta-data-loader.js +12 -1
  37. package/dist/loader/meta-data-loader.js.map +1 -1
  38. package/dist/loader/validation-passes.d.ts +1 -0
  39. package/dist/loader/validation-passes.d.ts.map +1 -1
  40. package/dist/loader/validation-passes.js +264 -13
  41. package/dist/loader/validation-passes.js.map +1 -1
  42. package/dist/persistence/db/db-definition.embedded.d.ts.map +1 -1
  43. package/dist/persistence/db/db-definition.embedded.js +16 -0
  44. package/dist/persistence/db/db-definition.embedded.js.map +1 -1
  45. package/dist/persistence/origin/meta-origin.d.ts +30 -3
  46. package/dist/persistence/origin/meta-origin.d.ts.map +1 -1
  47. package/dist/persistence/origin/meta-origin.js +44 -2
  48. package/dist/persistence/origin/meta-origin.js.map +1 -1
  49. package/dist/persistence/origin/origin-constants.d.ts +15 -1
  50. package/dist/persistence/origin/origin-constants.d.ts.map +1 -1
  51. package/dist/persistence/origin/origin-constants.js +40 -3
  52. package/dist/persistence/origin/origin-constants.js.map +1 -1
  53. package/dist/persistence/origin/origin-definition.embedded.d.ts.map +1 -1
  54. package/dist/persistence/origin/origin-definition.embedded.js +86 -8
  55. package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
  56. package/dist/persistence/source/meta-source.d.ts +12 -0
  57. package/dist/persistence/source/meta-source.d.ts.map +1 -1
  58. package/dist/persistence/source/meta-source.js +20 -1
  59. package/dist/persistence/source/meta-source.js.map +1 -1
  60. package/dist/persistence/source/source-constants.d.ts +9 -0
  61. package/dist/persistence/source/source-constants.d.ts.map +1 -1
  62. package/dist/persistence/source/source-constants.js +9 -0
  63. package/dist/persistence/source/source-constants.js.map +1 -1
  64. package/dist/persistence/source/validate-source-escapes.d.ts +9 -0
  65. package/dist/persistence/source/validate-source-escapes.d.ts.map +1 -0
  66. package/dist/persistence/source/validate-source-escapes.js +115 -0
  67. package/dist/persistence/source/validate-source-escapes.js.map +1 -0
  68. package/package.json +1 -1
  69. package/src/core/attr/attr-constants.ts +4 -0
  70. package/src/core/attr/attr-definition.embedded.ts +6 -0
  71. package/src/core/attr/meta-attr-expression.ts +230 -0
  72. package/src/core/field/meta-field.ts +13 -1
  73. package/src/core/object/object-constants.ts +10 -0
  74. package/src/core/object/object-definition.embedded.ts +11 -1
  75. package/src/core-types.ts +6 -1
  76. package/src/errors.ts +28 -0
  77. package/src/index.ts +2 -0
  78. package/src/loader/meta-data-loader.ts +14 -1
  79. package/src/loader/validation-passes.ts +370 -15
  80. package/src/persistence/db/db-definition.embedded.ts +16 -0
  81. package/src/persistence/origin/meta-origin.ts +55 -4
  82. package/src/persistence/origin/origin-constants.ts +45 -3
  83. package/src/persistence/origin/origin-definition.embedded.ts +86 -8
  84. package/src/persistence/source/meta-source.ts +23 -0
  85. package/src/persistence/source/source-constants.ts +10 -0
  86. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metaobjectsdev/metadata",
3
- "version": "0.16.0",
3
+ "version": "0.17.0-rc.1",
4
4
  "description": "Metamodel loader, types, and constants for the MetaObjects standard.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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);