@atscript/db 0.1.139 → 0.1.141

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 (49) hide show
  1. package/dist/agg.d.cts +1 -1
  2. package/dist/agg.d.mts +1 -1
  3. package/dist/{buckets-C-27xmtq.d.cts → buckets-Bv4pah66.d.cts} +225 -22
  4. package/dist/{buckets-BFG2RYRW.d.mts → buckets-CjL7F-hp.d.mts} +225 -22
  5. package/dist/{column-diff-CgxgFKzx.cjs → column-diff-CfPNcP6e.cjs} +663 -239
  6. package/dist/{column-diff-BwOA5101.mjs → column-diff-CmFNXV8C.mjs} +638 -220
  7. package/dist/column-diff-DiBbXyLA.d.cts +211 -0
  8. package/dist/column-diff-n-k5KY0u.d.mts +211 -0
  9. package/dist/derived-rules-0sKn4f5C.mjs +44 -0
  10. package/dist/derived-rules-YstgIxG-.cjs +67 -0
  11. package/dist/index.cjs +38 -4
  12. package/dist/index.d.cts +23 -6
  13. package/dist/index.d.mts +23 -6
  14. package/dist/index.mjs +34 -4
  15. package/dist/{nested-writer-FWD5oOYh.mjs → nested-writer-BO3vhbkP.mjs} +8 -4
  16. package/dist/{nested-writer-BZNCuqI6.cjs → nested-writer-DYsRxZ5f.cjs} +8 -4
  17. package/dist/object-DSN0h9lB.d.cts +30 -0
  18. package/dist/object-DSN0h9lB.d.mts +30 -0
  19. package/dist/plugin.cjs +392 -139
  20. package/dist/plugin.mjs +392 -139
  21. package/dist/rel.cjs +2 -2
  22. package/dist/rel.d.cts +2 -2
  23. package/dist/rel.d.mts +2 -2
  24. package/dist/rel.mjs +2 -2
  25. package/dist/{relation-helpers-D3Zu0Mta.d.mts → relation-helpers-B59to_dG.d.mts} +5 -4
  26. package/dist/{relation-helpers-DxrvS6ar.d.cts → relation-helpers-DQ_nRsV9.d.cts} +5 -4
  27. package/dist/{relation-loader-6ZB_5KFq.cjs → relation-loader-CgJ8bK6X.cjs} +1 -1
  28. package/dist/{relation-loader-CTFaZpVa.mjs → relation-loader-CuhEBzFU.mjs} +1 -1
  29. package/dist/shared.cjs +6 -1
  30. package/dist/shared.d.cts +48 -9
  31. package/dist/shared.d.mts +48 -9
  32. package/dist/shared.mjs +2 -2
  33. package/dist/sync.cjs +331 -105
  34. package/dist/sync.d.cts +62 -163
  35. package/dist/sync.d.mts +62 -163
  36. package/dist/sync.mjs +331 -105
  37. package/dist/{validation-utils-B4h-GW4d.mjs → validation-utils-CMR4fe2M.mjs} +99 -34
  38. package/dist/{validation-utils-Dg0hW6dn.cjs → validation-utils-DOsB4e6G.cjs} +128 -33
  39. package/dist/{validator-Drb2N-YL.d.cts → validator-Bw6ks9Hy.d.cts} +1 -11
  40. package/dist/{validator-Drb2N-YL.d.mts → validator-Bw6ks9Hy.d.mts} +1 -11
  41. package/dist/{validator-Ch7UIQl9.mjs → validator-D8bPsXPN.mjs} +54 -2
  42. package/dist/{validator-BtZbcLN2.cjs → validator-DASnXf1j.cjs} +77 -1
  43. package/dist/validator.cjs +1 -1
  44. package/dist/validator.d.cts +2 -1
  45. package/dist/validator.d.mts +2 -1
  46. package/dist/validator.mjs +1 -1
  47. package/package.json +6 -6
  48. package/dist/column-diff-BmqvgBWw.d.cts +0 -24
  49. package/dist/column-diff-DPkbZIVE.d.mts +0 -24
package/dist/plugin.cjs CHANGED
@@ -34,21 +34,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
34
34
  }) : target, mod));
35
35
  //#endregion
36
36
  const require_aggregate_fns = require("./aggregate-fns-CGBv3E8S.cjs");
37
+ const require_derived_rules = require("./derived-rules-YstgIxG-.cjs");
37
38
  require("./consts-BzRfCcH2.cjs");
38
- const require_validation_utils = require("./validation-utils-Dg0hW6dn.cjs");
39
+ const require_validation_utils = require("./validation-utils-DOsB4e6G.cjs");
39
40
  let node_path = require("node:path");
40
41
  node_path = __toESM(node_path, 1);
41
42
  let node_url = require("node:url");
42
43
  let _atscript_core = require("@atscript/core");
43
44
  //#region src/plugin/manifest.ts
