@atscript/db 0.1.139 → 0.1.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/agg.d.cts +1 -1
  2. package/dist/agg.d.mts +1 -1
  3. package/dist/{buckets-C-27xmtq.d.cts → buckets-Bv4pah66.d.cts} +225 -22
  4. package/dist/{buckets-BFG2RYRW.d.mts → buckets-CjL7F-hp.d.mts} +225 -22
  5. package/dist/{column-diff-CgxgFKzx.cjs → column-diff-CfPNcP6e.cjs} +663 -239
  6. package/dist/{column-diff-BwOA5101.mjs → column-diff-CmFNXV8C.mjs} +638 -220
  7. package/dist/column-diff-DiBbXyLA.d.cts +211 -0
  8. package/dist/column-diff-n-k5KY0u.d.mts +211 -0
  9. package/dist/derived-rules-0sKn4f5C.mjs +44 -0
  10. package/dist/derived-rules-YstgIxG-.cjs +67 -0
  11. package/dist/index.cjs +38 -4
  12. package/dist/index.d.cts +23 -6
  13. package/dist/index.d.mts +23 -6
  14. package/dist/index.mjs +34 -4
  15. package/dist/{nested-writer-FWD5oOYh.mjs → nested-writer-BO3vhbkP.mjs} +8 -4
  16. package/dist/{nested-writer-BZNCuqI6.cjs → nested-writer-DYsRxZ5f.cjs} +8 -4
  17. package/dist/object-DSN0h9lB.d.cts +30 -0
  18. package/dist/object-DSN0h9lB.d.mts +30 -0
  19. package/dist/plugin.cjs +392 -139
  20. package/dist/plugin.mjs +392 -139
  21. package/dist/rel.cjs +2 -2
  22. package/dist/rel.d.cts +2 -2
  23. package/dist/rel.d.mts +2 -2
  24. package/dist/rel.mjs +2 -2
  25. package/dist/{relation-helpers-D3Zu0Mta.d.mts → relation-helpers-B59to_dG.d.mts} +5 -4
  26. package/dist/{relation-helpers-DxrvS6ar.d.cts → relation-helpers-DQ_nRsV9.d.cts} +5 -4
  27. package/dist/{relation-loader-6ZB_5KFq.cjs → relation-loader-CgJ8bK6X.cjs} +1 -1
  28. package/dist/{relation-loader-CTFaZpVa.mjs → relation-loader-CuhEBzFU.mjs} +1 -1
  29. package/dist/shared.cjs +6 -1
  30. package/dist/shared.d.cts +48 -9
  31. package/dist/shared.d.mts +48 -9
  32. package/dist/shared.mjs +2 -2
  33. package/dist/sync.cjs +331 -105
  34. package/dist/sync.d.cts +62 -163
  35. package/dist/sync.d.mts +62 -163
  36. package/dist/sync.mjs +331 -105
  37. package/dist/{validation-utils-B4h-GW4d.mjs → validation-utils-CMR4fe2M.mjs} +99 -34
  38. package/dist/{validation-utils-Dg0hW6dn.cjs → validation-utils-DOsB4e6G.cjs} +128 -33
  39. package/dist/{validator-Drb2N-YL.d.cts → validator-Bw6ks9Hy.d.cts} +1 -11
  40. package/dist/{validator-Drb2N-YL.d.mts → validator-Bw6ks9Hy.d.mts} +1 -11
  41. package/dist/{validator-Ch7UIQl9.mjs → validator-D8bPsXPN.mjs} +54 -2
  42. package/dist/{validator-BtZbcLN2.cjs → validator-DASnXf1j.cjs} +77 -1
  43. package/dist/validator.cjs +1 -1
  44. package/dist/validator.d.cts +2 -1
  45. package/dist/validator.d.mts +2 -1
  46. package/dist/validator.mjs +1 -1
  47. package/package.json +6 -6
  48. package/dist/column-diff-BmqvgBWw.d.cts +0 -24
  49. package/dist/column-diff-DPkbZIVE.d.mts +0 -24
@@ -1,8 +1,9 @@
1
1
  const require_db_error = require("./db-error-DTkkeu5b.cjs");
2
2
  const require_aggregate_fns = require("./aggregate-fns-CGBv3E8S.cjs");
3
- const require_nested_writer = require("./nested-writer-BZNCuqI6.cjs");
3
+ const require_nested_writer = require("./nested-writer-DYsRxZ5f.cjs");
4
+ const require_derived_rules = require("./derived-rules-YstgIxG-.cjs");
5
+ const require_validator = require("./validator-DASnXf1j.cjs");
4
6
  require("./agg.cjs");
5
- const require_validator = require("./validator-BtZbcLN2.cjs");
6
7
  const require_ops = require("./ops.cjs");
7
8
  let _atscript_typescript_utils = require("@atscript/typescript/utils");
8
9
  let _uniqu_core = require("@uniqu/core");
@@ -101,22 +102,184 @@ function jsonValueAncestor(path, jsonValueParents) {
101
102
  }
102
103
  }
103
104
  //#endregion
