@metaobjectsdev/codegen-ts 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 (66) hide show
  1. package/dist/column-mapper.d.ts +10 -0
  2. package/dist/column-mapper.d.ts.map +1 -1
  3. package/dist/column-mapper.js +21 -2
  4. package/dist/column-mapper.js.map +1 -1
  5. package/dist/naming.d.ts +4 -0
  6. package/dist/naming.d.ts.map +1 -1
  7. package/dist/naming.js +6 -0
  8. package/dist/naming.js.map +1 -1
  9. package/dist/projection/build-projection-views.d.ts +5 -1
  10. package/dist/projection/build-projection-views.d.ts.map +1 -1
  11. package/dist/projection/build-projection-views.js +165 -47
  12. package/dist/projection/build-projection-views.js.map +1 -1
  13. package/dist/projection/extract-view-spec.d.ts +8 -1
  14. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  15. package/dist/projection/extract-view-spec.js +430 -29
  16. package/dist/projection/extract-view-spec.js.map +1 -1
  17. package/dist/projection/view-ddl-emit.d.ts.map +1 -1
  18. package/dist/projection/view-ddl-emit.js +142 -25
  19. package/dist/projection/view-ddl-emit.js.map +1 -1
  20. package/dist/projection/view-spec.d.ts +101 -3
  21. package/dist/projection/view-spec.d.ts.map +1 -1
  22. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  23. package/dist/templates/drizzle-schema.js +9 -0
  24. package/dist/templates/drizzle-schema.js.map +1 -1
  25. package/dist/templates/entity-file.d.ts.map +1 -1
  26. package/dist/templates/entity-file.js +39 -3
  27. package/dist/templates/entity-file.js.map +1 -1
  28. package/dist/templates/inferred-types.d.ts +1 -1
  29. package/dist/templates/inferred-types.d.ts.map +1 -1
  30. package/dist/templates/inferred-types.js +13 -1
  31. package/dist/templates/inferred-types.js.map +1 -1
  32. package/dist/templates/projection-decl.d.ts.map +1 -1
  33. package/dist/templates/projection-decl.js +9 -70
  34. package/dist/templates/projection-decl.js.map +1 -1
  35. package/dist/templates/queries-file.d.ts.map +1 -1
  36. package/dist/templates/queries-file.js +117 -43
  37. package/dist/templates/queries-file.js.map +1 -1
  38. package/dist/templates/queries.d.ts +21 -4
  39. package/dist/templates/queries.d.ts.map +1 -1
  40. package/dist/templates/queries.js +48 -12
  41. package/dist/templates/queries.js.map +1 -1
  42. package/dist/templates/view-decl.d.ts +28 -0
  43. package/dist/templates/view-decl.d.ts.map +1 -0
  44. package/dist/templates/view-decl.js +107 -0
  45. package/dist/templates/view-decl.js.map +1 -0
  46. package/dist/templates/zod-validators.d.ts +6 -0
  47. package/dist/templates/zod-validators.d.ts.map +1 -1
  48. package/dist/templates/zod-validators.js +53 -7
  49. package/dist/templates/zod-validators.js.map +1 -1
  50. package/package.json +6 -6
  51. package/src/column-mapper.ts +26 -1
  52. package/src/naming.ts +7 -0
  53. package/src/projection/build-projection-views.ts +211 -50
  54. package/src/projection/extract-view-spec.ts +468 -29
  55. package/src/projection/view-ddl-emit.ts +158 -24
  56. package/src/projection/view-spec.ts +104 -3
  57. package/src/reference/entity.ts +11 -1
  58. package/src/reference/queries.ts +4 -1
  59. package/src/templates/drizzle-schema.ts +7 -0
  60. package/src/templates/entity-file.ts +46 -3
  61. package/src/templates/inferred-types.ts +18 -1
  62. package/src/templates/projection-decl.ts +8 -74
  63. package/src/templates/queries-file.ts +133 -48
  64. package/src/templates/queries.ts +50 -11
  65. package/src/templates/view-decl.ts +128 -0
  66. package/src/templates/zod-validators.ts +54 -9
@@ -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, JoinTree, ViewSpec } from "./view-spec.js";
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: ExpectedViewColumn[];
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
- // Only PLAIN-VIEW projections produce managed CREATE VIEW DDL. The other
93
- // read-only kinds must be skipped, not fed to extractViewSpec:
94
- // - storedProc / tableFunction (FR-015) are CALLABLES, not views. They are
95
- // base-less (no extends-bound identity), so extractViewSpec THROWS for
96
- // them and the CLI calls this function unconditionally, so one proc
97
- // projection used to crash `meta migrate` outright.
98
- // - materializedView cannot be managed by the migrate pipeline today:
99
- // there is no CREATE MATERIALIZED VIEW emit, and PG introspection cannot
100
- // even see matviews (information_schema.views excludes them), so a
101
- // "managed" matview would re-propose create-view on every run and the
102
- // apply would collide with the existing object. Worse, feeding it
103
- // through here silently created a PLAIN view under the matview's name.
104
- // Matviews are hand-managed, like the documented custom-SQL-view
105
- // exception: migrate neither creates nor drops them.
106
- // - a STANDALONE read-model a plain view projection declaring its own
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
- const spec = extractViewSpec(projection, root, { columnNamingStrategy });
123
- const baseTableName = joinTables[spec.joinTree.baseEntity];
124
- if (!baseTableName) continue; // unresolved base — skip (loader/codegen surface the error elsewhere)
125
- const body = emitViewDdl(spec, { dialect, baseTableName, joinTables, bodyOnly: true });
126
- const schema = resolveTableSchema(projection);
127
- const dependsOn = collectDependsOn(spec.joinTree, baseTableName, joinTables);
128
- const columns = collectViewColumns(spec, baseTableName, joinTables);
129
- out.push({
130
- name: spec.viewName,
131
- sql: body,
132
- dependsOn,
133
- columns,
134
- ...(schema !== undefined ? { schema } : {}),
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, deduped the physical tables the view reads. */
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
- joinTree: JoinTree,
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
  }