@atscript/db 0.1.136 → 0.1.137

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.
@@ -119,18 +119,24 @@ function findAncestorInSet(path, set) {
119
119
  }
120
120
  }
121
121
  /**
122
+ * Whether a `@db.column` / `@db.column.renamed` on `path` applies: always on
123
+ * relational storage; on document storage (`nestedObjects`) for top-level
124
+ * fields only — nested keys are stored as-is. Column maps are built with it.
125
+ */
126
+ function columnOverrideApplies(path, nestedObjects) {
127
+ return !nestedObjects || !path.includes(".");
128
+ }
129
+ /**
122
130
  * Logical field path → its physical path in document storage (nested
123
- * objects kept inline). `@db.column` renames (`columnMap`) apply to the
124
- * annotated key, and a document renames the TOP-LEVEL key only — nested keys
125
- * are stored as-is — so a dotted path under a renamed top-level object
126
- * renames its first segment: `profile.bio` under `@db.column 'prof'` →
127
- * `prof.bio`.
131
+ * objects kept inline). A document renames the TOP-LEVEL key only — nested
132
+ * keys are stored as-is, so a `@db.column` on a nested leaf renames nothing
133
+ * (`address.zip` stays `address.zip`; see {@link columnOverrideApplies}) —
134
+ * and a dotted path under a renamed top-level object renames its first
135
+ * segment: `profile.bio` under `@db.column 'prof'` → `prof.bio`.
128
136
  */
129
137
  function documentPath(columnMap, path) {
130
- const direct = columnMap.get(path);
131
- if (direct !== void 0) return direct;
132
138
  const dot = path.indexOf(".");
133
- if (dot === -1) return path;
139
+ if (dot === -1) return columnMap.get(path) ?? path;
134
140
  const top = columnMap.get(path.slice(0, dot));
135
141
  return top === void 0 ? path : top + path.slice(dot);
136
142
  }
