@atscript/db 0.1.150 → 0.1.152

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.
@@ -1,6 +1,6 @@
1
1
  import { a as aggregateExpressionsNotSupported, l as spaceClosedError, n as CasMismatchError, r as DbError } from "./db-error-Az85UhTX.mjs";
2
2
  import { a as SOURCE_VALUE_FNS, c as isFirstLast, o as SUPPORTED_AGGREGATE_FNS, r as BASE_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-CyaZyb9I.mjs";
3
- import { B as slicePerGroup, D as createRelationFilterHost, E as containsRelationFilter, F as compositeKey, H as computeOf, I as findFKEntryForRelation, J as walkViewExpr, L as findFKForRelation, M as relGuardState, P as resolveRelationFilterTree, R as findRemoteFK, U as computedOperands, V as tableNameOf, _ as remapDeleteFkViolation, a as batchPatchNestedFrom, b as sameKey, c as batchReplaceNestedFrom, d as checkDepthOverflow, f as planNestedFromVia, g as enrichFkViolation, h as validateBatch, i as batchInsertNestedVia, j as noteRelationFilter, k as hasRelationOp, l as batchReplaceNestedTo, m as preValidateNestedFrom, n as batchInsertNestedFrom, p as planPatchNestedTo, q as viewExprNullable, r as batchInsertNestedTo, s as batchPatchNestedVia, t as applyPatchNestedTo, u as batchReplaceNestedVia, v as pkTupleKey, x as uniqueKeyTuple, y as rowMatchesKey } from "./nested-writer-CUBoq1ZO.mjs";
3
+ import { A as hasRelationOp, D as containsRelationFilter, F as resolveRelationFilterTree, H as tableNameOf, I as compositeKey, J as viewExprNullable, L as findFKEntryForRelation, M as noteRelationFilter, N as relGuardState, O as createRelationFilterHost, R as findFKForRelation, S as uniqueKeyTuple, U as computeOf, V as slicePerGroup, W as computedOperands, Y as walkViewExpr, _ as remapDeleteFkViolation, a as batchPatchNestedFrom, b as rowMatchesKey, c as batchReplaceNestedFrom, d as checkDepthOverflow, f as planNestedFromVia, g as enrichFkViolation, h as validateBatch, i as batchInsertNestedVia, l as batchReplaceNestedTo, m as preValidateNestedFrom, n as batchInsertNestedFrom, p as planPatchNestedTo, r as batchInsertNestedTo, s as batchPatchNestedVia, t as applyPatchNestedTo, u as batchReplaceNestedVia, v as findRowsByKeys, x as sameKey, y as pkTupleKey, z as findRemoteFK } from "./nested-writer-cvbVAkNo.mjs";
4
4
  import { a as DERIVED_INCOMPATIBLE, n as searchMemberKind, r as numericTypeProblem, s as isJsonLeafType } from "./search-fields-DBIZKwyb.mjs";
5
5
  import { a as isPlainObject, i as isEmptyObject, n as findAncestorInSet, o as selfOrAncestor, r as getPath, t as deletePath } from "./object-TkiJQ-Dp.mjs";
6
6
  import { resolveAlias as resolveAlias$1 } from "./agg.mjs";
@@ -2520,23 +2520,37 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
2520
2520
  this.reverseColumnRenames(row, meta);
2521
2521
  return row;
2522
2522
  }
2523
+ const plan = readPlanFor(meta);
2524
+ const cols = plan.columns;
2523
2525
  const result = {};
2524
- const fromFmts = meta.fromStorageFormatters;
2525
2526
  for (const physical of Object.keys(row)) {
2526
- const fd = meta.leafByPhysical.get(physical);
2527
- if (!fd) {
2527
+ const col = cols.get(physical);
2528
+ if (col === void 0) {
2528
2529
  result[physical] = row[physical];
2529
2530
  continue;
2530
2531
  }
2531
- let raw = row[physical];
2532
- const fromFmt = fromFmts?.get(physical);
2533
- if (fromFmt && raw !== null && raw !== void 0) raw = fromFmt(raw);
2534
- const value = fd.designType === "boolean" ? toBool(raw) : fd.designType === "decimal" ? toDecimalString(raw) : raw;
2535
- if (fd.storage === "json") this.setNestedValue(result, fd.path, typeof value === "string" ? JSON.parse(value) : value);
2536
- else if (fd.storage === "flattened") this.setNestedValue(result, fd.path, value);
2537
- else result[fd.path] = value;
2538
- }
2539
- for (const parentPath of meta.flattenedParents) this.reconstructNullParent(result, parentPath, meta);
2532
+ let value = row[physical];
2533
+ if (col.fromFmt !== void 0 && value !== null && value !== void 0) value = col.fromFmt(value);
2534
+ if (col.coerce !== void 0) value = col.coerce(value);
2535
+ if (col.json && typeof value === "string") value = JSON.parse(value);
2536
+ const parents = col.parents;
2537
+ if (parents === void 0) {
2538
+ result[col.path] = value;
2539
+ continue;
2540
+ }
2541
+ let current = result;
2542
+ for (let i = 0; i < parents.length; i++) {
2543
+ const part = parents[i];
2544
+ const next = current[part];
2545
+ if (next === void 0 || next === null) {
2546
+ const created = {};
2547
+ current[part] = created;
2548
+ current = created;
2549
+ } else current = next;
2550
+ }
2551
+ current[col.last] = value;
2552
+ }
2553
+ for (const parent of plan.parents) collapseNullParent(result, parent);
2540
2554
  return result;