44
- const DB_ENTITY_ANNOTATIONS = [
45
- "db.table",
46
- "db.view",
47
- "db.view.for"
48
- ];
49
- function isDbEntity(node) {
50
- return DB_ENTITY_ANNOTATIONS.some((name) => node.countAnnotations(name) > 0);
51
- }
52
45
  /**
53
46
  * Renders the model-manifest module: an inventory of every exported
54
47
  * `@db.table` / `@db.view` entity in the project, grouped by `@db.space`.
@@ -77,7 +70,7 @@ async function generateModelManifest(options, output, format, repo) {
77
70
  let specifier = node_path.default.relative(manifestDir, docPath).split(node_path.default.sep).join("/");
78
71
  if (!specifier.startsWith(".")) specifier = `./${specifier}`;
79
72
  for (const [exportName, node] of [...doc.exports.entries()].toSorted(([a], [b]) => a.localeCompare(b))) {
80
- if (!isDbEntity(node)) continue;
73
+ if (!require_validation_utils.isDbSourceDecl(node)) continue;
81
74
  let alias = exportName;
82
75
  for (let n = 1; usedAliases.has(alias); n++) alias = `${exportName}_${n}`;
83
76
  usedAliases.add(alias);
@@ -131,6 +124,113 @@ async function generateModelManifest(options, output, format, repo) {
131
124
  });
132
125
  }
133
126
  //#endregion
127
+ //#region src/plugin/lsp-scopes.ts
128
+ /**
129
+ * Editor scopes of the `@db.*` query / field-path arguments and the type
130
+ * filters of the ref arguments — one function per rule. The validators in
131
+ * `annotations/*` derive their in-scope types from these SAME functions, so a
132
+ * diagnostic and what the editor completes, hovers and jumps to never diverge.
133
+ * @since 0.1.141
134
+ */
135
+ /** The `@db.view.for` entry name of a view node. */
136
+ function viewEntry(view) {
137
+ return require_validation_utils.getAnnotationAlias(view, "db.view.for");
138
+ }
139
+ /**
140
+ * `@db.view.filter` — and a conditional `@db.agg.*` of the same view: the
141
+ * entry table and every `@db.view.joins` target (`@db.alias` names included),
142
+ * in declaration order; an unqualified field belongs to the entry.
143
+ */
144
+ function viewFilterScope(view) {
145
+ const entry = viewEntry(view);
146
+ return entry ? {
147
+ allowedTypes: require_validation_utils.viewScopeTypes(view),
148
+ unqualifiedTarget: entry
149
+ } : void 0;
150
+ }
151
+ /**
152
+ * `@db.view.joins` condition: the join target, the entry table and the joins
153
+ * declared before this one (chained joins — `earlier`, computed from the
154
+ * view's {@link viewJoins} when the caller has not); an unqualified field
155
+ * belongs to the entry.
156
+ */
157
+ function viewJoinScope(view, join, earlier = require_validation_utils.earlierJoinTargets(require_validation_utils.viewJoins(view), join)) {
158
+ const entry = viewEntry(view);
159
+ const target = join.args[0]?.text;
160
+ if (!entry || !target) return;
161
+ return {
162
+ allowedTypes: [
163
+ target,
164
+ entry,
165
+ ...earlier
166
+ ],
167
+ unqualifiedTarget: entry
168
+ };
169
+ }
170
+ /** `@db.view.having`: the view's own fields, unqualified — no source type is in scope. */
171
+ function viewHavingScope(view) {
172
+ return view.id ? {
173
+ allowedTypes: [],
174
+ unqualifiedTarget: view.id
175
+ } : void 0;
176
+ }
177
+ /** The condition of a `@db.agg.*` on a view field: the scope of the view's `@db.view.filter`. */
178
+ function aggConditionScope(propToken) {
179
+ const view = require_validation_utils.getDbTableOwner(propToken);
180
+ return view ? viewFilterScope(view) : void 0;
181
+ }
182
+ /**
183
+ * The field of a `@db.agg.*` (`'amount'`, `'settings.level'`): a field path
184
+ * of the prop's chain-ref type, else of the view's `@db.view.for` entry —
185
+ * where the runtime reads the aggregate's source column from.
186
+ */
187
+ function aggFieldScope(propToken) {
188
+ const def = propToken.parentNode?.getDefinition();
189
+ const refType = def && (0, _atscript_core.isRef)(def) && def.hasChain ? def.id : void 0;
190
+ const view = refType ? void 0 : require_validation_utils.getDbTableOwner(propToken);
191
+ const target = refType ?? (view ? viewEntry(view) : void 0);
192
+ return target ? {
193
+ allowedTypes: [],
194
+ unqualifiedTarget: target
195
+ } : void 0;
196
+ }
197
+ /**
198
+ * `@db.rel.filter` on a navigational field: the related type (`Post` of
199
+ * `posts: Post[]`) and, for `@db.rel.via`, the junction table; an
200
+ * unqualified field belongs to the related type.
201
+ */
202
+ function relFilterScope(field) {
203
+ const target = require_validation_utils.getNavTargetTypeName(field);
204
+ if (!target) return;
205
+ const junction = require_validation_utils.getAnnotationAlias(field, "db.rel.via");
206
+ return {
207
+ allowedTypes: junction ? [target, junction] : [target],
208
+ unqualifiedTarget: target
209
+ };
210
+ }
211
+ /** The `fieldScope` hooks (argument token → scope) the annotation specs declare. */
212
+ const fieldScopes = {
213
+ viewFilter: (arg) => arg.parentNode ? viewFilterScope(arg.parentNode) : void 0,
214
+ viewJoin: (arg) => {
215
+ const view = arg.parentNode;
216
+ const joins = view ? require_validation_utils.viewJoins(view) : [];
217
+ const join = joins.find((a) => a.args.includes(arg));
218
+ return view && join ? viewJoinScope(view, join, require_validation_utils.earlierJoinTargets(joins, join)) : void 0;
219
+ },
220
+ viewHaving: (arg) => arg.parentNode ? viewHavingScope(arg.parentNode) : void 0,
221
+ aggCondition: aggConditionScope,
222
+ aggField: aggFieldScope,
223
+ relFilter: (arg) => arg.parentNode ? relFilterScope(arg.parentNode) : void 0
224
+ };
225
+ /** `refFilter` of the `@db.view.joins` target: a view source or a `@db.alias` of one. */
226
+ function isJoinTarget(decl) {
227
+ return require_validation_utils.isDbSourceDecl(decl) || require_validation_utils.isAliasDecl(decl);
228
+ }
229
+ /** `refFilter` of `@db.rel.via`: a `@db.table`. */
230
+ function isDbTable(decl) {
231
+ return decl.countAnnotations("db.table") > 0;
232
+ }
233
+ //#endregion
134
234
  //#region src/plugin/annotations/agg.ts
