@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,9 +49,9 @@ export const ORIGIN_DEFINITION: ProviderDefinition = {
|
|
|
49
49
|
{
|
|
50
50
|
"type": "origin",
|
|
51
51
|
"subType": "aggregate",
|
|
52
|
-
"description": "A count/sum/avg/min/max
|
|
53
|
-
"whenToUse": "A projection needs a derived count/sum/avg/min/max
|
|
54
|
-
"rules": "@via may be omitted only when exactly one single-hop relationship leads from the base entity to the @of entity (single-hop-unique inference; FR-024, ADR-0029). Multi-hop paths must always be stated explicitly.",
|
|
52
|
+
"description": "A value reduced from the related row-set reached along a relationship path (@via) from the base entity: count/sum/avg/min/max over a column (@of); any/all predicate quantifiers over a @filter; or collect (an array rollup of @of).",
|
|
53
|
+
"whenToUse": "A projection needs a value derived by reducing related rows — a count/sum/avg/min/max, a 'did any/every related row match' flag, or an array of collected values. Declare it instead of hand-writing the aggregate query — it stays consistent and regenerates.",
|
|
54
|
+
"rules": "@via may be omitted only when exactly one single-hop relationship leads from the base entity to the @of entity (single-hop-unique inference; FR-024, ADR-0029). Multi-hop paths must always be stated explicitly. @of is required for count/sum/avg/min/max/collect and forbidden for any/all (which quantify over rows via @filter, not a column). @filter is required for any/all. The field must be isArray:true for collect and isArray:false for every other @agg. @distinct and @orderBy are collect-only.",
|
|
55
55
|
"children": [
|
|
56
56
|
{
|
|
57
57
|
"type": "attr",
|
|
@@ -64,17 +64,20 @@ export const ORIGIN_DEFINITION: ProviderDefinition = {
|
|
|
64
64
|
"sum",
|
|
65
65
|
"avg",
|
|
66
66
|
"min",
|
|
67
|
-
"max"
|
|
67
|
+
"max",
|
|
68
|
+
"any",
|
|
69
|
+
"all",
|
|
70
|
+
"collect"
|
|
68
71
|
],
|
|
69
|
-
"description": "
|
|
72
|
+
"description": "The reducing function applied over the related row-set: count/sum/avg/min/max (numeric/ordinal reduces over @of); any/all (predicate quantifiers over @filter — @of forbidden; empty set → any=false, all=true); collect (array rollup of @of — the field must be isArray)."
|
|
70
73
|
},
|
|
71
74
|
{
|
|
72
75
|
"type": "attr",
|
|
73
76
|
"subType": "string",
|
|
74
77
|
"name": "of",
|
|
75
|
-
"min":
|
|
78
|
+
"min": 0,
|
|
76
79
|
"max": 1,
|
|
77
|
-
"description": "Dotted Entity.field reference identifying the column being aggregated (e.g. 'Week.durationMinutes')."
|
|
80
|
+
"description": "Dotted Entity.field reference identifying the column being aggregated (e.g. 'Week.durationMinutes'). Required for count/sum/avg/min/max/collect; forbidden for any/all (which quantify over rows via @filter, not a column)."
|
|
78
81
|
},
|
|
79
82
|
{
|
|
80
83
|
"type": "attr",
|
|
@@ -90,7 +93,24 @@ export const ORIGIN_DEFINITION: ProviderDefinition = {
|
|
|
90
93
|
"name": "filter",
|
|
91
94
|
"min": 0,
|
|
92
95
|
"max": 1,
|
|
93
|
-
"description": "Optional structured predicate scoping which related rows the aggregate spans. A portable attr.filter object (eq/ne/in/isNull with and/or), desugared to canonical { field: { op: value } } at parse time; codegen renders it per target (e.g. SQL FILTER (WHERE ...) or SQLite CASE WHEN for a relational view)."
|
|
96
|
+
"description": "Optional structured predicate scoping which related rows the aggregate spans (required for any/all, where it is the quantified predicate). A portable attr.filter object (eq/ne/in/isNull with and/or), desugared to canonical { field: { op: value } } at parse time; codegen renders it per target (e.g. SQL FILTER (WHERE ...) or SQLite CASE WHEN for a relational view)."
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"type": "attr",
|
|
100
|
+
"subType": "boolean",
|
|
101
|
+
"name": "distinct",
|
|
102
|
+
"min": 0,
|
|
103
|
+
"max": 1,
|
|
104
|
+
"description": "Set (collect-only) to dedupe collected values (set semantics)."
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"type": "attr",
|
|
108
|
+
"subType": "string",
|
|
109
|
+
"name": "orderBy",
|
|
110
|
+
"isArray": true,
|
|
111
|
+
"min": 0,
|
|
112
|
+
"max": 1,
|
|
113
|
+
"description": "Ordering keys as 'field[:asc|desc]' (default asc) over the related entity's fields; nulls sort last. On @agg:collect sets element order (non-distinct only); on origin.first (required) selects the row. Semantic — carries no SQL syntax."
|
|
94
114
|
}
|
|
95
115
|
]
|
|
96
116
|
},
|
|
@@ -109,6 +129,64 @@ export const ORIGIN_DEFINITION: ProviderDefinition = {
|
|
|
109
129
|
"description": "Dotted relationship path the collection walks to produce an array of nested view-objects (e.g. 'Author.posts'), or a wildcard selector for a package-spanning collection (e.g. '*.User')."
|
|
110
130
|
}
|
|
111
131
|
]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"type": "origin",
|
|
135
|
+
"subType": "computed",
|
|
136
|
+
"description": "A row-level value computed from the base entity's own fields via a structured expression tree (@expr). No related rows, no @via. Read-only; the expression's inferred type must equal the field's declared subType.",
|
|
137
|
+
"whenToUse": "A projection field is a cheap derived value over the base row itself (e.g. 'payload IS NOT NULL' to avoid shipping a heavy column). Declare the expression tree instead of hand-writing the derived SELECT column.",
|
|
138
|
+
"children": [
|
|
139
|
+
{
|
|
140
|
+
"type": "attr",
|
|
141
|
+
"subType": "expression",
|
|
142
|
+
"name": "expr",
|
|
143
|
+
"min": 1,
|
|
144
|
+
"max": 1,
|
|
145
|
+
"description": "The structured expression tree computing this field's value from the base entity's own fields (closed node grammar; shares the filter op vocabulary). Its inferred root type must equal the field's declared subType (ERR_COMPUTED_TYPE_MISMATCH)."
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"type": "origin",
|
|
151
|
+
"subType": "first",
|
|
152
|
+
"description": "The single related row selected by @orderBy along @via, projecting its @of column (argmax then project). Latest = @orderBy desc. Read-only; empty related set (after @filter) → null, so the field must not be @required.",
|
|
153
|
+
"whenToUse": "A projection needs one related row's column chosen by an ordering — 'the latest child's status', 'the earliest event's timestamp'. Declare @via + @of + @orderBy instead of hand-writing the correlated ORDER BY … LIMIT 1 subquery.",
|
|
154
|
+
"rules": "@via may be omitted only when exactly one single-hop relationship leads from the base entity to the @of entity (FR-024, ADR-0029). The related entity's primary key ascending is always appended as the final ordering tie-breaker so the selection is deterministic.",
|
|
155
|
+
"children": [
|
|
156
|
+
{
|
|
157
|
+
"type": "attr",
|
|
158
|
+
"subType": "string",
|
|
159
|
+
"name": "of",
|
|
160
|
+
"min": 1,
|
|
161
|
+
"max": 1,
|
|
162
|
+
"description": "Dotted Entity.field reference identifying the column projected from the selected row (e.g. 'ChildA.label'). Type-preserving: the field's declared subType must equal this column's subType (#185 doctrine)."
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"type": "attr",
|
|
166
|
+
"subType": "string",
|
|
167
|
+
"name": "via",
|
|
168
|
+
"min": 0,
|
|
169
|
+
"max": 1,
|
|
170
|
+
"description": "Dotted relationship path from the base entity to the related rows (e.g. 'Parent.childAs'). May be omitted only when exactly one single-hop relationship leads from the base entity to the @of entity (FR-024, ADR-0029)."
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"type": "attr",
|
|
174
|
+
"subType": "string",
|
|
175
|
+
"name": "orderBy",
|
|
176
|
+
"isArray": true,
|
|
177
|
+
"min": 1,
|
|
178
|
+
"max": 1,
|
|
179
|
+
"description": "Ordering keys as 'field[:asc|desc]' (default asc) over the related entity's fields; nulls sort last. Selects the single row (the first after ordering). The related PK ascending is appended as the deterministic tie-breaker. Semantic — carries no SQL syntax."
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"type": "attr",
|
|
183
|
+
"subType": "filter",
|
|
184
|
+
"name": "filter",
|
|
185
|
+
"min": 0,
|
|
186
|
+
"max": 1,
|
|
187
|
+
"description": "Optional structured predicate scoping which related rows are eligible for selection. A portable attr.filter object (eq/ne/in/isNull with and/or), desugared to canonical { field: { op: value } } at parse time."
|
|
188
|
+
}
|
|
189
|
+
]
|
|
112
190
|
}
|
|
113
191
|
]
|
|
114
192
|
};
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
SOURCE_ATTR_FUNCTION,
|
|
16
16
|
SOURCE_ATTR_KIND,
|
|
17
17
|
SOURCE_ATTR_ROLE,
|
|
18
|
+
SOURCE_ATTR_SQL,
|
|
19
|
+
SOURCE_ATTR_UNMANAGED,
|
|
18
20
|
SOURCE_READ_ONLY_KINDS,
|
|
19
21
|
DEFAULT_SOURCE_KIND,
|
|
20
22
|
DEFAULT_SOURCE_ROLE,
|
|
@@ -59,6 +61,27 @@ export class MetaSource extends MetaData {
|
|
|
59
61
|
return typeof v === "string" && v !== "" ? v : DEFAULT_SOURCE_ROLE;
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
/**
|
|
65
|
+
* FR-024/#208 — the hand-written SQL body for this source's DB object, when
|
|
66
|
+
* declared via `@sql`. The tool registers + fingerprints + drift-checks this
|
|
67
|
+
* body but never authors or parses it.
|
|
68
|
+
*/
|
|
69
|
+
get sqlBody(): string | undefined {
|
|
70
|
+
// ADR-0039: resolving — an inherited source's @sql lives on the super node.
|
|
71
|
+
const v = this.attr(SOURCE_ATTR_SQL);
|
|
72
|
+
return typeof v === "string" && v !== "" ? v : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* FR-024/#208 — true when this source's DB object is managed elsewhere
|
|
77
|
+
* (Flyway / a hand-migration owns its DDL); `meta migrate` does not
|
|
78
|
+
* create/drop/drift-check it.
|
|
79
|
+
*/
|
|
80
|
+
get isUnmanaged(): boolean {
|
|
81
|
+
// ADR-0039: resolving — an inherited source's @unmanaged lives on the super node.
|
|
82
|
+
return this.attr(SOURCE_ATTR_UNMANAGED) === true;
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
/**
|
|
63
86
|
* True when this source's effective kind is read-only (view, materializedView,
|
|
64
87
|
* storedProc, tableFunction). Derived from the @kind attr on source.rdb.
|
|
@@ -64,6 +64,16 @@ export const SOURCE_ATTR_ROLE = "role";
|
|
|
64
64
|
* new parameter.* node type. */
|
|
65
65
|
export const SOURCE_ATTR_PARAMETER_REF = "parameterRef";
|
|
66
66
|
|
|
67
|
+
/** FR-024/#208 escape valve — a hand-written SQL body the tool REGISTERS +
|
|
68
|
+
* fingerprints + drift-checks but never authors or parses. Legal only on a
|
|
69
|
+
* read-only @kind (not "table"). Mutually exclusive with @unmanaged; forbids
|
|
70
|
+
* origin.* children. */
|
|
71
|
+
export const SOURCE_ATTR_SQL = "sql";
|
|
72
|
+
/** FR-024/#208 escape valve — this DB object is managed elsewhere (Flyway / a
|
|
73
|
+
* hand-migration owns its DDL); meta migrate does not create/drop/drift-check
|
|
74
|
+
* it. Legal on any @kind including "table". Mutually exclusive with @sql. */
|
|
75
|
+
export const SOURCE_ATTR_UNMANAGED = "unmanaged";
|
|
76
|
+
|
|
67
77
|
export const SOURCE_KIND_TABLE = "table";
|
|
68
78
|
export const SOURCE_KIND_VIEW = "view";
|
|
69
79
|
export const SOURCE_KIND_MATERIALIZED_VIEW = "materializedView";
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
|
|
24
|
+
import type { MetaData } from "../../shared/meta-data.js";
|
|
25
|
+
import { ParseError } from "../../errors.js";
|
|
26
|
+
import type { LoaderWarning } from "../../source.js";
|
|
27
|
+
import { TYPE_OBJECT, TYPE_SOURCE, TYPE_FIELD } from "../../shared/base-types.js";
|
|
28
|
+
import { OBJECT_SUBTYPE_PROJECTION, OBJECT_PROJECTION_ATTR_FILTER } from "../../core/object/object-constants.js";
|
|
29
|
+
import { MetaSource } from "./meta-source.js";
|
|
30
|
+
import { MetaField } from "../../core/field/meta-field.js";
|
|
31
|
+
import { SOURCE_ATTR_SQL, SOURCE_SUBTYPE_RDB } from "./source-constants.js";
|
|
32
|
+
|
|
33
|
+
export interface SourceEscapeValidationResult {
|
|
34
|
+
errors: ParseError[];
|
|
35
|
+
warnings: LoaderWarning[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateSourceEscapes(root: MetaData): SourceEscapeValidationResult {
|
|
39
|
+
const errors: ParseError[] = [];
|
|
40
|
+
const warnings: LoaderWarning[] = [];
|
|
41
|
+
|
|
42
|
+
// ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
|
|
43
|
+
for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
|
|
44
|
+
// ADR-0039: own — declaration-layer source iteration (mirrors validateSourceRoles /
|
|
45
|
+
// validateSourceParameterRef): R1–R3 judge markers DECLARED on this object's own sources.
|
|
46
|
+
const sources = obj
|
|
47
|
+
.ownChildren()
|
|
48
|
+
.filter(
|
|
49
|
+
(c): c is MetaSource =>
|
|
50
|
+
c.type === TYPE_SOURCE && c.subType === SOURCE_SUBTYPE_RDB && c instanceof MetaSource,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let hasSqlHost = false;
|
|
54
|
+
let hasUnmanagedHost = false;
|
|
55
|
+
|
|
56
|
+
for (const source of sources) {
|
|
57
|
+
// ADR-0039: resolving — @sql/@unmanaged are inheritable (follow the
|
|
58
|
+
// @role/effectiveKind precedent — sources are inheritable, NOT the
|
|
59
|
+
// @dbColumnType own-only exception).
|
|
60
|
+
const sqlSet = source.sqlBody !== undefined;
|
|
61
|
+
const unmanagedSet = source.isUnmanaged;
|
|
62
|
+
|
|
63
|
+
// R1 — contradictory DDL owners on the same source.
|
|
64
|
+
if (sqlSet && unmanagedSet) {
|
|
65
|
+
errors.push(
|
|
66
|
+
new ParseError(
|
|
67
|
+
`source.rdb on object "${obj.name}" declares both @sql and @unmanaged — these are the ` +
|
|
68
|
+
`mutually exclusive non-default states of one DDL-ownership axis (an author-supplied body ` +
|
|
69
|
+
`contradicts "someone else owns this DDL")`,
|
|
70
|
+
{ code: "ERR_SQL_BODY_WITH_UNMANAGED", source: source.source },
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// R2 — @sql on a writable kind would bypass the column-diff machinery;
|
|
76
|
+
// tables are fully modeled or @unmanaged, never opaque-bodied.
|
|
77
|
+
if (sqlSet && source.isWritable()) {
|
|
78
|
+
errors.push(
|
|
79
|
+
new ParseError(
|
|
80
|
+
`source.rdb on object "${obj.name}" declares @sql with a writable @kind ("${source.effectiveKind}") — ` +
|
|
81
|
+
`@sql is legal only on a read-only kind (view/materializedView/storedProc/tableFunction); ` +
|
|
82
|
+
`a writable table is either fully modeled or marked @unmanaged, never opaque-bodied`,
|
|
83
|
+
{ code: "ERR_SQL_BODY_ON_WRITABLE_KIND", source: source.source },
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// R3 — @sql present but empty/whitespace. MUST read the raw attr, not
|
|
89
|
+
// the sqlBody accessor: sqlBody already narrows an empty string to
|
|
90
|
+
// undefined, which would make a present-but-empty @sql indistinguishable
|
|
91
|
+
// from an absent one.
|
|
92
|
+
const rawSql = source.attr(SOURCE_ATTR_SQL);
|
|
93
|
+
if (rawSql !== undefined && (typeof rawSql !== "string" || rawSql.trim() === "")) {
|
|
94
|
+
errors.push(
|
|
95
|
+
new ParseError(
|
|
96
|
+
`source.rdb on object "${obj.name}" sets @sql to an empty/whitespace-only value; ` +
|
|
97
|
+
`@sql requires a non-empty SQL body`,
|
|
98
|
+
{ code: "ERR_BAD_ATTR_VALUE", source: source.source },
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (sqlSet) hasSqlHost = true;
|
|
104
|
+
if (unmanagedSet) hasUnmanagedHost = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!hasSqlHost && !hasUnmanagedHost) continue;
|
|
108
|
+
|
|
109
|
+
// R4 / R6 — origin.*-bearing (derived) own fields under an @sql / @unmanaged
|
|
110
|
+
// host. ADR-0039: own — origin.* never inherits (ADR-0029), so isDerived()
|
|
111
|
+
// is own-only by policy (mirrors validateDerivedFieldProvidability).
|
|
112
|
+
// @sql (hard error) takes priority over @unmanaged (warn) when a host
|
|
113
|
+
// happens to declare both markers across different sources.
|
|
114
|
+
for (const field of obj.ownChildren().filter((c): c is MetaField => c.type === TYPE_FIELD && c instanceof MetaField)) {
|
|
115
|
+
if (!field.isDerived()) continue;
|
|
116
|
+
if (hasSqlHost) {
|
|
117
|
+
errors.push(
|
|
118
|
+
new ParseError(
|
|
119
|
+
`field "${obj.name}.${field.name}" carries an origin.* (derived) child, but "${obj.name}" has a ` +
|
|
120
|
+
`read source carrying @sql — the synthesized derivation and the author's verbatim SQL are two ` +
|
|
121
|
+
`sources of truth for the same body`,
|
|
122
|
+
{ code: "ERR_ORIGIN_UNDER_SQL_BODY", source: field.source },
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
warnings.push({
|
|
127
|
+
code: "WARN_ORIGIN_UNDER_UNMANAGED",
|
|
128
|
+
message:
|
|
129
|
+
`field "${obj.name}.${field.name}" carries an origin.* (derived) child, but "${obj.name}" has a ` +
|
|
130
|
+
`source marked @unmanaged — the tool never touches this object's DDL, so the derivation is ` +
|
|
131
|
+
`documented lineage only (not acted on); this is informational, not an error`,
|
|
132
|
+
source: field.source,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// R5 — a projection's row-scope @filter (#207) lowers to the outer WHERE
|
|
138
|
+
// of a TOOL-SYNTHESIZED body; with @sql the author owns the body (and its
|
|
139
|
+
// WHERE), so wrapping it is deferred cleverness (design doc D5) — reject.
|
|
140
|
+
if (hasSqlHost && obj.subType === OBJECT_SUBTYPE_PROJECTION) {
|
|
141
|
+
// ADR-0039: own — the @filter is declared locally on this projection
|
|
142
|
+
// (mirrors validateProjectionFilter).
|
|
143
|
+
const filter = obj.ownAttr(OBJECT_PROJECTION_ATTR_FILTER);
|
|
144
|
+
if (filter !== undefined) {
|
|
145
|
+
errors.push(
|
|
146
|
+
new ParseError(
|
|
147
|
+
`projection "${obj.name}" declares both @filter and an @sql read source — a view-level @filter ` +
|
|
148
|
+
`lowers to the outer WHERE of a synthesized body; with @sql the author owns the body (and its ` +
|
|
149
|
+
`WHERE), so the two cannot be combined`,
|
|
150
|
+
{ code: "ERR_ORIGIN_UNDER_SQL_BODY", source: obj.source },
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { errors, warnings };
|
|
158
|
+
}
|