2541
2555
  }
2542
2556
  translateQuery(query, meta) {
@@ -2595,17 +2609,16 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
2595
2609
  return result;
2596
2610
  }
2597
2611
  prepareForWrite(payload, meta, adapter) {
2612
+ if (meta.requiresMappings && !meta.onlyColumnRenames) return this.formatWriteValues(this.flattenWritePayload(payload, meta, adapter), meta);
2598
2613
  const data = { ...payload };
2599
2614
  this.prepareCommon(data, meta, adapter);
2600
- if (!meta.requiresMappings) return this.formatWriteValues(data, meta);
2601
2615
  if (meta.onlyColumnRenames) {
2602
2616
  for (const [logical, physical] of meta.columnMap.entries()) if (logical in data) {
2603
2617
  data[physical] = data[logical];
2604
2618
  delete data[logical];
2605
2619
  }
2606
- return this.formatWriteValues(data, meta);
2607
2620
  }
2608
- return this.formatWriteValues(this.flattenPayload(data, meta), meta);
2621
+ return this.formatWriteValues(data, meta);
2609
2622
  }
2610
2623
  translatePatchKeys(update, meta) {
2611
2624
  if (!meta.requiresMappings && !meta.toStorageFormatters) return update;
@@ -2680,41 +2693,160 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
2680
2693
  return result;
2681
2694
  }
2682
2695
  /**
2683
- * Flattens nested object fields into __-separated keys and
2684
- * JSON-stringifies @db.json / array fields.
2696
+ * Flattens nested objects into __-separated keys and JSON-stringifies
2697
+ * @db.json / array fields — with `prepareCommon` folded in: primary-key
2698
+ * values prepared (`adapter.prepareId`), top-level ignored fields (skipped
2699
+ * by {@link writeFlattenedField}) and derived fields left out. `payload`
2700
+ * itself is not modified.
2685
2701
  */