135
235
  /**
136
236
  * A conditional aggregate that is NULL when no row matches (so its field
@@ -145,7 +245,8 @@ const CONDITION_ARG = {
145
245
  name: "condition",
146
246
  type: "query",
147
247
  optional: true,
148
- description: "Row predicate of a conditional aggregate: only rows where it holds are aggregated (SQL `FN(CASE WHEN … THEN field END)`). May reference the entry table and every join; unqualified fields resolve to the entry table."
248
+ description: "Row predicate of a conditional aggregate: only rows where it holds are aggregated (SQL `FN(CASE WHEN … THEN field END)`). May reference the entry table and every join; unqualified fields resolve to the entry table.",
249
+ fieldScope: fieldScopes.aggCondition
149
250
  };
150
251
  /**
151
252
  * The rules every `@db.agg.*` shares: `'*'` is `count`'s only, and a
@@ -163,9 +264,8 @@ function validateAggArgs(name, token, args, doc) {
163
264
  });
164
265
  const condition = args[1];
165
266
  if (!condition?.queryNode) return errors;
166
- const owner = require_validation_utils.getDbTableOwner(token);
167
- const entryTypeName = owner ? require_validation_utils.getAnnotationAlias(owner, "db.view.for") : void 0;
168
- if (!owner || !entryTypeName) {
267
+ const scope = aggConditionScope(token);
268
+ if (!scope) {
169
269
  errors.push({
170
270
  message: `A conditional ${annotation} requires @db.view.for on the view`,
171
271
  severity: 1,
@@ -173,7 +273,7 @@ function validateAggArgs(name, token, args, doc) {
173
273
  });
174
274
  return errors;
175
275
  }
176
- errors.push(...require_validation_utils.validateQueryScope(condition, require_validation_utils.viewScopeTypes(owner), entryTypeName, doc));
276
+ errors.push(...require_validation_utils.validateQueryScope(condition, scope, doc));
177
277
  const prop = token.parentNode;
178
278
  if (conditionalIsNullable(name) && prop && !prop.has("optional")) {
179
279
  const field = prop.id ?? "field";
@@ -205,7 +305,8 @@ function aggSpec(name, description, field, types, extra = "") {
205
305
  name: "field",
206
306
  type: "string",
207
307
  optional: field.optional,
208
- description: field.description
308
+ description: field.description,
309
+ fieldScope: fieldScopes.aggField
209
310
  }, CONDITION_ARG],
210
311
  validate(token, args, doc) {
211
312
  const errors = validateAggArgs(name, token, args, doc);
@@ -226,6 +327,51 @@ const dbAggAnnotations = { agg: {
226
327
  max: aggSpec("max", "Declares a view field as MAX of a source column.", { description: "Source column name." })
227
328
  } };
228
329
  //#endregion
330
+ //#region src/plugin/annotations/alias.ts
331
+ /** The `accept` rule of a view-source ref argument (`@db.view.for`, `@db.alias`). */
332
+ const VIEW_SOURCE_ARGUMENT = {
333
+ accept: require_validation_utils.isDbSourceDecl,
334
+ expected: "must be a @db.table or a @db.view."
335
+ };
336
+ /**
337
+ * `@db.alias <Target>` — a named join scope over a table or view, so a view
338
+ * can join the same table twice or join its own entry table (self-join).
339
+ * @since 0.1.141
340
+ */
341
+ const dbAliasAnnotations = { alias: new _atscript_core.AnnotationSpec({
342
+ description: "Declares a **join alias**: a type alias of a `@db.table` or `@db.view` that a view can join under its own name — to join the same table twice or to self-join the entry table. Only valid on `export type X = Target`, where `Target` is the argument. The alias is a scope name inside view definitions, not a table: it is never synced or registered on a `DbSpace`, and it cannot be the `@db.view.for` entry.\n\n**Example:**\n```atscript\n@db.alias Employee\nexport type Manager = Employee\n\n@db.view.for Employee\n@db.view.joins Manager, `Manager.id = Employee.managerId`, 'left'\nexport interface Staff {\n id: Employee.id\n managerName?: Manager.name\n}\n```\n",
343
+ nodeType: ["type"],
344
+ passedWhenReferred: false,
345
+ argument: {
346
+ name: "target",
347
+ type: "ref",
348
+ description: "The aliased table or view type (must have @db.table or @db.view).",
349
+ refFilter: require_validation_utils.isDbSourceDecl
350
+ },
351
+ validate(token, args, doc) {
352
+ const errors = [];
353
+ const owner = token.parentNode;
354
+ const target = args[0]?.text;
355
+ for (const name of require_derived_rules.DB_ENTITY_ANNOTATIONS) if (owner.countAnnotations(name) > 0) {
356
+ errors.push({
357
+ message: `A @db.alias type cannot carry @${name} — it names a join scope over "${target ?? "…"}", not a table or view`,
358
+ severity: 1,
359
+ range: token.range
360
+ });
361
+ break;
362
+ }
363
+ if (!target) return errors;
364
+ const def = owner.getDefinition();
365
+ if ((def && (0, _atscript_core.isRef)(def) && !def.hasChain ? def.id : void 0) !== target) errors.push({
366
+ message: `@db.alias ${target} must be declared on 'export type ${owner.id ?? "X"} = ${target}' — the type must be a plain reference to the aliased ${target}`,
367
+ severity: 1,
368
+ range: token.range
369
+ });
370
+ errors.push(...require_validation_utils.validateRefArgument(args[0], doc, VIEW_SOURCE_ARGUMENT));
371
+ return errors;
372
+ }
373
+ }) };
374
+ //#endregion
229
375
  //#region src/plugin/annotations/amount.ts
230
376
  const CURRENCY_CODE_PATTERN = /^[A-Z0-9]{2,10}$/;