104
- //#region src/table/table-metadata.ts
105
- const INDEX_PREFIX = "atscript__";
106
- function indexKey(type, name) {
107
- return `${INDEX_PREFIX}${type}__${name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 117 - type.length - 2)}`;
105
+ //#region src/table/view-source.ts
106
+ /** Resolves a compiled type reference (a class, a lazy `() => T`, or `{ type: () => T }`). */
107
+ function resolveTypeRef(value) {
108
+ if ((0, _atscript_typescript_utils.isAnnotatedType)(value)) return value;
109
+ if (typeof value === "function") {
110
+ const resolved = value();
111
+ return (0, _atscript_typescript_utils.isAnnotatedType)(resolved) ? resolved : void 0;
112
+ }
113
+ if (value && typeof value === "object" && typeof value.type === "function") return resolveTypeRef(value.type);
108
114
  }
109
115
  /**
110
- * Finds the nearest ancestor of `path` that belongs to `set`.
111
- * Used by both the build pipeline (in `_classifyFields`) and
112
- * runtime reconstruction on the Readable.
116
+ * The table / view a `@db.alias` type stands for, or `undefined` when `type`
117
+ * is not an alias.
118
+ * @since 0.1.141
113
119
  */
114
- function findAncestorInSet(path, set) {
115
- let pos = path.length;
116
- while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) {
117
- const ancestor = path.slice(0, pos);
118
- if (set.has(ancestor)) return ancestor;
120
+ function aliasTargetOf(type) {
121
+ const target = type?.metadata?.get("db.alias");
122
+ if (target === void 0) return void 0;
123
+ const resolved = resolveTypeRef(target);
124
+ if (!resolved) throw new Error(`@db.alias of "${type?.id ?? ""}" does not resolve to an annotated type`);
125
+ return resolved;
126
+ }
127
+ /**
128
+ * How a view plan addresses a source type: a plain table or view is scoped
129
+ * by its physical name; a `@db.alias` type is scoped by its own type name
130
+ * over the aliased table / view (`JOIN "employees" AS "Manager"`).
131
+ * @since 0.1.141
132
+ */
133
+ function viewSourceOf(type) {
134
+ const target = aliasTargetOf(type);
135
+ if (!target) {
136
+ const table = require_nested_writer.tableNameOf(type);
137
+ return {
138
+ name: table,
139
+ table,
140
+ type
141
+ };
142
+ }
143
+ if (aliasTargetOf(target)) throw new Error(`@db.alias "${type.id ?? ""}" targets "${target.id ?? ""}", which is itself a @db.alias — alias the table or view directly`);
144
+ return {
145
+ name: type.id ?? "",
146
+ table: require_nested_writer.tableNameOf(target),
147
+ type: target,
148
+ alias: true
149
+ };
150
+ }
151
+ const indexCache = /* @__PURE__ */ new WeakMap();
152
+ function sourceIndex(type) {
153
+ let idx = indexCache.get(type);
154
+ if (idx) return idx;
155
+ idx = {
156
+ flatMap: /* @__PURE__ */ new Map(),
157
+ columnMap: /* @__PURE__ */ new Map(),
158
+ documentColumnMap: /* @__PURE__ */ new Map(),
159
+ unstored: /* @__PURE__ */ new Set(),
160
+ jsonRoots: /* @__PURE__ */ new Set(),
161
+ encrypted: /* @__PURE__ */ new Set(),
162
+ optional: /* @__PURE__ */ new Set(),
163
+ derived: /* @__PURE__ */ new Map()
164
+ };
165
+ if (type.type.kind === "object") {
166
+ const navFields = /* @__PURE__ */ new Set();
167
+ const collected = [];
168
+ idx.flatMap = (0, _atscript_typescript_utils.flattenAnnotatedType)(type, {
169
+ excludePhantomTypes: true,
170
+ onField: (path, fieldType, metadata) => {
171
+ if (isNavRelation(metadata)) navFields.add(path);
172
+ collected.push([
173
+ path,
174
+ fieldType,
175
+ metadata
176
+ ]);
177
+ }
178
+ });
179
+ const storage = [];
180
+ for (const [path, fieldType, metadata] of collected) {
181
+ if (!path || require_validator.findAncestorInSet(path, navFields) !== void 0) continue;
182
+ const column = metadata.get("db.column");
183
+ if (column) {
184
+ idx.columnMap.set(path, column);
185
+ if (columnOverrideApplies(path, true)) idx.documentColumnMap.set(path, column);
186
+ }
187
+ if (metadata.has("db.ignore") || navFields.has(path)) idx.unstored.add(path);
188
+ if (metadata.has("db.column.derived") && fieldType.ref?.field) idx.derived.set(path, fieldType.ref.field);
189
+ if (metadata.has("db.encrypted")) storage.push([path, false]);
190
+ else if (metadata.has("db.json") || resolveDesignType(fieldType) === "array") storage.push([path, true]);
191
+ }
192
+ storage.sort(([a], [b]) => a.split(".").length - b.split(".").length);
193
+ for (const [path, json] of storage) if (require_validator.findAncestorInSet(path, idx.jsonRoots) === void 0 && require_validator.findAncestorInSet(path, idx.encrypted) === void 0) (json ? idx.jsonRoots : idx.encrypted).add(path);
194
+ for (const [path, node] of idx.flatMap) if (path && node.optional) idx.optional.add(path);
195
+ }
196
+ indexCache.set(type, idx);
197
+ return idx;
198
+ }
199
+ /**
200
+ * Resolves a LOGICAL path of a source table (a view field's chain ref, an
201
+ * aggregate's field, a predicate operand) to where it is physically stored.
202
+ * Internal — `AtscriptDbView.resolveRefSource` is the public entry.
203
+ *
204
+ * Relational rules (`TableMetadata`'s): the outermost `@db.json` or array
205
+ * node with segments remaining is the column and the rest becomes
206
+ * {@link TViewSource.jsonPath}; a flattened leaf is its parent segments
207
+ * joined with `__` plus its `@db.column` (or segment); a top-level field is
208
+ * its `@db.column` or name. Nested-object adapters use the document path
209
+ * (only a top-level key is renamed) and never a JSON path.
210
+ *
211
+ * A `@db.column.derived` path (since 0.1.141) resolves to its generated
212
+ * column on relational adapters and to its JSON-leaf source path on
213
+ * nested-object adapters — always optional (the extraction yields NULL for a
214
+ * missing or off-type leaf).
215
+ *
216
+ * A path the type does not declare resolves to itself — the database
217
+ * reports the unknown column, as before.
218
+ * @throws for a path without storage (`@db.ignore`, a navigation relation,
219
+ * or inside one), and — relational — for a path inside an `@db.encrypted` field.
220
+ */
221
+ function resolveViewSource(sourceType, logicalPath, nestedObjects) {
222
+ const idx = sourceIndex(sourceType);
223
+ const node = idx.flatMap.get(logicalPath);
224
+ const derivedSource = idx.derived.get(logicalPath);
225
+ if (node && derivedSource !== void 0) {
226
+ if (nestedObjects) return {
227
+ ...resolveViewSource(sourceType, derivedSource, true),
228
+ optional: true
229
+ };
230
+ return {
231
+ column: relationalColumnName(logicalPath, idx.columnMap.get(logicalPath), false),
232
+ designType: resolveDesignType(node),
233
+ optional: true
234
+ };
235
+ }
236
+ const jsonRoot = require_validator.selfOrAncestor(logicalPath, idx.jsonRoots);
237
+ const optional = require_validator.selfOrAncestor(logicalPath, idx.optional) !== void 0 || jsonRoot !== void 0 && jsonRoot !== logicalPath;
238
+ if (!node) return {
239
+ column: logicalPath,
240
+ designType: "unknown",
241
+ optional
242
+ };
243
+ const unstored = require_validator.selfOrAncestor(logicalPath, idx.unstored);
244
+ if (unstored !== void 0) throw new Error(`"${logicalPath}" has no column — "${unstored}" is @db.ignore or a navigation relation`);
245
+ const designType = resolveDesignType(node);
246
+ if (nestedObjects) return {
247
+ column: documentPath(idx.documentColumnMap, logicalPath),
248
+ designType,
249
+ optional
250
+ };
251
+ const encrypted = require_validator.findAncestorInSet(logicalPath, idx.encrypted);
252
+ if (encrypted !== void 0) throw new Error(`"${logicalPath}" is inside the @db.encrypted field "${encrypted}" — reference "${encrypted}" itself`);
253
+ if (jsonRoot !== void 0) {
254
+ const column = relationalColumnName(jsonRoot, idx.columnMap.get(jsonRoot), jsonRoot.includes("."));
255
+ return jsonRoot === logicalPath ? {
256
+ column,
257
+ designType,
258
+ optional
259
+ } : {
260
+ column,
261
+ jsonPath: logicalPath.slice(jsonRoot.length + 1).split("."),
262
+ designType,
263
+ optional
264
+ };
119
265
  }
266
+ if (designType === "object" && !idx.encrypted.has(logicalPath)) return {
267
+ column: logicalPath.replace(/\./g, "__"),
268
+ designType,
269
+ flattened: true,
270
+ optional
271
+ };
272
+ return {
273
+ column: relationalColumnName(logicalPath, idx.columnMap.get(logicalPath), logicalPath.includes(".")),
274
+ designType,
275
+ optional
276
+ };
277
+ }
278
+ //#endregion
279
+ //#region src/table/table-metadata.ts
280
+ const INDEX_PREFIX = "atscript__";
281
+ function indexKey(type, name) {
282
+ return `${INDEX_PREFIX}${type}__${name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 117 - type.length - 2)}`;
120
283
  }
121
284
  /**
122
285
  * Whether a `@db.column` / `@db.column.renamed` on `path` applies: always on
@@ -192,6 +355,22 @@ var TableMetadata = class {
192
355
  nestedObjects;
193
356
  flatMap;
194
357
  fieldDescriptors;
358
+ /**
359
+ * The descriptors schema sync manages as columns of this table: the
360
+ * non-ignored ones — on nested-object adapters without the
361
+ * `@db.column.derived` fields, which store nothing there (their
362
+ * `physicalName` is the source's document path). The desired side of every
363
+ * column diff and the column list of a fresh create.
364
+ * @since 0.1.141
365
+ */
366
+ columnDescriptors = [];
367
+ /**
368
+ * The columns that hold a value of their own — non-ignored and not derived
369
+ * (a generated column is computed, never assigned): what a table recreation
370
+ * copies and a full replace assigns.
371
+ * @since 0.1.141
372
+ */
373
+ storedDescriptors = [];
195
374
  primaryKeys = [];
196
375
  preferredId = [];
197
376
  originalMetaIdFields = [];
@@ -212,6 +391,13 @@ var TableMetadata = class {
212
391
  quantityRefByField = /* @__PURE__ */ new Map();
213
392
  /** Logical paths annotated with `@db.encrypted` — stored as one opaque ciphertext column. */
214
393
  encryptedFields = /* @__PURE__ */ new Set();
394
+ /**
395
+ * `@db.column.derived` fields (top-level logical name → what they read),
396
+ * since 0.1.141. A generated column on relational adapters; on nested-object
397
+ * adapters nothing is stored — {@link physicalPath} maps the name to the
398
+ * source's document path and reads fill the field from it.
399
+ */
400
+ derivedFields = /* @__PURE__ */ new Map();
215
401
  pathToPhysical = /* @__PURE__ */ new Map();
216
402
  physicalToPath = /* @__PURE__ */ new Map();
217
403
  flattenedParents = /* @__PURE__ */ new Set();
@@ -268,9 +454,12 @@ var TableMetadata = class {
268
454
  get isBuilt() {
269
455
  return this._built;
270
456
  }
271
- /** {@link documentPath} over this table's `columnMap`. */
457
+ /**
458
+ * {@link documentPath} over this table's `columnMap`. A `@db.column.derived`
459
+ * field has no stored path of its own: it maps to its source leaf.
460
+ */
272
461
  documentPath(path) {
273
- return documentPath(this.columnMap, path);
462
+ return documentPath(this.columnMap, this.derivedFields.get(path)?.sourcePath ?? path);
274
463
  }
275
464
  /**
276
465
  * Physical name of a logical path: the document path on nested-object
@@ -278,7 +467,17 @@ var TableMetadata = class {
278
467
  * `@db.column` override).
279
468
  */
280
469
  physicalPath(logical) {
281
- return this.nestedObjects ? this.documentPath(logical) : this.pathToPhysical.get(logical) ?? this.columnMap.get(logical) ?? logical;
470
+ if (this.nestedObjects) return this.documentPath(logical);
471
+ return this.pathToPhysical.get(logical) ?? this.columnMap.get(logical) ?? logical;
472
+ }
473
+ /**
474
+ * Drops the `@db.column.derived` keys of a write payload in place (a
475
+ * derived field is always top-level): the column is computed from the row
476
+ * and never written, so a row read back can be written back as-is.
477
+ * @since 0.1.141
478
+ */
479
+ stripDerived(data) {
480
+ for (const field of this.derivedFields.keys()) delete data[field];
282
481
  }
283
482
  /**
284
483
  * Runs the full metadata compilation pipeline. Called once by
@@ -313,11 +512,15 @@ var TableMetadata = class {
313
512
  }
314
513
  });
315
514
  for (const entry of collected) {
316
- if (findAncestorInSet(entry.path, this.navFields) !== void 0) continue;
515
+ if (require_validator.findAncestorInSet(entry.path, this.navFields) !== void 0) {
516
+ this.ignoredFields.add(entry.path);
517
+ continue;
518
+ }
317
519
  this._scanGenericAnnotations(entry.path, entry.type, entry.metadata, logger);
520
+ if (entry.metadata.has("db.column.derived")) this.derivedFields.set(entry.path, this._validateDerivedField(entry.path, entry.type, entry.metadata, type));
318
521
  adapter.onFieldScanned?.(entry.path, entry.type, entry.metadata);
319
522
  }
320
- for (const path of this.encryptedFields) if (findAncestorInSet(path, this.encryptedFields) !== void 0) this.encryptedFields.delete(path);
523
+ for (const path of this.encryptedFields) if (require_validator.findAncestorInSet(path, this.encryptedFields) !== void 0) this.encryptedFields.delete(path);
321
524
  if (!this.nestedObjects) this._classifyFields();
322
525
  const overrides = adapter.getMetadataOverrides?.(this);
323
526
  if (overrides) this._applyOverrides(overrides);
@@ -337,11 +540,10 @@ var TableMetadata = class {
337
540
  this.jsonFields.clear();
338
541
  this._built = true;
339
542
  adapter.onAfterFlatten?.();
340
- if (this.nestedObjects && this.flatMap) {
341
- for (const path of this.flatMap.keys()) if (path && !this.ignoredFields.has(path) && !this.navFields.has(path) && findAncestorInSet(path, this.navFields) === void 0) this.allPhysicalFields.push(this.documentPath(path));
342
- } else for (const [path, physical] of this.pathToPhysical) {
543
+ if (this.nestedObjects) for (const fd of this.columnDescriptors) this.allPhysicalFields.push(fd.physicalName);
544
+ else for (const [path, physical] of this.pathToPhysical) {
343
545
  if (this.navFields.has(path)) continue;
344
- if (findAncestorInSet(path, this.navFields) !== void 0) continue;
546
+ if (require_validator.findAncestorInSet(path, this.navFields) !== void 0) continue;
345
547
  this.allPhysicalFields.push(physical);
346
548
  }
347
549
  }
@@ -485,6 +687,45 @@ var TableMetadata = class {
485
687
  if (metadata.has("db.default.increment") || metadata.has("db.default.now")) reject("@db.default.increment / @db.default.now", "engine-side defaults bypass the encryption transform");
486
688
  if (metadata.get("db.patch.strategy") === "merge") reject("@db.patch.strategy \"merge\"", "ciphertext is opaque — partial merges would silently drop omitted keys");
487
689
  }
690
+ /**
691
+ * Build-time diagnostics for `@db.column.derived` (rules D1–D8 of the
692
+ * derived-column design) — the runtime mirror of the compile-time check, so
693
+ * pre-compiled models fail fast — and the resolution of what the field
694
+ * reads: the source leaf's JSON column + path (relational layout, via the
695
+ * views' `resolveViewSource`) and its declared type.
696
+ */
697
+ _validateDerivedField(fieldName, fieldType, metadata, rootType) {
698
+ const reject = (why) => {
699
+ throw new Error(`@db.column.derived on "${fieldName}": ${why}`);
700
+ };
701
+ if (fieldName.includes(".")) reject("only a top-level field of a table can be derived");
702
+ for (const [name, why] of require_derived_rules.DERIVED_INCOMPATIBLE) if (metadata.has(name)) reject(`cannot coexist with @${name} — ${why}`);
703
+ const ref = fieldType.ref;
704
+ if (!ref?.field) reject("requires a chain reference into a @db.json field of the same table (e.g. `customerId: Order.payload.customer.id`)");
705
+ const target = ref.type();
706
+ if (target !== rootType) reject(`must reference the enclosing table "${rootType.id ?? ""}", not "${target?.id ?? ""}" — a derived column reads its own row`);
707
+ const sourcePath = ref.field;
708
+ const segments = sourcePath.split(".");
709
+ let jsonRoot;
710
+ for (let i = 1; i <= segments.length; i++) {
711
+ const prefix = segments.slice(0, i).join(".");
712
+ const node = this.flatMap.get(prefix);
713
+ if (!node) reject(`path "${sourcePath}" does not exist on the table`);
714
+ if (node.metadata.has("db.encrypted")) reject(`path "${sourcePath}" reads inside the @db.encrypted field "${prefix}" — ciphertext cannot be extracted`);
715
+ if (resolveDesignType(node) === "array") reject(`path "${sourcePath}" crosses the array "${prefix}" — a derived column reads one scalar leaf`);
716
+ if (jsonRoot === void 0 && node.metadata.has("db.json")) jsonRoot = prefix;
717
+ }
718
+ if (jsonRoot === void 0 || jsonRoot === sourcePath) reject(`path "${sourcePath}" does not read inside a @db.json field — a flattened or scalar column needs no derived column`);
719
+ const leafType = resolveDesignType(this.flatMap.get(sourcePath));
720
+ if (!require_derived_rules.isJsonLeafType(leafType)) return reject(`path "${sourcePath}" must end at a string, number or boolean leaf (got "${leafType}")`);
721
+ const source = resolveViewSource(rootType, sourcePath, false);
722
+ return {
723
+ sourcePath,
724
+ sourceColumn: source.column,
725
+ jsonPath: source.jsonPath,
726
+ type: leafType
727
+ };
728
+ }
488
729
  /** Build-time diagnostics for `@db.index.geo` (§3 of the geo-index spec). */
489
730
  _validateGeoIndexField(fieldName, fieldType, metadata) {
490
731
  const reject = (why) => {
@@ -521,7 +762,7 @@ var TableMetadata = class {
521
762
  _classifyFields() {
522
763
  for (const [path, type] of this.flatMap.entries()) {
523
764
  if (!path) continue;
524
- if (this.encryptedFields.has(path) || findAncestorInSet(path, this.encryptedFields)) continue;
765
+ if (this.encryptedFields.has(path) || require_validator.findAncestorInSet(path, this.encryptedFields)) continue;
525
766
  const designType = resolveDesignType(type);
526
767
  const isJson = this.jsonFields.has(path);
527
768
  const isArray = designType === "array";
@@ -537,9 +778,9 @@ var TableMetadata = class {
537
778
  for (const [path] of this.flatMap.entries()) {
538
779
  if (!path) continue;
539
780
  if (this.flattenedParents.has(path)) continue;
540
- if (findAncestorInSet(path, this.jsonFields) !== void 0) continue;
541
- if (findAncestorInSet(path, this.encryptedFields) !== void 0) continue;
542
- const isFlattened = findAncestorInSet(path, this.flattenedParents) !== void 0;
781
+ if (require_validator.findAncestorInSet(path, this.jsonFields) !== void 0) continue;
782
+ if (require_validator.findAncestorInSet(path, this.encryptedFields) !== void 0) continue;
783
+ const isFlattened = require_validator.findAncestorInSet(path, this.flattenedParents) !== void 0;
543
784
  const physicalName = relationalColumnName(path, this.columnMap.get(path), isFlattened);
544
785
  this.pathToPhysical.set(path, physicalName);
545
786
  this.physicalToPath.set(physicalName, path);
@@ -573,7 +814,7 @@ var TableMetadata = class {
573
814
  for (const fd of this.fieldDescriptors) {
574
815
  physicalNames.add(fd.physicalName);
575
816
  if (fd.ignored) continue;
576
- if (this.navFields.has(fd.path) || findAncestorInSet(fd.path, this.navFields) !== void 0) continue;
817
+ if (this.navFields.has(fd.path) || require_validator.findAncestorInSet(fd.path, this.navFields) !== void 0) continue;
577
818
  this.descriptorByPath.set(fd.path, fd);
578
819
  if (fd.storage === "json") jsonParents.add(fd.path);
579
820
  if (isJsonValueField(fd)) jsonValueParents.add(fd.path);
@@ -617,12 +858,12 @@ var TableMetadata = class {
617
858
  for (const [path, type] of this.flatMap.entries()) {
618
859
  if (!path) continue;
619
860
  if (!skipFlattening && this.flattenedParents.has(path)) continue;
620
- if (!skipFlattening && findAncestorInSet(path, this.jsonFields) !== void 0) continue;
861
+ if (!skipFlattening && require_validator.findAncestorInSet(path, this.jsonFields) !== void 0) continue;
621
862
  const isEncrypted = this.encryptedFields.has(path);
622
- const underEncrypted = findAncestorInSet(path, this.encryptedFields) !== void 0;
863
+ const underEncrypted = require_validator.findAncestorInSet(path, this.encryptedFields) !== void 0;
623
864
  if (!skipFlattening && underEncrypted) continue;
624
865
  const isJson = this.jsonFields.has(path);
625
- const isFlattened = !skipFlattening && findAncestorInSet(path, this.flattenedParents) !== void 0;
866
+ const isFlattened = !skipFlattening && require_validator.findAncestorInSet(path, this.flattenedParents) !== void 0;
626
867
  const designType = isEncrypted ? "string" : isJson ? "json" : resolveDesignType(type);
627
868
  let storage;
628
869
  if (skipFlattening) storage = "column";
@@ -659,12 +900,17 @@ var TableMetadata = class {
659
900
  unitCode,
660
901
  unitRefField,
661
902
  encrypted: isEncrypted || underEncrypted || void 0,
662
- isGeoPoint: isGeoPointType(type) || void 0
903
+ isGeoPoint: isGeoPointType(type) || void 0,
904
+ derived: this.derivedFields.get(path)
663
905
  });
664
906
  }
665
907
  this._resolveFkTargetFields(descriptors);
908
+ Object.freeze(descriptors);
909
+ this.fieldDescriptors = descriptors;
910
+ this.columnDescriptors = Object.freeze(descriptors.filter((fd) => !fd.ignored && !(skipFlattening && fd.derived)));
911
+ this.storedDescriptors = Object.freeze(descriptors.filter((fd) => !fd.ignored && !fd.derived));
666
912
  const fmtHook = adapter.formatValue?.bind(adapter);
667
- if (fmtHook) for (const fd of descriptors) {
913
+ if (fmtHook) for (const fd of this.columnDescriptors) {
668
914
  const fmt = fmtHook(fd);
669
915
  if (fmt) if (typeof fmt === "function") {
670
916
  if (!this.toStorageFormatters) this.toStorageFormatters = /* @__PURE__ */ new Map();
@@ -680,8 +926,6 @@ var TableMetadata = class {
680
926
  }
681
927
  }
682
928
  }
683
- Object.freeze(descriptors);
684
- this.fieldDescriptors = descriptors;
685
929
  }
686
930
  /**
687
931
  * Resolves `fkTargetField` for FK fields in field descriptors.
@@ -915,6 +1159,79 @@ var UniquSelect = class UniquSelect {
915
1159
  };
916
1160
  //#endregion
917
1161
  //#region src/strategies/field-mapping.ts
1162
+ const NO_PLAN = {
1163
+ fill: [],
1164
+ prune: [],
1165
+ unexclude: /* @__PURE__ */ new Set()
1166
+ };
1167
+ /** The no-projection plan of a table (every derived field, nothing pruned) — built once per table. */
1168
+ const fullPlans = /* @__PURE__ */ new WeakMap();
1169
+ /** An object-form `$select` (inclusion or exclusion) — not an array. */
1170
+ function isObjectForm(select) {
1171
+ return select !== null && typeof select === "object" && !Array.isArray(select);
1172
+ }
1173
+ /** An exclusion projection: object form whose first flag is not `1` / `true`. */
1174
+ function isExclusionProjection(select) {
1175
+ if (!isObjectForm(select)) return false;
1176
+ for (const flag of Object.values(select)) return flag !== 1 && flag !== true;
1177
+ return false;
1178
+ }
1179
+ /**
1180
+ * The logical keys a read names, in one pass: the `$groupBy` dimensions
1181
+ * (array or single string) and the `$select` inclusions (array-form strings,
1182
+ * object-form keys flagged `1` / `true`). Empty without a projection.
1183
+ */
1184
+ function requestedKeys(controls) {
1185
+ const out = [];
1186
+ const groupBy = controls?.$groupBy;
1187
+ if (typeof groupBy === "string") out.push(groupBy);
1188
+ else if (Array.isArray(groupBy)) {
1189
+ for (const key of groupBy) if (typeof key === "string") out.push(key);
1190
+ }
1191
+ const select = controls?.$select;
1192
+ if (Array.isArray(select)) {
1193
+ for (const item of select) if (typeof item === "string") out.push(item);
1194
+ } else if (isObjectForm(select)) {
1195
+ for (const [key, flag] of Object.entries(select)) if (flag === 1 || flag === true) out.push(key);
1196
+ }
1197
+ return out;
1198
+ }
1199
+ /** {@link TPrunePath} of `path`, keeping the ancestors something in `requested` lives under. */
1200
+ function prunePath(path, requested) {
1201
+ const segments = path.split(".");
1202
+ for (let depth = segments.length - 1; depth >= 1; depth--) {
1203
+ const prefix = `${segments.slice(0, depth).join(".")}.`;
1204
+ for (const r of requested) if (r.startsWith(prefix)) return {
1205
+ segments,
1206
+ keep: depth
1207
+ };
1208
+ }
1209
+ return {
1210
+ segments,
1211
+ keep: 0
1212
+ };
1213
+ }
1214
+ /** Deletes a pruned path from a row, then every ancestor it left empty (down to `keep`). */
1215
+ function pruneNestedPath(row, { segments, keep }) {
1216
+ require_validator.deletePath(row, segments);
1217
+ for (let depth = segments.length - 1; depth > keep; depth--) {
1218
+ const ancestor = segments.slice(0, depth);
1219
+ const value = require_validator.getPath(row, ancestor);
1220
+ if (!isObjectForm(value) || Object.keys(value).length > 0) return;
1221
+ require_validator.deletePath(row, ancestor);
1222
+ }
1223
+ }
1224
+ /**
1225
+ * Fills the `@db.column.derived` fields a read asked for from their source
1226
+ * leaf (as stored — no type guard; `null` for a missing leaf) and removes
1227
+ * the source paths the caller did not select, so an inclusion `$select` of
1228
+ * a derived field never leaks its source and an exclusion of the source
1229
+ * still yields the derived value.
1230
+ */
1231
+ function fillDerived(row, plan) {
1232
+ for (const [name, source] of plan.fill) row[name] = require_validator.getPath(row, source) ?? null;
1233
+ for (const path of plan.prune) pruneNestedPath(row, path);
1234
+ }
918
1235
  /** Coerces a storage value (0/1/null) back to a JS boolean. */
919
1236
  function toBool(value) {
920
1237
  if (value === null || value === void 0) return value;
@@ -932,6 +1249,15 @@ function toDecimalString(value) {
932
1249
  * and `RelationalFieldMapper` (flattened columns, SQL).
933
1250
  */
934
1251
  var FieldMappingStrategy = class {
1252
+ /**
1253
+ * {@link reconstructFromRead} over every row of one read — the per-read
1254
+ * work (the derived read plan on document adapters) is done once for all
1255
+ * of them. The default maps the rows one by one.
1256
+ * @since 0.1.141
1257
+ */
1258
+ reconstructRows(rows, meta, controls) {
1259
+ return rows.map((row) => this.reconstructFromRead(row, meta, controls));
1260
+ }
935
1261
  /**
936
1262
  * Whether {@link physicalPath} can differ from the logical path for this
937
1263
  * table; `false` lets the path translations hand their input back as-is.
@@ -1027,7 +1353,7 @@ var FieldMappingStrategy = class {
1027
1353
  */
1028
1354
  translateFilter(filter, meta) {
1029
1355
  if (!filter || typeof filter !== "object") return filter;
1030
- if (!meta.toStorageFormatters && meta.columnMap.size === 0) return filter;
1356
+ if (!meta.toStorageFormatters && meta.columnMap.size === 0 && meta.derivedFields.size === 0) return filter;
1031
1357
  const result = {};
1032
1358
  for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") result[key] = value.map((f) => this.translateFilter(f, meta));
1033
1359
  else if (key === "$not") result[key] = this.translateFilter(value, meta);
@@ -1161,6 +1487,7 @@ var FieldMappingStrategy = class {
1161
1487
  if (fieldType) data[pk] = adapter.prepareId(data[pk], fieldType);
1162
1488
  }
1163
1489
  for (const field of meta.ignoredFields) if (!field.includes(".")) delete data[field];
1490
+ meta.stripDerived(data);
1164
1491
  }
1165
1492
  };
1166
1493
  /**
@@ -1169,12 +1496,86 @@ var FieldMappingStrategy = class {
1169
1496
  * value coercion.
1170
1497
  */
1171
1498
  var DocumentFieldMapper = class extends FieldMappingStrategy {
1172
- reconstructFromRead(row, meta) {
1499
+ reconstructFromRead(row, meta, controls) {
1500
+ return this._reconstruct(row, meta, this._derivedPlanFor(meta, controls));
1501
+ }
1502
+ reconstructRows(rows, meta, controls) {
1503
+ const plan = this._derivedPlanFor(meta, controls);
1504
+ return rows.map((row) => this._reconstruct(row, meta, plan));
1505
+ }
1506
+ _derivedPlanFor(meta, controls) {
1507
+ return meta.derivedFields.size > 0 ? this.derivedReadPlan(controls, meta) : void 0;
1508
+ }
1509
+ _reconstruct(row, meta, plan) {
1173
1510
  this.coerceFieldValues(row, meta);
1174
1511
  this.applyFromStorageFormatters(row, meta);
1175
1512
  if (meta.columnMap.size > 0) this.reverseColumnRenames(row, meta);
1513
+ if (plan) fillDerived(row, plan);
1176
1514
  return row;
1177
1515
  }
1516
+ /** See {@link TDerivedReadPlan}. */
1517
+ derivedReadPlan(controls, meta) {
1518
+ if (meta.derivedFields.size === 0) return NO_PLAN;
1519
+ const select = controls?.$select;
1520
+ const groupBy = controls?.$groupBy;
1521
+ if (!(Array.isArray(groupBy) ? groupBy.length > 0 : typeof groupBy === "string") && (select === void 0 || select === null || isObjectForm(select) && Object.keys(select).length === 0)) {
1522
+ let plan = fullPlans.get(meta);
1523
+ if (!plan) {
1524
+ plan = {
1525
+ fill: [...meta.derivedFields].map(([name, d]) => [name, d.sourcePath.split(".")]),
1526
+ prune: [],
1527
+ unexclude: /* @__PURE__ */ new Set()
1528
+ };
1529
+ fullPlans.set(meta, plan);
1530
+ }
1531
+ return plan;
1532
+ }
1533
+ const keys = requestedKeys(controls);
1534
+ const requested = new Set(keys.filter((key) => !meta.derivedFields.has(key)));
1535
+ const fill = [];
1536
+ const prune = /* @__PURE__ */ new Set();
1537
+ if (isExclusionProjection(select)) {
1538
+ const excluded = new Set(Object.entries(select).filter(([, flag]) => flag === 0 || flag === false).map(([key]) => key));
1539
+ const unexclude = /* @__PURE__ */ new Set();
1540
+ for (const [name, derived] of meta.derivedFields) {
1541
+ if (excluded.has(name)) continue;
1542
+ fill.push([name, derived.sourcePath.split(".")]);
1543
+ for (let key = require_validator.selfOrAncestor(derived.sourcePath, excluded); key !== void 0; key = require_validator.findAncestorInSet(key, excluded)) {
1544
+ unexclude.add(key);
1545
+ prune.add(key);
1546
+ }
1547
+ }
1548
+ return {
1549
+ fill,
1550
+ prune: [...prune].map((p) => prunePath(p, requested)),
1551
+ unexclude
1552
+ };
1553
+ }
1554
+ const names = new Set(keys);
1555
+ for (const [name, derived] of meta.derivedFields) {
1556
+ if (!names.has(name)) continue;
1557
+ fill.push([name, derived.sourcePath.split(".")]);
1558
+ if (require_validator.selfOrAncestor(derived.sourcePath, requested) === void 0) prune.add(derived.sourcePath);
1559
+ }
1560
+ return {
1561
+ fill,
1562
+ prune: [...prune].map((p) => prunePath(p, requested)),
1563
+ unexclude: /* @__PURE__ */ new Set()
1564
+ };
1565
+ }
1566
+ /**
1567
+ * {@link FieldMappingStrategy.physicalSelect} plus the derived-field rules
1568
+ * of an exclusion projection: a derived key is not a stored path (dropping
1569
+ * it just leaves the field unfilled), and the source subtree of a wanted
1570
+ * derived field is fetched even when excluded (pruned after the read).
1571
+ */
1572
+ physicalSelect(select, meta) {
1573
+ if (meta.derivedFields.size === 0 || !isExclusionProjection(select)) return super.physicalSelect(select, meta);
1574
+ const { unexclude } = this.derivedReadPlan({ $select: select }, meta);
1575
+ const stored = {};
1576
+ for (const [key, flag] of Object.entries(select)) if (!meta.derivedFields.has(key) && !unexclude.has(key)) stored[key] = flag;
1577
+ return super.physicalSelect(stored, meta);
1578
+ }
1178
1579
  /**
1179
1580
  * Every field-path position goes through `@db.column` renames
1180
1581
  * ({@link TableMetadata.documentPath}): filter keys, `$select` fields
@@ -1199,7 +1600,7 @@ var DocumentFieldMapper = class extends FieldMappingStrategy {
1199
1600
  return meta.documentPath(logical);
1200
1601
  }
1201
1602
  renamesPaths(meta) {
1202
- return meta.columnMap.size > 0;
1603
+ return meta.columnMap.size > 0 || meta.derivedFields.size > 0;
1203
1604
  }
1204
1605
  prepareForWrite(payload, meta, adapter) {
1205
1606
  const data = { ...payload };
@@ -1229,7 +1630,7 @@ var DocumentFieldMapper = class extends FieldMappingStrategy {
1229
1630
  * for queries, filters, and controls.
1230
1631
  */
1231
1632
  var RelationalFieldMapper = class extends FieldMappingStrategy {
1232
- reconstructFromRead(row, meta) {
1633
+ reconstructFromRead(row, meta, _controls) {
1233
1634
  if (!meta.requiresMappings) return this.applyFromStorageFormatters(this.coerceFieldValues(row, meta), meta);
1234
1635
  if (meta.onlyColumnRenames) {
1235
1636
  this.coerceFieldValues(row, meta);
@@ -1462,7 +1863,7 @@ function assertGeoPoint(point, path) {
1462
1863
  }]);
1463
1864
  }
1464
1865
  function isEncryptedRef(meta, field) {
1465
- return meta.encryptedFields.has(field) || findAncestorInSet(field, meta.encryptedFields) !== void 0;
1866
+ return meta.encryptedFields.has(field) || require_validator.findAncestorInSet(field, meta.encryptedFields) !== void 0;
1466
1867
  }
1467
1868
  function encryptedRefError(code, field, what) {
1468
1869
  return new require_db_error.DbError(code, [{
@@ -1764,19 +2165,19 @@ function collectQueryPaths(query, aggregate) {
1764
2165
  */
1765
2166
  function classifyQueryPath(source, path) {
1766
2167
  if (source.navFields.has(path)) return { kind: "nav" };
1767
- const navHead = findAncestorInSet(path, source.navFields);
2168
+ const navHead = require_validator.findAncestorInSet(path, source.navFields);
1768
2169
  if (navHead !== void 0) return {
1769
2170
  kind: "nav",
1770
2171
  parent: navHead
1771
2172
  };
1772
2173
  if (source.leaves.has(path)) return { kind: "leaf" };
1773
2174
  if (source.objectParents.has(path)) return { kind: "objectParent" };
1774
- const jsonParent = findAncestorInSet(path, source.jsonParents);
2175
+ const jsonParent = require_validator.findAncestorInSet(path, source.jsonParents);
1775
2176
  if (jsonParent !== void 0) return {
1776
2177
  kind: "jsonDescendant",
1777
2178
  parent: jsonParent
1778
2179
  };
1779
- const encryptedParent = findAncestorInSet(path, source.encryptedFields);
2180
+ const encryptedParent = require_validator.findAncestorInSet(path, source.encryptedFields);
1780
2181
  if (encryptedParent !== void 0) return {
1781
2182
  kind: "encryptedDescendant",
1782
2183
  parent: encryptedParent
@@ -2065,11 +2466,7 @@ var AtscriptDbReadable = class {
2065
2466
  this._tableResolver = _tableResolver;
2066
2467
  if (!(0, _atscript_typescript_utils.isAnnotatedType)(_type)) throw new Error("Atscript Annotated Type expected");
2067
2468
  if (_type.type.kind !== "object") throw new Error("Database type must be an object type");
2068
- const adapterName = adapter.getAdapterTableName?.(_type);
2069
- const dbTable = _type.metadata.get("db.table");
2070
- const dbViewName = _type.metadata.get("db.view");
2071
- const fallbackName = _type.id || "";
2072
- this.tableName = adapterName || dbTable || dbViewName || fallbackName;
2469
+ this.tableName = adapter.getAdapterTableName?.(_type) || require_nested_writer.tableNameOf(_type);
2073
2470
  if (!this.tableName) throw new Error("@db.table or @db.view annotation expected");
2074
2471
  this.schema = _type.metadata.get("db.schema");
2075
2472
  this._syncMethod = _type.metadata.get("db.sync.method");
@@ -2239,11 +2636,25 @@ var AtscriptDbReadable = class {
2239
2636
  return this._metaIdPhysical;
2240
2637
  }
2241
2638
  /**
2242
- * Physical column name of the field annotated with `@db.column.version`, or
2243
- * `undefined` when the table has no version column. Used by adapters and the
2244
- * REST integration to drive optimistic concurrency control (OCC).
2639
+ * Logical field name of the field annotated with `@db.column.version`, or
2640
+ * `undefined` when the table has no version column. This is the key used in
2641
+ * `$cas: { <versionColumn>: N }`, in write payloads, in rows read back, and in
2642
+ * `/meta`'s `versionColumn` — a `@db.column 'physical_name'` rename on the
2643
+ * field changes only the storage column (see {@link versionColumnPhysical}).
2245
2644
  */
2246
2645
  get versionColumn() {
2646
+ this._ensureBuilt();
2647
+ return this._meta.versionField;
2648
+ }
2649
+ /**
2650
+ * Physical column name of the `@db.column.version` field (after any
2651
+ * `@db.column` rename), or `undefined` when the table has no version column.
2652
+ * Adapters use it for the auto-bump, the CAS predicate, and the insert-time
2653
+ * `0` backfill — all of which operate on already-mapped physical rows.
2654
+ *
2655
+ * @internal Adapter-facing surface; not part of the consumer API.
2656
+ */
2657
+ get versionColumnPhysical() {
2247
2658
  this._ensureBuilt();
2248
2659
  const field = this._meta.versionField;
2249
2660
  if (field === void 0) return void 0;
@@ -2278,6 +2689,14 @@ var AtscriptDbReadable = class {
2278
2689
  this._ensureBuilt();
2279
2690
  return this._meta.ignoredFields;
2280
2691
  }
2692
+ /**
2693
+ * `@db.column.derived` fields (logical name → what they read) — computed
2694
+ * from a JSON leaf of the same row, never written. Since 0.1.141.
2695
+ */
2696
+ get derivedFields() {
2697
+ this._ensureBuilt();
2698
+ return this._meta.derivedFields;
2699
+ }
2281
2700
  /** Navigational fields (`@db.rel.to` / `@db.rel.from`) — not stored as columns. */
2282
2701
  get navFields() {
2283
2702
  this._ensureBuilt();
@@ -2333,6 +2752,14 @@ var AtscriptDbReadable = class {
2333
2752
  };
2334
2753
  }
2335
2754
  /**
2755
+ * Physical rows of one read → logical rows: the field mapper's per-read
2756
+ * work (the derived read plan on document adapters) is done once for all
2757
+ * of them. Every read path funnels through here.
2758
+ */
2759
+ _fromRead(rows, controls) {
2760
+ return this._fieldMapper.reconstructRows(rows, this._meta, controls);
2761
+ }
2762
+ /**
2336
2763
  * Pre-computed field metadata for adapter use.
2337
2764
  */
2338
2765
  get fieldDescriptors() {
@@ -2340,6 +2767,26 @@ var AtscriptDbReadable = class {
2340
2767
  return this._meta.fieldDescriptors;
2341
2768
  }
2342
2769
  /**
2770
+ * The descriptors schema sync manages as columns: non-ignored, and on
2771
+ * nested-object adapters without the `@db.column.derived` fields (they store
2772
+ * nothing there). See `TableMetadata.columnDescriptors`.
2773
+ * @since 0.1.141
2774
+ */
2775
+ get columnDescriptors() {
2776
+ this._ensureBuilt();
2777
+ return this._meta.columnDescriptors;
2778
+ }
2779
+ /**
2780
+ * The columns that hold a value of their own (non-ignored, not derived) —
2781
+ * what a table recreation copies and a full replace assigns. See
2782
+ * `TableMetadata.storedDescriptors`.
2783
+ * @since 0.1.141
2784
+ */
2785
+ get storedDescriptors() {
2786
+ this._ensureBuilt();
2787
+ return this._meta.storedDescriptors;
2788
+ }
2789
+ /**
2343
2790
  * The target table of the navigation relation `navField` (since 0.1.134),
2344
2791
  * resolved through the table resolver this readable was built with (the
2345
2792
  * `DbSpace`'s). `undefined` when `navField` is not a relation of this table
@@ -2399,7 +2846,7 @@ var AtscriptDbReadable = class {
2399
2846
  const translatedQuery = this._fieldMapper.translateQuery(query, this._meta);
2400
2847
  const result = await this.adapter.findOne(translatedQuery);
2401
2848
  if (!result) return null;
2402
- const row = this._fieldMapper.reconstructFromRead(result, this._meta);
2849
+ const [row] = this._fromRead([result], query.controls);
2403
2850
  await this._decryptRows([row]);
2404
2851
  if (withRelations?.length) await this.loadRelations([row], withRelations);
2405
2852
  return row;
@@ -2414,7 +2861,8 @@ var AtscriptDbReadable = class {
2414
2861
  this._guardQuery(query);
2415
2862
  const withRelations = query.controls?.$with;
2416
2863
  const translatedQuery = this._fieldMapper.translateQuery(query, this._meta);
2417
- const rows = (await this.adapter.findMany(translatedQuery)).map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
2864
+ const results = await this.adapter.findMany(translatedQuery);
2865
+ const rows = this._fromRead(results, query.controls);
2418
2866
  await this._decryptRows(rows);
2419
2867
  if (withRelations?.length) await this.loadRelations(rows, withRelations);
2420
2868
  return rows;
@@ -2440,7 +2888,7 @@ var AtscriptDbReadable = class {
2440
2888
  const withRelations = query.controls?.$with;
2441
2889
  const translated = this._fieldMapper.translateQuery(query, this._meta);
2442
2890
  const result = await this.adapter.findManyWithCount(translated);
2443
- const rows = result.data.map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
2891
+ const rows = this._fromRead(result.data, query.controls);
2444
2892
  await this._decryptRows(rows);
2445
2893
  if (withRelations?.length) await this.loadRelations(rows, withRelations);
2446
2894
  return {
@@ -2521,7 +2969,8 @@ var AtscriptDbReadable = class {
2521
2969
  }
2522
2970
  guardAggregate(this._meta, this.adapter, query, buckets);
2523
2971
  const dbQuery = this._fieldMapper.translateAggregateQuery(query, this._meta, buckets);
2524
- return (await this.adapter.aggregate(dbQuery)).map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
2972
+ const results = await this.adapter.aggregate(dbQuery);
2973
+ return this._fromRead(results, query.controls);
2525
2974
  }
2526
2975
  /** Whether the underlying adapter supports text search. */
2527
2976
  isSearchable() {
@@ -2556,7 +3005,8 @@ var AtscriptDbReadable = class {
2556
3005
  this._guardQuery(query);
2557
3006
  const withRelations = query.controls?.$with;
2558
3007
  const translated = this._fieldMapper.translateQuery(query, this._meta);
2559
- const rows = (await this.adapter.search(text, translated, indexName)).map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
3008
+ const results = await this.adapter.search(text, translated, indexName);
3009
+ const rows = this._fromRead(results, query.controls);
2560
3010
  await this._decryptRows(rows);
2561
3011
  if (withRelations?.length) await this.loadRelations(rows, withRelations);
2562
3012
  return rows;
@@ -2571,7 +3021,7 @@ var AtscriptDbReadable = class {
2571
3021
  const withRelations = query.controls?.$with;
2572
3022
  const translated = this._fieldMapper.translateQuery(query, this._meta);
2573
3023
  const result = await this.adapter.searchWithCount(text, translated, indexName);
2574
- const rows = result.data.map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
3024
+ const rows = this._fromRead(result.data, query.controls);
2575
3025
  await this._decryptRows(rows);
2576
3026
  if (withRelations?.length) await this.loadRelations(rows, withRelations);
2577
3027
  return {
@@ -2596,7 +3046,8 @@ var AtscriptDbReadable = class {
2596
3046
  this._guardQuery(query);
2597
3047
  const withRelations = (query?.controls)?.$with;
2598
3048
  const translated = this._fieldMapper.translateQuery(query || {}, this._meta);
2599
- const rows = (await this.adapter.vectorSearch(vector, translated, indexName)).map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
3049
+ const results = await this.adapter.vectorSearch(vector, translated, indexName);
3050
+ const rows = this._fromRead(results, query?.controls);
2600
3051
  await this._decryptRows(rows);
2601
3052
  if (withRelations?.length) await this.loadRelations(rows, withRelations);
2602
3053
  return rows;
@@ -2615,7 +3066,7 @@ var AtscriptDbReadable = class {
2615
3066
  const withRelations = (query?.controls)?.$with;
2616
3067
  const translated = this._fieldMapper.translateQuery(query || {}, this._meta);
2617
3068
  const result = await this.adapter.vectorSearchWithCount(vector, translated, indexName);
2618
- const rows = result.data.map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
3069
+ const rows = this._fromRead(result.data, query?.controls);
2619
3070
  await this._decryptRows(rows);
2620
3071
  if (withRelations?.length) await this.loadRelations(rows, withRelations);
2621
3072
  return {
@@ -2653,7 +3104,8 @@ var AtscriptDbReadable = class {
2653
3104
  async geoSearch(pointOrIndex, maybePointOrQuery, maybeQuery) {
2654
3105
  const { point, query, indexName } = this._resolveGeoSearchArgs(pointOrIndex, maybePointOrQuery, maybeQuery);
2655
3106
  const { translated, withRelations } = this._prepareGeoSearch(point, query, indexName);
2656
- const rows = (await this.adapter.geoSearch(point, translated, indexName)).map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
3107
+ const results = await this.adapter.geoSearch(point, translated, indexName);
3108
+ const rows = this._fromRead(results, query?.controls);
2657
3109
  await this._decryptRows(rows);
2658
3110
  if (withRelations?.length) await this.loadRelations(rows, withRelations);
2659
3111
  return rows;
@@ -2669,7 +3121,7 @@ var AtscriptDbReadable = class {
2669
3121
  const { point, query, indexName } = this._resolveGeoSearchArgs(pointOrIndex, maybePointOrQuery, maybeQuery);
2670
3122
  const { translated, withRelations } = this._prepareGeoSearch(point, query, indexName);
2671
3123
  const result = await this.adapter.geoSearchWithCount(point, translated, indexName);
2672
- const rows = result.data.map((row) => this._fieldMapper.reconstructFromRead(row, this._meta));
3124
+ const rows = this._fromRead(result.data, query?.controls);
2673
3125
  await this._decryptRows(rows);
2674
3126
  if (withRelations?.length) await this.loadRelations(rows, withRelations);
2675
3127
  return {
@@ -2829,7 +3281,7 @@ var AtscriptDbReadable = class {
2829
3281
  * Public entry point for relation loading. Used by adapters for nested $with delegation.
2830
3282
  */
2831
3283
  async loadRelations(rows, withRelations) {
2832
- const { loadRelationsImpl } = await Promise.resolve().then(() => require("./relation-loader-6ZB_5KFq.cjs")).then((n) => n.relation_loader_exports);
3284
+ const { loadRelationsImpl } = await Promise.resolve().then(() => require("./relation-loader-CgJ8bK6X.cjs")).then((n) => n.relation_loader_exports);
2833
3285
  return loadRelationsImpl(rows, withRelations, this);
2834
3286
  }
2835
3287
  /**
@@ -4306,6 +4758,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
4306
4758
  for (const navField of this._meta.navFields) delete data[navField];
4307
4759
  const filter = this._extractRecordFilter(data, opts);
4308
4760
  for (const key of Object.keys(filter)) delete data[key];
4761
+ this._meta.stripDerived(data);
4309
4762
  if (versionColumn !== void 0) assertNoVersionWrites(data, versionColumn);
4310
4763
  const translatedFilter = this._fieldMapper.translateFilter(filter, this._meta);
4311
4764
  if (require_validator.isEmptyObject(data) && expectedVersion === void 0) {
@@ -4455,6 +4908,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
4455
4908
  this._guardMutationFilter(filter);
4456
4909
  await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver, true);
4457
4910
  const dataCopy = _cloneWritePayload(data);
4911
+ this._meta.stripDerived(dataCopy);
4458
4912
  const versionColumn = this.versionColumn;
4459
4913
  if ("$cas" in dataCopy) throw new require_db_error.DbError("INVALID_QUERY", [{
4460
4914
  path: "$cas",
@@ -4738,120 +5192,6 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
4738
5192
  }
4739
5193
  };
4740
5194
  //#endregion
4741
- //#region src/table/view-source.ts
4742
- const indexCache = /* @__PURE__ */ new WeakMap();
4743
- function sourceIndex(type) {
4744
- let idx = indexCache.get(type);
4745
- if (idx) return idx;
4746
- idx = {
4747
- flatMap: /* @__PURE__ */ new Map(),
4748
- columnMap: /* @__PURE__ */ new Map(),
4749
- documentColumnMap: /* @__PURE__ */ new Map(),
4750
- unstored: /* @__PURE__ */ new Set(),
4751
- jsonRoots: /* @__PURE__ */ new Set(),
4752
- encrypted: /* @__PURE__ */ new Set(),
4753
- optional: /* @__PURE__ */ new Set()
4754
- };
4755
- if (type.type.kind === "object") {
4756
- const navFields = /* @__PURE__ */ new Set();
4757
- const collected = [];
4758
- idx.flatMap = (0, _atscript_typescript_utils.flattenAnnotatedType)(type, {
4759
- excludePhantomTypes: true,
4760
- onField: (path, fieldType, metadata) => {
4761
- if (isNavRelation(metadata)) navFields.add(path);
4762
- collected.push([
4763
- path,
4764
- fieldType,
4765
- metadata
4766
- ]);
4767
- }
4768
- });
4769
- const storage = [];
4770
- for (const [path, fieldType, metadata] of collected) {
4771
- if (!path || findAncestorInSet(path, navFields) !== void 0) continue;
4772
- const column = metadata.get("db.column");
4773
- if (column) {
4774
- idx.columnMap.set(path, column);
4775
- if (columnOverrideApplies(path, true)) idx.documentColumnMap.set(path, column);
4776
- }
4777
- if (metadata.has("db.ignore") || navFields.has(path)) idx.unstored.add(path);
4778
- if (metadata.has("db.encrypted")) storage.push([path, false]);
4779
- else if (metadata.has("db.json") || resolveDesignType(fieldType) === "array") storage.push([path, true]);
4780
- }
4781
- storage.sort(([a], [b]) => a.split(".").length - b.split(".").length);
4782
- for (const [path, json] of storage) if (findAncestorInSet(path, idx.jsonRoots) === void 0 && findAncestorInSet(path, idx.encrypted) === void 0) (json ? idx.jsonRoots : idx.encrypted).add(path);
4783
- for (const [path, node] of idx.flatMap) if (path && node.optional) idx.optional.add(path);
4784
- }
4785
- indexCache.set(type, idx);
4786
- return idx;
4787
- }
4788
- /** `path` itself if it is in `set`, else its nearest ancestor in `set`. */
4789
- function selfOrAncestor(path, set) {
4790
- return set.has(path) ? path : findAncestorInSet(path, set);
4791
- }
4792
- /**
4793
- * Resolves a LOGICAL path of a source table (a view field's chain ref, an
4794
- * aggregate's field, a predicate operand) to where it is physically stored.
4795
- * Internal — `AtscriptDbView.resolveRefSource` is the public entry.
4796
- *
4797
- * Relational rules (`TableMetadata`'s): the outermost `@db.json` or array
4798
- * node with segments remaining is the column and the rest becomes
4799
- * {@link TViewSource.jsonPath}; a flattened leaf is its parent segments
4800
- * joined with `__` plus its `@db.column` (or segment); a top-level field is
4801
- * its `@db.column` or name. Nested-object adapters use the document path
4802
- * (only a top-level key is renamed) and never a JSON path.
4803
- *
4804
- * A path the type does not declare resolves to itself — the database
4805
- * reports the unknown column, as before.
4806
- * @throws for a path without storage (`@db.ignore`, a navigation relation,
4807
- * or inside one), and — relational — for a path inside an `@db.encrypted` field.
4808
- */
4809
- function resolveViewSource(sourceType, logicalPath, nestedObjects) {
4810
- const idx = sourceIndex(sourceType);
4811
- const node = idx.flatMap.get(logicalPath);
4812
- const jsonRoot = selfOrAncestor(logicalPath, idx.jsonRoots);
4813
- const optional = selfOrAncestor(logicalPath, idx.optional) !== void 0 || jsonRoot !== void 0 && jsonRoot !== logicalPath;
4814
- if (!node) return {
4815
- column: logicalPath,
4816
- designType: "unknown",
4817
- optional
4818
- };
4819
- const unstored = selfOrAncestor(logicalPath, idx.unstored);
4820
- if (unstored !== void 0) throw new Error(`"${logicalPath}" has no column — "${unstored}" is @db.ignore or a navigation relation`);
4821
- const designType = resolveDesignType(node);
4822
- if (nestedObjects) return {
4823
- column: documentPath(idx.documentColumnMap, logicalPath),
4824
- designType,
4825
- optional
4826
- };
4827
- const encrypted = findAncestorInSet(logicalPath, idx.encrypted);
4828
- if (encrypted !== void 0) throw new Error(`"${logicalPath}" is inside the @db.encrypted field "${encrypted}" — reference "${encrypted}" itself`);
4829
- if (jsonRoot !== void 0) {
4830
- const column = relationalColumnName(jsonRoot, idx.columnMap.get(jsonRoot), jsonRoot.includes("."));
4831
- return jsonRoot === logicalPath ? {
4832
- column,
4833
- designType,
4834
- optional
4835
- } : {
4836
- column,
4837
- jsonPath: logicalPath.slice(jsonRoot.length + 1).split("."),
4838
- designType,
4839
- optional
4840
- };
4841
- }
4842
- if (designType === "object" && !idx.encrypted.has(logicalPath)) return {
4843
- column: logicalPath.replace(/\./g, "__"),
4844
- designType,
4845
- flattened: true,
4846
- optional
4847
- };
4848
- return {
4849
- column: relationalColumnName(logicalPath, idx.columnMap.get(logicalPath), logicalPath.includes(".")),
4850
- designType,
4851
- optional
4852
- };
4853
- }
4854
- //#endregion
4855
5195
  //#region src/table/db-view.ts
4856
5196
  /** The `@db.agg.*` annotations, one per supported aggregate function. */
4857
5197
  const AGG_KEYS = require_aggregate_fns.SUPPORTED_AGGREGATE_FNS.map((fn) => `db.agg.${fn}`);
@@ -4881,11 +5221,6 @@ function readViewAgg(metadata) {
4881
5221
  };
4882
5222
  }
4883
5223
  }
4884
- const JSON_LEAF_TYPES = new Set([
4885
- "string",
4886
- "number",
4887
- "boolean"
4888
- ]);
4889
5224
  /**
4890
5225
  * Database view abstraction driven by Atscript `@db.view.*` annotations.
4891
5226
  *
@@ -4927,16 +5262,19 @@ var AtscriptDbView = class extends AtscriptDbReadable {
4927
5262
  const metadata = this._type.metadata;
4928
5263
  const forRef = metadata.get("db.view.for");
4929
5264
  const entryType = typeof forRef === "function" ? forRef : forRef.type;
4930
- const entryTable = require_nested_writer.tableNameOf(entryType());
5265
+ const entry = viewSourceOf(entryType());
5266
+ if (entry.alias) throw new Error(`View "${this.tableName}": @db.view.for "${entry.name}" is a @db.alias — a join alias cannot be the entry table`);
5267
+ const entryTable = entry.table;
4931
5268
  const rawJoins = metadata.get("db.view.joins");
4932
5269
  const joins = [];
4933
5270
  if (rawJoins) for (const join of rawJoins) {
4934
5271
  const targetRef = join.target;
4935
5272
  const targetType = typeof targetRef === "function" ? targetRef : targetRef.type;
4936
- const targetTypeResolved = targetType();
5273
+ const target = viewSourceOf(targetType());
4937
5274
  joins.push({
4938
5275
  targetType,
4939
- targetTable: require_nested_writer.tableNameOf(targetTypeResolved),
5276
+ targetTable: target.table,
5277
+ scope: target.name,
4940
5278
  condition: join.condition,
4941
5279
  kind: join.kind === "left" ? "left" : "inner"
4942
5280
  });
@@ -4960,19 +5298,22 @@ var AtscriptDbView = class extends AtscriptDbReadable {
4960
5298
  }
4961
5299
  /**
4962
5300
  * Resolves a view query field ref (join condition, `@db.view.filter`,
4963
- * conditional-aggregate predicate) to its table name and PHYSICAL source on
5301
+ * conditional-aggregate predicate) to its scope name and PHYSICAL source on
4964
5302
  * this view's adapter — the column (or document path) with `TableMetadata`'s
4965
5303
  * layout rules, the path inside a JSON column, and whether the value may be
4966
- * absent. An unqualified ref resolves against the entry table.
5304
+ * absent. An unqualified ref resolves against the entry table. `table` is
5305
+ * the physical table / view name, or the alias name for a `@db.alias`
5306
+ * target (since 0.1.141); a source that is itself a managed view resolves
5307
+ * to that view's own columns.
4967
5308
  * @throws for a ref without storage (`@db.ignore`, navigation relation) or
4968
5309
  * inside an `@db.encrypted` field (relational adapters).
4969
5310
  * @since 0.1.136
4970
5311
  */
4971
5312
  resolveRefSource(ref) {
4972
- const type = ref.type ? ref.type() : this.viewPlan.entryType();
5313
+ const src = viewSourceOf(ref.type ? ref.type() : this.viewPlan.entryType());
4973
5314
  return {
4974
- table: require_nested_writer.tableNameOf(type),
4975
- source: resolveViewSource(type, ref.field, this._nested)
5315
+ table: src.name,
5316
+ source: resolveViewSource(src.type, ref.field, this._nested)
4976
5317
  };
4977
5318
  }
4978
5319
  /**
@@ -5024,7 +5365,7 @@ var AtscriptDbView = class extends AtscriptDbReadable {
5024
5365
  const fail = (field, message) => {
5025
5366
  throw new Error(`View "${this.tableName}" field "${field}": ${message}`);
5026
5367
  };
5027
- const leftJoined = new Set(plan.joins.filter((j) => j.kind === "left").map((j) => j.targetTable));
5368
+ const leftJoined = new Set(plan.joins.filter((j) => j.kind === "left").map((j) => j.scope));
5028
5369
  for (const [fieldName, fieldType] of this._type.type.props.entries()) {
5029
5370
  if (ignored.has(fieldName)) continue;
5030
5371
  const agg = readViewAgg(fieldType.metadata);
@@ -5032,16 +5373,18 @@ var AtscriptDbView = class extends AtscriptDbReadable {
5032
5373
  if (aggField === "*" && agg?.aggFn !== "count") fail(fieldName, `aggregate "${agg?.aggFn}" needs a field — only count accepts *`);
5033
5374
  let sourceType;
5034
5375
  let sourcePath;
5035
- if (fieldType.ref) {
5036
- sourceType = fieldType.ref.type();
5037
- sourcePath = fieldType.ref.field || fieldName;
5376
+ const chainRef = fieldType.ref?.field ? fieldType.ref : void 0;
5377
+ if (chainRef) {
5378
+ sourceType = chainRef.type();
5379
+ sourcePath = chainRef.field;
5038
5380
  } else {
5039
5381
  sourceType = plan.entryType();
5040
5382
  sourcePath = aggField && aggField !== "*" ? aggField : fieldName;
5041
5383
  }
5042
- const sourceTable = require_nested_writer.tableNameOf(sourceType);
5384
+ const src = viewSourceOf(sourceType);
5385
+ const sourceTable = src.name;
5043
5386
  const joinNullable = leftJoined.has(sourceTable);
5044
- if (aggField === "*" && !fieldType.ref) {
5387
+ if (aggField === "*" && !chainRef) {
5045
5388
  mappings.push({
5046
5389
  viewColumn: viewName(fieldName) ?? fieldName,
5047
5390
  viewPath: fieldName,
@@ -5051,7 +5394,7 @@ var AtscriptDbView = class extends AtscriptDbReadable {
5051
5394
  });
5052
5395
  continue;
5053
5396
  }
5054
- const source = resolveViewSource(sourceType, sourcePath, nested);
5397
+ const source = resolveViewSource(src.type, sourcePath, nested);
5055
5398
  const ownColumn = viewName(fieldName);
5056
5399
  if (ownColumn === void 0 && !nested) {
5057
5400
  if (!source.flattened) fail(fieldName, "source is a JSON column — add @db.json to the view field");
@@ -5059,7 +5402,7 @@ var AtscriptDbView = class extends AtscriptDbReadable {
5059
5402
  for (const [viewPath, viewColumn] of meta.pathToPhysical) {
5060
5403
  if (!viewPath.startsWith(prefix) || ignored.has(viewPath)) continue;
5061
5404
  const leafPath = `${sourcePath}.${viewPath.slice(prefix.length)}`;
5062
- const leaf = resolveViewSource(sourceType, leafPath, nested);
5405
+ const leaf = resolveViewSource(src.type, leafPath, nested);
5063
5406
  mappings.push(this._leafMapping(viewPath, viewColumn, sourceTable, leaf, joinNullable));
5064
5407
  }
5065
5408
  continue;
@@ -5083,7 +5426,7 @@ var AtscriptDbView = class extends AtscriptDbReadable {
5083
5426
  };
5084
5427
  if (joinNullable || source.optional || source.designType === "unknown") mapping.nullable = true;
5085
5428
  if (source.jsonPath) {
5086
- if (!JSON_LEAF_TYPES.has(source.designType)) throw new Error(`View "${this.tableName}" field "${viewPath}": JSON extraction supports string, number and boolean leaves only`);
5429
+ if (!require_derived_rules.isJsonLeafType(source.designType)) throw new Error(`View "${this.tableName}" field "${viewPath}": JSON extraction supports string, number and boolean leaves only`);
5087
5430
  mapping.json = {
5088
5431
  path: source.jsonPath,
5089
5432
  type: source.designType
@@ -5106,9 +5449,33 @@ function isAtscriptDbView(readable) {
5106
5449
  }
5107
5450
  //#endregion
5108
5451
  //#region src/schema/schema-hash.ts
5109
- /** Extracts sorted field snapshots from a readable's field descriptors. */
5110
- function extractFieldSnapshots(fields, typeMapper) {
5111
- return fields.filter((f) => !f.ignored).map((f) => {
5452
+ /**
5453
+ * The physical sources a stored view snapshot reads: its entry table and the
5454
+ * physical table of every join (an aliased join's `table`). External views
5455
+ * (no plan) yield `[]`.
5456
+ * @since 0.1.141
5457
+ */
5458
+ function viewSnapshotSources(snapshot) {
5459
+ const sources = (snapshot.joinTables ?? []).map((j) => j.table ?? j.targetTable);
5460
+ return snapshot.entryTable ? [snapshot.entryTable, ...sources] : sources;
5461
+ }
5462
+ /**
5463
+ * The descriptors a snapshot lists: the non-ignored ones — plus, on a
5464
+ * nested-object adapter, the subfields of navigation properties. Those are
5465
+ * `ignored` since 0.1.141 (never columns: out of the plan, the column diff
5466
+ * and every write), but earlier releases wrote them into a document
5467
+ * adapter's snapshot; they stay listed so the upgrade leaves every stored
5468
+ * hash unchanged. {@link snapshotToExistingColumns} (given the readable)
5469
+ * leaves them out when a stored snapshot stands in for the live columns.
5470
+ */
5471
+ function snapshotFields(readable) {
5472
+ const nav = readable.navFields;
5473
+ if (!nav?.size || !readable.dbAdapter.supportsNestedObjects()) return readable.fieldDescriptors.filter((f) => !f.ignored);
5474
+ return readable.fieldDescriptors.filter((f) => !f.ignored || require_validator.findAncestorInSet(f.path, nav) !== void 0);
5475
+ }
5476
+ /** Extracts sorted field snapshots from a readable's {@link snapshotFields}. */
5477
+ function extractFieldSnapshots(readable, typeMapper) {
5478
+ return snapshotFields(readable).map((f) => {
5112
5479
  const snap = {
5113
5480
  physicalName: f.physicalName,
5114
5481
  designType: f.designType,
@@ -5119,6 +5486,11 @@ function extractFieldSnapshots(fields, typeMapper) {
5119
5486
  if (f.defaultValue) snap.defaultValue = f.defaultValue;
5120
5487
  if (typeMapper) snap.mappedType = typeMapper(f);
5121
5488
  if (f.encrypted) snap.encrypted = true;
5489
+ if (f.derived) snap.derived = {
5490
+ sourceColumn: f.derived.sourceColumn,
5491
+ jsonPath: [...f.derived.jsonPath],
5492
+ type: f.derived.type
5493
+ };
5122
5494
  return snap;
5123
5495
  }).toSorted((a, b) => a.physicalName.localeCompare(b.physicalName));
5124
5496
  }
@@ -5132,7 +5504,7 @@ function extractFieldSnapshots(fields, typeMapper) {
5132
5504
  * for precise type change detection.
5133
5505
  */
5134
5506
  function computeTableSnapshot(readable, typeMapper, tableOptions) {
5135
- const fields = extractFieldSnapshots(readable.fieldDescriptors, typeMapper);
5507
+ const fields = extractFieldSnapshots(readable, typeMapper);
5136
5508
  const indexes = [...readable.indexes.values()].map((idx) => ({
5137
5509
  key: idx.key,
5138
5510
  type: idx.type,
@@ -5163,7 +5535,7 @@ function computeTableSnapshot(readable, typeMapper, tableOptions) {
5163
5535
  * detecting view definition changes.
5164
5536
  */
5165
5537
  function computeViewSnapshot(view) {
5166
- const fields = extractFieldSnapshots(view.fieldDescriptors);
5538
+ const fields = extractFieldSnapshots(view);
5167
5539
  if (view.isExternal) return {
5168
5540
  tableName: view.tableName,
5169
5541
  viewType: "E",
@@ -5193,7 +5565,8 @@ function computeViewSnapshot(view) {
5193
5565
  entryTable: plan.entryTable,
5194
5566
  joinTables: plan.joins.map((j) => {
5195
5567
  const join = {
5196
- targetTable: j.targetTable,
5568
+ targetTable: j.scope,
5569
+ ...j.scope !== j.targetTable ? { table: j.targetTable } : {},
5197
5570
  condition: canonical(j.condition)
5198
5571
  };
5199
5572
  if (j.kind === "left") join.kind = "left";
@@ -5255,10 +5628,19 @@ function computeTableHash(snapshot) {
5255
5628
  * native column introspection (e.g., MongoDB).
5256
5629
  *
5257
5630
  * The `type` field uses `mappedType` when available (adapter-specific),
5258
- * falling back to `designType`.
5631
+ * falling back to `designType`. Derived fields are left out (since 0.1.141):
5632
+ * an adapter without column introspection stores no derived column, and a
5633
+ * `physicalName` of one is its SOURCE path — reporting it as an existing
5634
+ * column would have the diff drop the source leaf.
5635
+ *
5636
+ * With `readable`, the subfields of its navigation properties are left out
5637
+ * too: a document adapter's snapshot lists them (for hash stability — see
5638
+ * `snapshotFields`) although nothing is stored under a nav field — they are
5639
+ * not columns to drop.
5259
5640
  */
5260
- function snapshotToExistingColumns(snapshot) {
5261
- return snapshot.fields.map((f) => ({
5641
+ function snapshotToExistingColumns(snapshot, readable) {
5642
+ const navPrefixes = readable && readable.navFields.size > 0 ? readable.fieldDescriptors.filter((fd) => readable.navFields.has(fd.path)).map((fd) => `${fd.physicalName}.`) : [];
5643
+ return snapshot.fields.filter((f) => !f.derived && !navPrefixes.some((p) => f.physicalName.startsWith(p))).map((f) => ({
5262
5644
  name: f.physicalName,
5263
5645
  type: f.mappedType ?? f.designType,
5264
5646
  notnull: !f.optional,
@@ -5337,6 +5719,18 @@ function fkPropertiesDiffer(desired, existing) {
5337
5719
  //#endregion
5338
5720
  //#region src/schema/column-diff.ts
5339
5721
  /**
5722
+ * Why a derived column (desired, live, or both) must be dropped and re-added,
5723
+ * or `undefined` when it is unchanged. Nullability and defaults are never
5724
+ * compared for one: a generated column is nullable and has no DEFAULT.
5725
+ */
5726
+ function derivedChangeReason(field, existingCol, typeMapper, snap) {
5727
+ if (!field.derived) return existingCol.generated ? "kind" : void 0;
5728
+ if (!existingCol.generated) return "kind";
5729
+ const stored = snap?.derived;
5730
+ if (stored && (stored.sourceColumn !== field.derived.sourceColumn || stored.type !== field.derived.type || stored.jsonPath.length !== field.derived.jsonPath.length || stored.jsonPath.some((seg, i) => seg !== field.derived.jsonPath[i]))) return "expression";
5731
+ if (typeMapper && isColumnTypeChanged(existingCol.type, typeMapper(field))) return "type";
5732
+ }
5733
+ /**
5340
5734
  * Whether a live column's type differs from the type the adapter's
5341
5735
  * `typeMapper` gives the field — the one rule schema sync diffs column types
5342
5736
  * by (case-insensitive). Exported for adapters that must agree with it (the
@@ -5349,15 +5743,22 @@ function isColumnTypeChanged(existingType, expectedType) {
5349
5743
  /**
5350
5744
  * Computes the difference between desired schema fields and existing database columns.
5351
5745
  *
5352
- * @param desired - Field descriptors from the Atscript type (after flattening).
5746
+ * @param desired - Field descriptors from the Atscript type (after flattening) —
5747
+ * the readable's `columnDescriptors`; ignored descriptors are skipped.
5353
5748
  * @param existing - Columns currently in the database (from introspection).
5354
5749
  * @param typeMapper - Optional function to map field metadata to DB-native type strings.
5355
5750
  * Receives the full field meta (design type, annotations, PK status, etc.)
5356
5751
  * so adapters can produce context-aware types (e.g., `VARCHAR(255)` from maxLength).
5357
5752
  * Required for type change detection.
5753
+ * @param opts.snapshot - The table's stored snapshot (since 0.1.141) — the baseline a
5754
+ * derived column's expression is compared with (the engine normalizes
5755
+ * the expression it stores, so the live column cannot be). Without
5756
+ * one, only a kind or type drift is seen.
5358
5757
  */
5359
- function computeColumnDiff(desired, existing, typeMapper) {
5758
+ function computeColumnDiff(desired, existing, typeMapper, opts) {
5360
5759
  const existingByName = new Map(existing.map((c) => [c.name, c]));
5760
+ const snapshotByName = opts?.snapshot ? new Map(opts.snapshot.fields.map((f) => [f.physicalName, f])) : void 0;
5761
+ const derivedChanged = [];
5361
5762
  const desiredByName = /* @__PURE__ */ new Map();
5362
5763
  const renamedOldNames = /* @__PURE__ */ new Set();
5363
5764
  const added = [];
@@ -5366,44 +5767,60 @@ function computeColumnDiff(desired, existing, typeMapper) {
5366
5767
  const nullableChanged = [];
5367
5768
  const defaultChanged = [];
5368
5769
  const conflicts = [];
5770
+ /**
5771
+ * A derived column (on either side) is compared as a whole — any drift is a
5772
+ * drop + add, and nullability / defaults are not the model's to manage.
5773
+ * `true` when the pair was a derived one (handled here).
5774
+ */
5775
+ const checkDerived = (field, col, snapKey) => {
5776
+ if (!field.derived && !col.generated) return false;
5777
+ const reason = derivedChangeReason(field, col, typeMapper, snapshotByName?.get(snapKey));
5778
+ if (reason) derivedChanged.push({
5779
+ field,
5780
+ reason
5781
+ });
5782
+ return true;
5783
+ };
5369
5784
  for (const field of desired) {
5370
5785
  if (field.ignored) continue;
5371
5786
  desiredByName.set(field.physicalName, field);
5372
5787
  const existingCol = existingByName.get(field.physicalName);
5373
- if (existingCol) if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
5374
- conflicts.push({
5375
- field,
5376
- oldName: field.renamedFrom,
5377
- conflictsWith: field.physicalName
5378
- });
5379
- renamedOldNames.add(field.renamedFrom);
5380
- } else {
5381
- if (typeMapper) {
5382
- if (isColumnTypeChanged(existingCol.type, typeMapper(field))) typeChanged.push({
5788
+ if (existingCol) {
5789
+ if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
5790
+ conflicts.push({
5383
5791
  field,
5384
- existingType: existingCol.type
5792
+ oldName: field.renamedFrom,
5793
+ conflictsWith: field.physicalName
5385
5794
  });
5386
- }
5387
- if (!field.isPrimaryKey && !existingCol.pk) {
5388
- const desiredNotNull = !field.optional;
5389
- if (existingCol.notnull !== desiredNotNull) nullableChanged.push({
5795
+ renamedOldNames.add(field.renamedFrom);
5796
+ } else if (!checkDerived(field, existingCol, field.physicalName)) {
5797
+ if (typeMapper) {
5798
+ if (isColumnTypeChanged(existingCol.type, typeMapper(field))) typeChanged.push({
5799
+ field,
5800
+ existingType: existingCol.type
5801
+ });
5802
+ }
5803
+ if (!field.isPrimaryKey && !existingCol.pk) {
5804
+ const desiredNotNull = !field.optional;
5805
+ if (existingCol.notnull !== desiredNotNull) nullableChanged.push({
5806
+ field,
5807
+ wasNullable: !existingCol.notnull
5808
+ });
5809
+ }
5810
+ const desiredDefault = serializeDefaultValue(field.defaultValue);
5811
+ if (existingCol.dflt_value !== void 0 && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
5390
5812
  field,
5391
- wasNullable: !existingCol.notnull
5813
+ oldDefault: existingCol.dflt_value,
5814
+ newDefault: desiredDefault
5392
5815
  });
5393
5816
  }
5394
- const desiredDefault = serializeDefaultValue(field.defaultValue);
5395
- if (existingCol.dflt_value !== void 0 && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
5396
- field,
5397
- oldDefault: existingCol.dflt_value,
5398
- newDefault: desiredDefault
5399
- });
5400
- }
5401
- else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
5817
+ } else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
5402
5818
  renamed.push({
5403
5819
  field,
5404
5820
  oldName: field.renamedFrom
5405
5821
  });
5406
5822
  renamedOldNames.add(field.renamedFrom);
5823
+ checkDerived(field, existingByName.get(field.renamedFrom), field.renamedFrom);
5407
5824
  } else added.push(field);
5408
5825
  }
5409
5826
  const diff = {
@@ -5415,6 +5832,7 @@ function computeColumnDiff(desired, existing, typeMapper) {
5415
5832
  defaultChanged,
5416
5833
  conflicts
5417
5834
  };
5835
+ if (derivedChanged.length > 0) diff.derivedChanged = derivedChanged;
5418
5836
  if (existing.length > 0) {
5419
5837
  const newNameByOld = new Map(renamed.map((r) => [r.oldName, r.field.physicalName]));
5420
5838
  const from = existing.filter((c) => c.pk).map((c) => newNameByOld.get(c.name) ?? c.name);
@@ -5529,6 +5947,12 @@ Object.defineProperty(exports, "acceptedOperatorsHint", {
5529
5947
  return acceptedOperatorsHint;
5530
5948
  }
5531
5949
  });
5950
+ Object.defineProperty(exports, "aliasTargetOf", {
5951
+ enumerable: true,
5952
+ get: function() {
5953
+ return aliasTargetOf;
5954
+ }
5955
+ });
5532
5956
  Object.defineProperty(exports, "assertGeoPoint", {
5533
5957
  enumerable: true,
5534
5958
  get: function() {
@@ -5625,12 +6049,6 @@ Object.defineProperty(exports, "decomposePatch", {
5625
6049
  return decomposePatch;
5626
6050
  }
5627
6051
  });
5628
- Object.defineProperty(exports, "findAncestorInSet", {
5629
- enumerable: true,
5630
- get: function() {
5631
- return findAncestorInSet;
5632
- }
5633
- });
5634
6052
  Object.defineProperty(exports, "fkKey", {
5635
6053
  enumerable: true,
5636
6054
  get: function() {
@@ -5763,3 +6181,9 @@ Object.defineProperty(exports, "unsupportedOperatorMessage", {
5763
6181
  return unsupportedOperatorMessage;
5764
6182
  }
5765
6183
  });
6184
+ Object.defineProperty(exports, "viewSnapshotSources", {
6185
+ enumerable: true,
6186
+ get: function() {
6187
+ return viewSnapshotSources;
6188
+ }
6189
+ });