2686
- flattenPayload(data, meta) {
2702
+ flattenWritePayload(payload, meta, adapter) {
2687
2703
  const result = {};
2688
- for (const key of Object.keys(data)) this.writeFlattenedField(key, data[key], result, meta);
2704
+ const primaryKeys = meta.primaryKeys;
2705
+ const root = writePlanFor(meta);
2706
+ for (const key of Object.keys(payload)) {
2707
+ if (meta.derivedFields.has(key)) continue;
2708
+ let value = payload[key];
2709
+ if (value !== void 0 && primaryKeys.includes(key)) {
2710
+ const fieldType = meta.flatMap?.get(key);
2711
+ if (fieldType) value = adapter.prepareId(value, fieldType);
2712
+ }
2713
+ writeFlattenedField(root, "", key, value, result, meta);
2714
+ }
2689
2715
  return result;
2690
2716
  }
2691
- /**
2692
- * Classifies and writes a single field to the result object.
2693
- * Recurses into nested objects that should be flattened.
2694
- */
2695
- writeFlattenedField(path, value, result, meta) {
2696
- if (meta.ignoredFields.has(path)) return;
2697
- if (meta.flattenedParents.has(path)) {
2698
- if (value === null || value === void 0) this.setFlattenedChildrenNull(path, result, meta);
2717
+ };
2718
+ /**
2719
+ * Per-table read plan of {@link RelationalFieldMapper.reconstructFromRead}:
2720
+ * the per-column decisions (coercion, JSON, nesting — the dot-path pre-split)
2721
+ * and the parent-collapse list, compiled once per built metadata instead of
2722
+ * per row. Metadata is immutable after `build()`, which precedes any read.
2723
+ */
2724
+ const readPlans = /* @__PURE__ */ new WeakMap();
2725
+ function readPlanFor(meta) {
2726
+ let plan = readPlans.get(meta);
2727
+ if (plan === void 0) {
2728
+ plan = compileReadPlan(meta);
2729
+ readPlans.set(meta, plan);
2730
+ }
2731
+ return plan;
2732
+ }
2733
+ function compileReadPlan(meta) {
2734
+ const fromFmts = meta.fromStorageFormatters;
2735
+ const columns = /* @__PURE__ */ new Map();
2736
+ for (const [physical, fd] of meta.leafByPhysical) {
2737
+ const segs = (fd.storage === "json" || fd.storage === "flattened") && fd.path.includes(".") ? fd.path.split(".") : void 0;
2738
+ columns.set(physical, {
2739
+ path: fd.path,
2740
+ parents: segs?.slice(0, -1),
2741
+ last: segs ? segs[segs.length - 1] : fd.path,
2742
+ json: fd.storage === "json",
2743
+ coerce: fd.designType === "boolean" ? toBool : fd.designType === "decimal" ? toDecimalString : void 0,
2744
+ fromFmt: fromFmts?.get(physical)
2745
+ });
2746
+ }
2747
+ const parents = [];
2748
+ for (const parentPath of meta.flattenedParents) {
2749
+ const segs = parentPath.split(".");
2750
+ parents.push({
2751
+ ancestors: segs.slice(0, -1),
2752
+ last: segs[segs.length - 1],
2753
+ optional: !!meta.flatMap?.get(parentPath)?.optional
2754
+ });
2755
+ }
2756
+ return {
2757
+ columns,
2758
+ parents
2759
+ };
2760
+ }
2761
+ /** If every child of a flattened parent is null / undefined, collapse it (`null` when optional, else `{}`). */
2762
+ function collapseNullParent(obj, parent) {
2763
+ let current = obj;
2764
+ const ancestors = parent.ancestors;
2765
+ for (let i = 0; i < ancestors.length; i++) {
2766
+ const next = current[ancestors[i]];
2767
+ if (next === void 0 || next === null) return;
2768
+ current = next;
2769
+ }
2770
+ const parentObj = current[parent.last];
2771
+ if (typeof parentObj !== "object" || parentObj === null) return;
2772
+ for (const k of Object.keys(parentObj)) {
2773
+ const v = parentObj[k];
2774
+ if (v !== null && v !== void 0) return;
2775
+ }
2776
+ current[parent.last] = parent.optional ? null : {};
2777
+ }
2778
+ /**
2779
+ * Per-table write plan of the relational flatten: each logical path's
2780
+ * classification (ignored / flattened parent / json / plain column) and
2781
+ * physical column, resolved once per path instead of per row (no per-key
2782
+ * path concatenation and map lookups). Only schema paths are memoised — a
2783
+ * key the metadata does not know is resolved per call, so arbitrary payload
2784
+ * keys cannot grow the plan.
2785
+ */
2786
+ const writePlans = /* @__PURE__ */ new WeakMap();
2787
+ function writePlanFor(meta) {
2788
+ let root = writePlans.get(meta);
2789
+ if (root === void 0) {
2790
+ root = /* @__PURE__ */ new Map();
2791
+ writePlans.set(meta, root);
2792
+ }
2793
+ return root;
2794
+ }
2795
+ function resolveWriteNode(path, meta) {
2796
+ if (meta.ignoredFields.has(path)) return {
2797
+ node: {
2798
+ kind: "ignored",
2799
+ path,
2800
+ physical: ""
2801
+ },
2802
+ known: true
2803
+ };
2804
+ if (meta.flattenedParents.has(path)) return {
2805
+ node: {
2806
+ kind: "parent",
2807
+ path,
2808
+ physical: "",
2809
+ children: /* @__PURE__ */ new Map(),
2810
+ nullChildren: meta.childrenByParent.get(path) ?? []
2811
+ },
2812
+ known: true
2813
+ };
2814
+ const fd = meta.leafByLogical.get(path);
2815
+ return {
2816
+ node: {
2817
+ kind: fd?.storage === "json" ? "json" : "leaf",
2818
+ path,
2819
+ physical: fd?.physicalName ?? path.replace(/\./g, "__")
2820
+ },
2821
+ known: fd !== void 0
2822
+ };
2823
+ }
2824
+ /**
2825
+ * Classifies and writes a single field to the result object — recursing into
2826
+ * nested objects that are flattened; JSON-stringifies @db.json / array fields.
2827
+ */
2828
+ function writeFlattenedField(level, prefix, key, value, result, meta) {
2829
+ let node = level.get(key);
2830
+ if (node === void 0) {
2831
+ const resolved = resolveWriteNode(prefix ? `${prefix}.${key}` : key, meta);
2832
+ node = resolved.node;
2833
+ if (resolved.known) level.set(key, node);
2834
+ }
2835
+ switch (node.kind) {
2836
+ case "ignored": return;
2837
+ case "parent":
2838
+ if (value === null || value === void 0) for (const physical of node.nullChildren) result[physical] = null;
2699
2839
  else if (typeof value === "object" && !Array.isArray(value)) {
2700
2840
  const obj = value;
2701
- for (const key of Object.keys(obj)) this.writeFlattenedField(`${path}.${key}`, obj[key], result, meta);
2841
+ for (const childKey of Object.keys(obj)) writeFlattenedField(node.children, node.path, childKey, obj[childKey], result, meta);
2702
2842
  }
2703
- } else {
2704
- const fd = meta.leafByLogical.get(path);
2705
- const physical = fd?.physicalName ?? path.replace(/\./g, "__");
2706
- if (fd?.storage === "json") result[physical] = value !== void 0 && value !== null ? JSON.stringify(value) : value;
2707
- else result[physical] = value;
2708
- }
2709
- }
2710
- /**
2711
- * When a parent object is null/undefined, set all its flattened children to null.
2712
- */
2713
- setFlattenedChildrenNull(parentPath, result, meta) {
2714
- const children = meta.childrenByParent.get(parentPath);
2715
- if (children) for (const physical of children) result[physical] = null;
2843
+ return;
2844
+ case "json":
2845
+ result[node.physical] = value !== void 0 && value !== null ? JSON.stringify(value) : value;
2846
+ return;
2847
+ default: result[node.physical] = value;
2716
2848
  }
2717
- };
2849
+ }
2718
2850
  //#endregion
