@atscript/db 0.1.161 → 0.1.163

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 (65) hide show
  1. package/dist/agg.cjs +1 -1
  2. package/dist/agg.d.cts +1 -1
  3. package/dist/agg.d.mts +1 -1
  4. package/dist/agg.mjs +1 -1
  5. package/dist/{aggregate-fns-B_sgl4mB.mjs → aggregate-fns-DsS9UQVB.mjs} +1 -1
  6. package/dist/{aggregate-fns-DFtKAv7Q.cjs → aggregate-fns-DupYzFD7.cjs} +1 -1
  7. package/dist/{buckets-B4T_alUe.d.cts → buckets-B8UOod-v.d.cts} +488 -57
  8. package/dist/{buckets-8mG67Mtw.d.mts → buckets-CHyspjYP.d.mts} +488 -57
  9. package/dist/{column-diff-DpeUbhEI.cjs → column-diff-BIGX3sRH.cjs} +853 -232
  10. package/dist/{column-diff-DR1glykD.mjs → column-diff-BfNIlMwX.mjs} +836 -233
  11. package/dist/{validator-B3xlbrWf.d.cts → db-error-B_DRL1E9.d.cts} +18 -89
  12. package/dist/{validator-B3xlbrWf.d.mts → db-error-B_DRL1E9.d.mts} +18 -89
  13. package/dist/{db-error-JcfJqqpk.cjs → db-error-DmF1_xrz.cjs} +13 -0
  14. package/dist/{db-error-eSsQX7rt.mjs → db-error-hraC12B9.mjs} +8 -1
  15. package/dist/{db-validator-plugin-BWy60OvG.d.cts → db-validator-plugin-wRqMYNBI.d.cts} +6 -0
  16. package/dist/{db-validator-plugin-BWy60OvG.d.mts → db-validator-plugin-wRqMYNBI.d.mts} +6 -0
  17. package/dist/{fk-diff-5gX42hBt.d.mts → fk-diff-BNGawZi9.d.cts} +34 -10
  18. package/dist/{fk-diff-BWdQ7kRZ.d.cts → fk-diff-rj9N2QLw.d.mts} +34 -10
  19. package/dist/index.cjs +15 -11
  20. package/dist/index.d.cts +24 -21
  21. package/dist/index.d.mts +24 -21
  22. package/dist/index.mjs +10 -12
  23. package/dist/{nested-writer-BOj_c_lI.mjs → nested-writer-ClUX0DB0.mjs} +16 -22
  24. package/dist/{nested-writer-Bl1SZS7x.cjs → nested-writer-yCy6wGuN.cjs} +16 -22
  25. package/dist/{object-CtPYTIvy.d.cts → object-Czia_qD4.d.cts} +7 -1
  26. package/dist/{object-CtPYTIvy.d.mts → object-Czia_qD4.d.mts} +7 -1
  27. package/dist/{object-Djg28csK.cjs → object-GjsuPNqo.cjs} +19 -0
  28. package/dist/{object-TkiJQ-Dp.mjs → object-PQ1J8jri.mjs} +14 -1
  29. package/dist/ops.cjs +6 -21
  30. package/dist/ops.mjs +6 -21
  31. package/dist/plugin.cjs +263 -323
  32. package/dist/plugin.mjs +163 -223
  33. package/dist/rel.cjs +2 -2
  34. package/dist/rel.d.cts +3 -3
  35. package/dist/rel.d.mts +3 -3
  36. package/dist/rel.mjs +2 -2
  37. package/dist/{relation-helpers-17_2f5hX.d.mts → relation-helpers-DloMywNs.d.cts} +1 -1
  38. package/dist/{relation-helpers-BLBrLQYi.d.cts → relation-helpers-zwt19E7r.d.mts} +1 -1
  39. package/dist/{relation-loader-DeulLrAw.cjs → relation-loader-DujL_Mh9.cjs} +1 -1
  40. package/dist/{relation-loader-CNBMPfRr.mjs → relation-loader-Qv3LrQ42.mjs} +1 -1
  41. package/dist/{search-fields-CT_UZ9fg.cjs → search-fields-B49-RCWd.cjs} +65 -1
  42. package/dist/{search-fields-CE8cmj9X.mjs → search-fields-BPNsxrCk.mjs} +48 -2
  43. package/dist/search-fields-CHqqcC3W.d.cts +30 -0
  44. package/dist/search-fields-CHqqcC3W.d.mts +30 -0
  45. package/dist/server-stamped-B6XbUQ4p.cjs +33 -0
  46. package/dist/server-stamped-BoP56K9M.mjs +22 -0
  47. package/dist/shared.cjs +42 -26
  48. package/dist/shared.d.cts +98 -4
  49. package/dist/shared.d.mts +98 -4
  50. package/dist/shared.mjs +4 -2
  51. package/dist/sync.cjs +228 -65
  52. package/dist/sync.d.cts +41 -5
  53. package/dist/sync.d.mts +41 -5
  54. package/dist/sync.mjs +228 -66
  55. package/dist/{validator-B4YDLUkj.cjs → validator-DK9Ijpo7.cjs} +3 -2
  56. package/dist/validator-DX97RzC2.d.cts +108 -0
  57. package/dist/validator-DX97RzC2.d.mts +108 -0
  58. package/dist/{validator-B3fPJjS1.mjs → validator-DuDWsIdQ.mjs} +3 -2
  59. package/dist/validator.cjs +6 -3
  60. package/dist/validator.d.cts +5 -4
  61. package/dist/validator.d.mts +5 -4
  62. package/dist/validator.mjs +5 -4
  63. package/dist/{validation-utils-BGjqc2jU.mjs → view-validation-BcgDq7M9.mjs} +364 -20
  64. package/dist/{validation-utils-CtLNlBtB.cjs → view-validation-BiDLCYMw.cjs} +452 -18
  65. package/package.json +6 -6
package/dist/plugin.cjs CHANGED
@@ -33,14 +33,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
33
33
  enumerable: true
34
34
  }) : target, mod));
35
35
  //#endregion
36
- const require_aggregate_fns = require("./aggregate-fns-DFtKAv7Q.cjs");
37
- const require_search_fields = require("./search-fields-CT_UZ9fg.cjs");
36
+ const require_aggregate_fns = require("./aggregate-fns-DupYzFD7.cjs");
37
+ const require_search_fields = require("./search-fields-B49-RCWd.cjs");
38
38
  require("./consts-BzRfCcH2.cjs");
39
- const require_validation_utils = require("./validation-utils-CtLNlBtB.cjs");
39
+ const require_view_validation = require("./view-validation-BiDLCYMw.cjs");
40
+ let _atscript_core = require("@atscript/core");
40
41
  let node_path = require("node:path");
41
42
  node_path = __toESM(node_path, 1);
42
43
  let node_url = require("node:url");
43
- let _atscript_core = require("@atscript/core");
44
44
  //#region src/plugin/manifest.ts
45
45
  /**
46
46
  * Renders the model-manifest module: an inventory of every exported
@@ -70,7 +70,7 @@ async function generateModelManifest(options, output, format, repo) {
70
70
  let specifier = node_path.default.relative(manifestDir, docPath).split(node_path.default.sep).join("/");
71
71
  if (!specifier.startsWith(".")) specifier = `./${specifier}`;
72
72
  for (const [exportName, node] of [...doc.exports.entries()].toSorted(([a], [b]) => a.localeCompare(b))) {
73
- if (!require_validation_utils.isDbSourceDecl(node)) continue;
73
+ if (!require_view_validation.isDbSourceDecl(node)) continue;
74
74
  let alias = exportName;
75
75
  for (let n = 1; usedAliases.has(alias); n++) alias = `${exportName}_${n}`;
76
76
  usedAliases.add(alias);
@@ -79,7 +79,7 @@ async function generateModelManifest(options, output, format, repo) {
79
79
  exportName,
80
80
  specifier,
81
81
  isView: node.countAnnotations("db.view") > 0 || node.countAnnotations("db.view.for") > 0,
82
- space: require_validation_utils.getAnnotationAlias(node, "db.space") || "default"
82
+ space: require_view_validation.getAnnotationAlias(node, "db.space") || "default"
83
83
  });
84
84
  }
85
85
  }
@@ -134,7 +134,7 @@ async function generateModelManifest(options, output, format, repo) {
134
134
  */
135
135
  /** The `@db.view.for` entry name of a view node. */
136
136
  function viewEntry(view) {
137
- return require_validation_utils.getAnnotationAlias(view, "db.view.for");
137
+ return require_view_validation.getAnnotationAlias(view, "db.view.for");
138
138
  }
139
139
  /**
140
140
  * `@db.view.filter` — and a conditional `@db.agg.*` of the same view: the
@@ -144,7 +144,7 @@ function viewEntry(view) {
144
144
  function viewFilterScope(view) {
145
145
  const entry = viewEntry(view);
146
146
  return entry ? {
147
- allowedTypes: require_validation_utils.viewScopeTypes(view),
147
+ allowedTypes: require_view_validation.viewScopeTypes(view),
148
148
  unqualifiedTarget: entry
149
149
  } : void 0;
150
150
  }
@@ -154,7 +154,7 @@ function viewFilterScope(view) {
154
154
  * view's {@link viewJoins} when the caller has not); an unqualified field
155
155
  * belongs to the entry.
156
156
  */