231
377
  const dbAmountAnnotations = { amount: { currency: {
@@ -280,6 +426,100 @@ const dbAmountAnnotations = { amount: { currency: {
280
426
  })
281
427
  } } };
282
428
  //#endregion
429
+ //#region src/shared/view-validation.ts
430
+ /** `@db.agg.*` annotations that are never NULL (the counts: 0 over no value). */
431
+ const NULL_SAFE_AGG_ANNOTATIONS = require_aggregate_fns.SUPPORTED_AGGREGATE_FNS.filter((fn) => !require_aggregate_fns.NULL_WHEN_EMPTY_AGGREGATE_FNS.has(fn)).map((fn) => `db.agg.${fn}`);
432
+ /**
433
+ * Walks a chain ref step by step (`Order.payload`, `Order.payload.customer`, …)
434
+ * and reports the JSON root, arrays, encryption, optionality and the leaf type.
435
+ * A chain shorter than 2 is not a field path (`resolved: false`).
436
+ * @since 0.1.141
437
+ */
438
+ function jsonChainInfo(ref, doc) {
439
+ const typeName = ref.id ?? "";
440
+ const chain = ref.chain.map((t) => t.text);
441
+ const info = {
442
+ typeName,
443
+ chain,
444
+ resolved: false,
445
+ viaArray: false,
446
+ viaEncrypted: false,
447
+ optional: false
448
+ };
449
+ if (!typeName || chain.length < 2) return info;
450
+ for (let i = 1; i <= chain.length; i++) {
451
+ const step = doc.unwindType(typeName, chain.slice(0, i));
452
+ if (!step) return info;
453
+ const node = step.node;
454
+ const prop = node && (0, _atscript_core.isProp)(node) ? node : void 0;
455
+ if (prop?.has("optional")) info.optional = true;
456
+ if (prop && prop.countAnnotations("db.encrypted") > 0) info.viaEncrypted = true;
457
+ const isJsonProp = prop !== void 0 && prop.countAnnotations("db.json") > 0;
458
+ const isArr = (0, _atscript_core.isArray)(step.def);
459
+ if (isArr) info.viaArray = true;
460
+ if (info.jsonRoot === void 0 && (isJsonProp || isArr)) info.jsonRoot = i;
461
+ if (i === chain.length) info.leafType = require_validation_utils.primitiveBaseType(step.def);
462
+ }
463
+ info.resolved = true;
464
+ return info;
465
+ }
466
+ /** Props of a view interface (its own structure; views don't use `extends`). */
467
+ function viewProps(owner) {
468
+ if ((0, _atscript_core.isInterface)(owner)) return owner.props;
469
+ const def = owner.getDefinition();
470
+ return def && (0, _atscript_core.isStructure)(def) ? def.props : void 0;
471
+ }
472
+ /**
473
+ * VW8: a chain ref that passes through a `@db.json` or array node reads a
474
+ * JSON leaf, which views can only extract as a string / number / boolean.
475
+ * Intermediate nodes that `unwindType` can't reach are skipped (sync-time
476
+ * resolution reports them).
477
+ */
478
+ function validateJsonChain(fieldName, ref, doc, range) {
479
+ const info = jsonChainInfo(ref, doc);
480
+ if (!info.resolved || info.jsonRoot === void 0 || info.jsonRoot >= info.chain.length) return [];
481
+ if (info.leafType !== void 0 && require_derived_rules.JSON_LEAF_TYPES.has(info.leafType)) return [];
482
+ return [{
483
+ message: `Field "${fieldName}" reads "${info.typeName}.${info.chain.join(".")}" inside a JSON-stored field — it must end at a string, number or boolean leaf`,
484
+ severity: 1,
485
+ range
486
+ }];
487
+ }
488
+ /**
489
+ * Whole-view checks, run once per `@db.view.for` interface:
490
+ *
491
+ * - VW7 — a field reading from a left-joined table must be optional (the
492
+ * join yields NULL for unmatched rows); `@db.agg.count` / `countDistinct`
493
+ * fields are exempt (they count 0, never NULL).
494
+ * - VW8 — a chain ref through a `@db.json` or array node must end at a
495
+ * primitive string / number / boolean leaf.
496
+ * @since 0.1.136
497
+ */
498
+ function validateViewInterface(owner, doc) {
499
+ const errors = [];
500
+ const props = viewProps(owner);
501
+ if (!props) return errors;
502
+ const leftJoined = /* @__PURE__ */ new Set();
503
+ for (const join of require_validation_utils.viewJoins(owner)) {
504
+ const target = join.args[0]?.text;
505
+ if (target && join.args[2]?.text === "left") leftJoined.add(target);
506
+ }
507
+ for (const [fieldName, prop] of props) {
508
+ const def = prop.getDefinition();
509
+ if (!def || !(0, _atscript_core.isRef)(def)) continue;
510
+ const ref = def;
511
+ const range = (prop.token("identifier") ?? ref.token("identifier"))?.range;
512
+ if (!range) continue;
513
+ if (ref.id && leftJoined.has(ref.id) && !prop.has("optional") && !NULL_SAFE_AGG_ANNOTATIONS.some((name) => prop.countAnnotations(name) > 0)) errors.push({
514
+ message: `Field "${fieldName}" reads from left-joined "${ref.id}" and must be optional (${fieldName}?: …)`,
515
+ severity: 1,
516
+ range
517
+ });
518
+ errors.push(...validateJsonChain(fieldName, ref, doc, range));
519
+ }
520
+ return errors;
521
+ }
522
+ //#endregion
283
523
  //#region src/plugin/annotations/column.ts
284
524
  const dbColumnAnnotations = {
285
525
  patch: { strategy: new _atscript_core.AnnotationSpec({
@@ -367,6 +607,66 @@ const dbColumnAnnotations = {
367
607
  return require_validation_utils.validateFieldBaseType(token, doc, "@db.column.precision", ["number", "decimal"]);
368
608
  }
369
609
  }),
610
+ derived: new _atscript_core.AnnotationSpec({
611
+ description: "Declares a **derived column**: a read-only column computed from one `string`, `number` or `boolean` leaf inside a `@db.json` field of the **same table**, so the leaf can be filtered, sorted, grouped and indexed like a real column. The field's type is a chain reference into that field (`customerId: Order.payload.customer.id`); the path may not cross an array or a `@db.encrypted` field. Declare the field optional when the path can be absent (a missing leaf reads as `null`). A value written to it is dropped; `$inc` / `$dec` / `$mul` on it are rejected.\n\nStorage per adapter, schema sync and the compatible annotations: https://atscript.dev/db/api/storage#derived-columns\n\n**Example:**\n```atscript\n@db.table 'orders'\nexport interface Order {\n @meta.id\n id: number\n\n @db.json\n payload: {\n customer: { id: string, vip: boolean }\n total: number\n }\n\n @db.column.derived\n @db.index.plain\n customerId: Order.payload.customer.id\n\n @db.column.derived\n vip?: Order.payload.customer.vip\n}\n```\n",
612
+ nodeType: ["prop"],
613
+ passedWhenReferred: false,
614
+ multiple: false,
615
+ validate(token, _args, doc) {
616
+ const errors = [];
617
+ const field = token.parentNode;
618
+ const fail = (message, severity = 1) => {
619
+ errors.push({
620
+ message,
621
+ severity,
622
+ range: token.range
623
+ });
624
+ };
625
+ const owner = require_validation_utils.getDbTableOwner(token);
626
+ if (!owner || !(0, _atscript_core.isInterface)(owner) || owner.countAnnotations("db.table") === 0) {
627
+ fail("@db.column.derived is only valid on a top-level field of a @db.table interface");
628
+ return errors;
629
+ }
630
+ for (const [name, why] of require_derived_rules.DERIVED_INCOMPATIBLE) if (field.countAnnotations(name) > 0) fail(`@db.column.derived cannot coexist with @${name} — ${why}`);
631
+ const definition = field.getDefinition();
632
+ if (!definition || !(0, _atscript_core.isRef)(definition) || !definition.hasChain) {
633
+ fail("@db.column.derived requires a chain reference into a @db.json field of the same table (e.g. `customerId: Order.payload.customer.id`)");
634
+ return errors;
635
+ }
636
+ const ref = definition;
637
+ const tableName = require_validation_utils.getParentTypeName(token);
638
+ if (ref.id !== tableName) {
639
+ fail(`@db.column.derived must reference the enclosing table '${tableName ?? ""}', not '${ref.id ?? ""}' — a derived column reads its own row`);
640
+ return errors;
641
+ }
642
+ const info = jsonChainInfo(ref, doc);
643
+ const path = `${info.typeName}.${info.chain.join(".")}`;
644
+ const notInsideJson = `@db.column.derived path '${path}' does not read inside a @db.json field — a flattened or scalar column needs no derived column`;
645
+ if (info.chain.length < 2) {
646
+ fail(notInsideJson);
647
+ return errors;
648
+ }
649
+ if (!info.resolved) return errors;
650
+ if (info.viaArray) {
651
+ fail(`@db.column.derived path '${path}' crosses an array — a derived column reads one scalar leaf`);
652
+ return errors;
653
+ }
654
+ if (info.jsonRoot === void 0 || info.jsonRoot >= info.chain.length) {
655
+ fail(notInsideJson);
656
+ return errors;
657
+ }
658
+ if (info.viaEncrypted) {
659
+ fail(`@db.column.derived path '${path}' reads inside a @db.encrypted field — ciphertext cannot be extracted`);
660
+ return errors;
661
+ }
662
+ if (info.leafType === void 0 || !require_derived_rules.JSON_LEAF_TYPES.has(info.leafType)) {
663
+ fail(`@db.column.derived path '${path}' must end at a string, number or boolean leaf` + (info.leafType ? ` (got '${info.leafType}')` : ""));
664
+ return errors;
665
+ }
666
+ if (info.optional && !field.has("optional")) fail(`@db.column.derived path '${path}' may be absent — declare the field optional (\`${field.id ?? ""}?:\`) so a missing leaf reads as null`, 2);
667
+ return errors;
668
+ }
669
+ }),
370
670
  dimension: new _atscript_core.AnnotationSpec({
371
671
  description: "Marks a field as a dimension — groupable in aggregate queries ($groupBy). Dimension fields automatically receive a database index during schema sync.",
372
672
  nodeType: ["prop"],
@@ -901,7 +1201,8 @@ const dbRelAnnotations = { rel: {
901
1201
  argument: {
902
1202
  name: "junction",
903
1203
  type: "ref",
904
- description: "The junction table type (must have @db.table and @db.rel.FK fields pointing to both sides)."
1204
+ description: "The junction table type (must have @db.table and @db.rel.FK fields pointing to both sides).",
1205
+ refFilter: isDbTable
905
1206
  },
906
1207
  validate(token, args, doc) {
907
1208
  const errors = [];
@@ -918,7 +1219,10 @@ const dbRelAnnotations = { rel: {
918
1219
  });
919
1220
  if (!args[0]) return errors;
920
1221
  const junctionName = args[0].text;
921
- errors.push(...require_validation_utils.validateRefArgument(args[0], doc, { requireDbTable: true }));
1222
+ errors.push(...require_validation_utils.validateRefArgument(args[0], doc, {
1223
+ accept: isDbTable,
1224
+ expected: "must have @db.table annotation."
1225
+ }));
922
1226
  if (errors.length > 0) return errors;
923
1227
  const junctionUnwound = doc.unwindType(junctionName);
924
1228
  if (!junctionUnwound) return errors;
@@ -961,7 +1265,8 @@ const dbRelAnnotations = { rel: {
961
1265
  argument: {
962
1266
  name: "condition",
963
1267
  type: "query",
964
- description: "Filter expression restricting which related records are loaded."
1268
+ description: "Filter expression restricting which related records are loaded — references the related type (and the @db.rel.via junction); unqualified fields belong to the related type.",
1269
+ fieldScope: fieldScopes.relFilter
965
1270
  },
966
1271
  validate(token, args, doc) {
967
1272
  const errors = [];
@@ -978,14 +1283,8 @@ const dbRelAnnotations = { rel: {
978
1283
  return errors;
979
1284
  }
980
1285
  if (!args[0]?.queryNode) return errors;
981
- const targetTypeName = require_validation_utils.getNavTargetTypeName(field);
982
- if (!targetTypeName) return errors;
983
- const allowedTypes = [targetTypeName];
984
- if (hasVia) {
985
- const junctionType = require_validation_utils.getAnnotationAlias(field, "db.rel.via");
986
- if (junctionType) allowedTypes.push(junctionType);
987
- }
988
- errors.push(...require_validation_utils.validateQueryScope(args[0], allowedTypes, targetTypeName, doc));
1286
+ const scope = relFilterScope(field);
1287
+ if (scope) errors.push(...require_validation_utils.validateQueryScope(args[0], scope, doc));
989
1288
  return errors;
990
1289
  }
991
1290
  })
@@ -1074,6 +1373,7 @@ const dbTableAnnotations = {
1074
1373
  $self: new _atscript_core.AnnotationSpec({
1075
1374
  description: "Marks an interface as a database-persisted entity (table in SQL, collection in MongoDB). If the name argument is omitted, the adapter derives the table name from the interface name.\n\n**Example:**\n```atscript\n@db.table \"users\"\nexport interface User { ... }\n```\n",
1076
1375
  nodeType: ["interface"],
1376
+ passedWhenReferred: false,
1077
1377
  argument: {
1078
1378
  optional: true,
1079
1379
  name: "name",
@@ -1094,6 +1394,7 @@ const dbTableAnnotations = {
1094
1394
  renamed: new _atscript_core.AnnotationSpec({
1095
1395
  description: "Specifies the previous table name for table rename migration. The sync engine generates ALTER TABLE RENAME instead of drop+create.\n\n**Example:**\n```atscript\n@db.table \"app_users\"\n@db.table.renamed \"users\"\nexport interface User { ... }\n```\n",
1096
1396
  nodeType: ["interface"],
1397
+ passedWhenReferred: false,
1097
1398
  argument: {
1098
1399
  name: "oldName",
1099
1400
  type: "string",
@@ -1112,6 +1413,7 @@ const dbTableAnnotations = {
1112
1413
  preferredId: { uniqueIndex: new _atscript_core.AnnotationSpec({
1113
1414
  description: "Selects a unique index as the table's preferred row identifier. When the unique-index name is omitted, the first declared unique-index group wins.\n\n**Example:**\n```atscript\n@db.table\n@db.table.preferredId.uniqueIndex \"by_slug\"\nexport interface Post {\n @db.index.unique \"by_slug\"\n slug: string\n}\n```\n",
1114
1415
  nodeType: ["interface"],
1416
+ passedWhenReferred: false,
1115
1417
  multiple: false,
1116
1418
  argument: {
1117
1419
  optional: true,
@@ -1159,6 +1461,7 @@ const dbTableAnnotations = {
1159
1461
  schema: new _atscript_core.AnnotationSpec({
1160
1462
  description: "Assigns the entity to a database schema/namespace.\n\n**Example:**\n```atscript\n@db.table \"users\"\n@db.schema \"auth\"\nexport interface User { ... }\n```\n",
1161
1463
  nodeType: ["interface"],
1464
+ passedWhenReferred: false,
1162
1465
  argument: {
1163
1466
  name: "name",
1164
1467
  type: "string",
@@ -1168,6 +1471,7 @@ const dbTableAnnotations = {
1168
1471
  space: new _atscript_core.AnnotationSpec({
1169
1472
  description: "Binds the entity to a named database space (a `DbSpace` — one per physical database) for apps running more than one database at once (e.g. MongoDB + PostgreSQL). Absent means the default space.\n\nConsumed by the generated model manifest (grouping in `modelsBySpace`) and by `@TableController(Model)` token binding, which resolves the space registered under this name via `provideDbSpace(space, name)`.\n\n**Example:**\n```atscript\n@db.table \"feed_runs\"\n@db.space \"analytics\"\nexport interface FeedRun { ... }\n```\n",
1170
1473
  nodeType: ["interface"],
1474
+ passedWhenReferred: false,
1171
1475
  argument: {
1172
1476
  name: "name",
1173
1477
  type: "string",
@@ -1177,6 +1481,7 @@ const dbTableAnnotations = {
1177
1481
  http: { path: new _atscript_core.AnnotationSpec({
1178
1482
  description: "HTTP endpoint path where this table is served. Used by the UI for value-help on FK fields. Gets overwritten by the final controller prefix at runtime.\n\n**Example:**\n```atscript\n@db.table \"authors\"\n@db.http.path \"/authors\"\nexport interface Author { ... }\n```\n",
1179
1483
  nodeType: ["interface"],
1484
+ passedWhenReferred: false,
1180
1485
  argument: {
1181
1486
  name: "path",
1182
1487
  type: "string",
@@ -1186,6 +1491,7 @@ const dbTableAnnotations = {
1186
1491
  sync: { method: new _atscript_core.AnnotationSpec({
1187
1492
  description: "Controls how the sync engine handles structural changes that cannot be applied via ALTER TABLE.\n\n- `\"recreate\"` — lossless: create temp table, copy data, drop old, rename.\n- `\"drop\"` — lossy: drop table entirely and create from scratch.\n\nWithout this annotation, structural changes fail with an error requiring manual intervention.\n\n**Example:**\n```atscript\n@db.sync.method \"drop\"\ninterface Logs { ... }\n```\n",
1188
1493
  nodeType: ["interface"],
1494
+ passedWhenReferred: false,
1189
1495
  argument: {
1190
1496
  name: "method",
1191
1497
  type: "string",
@@ -1196,6 +1502,7 @@ const dbTableAnnotations = {
1196
1502
  depth: { limit: new _atscript_core.AnnotationSpec({
1197
1503
  description: "Security guard on nested-write payloads. `N` is a non-negative integer declaring the maximum depth a client may nest `@db.rel.from` children in insert, replace, or patch payloads. Writes deeper than `N` are rejected at the server boundary with HTTP 400 before any DB access.\n\n**Default when absent:** `0` — any nested-write payload is rejected. Authors opt in explicitly to `N >= 1` when they want the server to accept deep writes. This is a security / blast-radius control, not a performance knob.\n\n**Scope:** affects only write acceptance. Has no effect on `/meta` serialization, read/query paths, or wire shape — the meta endpoint always ships FK refs as the shallow `{ id, metadata }` shape regardless of this annotation.\n\n**Example:**\n```atscript\n@db.table \"authors\"\n@db.depth.limit 2\nexport interface Author { ... }\n```\n",
1198
1504
  nodeType: ["interface"],
1505
+ passedWhenReferred: false,
1199
1506
  multiple: false,
1200
1507
  argument: {
1201
1508
  name: "depth",
@@ -1261,6 +1568,7 @@ function tableCapability(capability) {
1261
1568
  @db.table.${capability} "manual"\nexport interface User {
1262
1569
  ` + example + "}\n```\n",
1263
1570
  nodeType: ["interface"],
1571
+ passedWhenReferred: false,
1264
1572
  multiple: false,
1265
1573
  argument: {
1266
1574
  optional: true,
@@ -1329,91 +1637,12 @@ const dbUnitAnnotations = { unit: {
1329
1637
  })
1330
1638
  } };
1331
1639
  //#endregion
1332
- //#region src/shared/view-validation.ts
1333
- /** `@db.agg.*` annotations that are never NULL (the counts: 0 over no value). */
1334
- const NULL_SAFE_AGG_ANNOTATIONS = require_aggregate_fns.SUPPORTED_AGGREGATE_FNS.filter((fn) => !require_aggregate_fns.NULL_WHEN_EMPTY_AGGREGATE_FNS.has(fn)).map((fn) => `db.agg.${fn}`);
1335
- /** Primitive leaf types a JSON-stored path may end at. */
1336
- const JSON_LEAF_TYPES = new Set([
1337
- "string",
1338
- "number",
1339
- "boolean"
1340
- ]);
1341
- /** Props of a view interface (its own structure; views don't use `extends`). */
1342
- function viewProps(owner) {
1343
- if ((0, _atscript_core.isInterface)(owner)) return owner.props;
1344
- const def = owner.getDefinition();
1345
- return def && (0, _atscript_core.isStructure)(def) ? def.props : void 0;
1346
- }
1347
- /**
1348
- * VW8: a chain ref that passes through a `@db.json` or array node reads a
1349
- * JSON leaf, which views can only extract as a string / number / boolean.
1350
- * Intermediate nodes that `unwindType` can't reach are skipped (sync-time
1351
- * resolution reports them).
1352
- */
1353
- function validateJsonChain(fieldName, ref, doc, range) {
1354
- const typeName = ref.id;
1355
- const chain = ref.chain.map((t) => t.text);
1356
- if (!typeName || chain.length < 2) return [];
1357
- let throughJson = false;
1358
- for (let i = 1; i < chain.length; i++) {
1359
- const step = doc.unwindType(typeName, chain.slice(0, i));
1360
- if (!step) return [];
1361
- const stepNode = step.node;
1362
- if (stepNode && (0, _atscript_core.isProp)(stepNode) && stepNode.countAnnotations("db.json") > 0 || (0, _atscript_core.isArray)(step.def)) {
1363
- throughJson = true;
1364
- break;
1365
- }
1366
- }
1367
- if (!throughJson) return [];
1368
- const leaf = doc.unwindType(typeName, chain)?.def;
1369
- const leafType = require_validation_utils.primitiveBaseType(leaf);
1370
- if (leafType !== void 0 && JSON_LEAF_TYPES.has(leafType)) return [];
1371
- return [{
1372
- message: `Field "${fieldName}" reads "${typeName}.${chain.join(".")}" inside a JSON-stored field — it must end at a string, number or boolean leaf`,
1373
- severity: 1,
1374
- range
1375
- }];
1376
- }
1377
- /**
1378
- * Whole-view checks, run once per `@db.view.for` interface:
1379
- *
1380
- * - VW7 — a field reading from a left-joined table must be optional (the
1381
- * join yields NULL for unmatched rows); `@db.agg.count` / `countDistinct`
1382
- * fields are exempt (they count 0, never NULL).
1383
- * - VW8 — a chain ref through a `@db.json` or array node must end at a
1384
- * primitive string / number / boolean leaf.
1385
- * @since 0.1.136
1386
- */
1387
- function validateViewInterface(owner, doc) {
1388
- const errors = [];
1389
- const props = viewProps(owner);
1390
- if (!props) return errors;
1391
- const leftJoined = /* @__PURE__ */ new Set();
1392
- for (const join of require_validation_utils.viewJoins(owner)) {
1393
- const target = join.args[0]?.text;
1394
- if (target && join.args[2]?.text === "left") leftJoined.add(target);
1395
- }
1396
- for (const [fieldName, prop] of props) {
1397
- const def = prop.getDefinition();
1398
- if (!def || !(0, _atscript_core.isRef)(def)) continue;
1399
- const ref = def;
1400
- const range = (prop.token("identifier") ?? ref.token("identifier"))?.range;
1401
- if (!range) continue;
1402
- if (ref.id && leftJoined.has(ref.id) && !prop.has("optional") && !NULL_SAFE_AGG_ANNOTATIONS.some((name) => prop.countAnnotations(name) > 0)) errors.push({
1403
- message: `Field "${fieldName}" reads from left-joined "${ref.id}" and must be optional (${fieldName}?: …)`,
1404
- severity: 1,
1405
- range
1406
- });
1407
- errors.push(...validateJsonChain(fieldName, ref, doc, range));
1408
- }
1409
- return errors;
1410
- }
1411
- //#endregion
1412
1640
  //#region src/plugin/annotations/view.ts
1413
1641
  const dbViewAnnotations = { view: {
1414
1642
  $self: new _atscript_core.AnnotationSpec({
1415
1643
  description: "Marks an interface as a **database view**. Optionally takes a view name argument.\n\n**Example:**\n```atscript\n@db.view \"active_premium_users\"\n@db.view.for User\nexport interface ActivePremiumUser { ... }\n```\n",
1416
1644
  nodeType: ["interface"],
1645
+ passedWhenReferred: false,
1417
1646
  argument: {
1418
1647
  optional: true,
1419
1648
  name: "name",
@@ -1431,12 +1660,14 @@ const dbViewAnnotations = { view: {
1431
1660
  }
1432
1661
  }),
1433
1662
  for: new _atscript_core.AnnotationSpec({
1434
- description: "Specifies the entry/primary table for a computed view. Required for views that map fields via chain refs.\n\n**Example:**\n```atscript\n@db.view.for Order\n@db.view.filter `Order.status = 'active'`\nexport interface ActiveOrderDetails { ... }\n```\n",
1663
+ description: "Specifies the entry/primary source for a computed view — a `@db.table`, or another `@db.view` (views over views, since 0.1.141). Required for views that map fields via chain refs. A `@db.alias` type cannot be the entry.\n\n**Example:**\n```atscript\n@db.view.for Order\n@db.view.filter `Order.status = 'active'`\nexport interface ActiveOrderDetails { ... }\n```\n",
1435
1664
  nodeType: ["interface"],
1665
+ passedWhenReferred: false,
1436
1666
  argument: {
1437
1667
  name: "entry",
1438
1668
  type: "ref",
1439
- description: "The primary/entry table type (must have @db.table)."
1669
+ description: "The primary/entry table or view type (must have @db.table or @db.view).",
1670
+ refFilter: require_validation_utils.isDbSourceDecl
1440
1671
  },
1441
1672
  validate(token, args, doc) {
1442
1673
  const errors = [];
@@ -1446,26 +1677,35 @@ const dbViewAnnotations = { view: {
1446
1677
  severity: 1,
1447
1678
  range: token.range
1448
1679
  });
1449
- if (args[0]) errors.push(...require_validation_utils.validateRefArgument(args[0], doc, { requireDbTable: true }));
1680
+ if (args[0]) errors.push(...require_validation_utils.validateRefArgument(args[0], doc, VIEW_SOURCE_ARGUMENT));
1681
+ const cycle = require_validation_utils.findViewCycle(owner, doc);
1682
+ if (cycle) errors.push({
1683
+ message: `View '${cycle[0]}' depends on itself: ${cycle.join(" → ")}`,
1684
+ severity: 1,
1685
+ range: args[0]?.range ?? token.range
1686
+ });
1450
1687
  errors.push(...validateViewInterface(owner, doc));
1451
1688
  return errors;
1452
1689
  }
1453
1690
  }),
1454
1691
  joins: new _atscript_core.AnnotationSpec({
1455
- description: "Declares an explicit join for a view. Joins are INNER by default — pass `'left'` as the third argument to keep entry rows without a match (fields read from a left-joined table must be optional). A join condition may reference the entry table and joins declared before it (chained joins); a table can be joined once (no aliases / self-joins).\n\n**Example:**\n```atscript\n@db.view.for Order\n@db.view.joins Customer, `Customer.id = Order.customerId`\n@db.view.joins Region, `Region.id = Customer.regionId`, 'left'\nexport interface OrderRegion { ... }\n```\n",
1692
+ description: "Declares an explicit join for a view. Joins are INNER by default — pass `'left'` as the third argument to keep entry rows without a match (fields read from a left-joined table must be optional). A join condition may reference the entry table and joins declared before it (chained joins). The target is a `@db.table`, a `@db.view` (since 0.1.141), or a `@db.alias` type — the way to join one table twice or to self-join the entry table: every scope name (entry + joins) must be unique.\n\n**Example:**\n```atscript\n@db.view.for Order\n@db.view.joins Customer, `Customer.id = Order.customerId`\n@db.view.joins Region, `Region.id = Customer.regionId`, 'left'\nexport interface OrderRegion { ... }\n```\n",
1456
1693
  nodeType: ["interface"],
1694
+ passedWhenReferred: false,
1457
1695
  multiple: true,
1458
1696
  mergeStrategy: "append",
1459
1697
  argument: [
1460
1698
  {
1461
1699
  name: "target",
1462
1700
  type: "ref",
1463
- description: "The table type to join (must have @db.table)."
1701
+ description: "The type to join: a @db.table, a @db.view, or a @db.alias of one (for a second join of the same table / a self-join).",
1702
+ refFilter: isJoinTarget
1464
1703
  },
1465
1704
  {
1466
1705
  name: "condition",
1467
1706
  type: "query",
1468
- description: "Join condition expression."
1707
+ description: "Join condition expression — may reference the join target, the entry table and the joins declared before this one.",
1708
+ fieldScope: fieldScopes.viewJoin
1469
1709
  },
1470
1710
  {
1471
1711
  name: "kind",
@@ -1486,7 +1726,10 @@ const dbViewAnnotations = { view: {
1486
1726
  });
1487
1727
  return errors;
1488
1728
  }
1489
- if (args[0]) errors.push(...require_validation_utils.validateRefArgument(args[0], doc, { requireDbTable: true }));
1729
+ if (args[0]) errors.push(...require_validation_utils.validateRefArgument(args[0], doc, {
1730
+ accept: isJoinTarget,
1731
+ expected: "must be a @db.table or a @db.view."
1732
+ }));
1490
1733
  const entryTypeName = require_validation_utils.getAnnotationAlias(owner, "db.view.for");
1491
1734
  if (!entryTypeName) {
1492
1735
  errors.push({
@@ -1496,40 +1739,40 @@ const dbViewAnnotations = { view: {
1496
1739
  });
1497
1740
  return errors;
1498
1741
  }
1499
- const allJoins = require_validation_utils.viewJoins(owner);
1500
- const position = allJoins.findIndex((a) => a.token === token);
1501
- const earlier = require_validation_utils.joinTargets(position === -1 ? [] : allJoins.slice(0, position));
1742
+ const joins = require_validation_utils.viewJoins(owner);
1743
+ const join = joins.find((a) => a.token === token);
1744
+ const earlier = join ? require_validation_utils.earlierJoinTargets(joins, join) : [];
1502
1745
  if (args[0]) {
1503
1746
  const target = args[0].text;
1747
+ const aliasHint = (name) => {
1748
+ const decl = doc.getDeclarationOwnerNode(target)?.node;
1749
+ return decl && require_validation_utils.isAliasDecl(decl) ? "" : ` — declare a @db.alias type (\`@db.alias ${name}\` + \`export type Other = ${name}\`) to join it under another name`;
1750
+ };
1504
1751
  if (target === entryTypeName) errors.push({
1505
- message: `@db.view.joins cannot join the entry table '${target}' — no join aliases / self-joins yet`,
1752
+ message: `@db.view.joins cannot join the entry table '${target}' directly${aliasHint(target)}`,
1506
1753
  severity: 1,
1507
1754
  range: args[0].range
1508
1755
  });
1509
1756
  else if (earlier.includes(target)) errors.push({
1510
- message: `'${target}' is joined more than once — no join aliases / self-joins yet`,
1757
+ message: `'${target}' is joined more than once${aliasHint(target)}`,
1511
1758
  severity: 1,
1512
1759
  range: args[0].range
1513
1760
  });
1514
1761
  }
1515
- if (args[1]?.queryNode && args[0]) {
1516
- const joinTargetName = args[0].text;
1517
- errors.push(...require_validation_utils.validateQueryScope(args[1], [
1518
- joinTargetName,
1519
- entryTypeName,
1520
- ...earlier
1521
- ], entryTypeName, doc, "a join may reference the entry table and joins declared before it"));
1522
- }
1762
+ const scope = join && viewJoinScope(owner, join, earlier);
1763
+ if (args[1]?.queryNode && scope) errors.push(...require_validation_utils.validateQueryScope(args[1], scope, doc, "a join may reference the entry table and joins declared before it"));
1523
1764
  return errors;
1524
1765
  }
1525
1766
  }),
1526
1767
  filter: new _atscript_core.AnnotationSpec({
1527
1768
  description: "WHERE clause for a view, filtering which rows are included.\n\n**Example:**\n```atscript\n@db.view.for User\n@db.view.filter `User.status = 'active' and User.age >= 18`\nexport interface ActiveUser { ... }\n```\n",
1528
1769
  nodeType: ["interface"],
1770
+ passedWhenReferred: false,
1529
1771
  argument: {
1530
1772
  name: "condition",
1531
1773
  type: "query",
1532
- description: "Filter expression for the view WHERE clause."
1774
+ description: "Filter expression for the view WHERE clause — may reference the entry table and every join; unqualified fields belong to the entry.",
1775
+ fieldScope: fieldScopes.viewFilter
1533
1776
  },
1534
1777
  validate(token, args, doc) {
1535
1778
  const errors = [];
@@ -1543,8 +1786,8 @@ const dbViewAnnotations = { view: {
1543
1786
  return errors;
1544
1787
  }
1545
1788
  if (!args[0]?.queryNode) return errors;
1546
- const entryTypeName = require_validation_utils.getAnnotationAlias(owner, "db.view.for");
1547
- if (!entryTypeName) {
1789
+ const scope = viewFilterScope(owner);
1790
+ if (!scope) {
1548
1791
  errors.push({
1549
1792
  message: "@db.view.filter requires @db.view.for to identify the entry table",
1550
1793
  severity: 1,
@@ -1552,13 +1795,14 @@ const dbViewAnnotations = { view: {
1552
1795
  });
1553
1796
  return errors;
1554
1797
  }
1555
- errors.push(...require_validation_utils.validateQueryScope(args[0], require_validation_utils.viewScopeTypes(owner), entryTypeName, doc));
1798
+ errors.push(...require_validation_utils.validateQueryScope(args[0], scope, doc));
1556
1799
  return errors;
1557
1800
  }
1558
1801
  }),
1559
1802
  materialized: new _atscript_core.AnnotationSpec({
1560
1803
  description: "Marks a view as materialized (precomputed, stored on disk). Supported by PostgreSQL, CockroachDB, Oracle, SQL Server (indexed views), Snowflake. MongoDB supports on-demand materialized views via $merge/$out. Not applicable to MySQL or SQLite.\n\n**Example:**\n```atscript\n@db.view.materialized\n@db.view.for User\n@db.view.filter `User.status = 'active'`\nexport interface ActiveUsers { ... }\n```\n",
1561
1804
  nodeType: ["interface"],
1805
+ passedWhenReferred: false,
1562
1806
  validate(token, _args, _doc) {
1563
1807
  const errors = [];
1564
1808
  const owner = token.parentNode;
@@ -1573,6 +1817,7 @@ const dbViewAnnotations = { view: {
1573
1817
  renamed: new _atscript_core.AnnotationSpec({
1574
1818
  description: "Specifies the previous view name for view rename migration. The sync engine drops the old view and creates the new one.\n\n**Example:**\n```atscript\n@db.view \"active_premium_users\"\n@db.view.renamed \"active_users\"\n@db.view.for User\nexport interface ActivePremiumUser { ... }\n```\n",
1575
1819
  nodeType: ["interface"],
1820
+ passedWhenReferred: false,
1576
1821
  argument: {
1577
1822
  name: "oldName",
1578
1823
  type: "string",
@@ -1590,21 +1835,28 @@ const dbViewAnnotations = { view: {
1590
1835
  }
1591
1836
  }),
1592
1837
  having: new _atscript_core.AnnotationSpec({
1593
- description: "Post-aggregation filter (HAVING clause) for analytical views. References view field aliases with applied aggregate functions.\n\n**Example:**\n```atscript\n@db.view\n@db.view.for Order\n@db.view.having `totalRevenue > 100`\nexport interface TopCategories { ... }\n```\n",
1838
+ description: "Post-aggregation filter (HAVING clause) for analytical views. References the view's own fields, unqualified (`totalRevenue > 100`) — aggregate fields by their alias, plain fields as GROUP BY dimensions; source-table refs are not in scope.\n\n**Example:**\n```atscript\n@db.view\n@db.view.for Order\n@db.view.having `totalRevenue > 100`\nexport interface TopCategories { ... }\n```\n",
1594
1839
  nodeType: ["interface"],
1840
+ passedWhenReferred: false,
1595
1841
  argument: {
1596
1842
  name: "condition",
1597
1843
  type: "query",
1598
- description: "HAVING condition referencing view aliases."
1844
+ description: "HAVING condition — the view's own fields, unqualified.",
1845
+ fieldScope: fieldScopes.viewHaving
1599
1846
  },
1600
- validate(token, _args, _doc) {
1847
+ validate(token, args, doc) {
1601
1848
  const errors = [];
1602
1849
  const owner = token.parentNode;
1603
- if (!require_validation_utils.hasAnyViewAnnotation(owner)) errors.push({
1604
- message: "@db.view.having is only valid on @db.view interfaces",
1605
- severity: 1,
1606
- range: token.range
1607
- });
1850
+ if (!require_validation_utils.hasAnyViewAnnotation(owner)) {
1851
+ errors.push({
1852
+ message: "@db.view.having is only valid on @db.view interfaces",
1853
+ severity: 1,
1854
+ range: token.range
1855
+ });
1856
+ return errors;
1857
+ }
1858
+ const scope = viewHavingScope(owner);
1859
+ if (args[0]?.queryNode && scope) errors.push(...require_validation_utils.validateQueryScope(args[0], scope, doc, "@db.view.having references the view's own fields unqualified"));
1608
1860
  return errors;
1609
1861
  }
1610
1862
  })
@@ -1636,6 +1888,7 @@ const dbPlugin = (options) => ({
1636
1888
  depth: dbTableAnnotations.depth,
1637
1889
  rel: dbRelAnnotations.rel,
1638
1890
  view: dbViewAnnotations.view,
1891
+ alias: dbAliasAnnotations.alias,
1639
1892
  agg: dbAggAnnotations.agg,
1640
1893
  search: dbSearchAnnotations.search,
1641
1894
  amount: dbAmountAnnotations.amount,