2719
2851
  //#region src/query/query-guards.ts
2720
2852
  /**
@@ -3712,6 +3844,11 @@ var AtscriptDbReadable = class {
3712
3844
  */
3713
3845
  get uniqueKeySets() {
3714
3846
  this._ensureBuilt();
3847
+ this._uniqueKeySets ??= this._computeUniqueKeySets();
3848
+ return this._uniqueKeySets;
3849
+ }
3850
+ _uniqueKeySets;
3851
+ _computeUniqueKeySets() {
3715
3852
  const sets = [];
3716
3853
  if (this._meta.primaryKeys.length > 0) sets.push(this._meta.primaryKeys.map((f) => this._meta.physicalPath(f)));
3717
3854
  for (const index of this._meta.indexes.values()) if (index.type === "unique") sets.push(index.fields.map((f) => f.name));
@@ -3719,7 +3856,8 @@ var AtscriptDbReadable = class {
3719
3856
  const column = this._meta.physicalPath(prop);
3720
3857
  if (!sets.some((s) => s.length === 1 && s[0] === column)) sets.push([column]);
3721
3858
  }
3722
- return sets;
3859
+ for (const set of sets) Object.freeze(set);
3860
+ return Object.freeze(sets);
3723
3861
  }
3724
3862
  /** Preferred row identifier field names. Defaults to primary keys. */