157
- function viewJoinScope(view, join, earlier = require_validation_utils.earlierJoinTargets(require_validation_utils.viewJoins(view), join)) {
157
+ function viewJoinScope(view, join, earlier = require_view_validation.earlierJoinTargets(require_view_validation.viewJoins(view), join)) {
158
158
  const entry = viewEntry(view);
159
159
  const target = join.args[0]?.text;
160
160
  if (!entry || !target) return;
@@ -192,12 +192,12 @@ function joinOrderScope(join) {
192
192
  * @since 0.1.147
193
193
  */
194
194
  function computeScope(propToken) {
195
- const view = require_validation_utils.getDbTableOwner(propToken);
195
+ const view = require_view_validation.getDbTableOwner(propToken);
196
196
  return view ? viewHavingScope(view) : void 0;
197
197
  }
198
198
  /** The condition of a `@db.agg.*` on a view field: the scope of the view's `@db.view.filter`. */
199
199
  function aggConditionScope(propToken) {
200
- const view = require_validation_utils.getDbTableOwner(propToken);
200
+ const view = require_view_validation.getDbTableOwner(propToken);
201
201
  return view ? viewFilterScope(view) : void 0;
202
202
  }
203
203
  /**
@@ -208,7 +208,7 @@ function aggConditionScope(propToken) {
208
208
  function aggFieldScope(propToken) {
209
209
  const def = propToken.parentNode?.getDefinition();
210
210
  const refType = def && (0, _atscript_core.isRef)(def) && def.hasChain ? def.id : void 0;
211
- const view = refType ? void 0 : require_validation_utils.getDbTableOwner(propToken);
211
+ const view = refType ? void 0 : require_view_validation.getDbTableOwner(propToken);
212
212
  const target = refType ?? (view ? viewEntry(view) : void 0);
213
213
  return target ? {
214
214
  allowedTypes: [],
@@ -221,9 +221,9 @@ function aggFieldScope(propToken) {
221
221
  * unqualified field belongs to the related type.
222
222
  */
223
223
  function relFilterScope(field) {
224
- const target = require_validation_utils.getNavTargetTypeName(field);
224
+ const target = require_view_validation.getNavTargetTypeName(field);
225
225
  if (!target) return;
226
- const junction = require_validation_utils.getAnnotationAlias(field, "db.rel.via");
226
+ const junction = require_view_validation.getAnnotationAlias(field, "db.rel.via");
227
227
  return {
228
228
  allowedTypes: junction ? [target, junction] : [target],
229
229
  unqualifiedTarget: target
@@ -238,12 +238,12 @@ const fieldScopes = {
238
238
  viewFilter: (arg) => arg.parentNode ? viewFilterScope(arg.parentNode) : void 0,
239
239
  viewJoin: (arg) => {
240
240
  const view = arg.parentNode;
241
- const joins = view ? require_validation_utils.viewJoins(view) : [];
241
+ const joins = view ? require_view_validation.viewJoins(view) : [];
242
242
  const join = joinOfArg(joins, arg);
243
- return view && join ? viewJoinScope(view, join, require_validation_utils.earlierJoinTargets(joins, join)) : void 0;
243
+ return view && join ? viewJoinScope(view, join, require_view_validation.earlierJoinTargets(joins, join)) : void 0;
244
244
  },
245
245
  joinOrder: (arg) => {
246
- const join = arg.parentNode ? joinOfArg(require_validation_utils.viewJoins(arg.parentNode), arg) : void 0;
246
+ const join = arg.parentNode ? joinOfArg(require_view_validation.viewJoins(arg.parentNode), arg) : void 0;
247
247
  return join ? joinOrderScope(join) : void 0;
248
248
  },
249
249
  viewHaving: (arg) => arg.parentNode ? viewHavingScope(arg.parentNode) : void 0,
@@ -254,7 +254,7 @@ const fieldScopes = {
254
254
  };
255
255
  /** `refFilter` of the `@db.view.joins` target: a view source or a `@db.alias` of one. */
256
256
  function isJoinTarget(decl) {
257
- return require_validation_utils.isDbSourceDecl(decl) || require_validation_utils.isAliasDecl(decl);
257
+ return require_view_validation.isDbSourceDecl(decl) || require_view_validation.isAliasDecl(decl);
258
258
  }
259
259
  /** `refFilter` of `@db.rel.via`: a `@db.table`. */
260
260
  function isDbTable(decl) {
@@ -303,7 +303,7 @@ function validateAggArgs(name, token, args, doc) {
303
303
  });
304
304
  return errors;
305
305
  }
306
- errors.push(...require_validation_utils.validateQueryScope(condition, scope, doc));
306
+ errors.push(...require_view_validation.validateQueryScope(condition, scope, doc));
307
307
  const prop = token.parentNode;
308
308
  if (conditionalIsNullable(name) && prop && !prop.has("optional")) {
309
309
  const field = prop.id ?? "field";
@@ -340,7 +340,7 @@ function aggSpec(name, description, field, types, extra = "") {
340
340
  }, CONDITION_ARG],
341
341
  validate(token, args, doc) {
342
342
  const errors = validateAggArgs(name, token, args, doc);
343
- if (types) errors.push(...require_validation_utils.validateFieldBaseType(token, doc, `@db.agg.${name}`, types));
343
+ if (types) errors.push(...require_view_validation.validateFieldBaseType(token, doc, `@db.agg.${name}`, types));
344
344
  return errors;
345
345
  }
346
346
  });
@@ -360,7 +360,7 @@ const dbAggAnnotations = { agg: {
360
360
  //#region src/plugin/annotations/alias.ts
361
361
  /** The `accept` rule of a view-source ref argument (`@db.view.for`, `@db.alias`). */
362
362
  const VIEW_SOURCE_ARGUMENT = {
363
- accept: require_validation_utils.isDbSourceDecl,
363
+ accept: require_view_validation.isDbSourceDecl,
364
364
  expected: "must be a @db.table or a @db.view."
365
365
  };
366
366
  /**
@@ -369,14 +369,14 @@ const VIEW_SOURCE_ARGUMENT = {
369
369
  * @since 0.1.141
370
370
  */
371
371
  const dbAliasAnnotations = { alias: new _atscript_core.AnnotationSpec({
372
- 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",
372
+ description: (0, _atscript_core.annotationExample)("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.", "@db.alias Employee", "export type Manager = Employee", "", "@db.view.for Employee", "@db.view.joins Manager, `Manager.id = Employee.managerId`, 'left'", "export interface Staff {", " id: Employee.id", " managerName?: Manager.name", "}"),
373
373
  nodeType: ["type"],
374
374
  passedWhenReferred: false,
375
375
  argument: {
376
376
  name: "target",
377
377
  type: "ref",
378
378
  description: "The aliased table or view type (must have @db.table or @db.view).",
379
- refFilter: require_validation_utils.isDbSourceDecl
379
+ refFilter: require_view_validation.isDbSourceDecl
380
380
  },
381
381
  validate(token, args, doc) {
382
382
  const errors = [];
@@ -397,7 +397,7 @@ const dbAliasAnnotations = { alias: new _atscript_core.AnnotationSpec({
397
397
  severity: 1,
398
398
  range: token.range
399
399
  });
400
- errors.push(...require_validation_utils.validateRefArgument(args[0], doc, VIEW_SOURCE_ARGUMENT));
400
+ errors.push(...require_view_validation.validateRefArgument(args[0], doc, VIEW_SOURCE_ARGUMENT));
401
401
  return errors;
402
402
  }
403
403
  }) };
@@ -406,7 +406,7 @@ const dbAliasAnnotations = { alias: new _atscript_core.AnnotationSpec({
406
406
  const CURRENCY_CODE_PATTERN = /^[A-Z0-9]{2,10}$/;
407
407
  const dbAmountAnnotations = { amount: { currency: {
408
408
  $self: new _atscript_core.AnnotationSpec({
409
- description: "Hard-coded **currency code** for this amount field. Use when a table stores amounts in a single, fixed currency.\n\nFor per-row currency, use `@db.amount.currency.ref 'fieldName'` instead.\n\nOnly valid on `decimal` fields — money in floating-point is wrong.\n\n**Example:**\n```atscript\n@db.amount.currency 'EUR'\namount: decimal\n```\n",
409
+ description: (0, _atscript_core.annotationExample)("Hard-coded **currency code** for this amount field. Use when a table stores amounts in a single, fixed currency.\n\nFor per-row currency, use `@db.amount.currency.ref 'fieldName'` instead.\n\nOnly valid on `decimal` fields — money in floating-point is wrong.", "@db.amount.currency 'EUR'", "amount: decimal"),
410
410
  nodeType: ["prop"],
411
411
  multiple: false,
412
412
  argument: {
@@ -416,11 +416,11 @@ const dbAmountAnnotations = { amount: { currency: {
416
416
  },
417
417
  validate(token, args, doc) {
418
418
  const errors = [];
419
- errors.push(...require_validation_utils.validateFieldBaseType(token, doc, "@db.amount.currency", "decimal"));
420
- errors.push(...require_validation_utils.validateExclusiveWith(token, "@db.amount.currency", [{
419
+ errors.push(...require_view_validation.validateFieldBaseType(token, doc, "@db.amount.currency", "decimal"));
420
+ errors.push(...require_view_validation.validateExclusiveWith(token, "@db.amount.currency", [{
421
421
  key: "db.amount.currency.ref",
422
422
  displayName: "@db.amount.currency.ref"
423
- }]));
423
+ }], doc));
424
424
  const code = args[0]?.text;
425
425
  if (code && !CURRENCY_CODE_PATTERN.test(code)) errors.push({
426
426
  message: `@db.amount.currency '${code}': invalid currency code (expected 2–10 uppercase letters or digits)`,
@@ -431,7 +431,7 @@ const dbAmountAnnotations = { amount: { currency: {
431
431
  }
432
432
  }),
433
433
  ref: new _atscript_core.AnnotationSpec({
434
- description: "Binds this amount to a **sibling field** that holds its currency code. Use when each row may have a different currency.\n\nThe referenced field should be of type `db.currencyCode` (or at least a `string`).\n\nOnly valid on `decimal` fields.\n\n**Example:**\n```atscript\ncurrency: db.currencyCode\n@db.amount.currency.ref 'currency'\namount: decimal\n```\n",
434
+ description: (0, _atscript_core.annotationExample)("Binds this amount to a **sibling field** that holds its currency code. Use when each row may have a different currency.\n\nThe referenced field should be of type `db.currencyCode` (or at least a `string`).\n\nOnly valid on `decimal` fields.", "currency: db.currencyCode", "@db.amount.currency.ref 'currency'", "amount: decimal"),
435
435
  nodeType: ["prop"],
436
436
  passedWhenReferred: false,
437
437
  multiple: false,
@@ -442,121 +442,21 @@ const dbAmountAnnotations = { amount: { currency: {
442
442
  },
443
443
  validate(token, args, doc) {
444
444
  const errors = [];
445
- errors.push(...require_validation_utils.validateFieldBaseType(token, doc, "@db.amount.currency.ref", "decimal"));
446
- errors.push(...require_validation_utils.validateExclusiveWith(token, "@db.amount.currency.ref", [{
445
+ errors.push(...require_view_validation.validateFieldBaseType(token, doc, "@db.amount.currency.ref", "decimal"));
446
+ errors.push(...require_view_validation.validateExclusiveWith(token, "@db.amount.currency.ref", [{
447
447
  key: "db.amount.currency",
448
448
  displayName: "@db.amount.currency"
449
449
  }, {
450
450
  key: "db.unit.ref",
451
451
  displayName: "@db.unit.ref"
452
- }]));
453
- errors.push(...require_validation_utils.validateSiblingStringField(token, args, doc, "@db.amount.currency.ref"));
452
+ }], doc));
453
+ errors.push(...require_view_validation.validateSiblingStringField(token, args, doc, "@db.amount.currency.ref"));
454
454
  return errors;
455
455
  }
456
456
  })
457
457
  } } };
458
458
  //#endregion
459
- //#region src/shared/view-validation.ts
460
- /** `@db.agg.*` annotations that are never NULL (the counts: 0 over no value). */
461
- 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}`);
462
- /**
463
- * Walks a chain ref step by step (`Order.payload`, `Order.payload.customer`, …)
464
- * and reports the JSON root, arrays, encryption, optionality and the leaf type.
465
- * A chain shorter than 2 is not a field path (`resolved: false`).
466
- * @since 0.1.141
467
- */
468
- function jsonChainInfo(ref, doc) {
469
- const typeName = ref.id ?? "";
470
- const chain = ref.chain.map((t) => t.text);
471
- const info = {
472
- typeName,
473
- chain,
474
- resolved: false,
475
- viaArray: false,
476
- viaEncrypted: false,
477
- optional: false
478
- };
479
- if (!typeName || chain.length < 2) return info;
480
- for (let i = 1; i <= chain.length; i++) {
481
- const step = doc.unwindType(typeName, chain.slice(0, i));
482
- if (!step) return info;
483
- const node = step.node;
484
- const prop = node && (0, _atscript_core.isProp)(node) ? node : void 0;
485
- if (prop?.has("optional")) info.optional = true;
486
- if (prop && prop.countAnnotations("db.encrypted") > 0) info.viaEncrypted = true;
487
- const isJsonProp = prop !== void 0 && prop.countAnnotations("db.json") > 0;
488
- const isArr = (0, _atscript_core.isArray)(step.def);
489
- if (isArr) info.viaArray = true;
490
- if (info.jsonRoot === void 0 && (isJsonProp || isArr)) info.jsonRoot = i;
491
- if (i === chain.length) info.leafType = require_validation_utils.primitiveBaseType(step.def);
492
- }
493
- info.resolved = true;
494
- return info;
495
- }
496
- /** Props of a view interface (its own structure; views don't use `extends`). */
497
- function viewProps(owner) {
498
- if ((0, _atscript_core.isInterface)(owner)) return owner.props;
499
- const def = owner.getDefinition();
500
- return def && (0, _atscript_core.isStructure)(def) ? def.props : void 0;
501
- }
502
- /**
503
- * VW8: a chain ref that passes through a `@db.json` or array node reads a
504
- * JSON leaf, which views can only extract as a string / number / boolean.
505
- * Intermediate nodes that `unwindType` can't reach are skipped (sync-time
506
- * resolution reports them).
507
- */
508
- function validateJsonChain(fieldName, ref, doc, range) {
509
- const info = jsonChainInfo(ref, doc);
510
- if (!info.resolved || info.jsonRoot === void 0 || info.jsonRoot >= info.chain.length) return [];
511
- if (info.leafType !== void 0 && require_search_fields.JSON_LEAF_TYPES.has(info.leafType)) return [];
512
- return [{
513
- message: `Field "${fieldName}" reads "${info.typeName}.${info.chain.join(".")}" inside a JSON-stored field — it must end at a string, number or boolean leaf`,
514
- severity: 1,
515
- range
516
- }];
517
- }
518
- /**
519
- * Whole-view checks, run once per `@db.view.for` interface:
520
- *
521
- * - VW7 — a field reading from a left-joined table must be optional (the
522
- * join yields NULL for unmatched rows); `@db.agg.count` / `countDistinct`
523
- * fields are exempt (they count 0, never NULL).
524
- * - VW8 — a chain ref through a `@db.json` or array node must end at a
525
- * primitive string / number / boolean leaf.
526
- * @since 0.1.136
527
- */
528
- function validateViewInterface(owner, doc) {
529
- const errors = [];
530
- const props = viewProps(owner);
531
- if (!props) return errors;
532
- const leftJoined = /* @__PURE__ */ new Set();
533
- for (const join of require_validation_utils.viewJoins(owner)) {
534
- const target = join.args[0]?.text;
535
- if (target && join.args[2]?.text === "left") leftJoined.add(target);
536
- }
537
- for (const [fieldName, prop] of props) {
538
- const def = prop.getDefinition();
539
- if (!def || !(0, _atscript_core.isRef)(def)) continue;
540
- const ref = def;
541
- const range = (prop.token("identifier") ?? ref.token("identifier"))?.range;
542
- if (!range) continue;
543
- if (ref.id && leftJoined.has(ref.id) && !prop.has("optional") && !NULL_SAFE_AGG_ANNOTATIONS.some((name) => prop.countAnnotations(name) > 0)) errors.push({
544
- message: `Field "${fieldName}" reads from left-joined "${ref.id}" and must be optional (${fieldName}?: …)`,
545
- severity: 1,
546
- range
547
- });
548
- errors.push(...validateJsonChain(fieldName, ref, doc, range));
549
- }
550
- return errors;
551
- }
552
- //#endregion
553
459
  //#region src/plugin/annotations/column.ts
554
- /** Nav-field annotations: a navigation field has no column on this table. */
555
- const NAV_ANNOTATIONS = [
556
- "db.rel.to",
557
- "db.rel.from",
558
- "db.rel.via"
559
- ];
560
460
  /**
561
461
  * Compile-time placement rules of `@db.column.version.exempt` (E1–E5, W1, W2).
562
462
  * E6 (derived) lives in `DERIVED_INCOMPATIBLE`. The runtime mirror is
@@ -576,7 +476,7 @@ function validateVersionExempt(token) {
576
476
  const tag = "@db.column.version.exempt";
577
477
  if (field.countAnnotations("db.column.version") > 0) fail(`${tag} cannot mark the version column itself`);
578
478
  if (field.countAnnotations("meta.id") > 0) fail(`${tag} cannot mark a primary key — it identifies the row and is never patched`);
579
- if (NAV_ANNOTATIONS.some((n) => field.countAnnotations(n) > 0)) fail(`${tag} cannot mark a navigation field — related rows follow their own table's versioning`);
479
+ if (require_view_validation.isNavProp(field)) fail(`${tag} cannot mark a navigation field — related rows follow their own table's versioning`);
580
480
  if (field.countAnnotations("db.ignore") > 0) fail(`${tag} has no effect on an ignored field`, 2);
581
481
  let node = field;
582
482
  let tableOwner;
@@ -613,9 +513,51 @@ function validateVersionExempt(token) {
613
513
  }
614
514
  return errors;
615
515
  }
516
+ /** Annotations a `@db.onUpdate.inc` field cannot carry, with the reason. */
517
+ const ON_UPDATE_INC_INCOMPATIBLE = [
518
+ ["meta.id", "the primary key identifies the row and is never updated"],
519
+ ["db.column.version", "the version column is already bumped on every update"],
520
+ ["db.onUpdate.now", "a field is either a time stamp or a counter"],
521
+ ["db.default", "the counter starts at 0"],
522
+ ["db.default.increment", "the counter starts at 0"],
523
+ ["db.default.uuid", "the counter starts at 0"],
524
+ ["db.default.now", "the counter starts at 0"],
525
+ ["db.json", "a JSON column is written as one value"]
526
+ ];
527
+ /**
528
+ * Compile-time rules of `@db.onUpdate.inc` (since 0.1.162). The runtime
529
+ * mirror (top level, encryption) is in `TableMetadata`.
530
+ */
531
+ function validateOnUpdateInc(token, doc) {
532
+ const tag = "@db.onUpdate.inc";
533
+ const errors = require_view_validation.validateFieldBaseType(token, doc, tag, "number");
534
+ const field = token.parentNode;
535
+ const fail = (message) => {
536
+ errors.push({
537
+ message,
538
+ severity: 1,
539
+ range: token.range
540
+ });
541
+ };
542
+ if (errors.length === 0 && !isIntegerField(field, doc)) fail(`${tag} requires an integer field — use number.int (or @expect.int)`);
543
+ if (field.has("optional")) fail(`${tag} requires a non-optional field — the counter is always populated (default 0)`);
544
+ const struct = require_view_validation.getParentStruct(token);
545
+ if (!struct?.ownerNode || !(0, _atscript_core.isInterface)(struct.ownerNode)) fail(`${tag} must sit on a top-level field of the table, not inside a nested object`);
546
+ errors.push(...require_view_validation.validateIncompatible(token, tag, ON_UPDATE_INC_INCOMPATIBLE, doc));
547
+ return errors;
548
+ }
549
+ /** Whether a numeric field is an integer: `number.int`, an `integer` base or `@expect.int`. */
550
+ function isIntegerField(field, doc) {
551
+ const def = field.getDefinition();
552
+ if (!def || !(0, _atscript_core.isRef)(def)) return true;
553
+ const leaf = doc.unwindType(def.id, def.chain)?.def;
554
+ if (require_view_validation.primitiveBaseType(leaf) !== "number") return true;
555
+ if (leaf.tags?.has("int")) return true;
556
+ return require_view_validation.effectiveAnnotationNames(field, doc).has("expect.int");
557
+ }
616
558
  const dbColumnAnnotations = {
617
559
  patch: { strategy: new _atscript_core.AnnotationSpec({
618
- description: "Defines the **patching strategy** for updating nested objects.\n\n- **\"replace\"** → The field or object will be **fully replaced**.\n- **\"merge\"** → The field or object will be **merged recursively** (applies only to objects, not arrays).\n\n**Example:**\n```atscript\n@db.patch.strategy \"merge\"\nsettings: {\n notifications: boolean\n preferences: {\n theme: string\n }\n}\n```\n",
560
+ description: (0, _atscript_core.annotationExample)("Defines the **patching strategy** for updating nested objects.\n\n- **\"replace\"** → The field or object will be **fully replaced**.\n- **\"merge\"** → The field or object will be **merged recursively** (applies only to objects, not arrays).", "@db.patch.strategy \"merge\"", "settings: {", " notifications: boolean", " preferences: {", " theme: string", " }", "}"),
619
561
  nodeType: ["prop"],
620
562
  passedWhenReferred: false,
621
563
  multiple: false,
@@ -645,7 +587,7 @@ const dbColumnAnnotations = {
645
587
  }) },
646
588
  column: {
647
589
  $self: new _atscript_core.AnnotationSpec({
648
- description: "Overrides the physical column name in the database. For nested (flattened) fields, the parent prefix is still prepended automatically. Document storage (MongoDB) renames top-level fields only — a nested field keeps its name there.\n\n**Example:**\n```atscript\n@db.column \"first_name\"\nfirstName: string\n// → physical column: first_name\n\n// Nested:\naddress: {\n @db.column \"zip_code\"\n zip: string\n}\n// → physical column: address__zip_code\n```\n",
590
+ description: (0, _atscript_core.annotationExample)("Overrides the physical column name in the database. For nested (flattened) fields, the parent prefix is still prepended automatically. Document storage (MongoDB) renames top-level fields only — a nested field keeps its name there.", "@db.column \"first_name\"", "firstName: string", "// → physical column: first_name", "", "// Nested:", "address: {", " @db.column \"zip_code\"", " zip: string", "}", "// → physical column: address__zip_code"),
649
591
  nodeType: ["prop"],
650
592
  passedWhenReferred: false,
651
593
  argument: {
@@ -655,7 +597,7 @@ const dbColumnAnnotations = {
655
597
  }
656
598
  }),
657
599
  renamed: new _atscript_core.AnnotationSpec({
658
- description: "Specifies the previous local field name for column rename migration. The sync engine generates ALTER TABLE RENAME COLUMN instead of drop+add (on document storage, top-level fields only).\n\n**Example:**\n```atscript\n@db.column.renamed \"zip\"\npostalCode: string\n// Renames address__zip → address__postalCode\n```\n",
600
+ description: (0, _atscript_core.annotationExample)("Specifies the previous local field name for column rename migration. The sync engine generates ALTER TABLE RENAME COLUMN instead of drop+add (on document storage, top-level fields only).", "@db.column.renamed \"zip\"", "postalCode: string", "// Renames address__zip → address__postalCode"),
659
601
  nodeType: ["prop"],
660
602
  passedWhenReferred: false,
661
603
  argument: {
@@ -665,7 +607,7 @@ const dbColumnAnnotations = {
665
607
  }
666
608
  }),
667
609
  collate: new _atscript_core.AnnotationSpec({
668
- description: "Portable collation for string comparison and sorting. Adapters map the generic value to their native collation.\n\n- **\"binary\"** — exact byte comparison (case-sensitive)\n- **\"nocase\"** — case-insensitive comparison\n- **\"unicode\"** — full Unicode-aware sorting\n\nFor adapter-specific collations, use `@db.<engine>.collate` instead.\n\n**Example:**\n```atscript\n@db.column.collate \"nocase\"\nusername: string\n```\n",
610
+ description: (0, _atscript_core.annotationExample)("Portable collation for string comparison and sorting. Adapters map the generic value to their native collation.\n\n- **\"binary\"** — exact byte comparison (case-sensitive)\n- **\"nocase\"** — case-insensitive comparison\n- **\"unicode\"** — full Unicode-aware sorting\n\nFor adapter-specific collations, use `@db.<engine>.collate` instead.", "@db.column.collate \"nocase\"", "username: string"),
669
611
  nodeType: ["prop"],
670
612
  passedWhenReferred: false,
671
613
  argument: {
@@ -679,11 +621,11 @@ const dbColumnAnnotations = {
679
621
  description: "Portable collation mode: \"binary\", \"nocase\", or \"unicode\"."
680
622
  },
681
623
  validate(token, args, doc) {
682
- return require_validation_utils.validateFieldBaseType(token, doc, "@db.column.collate", "string");
624
+ return require_view_validation.validateFieldBaseType(token, doc, "@db.column.collate", "string");
683
625
  }
684
626
  }),
685
627
  precision: new _atscript_core.AnnotationSpec({
686
- description: "Sets decimal precision and scale for database storage. Adapters map this to their native decimal type (e.g., `DECIMAL(10,2)` in SQL, ignored in MongoDB).\n\nFor `decimal` fields the runtime value is a string; for `number` fields this is a DB storage hint only.\n\n**Example:**\n```atscript\n@db.column.precision 10, 2\nprice: decimal\n```\n",
628
+ description: (0, _atscript_core.annotationExample)("Sets decimal precision and scale for database storage. Adapters map this to their native decimal type (e.g., `DECIMAL(10,2)` in SQL, ignored in MongoDB).\n\nFor `decimal` fields the runtime value is a string; for `number` fields this is a DB storage hint only.", "@db.column.precision 10, 2", "price: decimal"),
687
629
  nodeType: ["prop"],
688
630
  passedWhenReferred: false,
689
631
  argument: [{
@@ -696,11 +638,11 @@ const dbColumnAnnotations = {
696
638
  description: "Number of digits after the decimal point."
697
639
  }],
698
640
  validate(token, args, doc) {
699
- return require_validation_utils.validateFieldBaseType(token, doc, "@db.column.precision", ["number", "decimal"]);
641
+ return require_view_validation.validateFieldBaseType(token, doc, "@db.column.precision", ["number", "decimal"]);
700
642
  }
701
643
  }),
702
644
  derived: new _atscript_core.AnnotationSpec({
703
- 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",
645
+ description: (0, _atscript_core.annotationExample)("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", "@db.table 'orders'", "export interface Order {", " @meta.id", " id: number", "", " @db.json", " payload: {", " customer: { id: string, vip: boolean }", " total: number", " }", "", " @db.column.derived", " @db.index.plain", " customerId: Order.payload.customer.id", "", " @db.column.derived", " vip?: Order.payload.customer.vip", "}"),
704
646
  nodeType: ["prop"],
705
647
  passedWhenReferred: false,
706
648
  multiple: false,
@@ -714,7 +656,7 @@ const dbColumnAnnotations = {
714
656
  range: token.range
715
657
  });
716
658
  };
717
- const owner = require_validation_utils.getDbTableOwner(token);
659
+ const owner = require_view_validation.getDbTableOwner(token);
718
660
  if (!owner || !(0, _atscript_core.isInterface)(owner) || owner.countAnnotations("db.table") === 0) {
719
661
  fail("@db.column.derived is only valid on a top-level field of a @db.table interface");
720
662
  return errors;
@@ -726,12 +668,12 @@ const dbColumnAnnotations = {
726
668
  return errors;
727
669
  }
728
670
  const ref = definition;
729
- const tableName = require_validation_utils.getParentTypeName(token);
671
+ const tableName = require_view_validation.getParentTypeName(token);
730
672
  if (ref.id !== tableName) {
731
673
  fail(`@db.column.derived must reference the enclosing table '${tableName ?? ""}', not '${ref.id ?? ""}' — a derived column reads its own row`);
732
674
  return errors;
733
675
  }
734
- const info = jsonChainInfo(ref, doc);
676
+ const info = require_view_validation.jsonChainInfo(ref, doc);
735
677
  const path = `${info.typeName}.${info.chain.join(".")}`;
736
678
  const notInsideJson = `@db.column.derived path '${path}' does not read inside a @db.json field — a flattened or scalar column needs no derived column`;
737
679
  if (info.chain.length < 2) {
@@ -769,18 +711,20 @@ const dbColumnAnnotations = {
769
711
  nodeType: ["prop"],
770
712
  passedWhenReferred: false,
771
713
  validate(token, _args, doc) {
772
- return require_validation_utils.validateFieldBaseType(token, doc, "@db.column.measure", ["number", "decimal"]);
714
+ return require_view_validation.validateFieldBaseType(token, doc, "@db.column.measure", ["number", "decimal"]);
773
715
  }
774
716
  }),
775
717
  filterable: columnCapability("filterable", "filtering"),
776
718
  sortable: columnCapability("sortable", "sorting"),
777
719
  searchable: new _atscript_core.AnnotationSpec({
778
- description: "Includes this column in the generic `$search` fallback: when the adapter reports no native search capability (no FTS / Atlas index configured), the readable controller matches the `$search` term as a case-insensitive substring across all `@db.column.searchable` fields (`$or`). Where adapter-native search IS available it wins and this annotation is not consulted. The term is escaped literally — no user-supplied regex. String and integer columns only: an integer column (`number.int` and its sizes, `@expect.int`, `@db.default.increment`) matches when the term is a substring of the number's decimal text (`2946` finds `29461277`). Floats, decimals and timestamps are refused.\n\n**Example:**\n```atscript\n@db.table \"jobs\"\nexport interface Job {\n @db.column.searchable\n jobName: string\n @db.column.searchable\n description: string\n @db.column.searchable\n refNo: number.int\n}\n```\n",
720
+ description: (0, _atscript_core.annotationExample)("Includes this column in the generic `$search` fallback: when the adapter reports no native search capability (no FTS / Atlas index configured), the readable controller matches the `$search` term as a case-insensitive substring across all `@db.column.searchable` fields (`$or`). Where adapter-native search IS available it wins and this annotation is not consulted. The term is escaped literally — no user-supplied regex. String and integer columns only: an integer column (`number.int` and its sizes, `@expect.int`, `@db.default.increment`) matches when the term is a substring of the number's decimal text (`2946` finds `29461277`). Floats, decimals and timestamps are refused.", "@db.table \"jobs\"", "export interface Job {", " @db.column.searchable", " jobName: string", " @db.column.searchable", " description: string", " @db.column.searchable", " refNo: number.int", "}"),
779
721
  nodeType: ["prop"],
780
722
  passedWhenReferred: false,
781
723
  multiple: false,
782
724
  validate(token, _args, doc) {
783
- const verdict = require_validation_utils.searchFieldVerdict(token.parentNode, doc);
725
+ const sealed = require_view_validation.sealedSearchProblem(token, doc, "@db.column.searchable");
726
+ if (sealed) return [sealed];
727
+ const verdict = require_view_validation.searchFieldVerdict(token.parentNode, doc);
784
728
  if (!("problem" in verdict)) return [];
785
729
  return [{
786
730
  message: `@db.column.searchable needs a string or an integer field — "${token.parentNode.id}" ${verdict.problem}`,
@@ -791,17 +735,17 @@ const dbColumnAnnotations = {
791
735
  }),
792
736
  version: {
793
737
  $self: new _atscript_core.AnnotationSpec({
794
- description: "Marks a numeric column as the row's version for optimistic concurrency control (OCC). The adapter auto-increments this column on every UPDATE, and callers may pass `$cas: { <col>: N }` in a write payload to make the update conditional on the current version. Direct writes to the version column (as plain SET, `$inc`, or `$mul`) are rejected. Fields marked `@db.column.version.exempt` do not bump it.\n\n**Constraints:**\n- At most one version column per table.\n- Must resolve to an integer type (`int`, `int32`, `int64`, etc.).\n- Default value on insert is `0`.\n\n**Example:**\n```atscript\n@db.column.version\nversion: int\n```\n",
738
+ description: (0, _atscript_core.annotationExample)("Marks a numeric column as the row's version for optimistic concurrency control (OCC). The adapter auto-increments this column on every UPDATE, and callers may pass `$cas: { <col>: N }` in a write payload to make the update conditional on the current version. Direct writes to the version column (as plain SET, `$inc`, or `$mul`) are rejected. Fields marked `@db.column.version.exempt` do not bump it.\n\n**Constraints:**\n- At most one version column per table.\n- Must resolve to an integer type (`int`, `int32`, `int64`, etc.).\n- Default value on insert is `0`.", "@db.column.version", "version: int"),
795
739
  nodeType: ["prop"],
796
740
  passedWhenReferred: false,
797
741
  validate(token, _args, doc) {
798
- const errors = require_validation_utils.validateFieldBaseType(token, doc, "@db.column.version", "number");
742
+ const errors = require_view_validation.validateFieldBaseType(token, doc, "@db.column.version", "number");
799
743
  if (token.parentNode.has("optional")) errors.push({
800
744
  message: "@db.column.version requires a non-optional field — version columns are always populated (default 0)",
801
745
  severity: 1,
802
746
  range: token.range
803
747
  });
804
- const struct = require_validation_utils.getParentStruct(token);
748
+ const struct = require_view_validation.getParentStruct(token);
805
749
  if (struct) {
806
750
  let count = 0;
807
751
  for (const [, prop] of struct.props) if (prop.countAnnotations("db.column.version") > 0) count++;
@@ -815,7 +759,7 @@ const dbColumnAnnotations = {
815
759
  }
816
760
  }),
817
761
  exempt: new _atscript_core.AnnotationSpec({
818
- description: "Marks a field as **version-exempt**: a patch that writes ONLY exempt fields (`updateOne` / `bulkUpdate` without `$cas`, `updateMany`) leaves `@db.column.version` unchanged and adds no version check. A patch touching any other field bumps as usual; `$cas`, replace and `touchMany` always bump. Use it for derived/reporting columns refreshed in the background (scores, counters, caches) so they do not invalidate versions held by editors. On an object field it covers every nested field.\n\n**Example:**\n```atscript\n@db.column.version\nversion: number.int\n@db.column.version.exempt\nscore: number\n```\n",
762
+ description: (0, _atscript_core.annotationExample)("Marks a field as **version-exempt**: a patch that writes ONLY exempt fields (`updateOne` / `bulkUpdate` without `$cas`, `updateMany`) leaves `@db.column.version` unchanged and adds no version check. A patch touching any other field bumps as usual; `$cas`, replace and `touchMany` always bump. Use it for derived/reporting columns refreshed in the background (scores, counters, caches) so they do not invalidate versions held by editors. On an object field it covers every nested field.", "@db.column.version", "version: number.int", "@db.column.version.exempt", "score: number"),
819
763
  nodeType: ["prop"],
820
764
  passedWhenReferred: false,
821
765
  multiple: false,
@@ -827,7 +771,7 @@ const dbColumnAnnotations = {
827
771
  },
828
772
  default: {
829
773
  $self: new _atscript_core.AnnotationSpec({
830
- description: "Sets a static DB-level default value (used in DDL DEFAULT clause). For string fields the value is used as-is; for other types it is parsed as JSON.\n\n**Example:**\n```atscript\n@db.default \"active\"\nstatus: string\n```\n",
774
+ description: (0, _atscript_core.annotationExample)("Sets a static DB-level default value (used in DDL DEFAULT clause). For string fields the value is used as-is; for other types it is parsed as JSON.", "@db.default \"active\"", "status: string"),
831
775
  nodeType: ["prop"],
832
776
  passedWhenReferred: false,
833
777
  argument: {
@@ -837,7 +781,7 @@ const dbColumnAnnotations = {
837
781
  }
838
782
  }),
839
783
  increment: new _atscript_core.AnnotationSpec({
840
- description: "Auto-incrementing integer default. Each adapter maps this to its native mechanism (e.g., `AUTO_INCREMENT` in MySQL, `INTEGER PRIMARY KEY` in SQLite, counter collection in MongoDB).\n\n**Example:**\n```atscript\n@db.default.increment\nid: number.int\n\n// With optional start value:\n@db.default.increment 1000\nid: number.int\n```\n",
784
+ description: (0, _atscript_core.annotationExample)("Auto-incrementing integer default. Each adapter maps this to its native mechanism (e.g., `AUTO_INCREMENT` in MySQL, `INTEGER PRIMARY KEY` in SQLite, counter collection in MongoDB).", "@db.default.increment", "id: number.int", "", "// With optional start value:", "@db.default.increment 1000", "id: number.int"),
841
785
  nodeType: ["prop"],
842
786
  passedWhenReferred: false,
843
787
  argument: {
@@ -847,7 +791,7 @@ const dbColumnAnnotations = {
847
791
  description: "Starting value for the auto-increment sequence. Adapter-specific behavior; some adapters may ignore this."
848
792
  },
849
793
  validate(token, args, doc) {
850
- const errors = require_validation_utils.validateFieldBaseType(token, doc, "db.default.increment", "number");
794
+ const errors = require_view_validation.validateFieldBaseType(token, doc, "db.default.increment", "number");
851
795
  if (token.parentNode.countAnnotations("meta.id") === 0) errors.push({
852
796
  message: `@db.default.increment on a field without @meta.id — auto-increment columns must be primary-key columns for schema sync to manage them`,
853
797
  severity: 2,
@@ -857,39 +801,50 @@ const dbColumnAnnotations = {
857
801
  }
858
802
  }),
859
803
  uuid: new _atscript_core.AnnotationSpec({
860
- description: "UUID generation default. Each adapter maps this to its native mechanism (e.g., `DEFAULT (UUID())` in MySQL, `gen_random_uuid()` in PostgreSQL, app-level in SQLite).\n\n**Example:**\n```atscript\n@db.default.uuid\nid: string.uuid\n```\n",
804
+ description: (0, _atscript_core.annotationExample)("UUID generation default. Each adapter maps this to its native mechanism (e.g., `DEFAULT (UUID())` in MySQL, `gen_random_uuid()` in PostgreSQL, app-level in SQLite).", "@db.default.uuid", "id: string.uuid"),
861
805
  nodeType: ["prop"],
862
806
  passedWhenReferred: false,
863
807
  validate(token, args, doc) {
864
- return require_validation_utils.validateFieldBaseType(token, doc, "db.default.uuid", "string");
808
+ return require_view_validation.validateFieldBaseType(token, doc, "db.default.uuid", "string");
865
809
  }
866
810
  }),
867
811
  now: new _atscript_core.AnnotationSpec({
868
- description: "Current timestamp default. Each adapter maps this to its native mechanism (e.g., `DEFAULT CURRENT_TIMESTAMP` in MySQL, `DEFAULT now()` in PostgreSQL).\n\n**Example:**\n```atscript\n@db.default.now\ncreatedAt: number.timestamp\n```\n",
812
+ description: (0, _atscript_core.annotationExample)("Current timestamp default. Each adapter maps this to its native mechanism (e.g., `DEFAULT CURRENT_TIMESTAMP` in MySQL, `DEFAULT now()` in PostgreSQL).", "@db.default.now", "createdAt: number.timestamp"),
869
813
  nodeType: ["prop"],
870
814
  passedWhenReferred: false,
871
815
  validate(token, args, doc) {
872
- return require_validation_utils.validateFieldBaseType(token, doc, "db.default.now", ["number", "string"]);
816
+ return require_view_validation.validateFieldBaseType(token, doc, "db.default.now", ["number", "string"]);
817
+ }
818
+ })
819
+ },
820
+ onUpdate: {
821
+ now: new _atscript_core.AnnotationSpec({
822
+ description: (0, _atscript_core.annotationExample)("Sets the field to the current time (epoch ms) on every update: patches (`updateOne`, `bulkUpdate`, `updateMany`, HTTP `PATCH`) and replaces (`replaceOne`, `bulkReplace`, `replaceMany`, HTTP `PUT`). The value is written by the SDK in the same statement on every adapter and overrides a value the payload carries. Inserts are not affected — combine with `@db.default.now` (`number.timestamp.updated` applies both). Writes outside the SDK (raw SQL) do not set it.", "@db.default.now", "@db.onUpdate.now", "updatedAt: number.timestamp"),
823
+ nodeType: ["prop"],
824
+ passedWhenReferred: false,
825
+ validate(token, args, doc) {
826
+ const errors = require_view_validation.validateFieldBaseType(token, doc, "@db.onUpdate.now", "number");
827
+ const field = token.parentNode;
828
+ for (const [name, why] of [["meta.id", "the primary key identifies the row and is never updated"], ["db.column.version", "the version column is adapter-managed"]]) if (field.countAnnotations(name) > 0) errors.push({
829
+ message: `@db.onUpdate.now cannot coexist with @${name} — ${why}`,
830
+ severity: 1,
831
+ range: token.range
832
+ });
833
+ return errors;
834
+ }
835
+ }),
836
+ inc: new _atscript_core.AnnotationSpec({
837
+ description: (0, _atscript_core.annotationExample)("A per-row **change counter**: `0` on insert (unless the insert supplies a value), then `+1` in the same statement on every SDK update of the row — patches (`updateOne`, `bulkUpdate`, `updateMany`, HTTP `PATCH`, including patches that write only `@db.column.version.exempt` fields), replaces (`replaceOne`, `bulkReplace`, `replaceMany`, HTTP `PUT`) and `touchMany`. A value the payload carries is ignored, so clients cannot set it. An empty patch writes nothing and leaves it unchanged. It has no compare-and-set and does not affect `@db.column.version`. Writes outside the SDK (raw SQL, native `ON DELETE SET NULL` actions) do not move it.\n\n**Constraints:** a non-optional integer field at the top level of the table; not the primary key, the version column, a `@db.onUpdate.now`, `@db.default*`, `@db.json` or `@db.encrypted` field.", "@db.onUpdate.inc", "revision: number.int"),
838
+ nodeType: ["prop"],
839
+ passedWhenReferred: false,
840
+ multiple: false,
841
+ validate(token, _args, doc) {
842
+ return validateOnUpdateInc(token, doc);
873
843
  }
874
844
  })
875
845
  },
876
- onUpdate: { now: new _atscript_core.AnnotationSpec({
877
- description: "Sets the field to the current time (epoch ms) on every update: patches (`updateOne`, `bulkUpdate`, `updateMany`, HTTP `PATCH`) and replaces (`replaceOne`, `bulkReplace`, `replaceMany`, HTTP `PUT`). The value is written by the SDK in the same statement on every adapter and overrides a value the payload carries. Inserts are not affected — combine with `@db.default.now` (`number.timestamp.updated` applies both). Writes outside the SDK (raw SQL) do not set it.\n\n**Example:**\n```atscript\n@db.default.now\n@db.onUpdate.now\nupdatedAt: number.timestamp\n```\n",
878
- nodeType: ["prop"],
879
- passedWhenReferred: false,
880
- validate(token, args, doc) {
881
- const errors = require_validation_utils.validateFieldBaseType(token, doc, "@db.onUpdate.now", "number");
882
- const field = token.parentNode;
883
- for (const [name, why] of [["meta.id", "the primary key identifies the row and is never updated"], ["db.column.version", "the version column is adapter-managed"]]) if (field.countAnnotations(name) > 0) errors.push({
884
- message: `@db.onUpdate.now cannot coexist with @${name} — ${why}`,
885
- severity: 1,
886
- range: token.range
887
- });
888
- return errors;
889
- }
890
- }) },
891
846
  json: new _atscript_core.AnnotationSpec({
892
- description: "Forces a field to be stored as a single JSON column instead of being flattened into separate columns. Use on nested object fields that should remain as JSON in the database.\n\n**Example:**\n```atscript\n@db.json\nmetadata: { key: string, value: string }\n```\n",
847
+ description: (0, _atscript_core.annotationExample)("Forces a field to be stored as a single JSON column instead of being flattened into separate columns. Use on nested object fields that should remain as JSON in the database. On a primitive field (string / number / boolean) it stores the value as JSON too: SQL adapters filter the column only with `$exists` and never sort it — drop `@db.json` to get a typed, sortable scalar column.", "@db.json", "metadata: { key: string, value: string }"),
893
848
  nodeType: ["prop"],
894
849
  passedWhenReferred: false,
895
850
  validate(token, _args, doc) {
@@ -898,7 +853,7 @@ const dbColumnAnnotations = {
898
853
  if (definition && (0, _atscript_core.isRef)(definition)) {
899
854
  const unwound = doc.unwindType(definition.id, definition.chain);
900
855
  if (unwound && (0, _atscript_core.isPrimitive)(unwound.def)) errors.push({
901
- message: "@db.json on a primitive field has no effect — primitive fields are already stored as scalar columns",
856
+ message: "@db.json on a primitive field stores it as one JSON column — filterable only with $exists and never sortable; drop @db.json for a typed scalar column",
902
857
  severity: 2,
903
858
  range: token.range
904
859
  });
@@ -907,7 +862,7 @@ const dbColumnAnnotations = {
907
862
  }
908
863
  }),
909
864
  ignore: new _atscript_core.AnnotationSpec({
910
- description: "Excludes a field from the database schema. The field exists in the Atscript type but has no column in the DB.\n\n**Example:**\n```atscript\n@db.ignore\ndisplayName: string\n```\n",
865
+ description: (0, _atscript_core.annotationExample)("Excludes a field from the database schema. The field exists in the Atscript type but has no column in the DB.", "@db.ignore", "displayName: string"),
911
866
  nodeType: ["prop"],
912
867
  passedWhenReferred: false,
913
868
  validate(token, _args, _doc) {
@@ -921,38 +876,18 @@ const dbColumnAnnotations = {
921
876
  }
922
877
  }),
923
878
  encrypted: new _atscript_core.AnnotationSpec({
924
- description: "Encrypts this field at rest. Values are AES-256-GCM encrypted by the core layer before reaching the database and decrypted on read — transparent to application code.\n\nConstraints: the field cannot be filtered, sorted, indexed, used as PK/FK, patched with arithmetic ops, or referenced by search/vector/geo features.\n\n**Example:**\n```atscript\n@db.encrypted\napiToken?: string\n```\n",
879
+ description: (0, _atscript_core.annotationExample)("Encrypts this field at rest. Values are AES-256-GCM encrypted by the core layer before reaching the database and decrypted on read — transparent to application code.\n\nConstraints: the field cannot be filtered, sorted, indexed, used as PK/FK, patched with arithmetic ops, or referenced by search/vector/geo features.", "@db.encrypted", "apiToken?: string"),
925
880
  nodeType: ["prop"],
926
881
  passedWhenReferred: false,
927
882
  multiple: false,
928
- validate(token, _args, _doc) {
929
- const errors = [];
930
- const field = token.parentNode;
931
- for (const [name, why] of [
932
- ["meta.id", "the primary key must be addressable"],
933
- ["db.rel.FK", "joins are impossible over ciphertext"],
934
- ["db.index.plain", "indexes over ciphertext are meaningless"],
935
- ["db.index.unique", "indexes over ciphertext are meaningless"],
936
- ["db.index.fulltext", "indexes over ciphertext are meaningless"],
937
- ["db.index.geo", "geo indexes over ciphertext are meaningless"],
938
- ["db.search.vector", "vector search over ciphertext is impossible"],
939
- ["db.search.filter", "search filters need cleartext equality"],
940
- ["db.column.version", "the OCC filter needs cleartext equality"],
941
- ["db.default.increment", "engine-side defaults bypass the encryption transform"],
942
- ["db.default.now", "engine-side defaults bypass the encryption transform"],
943
- ["db.onUpdate.now", "the update time is set after the encryption transform"],
944
- ["db.mongo.search.text", "Atlas Search over ciphertext is impossible"],
945
- ["db.mongo.search.autocomplete", "Atlas Search over ciphertext is impossible"]
946
- ]) if (field.countAnnotations(name) > 0) errors.push({
947
- message: `@db.encrypted cannot coexist with @${name} — ${why}`,
948
- severity: 1,
949
- range: token.range
950
- });
951
- return errors;
883
+ validate(token, _args, doc) {
884
+ const typeProblem = require_view_validation.encryptedTypeSearchProblem(token, doc);
885
+ const errors = require_view_validation.validateIncompatible(token, "@db.encrypted", require_search_fields.ENCRYPTED_INCOMPATIBLE, doc);
886
+ return typeProblem ? [typeProblem, ...errors] : errors;
952
887
  }
953
888
  }),
954
889
  writeOnly: new _atscript_core.AnnotationSpec({
955
- description: "Marks a field as write-only over HTTP: it may be set through insert/update/replace payloads but NEVER appears in read responses — the readable controller excludes it from every projection, rejects filtering/sorting/grouping on it, and `/meta` serves its TYPE (flagged `writeOnly`) so client preflight validation and generated forms still know its shape. The classic case is a sealed secret (pair with `@db.encrypted`): writable via generic forms, unreadable by anyone.\n\nServer-side code reading through `AtscriptDbTable` still sees the value — the seal is an HTTP-layer contract, not a storage one. It cannot be combined with `@db.index.fulltext` or `@db.column.searchable`: a search hit would reveal the sealed value.\n\n**Example:**\n```atscript\n@db.writeOnly\n@db.encrypted\ncredentials?: string\n```\n",
890
+ description: (0, _atscript_core.annotationExample)("Marks a field as write-only over HTTP: it may be set through insert/update/replace payloads but NEVER appears in read responses — the readable controller excludes it from every projection, rejects filtering/sorting/grouping on it, and `/meta` serves its TYPE (flagged `writeOnly`) so client preflight validation and generated forms still know its shape. The classic case is a sealed secret (pair with `@db.encrypted`): writable via generic forms, unreadable by anyone.\n\nServer-side code reading through `AtscriptDbTable` still sees the value — the seal is an HTTP-layer contract, not a storage one. It cannot be combined with `@db.index.fulltext` or `@db.column.searchable`: a search hit would reveal the sealed value.", "@db.writeOnly", "@db.encrypted", "credentials?: string"),
956
891
  nodeType: ["prop"],
957
892
  passedWhenReferred: false,
958
893
  multiple: false,
@@ -974,21 +909,15 @@ const dbColumnAnnotations = {
974
909
  })
975
910
  };
976
911
  function columnCapability(capability, verb) {
977
- const example = capability === "filterable" ? " @db.column.filterable\n email: string\n" : " @db.column.sortable\n createdAt: number.timestamp\n";
912
+ const example = capability === "filterable" ? [" @db.column.filterable", " email: string"] : [" @db.column.sortable", " createdAt: number.timestamp"];
978
913
  return new _atscript_core.AnnotationSpec({
979
- description: `Marks a column as ${capability} in the readable controller's query/pages endpoints. Relevant only when the host \`@db.table\` interface opts into strict mode with \`@db.table.${capability} 'manual'\`; otherwise ${verb} is open on every column the adapter can handle (default-open, back-compat) and the annotation is a no-op. Adapter capability always wins: \`@db.json\` / array columns stay non-sortable (and non-filterable on SQL adapters) even when annotated. \`/meta.fields\` advertises exactly what the gate accepts; index-backed columns additionally carry the advisory \`indexed\` flag.
980
-
981
- **Example:**
982
- \`\`\`atscript
983
- @db.table "users"
984
- @db.table.${capability} "manual"\nexport interface User {
985
- ` + example + "}\n```\n",
914
+ description: (0, _atscript_core.annotationExample)(`Marks a column as ${capability} in the readable controller's query/pages endpoints. Relevant only when the host \`@db.table\` interface opts into strict mode with \`@db.table.${capability} 'manual'\`; otherwise ${verb} is open on every column the adapter can handle (default-open, back-compat) and the annotation is a no-op. Adapter capability always wins: \`@db.json\` / array columns stay non-sortable (and non-filterable on SQL adapters) even when annotated. \`/meta.fields\` advertises exactly what the gate accepts; index-backed columns additionally carry the advisory \`indexed\` flag.`, "@db.table \"users\"", `@db.table.${capability} "manual"`, "export interface User {", ...example, "}"),
986
915
  nodeType: ["prop"],
987
916
  passedWhenReferred: false,
988
917
  multiple: false,
989
918
  validate(token, _args, _doc) {
990
919
  const errors = [];
991
- const owner = require_validation_utils.getDbTableOwner(token);
920
+ const owner = require_view_validation.getDbTableOwner(token);
992
921
  if (!owner || owner.countAnnotations("db.table") === 0) errors.push({
993
922
  message: `@db.column.${capability} is only valid on fields of a @db.table interface`,
994
923
  severity: 1,
@@ -1029,7 +958,7 @@ function operandProblem(prop, doc) {
1029
958
  if (computeArg(prop)) return void 0;
1030
959
  const def = prop.getDefinition();
1031
960
  const leaf = def && (0, _atscript_core.isRef)(def) ? doc.unwindType(def.id, def.chain)?.def : def;
1032
- const base = require_validation_utils.primitiveBaseType(leaf);
961
+ const base = require_view_validation.primitiveBaseType(leaf);
1033
962
  if (base === void 0) return void 0;
1034
963
  return require_search_fields.numericTypeProblem({
1035
964
  base,
@@ -1071,7 +1000,7 @@ function validateCompute(token, args, doc) {
1071
1000
  const errors = [];
1072
1001
  const prop = token.parentNode;
1073
1002
  const field = prop.id ?? "field";
1074
- const view = require_validation_utils.getDbTableOwner(token);
1003
+ const view = require_view_validation.getDbTableOwner(token);
1075
1004
  const error = (message, range = token.range) => errors.push({
1076
1005
  message,
1077
1006
  severity: 1,
@@ -1081,15 +1010,15 @@ function validateCompute(token, args, doc) {
1081
1010
  error("@db.compute is only valid on a field of a @db.view.for view");
1082
1011
  return errors;
1083
1012
  }
1084
- errors.push(...require_validation_utils.validateExclusiveWith(token, "@db.compute", EXCLUSIVE_WITH));
1013
+ errors.push(...require_view_validation.validateExclusiveWith(token, "@db.compute", EXCLUSIVE_WITH, doc));
1085
1014
  if (!isPlainNumber(prop, doc)) error(`Field "${field}" has a @db.compute and must be typed \`number\``);
1086
1015
  const expr = args[0]?.exprNode;
1087
1016
  if (!expr) return errors;
1088
1017
  const refs = expr.fieldRefs();
1089
- const props = viewProps(view) ?? /* @__PURE__ */ new Map();
1018
+ const props = require_view_validation.viewProps(view) ?? /* @__PURE__ */ new Map();
1090
1019
  if (refs.length === 0) error("@db.compute needs at least one view field — a constant column is not supported", args[0].range);
1091
1020
  const scope = viewHavingScope(view);
1092
- if (scope) errors.push(...require_validation_utils.validateQueryScope(args[0], scope, doc, "@db.compute references the view's own fields — declare it on the view first (`field: Type.field`)"));
1021
+ if (scope) errors.push(...require_view_validation.validateQueryScope(args[0], scope, doc, "@db.compute references the view's own fields — declare it on the view first (`field: Type.field`)"));
1093
1022
  for (const ref of refs) {
1094
1023
  if (ref.typeRef) continue;
1095
1024
  const operand = props.get(ref.fieldRef.text);
@@ -1104,7 +1033,7 @@ function validateCompute(token, args, doc) {
1104
1033
  return errors;
1105
1034
  }
1106
1035
  const dbComputeAnnotations = { compute: new _atscript_core.AnnotationSpec({
1107
- description: "Declares a **computed view column** (since 0.1.147): closed arithmetic over the view's own fields — dimensions, aggregates, first-row-join fields and other computed fields — evaluated by the database, so the column sorts, filters and pages like any other.\n\nGrammar: `+ - * /`, unary `-`, parentheses, numeric literals, `coalesce(a, b, …)`. Every value is a double (`7 / 2 = 3.5`); a NULL operand yields NULL; division by zero yields NULL — so an expression with `/` or an optional operand needs an optional field.\n\n**Example:**\n```atscript\n@db.agg.count 'id', `Issue.status = 'open'`\nopenCount: Issue.id\n\n@db.compute `openCount * 10 + coalesce(oldestSeverity, 0)`\nrank: number\n```\n",
1036
+ description: (0, _atscript_core.annotationExample)("Declares a **computed view column** (since 0.1.147): closed arithmetic over the view's own fields — dimensions, aggregates, first-row-join fields and other computed fields — evaluated by the database, so the column sorts, filters and pages like any other.\n\nGrammar: `+ - * /`, unary `-`, parentheses, numeric literals, `coalesce(a, b, …)`. Every value is a double (`7 / 2 = 3.5`); a NULL operand yields NULL; division by zero yields NULL — so an expression with `/` or an optional operand needs an optional field.", "@db.agg.count 'id', `Issue.status = 'open'`", "openCount: Issue.id", "", "@db.compute `openCount * 10 + coalesce(oldestSeverity, 0)`", "rank: number"),
1108
1037
  nodeType: ["prop"],
1109
1038
  passedWhenReferred: false,
1110
1039
  argument: {
@@ -1119,7 +1048,7 @@ const dbComputeAnnotations = { compute: new _atscript_core.AnnotationSpec({
1119
1048
  //#region src/plugin/annotations/index-ann.ts
1120
1049
  const dbIndexAnnotations = { index: {
1121
1050
  plain: new _atscript_core.AnnotationSpec({
1122
- description: "Standard (non-unique) index for query performance. Fields sharing the same index name form a composite index.\n\n**Example:**\n```atscript\n@db.index.plain \"idx_timeline\", \"desc\"\ncreatedAt: number.timestamp\n```\n",
1051
+ description: (0, _atscript_core.annotationExample)("Standard (non-unique) index for query performance. Fields sharing the same index name form a composite index.", "@db.index.plain \"idx_timeline\", \"desc\"", "createdAt: number.timestamp"),
1123
1052
  nodeType: ["prop"],
1124
1053
  passedWhenReferred: false,
1125
1054
  multiple: true,
@@ -1138,7 +1067,7 @@ const dbIndexAnnotations = { index: {
1138
1067
  }]
1139
1068
  }),
1140
1069
  unique: new _atscript_core.AnnotationSpec({
1141
- description: "Unique index — ensures no two rows/documents have the same value(s). Fields sharing the same index name form a composite unique constraint.\n\n**Example:**\n```atscript\n@db.index.unique \"tenant_email\"\nemail: string.email\n```\n",
1070
+ description: (0, _atscript_core.annotationExample)("Unique index — ensures no two rows/documents have the same value(s). Fields sharing the same index name form a composite unique constraint.", "@db.index.unique \"tenant_email\"", "email: string.email"),
1142
1071
  nodeType: ["prop"],
1143
1072
  passedWhenReferred: false,
1144
1073
  multiple: true,
@@ -1151,10 +1080,12 @@ const dbIndexAnnotations = { index: {
1151
1080
  }
1152
1081
  }),
1153
1082
  fulltext: new _atscript_core.AnnotationSpec({
1154
- description: "Full-text search index. Fields sharing the same index name form a composite full-text index. A string member is part of the engine's text index. An integer member (`number.int` and its sizes, `@expect.int`, `@db.default.increment`) is never part of the physical text index: when the whole search term is a whole number (`2946`, `-12`; no leading zeros), rows whose member equals it exactly also match (OR'd with the text match). An integer member must be index-backed — the primary key (first `@meta.id`) or the first field of a `@db.index.plain` / `@db.index.unique`. Floats, decimals and timestamps are refused.\n\n**Example:**\n```atscript\n@db.index.fulltext \"ft_content\"\ntitle: string\n\n@db.index.fulltext \"ft_content\", 5\nbio: string\n\n@db.index.fulltext \"ft_content\"\n@db.index.unique\nrefNo: number.int\n```\n",
1083
+ description: (0, _atscript_core.annotationExample)("Full-text search index. Fields sharing the same index name form a composite full-text index. A string member is part of the engine's text index. An integer member (`number.int` and its sizes, `@expect.int`, `@db.default.increment`) is never part of the physical text index: when the whole search term is a whole number (`2946`, `-12`; no leading zeros), rows whose member equals it exactly also match (OR'd with the text match). An integer member must be index-backed — the primary key (first `@meta.id`) or the first field of a `@db.index.plain` / `@db.index.unique`. Floats, decimals and timestamps are refused.", "@db.index.fulltext \"ft_content\"", "title: string", "", "@db.index.fulltext \"ft_content\", 5", "bio: string", "", "@db.index.fulltext \"ft_content\"", "@db.index.unique", "refNo: number.int"),
1155
1084
  validate(token, args, doc) {
1156
1085
  const field = token.parentNode;
1157
- const verdict = require_validation_utils.searchFieldVerdict(field, doc);
1086
+ const sealed = require_view_validation.sealedSearchProblem(token, doc, "@db.index.fulltext");
1087
+ if (sealed) return [sealed];
1088
+ const verdict = require_view_validation.searchFieldVerdict(field, doc);
1158
1089
  if ("problem" in verdict) return [{
1159
1090
  message: `@db.index.fulltext needs a string or an integer field — "${field.id}" ${verdict.problem}`,
1160
1091
  severity: 1,
@@ -1162,7 +1093,7 @@ const dbIndexAnnotations = { index: {
1162
1093
  }];
1163
1094
  if (verdict.kind !== "integer") return [];
1164
1095
  const errors = [];
1165
- const missing = require_validation_utils.integerMemberIndexProblem(token);
1096
+ const missing = require_view_validation.integerMemberIndexProblem(token);
1166
1097
  if (missing) errors.push({
1167
1098
  message: missing,
1168
1099
  severity: 1,
@@ -1192,7 +1123,7 @@ const dbIndexAnnotations = { index: {
1192
1123
  }]
1193
1124
  }),
1194
1125
  geo: new _atscript_core.AnnotationSpec({
1195
- description: "Geospatial index on a `db.geoPoint` field. Enables `geoSearch()` (distance-ranked) and accelerates `$geoWithin` filters.\n\n**Example:**\n```atscript\n@db.index.geo\ngeo: db.geoPoint\n```\n",
1126
+ description: (0, _atscript_core.annotationExample)("Geospatial index on a `db.geoPoint` field. Enables `geoSearch()` (distance-ranked) and accelerates `$geoWithin` filters.", "@db.index.geo", "geo: db.geoPoint"),
1196
1127
  nodeType: ["prop"],
1197
1128
  passedWhenReferred: false,
1198
1129
  multiple: false,
@@ -1201,7 +1132,8 @@ const dbIndexAnnotations = { index: {
1201
1132
  name: "name",
1202
1133
  type: "string",
1203
1134
  description: "Index name. Defaults to the field name."
1204
- }
1135
+ },
1136
+ validate: require_view_validation.refuseCiphertext("@db.index.geo")
1205
1137
  })
1206
1138
  } };
1207
1139
  //#endregion
@@ -1217,13 +1149,13 @@ function checkCompositeFkCoverage(token, alias, refTypeName, targetStruct, targe
1217
1149
  if (targetProp.countAnnotations("db.index.unique") > 0) return true;
1218
1150
  const pk = [...targetStruct.props.entries()].filter(([, p]) => p.countAnnotations("meta.id") > 0).map(([name]) => name);
1219
1151
  if (pk.length <= 1) return true;
1220
- const hostStruct = require_validation_utils.getParentStruct(token);
1221
- const owner = require_validation_utils.getDbTableOwner(token);
1152
+ const hostStruct = require_view_validation.getParentStruct(token);
1153
+ const owner = require_view_validation.getDbTableOwner(token);
1222
1154
  if (!hostStruct || owner === void 0 || !isDbTable(owner)) return true;
1223
1155
  const covered = /* @__PURE__ */ new Set();
1224
1156
  for (const [, sibling] of hostStruct.props) {
1225
1157
  if (sibling.countAnnotations("db.rel.FK") === 0) continue;
1226
- if (require_validation_utils.getAnnotationAlias(sibling, "db.rel.FK") !== alias) continue;
1158
+ if (require_view_validation.getAnnotationAlias(sibling, "db.rel.FK") !== alias) continue;
1227
1159
  const siblingDef = sibling.getDefinition();
1228
1160
  if (siblingDef && (0, _atscript_core.isRef)(siblingDef) && siblingDef.id === refTypeName) {
1229
1161
  if (siblingDef.chain.length === 1) covered.add(siblingDef.chain[0].text);
@@ -1233,7 +1165,7 @@ function checkCompositeFkCoverage(token, alias, refTypeName, targetStruct, targe
1233
1165
  }
1234
1166
  const dbRelAnnotations = { rel: {
1235
1167
  FK: new _atscript_core.AnnotationSpec({
1236
- description: "Declares a foreign key reference on this field. The field must use a chain reference type (e.g., `User.id`) whose target is a primary key (`@meta.id`) or unique (`@db.index.unique`) field.\n\n**Dual role:**\n- On a `@db.table` interface, `@db.rel.FK` additionally drives DB-relation semantics — relation loading with `@db.rel.to` / `@db.rel.from`, junction pairing with `@db.rel.via`, etc.\n- On any other interface (value-help sources, WF forms, plain interfaces), `@db.rel.FK` acts purely as the value-help indicator: the client-side picker resolver uses it to decide which fields render a value-help picker. The target's `@db.http.path` (published by its canonical readable controller) supplies the picker URL.\n- In `/meta` (and `/meta/form/:name`) the marker is inherited through reference chains: a field declared as `code: Issue.code` where `Issue.code: Dict.code` carries `@db.rel.FK` is served with `ref` pointing at the terminal field (`Dict.code`) and `db.rel.FK: true`, so view fields get the dictionary picker. Runtime metadata is untouched.\n\n**Example:**\n```atscript\n@db.rel.FK\nauthorId: User.id\n\n// With alias (required when multiple FKs point to the same type)\n@db.rel.FK \"author\"\nauthorId: User.id\n```\n",
1168
+ description: (0, _atscript_core.annotationExample)("Declares a foreign key reference on this field. The field must use a chain reference type (e.g., `User.id`) whose target is a primary key (`@meta.id`) or unique (`@db.index.unique`) field.\n\n**Dual role:**\n- On a `@db.table` interface, `@db.rel.FK` additionally drives DB-relation semantics — relation loading with `@db.rel.to` / `@db.rel.from`, junction pairing with `@db.rel.via`, etc.\n- On any other interface (value-help sources, WF forms, plain interfaces), `@db.rel.FK` acts purely as the value-help indicator: the client-side picker resolver uses it to decide which fields render a value-help picker. The target's `@db.http.path` (published by its canonical readable controller) supplies the picker URL.\n- In `/meta` (and `/meta/form/:name`) the marker is inherited through reference chains: a field declared as `code: Issue.code` where `Issue.code: Dict.code` carries `@db.rel.FK` is served with `ref` pointing at the terminal field (`Dict.code`) and `db.rel.FK: true`, so view fields get the dictionary picker. Runtime metadata is untouched.", "@db.rel.FK", "authorId: User.id", "", "// With alias (required when multiple FKs point to the same type)", "@db.rel.FK \"author\"", "authorId: User.id"),
1237
1169
  nodeType: ["prop"],
1238
1170
  passedWhenReferred: false,
1239
1171
  argument: {
@@ -1299,7 +1231,7 @@ const dbRelAnnotations = { rel: {
1299
1231
  }
1300
1232
  }
1301
1233
  if (!alias) {
1302
- const struct = require_validation_utils.getParentStruct(token);
1234
+ const struct = require_view_validation.getParentStruct(token);
1303
1235
  if (struct) {
1304
1236
  let sameTargetCount = 0;
1305
1237
  for (const [, prop] of struct.props) {
@@ -1309,7 +1241,7 @@ const dbRelAnnotations = { rel: {
1309
1241
  const r = def;
1310
1242
  if (!r.hasChain) continue;
1311
1243
  if (r.id === refTypeName) {
1312
- if (!require_validation_utils.getAnnotationAlias(prop, "db.rel.FK")) sameTargetCount++;
1244
+ if (!require_view_validation.getAnnotationAlias(prop, "db.rel.FK")) sameTargetCount++;
1313
1245
  }
1314
1246
  }
1315
1247
  if (sameTargetCount > 1) errors.push({
@@ -1323,7 +1255,7 @@ const dbRelAnnotations = { rel: {
1323
1255
  }
1324
1256
  }),
1325
1257
  to: new _atscript_core.AnnotationSpec({
1326
- description: "Forward navigational property — the FK is on **this** interface. The compiler resolves the matching @db.rel.FK by target type or alias.\n\n**Example:**\n```atscript\n@db.rel.to\nauthor?: User\n\n// With alias\n@db.rel.to \"author\"\nauthor?: User\n```\n",
1258
+ description: (0, _atscript_core.annotationExample)("Forward navigational property — the FK is on **this** interface. The compiler resolves the matching @db.rel.FK by target type or alias.", "@db.rel.to", "author?: User", "", "// With alias", "@db.rel.to \"author\"", "author?: User"),
1327
1259
  nodeType: ["prop"],
1328
1260
  passedWhenReferred: false,
1329
1261
  argument: {
@@ -1336,7 +1268,7 @@ const dbRelAnnotations = { rel: {
1336
1268
  const errors = [];
1337
1269
  const field = token.parentNode;
1338
1270
  const alias = args[0]?.text;
1339
- const owner = require_validation_utils.getDbTableOwner(token);
1271
+ const owner = require_view_validation.getDbTableOwner(token);
1340
1272
  if (!owner || owner.countAnnotations("db.table") === 0) errors.push({
1341
1273
  message: "@db.rel.to is only valid on fields of a @db.table interface",
1342
1274
  severity: 1,
@@ -1347,7 +1279,7 @@ const dbRelAnnotations = { rel: {
1347
1279
  severity: 1,
1348
1280
  range: token.range
1349
1281
  });
1350
- const targetTypeName = require_validation_utils.getNavTargetTypeName(field);
1282
+ const targetTypeName = require_view_validation.getNavTargetTypeName(field);
1351
1283
  if (!targetTypeName) return errors;
1352
1284
  const unwound = doc.unwindType(targetTypeName);
1353
1285
  if (unwound) {
@@ -1365,15 +1297,15 @@ const dbRelAnnotations = { rel: {
1365
1297
  severity: 1,
1366
1298
  range: token.range
1367
1299
  });
1368
- const struct = require_validation_utils.getParentStruct(token);
1300
+ const struct = require_view_validation.getParentStruct(token);
1369
1301
  if (struct) {
1370
1302
  const fieldName = field.id;
1371
1303
  for (const [name, prop] of struct.props) {
1372
1304
  if (name === fieldName) continue;
1373
1305
  if (prop.countAnnotations("db.rel.to") === 0) continue;
1374
- const propAlias = require_validation_utils.getAnnotationAlias(prop, "db.rel.to");
1306
+ const propAlias = require_view_validation.getAnnotationAlias(prop, "db.rel.to");
1375
1307
  if ((alias || void 0) === (propAlias || void 0)) {
1376
- if (require_validation_utils.getNavTargetTypeName(prop) === targetTypeName) {
1308
+ if (require_view_validation.getNavTargetTypeName(prop) === targetTypeName) {
1377
1309
  errors.push({
1378
1310
  message: `Duplicate @db.rel.to '${alias || targetTypeName}' — only one forward navigational property per alias`,
1379
1311
  severity: 1,
@@ -1384,13 +1316,13 @@ const dbRelAnnotations = { rel: {
1384
1316
  }
1385
1317
  }
1386
1318
  if (alias) {
1387
- if (require_validation_utils.findFKFieldsPointingTo(doc, struct, targetTypeName, alias).length === 0) errors.push({
1319
+ if (require_view_validation.findFKFieldsPointingTo(doc, struct, targetTypeName, alias).length === 0) errors.push({
1388
1320
  message: `No @db.rel.FK '${alias}' found on this interface`,
1389
1321
  severity: 1,
1390
1322
  range: token.range
1391
1323
  });
1392
1324
  } else {
1393
- const matches = require_validation_utils.findFKFieldsPointingTo(doc, struct, targetTypeName);
1325
+ const matches = require_view_validation.findFKFieldsPointingTo(doc, struct, targetTypeName);
1394
1326
  if (matches.length === 0) errors.push({
1395
1327
  message: `No @db.rel.FK on this interface points to '${targetTypeName}' — did you mean @db.rel.from?`,
1396
1328
  severity: 1,
@@ -1407,7 +1339,7 @@ const dbRelAnnotations = { rel: {
1407
1339
  }
1408
1340
  }),
1409
1341
  from: new _atscript_core.AnnotationSpec({
1410
- description: "Inverse navigational property — the FK is on the **target** interface, pointing back to this one.\n\n**Example:**\n```atscript\n@db.rel.from\nposts: Post[]\n\n// With alias\n@db.rel.from \"original\"\ncomments: Comment[]\n```\n",
1342
+ description: (0, _atscript_core.annotationExample)("Inverse navigational property — the FK is on the **target** interface, pointing back to this one.", "@db.rel.from", "posts: Post[]", "", "// With alias", "@db.rel.from \"original\"", "comments: Comment[]"),
1411
1343
  nodeType: ["prop"],
1412
1344
  passedWhenReferred: false,
1413
1345
  argument: {
@@ -1420,7 +1352,7 @@ const dbRelAnnotations = { rel: {
1420
1352
  const errors = [];
1421
1353
  const field = token.parentNode;
1422
1354
  const alias = args[0]?.text;
1423
- const owner = require_validation_utils.getDbTableOwner(token);
1355
+ const owner = require_view_validation.getDbTableOwner(token);
1424
1356
  if (!owner || owner.countAnnotations("db.table") === 0) errors.push({
1425
1357
  message: "@db.rel.from is only valid on fields of a @db.table interface",
1426
1358
  severity: 1,
@@ -1431,7 +1363,7 @@ const dbRelAnnotations = { rel: {
1431
1363
  severity: 1,
1432
1364
  range: token.range
1433
1365
  });
1434
- const targetTypeName = require_validation_utils.getNavTargetTypeName(field);
1366
+ const targetTypeName = require_view_validation.getNavTargetTypeName(field);
1435
1367
  if (!targetTypeName) return errors;
1436
1368
  const unwound = doc.unwindType(targetTypeName);
1437
1369
  if (!unwound) return errors;
@@ -1445,15 +1377,15 @@ const dbRelAnnotations = { rel: {
1445
1377
  });
1446
1378
  return errors;
1447
1379
  }
1448
- const struct = require_validation_utils.getParentStruct(token);
1380
+ const struct = require_view_validation.getParentStruct(token);
1449
1381
  if (struct) {
1450
1382
  const fieldName = field.id;
1451
1383
  for (const [name, prop] of struct.props) {
1452
1384
  if (name === fieldName) continue;
1453
1385
  if (prop.countAnnotations("db.rel.from") === 0) continue;
1454
- const propAlias = require_validation_utils.getAnnotationAlias(prop, "db.rel.from");
1386
+ const propAlias = require_view_validation.getAnnotationAlias(prop, "db.rel.from");
1455
1387
  if ((alias || void 0) === (propAlias || void 0)) {
1456
- if (require_validation_utils.getNavTargetTypeName(prop) === targetTypeName) {
1388
+ if (require_view_validation.getNavTargetTypeName(prop) === targetTypeName) {
1457
1389
  errors.push({
1458
1390
  message: `Duplicate @db.rel.from '${alias || targetTypeName}' — only one inverse navigational property per alias`,
1459
1391
  severity: 1,
@@ -1464,9 +1396,9 @@ const dbRelAnnotations = { rel: {
1464
1396
  }
1465
1397
  }
1466
1398
  }
1467
- const thisTypeName = require_validation_utils.getParentTypeName(token);
1399
+ const thisTypeName = require_view_validation.getParentTypeName(token);
1468
1400
  if (!thisTypeName) return errors;
1469
- const matches = require_validation_utils.findFKFieldsPointingTo(targetDoc, targetDef, thisTypeName, alias);
1401
+ const matches = require_view_validation.findFKFieldsPointingTo(targetDoc, targetDef, thisTypeName, alias);
1470
1402
  if (alias) {
1471
1403
  if (matches.length === 0) errors.push({
1472
1404
  message: `No @db.rel.FK '${alias}' found on '${targetTypeName}'`,
@@ -1493,10 +1425,10 @@ const dbRelAnnotations = { rel: {
1493
1425
  return errors;
1494
1426
  }
1495
1427
  }),
1496
- onDelete: require_validation_utils.refActionAnnotation("onDelete"),
1497
- onUpdate: require_validation_utils.refActionAnnotation("onUpdate"),
1428
+ onDelete: require_view_validation.refActionAnnotation("onDelete"),
1429
+ onUpdate: require_view_validation.refActionAnnotation("onUpdate"),
1498
1430
  via: new _atscript_core.AnnotationSpec({
1499
- description: "Declares a many-to-many navigational property through an explicit junction table. `@db.rel.via` is self-sufficient — no `@db.rel.from` pairing is needed.\n\n**Example:**\n```atscript\n@db.rel.via PostTag\ntags: Tag[]\n```\n",
1431
+ description: (0, _atscript_core.annotationExample)("Declares a many-to-many navigational property through an explicit junction table. `@db.rel.via` is self-sufficient — no `@db.rel.from` pairing is needed.", "@db.rel.via PostTag", "tags: Tag[]"),
1500
1432
  nodeType: ["prop"],
1501
1433
  passedWhenReferred: false,
1502
1434
  argument: {
@@ -1520,7 +1452,7 @@ const dbRelAnnotations = { rel: {
1520
1452
  });
1521
1453
  if (!args[0]) return errors;
1522
1454
  const junctionName = args[0].text;
1523
- errors.push(...require_validation_utils.validateRefArgument(args[0], doc, {
1455
+ errors.push(...require_view_validation.validateRefArgument(args[0], doc, {
1524
1456
  accept: isDbTable,
1525
1457
  expected: "must have @db.table annotation."
1526
1458
  }));
@@ -1529,10 +1461,10 @@ const dbRelAnnotations = { rel: {
1529
1461
  if (!junctionUnwound) return errors;
1530
1462
  const junctionDef = junctionUnwound.def;
1531
1463
  if (!(0, _atscript_core.isInterface)(junctionDef)) return errors;
1532
- const thisTypeName = require_validation_utils.getParentTypeName(token);
1533
- const targetTypeName = require_validation_utils.getNavTargetTypeName(field);
1464
+ const thisTypeName = require_view_validation.getParentTypeName(token);
1465
+ const targetTypeName = require_view_validation.getNavTargetTypeName(field);
1534
1466
  if (!thisTypeName || !targetTypeName) return errors;
1535
- const fksToThis = require_validation_utils.findFKFieldsPointingTo(junctionUnwound.doc, junctionDef, thisTypeName);
1467
+ const fksToThis = require_view_validation.findFKFieldsPointingTo(junctionUnwound.doc, junctionDef, thisTypeName);
1536
1468
  if (fksToThis.length === 0) errors.push({
1537
1469
  message: `Junction '${junctionName}' has no @db.rel.FK pointing to '${thisTypeName}'`,
1538
1470
  severity: 1,
@@ -1544,7 +1476,7 @@ const dbRelAnnotations = { rel: {
1544
1476
  range: args[0].range
1545
1477
  });
1546
1478
  if (targetTypeName !== thisTypeName) {
1547
- const fksToTarget = require_validation_utils.findFKFieldsPointingTo(junctionUnwound.doc, junctionDef, targetTypeName);
1479
+ const fksToTarget = require_view_validation.findFKFieldsPointingTo(junctionUnwound.doc, junctionDef, targetTypeName);
1548
1480
  if (fksToTarget.length === 0) errors.push({
1549
1481
  message: `Junction '${junctionName}' has no @db.rel.FK pointing to '${targetTypeName}'`,
1550
1482
  severity: 1,
@@ -1560,7 +1492,7 @@ const dbRelAnnotations = { rel: {
1560
1492
  }
1561
1493
  }),
1562
1494
  filterable: new _atscript_core.AnnotationSpec({
1563
- description: "Lets HTTP clients filter the parent rows by this relation's related rows — `ticket=$some(status=open)` / `ticket=$none(...)` in a moost-db query (`{ ticket: { $some: { status: 'open' } } }`). Off by default: a client predicate filters the PARENT set and runs a correlated subquery. Server-side code (`transformFilter`, `actionRowScope`, direct table calls) never needs it. The related table's own field rules (visibility, encryption, write-only) apply inside the predicate.\n\n**Example:**\n```atscript\n@db.rel.to\n@db.rel.filterable\nticket?: Ticket\n```\n",
1495
+ description: (0, _atscript_core.annotationExample)("Lets HTTP clients filter the parent rows by this relation's related rows — `ticket=$some(status=open)` / `ticket=$none(...)` in a moost-db query (`{ ticket: { $some: { status: 'open' } } }`). Off by default: a client predicate filters the PARENT set and runs a correlated subquery. Server-side code (`transformFilter`, `actionRowScope`, direct table calls) never needs it. The related table's own field rules (visibility, encryption, write-only) apply inside the predicate.", "@db.rel.to", "@db.rel.filterable", "ticket?: Ticket"),
1564
1496
  nodeType: ["prop"],
1565
1497
  passedWhenReferred: false,
1566
1498
  validate(token) {
@@ -1575,7 +1507,7 @@ const dbRelAnnotations = { rel: {
1575
1507
  }
1576
1508
  }),
1577
1509
  filter: new _atscript_core.AnnotationSpec({
1578
- description: "Applies a filter to a navigational property, restricting which related records are loaded (`$with`) and which count in relational predicates (`$some` / `$none`).\n\n**Example:**\n```atscript\n@db.rel.from\n@db.rel.filter `Post.published = true`\npublishedPosts: Post[]\n```\n",
1510
+ description: (0, _atscript_core.annotationExample)("Applies a filter to a navigational property, restricting which related records are loaded (`$with`) and which count in relational predicates (`$some` / `$none`).", "@db.rel.from", "@db.rel.filter `Post.published = true`", "publishedPosts: Post[]"),
1579
1511
  nodeType: ["prop"],
1580
1512
  passedWhenReferred: false,
1581
1513
  argument: {
@@ -1600,7 +1532,7 @@ const dbRelAnnotations = { rel: {
1600
1532
  }
1601
1533
  if (!args[0]?.queryNode) return errors;
1602
1534
  const scope = relFilterScope(field);
1603
- if (scope) errors.push(...require_validation_utils.validateQueryScope(args[0], scope, doc));
1535
+ if (scope) errors.push(...require_view_validation.validateQueryScope(args[0], scope, doc));
1604
1536
  const expression = args[0].queryNode.expression;
1605
1537
  forEachComparison(expression, (cmp) => {
1606
1538
  if (cmp.right && "fieldRef" in cmp.right) errors.push({
@@ -1609,13 +1541,13 @@ const dbRelAnnotations = { rel: {
1609
1541
  range: cmp.right.fieldRef.range
1610
1542
  });
1611
1543
  });
1612
- const junction = hasVia ? require_validation_utils.getAnnotationAlias(field, "db.rel.via") : void 0;
1544
+ const junction = hasVia ? require_view_validation.getAnnotationAlias(field, "db.rel.via") : void 0;
1613
1545
  if (junction) {
1614
1546
  const conjuncts = (0, _atscript_core.isQueryLogical)(expression) && expression.operator === "and" ? expression.operands : [expression];
1615
1547
  for (const conjunct of conjuncts) {
1616
1548
  let junctionRef;
1617
1549
  let targetRef = false;
1618
- require_validation_utils.forEachFieldRef(conjunct, (ref) => {
1550
+ require_view_validation.forEachFieldRef(conjunct, (ref) => {
1619
1551
  if (ref.typeRef?.text === junction) junctionRef ??= ref.typeRef.range;
1620
1552
  else targetRef = true;
1621
1553
  });
@@ -1640,8 +1572,9 @@ function forEachComparison(expr, fn) {
1640
1572
  const dbSearchAnnotations = { search: {
1641
1573
  vector: {
1642
1574
  $self: new _atscript_core.AnnotationSpec({
1643
- description: "Marks a field as a **vector embedding** for **similarity search**.\n\n- Each adapter maps this to its native vector type and index:\n - **MongoDB** → Atlas `$vectorSearch` index\n - **MySQL 9+** → `VECTOR(N)` column + `VEC_DISTANCE_*` functions\n - **PostgreSQL** → pgvector `vector(N)` column + distance operators\n - **SQLite** → `BLOB` column + `vec0` virtual index (requires the `sqlite-vec` extension; falls back to JSON `TEXT` when unavailable)\n\n**Example:**\n```atscript\n@db.search.vector 1536, \"cosine\"\nembedding: db.vector\n```\n",
1575
+ description: (0, _atscript_core.annotationExample)("Marks a field as a **vector embedding** for **similarity search**.\n\n- Each adapter maps this to its native vector type and index:\n - **MongoDB** → Atlas `$vectorSearch` index\n - **MySQL 9+** → `VECTOR(N)` column + `VEC_DISTANCE_*` functions\n - **PostgreSQL** → pgvector `vector(N)` column + distance operators\n - **SQLite** → `BLOB` column + `vec0` virtual index (requires the `sqlite-vec` extension; falls back to JSON `TEXT` when unavailable)", "@db.search.vector 1536, \"cosine\"", "embedding: db.vector"),
1644
1576
  nodeType: ["prop"],
1577
+ validate: require_view_validation.refuseCiphertext("@db.search.vector"),
1645
1578
  passedWhenReferred: false,
1646
1579
  multiple: false,
1647
1580
  argument: [
@@ -1685,7 +1618,7 @@ const dbSearchAnnotations = { search: {
1685
1618
  ]
1686
1619
  }),
1687
1620
  threshold: new _atscript_core.AnnotationSpec({
1688
- description: "Sets a **default minimum similarity threshold** for vector search queries on this field.\n\n- Results with a similarity score below this threshold are excluded.\n- Query-time `$threshold` control overrides this default.\n- Value range: `0` to `1` (where `1` means exact match).\n\n**Example:**\n```atscript\n@db.search.vector 1536, \"cosine\"\n@db.search.vector.threshold 0.7\nembedding: db.vector\n```\n",
1621
+ description: (0, _atscript_core.annotationExample)("Sets a **default minimum similarity threshold** for vector search queries on this field.\n\n- Results with a similarity score below this threshold are excluded.\n- Query-time `$threshold` control overrides this default.\n- Value range: `0` to `1` (where `1` means exact match).", "@db.search.vector 1536, \"cosine\"", "@db.search.vector.threshold 0.7", "embedding: db.vector"),
1689
1622
  nodeType: ["prop"],
1690
1623
  passedWhenReferred: false,
1691
1624
  multiple: false,
@@ -1698,8 +1631,9 @@ const dbSearchAnnotations = { search: {
1698
1631
  })
1699
1632
  },
1700
1633
  filter: new _atscript_core.AnnotationSpec({
1701
- description: "Assigns a field as a **pre-filter** for a **vector search index**.\n\n- Filters allow vector search queries to return results only within a specific subset.\n- The referenced index must be defined using `@db.search.vector`.\n\n**Example:**\n```atscript\n@db.search.vector 1536, \"cosine\"\nembedding: db.vector\n\n@db.search.filter \"embedding\"\ncategory: string\n```\n",
1634
+ description: (0, _atscript_core.annotationExample)("Assigns a field as a **pre-filter** for a **vector search index**.\n\n- Filters allow vector search queries to return results only within a specific subset.\n- The referenced index must be defined using `@db.search.vector`.", "@db.search.vector 1536, \"cosine\"", "embedding: db.vector", "", "@db.search.filter \"embedding\"", "category: string"),
1702
1635
  nodeType: ["prop"],
1636
+ validate: require_view_validation.refuseCiphertext("@db.search.filter"),
1703
1637
  passedWhenReferred: false,
1704
1638
  multiple: true,
1705
1639
  argument: {
@@ -1717,7 +1651,7 @@ const dbSearchAnnotations = { search: {
1717
1651
  * read sorts by it without a `$nulls` entry for it.
1718
1652
  */
1719
1653
  const dbSortAnnotations = { sort: { nulls: new _atscript_core.AnnotationSpec({
1720
- description: "Default **NULL placement** when a read sorts by this field and the request names none (`$nulls` control, URL suffix `$sort=-closedAt:last`).\n\n- **`'first'`** — rows where the field is NULL (or missing) come before every value, in both sort directions\n- **`'last'`** — they come after every value, in both directions\n\nWithout it (and without `$nulls`) NULL placement is the database's own: first in ascending order on SQLite, MySQL, MongoDB and the in-memory adapter, last on PostgreSQL. Applies to `$sort` and to the `$rowOrder` of `first()` / `last()`. Only optional and `| null` fields can be NULL — on another required table field it has no effect. On PostgreSQL, MySQL and MongoDB a placement the engine does not produce natively costs the index its ORDER BY (a sort step instead).\n\n**Example:**\n```atscript\n@db.sort.nulls 'last'\nclosedAt?: number.timestamp\n```\n",
1654
+ description: (0, _atscript_core.annotationExample)("Default **NULL placement** when a read sorts by this field and the request names none (`$nulls` control, URL suffix `$sort=-closedAt:last`).\n\n- **`'first'`** — rows where the field is NULL (or missing) come before every value, in both sort directions\n- **`'last'`** — they come after every value, in both directions\n\nWithout it (and without `$nulls`) NULL placement is the database's own: first in ascending order on SQLite, MySQL, MongoDB and the in-memory adapter, last on PostgreSQL. Applies to `$sort` and to the `$rowOrder` of `first()` / `last()`. Only optional and `| null` fields can be NULL — on another required table field it has no effect. On PostgreSQL, MySQL and MongoDB a placement the engine does not produce natively costs the index its ORDER BY (a sort step instead).", "@db.sort.nulls 'last'", "closedAt?: number.timestamp"),
1721
1655
  nodeType: ["prop"],
1722
1656
  passedWhenReferred: false,
1723
1657
  multiple: false,
@@ -1743,16 +1677,20 @@ const dbSortAnnotations = { sort: { nulls: new _atscript_core.AnnotationSpec({
1743
1677
  fail("@db.sort.nulls on a @db.encrypted field — encrypted fields are not sortable");
1744
1678
  return errors;
1745
1679
  }
1680
+ if (require_view_validation.viewFieldSeals(field, doc).encrypted) {
1681
+ fail("@db.sort.nulls on a view field over a @db.encrypted source field — encrypted fields are not sortable");
1682
+ return errors;
1683
+ }
1746
1684
  const def = field.getDefinition();
1747
1685
  let leaf = def;
1748
1686
  if (def && (0, _atscript_core.isRef)(def)) leaf = doc.unwindType(def.id, def.chain)?.def;
1749
- const base = require_validation_utils.primitiveBaseType(leaf);
1687
+ const base = require_view_validation.primitiveBaseType(leaf);
1750
1688
  if (def && ((0, _atscript_core.isArray)(def) || (0, _atscript_core.isStructure)(def)) || leaf && ((0, _atscript_core.isArray)(leaf) || (0, _atscript_core.isStructure)(leaf) || (0, _atscript_core.isInterface)(leaf)) || leaf && (0, _atscript_core.isPrimitive)(leaf) && (base === "array" || base === "object")) {
1751
1689
  fail("@db.sort.nulls needs a sortable scalar field — not an object or an array");
1752
1690
  return errors;
1753
1691
  }
1754
- const owner = require_validation_utils.getDbTableOwner(token);
1755
- if (!field.has("optional") && !require_validation_utils.isNullableUnion(def, doc) && owner && owner.countAnnotations("db.table") > 0 && field.ownerNode?.ownerNode === owner) fail("@db.sort.nulls on a required field has no effect — the field is never NULL; make it optional (field?: …) or remove the annotation", 2);
1692
+ const owner = require_view_validation.getDbTableOwner(token);
1693
+ if (!field.has("optional") && !require_view_validation.isNullableUnion(def, doc) && owner && owner.countAnnotations("db.table") > 0 && field.ownerNode?.ownerNode === owner) fail("@db.sort.nulls on a required field has no effect — the field is never NULL; make it optional (field?: …) or remove the annotation", 2);
1756
1694
  return errors;
1757
1695
  }
1758
1696
  }) } };
@@ -1763,7 +1701,7 @@ const dbTableAnnotations = {
1763
1701
  filterable: tableCapability("filterable"),
1764
1702
  sortable: tableCapability("sortable"),
1765
1703
  $self: new _atscript_core.AnnotationSpec({
1766
- 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",
1704
+ description: (0, _atscript_core.annotationExample)("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.", "@db.table \"users\"", "export interface User { ... }"),
1767
1705
  nodeType: ["interface"],
1768
1706
  passedWhenReferred: false,
1769
1707
  argument: {
@@ -1775,7 +1713,7 @@ const dbTableAnnotations = {
1775
1713
  validate(token, _args, _doc) {
1776
1714
  const errors = [];
1777
1715
  const owner = token.parentNode;
1778
- if (require_validation_utils.hasAnyViewAnnotation(owner)) errors.push({
1716
+ if (require_view_validation.hasAnyViewAnnotation(owner)) errors.push({
1779
1717
  message: "An interface cannot be both a @db.table and a @db.view",
1780
1718
  severity: 1,
1781
1719
  range: token.range
@@ -1784,7 +1722,7 @@ const dbTableAnnotations = {
1784
1722
  }
1785
1723
  }),
1786
1724
  renamed: new _atscript_core.AnnotationSpec({
1787
- 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",
1725
+ description: (0, _atscript_core.annotationExample)("Specifies the previous table name for table rename migration. The sync engine generates ALTER TABLE RENAME instead of drop+create.", "@db.table \"app_users\"", "@db.table.renamed \"users\"", "export interface User { ... }"),
1788
1726
  nodeType: ["interface"],
1789
1727
  passedWhenReferred: false,
1790
1728
  argument: {
@@ -1803,7 +1741,7 @@ const dbTableAnnotations = {
1803
1741
  }
1804
1742
  }),
1805
1743
  preferredId: { uniqueIndex: new _atscript_core.AnnotationSpec({
1806
- 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",
1744
+ description: (0, _atscript_core.annotationExample)("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.", "@db.table", "@db.table.preferredId.uniqueIndex \"by_slug\"", "export interface Post {", " @db.index.unique \"by_slug\"", " slug: string", "}"),
1807
1745
  nodeType: ["interface"],
1808
1746
  passedWhenReferred: false,
1809
1747
  multiple: false,
@@ -1816,7 +1754,7 @@ const dbTableAnnotations = {
1816
1754
  validate(token, args, _doc) {
1817
1755
  const errors = [];
1818
1756
  const owner = token.parentNode;
1819
- if (require_validation_utils.hasAnyViewAnnotation(owner)) errors.push({
1757
+ if (require_view_validation.hasAnyViewAnnotation(owner)) errors.push({
1820
1758
  message: "@db.table.preferredId.uniqueIndex is not supported on @db.view interfaces (views have no unique-index declarations).",
1821
1759
  severity: 1,
1822
1760
  range: token.range
@@ -1851,7 +1789,7 @@ const dbTableAnnotations = {
1851
1789
  }) }
1852
1790
  },
1853
1791
  schema: new _atscript_core.AnnotationSpec({
1854
- 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",
1792
+ description: (0, _atscript_core.annotationExample)("Assigns the entity to a database schema/namespace.", "@db.table \"users\"", "@db.schema \"auth\"", "export interface User { ... }"),
1855
1793
  nodeType: ["interface"],
1856
1794
  passedWhenReferred: false,
1857
1795
  argument: {
@@ -1861,7 +1799,7 @@ const dbTableAnnotations = {
1861
1799
  }
1862
1800
  }),
1863
1801
  space: new _atscript_core.AnnotationSpec({
1864
- 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",
1802
+ description: (0, _atscript_core.annotationExample)("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)`.", "@db.table \"feed_runs\"", "@db.space \"analytics\"", "export interface FeedRun { ... }"),
1865
1803
  nodeType: ["interface"],
1866
1804
  passedWhenReferred: false,
1867
1805
  argument: {
@@ -1871,7 +1809,7 @@ const dbTableAnnotations = {
1871
1809
  }
1872
1810
  }),
1873
1811
  http: { path: new _atscript_core.AnnotationSpec({
1874
- description: "HTTP endpoint path where this table is served. Used by the UI for value-help on FK fields. After app.init() the app publishes the canonical controller route here at runtime; a model mounted on several routes needs `canonical: true` on one of its controllers (or declare this annotation and mount that controller without an explicit prefix).\n\n**Example:**\n```atscript\n@db.table \"authors\"\n@db.http.path \"/authors\"\nexport interface Author { ... }\n```\n",
1812
+ description: (0, _atscript_core.annotationExample)("HTTP endpoint path where this table is served. Used by the UI for value-help on FK fields. After app.init() the app publishes the canonical controller route here at runtime; a model mounted on several routes needs `canonical: true` on one of its controllers (or declare this annotation and mount that controller without an explicit prefix).", "@db.table \"authors\"", "@db.http.path \"/authors\"", "export interface Author { ... }"),
1875
1813
  nodeType: ["interface"],
1876
1814
  passedWhenReferred: false,
1877
1815
  argument: {
@@ -1881,7 +1819,7 @@ const dbTableAnnotations = {
1881
1819
  }
1882
1820
  }) },
1883
1821
  sync: { method: new _atscript_core.AnnotationSpec({
1884
- 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",
1822
+ description: (0, _atscript_core.annotationExample)("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.", "@db.sync.method \"drop\"", "interface Logs { ... }"),
1885
1823
  nodeType: ["interface"],
1886
1824
  passedWhenReferred: false,
1887
1825
  argument: {
@@ -1892,7 +1830,7 @@ const dbTableAnnotations = {
1892
1830
  }
1893
1831
  }) },
1894
1832
  depth: { limit: new _atscript_core.AnnotationSpec({
1895
- 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",
1833
+ description: (0, _atscript_core.annotationExample)("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.", "@db.table \"authors\"", "@db.depth.limit 2", "export interface Author { ... }"),
1896
1834
  nodeType: ["interface"],
1897
1835
  passedWhenReferred: false,
1898
1836
  multiple: false,
@@ -1946,19 +1884,21 @@ function collectUniqueIndexGroups(owner) {
1946
1884
  return groups;
1947
1885
  }
1948
1886
  function tableCapability(capability) {
1949
- const example = capability === "filterable" ? " @db.column.filterable\n email: string\n // other fields not filterable via the controller\n" : " @db.column.sortable\n createdAt: number.timestamp\n // other fields not sortable via the controller\n";
1887
+ const example = capability === "filterable" ? [
1888
+ " @db.column.filterable",
1889
+ " email: string",
1890
+ " // other fields not filterable via the controller"
1891
+ ] : [
1892
+ " @db.column.sortable",
1893
+ " createdAt: number.timestamp",
1894
+ " // other fields not sortable via the controller"
1895
+ ];
1950
1896
  const verb = capability === "filterable" ? "filter" : "sort";
1951
1897
  return new _atscript_core.AnnotationSpec({
1952
- description: `Controls ${verb}-gating on the readable controller's \`/query\` and \`/pages\` endpoints.\n\n- **\`'auto'\`** (default when the annotation is absent) — every column the adapter can ${verb} is ${capability} (JSON / array columns are never sortable, and not filterable on SQL adapters).
1898
+ description: (0, _atscript_core.annotationExample)(`Controls ${verb}-gating on the readable controller's \`/query\` and \`/pages\` endpoints.\n\n- **\`'auto'\`** (default when the annotation is absent) — every column the adapter can ${verb} is ${capability} (JSON / array columns are never sortable, and not filterable on SQL adapters).
1953
1899
  - **\`'manual'\`** — only fields annotated \`@db.column.${capability}\` are ${capability}; all others are rejected with HTTP 400.
1954
1900
 
1955
- \`/meta.fields[*].` + capability + "` advertises exactly what the gate accepts in either mode. The policy applies to " + (capability === "filterable" ? "filters only — `$groupBy`, `$having` keys and aggregate fields use the adapter's physical capability.\n\n" : "`$sort` only.\n\n") + `Writing the annotation explicitly as \`@db.table.${capability} 'auto'\` has the same runtime effect as omitting it; use it to document intent.
1956
-
1957
- **Example:**
1958
- \`\`\`atscript
1959
- @db.table "users"
1960
- @db.table.${capability} "manual"\nexport interface User {
1961
- ` + example + "}\n```\n",
1901
+ \`/meta.fields[*].` + capability + "` advertises exactly what the gate accepts in either mode. The policy applies to " + (capability === "filterable" ? "filters only — `$groupBy`, `$having` keys and aggregate fields use the adapter's physical capability.\n\n" : "`$sort` only.\n\n") + `Writing the annotation explicitly as \`@db.table.${capability} 'auto'\` has the same runtime effect as omitting it; use it to document intent.`, "@db.table \"users\"", `@db.table.${capability} "manual"`, "export interface User {", ...example, "}"),
1962
1902
  nodeType: ["interface"],
1963
1903
  passedWhenReferred: false,
1964
1904
  multiple: false,
@@ -1985,7 +1925,7 @@ function tableCapability(capability) {
1985
1925
  const UNIT_HOST_TYPES = ["decimal", "number"];
1986
1926
  const dbUnitAnnotations = { unit: {
1987
1927
  $self: new _atscript_core.AnnotationSpec({
1988
- description: "Hard-coded **unit of measure** for this quantity field. Use when a table stores all values for this field in a single, fixed unit.\n\nFor per-row units, use `@db.unit.ref 'fieldName'` instead.\n\nValid on `decimal` and `number` fields. Unlike `@db.amount.currency`, the unit code is free-form (`'kg'`, `'rpm'`, `'qps'`, …) — no ISO-style validation.\n\nAggregations on a unit-tagged field do not need grouping when the unit is a fixed literal — the constraint is satisfied schema-wide.\n\n**Example:**\n```atscript\n@db.unit 'kg'\nweight: decimal\n```\n",
1928
+ description: (0, _atscript_core.annotationExample)("Hard-coded **unit of measure** for this quantity field. Use when a table stores all values for this field in a single, fixed unit.\n\nFor per-row units, use `@db.unit.ref 'fieldName'` instead.\n\nValid on `decimal` and `number` fields. Unlike `@db.amount.currency`, the unit code is free-form (`'kg'`, `'rpm'`, `'qps'`, …) — no ISO-style validation.\n\nAggregations on a unit-tagged field do not need grouping when the unit is a fixed literal — the constraint is satisfied schema-wide.", "@db.unit 'kg'", "weight: decimal"),
1989
1929
  nodeType: ["prop"],
1990
1930
  multiple: false,
1991
1931
  argument: {
@@ -1995,16 +1935,16 @@ const dbUnitAnnotations = { unit: {
1995
1935
  },
1996
1936
  validate(token, _args, doc) {
1997
1937
  const errors = [];
1998
- errors.push(...require_validation_utils.validateFieldBaseType(token, doc, "@db.unit", UNIT_HOST_TYPES));
1999
- errors.push(...require_validation_utils.validateExclusiveWith(token, "@db.unit", [{
1938
+ errors.push(...require_view_validation.validateFieldBaseType(token, doc, "@db.unit", UNIT_HOST_TYPES));
1939
+ errors.push(...require_view_validation.validateExclusiveWith(token, "@db.unit", [{
2000
1940
  key: "db.unit.ref",
2001
1941
  displayName: "@db.unit.ref"
2002
- }]));
1942
+ }], doc));
2003
1943
  return errors;
2004
1944
  }
2005
1945
  }),
2006
1946
  ref: new _atscript_core.AnnotationSpec({
2007
- description: "Binds this quantity to a **sibling field** that holds its unit. Use when different rows may carry different units (e.g. mixed kg/lb measurements).\n\nThe referenced field must be a `string`.\n\nValid on `decimal` and `number` fields. Aggregating this field forces the referenced unit field into `$groupBy` at runtime — summing rows with different units is rejected.\n\n**Example:**\n```atscript\nunit: string\n@db.unit.ref 'unit'\nweight: decimal\n```\n",
1947
+ description: (0, _atscript_core.annotationExample)("Binds this quantity to a **sibling field** that holds its unit. Use when different rows may carry different units (e.g. mixed kg/lb measurements).\n\nThe referenced field must be a `string`.\n\nValid on `decimal` and `number` fields. Aggregating this field forces the referenced unit field into `$groupBy` at runtime — summing rows with different units is rejected.", "unit: string", "@db.unit.ref 'unit'", "weight: decimal"),
2008
1948
  nodeType: ["prop"],
2009
1949
  passedWhenReferred: false,
2010
1950
  multiple: false,
@@ -2015,15 +1955,15 @@ const dbUnitAnnotations = { unit: {
2015
1955
  },
2016
1956
  validate(token, args, doc) {
2017
1957
  const errors = [];
2018
- errors.push(...require_validation_utils.validateFieldBaseType(token, doc, "@db.unit.ref", UNIT_HOST_TYPES));
2019
- errors.push(...require_validation_utils.validateExclusiveWith(token, "@db.unit.ref", [{
1958
+ errors.push(...require_view_validation.validateFieldBaseType(token, doc, "@db.unit.ref", UNIT_HOST_TYPES));
1959
+ errors.push(...require_view_validation.validateExclusiveWith(token, "@db.unit.ref", [{
2020
1960
  key: "db.unit",
2021
1961
  displayName: "@db.unit"
2022
1962
  }, {
2023
1963
  key: "db.amount.currency.ref",
2024
1964
  displayName: "@db.amount.currency.ref"
2025
- }]));
2026
- errors.push(...require_validation_utils.validateSiblingStringField(token, args, doc, "@db.unit.ref"));
1965
+ }], doc));
1966
+ errors.push(...require_view_validation.validateSiblingStringField(token, args, doc, "@db.unit.ref"));
2027
1967
  return errors;
2028
1968
  }
2029
1969
  })
@@ -2039,7 +1979,7 @@ const dbUnitAnnotations = { unit: {
2039
1979
  */
2040
1980
  function validateJoinOrder(orderToken, scope, doc) {
2041
1981
  const target = scope.unqualifiedTarget;
2042
- const errors = require_validation_utils.validateQueryScope(orderToken, scope, doc, `a first-row join orders by fields of its target '${target}'`);
1982
+ const errors = require_view_validation.validateQueryScope(orderToken, scope, doc, `a first-row join orders by fields of its target '${target}'`);
2043
1983
  const seen = /* @__PURE__ */ new Set();
2044
1984
  for (const item of orderToken.orderNode.items) {
2045
1985
  const { ref } = item;
@@ -2087,7 +2027,7 @@ function orderKeyProblem(doc, target, path) {
2087
2027
  }
2088
2028
  const dbViewAnnotations = { view: {
2089
2029
  $self: new _atscript_core.AnnotationSpec({
2090
- 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",
2030
+ description: (0, _atscript_core.annotationExample)("Marks an interface as a **database view**. Optionally takes a view name argument.", "@db.view \"active_premium_users\"", "@db.view.for User", "export interface ActivePremiumUser { ... }"),
2091
2031
  nodeType: ["interface"],
2092
2032
  passedWhenReferred: false,
2093
2033
  argument: {
@@ -2107,14 +2047,14 @@ const dbViewAnnotations = { view: {
2107
2047
  }
2108
2048
  }),
2109
2049
  for: new _atscript_core.AnnotationSpec({
2110
- 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",
2050
+ description: (0, _atscript_core.annotationExample)("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.", "@db.view.for Order", "@db.view.filter `Order.status = 'active'`", "export interface ActiveOrderDetails { ... }"),
2111
2051
  nodeType: ["interface"],
2112
2052
  passedWhenReferred: false,
2113
2053
  argument: {
2114
2054
  name: "entry",
2115
2055
  type: "ref",
2116
2056
  description: "The primary/entry table or view type (must have @db.table or @db.view).",
2117
- refFilter: require_validation_utils.isDbSourceDecl
2057
+ refFilter: require_view_validation.isDbSourceDecl
2118
2058
  },
2119
2059
  validate(token, args, doc) {
2120
2060
  const errors = [];
@@ -2124,19 +2064,19 @@ const dbViewAnnotations = { view: {
2124
2064
  severity: 1,
2125
2065
  range: token.range
2126
2066
  });
2127
- if (args[0]) errors.push(...require_validation_utils.validateRefArgument(args[0], doc, VIEW_SOURCE_ARGUMENT));
2128
- const cycle = require_validation_utils.findViewCycle(owner, doc);
2067
+ if (args[0]) errors.push(...require_view_validation.validateRefArgument(args[0], doc, VIEW_SOURCE_ARGUMENT));
2068
+ const cycle = require_view_validation.findViewCycle(owner, doc);
2129
2069
  if (cycle) errors.push({
2130
2070
  message: `View '${cycle[0]}' depends on itself: ${cycle.join(" → ")}`,
2131
2071
  severity: 1,
2132
2072
  range: args[0]?.range ?? token.range
2133
2073
  });
2134
- errors.push(...validateViewInterface(owner, doc));
2074
+ errors.push(...require_view_validation.validateViewInterface(owner, doc));
2135
2075
  return errors;
2136
2076
  }
2137
2077
  }),
2138
2078
  joins: new _atscript_core.AnnotationSpec({
2139
- 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\n**First-row join** (since 0.1.147): a 4th argument — an ordering of the target's fields (`` `raisedAt, id` ``, `` `severity desc` ``) — joins only the FIRST matching target row by that ordering (the target's primary key is appended as the final tie-break), so every field read through the join comes from that one row. NULL sorts first in `asc`.\n```atscript\n@db.view.joins OldestOpenIssue, `OldestOpenIssue.ticketId = Ticket.id and OldestOpenIssue.status = 'open'`, 'left', `raisedAt`\n```\n",
2079
+ description: (0, _atscript_core.annotationExample)("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.", "@db.view.for Order", "@db.view.joins Customer, `Customer.id = Order.customerId`", "@db.view.joins Region, `Region.id = Customer.regionId`, 'left'", "export interface OrderRegion { ... }", "```", "", "**First-row join** (since 0.1.147): a 4th argument — an ordering of the target's fields (`` `raisedAt, id` ``, `` `severity desc` ``) — joins only the FIRST matching target row by that ordering (the target's primary key is appended as the final tie-break), so every field read through the join comes from that one row. NULL sorts first in `asc`.", "```atscript", "@db.view.joins OldestOpenIssue, `OldestOpenIssue.ticketId = Ticket.id and OldestOpenIssue.status = 'open'`, 'left', `raisedAt`"),
2140
2080
  nodeType: ["interface"],
2141
2081
  passedWhenReferred: false,
2142
2082
  multiple: true,
@@ -2172,7 +2112,7 @@ const dbViewAnnotations = { view: {
2172
2112
  validate(token, args, doc) {
2173
2113
  const errors = [];
2174
2114
  const owner = token.parentNode;
2175
- if (!require_validation_utils.hasAnyViewAnnotation(owner) && !args[0]) {
2115
+ if (!require_view_validation.hasAnyViewAnnotation(owner) && !args[0]) {
2176
2116
  errors.push({
2177
2117
  message: "@db.view.joins is only valid on @db.view interfaces",
2178
2118
  severity: 1,
@@ -2180,11 +2120,11 @@ const dbViewAnnotations = { view: {
2180
2120
  });
2181
2121
  return errors;
2182
2122
  }
2183
- if (args[0]) errors.push(...require_validation_utils.validateRefArgument(args[0], doc, {
2123
+ if (args[0]) errors.push(...require_view_validation.validateRefArgument(args[0], doc, {
2184
2124
  accept: isJoinTarget,
2185
2125
  expected: "must be a @db.table or a @db.view."
2186
2126
  }));
2187
- const entryTypeName = require_validation_utils.getAnnotationAlias(owner, "db.view.for");
2127
+ const entryTypeName = require_view_validation.getAnnotationAlias(owner, "db.view.for");
2188
2128
  if (!entryTypeName) {
2189
2129
  errors.push({
2190
2130
  message: "@db.view.joins requires @db.view.for to identify the entry table",
@@ -2193,14 +2133,14 @@ const dbViewAnnotations = { view: {
2193
2133
  });
2194
2134
  return errors;
2195
2135
  }
2196
- const joins = require_validation_utils.viewJoins(owner);
2136
+ const joins = require_view_validation.viewJoins(owner);
2197
2137
  const join = joins.find((a) => a.token === token);
2198
- const earlier = join ? require_validation_utils.earlierJoinTargets(joins, join) : [];
2138
+ const earlier = join ? require_view_validation.earlierJoinTargets(joins, join) : [];
2199
2139
  if (args[0]) {
2200
2140
  const target = args[0].text;
2201
2141
  const aliasHint = (name) => {
2202
2142
  const decl = doc.getDeclarationOwnerNode(target)?.node;
2203
- 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`;
2143
+ return decl && require_view_validation.isAliasDecl(decl) ? "" : ` — declare a @db.alias type (\`@db.alias ${name}\` + \`export type Other = ${name}\`) to join it under another name`;
2204
2144
  };
2205
2145
  if (target === entryTypeName) errors.push({
2206
2146
  message: `@db.view.joins cannot join the entry table '${target}' directly${aliasHint(target)}`,
@@ -2214,14 +2154,14 @@ const dbViewAnnotations = { view: {
2214
2154
  });
2215
2155
  }
2216
2156
  const scope = join && viewJoinScope(owner, join, earlier);
2217
- 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"));
2157
+ if (args[1]?.queryNode && scope) errors.push(...require_view_validation.validateQueryScope(args[1], scope, doc, "a join may reference the entry table and joins declared before it"));
2218
2158
  const order = join && args[3]?.orderNode ? joinOrderScope(join) : void 0;
2219
2159
  if (order) errors.push(...validateJoinOrder(args[3], order, doc));
2220
2160
  return errors;
2221
2161
  }
2222
2162
  }),
2223
2163
  filter: new _atscript_core.AnnotationSpec({
2224
- 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",
2164
+ description: (0, _atscript_core.annotationExample)("WHERE clause for a view, filtering which rows are included.", "@db.view.for User", "@db.view.filter `User.status = 'active' and User.age >= 18`", "export interface ActiveUser { ... }"),
2225
2165
  nodeType: ["interface"],
2226
2166
  passedWhenReferred: false,
2227
2167
  argument: {
@@ -2233,7 +2173,7 @@ const dbViewAnnotations = { view: {
2233
2173
  validate(token, args, doc) {
2234
2174
  const errors = [];
2235
2175
  const owner = token.parentNode;
2236
- if (!require_validation_utils.hasAnyViewAnnotation(owner) && !args[0]) {
2176
+ if (!require_view_validation.hasAnyViewAnnotation(owner) && !args[0]) {
2237
2177
  errors.push({
2238
2178
  message: "@db.view.filter is only valid on @db.view interfaces",
2239
2179
  severity: 1,
@@ -2251,18 +2191,18 @@ const dbViewAnnotations = { view: {
2251
2191
  });
2252
2192
  return errors;
2253
2193
  }
2254
- errors.push(...require_validation_utils.validateQueryScope(args[0], scope, doc));
2194
+ errors.push(...require_view_validation.validateQueryScope(args[0], scope, doc));
2255
2195
  return errors;
2256
2196
  }
2257
2197
  }),
2258
2198
  materialized: new _atscript_core.AnnotationSpec({
2259
- 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",
2199
+ description: (0, _atscript_core.annotationExample)("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.", "@db.view.materialized", "@db.view.for User", "@db.view.filter `User.status = 'active'`", "export interface ActiveUsers { ... }"),
2260
2200
  nodeType: ["interface"],
2261
2201
  passedWhenReferred: false,
2262
2202
  validate(token, _args, _doc) {
2263
2203
  const errors = [];
2264
2204
  const owner = token.parentNode;
2265
- if (!require_validation_utils.hasAnyViewAnnotation(owner)) errors.push({
2205
+ if (!require_view_validation.hasAnyViewAnnotation(owner)) errors.push({
2266
2206
  message: "@db.view.materialized is only valid on @db.view interfaces",
2267
2207
  severity: 1,
2268
2208
  range: token.range
@@ -2271,7 +2211,7 @@ const dbViewAnnotations = { view: {
2271
2211
  }
2272
2212
  }),
2273
2213
  renamed: new _atscript_core.AnnotationSpec({
2274
- 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",
2214
+ description: (0, _atscript_core.annotationExample)("Specifies the previous view name for view rename migration. The sync engine drops the old view and creates the new one.", "@db.view \"active_premium_users\"", "@db.view.renamed \"active_users\"", "@db.view.for User", "export interface ActivePremiumUser { ... }"),
2275
2215
  nodeType: ["interface"],
2276
2216
  passedWhenReferred: false,
2277
2217
  argument: {
@@ -2282,7 +2222,7 @@ const dbViewAnnotations = { view: {
2282
2222
  validate(token, _args, _doc) {
2283
2223
  const errors = [];
2284
2224
  const owner = token.parentNode;
2285
- if (!require_validation_utils.hasAnyViewAnnotation(owner)) errors.push({
2225
+ if (!require_view_validation.hasAnyViewAnnotation(owner)) errors.push({
2286
2226
  message: "@db.view.renamed requires @db.view on the same interface",
2287
2227
  severity: 1,
2288
2228
  range: token.range
@@ -2291,7 +2231,7 @@ const dbViewAnnotations = { view: {
2291
2231
  }
2292
2232
  }),
2293
2233
  having: new _atscript_core.AnnotationSpec({
2294
- 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",
2234
+ description: (0, _atscript_core.annotationExample)("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.", "@db.view", "@db.view.for Order", "@db.view.having `totalRevenue > 100`", "export interface TopCategories { ... }"),
2295
2235
  nodeType: ["interface"],
2296
2236
  passedWhenReferred: false,
2297
2237
  argument: {
@@ -2303,7 +2243,7 @@ const dbViewAnnotations = { view: {
2303
2243
  validate(token, args, doc) {
2304
2244
  const errors = [];
2305
2245
  const owner = token.parentNode;
2306
- if (!require_validation_utils.hasAnyViewAnnotation(owner)) {
2246
+ if (!require_view_validation.hasAnyViewAnnotation(owner)) {
2307
2247
  errors.push({
2308
2248
  message: "@db.view.having is only valid on @db.view interfaces",
2309
2249
  severity: 1,
@@ -2312,7 +2252,7 @@ const dbViewAnnotations = { view: {
2312
2252
  return errors;
2313
2253
  }
2314
2254
  const scope = viewHavingScope(owner);
2315
- 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"));
2255
+ if (args[0]?.queryNode && scope) errors.push(...require_view_validation.validateQueryScope(args[0], scope, doc, "@db.view.having references the view's own fields unqualified"));
2316
2256
  return errors;
2317
2257
  }
2318
2258
  })
@@ -2359,18 +2299,18 @@ const dbPlugin = (options) => ({
2359
2299
  kind: "array",
2360
2300
  of: "number"
2361
2301
  },
2362
- documentation: "Represents a **vector embedding** (array of numbers) for **similarity search**.\n\n- Equivalent to `number[]` but explicitly marks the field as a vector embedding.\n- Each adapter maps this to its native vector type:\n - **MongoDB** → BSON array\n - **MySQL 9+** → `VECTOR(N)`\n - **PostgreSQL** → pgvector `vector(N)`\n - **SQLite** → JSON\n\n**Example:**\n```atscript\n@db.search.vector 1536, \"cosine\"\nembedding: db.vector\n```\n"
2302
+ documentation: (0, _atscript_core.annotationExample)("Represents a **vector embedding** (array of numbers) for **similarity search**.\n\n- Equivalent to `number[]` but explicitly marks the field as a vector embedding.\n- Each adapter maps this to its native vector type:\n - **MongoDB** → BSON array\n - **MySQL 9+** → `VECTOR(N)`\n - **PostgreSQL** → pgvector `vector(N)`\n - **SQLite** → JSON", "@db.search.vector 1536, \"cosine\"", "embedding: db.vector")
2363
2303
  },
2364
2304
  geoPoint: {
2365
2305
  type: {
2366
2306
  kind: "array",
2367
2307
  of: "number"
2368
2308
  },
2369
- documentation: "Represents a **geographic point** as a `[longitude, latitude]` tuple (GeoJSON coordinate order).\n\n- Equivalent to `number[]` of length 2, but explicitly marks the field as a geo point.\n- **Coordinate order is GeoJSON order: longitude first.**\n- Each adapter maps this to its native storage:\n - **MongoDB** → GeoJSON `{ type: 'Point', coordinates: [lng, lat] }`\n - **PostgreSQL** → PostGIS `geography(Point,4326)` (JSONB without PostGIS)\n - **MySQL** → `POINT SRID 4326`\n - **SQLite** → JSON `TEXT` (haversine-based search)\n\n**Example:**\n```atscript\n@db.index.geo\ngeo: db.geoPoint\n```\n"
2309
+ documentation: (0, _atscript_core.annotationExample)("Represents a **geographic point** as a `[longitude, latitude]` tuple (GeoJSON coordinate order).\n\n- Equivalent to `number[]` of length 2, but explicitly marks the field as a geo point.\n- **Coordinate order is GeoJSON order: longitude first.**\n- Each adapter maps this to its native storage:\n - **MongoDB** → GeoJSON `{ type: 'Point', coordinates: [lng, lat] }`\n - **PostgreSQL** → PostGIS `geography(Point,4326)` (JSONB without PostGIS)\n - **MySQL** → `POINT SRID 4326`\n - **SQLite** → JSON `TEXT` (haversine-based search)", "@db.index.geo", "geo: db.geoPoint")
2370
2310
  },
2371
2311
  currencyCode: {
2372
2312
  type: "string",
2373
- documentation: "Represents a **currency code** — typically ISO 4217 (`'USD'`, `'EUR'`, `'JPY'`) but accepts any uppercase alphanumeric code 2–10 chars long, so crypto and custom codes (`'BTC'`, `'USDC'`, `'POINTS'`) fit too.\n\nPair with `@db.amount.currency.ref 'fieldName'` on a `decimal` field to bind the amount to its row-level currency. The validator checks that the ref target resolves to this type (or a plain `string`).\n\n**Example:**\n```atscript\ncurrency: db.currencyCode\n@db.amount.currency.ref 'currency'\namount: decimal\n```\n",
2313
+ documentation: (0, _atscript_core.annotationExample)("Represents a **currency code** — typically ISO 4217 (`'USD'`, `'EUR'`, `'JPY'`) but accepts any uppercase alphanumeric code 2–10 chars long, so crypto and custom codes (`'BTC'`, `'USDC'`, `'POINTS'`) fit too.\n\nPair with `@db.amount.currency.ref 'fieldName'` on a `decimal` field to bind the amount to its row-level currency. The validator checks that the ref target resolves to this type (or a plain `string`).", "currency: db.currencyCode", "@db.amount.currency.ref 'currency'", "amount: decimal"),
2374
2314
  annotations: { "expect.pattern": [{
2375
2315
  pattern: "^[A-Z0-9]{2,10}$",
2376
2316
  message: "Invalid currency code (expected 2–10 uppercase letters or digits)"