@@ -196,6 +202,7 @@ var TableMetadata = class {
196
202
  ignoredFields = /* @__PURE__ */ new Set();
197
203
  uniqueProps = /* @__PURE__ */ new Set();
198
204
  defaults = /* @__PURE__ */ new Map();
205
+ /** Logical path → `@db.column` override (top-level keys only on document storage). */
199
206
  columnMap = /* @__PURE__ */ new Map();
200
207
  dimensions = [];
201
208
  measures = [];
@@ -261,17 +268,19 @@ var TableMetadata = class {
261
268
  get isBuilt() {
262
269
  return this._built;
263
270
  }
264
- /**
265
- * Logical field path → its physical path in document storage (nested
266
- * objects kept inline). `@db.column` renames apply to the annotated key,
267
- * and a document renames the TOP-LEVEL key only — nested keys are stored
268
- * as-is — so a dotted path under a renamed top-level object renames its
269
- * first segment: `profile.bio` under `@db.column 'prof'` → `prof.bio`.
270
- */
271
+ /** {@link documentPath} over this table's `columnMap`. */
271
272
  documentPath(path) {
272
273
  return documentPath(this.columnMap, path);
273
274
  }
274
275
  /**
276
+ * Physical name of a logical path: the document path on nested-object
277
+ * adapters, else the relational column (`pathToPhysical`, then the
278
+ * `@db.column` override).
279
+ */
280
+ physicalPath(logical) {
281
+ return this.nestedObjects ? this.documentPath(logical) : this.pathToPhysical.get(logical) ?? this.columnMap.get(logical) ?? logical;
282
+ }
283
+ /**
275
284
  * Runs the full metadata compilation pipeline. Called once by
276
285
  * `AtscriptDbReadable._ensureBuilt()` on first metadata access.
277
286
  *
@@ -359,10 +368,11 @@ var TableMetadata = class {
359
368
  this.primaryKeys.push(fieldName);
360
369
  this.originalMetaIdFields.push(fieldName);
361
370
  }
371
+ const renamable = columnOverrideApplies(fieldName, this.nestedObjects);
362
372
  const column = metadata.get("db.column");
363
- if (column) this.columnMap.set(fieldName, column);
373
+ if (column && renamable) this.columnMap.set(fieldName, column);
364
374
  const columnFrom = metadata.get("db.column.renamed");
365
- if (columnFrom) this._columnFromMap.set(fieldName, columnFrom);
375
+ if (columnFrom && renamable) this._columnFromMap.set(fieldName, columnFrom);
366
376
  const resolvedDefault = resolveDefaultFromMetadata(metadata);
367
377
  if (resolvedDefault) this.defaults.set(fieldName, resolvedDefault);
368
378
  if (metadata.has("db.ignore")) this.ignoredFields.add(fieldName);
@@ -620,7 +630,7 @@ var TableMetadata = class {
620
630
  else if (isJson) storage = "json";
621
631
  else if (isFlattened) storage = "flattened";
622
632
  else storage = "column";
623
- const physicalName = skipFlattening ? this.columnMap.get(path) ?? path : this.pathToPhysical.get(path) ?? this.columnMap.get(path) ?? path;
633
+ const physicalName = this.physicalPath(path);
624
634
  const fromLocal = this._columnFromMap.get(path);
625
635
  let renamedFrom;
626
636
  if (fromLocal) renamedFrom = isFlattened ? flattenedPrefix(path) + fromLocal : fromLocal;
@@ -724,7 +734,7 @@ var TableMetadata = class {
724
734
  field.optional = ftype.optional === true;
725
735
  field.designType = ftype.type.tags?.has("objectId") ? "objectId" : resolveDesignType(ftype);
726
736
  }
727
- field.name = this.pathToPhysical.get(field.name) ?? this.columnMap.get(field.name) ?? field.name;
737
+ field.name = this.physicalPath(field.name);
728
738
  }
729
739
  }
730
740
  /**
@@ -1023,11 +1033,24 @@ var FieldMappingStrategy = class {
1023
1033
  else if (key === "$not") result[key] = this.translateFilter(value, meta);
1024
1034
  else if (key.startsWith("$")) result[key] = value;
1025
1035
  else {
1026
- const formatKey = meta.columnMap.get(key) ?? key;
1027
- result[meta.documentPath(key)] = this.formatFilterValue(formatKey, value, meta);
1036
+ const physical = this.physicalPath(key, meta);
1037
+ result[physical] = this.formatFilterValue(physical, value, meta);
1028
1038
  }
1029
1039
  return result;
1030
1040
  }
1041
+ /** `$inc` / `$mul` field-op keys to physical names ({@link physicalPath}). */
1042
+ translateOpsKeys(ops, meta) {
1043
+ if (!this.renamesPaths(meta)) return ops;
1044
+ const physical = (rec) => {
1045
+ const out = {};
1046
+ for (const key in rec) out[this.physicalPath(key, meta)] = rec[key];
1047
+ return out;
1048
+ };
1049
+ return {
1050
+ inc: ops.inc && physical(ops.inc),
1051
+ mul: ops.mul && physical(ops.mul)
1052
+ };
1053
+ }
1031
1054
  /**
1032
1055
  * Reverse-maps `@db.column` renames on a row read from storage.
1033
1056
  * Renames physical keys back to logical names in-place.
@@ -1187,10 +1210,11 @@ var DocumentFieldMapper = class extends FieldMappingStrategy {
1187
1210
  }
1188
1211
  return this.formatWriteValues(data, meta);
1189
1212
  }
1213
+ /** Patch keys (top-level or decomposed dotted) to document paths. */
1190
1214
  translatePatchKeys(update, meta) {
1191
- if (meta.columnMap.size > 0) {
1215
+ if (this.renamesPaths(meta)) {
1192
1216
  const result = {};
1193
- for (const key of Object.keys(update)) result[meta.columnMap.get(key) ?? key] = update[key];
1217
+ for (const key of Object.keys(update)) result[this.physicalPath(key, meta)] = update[key];
1194
1218
  return this.formatWriteValues(result, meta);
1195
1219
  }
1196
1220
  return this.formatWriteValues(update, meta);
@@ -2239,7 +2263,7 @@ var AtscriptDbReadable = class {
2239
2263
  get syncMethod() {
2240
2264
  return this._syncMethod;
2241
2265
  }
2242
- /** Logical → physical column name mapping from `@db.column`. */
2266
+ /** Logical → physical column name mapping from `@db.column` (top-level only on document storage). */
2243
2267
  get columnMap() {
2244
2268
  this._ensureBuilt();
2245
2269
  return this._meta.columnMap;
@@ -2934,6 +2958,15 @@ var BaseDbAdapter = class {
2934
2958
  if (logger) this.logger = logger;
2935
2959
  }
2936
2960
  /**
2961
+ * Called by {@link DbSpace} right after its factory builds this adapter —
2962
+ * the administrative one included — before {@link registerReadable}. No-op
2963
+ * by default: override it to share state across a space's adapters when the
2964
+ * adapter has no driver to share it through (the memory adapter keeps one
2965
+ * store per space).
2966
+ * @since 0.1.137
2967
+ */
2968
+ registerSpace(_space) {}
2969
+ /**
2937
2970
  * Enables or disables verbose (debug-level) logging for this adapter.
2938
2971
  * When disabled, no log strings are constructed — zero overhead.
2939
2972
  */
@@ -3118,6 +3151,17 @@ var BaseDbAdapter = class {
3118
3151
  return require_aggregate_fns.BASE_AGGREGATE_FNS;
3119
3152
  }
3120
3153
  /**
3154
+ * Revision of how this adapter renders a managed view (its SQL / pipeline)
3155
+ * from an unchanged view definition. Stored in each managed view's sync
3156
+ * snapshot when defined, so bumping it recreates every managed view of the
3157
+ * adapter once on the next sync — return a new value whenever a rendering
3158
+ * fix changes what an existing view returns. `undefined` (the default)
3159
+ * leaves the snapshot, and its hash, as before. External views ignore it.
3160
+ *
3161
+ * @since 0.1.137
3162
+ */
3163
+ viewRenderRevision() {}
3164
+ /**
3121
3165
  * Whether this adapter enforces foreign key constraints natively.
3122
3166
  * When `true`, the generic layer skips application-level cascade/setNull
3123
3167
  * on delete — the DB engine handles it (e.g. SQLite `ON DELETE CASCADE`).
@@ -3406,6 +3450,26 @@ var BaseDbAdapter = class {
3406
3450
  */
3407
3451
  supportsColumnModify;
3408
3452
  /**
3453
+ * Drops a table by name (without needing a registered readable).
3454
+ * Used by schema sync to remove tables no longer in the schema. A missing
3455
+ * table is not an error (`DROP TABLE IF EXISTS`). The default throws —
3456
+ * schema sync then reports the removed table as an `error` entry and keeps
3457
+ * it tracked. Since 0.1.137; before, the method was optional and a drop the
3458
+ * adapter lacked was skipped silently while sync reported it done.
3459
+ */
3460
+ async dropTableByName(tableName) {
3461
+ throw new Error(`Cannot drop table "${tableName}": dropTableByName is not supported by this adapter`);
3462
+ }
3463
+ /**
3464
+ * Drops a view by name (without needing a registered readable).
3465
+ * Used by schema sync to remove views no longer in the schema, and to drop
3466
+ * a managed view before recreating it. A missing view is not an error. The
3467
+ * default throws, with the same history as {@link dropTableByName}.
3468
+ */
3469
+ async dropViewByName(viewName) {
3470
+ throw new Error(`Cannot drop view "${viewName}": dropViewByName is not supported by this adapter`);
3471
+ }
3472
+ /**
3409
3473
  * Drops several tables that reference each other (a foreign-key cycle) as
3410
3474
  * one operation. Schema sync only calls this for cycles whose members are
3411
3475
  * ALL being removed. Default: {@link dropTableByName} in the given order —
@@ -3415,7 +3479,7 @@ var BaseDbAdapter = class {
3415
3479
  * @since 0.1.128
3416
3480
  */
3417
3481
  async dropTablesByName(tableNames) {
3418
- for (const name of tableNames) await this.dropTableByName?.(name);
3482
+ for (const name of tableNames) await this.dropTableByName(name);
3419
3483
  }
3420
3484
  /**
3421
3485
  * Whether the table has at least one row. Schema sync uses it in the
@@ -3527,7 +3591,7 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
3527
3591
  const physicalToLogical = /* @__PURE__ */ new Map();
3528
3592
  const physicalFields = [];
3529
3593
  for (const logical of neededLogical) {
3530
- const physical = meta.pathToPhysical.get(logical) ?? meta.columnMap.get(logical) ?? logical;
3594
+ const physical = meta.physicalPath(logical);
3531
3595
  physicalFields.push(physical);
3532
3596
  physicalToLogical.set(physical, logical);
3533
3597
  }
@@ -3995,19 +4059,6 @@ var RemoveGuardContext = class {
3995
4059
  return this._pending;
3996
4060
  }
3997
4061
  };
3998
- /** Translates a single ops record from logical to physical column names. */
3999
- function _translateOpsRecord(rec, meta) {
4000
- const out = {};
4001
- for (const key in rec) out[meta.leafByLogical.get(key)?.physicalName ?? key] = rec[key];
4002
- return out;
4003
- }
4004
- /** Translates ops keys from logical field names to physical column names. */
4005
- function _translateOpsKeys(ops, meta) {
4006
- return {
4007
- inc: ops.inc ? _translateOpsRecord(ops.inc, meta) : void 0,
4008
- mul: ops.mul ? _translateOpsRecord(ops.mul, meta) : void 0
4009
- };
4010
- }
4011
4062
  /** Upper bound of keys per `touchMany` UPDATE statement (parameter-count safety). */
4012
4063
  const TOUCH_MANY_CHUNK = 500;
4013
4064
  /** `touchMany` input rejection — always `INVALID_QUERY`, path names the key. */
@@ -4268,13 +4319,13 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
4268
4319
  let result;
4269
4320
  if (this.adapter.supportsNativePatch()) {
4270
4321
  const ops = require_ops.separateFieldOps(data);
4271
- const translatedOps = ops ? _translateOpsKeys(ops, this._meta) : void 0;
4322
+ const translatedOps = ops ? this._fieldMapper.translateOpsKeys(ops, this._meta) : void 0;
4272
4323
  const translatedData = this._fieldMapper.translatePatchKeys(data, this._meta);
4273
4324
  result = await this.adapter.nativePatch(translatedFilter, translatedData, translatedOps, expectedVersion);
4274
4325
  } else {
4275
4326
  const update = decomposePatch(data, this);
4276
4327
  const ops = require_ops.separateFieldOps(update);
4277
- const translatedOps = ops ? _translateOpsKeys(ops, this._meta) : void 0;
4328
+ const translatedOps = ops ? this._fieldMapper.translateOpsKeys(ops, this._meta) : void 0;
4278
4329
  const translatedUpdate = this._fieldMapper.translatePatchKeys(update, this._meta);
4279
4330
  if (getArrayOpsFields(translatedUpdate).size > 0) {
4280
4331
  const resolved = resolveArrayOps(translatedUpdate, await this.adapter.findOne({
@@ -4413,7 +4464,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
4413
4464
  await this._encryptItems([dataCopy], "patch");
4414
4465
  const update = decomposePatch(dataCopy, this);
4415
4466
  const ops = require_ops.separateFieldOps(update);
4416
- const translatedOps = ops ? _translateOpsKeys(ops, this._meta) : void 0;
4467
+ const translatedOps = ops ? this._fieldMapper.translateOpsKeys(ops, this._meta) : void 0;
4417
4468
  const translatedUpdate = this._fieldMapper.translatePatchKeys(update, this._meta);
4418
4469
  const translatedFilter = this._fieldMapper.translateFilter(filter, this._meta);
4419
4470
  if (translatedOps === void 0 && require_validator.isEmptyObject(translatedUpdate)) return {
@@ -4695,6 +4746,7 @@ function sourceIndex(type) {
4695
4746
  idx = {
4696
4747
  flatMap: /* @__PURE__ */ new Map(),
4697
4748
  columnMap: /* @__PURE__ */ new Map(),
4749
+ documentColumnMap: /* @__PURE__ */ new Map(),
4698
4750
  unstored: /* @__PURE__ */ new Set(),
4699
4751
  jsonRoots: /* @__PURE__ */ new Set(),
4700
4752
  encrypted: /* @__PURE__ */ new Set(),
@@ -4718,7 +4770,10 @@ function sourceIndex(type) {
4718
4770
  for (const [path, fieldType, metadata] of collected) {
4719
4771
  if (!path || findAncestorInSet(path, navFields) !== void 0) continue;
4720
4772
  const column = metadata.get("db.column");
4721
- if (column) idx.columnMap.set(path, column);
4773
+ if (column) {
4774
+ idx.columnMap.set(path, column);
4775
+ if (columnOverrideApplies(path, true)) idx.documentColumnMap.set(path, column);
4776
+ }
4722
4777
  if (metadata.has("db.ignore") || navFields.has(path)) idx.unstored.add(path);
4723
4778
  if (metadata.has("db.encrypted")) storage.push([path, false]);
4724
4779
  else if (metadata.has("db.json") || resolveDesignType(fieldType) === "array") storage.push([path, true]);
@@ -4765,7 +4820,7 @@ function resolveViewSource(sourceType, logicalPath, nestedObjects) {
4765
4820
  if (unstored !== void 0) throw new Error(`"${logicalPath}" has no column — "${unstored}" is @db.ignore or a navigation relation`);
4766
4821
  const designType = resolveDesignType(node);
4767
4822
  if (nestedObjects) return {
4768
- column: documentPath(idx.columnMap, logicalPath),
4823
+ column: documentPath(idx.documentColumnMap, logicalPath),
4769
4824
  designType,
4770
4825
  optional
4771
4826
  };
@@ -4965,7 +5020,7 @@ var AtscriptDbView = class extends AtscriptDbReadable {
4965
5020
  const ignored = this.ignoredFields;
4966
5021
  const meta = this.getMetadata();
4967
5022
  const nested = this._nested;
4968
- const viewName = (path) => nested ? meta.documentPath(path) : meta.pathToPhysical.get(path);
5023
+ const viewName = (path) => nested ? meta.physicalPath(path) : meta.pathToPhysical.get(path);
4969
5024
  const fail = (field, message) => {
4970
5025
  throw new Error(`View "${this.tableName}" field "${field}": ${message}`);
4971
5026
  };
@@ -5050,6 +5105,328 @@ function isAtscriptDbView(readable) {
5050
5105
  return readable.isView;
5051
5106
  }
5052
5107
  //#endregion
5108
+ //#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) => {
5112
+ const snap = {
5113
+ physicalName: f.physicalName,
5114
+ designType: f.designType,
5115
+ optional: f.optional,
5116
+ isPrimaryKey: f.isPrimaryKey,
5117
+ storage: f.storage
5118
+ };
5119
+ if (f.defaultValue) snap.defaultValue = f.defaultValue;
5120
+ if (typeMapper) snap.mappedType = typeMapper(f);
5121
+ if (f.encrypted) snap.encrypted = true;
5122
+ return snap;
5123
+ }).toSorted((a, b) => a.physicalName.localeCompare(b.physicalName));
5124
+ }
5125
+ /**
5126
+ * Extracts a canonical, serializable snapshot from a readable's metadata.
5127
+ * Sorted deterministically so the hash is stable across runs.
5128
+ *
5129
+ * @param readable - The table/view readable.
5130
+ * @param typeMapper - Optional adapter-specific type mapper. When provided,
5131
+ * each field's mapped type (e.g., "VARCHAR(255)") is stored in the snapshot
5132
+ * for precise type change detection.
5133
+ */
5134
+ function computeTableSnapshot(readable, typeMapper, tableOptions) {
5135
+ const fields = extractFieldSnapshots(readable.fieldDescriptors, typeMapper);
5136
+ const indexes = [...readable.indexes.values()].map((idx) => ({
5137
+ key: idx.key,
5138
+ type: idx.type,
5139
+ fields: idx.fields.map((f) => ({
5140
+ name: f.name,
5141
+ sort: f.sort
5142
+ }))
5143
+ })).toSorted((a, b) => a.key.localeCompare(b.key));
5144
+ const foreignKeys = [...readable.foreignKeys.values()].map((fk) => ({
5145
+ fields: [...fk.fields].toSorted(),
5146
+ targetTable: fk.targetTable,
5147
+ targetFields: [...fk.targetFields].toSorted(),
5148
+ onDelete: fk.onDelete,
5149
+ onUpdate: fk.onUpdate
5150
+ })).toSorted((a, b) => a.fields.join(",").localeCompare(b.fields.join(",")));
5151
+ const snapshot = {
5152
+ tableName: readable.tableName,
5153
+ fields,
5154
+ indexes,
5155
+ foreignKeys
5156
+ };
5157
+ if (tableOptions?.length) snapshot.tableOptions = [...tableOptions].toSorted((a, b) => a.key.localeCompare(b.key));
5158
+ return snapshot;
5159
+ }
5160
+ /**
5161
+ * Extracts a canonical, serializable snapshot from a view's metadata.
5162
+ * Captures view plan (entry table, joins, filter, materialization) for
5163
+ * detecting view definition changes.
5164
+ */
5165
+ function computeViewSnapshot(view) {
5166
+ const fields = extractFieldSnapshots(view.fieldDescriptors);
5167
+ if (view.isExternal) return {
5168
+ tableName: view.tableName,
5169
+ viewType: "E",
5170
+ fields
5171
+ };
5172
+ const plan = view.viewPlan;
5173
+ const qualify = (ref) => view.resolveFieldRef(ref, (n) => n);
5174
+ const canonical = (node) => JSON.stringify(canonicalizeQueryNode(node, qualify));
5175
+ const columns = view.getViewColumnMappings().map((m) => {
5176
+ const col = {
5177
+ column: m.viewColumn,
5178
+ sourceTable: m.sourceTable,
5179
+ sourceColumn: m.sourceColumn
5180
+ };
5181
+ if (m.json) {
5182
+ col.jsonPath = JSON.stringify(m.json.path);
5183
+ col.jsonType = m.json.type;
5184
+ }
5185
+ if (m.aggFn) col.aggFn = m.aggFn;
5186
+ if (m.aggField) col.aggField = m.aggField;
5187
+ if (m.aggFilter) col.aggFilter = canonical(m.aggFilter);
5188
+ return col;
5189
+ }).toSorted((a, b) => a.column < b.column ? -1 : a.column > b.column ? 1 : 0);
5190
+ const result = {
5191
+ tableName: view.tableName,
5192
+ viewType: plan.materialized ? "M" : "V",
5193
+ entryTable: plan.entryTable,
5194
+ joinTables: plan.joins.map((j) => {
5195
+ const join = {
5196
+ targetTable: j.targetTable,
5197
+ condition: canonical(j.condition)
5198
+ };
5199
+ if (j.kind === "left") join.kind = "left";
5200
+ return join;
5201
+ }),
5202
+ columns
5203
+ };
5204
+ if (plan.filter) result.filterHash = fnv1a(canonical(plan.filter));
5205
+ if (plan.having) result.havingHash = fnv1a(canonical(plan.having));
5206
+ if (plan.materialized) result.materialized = true;
5207
+ const renderRevision = view.dbAdapter.viewRenderRevision();
5208
+ if (renderRevision !== void 0) result.renderRevision = renderRevision;
5209
+ return {
5210
+ ...result,
5211
+ fields
5212
+ };
5213
+ }
5214
+ /**
5215
+ * Converts a view predicate (join condition, `@db.view.filter`,
5216
+ * `@db.view.having`) into a serializable structure whose JSON is a stable
5217
+ * function of its MEANING: field refs become `qualify(ref)` — the view's
5218
+ * `resolveFieldRef(ref, (n) => n)`, i.e. `"<table>.<field>"`, so a predicate
5219
+ * retargeted to another table with the same field name changes — operators
5220
+ * and literal values are kept as-is, `$and`/`$or` keep declaration order, and
5221
+ * no function references survive. Two identical models produce byte-identical
5222
+ * JSON.
5223
+ * @since 0.1.128
5224
+ */
5225
+ function canonicalizeQueryNode(node, qualify) {
5226
+ if ("$and" in node) return { and: node.$and.map((n) => canonicalizeQueryNode(n, qualify)) };
5227
+ if ("$or" in node) return { or: node.$or.map((n) => canonicalizeQueryNode(n, qualify)) };
5228
+ if ("$not" in node) return { not: canonicalizeQueryNode(node.$not, qualify) };
5229
+ const comp = node;
5230
+ const out = {
5231
+ l: qualify(comp.left),
5232
+ op: comp.op
5233
+ };
5234
+ if (comp.right !== void 0) out.r = comp.right !== null && typeof comp.right === "object" && "field" in comp.right ? { f: qualify(comp.right) } : comp.right;
5235
+ return out;
5236
+ }
5237
+ /**
5238
+ * Computes a deterministic hash string from multiple table snapshots.
5239
+ * Uses FNV-1a for speed — not cryptographic, just needs stability + collision resistance.
5240
+ */
5241
+ function computeSchemaHash(snapshots) {
5242
+ const sorted = [...snapshots].toSorted((a, b) => a.tableName.localeCompare(b.tableName));
5243
+ return fnv1a(JSON.stringify(sorted));
5244
+ }
5245
+ /**
5246
+ * Computes a hash for a single table/view snapshot.
5247
+ * Used for per-table change detection via stored snapshots.
5248
+ */
5249
+ function computeTableHash(snapshot) {
5250
+ return fnv1a(JSON.stringify(snapshot));
5251
+ }
5252
+ /**
5253
+ * Converts stored snapshot fields to `TExistingColumn[]` format
5254
+ * for use with `computeColumnDiff`. Used by adapters that lack
5255
+ * native column introspection (e.g., MongoDB).
5256
+ *
5257
+ * The `type` field uses `mappedType` when available (adapter-specific),
5258
+ * falling back to `designType`.
5259
+ */
5260
+ function snapshotToExistingColumns(snapshot) {
5261
+ return snapshot.fields.map((f) => ({
5262
+ name: f.physicalName,
5263
+ type: f.mappedType ?? f.designType,
5264
+ notnull: !f.optional,
5265
+ pk: f.isPrimaryKey,
5266
+ dflt_value: serializeDefaultValue(f.defaultValue)
5267
+ }));
5268
+ }
5269
+ /**
5270
+ * Extracts table options from a stored snapshot for diff comparison.
5271
+ * Used as fallback when an adapter lacks native table option introspection.
5272
+ */
5273
+ function snapshotToExistingTableOptions(snapshot) {
5274
+ return snapshot.tableOptions ?? [];
5275
+ }
5276
+ /** Serializes a TDbDefaultValue to a comparable string. */
5277
+ function serializeDefaultValue(dv) {
5278
+ if (!dv) return;
5279
+ if (dv.kind === "value") return dv.value;
5280
+ return `fn:${dv.fn}`;
5281
+ }
5282
+ /** FNV-1a 32-bit hash → hex string */
5283
+ function fnv1a(str) {
5284
+ let hash = 2166136261;
5285
+ for (let i = 0; i < str.length; i++) {
5286
+ hash ^= str.codePointAt(i);
5287
+ hash = Math.imul(hash, 16777619);
5288
+ }
5289
+ return Math.trunc(hash).toString(16).padStart(8, "0");
5290
+ }
5291
+ //#endregion
5292
+ //#region src/schema/fk-diff.ts
5293
+ /** Canonical key for an FK: sorted local field names, comma-joined. */
5294
+ function fkKey(fields) {
5295
+ return [...fields].toSorted().join(",");
5296
+ }
5297
+ /**
5298
+ * Compares desired FK constraints against stored snapshot to detect
5299
+ * additions, removals, and property changes (target table, target fields,
5300
+ * onDelete, onUpdate).
5301
+ */
5302
+ function computeForeignKeyDiff(desired, existingSnapshot) {
5303
+ const added = [];
5304
+ const removed = [];
5305
+ const changed = [];
5306
+ const existingByKey = /* @__PURE__ */ new Map();
5307
+ for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
5308
+ const desiredKeys = /* @__PURE__ */ new Set();
5309
+ for (const fk of desired.values()) {
5310
+ const key = fkKey(fk.fields);
5311
+ desiredKeys.add(key);
5312
+ const existing = existingByKey.get(key);
5313
+ if (!existing) added.push(fk);
5314
+ else if (fkPropertiesDiffer(fk, existing)) changed.push({
5315
+ desired: fk,
5316
+ existing
5317
+ });
5318
+ }
5319
+ for (const [key, fk] of existingByKey) if (!desiredKeys.has(key)) removed.push(fk);
5320
+ return {
5321
+ added,
5322
+ removed,
5323
+ changed
5324
+ };
5325
+ }
5326
+ /** Whether the FK diff contains any changes. */
5327
+ function hasForeignKeyChanges(diff) {
5328
+ return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
5329
+ }
5330
+ function fkPropertiesDiffer(desired, existing) {
5331
+ if (desired.targetTable !== existing.targetTable) return true;
5332
+ if (fkKey(desired.targetFields) !== fkKey(existing.targetFields)) return true;
5333
+ if ((desired.onDelete ?? void 0) !== (existing.onDelete ?? void 0)) return true;
5334
+ if ((desired.onUpdate ?? void 0) !== (existing.onUpdate ?? void 0)) return true;
5335
+ return false;
5336
+ }
5337
+ //#endregion
5338
+ //#region src/schema/column-diff.ts
5339
+ /**
5340
+ * Whether a live column's type differs from the type the adapter's
5341
+ * `typeMapper` gives the field — the one rule schema sync diffs column types
5342
+ * by (case-insensitive). Exported for adapters that must agree with it (the
5343
+ * PostgreSQL recreate converts exactly the columns this reports changed).
5344
+ * @since 0.1.137
5345
+ */
5346
+ function isColumnTypeChanged(existingType, expectedType) {
5347
+ return existingType.toUpperCase() !== expectedType.toUpperCase();
5348
+ }
5349
+ /**
5350
+ * Computes the difference between desired schema fields and existing database columns.
5351
+ *
5352
+ * @param desired - Field descriptors from the Atscript type (after flattening).
5353
+ * @param existing - Columns currently in the database (from introspection).
5354
+ * @param typeMapper - Optional function to map field metadata to DB-native type strings.
5355
+ * Receives the full field meta (design type, annotations, PK status, etc.)
5356
+ * so adapters can produce context-aware types (e.g., `VARCHAR(255)` from maxLength).
5357
+ * Required for type change detection.
5358
+ */
5359
+ function computeColumnDiff(desired, existing, typeMapper) {
5360
+ const existingByName = new Map(existing.map((c) => [c.name, c]));
5361
+ const desiredByName = /* @__PURE__ */ new Map();
5362
+ const renamedOldNames = /* @__PURE__ */ new Set();
5363
+ const added = [];
5364
+ const renamed = [];
5365
+ const typeChanged = [];
5366
+ const nullableChanged = [];
5367
+ const defaultChanged = [];
5368
+ const conflicts = [];
5369
+ for (const field of desired) {
5370
+ if (field.ignored) continue;
5371
+ desiredByName.set(field.physicalName, field);
5372
+ 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({
5383
+ field,
5384
+ existingType: existingCol.type
5385
+ });
5386
+ }
5387
+ if (!field.isPrimaryKey && !existingCol.pk) {
5388
+ const desiredNotNull = !field.optional;
5389
+ if (existingCol.notnull !== desiredNotNull) nullableChanged.push({
5390
+ field,
5391
+ wasNullable: !existingCol.notnull
5392
+ });
5393
+ }
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)) {
5402
+ renamed.push({
5403
+ field,
5404
+ oldName: field.renamedFrom
5405
+ });
5406
+ renamedOldNames.add(field.renamedFrom);
5407
+ } else added.push(field);
5408
+ }
5409
+ const diff = {
5410
+ added,
5411
+ removed: existing.filter((c) => !desiredByName.has(c.name) && !renamedOldNames.has(c.name)),
5412
+ renamed,
5413
+ typeChanged,
5414
+ nullableChanged,
5415
+ defaultChanged,
5416
+ conflicts
5417
+ };
5418
+ if (existing.length > 0) {
5419
+ const newNameByOld = new Map(renamed.map((r) => [r.oldName, r.field.physicalName]));
5420
+ const from = existing.filter((c) => c.pk).map((c) => newNameByOld.get(c.name) ?? c.name);
5421
+ const to = desired.filter((f) => !f.ignored && f.isPrimaryKey).map((f) => f.physicalName);
5422
+ if (fkKey(from) !== fkKey(to)) diff.primaryKeyChanged = {
5423
+ from,
5424
+ to
5425
+ };
5426
+ }
5427
+ return diff;
5428
+ }
5429
+ //#endregion
5053
5430
  Object.defineProperty(exports, "ADAPTER_FILTER_REASON", {
5054
5431
  enumerable: true,
5055
5432
  get: function() {
@@ -5176,6 +5553,12 @@ Object.defineProperty(exports, "canFilterLeaf", {
5176
5553
  return canFilterLeaf;
5177
5554
  }
5178
5555
  });
5556
+ Object.defineProperty(exports, "canonicalizeQueryNode", {
5557
+ enumerable: true,
5558
+ get: function() {
5559
+ return canonicalizeQueryNode;
5560
+ }
5561
+ });
5179
5562
  Object.defineProperty(exports, "checkHavingKeys", {
5180
5563
  enumerable: true,
5181
5564
  get: function() {
@@ -5194,6 +5577,42 @@ Object.defineProperty(exports, "collectQueryPaths", {
5194
5577
  return collectQueryPaths;
5195
5578
  }
5196
5579
  });
5580
+ Object.defineProperty(exports, "computeColumnDiff", {
5581
+ enumerable: true,
5582
+ get: function() {
5583
+ return computeColumnDiff;
5584
+ }
5585
+ });
5586
+ Object.defineProperty(exports, "computeForeignKeyDiff", {
5587
+ enumerable: true,
5588
+ get: function() {
5589
+ return computeForeignKeyDiff;
5590
+ }
5591
+ });
5592
+ Object.defineProperty(exports, "computeSchemaHash", {
5593
+ enumerable: true,
5594
+ get: function() {
5595
+ return computeSchemaHash;
5596
+ }
5597
+ });
5598
+ Object.defineProperty(exports, "computeTableHash", {
5599
+ enumerable: true,
5600
+ get: function() {
5601
+ return computeTableHash;
5602
+ }
5603
+ });
5604
+ Object.defineProperty(exports, "computeTableSnapshot", {
5605
+ enumerable: true,
5606
+ get: function() {
5607
+ return computeTableSnapshot;
5608
+ }
5609
+ });
5610
+ Object.defineProperty(exports, "computeViewSnapshot", {
5611
+ enumerable: true,
5612
+ get: function() {
5613
+ return computeViewSnapshot;
5614
+ }
5615
+ });
5197
5616
  Object.defineProperty(exports, "createFailureCollector", {
5198
5617
  enumerable: true,
5199
5618
  get: function() {
@@ -5212,6 +5631,12 @@ Object.defineProperty(exports, "findAncestorInSet", {
5212
5631
  return findAncestorInSet;
5213
5632
  }
5214
5633
  });
5634
+ Object.defineProperty(exports, "fkKey", {
5635
+ enumerable: true,
5636
+ get: function() {
5637
+ return fkKey;
5638
+ }
5639
+ });
5215
5640
  Object.defineProperty(exports, "guardAggregate", {
5216
5641
  enumerable: true,
5217
5642
  get: function() {
@@ -5242,6 +5667,12 @@ Object.defineProperty(exports, "guardQuery", {
5242
5667
  return guardQuery;
5243
5668
  }
5244
5669
  });
5670
+ Object.defineProperty(exports, "hasForeignKeyChanges", {
5671
+ enumerable: true,
5672
+ get: function() {
5673
+ return hasForeignKeyChanges;
5674
+ }
5675
+ });
5245
5676
  Object.defineProperty(exports, "isAtscriptDbView", {
5246
5677
  enumerable: true,
5247
5678
  get: function() {
@@ -5254,6 +5685,12 @@ Object.defineProperty(exports, "isBucketableField", {
5254
5685
  return isBucketableField;
5255
5686
  }
5256
5687
  });
5688
+ Object.defineProperty(exports, "isColumnTypeChanged", {
5689
+ enumerable: true,
5690
+ get: function() {
5691
+ return isColumnTypeChanged;
5692
+ }
5693
+ });
5257
5694
  Object.defineProperty(exports, "isGeoIndexableType", {
5258
5695
  enumerable: true,
5259
5696
  get: function() {
@@ -5302,6 +5739,18 @@ Object.defineProperty(exports, "resolveDesignType", {
5302
5739
  return resolveDesignType;
5303
5740
  }
5304
5741
  });
5742
+ Object.defineProperty(exports, "snapshotToExistingColumns", {
5743
+ enumerable: true,
5744
+ get: function() {
5745
+ return snapshotToExistingColumns;
5746
+ }
5747
+ });
5748
+ Object.defineProperty(exports, "snapshotToExistingTableOptions", {
5749
+ enumerable: true,
5750
+ get: function() {
5751
+ return snapshotToExistingTableOptions;
5752
+ }
5753
+ });
5305
5754
  Object.defineProperty(exports, "sortFieldNames", {
5306
5755
  enumerable: true,
5307
5756
  get: function() {