3725
3863
  get preferredId() {
@@ -4693,7 +4831,7 @@ var AtscriptDbReadable = class {
4693
4831
  * Public entry point for relation loading. Used by adapters for nested $with delegation.
4694
4832
  */
4695
4833
  async loadRelations(rows, withRelations) {
4696
- const { loadRelationsImpl } = await import("./relation-loader-ByY1Byrl.mjs").then((n) => n.n);
4834
+ const { loadRelationsImpl } = await import("./relation-loader-DYab07lw.mjs").then((n) => n.n);
4697
4835
  return loadRelationsImpl(rows, withRelations, this);
4698
4836
  }
4699
4837
  /**
@@ -5721,8 +5859,14 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
5721
5859
  }
5722
5860
  return pin;
5723
5861
  }
5724
- needsCascade(cascadeResolver) {
5725
- return !!cascadeResolver;
5862
+ /**
5863
+ * A resolver is set and — when `tableName` is given (since 0.1.151) — some
5864
+ * table references `tableName` with an `onDelete` action: a delete from a
5865
+ * table nothing references needs no cascade pass (nor its transaction).
5866
+ */
5867
+ needsCascade(cascadeResolver, tableName) {
5868
+ if (!cascadeResolver) return false;
5869
+ return tableName === void 0 || cascadeResolver(tableName).length > 0;
5726
5870
  }
5727
5871
  recordKey(tableName, pkFields, record) {
5728
5872
  let key = tableName;
@@ -5918,10 +6062,6 @@ function matchByKeys(a, b, keys) {
5918
6062
  for (const key of keys) if (a[key] !== b[key]) return false;
5919
6063
  return true;
5920
6064
  }
5921
- //#endregion
5922
- //#region src/patch/patch-decomposer.ts
5923
- /** Metadata tag the flattener puts on top-level array fields (shared with `isVersionExemptPatch`). */
5924
- const TOP_LEVEL_ARRAY_TAG = "db.__topLevelArray";
5925
6065
  /**
5926
6066
  * Decomposes a patch payload into a flat update object for adapters
5927
6067
  * that don't support native patch operations.
@@ -5940,21 +6080,25 @@ const TOP_LEVEL_ARRAY_TAG = "db.__topLevelArray";
5940
6080
  */
5941
6081
  function decomposePatch(payload, table) {
5942
6082
  const update = {};
5943
- flattenPatchPayload(payload, "", update, table, TOP_LEVEL_ARRAY_TAG);
6083
+ flattenPatchPayload(payload, "", update, table.getMetadata(), table);
5944
6084
  return update;
5945
6085
  }
5946
- function flattenPatchPayload(payload, prefix, update, table, topLevelArrayTag) {
5947
- const meta = table.getMetadata();
5948
- for (const [_key, value] of Object.entries(payload)) {
6086
+ function flattenPatchPayload(payload, prefix, update, meta, table) {
6087
+ const primaryKeys = meta.primaryKeys;
6088
+ const flatMap = meta.flatMap;
6089
+ for (const _key of Object.keys(payload)) {
6090
+ const value = payload[_key];
5949
6091
  const key = prefix ? `${prefix}.${_key}` : _key;
5950
- if (table.primaryKeys.includes(key)) continue;
5951
- const flatType = table.flatMap.get(key);
5952
- const isTopLevelArray = flatType?.metadata?.get(topLevelArrayTag);
5953
- const isObjectValue = typeof value === "object" && value !== null && !Array.isArray(value);
5954
- if (isObjectValue && isTopLevelArray && !flatType?.metadata?.has("db.json")) decomposeArrayPatch(key, value, flatType, update, table);
5955
- else if (isObjectValue && flatType?.metadata?.get("db.patch.strategy") === "merge") flattenPatchPayload(value, key, update, table, topLevelArrayTag);
5956
- else if (isObjectValue && meta.flattenedParents.has(key)) {
5957
- flattenPatchPayload(value, key, update, table, topLevelArrayTag);
6092
+ if (primaryKeys.includes(key)) continue;
6093
+ if (!(typeof value === "object" && value !== null && !Array.isArray(value))) {
6094
+ update[key] = value;
6095
+ continue;
6096
+ }
6097
+ const flatType = flatMap.get(key);
6098
+ if (flatType?.metadata?.get("db.__topLevelArray") && !flatType?.metadata?.has("db.json")) decomposeArrayPatch(key, value, flatType, update, table);
6099
+ else if (flatType?.metadata?.get("db.patch.strategy") === "merge") flattenPatchPayload(value, key, update, meta, table);
6100
+ else if (meta.flattenedParents.has(key)) {
6101
+ flattenPatchPayload(value, key, update, meta, table);
5958
6102
  const optLeaves = meta.optionalLeavesByLogicalParent.get(key);
5959
6103
  if (optLeaves) {
5960
6104
  for (const lp of optLeaves) if (!(lp in update)) update[lp] = null;
@@ -6037,16 +6181,18 @@ function assertNoVersionWrites(data, versionColumn) {
6037
6181
  function isVersionExemptPatch(data, table) {
6038
6182
  const meta = table.getMetadata();
6039
6183
  if (meta.versionExemptPaths.size === 0) return false;
6184
+ const primaryKeys = meta.primaryKeys;
6185
+ const flatMap = meta.flatMap;
6040
6186
  const walk = (obj, prefix) => {
6041
6187
  let result = "none";
6042
6188
  for (const [k, value] of Object.entries(obj)) {
6043
6189
  const key = prefix ? `${prefix}.${k}` : k;
6044
- if (table.primaryKeys.includes(key)) continue;
6190
+ if (primaryKeys.includes(key)) continue;
6045
6191
  if (meta.isVersionExemptPath(key)) {
6046
6192
  result = "exempt";
6047
6193
  continue;
6048
6194
  }
6049
- const flatType = table.flatMap.get(key);
6195
+ const flatType = flatMap.get(key);
6050
6196
  if (typeof value === "object" && value !== null && !Array.isArray(value) && !isDbFieldOp(value) && !flatType?.metadata?.get("db.__topLevelArray") && !flatType?.metadata?.has("db.json") && flatType?.metadata?.get("db.patch.strategy") === "merge") {
6051
6197
  const inner = walk(value, key);
6052
6198
  if (inner === "versioned") return "versioned";
@@ -6211,8 +6357,7 @@ var WriteGuardContext = class {
6211
6357
  controls: {}
6212
6358
  });
6213
6359
  const used = /* @__PURE__ */ new Set();
6214
- const out = filters.map((filter) => {
6215
- const row = rows.find((r) => rowMatchesKey(r, filter));
6360
+ const out = findRowsByKeys(rows, filters).map((row) => {
6216
6361
  if (row) used.add(row);
6217
6362
  return row ?? null;
6218
6363
  });
@@ -6346,7 +6491,8 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
6346
6491
  const depth = _depth ?? 0;
6347
6492
  const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
6348
6493
  if (!canNest && this._meta.navFields.size > 0) checkDepthOverflow(payloads, maxDepth, this._meta);
6349
- return enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
6494
+ const singleStatement = !ignore && this._isSingleStatementWrite(payloads, depth, guard, check);
6495
+ return enrichFkViolation(this._meta, () => this._inWriteTransaction(singleStatement, async () => {
6350
6496
  const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
6351
6497
  let items = payloads.map((p) => this._applyDefaults(clone(p)));
6352
6498
  if (ignore) this._linkToByKeyInIgnoreMode(items);
@@ -6419,7 +6565,8 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
6419
6565
  const depth = _depth ?? 0;
6420
6566
  const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
6421
6567
  if (!canNest && this._meta.navFields.size > 0) checkDepthOverflow(payloads, maxDepth, this._meta);
6422
- return enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
6568
+ const singleStatement = this._isSingleStatementWrite(payloads, depth, guard, check);
6569
+ return enrichFkViolation(this._meta, () => this._inWriteTransaction(singleStatement, async () => {
6423
6570
  const versionColumn = this.versionColumn;
6424
6571
  const expectedVersions = Array.from({ length: payloads.length });
6425
6572
  const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
@@ -6505,7 +6652,8 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
6505
6652
  const depth = _depth ?? 0;
6506
6653
  const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
6507
6654
  if (!canNest && this._meta.navFields.size > 0) checkDepthOverflow(payloads, maxDepth, this._meta);
6508
- return enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
6655
+ const singleStatement = this._isSingleStatementWrite(payloads, depth, guard, check);
6656
+ return enrichFkViolation(this._meta, () => this._inWriteTransaction(singleStatement, async () => {
6509
6657
  const versionColumn = this.versionColumn;
6510
6658
  const expectedVersions = Array.from({ length: payloads.length });
6511
6659
  const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
@@ -6569,13 +6717,14 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
6569
6717
  const ops = separateFieldOps(update);
6570
6718
  const translatedOps = ops ? this._fieldMapper.translateOpsKeys(ops, this._meta) : void 0;
6571
6719
  const translatedUpdate = this._fieldMapper.translatePatchKeys(update, this._meta);
6572
- if (getArrayOpsFields(translatedUpdate).size > 0) {
6720
+ if (getArrayOpsFields(translatedUpdate).size > 0) result = await this.adapter.withTransaction(async () => {
6573
6721
  const resolved = resolveArrayOps(translatedUpdate, await this.adapter.findOne({
6574
6722
  filter: translatedFilter,
6575
6723
  controls: {}
6576
6724
  }), this);
6577
- result = await this.adapter.updateOne(translatedFilter, resolved, translatedOps, expectedVersion, ...updateOpts);
6578
- } else result = await this.adapter.updateOne(translatedFilter, translatedUpdate, translatedOps, expectedVersion, ...updateOpts);
6725
+ return this.adapter.updateOne(translatedFilter, resolved, translatedOps, expectedVersion, ...updateOpts);
6726
+ });
6727
+ else result = await this.adapter.updateOne(translatedFilter, translatedUpdate, translatedOps, expectedVersion, ...updateOpts);
6579
6728
  }
6580
6729
  modifiedCount += result.modifiedCount;
6581
6730
  matched[i] = result.matchedCount > 0;
@@ -6696,7 +6845,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
6696
6845
  const candidates = this._idCandidates(id, opts);
6697
6846
  if (candidates.length === 0) return { deletedCount: 0 };
6698
6847
  const guard = opts?.guard;
6699
- const needsCascade = this._integrity.needsCascade(this._cascadeResolver);
6848
+ const needsCascade = this._integrity.needsCascade(this._cascadeResolver, this.tableName);
6700
6849
  const run = async () => {
6701
6850
  const pinned = await this._pinIdCandidates(candidates, opts?.scope);
6702
6851
  const filter = this._andScope(pinned, opts?.scope);
@@ -6749,7 +6898,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
6749
6898
  async deleteMany(filter) {
6750
6899
  this._ensureBuilt();
6751
6900
  this._guardMutationFilter(filter);
6752
- if (this._integrity.needsCascade(this._cascadeResolver)) return remapDeleteFkViolation(this.tableName, () => this.adapter.withTransaction(async () => {
6901
+ if (this._integrity.needsCascade(this._cascadeResolver, this.tableName)) return remapDeleteFkViolation(this.tableName, () => this.adapter.withTransaction(async () => {
6753
6902
  const pin = await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
6754
6903
  if (pin) {
6755
6904
  let deletedCount = 0;
@@ -6837,6 +6986,33 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
6837
6986
  };
6838
6987
  });
6839
6988
  }
6989
+ /**
6990
+ * Whether a `*One` / `bulk*` write of `payloads` is a single statement that
6991
+ * needs no wrapping transaction (since 0.1.151): one root-level row, no
6992
+ * `guard` / `check` (their reads and the write must share one snapshot),
6993
+ * no nested navigation data (multi-table phases), and FK integrity enforced
6994
+ * by the database (application-level FK checks read before writing). The
6995
+ * write then calls the adapter outside any transaction of its own — one
6996
+ * `insertMany([row])` / `replaceOne` / `updateOne` / `nativePatch`, or a
6997
+ * read-only `count` for an empty patch; the array-operator read-modify-write
6998
+ * still opens a transaction for its pair. Adapters may rely on it: a
6999
+ * single-row `insertMany` reached this way runs outside a transaction.
7000
+ */
7001
+ _isSingleStatementWrite(payloads, depth, guard, check) {
7002
+ if (payloads.length !== 1 || depth !== 0 || guard || check) return false;
7003
+ if (this._meta.foreignKeys.size > 0 && !this.adapter.supportsNativeForeignKeys()) return false;
7004
+ const payload = payloads[0];
7005
+ if (!payload || typeof payload !== "object") return false;
7006
+ for (const navField of this._meta.navFields) {
7007
+ const value = payload[navField];
7008
+ if (value !== void 0 && value !== null) return false;
7009
+ }
7010
+ return true;
7011
+ }
7012
+ /** Runs a write body inside the adapter transaction — or directly for a single-statement write. */
7013
+ _inWriteTransaction(singleStatement, fn) {
7014
+ return singleStatement ? fn() : this.adapter.withTransaction(fn);
7015
+ }
6840
7016
  /** Whether `filter` names every primary-key field (so it IS the row's exact key). */
6841
7017
  _isPkFilter(filter) {
6842
7018
  const pkFields = this.primaryKeys;
@@ -6898,11 +7074,12 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
6898
7074
  controls
6899
7075
  });
6900
7076
  const used = /* @__PURE__ */ new Set();
6901
- for (const i of pending) {
6902
- const row = rows.find((r) => rowMatchesKey(r, rowFilters[i]));
7077
+ const found = findRowsByKeys(rows, filters);
7078
+ pending.forEach((i, k) => {
7079
+ const row = found[k];
6903
7080
  targets[i] = row ?? null;
6904
7081
  if (row) used.add(row);
6905
- }
7082
+ });
6906
7083
  if (used.size < rows.length) {
6907
7084
  for (const i of pending) if (targets[i] === null) targets[i] = await this.findOne({
6908
7085
  filter: rowFilters[i],
@@ -7092,7 +7269,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7092
7269
  for (const [field, def] of this._meta.defaults.entries()) {
7093
7270
  if (field === versionField) continue;
7094
7271
  if (data[field] === void 0) {
7095
- if (def.kind === "value") data[field] = this._parseValueDefault(field, def.value);
7272
+ if (def.kind === "value") data[field] = this._valueDefault(field, def.value);
7096
7273
  else if (def.kind === "fn" && !nativeFns.has(def.fn)) switch (def.fn) {
7097
7274
  case "now":
7098
7275
  data[field] = Date.now();
@@ -7106,6 +7283,19 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7106
7283
  return data;
7107
7284
  }
7108
7285
  /**
7286
+ * Parsed `@db.default 'literal'` values that are primitives — reused for
7287
+ * every row (an object / array default is parsed per row: each row gets
7288
+ * its own instance).
7289
+ */
7290
+ _primitiveDefaults;
7291
+ _valueDefault(field, literal) {
7292
+ const cache = this._primitiveDefaults ??= /* @__PURE__ */ new Map();
7293
+ if (cache.has(field)) return cache.get(field);
7294
+ const value = this._parseValueDefault(field, literal);
7295
+ if (value === null || typeof value !== "object") cache.set(field, value);
7296
+ return value;
7297
+ }
7298
+ /**
7109
7299
  * The JS value for a `@db.default 'literal'`: strings (including unions of
7110
7300
  * string literals) are used as-is, every other design type is parsed as
7111
7301
  * JSON — the same value the SQL adapters put into the DDL `DEFAULT` clause.
@@ -7789,6 +7979,7 @@ function extractFieldSnapshots(readable, typeMapper) {
7789
7979
  jsonPath: [...f.derived.jsonPath],
7790
7980
  type: f.derived.type
7791
7981
  };
7982
+ if (f.collate && f.collate !== "binary") snap.collate = f.collate;
7792
7983
  return snap;
7793
7984
  }).toSorted((a, b) => a.physicalName.localeCompare(b.physicalName));
7794
7985
  }
@@ -1,4 +1,4 @@
1
- import { B as TDbDefaultValue, H as TDbFieldMeta, U as TDbForeignKey, _t as TExistingTableOption, bn as AtscriptDbReadable, cn as AtscriptQueryFieldRef, en as AtscriptDbView, ht as TExistingColumn, k as TColumnDiff, ln as AtscriptQueryNode, pt as TDerivedColumn, rt as TDbStorageType } from "./buckets-24Ac4sIS.cjs";
1
+ import { B as TDbDefaultValue, H as TDbFieldMeta, R as TDbCollation, U as TDbForeignKey, _t as TExistingTableOption, bn as AtscriptDbReadable, cn as AtscriptQueryFieldRef, en as AtscriptDbView, ht as TExistingColumn, k as TColumnDiff, ln as AtscriptQueryNode, pt as TDerivedColumn, rt as TDbStorageType } from "./buckets-tzd8iIM2.cjs";
2
2
 
3
3
  //#region src/schema/schema-hash.d.ts
4
4
  interface TFieldSnapshot {
@@ -21,6 +21,13 @@ interface TFieldSnapshot {
21
21
  * @since 0.1.141
22
22
  */
23
23
  derived?: Omit<TDerivedColumn, "sourcePath">;
24
+ /**
25
+ * A non-binary `@db.column.collate` — changing it changes the snapshot hash
26
+ * on every adapter (MongoDB builds the field's indexes with its collation).
27
+ * Absent for byte-wise fields, so a table without one hashes as before.
28
+ * @since 0.1.151
29
+ */
30
+ collate?: TDbCollation;
24
31
  }
25
32
  interface TIndexSnapshot {
26
33
  key: string;
@@ -1,4 +1,4 @@
1
- import { B as TDbDefaultValue, H as TDbFieldMeta, U as TDbForeignKey, _t as TExistingTableOption, bn as AtscriptDbReadable, cn as AtscriptQueryFieldRef, en as AtscriptDbView, ht as TExistingColumn, k as TColumnDiff, ln as AtscriptQueryNode, pt as TDerivedColumn, rt as TDbStorageType } from "./buckets-XOz0Bw5x.mjs";
1
+ import { B as TDbDefaultValue, H as TDbFieldMeta, R as TDbCollation, U as TDbForeignKey, _t as TExistingTableOption, bn as AtscriptDbReadable, cn as AtscriptQueryFieldRef, en as AtscriptDbView, ht as TExistingColumn, k as TColumnDiff, ln as AtscriptQueryNode, pt as TDerivedColumn, rt as TDbStorageType } from "./buckets-Cb0zmhDc.mjs";
2
2
 
3
3
  //#region src/schema/schema-hash.d.ts
4
4
  interface TFieldSnapshot {
@@ -21,6 +21,13 @@ interface TFieldSnapshot {
21
21
  * @since 0.1.141
22
22
  */
23
23
  derived?: Omit<TDerivedColumn, "sourcePath">;
24
+ /**
25
+ * A non-binary `@db.column.collate` — changing it changes the snapshot hash
26
+ * on every adapter (MongoDB builds the field's indexes with its collation).
27
+ * Absent for byte-wise fields, so a table without one hashes as before.
28
+ * @since 0.1.151
29
+ */
30
+ collate?: TDbCollation;
24
31
  }
25
32
  interface TIndexSnapshot {
26
33
  key: string;
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_db_error = require("./db-error-DRQH4sLY.cjs");
3
- const require_column_diff = require("./column-diff-CKKBVDvx.cjs");
3
+ const require_column_diff = require("./column-diff-CU7KHj9-.cjs");
4
4
  const require_aggregate_fns = require("./aggregate-fns-C-UJRobm.cjs");
5
- const require_nested_writer = require("./nested-writer-BUQvk6QT.cjs");
5
+ const require_nested_writer = require("./nested-writer-BzbOPSUp.cjs");
6
6
  const require_search_fields = require("./search-fields-DwrsJUC1.cjs");
7
7
  const require_object = require("./object-Djg28csK.cjs");
8
8
  const require_consts = require("./consts-BzRfCcH2.cjs");
@@ -236,6 +236,12 @@ var DbSpace = class {
236
236
  _readables = /* @__PURE__ */ new WeakMap();
237
237
  /** All tables created in this space — used for reverse FK lookup during cascade. */
238
238
  _allTables = /* @__PURE__ */ new Set();
239
+ /**
240
+ * Per-table-name memo of {@link _getCascadeTargets} / {@link _getFkLookupTarget}
241
+ * (since 0.1.151) — both scan every table; cleared whenever a table is added.
242
+ */
243
+ _cascadeTargets = /* @__PURE__ */ new Map();
244
+ _fkLookupTargets = /* @__PURE__ */ new Map();
239
245
  /** Every table / view handle the space created — flagged closed on {@link close}. */
240
246
  _handles = /* @__PURE__ */ new Set();
241
247
  /** Lazily created adapter for administrative ops (drop table/view) that don't need a registered readable. */
@@ -285,6 +291,8 @@ var DbSpace = class {
285
291
  return resolved instanceof require_column_diff.AtscriptDbTable ? resolved : void 0;
286
292
  });
287
293
  this._allTables.add(readable);
294
+ this._cascadeTargets.clear();
295
+ this._fkLookupTargets.clear();
288
296
  readable.setCascadeResolver((tableName) => this._getCascadeTargets(tableName));
289
297
  readable.setFkLookupResolver((tableName) => this._getFkLookupTarget(tableName));
290
298
  readable.setEncryption(this._encryption);
@@ -413,6 +421,14 @@ var DbSpace = class {
413
421
  * Accesses `table.foreignKeys` which triggers `_flatten()` if needed.
414
422
  */
415
423
  _getCascadeTargets(tableName) {
424
+ let targets = this._cascadeTargets.get(tableName);
425
+ if (targets === void 0) {
426
+ targets = this._scanCascadeTargets(tableName);
427
+ this._cascadeTargets.set(tableName, targets);
428
+ }
429
+ return targets;
430
+ }
431
+ _scanCascadeTargets(tableName) {
416
432
  const targets = [];
417
433
  for (const table of this._allTables) for (const fk of table.foreignKeys.values()) if (fk.targetTable === tableName && fk.onDelete) targets.push({
418
434
  fk,
@@ -428,6 +444,12 @@ var DbSpace = class {
428
444
  * Searches all registered tables for one with the matching table name.
429
445
  */
430
446
  _getFkLookupTarget(tableName) {
447
+ if (this._fkLookupTargets.has(tableName)) return this._fkLookupTargets.get(tableName);
448
+ const target = this._scanFkLookupTarget(tableName);
449
+ this._fkLookupTargets.set(tableName, target);
450
+ return target;
451
+ }
452
+ _scanFkLookupTarget(tableName) {
431
453
  for (const table of this._allTables) if (table.tableName === tableName) return { count: (filter) => table.count({ filter }) };
432
454
  }
433
455
  };