@metaobjectsdev/codegen-ts 0.16.0-rc.1 → 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/column-mapper.d.ts +10 -0
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +21 -2
- package/dist/column-mapper.js.map +1 -1
- package/dist/naming.d.ts +4 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +6 -0
- package/dist/naming.js.map +1 -1
- package/dist/projection/build-projection-views.d.ts +5 -1
- package/dist/projection/build-projection-views.d.ts.map +1 -1
- package/dist/projection/build-projection-views.js +165 -47
- package/dist/projection/build-projection-views.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts +8 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +430 -29
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/projection/view-ddl-emit.d.ts.map +1 -1
- package/dist/projection/view-ddl-emit.js +142 -25
- package/dist/projection/view-ddl-emit.js.map +1 -1
- package/dist/projection/view-spec.d.ts +101 -3
- package/dist/projection/view-spec.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +9 -0
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-file.d.ts.map +1 -1
- package/dist/templates/entity-file.js +39 -3
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/inferred-types.d.ts +1 -1
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +13 -1
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/projection-decl.d.ts.map +1 -1
- package/dist/templates/projection-decl.js +9 -70
- package/dist/templates/projection-decl.js.map +1 -1
- package/dist/templates/queries-file.d.ts.map +1 -1
- package/dist/templates/queries-file.js +117 -43
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/queries.d.ts +21 -4
- package/dist/templates/queries.d.ts.map +1 -1
- package/dist/templates/queries.js +48 -12
- package/dist/templates/queries.js.map +1 -1
- package/dist/templates/view-decl.d.ts +28 -0
- package/dist/templates/view-decl.d.ts.map +1 -0
- package/dist/templates/view-decl.js +107 -0
- package/dist/templates/view-decl.js.map +1 -0
- package/dist/templates/zod-validators.d.ts +6 -0
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +53 -7
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +6 -6
- package/src/column-mapper.ts +26 -1
- package/src/naming.ts +7 -0
- package/src/projection/build-projection-views.ts +211 -50
- package/src/projection/extract-view-spec.ts +468 -29
- package/src/projection/view-ddl-emit.ts +158 -24
- package/src/projection/view-spec.ts +104 -3
- package/src/reference/entity.ts +11 -1
- package/src/reference/queries.ts +4 -1
- package/src/templates/drizzle-schema.ts +7 -0
- package/src/templates/entity-file.ts +46 -3
- package/src/templates/inferred-types.ts +18 -1
- package/src/templates/projection-decl.ts +8 -74
- package/src/templates/queries-file.ts +133 -48
- package/src/templates/queries.ts +50 -11
- package/src/templates/view-decl.ts +128 -0
- package/src/templates/zod-validators.ts +54 -9
package/src/column-mapper.ts
CHANGED
|
@@ -37,6 +37,12 @@ import {
|
|
|
37
37
|
DB_COLUMN_TYPE_UUID,
|
|
38
38
|
DB_COLUMN_TYPE_JSONB,
|
|
39
39
|
FIELD_ATTR_LOCAL_TIME,
|
|
40
|
+
TYPE_ORIGIN,
|
|
41
|
+
ORIGIN_SUBTYPE_AGGREGATE,
|
|
42
|
+
ORIGIN_AGGREGATE_ATTR_AGG,
|
|
43
|
+
AGG_ANY,
|
|
44
|
+
AGG_ALL,
|
|
45
|
+
AGG_COLLECT,
|
|
40
46
|
} from "@metaobjectsdev/metadata";
|
|
41
47
|
import { columnNameFromField } from "./naming.js";
|
|
42
48
|
import { enumValues } from "./enum-meta.js";
|
|
@@ -307,6 +313,25 @@ export function isRequired(field: MetaField): boolean {
|
|
|
307
313
|
return field.validators().some((child) => child.subType === VALIDATOR_SUBTYPE_REQUIRED);
|
|
308
314
|
}
|
|
309
315
|
|
|
316
|
+
/**
|
|
317
|
+
* #195 — a field whose value is derived by an origin.aggregate `@agg:any|all|collect`
|
|
318
|
+
* is COALESCE-guaranteed non-null in the synthesized view (any→false, all→true,
|
|
319
|
+
* collect→[]), so its read type is non-null even when the field is not `@required`.
|
|
320
|
+
* Drives `.notNull()` below so the Drizzle view column AND the Zod read schema agree
|
|
321
|
+
* (projection-decl derives its `.nullable()` from these modifiers). origin.first is
|
|
322
|
+
* deliberately NOT here — an empty related set selects no row (→ null); origin.computed
|
|
323
|
+
* nullability is expression-dependent, so it stays the conservative nullable default.
|
|
324
|
+
*/
|
|
325
|
+
export function originGuaranteedNonNull(field: MetaField): boolean {
|
|
326
|
+
// ADR-0039: own — origin.* never inherits (ADR-0029), so own is correct.
|
|
327
|
+
const origin = field
|
|
328
|
+
.ownChildren()
|
|
329
|
+
.find((c) => c.type === TYPE_ORIGIN && c.subType === ORIGIN_SUBTYPE_AGGREGATE);
|
|
330
|
+
if (origin === undefined) return false;
|
|
331
|
+
const agg = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_AGG);
|
|
332
|
+
return agg === AGG_ANY || agg === AGG_ALL || agg === AGG_COLLECT;
|
|
333
|
+
}
|
|
334
|
+
|
|
310
335
|
/** The bare (package-stripped) @objectRef name on a field.object, or undefined
|
|
311
336
|
* when unset. Used as the `.$type<VO>()` target + its sibling-module import.
|
|
312
337
|
* A fully-qualified ref (acme::ai::SourceLens) strips to the short name. */
|
|
@@ -527,7 +552,7 @@ export function mapColumnType(
|
|
|
527
552
|
// Note: dollarTypeRef is read alongside (and rendered ahead of) `modifiers`
|
|
528
553
|
// by `renderColumn` — see drizzle-schema.ts.
|
|
529
554
|
|
|
530
|
-
if (isRequired(field)) {
|
|
555
|
+
if (isRequired(field) || originGuaranteedNonNull(field)) {
|
|
531
556
|
modifiers.push(".notNull()");
|
|
532
557
|
}
|
|
533
558
|
|
package/src/naming.ts
CHANGED
|
@@ -111,6 +111,13 @@ export function createFnName(entityName: string): string {
|
|
|
111
111
|
return `create${entityName}`;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** Generated insert-preserving helper name: `insertPreserving<Entity>` (#203 —
|
|
115
|
+
* the import/restore/replication escape hatch that writes @autoSet columns
|
|
116
|
+
* verbatim). Emitted only for entities that declare @autoSet fields. */
|
|
117
|
+
export function insertPreservingFnName(entityName: string): string {
|
|
118
|
+
return `insertPreserving${entityName}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
114
121
|
/** Generated update helper name: `update<Entity>`. */
|
|
115
122
|
export function updateFnName(entityName: string): string {
|
|
116
123
|
return `update${entityName}`;
|
|
@@ -21,10 +21,10 @@ import {
|
|
|
21
21
|
resolveTableName,
|
|
22
22
|
resolveTableSchema,
|
|
23
23
|
} from "@metaobjectsdev/metadata";
|
|
24
|
-
import { isProjection } from "./projection-detector.js";
|
|
25
|
-
import { extractViewSpec } from "./extract-view-spec.js";
|
|
24
|
+
import { isProjection, isWriteThrough } from "./projection-detector.js";
|
|
25
|
+
import { extractViewSpec, refNamedOwner } from "./extract-view-spec.js";
|
|
26
26
|
import { emitViewDdl } from "./view-ddl-emit.js";
|
|
27
|
-
import type { JoinNode,
|
|
27
|
+
import type { JoinNode, ViewSpec } from "./view-spec.js";
|
|
28
28
|
import type { ColumnNamingStrategy } from "../metaobjects-config.js";
|
|
29
29
|
|
|
30
30
|
/** Structurally matches migrate-ts's `ViewDescriptor` (name + body sql + optional schema). */
|
|
@@ -64,8 +64,12 @@ export interface ExpectedView {
|
|
|
64
64
|
* projection lands last, so the change stays non-destructive. (Re-canonicalizing
|
|
65
65
|
* to, say, alphabetical order would be strictly WORSE — it would scatter an
|
|
66
66
|
* appended field into the middle and force a destructive drop+create.)
|
|
67
|
+
*
|
|
68
|
+
* OMITTED for an `@sql` (#208) view: the body is opaque — the tool never parses it,
|
|
69
|
+
* so its output columns are unknown. migrate-ts reads absent columns as "unknown"
|
|
70
|
+
* and fails safe to a gated drop+create instead of an illegal CREATE OR REPLACE.
|
|
67
71
|
*/
|
|
68
|
-
columns
|
|
72
|
+
columns?: ExpectedViewColumn[];
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
export interface BuildProjectionViewsOptions {
|
|
@@ -89,54 +93,193 @@ export function buildProjectionViews(
|
|
|
89
93
|
|
|
90
94
|
const out: ExpectedView[] = [];
|
|
91
95
|
for (const projection of root.objects().filter(isProjection)) {
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
// columns, with no origin.* children — is hand-authored SQL too. Codegen
|
|
108
|
-
// already treats it that way on purpose (see projection-decl.ts: "lets a
|
|
109
|
-
// standalone read-only view-entity — explicit columns, no `extends` —
|
|
110
|
-
// generate its read model … standalone views hand-author their SQL").
|
|
111
|
-
// The SCHEMA path never got that memo: extractViewSpec THREW on it
|
|
112
|
-
// ("cannot derive the base entity"), and because the CLI calls this
|
|
113
|
-
// function unconditionally, ONE such projection aborted `meta migrate`
|
|
114
|
-
// for the ENTIRE model — every other entity included. Same crash class as
|
|
115
|
-
// the proc projections above. Skip it instead.
|
|
116
|
-
// ADR-0039: own — mirrors isProjection/viewName's own-source classification.
|
|
117
|
-
const readOnlySource = projection.ownChildren().find(
|
|
118
|
-
(c): c is MetaSource => c instanceof MetaSource && c.isReadOnly(),
|
|
119
|
-
);
|
|
120
|
-
if (readOnlySource?.effectiveKind !== SOURCE_KIND_VIEW) continue;
|
|
96
|
+
// #208 §6 — classify DDL ownership BEFORE viewIsDerived (see classifyReadOnlySource),
|
|
97
|
+
// so an escape-valve view carrying extends-bound identity/fields (pure shape / row
|
|
98
|
+
// identity) is never mis-synthesized into a wrong base-table passthrough SELECT.
|
|
99
|
+
const cls = classifyReadOnlySource(projection);
|
|
100
|
+
if (cls.kind === "skip") continue;
|
|
101
|
+
if (cls.kind === "sql") {
|
|
102
|
+
emitSqlView(projection, cls.source, root, joinTables, out);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// A plain-view projection with no extends anchor and no origin.* is a STANDALONE
|
|
106
|
+
// read-model that hand-authors its own SQL — viewIsDerived returns false and we skip
|
|
107
|
+
// it (feeding it to extractViewSpec throws "cannot derive the base entity", and the
|
|
108
|
+
// CLI calls this unconditionally, so ONE such projection would abort `meta migrate`
|
|
109
|
+
// for the whole model). Codegen already treats this shape as intended in
|
|
110
|
+
// projection-decl.ts ("standalone views hand-author their SQL").
|
|
121
111
|
if (!viewIsDerived(projection)) continue;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
112
|
+
emitViewFor(projection, root, joinTables, dialect, columnNamingStrategy, out);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// #213 hole 2 — a write-through ENTITY (FR-024 §7 read-view: a writable table
|
|
116
|
+
// source PLUS a non-primary read-only view source, with derived origin.* fields)
|
|
117
|
+
// hosts its OWN view. Emit it through the SAME canonical emitter — "one emitter,
|
|
118
|
+
// two hosts". `isWriteThrough` had zero call sites, so this replica view was never
|
|
119
|
+
// generated or owned by migrate. Unlike a projection there is no extends anchor
|
|
120
|
+
// and no viewIsDerived gate: the entity IS the base (extractViewSpec detects the
|
|
121
|
+
// write-through host), and it always has a derived view to emit. Only a plain
|
|
122
|
+
// `view` read source synthesizes CREATE VIEW DDL — a matview/proc/tableFunction
|
|
123
|
+
// read source is hand-managed, exactly as for projections above.
|
|
124
|
+
for (const entity of root.objects().filter(isWriteThrough)) {
|
|
125
|
+
// #208 §6 — same DDL-ownership classification as the projection loop. Unlike a
|
|
126
|
+
// projection there is no viewIsDerived gate: a write-through entity IS the base and
|
|
127
|
+
// always has a derived view to emit (extractViewSpec detects the write-through host).
|
|
128
|
+
const cls = classifyReadOnlySource(entity);
|
|
129
|
+
if (cls.kind === "skip") continue;
|
|
130
|
+
if (cls.kind === "sql") {
|
|
131
|
+
emitSqlView(entity, cls.source, root, joinTables, out);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
emitViewFor(entity, root, joinTables, dialect, columnNamingStrategy, out);
|
|
136
135
|
}
|
|
137
136
|
return out;
|
|
138
137
|
}
|
|
139
138
|
|
|
139
|
+
/**
|
|
140
|
+
* #208 §6 — classify a host's read-only source by DDL OWNERSHIP, BEFORE any derivation
|
|
141
|
+
* decision. Shared by the projection and write-through loops so the ownership rules can
|
|
142
|
+
* never diverge between the two host kinds (the emit TAIL is already shared via emitViewFor).
|
|
143
|
+
*
|
|
144
|
+
* - no read-only source, OR @unmanaged (external), OR a non-`view` read-only kind →
|
|
145
|
+
* "skip". The non-view read-only kinds are hand-managed and MUST NOT reach
|
|
146
|
+
* extractViewSpec:
|
|
147
|
+
* · storedProc / tableFunction (FR-015) are CALLABLES, base-less → extractViewSpec
|
|
148
|
+
* throws (and the CLI calls this unconditionally, so one proc crashed migrate);
|
|
149
|
+
* · materializedView has no CREATE-MATVIEW emit and is invisible to
|
|
150
|
+
* information_schema.views, so a "managed" matview re-proposes create every run;
|
|
151
|
+
* · @unmanaged: Flyway / a hand-migration owns the DDL (§7).
|
|
152
|
+
* - @sql read source → "sql": the author owns a verbatim body (emitSqlView).
|
|
153
|
+
* - a plain `view` read source → "derive": a tool-synthesized body (the caller applies
|
|
154
|
+
* its own viewIsDerived / standalone-read-model gate).
|
|
155
|
+
*
|
|
156
|
+
* ADR-0039: own — mirrors isProjection/viewName's own-source classification.
|
|
157
|
+
*/
|
|
158
|
+
type ReadOnlySourceClass =
|
|
159
|
+
| { kind: "skip" }
|
|
160
|
+
| { kind: "sql"; source: MetaSource }
|
|
161
|
+
| { kind: "derive"; source: MetaSource };
|
|
162
|
+
|
|
163
|
+
function classifyReadOnlySource(host: MetaObject): ReadOnlySourceClass {
|
|
164
|
+
const source = host.ownChildren().find(
|
|
165
|
+
(c): c is MetaSource => c instanceof MetaSource && c.isReadOnly(),
|
|
166
|
+
);
|
|
167
|
+
if (source === undefined) return { kind: "skip" };
|
|
168
|
+
if (source.isUnmanaged) return { kind: "skip" }; // external — Flyway/hand-migration owns it
|
|
169
|
+
if (source.sqlBody !== undefined) return { kind: "sql", source }; // author-supplied body
|
|
170
|
+
if (source.effectiveKind !== SOURCE_KIND_VIEW) return { kind: "skip" }; // matview/proc/tableFunction
|
|
171
|
+
return { kind: "derive", source };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Extract + emit one host's (projection or write-through entity) view body and
|
|
175
|
+
* push it onto `out`. The single tail shared by both walks in buildProjectionViews. */
|
|
176
|
+
function emitViewFor(
|
|
177
|
+
host: MetaObject,
|
|
178
|
+
root: MetaRoot,
|
|
179
|
+
joinTables: Record<string, string>,
|
|
180
|
+
dialect: "postgres" | "sqlite",
|
|
181
|
+
columnNamingStrategy: ColumnNamingStrategy,
|
|
182
|
+
out: ExpectedView[],
|
|
183
|
+
): void {
|
|
184
|
+
const spec = extractViewSpec(host, root, { columnNamingStrategy });
|
|
185
|
+
const baseTableName = joinTables[spec.joinTree.baseEntity];
|
|
186
|
+
if (!baseTableName) return; // unresolved base — skip (loader/codegen surface the error elsewhere)
|
|
187
|
+
const body = emitViewDdl(spec, { dialect, baseTableName, joinTables, bodyOnly: true });
|
|
188
|
+
const schema = resolveTableSchema(host);
|
|
189
|
+
const dependsOn = collectDependsOn(spec, baseTableName, joinTables);
|
|
190
|
+
const columns = collectViewColumns(spec, baseTableName, joinTables);
|
|
191
|
+
out.push({
|
|
192
|
+
name: spec.viewName,
|
|
193
|
+
sql: body,
|
|
194
|
+
dependsOn,
|
|
195
|
+
columns,
|
|
196
|
+
...(schema !== undefined ? { schema } : {}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* #208 §7 — an `@sql` read source declares a hand-written view body. Push it VERBATIM
|
|
202
|
+
* into the exact same `ExpectedView` pipeline the synthesized body rides — almost no
|
|
203
|
+
* new machinery:
|
|
204
|
+
* - buildExpectedSchema Pass 4 fingerprints `v.sql` (hash of the body);
|
|
205
|
+
* - emit stamps the COMMENT marker; author re-indentation does not re-stamp;
|
|
206
|
+
* - `columns` is OMITTED (the body is opaque — the tool never parses it), so the diff
|
|
207
|
+
* fails safe to a gated drop+create instead of a wrong-but-confident OR REPLACE;
|
|
208
|
+
* - a pre-existing unstamped view at this name → `replace-view` blocked pending
|
|
209
|
+
* `migrate --allow adopt-view` (the one-time adoption ceremony).
|
|
210
|
+
* `dependsOn` is derived from the host's own writable table (a write-through host) plus
|
|
211
|
+
* its extends-bound anchor entities (a projection, D7) — no `@dependsOn` attr.
|
|
212
|
+
*/
|
|
213
|
+
function emitSqlView(
|
|
214
|
+
host: MetaObject,
|
|
215
|
+
source: MetaSource,
|
|
216
|
+
root: MetaRoot,
|
|
217
|
+
joinTables: Record<string, string>,
|
|
218
|
+
out: ExpectedView[],
|
|
219
|
+
): void {
|
|
220
|
+
// D4 — v1 migrate lowering accepts @sql only on a plain @kind: view. matview / proc /
|
|
221
|
+
// tableFunction need genuinely new introspection (pg_matviews, pg_get_functiondef,
|
|
222
|
+
// COMMENT-on-matview, a REFRESH story) and stay hand-managed for now — an actionable
|
|
223
|
+
// hard error, same tiering as SQLite's @schema rejection.
|
|
224
|
+
if (source.effectiveKind !== SOURCE_KIND_VIEW) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`@sql is not yet migrate-managed on @kind: "${source.effectiveKind}" ` +
|
|
227
|
+
`(source of "${host.name}"). Mark the source @unmanaged, or track the ` +
|
|
228
|
+
`matview/callable managed path as a follow-up (#208 D4).`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const schema = resolveTableSchema(host);
|
|
232
|
+
const dependsOn = collectSqlDependsOn(host, root, joinTables);
|
|
233
|
+
out.push({
|
|
234
|
+
name: source.physicalName, // FR-016 four-step physical name
|
|
235
|
+
sql: source.sqlBody!, // verbatim — never parsed, never re-wrapped
|
|
236
|
+
dependsOn,
|
|
237
|
+
// columns OMITTED → "unknown" → gated drop+create fail-safe.
|
|
238
|
+
...(schema !== undefined ? { schema } : {}),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The physical tables an `@sql` view depends on. migrate-ts uses this to drop+recreate
|
|
244
|
+
* the view around a column-altering change on a source table (Postgres blocks ALTER on a
|
|
245
|
+
* column a view depends on). Two sources, no `@dependsOn` attr:
|
|
246
|
+
*
|
|
247
|
+
* - A **write-through host** (a writable table source + an `@sql` read-view source)
|
|
248
|
+
* reads from its OWN table — its one certain dependency. It has NO extends anchors
|
|
249
|
+
* (an entity declares its own identity/fields), so without this its `@sql` view's
|
|
250
|
+
* dependsOn would be empty and a column ALTER on the host table would fail at apply.
|
|
251
|
+
* - A **projection** `@sql` view's dependencies are its extends-bound anchor tables
|
|
252
|
+
* (D7 — the `extends` bindings that anchor the read model's shape ARE the dependency
|
|
253
|
+
* declaration).
|
|
254
|
+
*
|
|
255
|
+
* Deduped. (A table the opaque body JOINs but neither hosts nor anchors is NOT tracked —
|
|
256
|
+
* the deferred `@dependsOn` escape, ADR-0043.)
|
|
257
|
+
*/
|
|
258
|
+
function collectSqlDependsOn(
|
|
259
|
+
host: MetaObject,
|
|
260
|
+
root: MetaRoot,
|
|
261
|
+
joinTables: Readonly<Record<string, string>>,
|
|
262
|
+
): string[] {
|
|
263
|
+
const tables = new Set<string>();
|
|
264
|
+
// The write-through host's own writable table (keyed the way the diff keys descriptors).
|
|
265
|
+
const hasWritableSource = host.ownChildren().some(
|
|
266
|
+
(c) => c instanceof MetaSource && c.isWritable(),
|
|
267
|
+
);
|
|
268
|
+
if (hasWritableSource) {
|
|
269
|
+
const t = joinTables[host.name];
|
|
270
|
+
if (t !== undefined) tables.add(t);
|
|
271
|
+
}
|
|
272
|
+
// Extends-bound anchor tables (a projection's read-model base).
|
|
273
|
+
for (const child of host.ownChildren()) {
|
|
274
|
+
if (child.type !== TYPE_IDENTITY && child.type !== TYPE_FIELD) continue;
|
|
275
|
+
const owner = refNamedOwner(child, root);
|
|
276
|
+
if (owner === undefined) continue;
|
|
277
|
+
const t = joinTables[owner.name];
|
|
278
|
+
if (t !== undefined) tables.add(t);
|
|
279
|
+
}
|
|
280
|
+
return [...tables];
|
|
281
|
+
}
|
|
282
|
+
|
|
140
283
|
/**
|
|
141
284
|
* The SELECT list as PHYSICAL (table, column) pairs, in emitted order.
|
|
142
285
|
*
|
|
@@ -158,6 +301,14 @@ function collectViewColumns(
|
|
|
158
301
|
|
|
159
302
|
const out: ExpectedViewColumn[] = [];
|
|
160
303
|
for (const c of spec.selectSpec.columns) {
|
|
304
|
+
// #195: the four new origin column kinds (predicateAgg/collectAgg/computed/first) do
|
|
305
|
+
// not resolve to a single (table, column) SqlType via the prefix rule — computed is
|
|
306
|
+
// an expression, first is a correlated subquery, and the array/boolean aggregate
|
|
307
|
+
// result types are richer than the OR-REPLACE prefix check models. Per this module's
|
|
308
|
+
// fail-safe doctrine (unknown → drop+create, never a wrong-but-confident replace),
|
|
309
|
+
// an unknown column drops the whole list so migrate routes through a gated
|
|
310
|
+
// drop+create. Precise native typing is a later phase.
|
|
311
|
+
if (c.kind !== "passthrough" && c.kind !== "aggregate") return [];
|
|
161
312
|
const sourceTable = aliasToTable.get(c.sourceAlias);
|
|
162
313
|
// An unresolvable alias would make the column list a lie, and a wrong list would
|
|
163
314
|
// make the diff propose an ILLEGAL `CREATE OR REPLACE VIEW` that fails at apply.
|
|
@@ -217,9 +368,13 @@ function viewIsDerived(projection: MetaObject): boolean {
|
|
|
217
368
|
);
|
|
218
369
|
}
|
|
219
370
|
|
|
220
|
-
/** The base table plus every joined table
|
|
371
|
+
/** The base table plus every joined table AND every origin.first child table, deduped —
|
|
372
|
+
* the physical tables the view reads. A `first` column is a correlated subquery, so its
|
|
373
|
+
* child table is NOT in the join tree (#195) yet the view still depends on it: if the
|
|
374
|
+
* child's columns change, the view must be dropped+recreated. Missing it would let a
|
|
375
|
+
* column-altering change on that table fail at apply. */
|
|
221
376
|
function collectDependsOn(
|
|
222
|
-
|
|
377
|
+
spec: ViewSpec,
|
|
223
378
|
baseTableName: string,
|
|
224
379
|
joinTables: Readonly<Record<string, string>>,
|
|
225
380
|
): string[] {
|
|
@@ -229,6 +384,12 @@ function collectDependsOn(
|
|
|
229
384
|
if (t) tables.add(t);
|
|
230
385
|
for (const child of node.children) walk(child);
|
|
231
386
|
};
|
|
232
|
-
for (const j of joinTree.joins) walk(j);
|
|
387
|
+
for (const j of spec.joinTree.joins) walk(j);
|
|
388
|
+
for (const c of spec.selectSpec.columns) {
|
|
389
|
+
if (c.kind === "first") {
|
|
390
|
+
const t = joinTables[c.childEntity];
|
|
391
|
+
if (t) tables.add(t);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
233
394
|
return [...tables];
|
|
234
395
|
}
|