@atscript/db 0.1.164 → 0.1.165

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 (55) hide show
  1. package/dist/agg.cjs +1 -1
  2. package/dist/agg.d.cts +1 -1
  3. package/dist/agg.d.mts +1 -1
  4. package/dist/agg.mjs +1 -1
  5. package/dist/{aggregate-fns-DupYzFD7.cjs → aggregate-fns-CNtrSxGH.cjs} +1 -1
  6. package/dist/{aggregate-fns-DsS9UQVB.mjs → aggregate-fns-CmtSNIHm.mjs} +1 -1
  7. package/dist/{buckets-B8UOod-v.d.cts → buckets-BDxKfCNo.d.cts} +143 -13
  8. package/dist/{buckets-CHyspjYP.d.mts → buckets-CHUgky8T.d.mts} +143 -13
  9. package/dist/{column-diff-BfNIlMwX.mjs → column-diff-9pF0d8JP.mjs} +354 -31
  10. package/dist/{column-diff-BIGX3sRH.cjs → column-diff-DjeUj9kb.cjs} +371 -30
  11. package/dist/control-B5BJypqL.cjs +16 -0
  12. package/dist/control-CevYIJC4.mjs +16 -0
  13. package/dist/{db-error-hraC12B9.mjs → db-error-BMy4-OWq.mjs} +23 -1
  14. package/dist/{db-error-B_DRL1E9.d.cts → db-error-DQUw2cED.d.cts} +65 -9
  15. package/dist/{db-error-B_DRL1E9.d.mts → db-error-DQUw2cED.d.mts} +65 -9
  16. package/dist/{db-error-DmF1_xrz.cjs → db-error-G3sgvL87.cjs} +28 -0
  17. package/dist/{fk-diff-BNGawZi9.d.cts → fk-diff-DSM7O7hs.d.cts} +1 -1
  18. package/dist/{fk-diff-rj9N2QLw.d.mts → fk-diff-Dapr5Dk-.d.mts} +1 -1
  19. package/dist/index.cjs +17 -11
  20. package/dist/index.d.cts +38 -12
  21. package/dist/index.d.mts +38 -12
  22. package/dist/index.mjs +14 -12
  23. package/dist/{nested-writer-ClUX0DB0.mjs → nested-writer-BraWjS8a.mjs} +177 -26
  24. package/dist/{nested-writer-yCy6wGuN.cjs → nested-writer-DOvdbYG-.cjs} +194 -31
  25. package/dist/ops.cjs +1 -1
  26. package/dist/ops.mjs +1 -1
  27. package/dist/plugin.cjs +15 -12
  28. package/dist/plugin.mjs +15 -12
  29. package/dist/rel.cjs +2 -2
  30. package/dist/rel.d.cts +2 -2
  31. package/dist/rel.d.mts +2 -2
  32. package/dist/rel.mjs +2 -2
  33. package/dist/{relation-helpers-DloMywNs.d.cts → relation-helpers-C4UNWYQu.d.cts} +1 -1
  34. package/dist/{relation-helpers-zwt19E7r.d.mts → relation-helpers-hdbuqwjV.d.mts} +1 -1
  35. package/dist/{relation-loader-Qv3LrQ42.mjs → relation-loader-BOEVhcBe.mjs} +1 -1
  36. package/dist/{relation-loader-DujL_Mh9.cjs → relation-loader-BrkakVF_.cjs} +1 -1
  37. package/dist/shared.cjs +3 -1
  38. package/dist/shared.d.cts +13 -2
  39. package/dist/shared.d.mts +13 -2
  40. package/dist/shared.mjs +2 -2
  41. package/dist/sync.cjs +107 -25
  42. package/dist/sync.d.cts +53 -6
  43. package/dist/sync.d.mts +53 -6
  44. package/dist/sync.mjs +107 -25
  45. package/dist/{validator-DK9Ijpo7.cjs → validator-D7E7T0Oy.cjs} +1 -1
  46. package/dist/{validator-DuDWsIdQ.mjs → validator-UpgPhwvH.mjs} +1 -1
  47. package/dist/validator.cjs +2 -2
  48. package/dist/validator.d.cts +1 -1
  49. package/dist/validator.d.mts +1 -1
  50. package/dist/validator.mjs +2 -2
  51. package/dist/{view-validation-BiDLCYMw.cjs → view-validation-C0UVLx_H.cjs} +38 -10
  52. package/dist/{view-validation-BcgDq7M9.mjs → view-validation-KW77xIKn.mjs} +28 -12
  53. package/package.json +6 -6
  54. package/dist/control-D5cQmzFj.mjs +0 -14
  55. package/dist/control-jIVU2hkf.cjs +0 -14
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { a as aggregateExpressionsNotSupported, c as invalidQuery, d as spaceClosedError, l as isConflict, n as CasMismatchError, o as aggregateFailure, r as DbError, s as bucketTimeZoneUnavailable, t as CasExhaustedError, u as isRetryableDbError } from "./db-error-hraC12B9.mjs";
2
- import { $ as narrowerFilterOps, B as acceptedOperatorsHint, C as ALL_VIEW_CAPABILITIES, E as createFailureCollector, F as geoIndexNotFoundMessage, G as classifyQueryPath, H as bucketSourceVerdict, I as searchIndexNotFoundMessage, J as guardAggregate, K as collectQueryPaths, L as vectorIndexNotFoundMessage, M as NativeIntegrity, N as AtscriptDbReadable, O as fkColumns, P as resolveDesignType, Q as guardQuery, R as ADAPTER_FILTER_REASON, S as ALL_ISOLATION_LEVELS, St as NoopLogger, T as createChangeRecorder, U as canFilterLeaf, V as assertGeoPoint, W as checkHavingKeys, X as guardPath, Y as guardFilter, Z as guardPaths, _ as isVersionExemptPatch, _t as isBucketableField, at as UniquSelect, b as ApplicationIntegrity, bt as normalizeComputedSelect, ct as defaultFulltextIndex, dt as splitFulltextFields, et as sortFieldNames, ft as TableMetadata, g as AtscriptDbTable, gt as aliasTargetOf, h as isViewType, it as FieldMappingStrategy, j as IntegrityStrategy, k as fkKey, lt as describeFulltext, m as isAtscriptDbView, mt as isGeoPointType, n as isColumnTypeChanged, nt as RelationalFieldMapper, ot as numericOperandProblem, p as AtscriptDbView, pt as isGeoIndexableType, q as groupSourceVerdict, rt as DocumentFieldMapper, st as INTEGER_REGEX_OP, tt as unsupportedOperatorMessage, ut as searchTermInteger, v as assertNoVersionWrites, vt as isJsonValueField, w as BaseDbAdapter, x as ALL_BUCKET_UNITS, xt as resolveCalendarBuckets, y as decomposePatch, yt as jsonValueAncestor, z as ENCRYPTED_REASON } from "./column-diff-BfNIlMwX.mjs";
3
- import { n as ALL_AGGREGATE_FNS } from "./aggregate-fns-DsS9UQVB.mjs";
4
- import { A as hasRelationOp, C as REL_FILTER_MAX_DEPTH, D as containsRelationFilter, E as andFilters, G as evaluateExpr, H as tableNameOf, K as isFieldRef, P as relationStaticFilter, S as uniqueKeyTuple, T as ResolvedRelationFilter, j as isResolvedRelationFilter, k as forEachResolvedRelation, q as translateQueryTree, w as REL_FILTER_MAX_NODES } from "./nested-writer-ClUX0DB0.mjs";
5
- import { a as forceNavNonOptional, c as getKeyProps, o as isNavRelation, r as buildValidationContext, s as createDbValidatorPlugin, t as buildDbValidator } from "./validator-DuDWsIdQ.mjs";
1
+ import { $ as guardQuery, A as fkKey, B as ENCRYPTED_REASON, C as ALL_ISOLATION_LEVELS, Ct as resolveCalendarBuckets, D as createFailureCollector, E as createChangeRecorder, F as resolveDesignType, G as checkHavingKeys, H as assertGeoPoint, I as geoIndexNotFoundMessage, J as groupSourceVerdict, K as classifyQueryPath, L as searchIndexNotFoundMessage, M as IntegrityStrategy, N as NativeIntegrity, P as AtscriptDbReadable, Q as guardPaths, R as vectorIndexNotFoundMessage, S as ALL_BUCKET_UNITS, St as normalizeComputedSelect, T as BaseDbAdapter, Tt as nestDottedWith, U as bucketSourceVerdict, V as acceptedOperatorsHint, W as canFilterLeaf, X as guardFilter, Y as guardAggregate, Z as guardPath, _ as isVersionExemptPatch, at as FieldMappingStrategy, b as fkLookupTargetOf, bt as isJsonValueField, ct as INTEGER_REGEX_OP, dt as resolveFulltextIndex, et as narrowerFilterOps, ft as searchTermInteger, g as AtscriptDbTable, gt as isGeoPointType, h as isViewType, ht as isGeoIndexableType, it as DocumentFieldMapper, k as fkColumns, lt as defaultFulltextIndex, m as isAtscriptDbView, mt as TableMetadata, n as isColumnTypeChanged, nt as unsupportedOperatorMessage, ot as UniquSelect, p as AtscriptDbView, pt as splitFulltextFields, q as collectQueryPaths, rt as RelationalFieldMapper, st as numericOperandProblem, tt as sortFieldNames, ut as describeFulltext, v as assertNoVersionWrites, vt as aliasTargetOf, w as ALL_VIEW_CAPABILITIES, wt as NoopLogger, x as ApplicationIntegrity, xt as jsonValueAncestor, y as decomposePatch, yt as isBucketableField, z as ADAPTER_FILTER_REASON } from "./column-diff-9pF0d8JP.mjs";
2
+ import { a as aggregateExpressionsNotSupported, c as constraintViolation, d as isRetryableDbError, f as spaceClosedError, l as invalidQuery, n as CasMismatchError, o as aggregateFailure, r as DbError, s as bucketTimeZoneUnavailable, t as CasExhaustedError, u as isConflict } from "./db-error-BMy4-OWq.mjs";
3
+ import { n as ALL_AGGREGATE_FNS } from "./aggregate-fns-CmtSNIHm.mjs";
4
+ import { D as ResolvedRelationFilter, E as REL_FILTER_MAX_NODES, I as relationStaticFilter, J as isFieldRef, M as hasRelationOp, N as isResolvedRelationFilter, O as andFilters, T as REL_FILTER_MAX_DEPTH, W as tableNameOf, Y as translateQueryTree, j as forEachResolvedRelation, k as containsRelationFilter, q as evaluateExpr, w as uniqueKeyTuple } from "./nested-writer-BraWjS8a.mjs";
5
+ import { a as forceNavNonOptional, c as getKeyProps, d as soleUnionMember, o as isNavRelation, r as buildValidationContext, s as createDbValidatorPlugin, t as buildDbValidator } from "./validator-UpgPhwvH.mjs";
6
6
  import { n as isServerStampedOnUpdate, t as SERVER_STAMPED_ON_UPDATE_KEYS } from "./server-stamped-BoP56K9M.mjs";
7
7
  import { a as searchMemberKind, s as DB_ENTITY_ANNOTATIONS } from "./search-fields-BPNsxrCk.mjs";
8
8
  import { a as isPlainObject, i as isEmptyObject, n as findAncestorInSet, o as selfOrAncestor, r as getPath, s as stableJson, t as deletePath } from "./object-PQ1J8jri.mjs";
@@ -533,7 +533,7 @@ var DbSpace = class {
533
533
  return target;
534
534
  }
535
535
  _scanFkLookupTarget(tableName) {
536
- for (const table of this._allTables) if (table.tableName === tableName) return { count: (filter) => table.count({ filter }) };
536
+ for (const table of this._allTables) if (table.tableName === tableName) return fkLookupTargetOf(table);
537
537
  }
538
538
  };
539
539
  if (typeof Symbol.asyncDispose === "symbol") DbSpace.prototype[Symbol.asyncDispose] = function() {
@@ -544,12 +544,14 @@ if (typeof Symbol.asyncDispose === "symbol") DbSpace.prototype[Symbol.asyncDispo
544
544
  /**
545
545
  * Whether `value` is a compiled type a `DbSpace` accepts: an annotated type
546
546
  * whose own declaration carries `@db.table`, `@db.view` or `@db.view.for`.
547
- * A `@db.alias` type is not one, and neither is a plain `export type X = Table`
548
- * or a field typed with a table (`customer: Customer`) — since 0.1.141 the
549
- * entity annotations stay on the declaring interface instead of travelling
550
- * with every reference.
547
+ * A `@db.alias` type is not one, and neither is a field typed with a table
548
+ * (`customer: Customer`) — since 0.1.141 the entity annotations stay on the
549
+ * declaring interface instead of travelling with every reference. A plain
550
+ * `export type X = Table` is `Table` itself (@atscript/typescript 0.2), so it
551
+ * passes.
551
552
  *
552
- * Filters a module namespace before a sync:
553
+ * Filters a module namespace before a sync (a type listed under two export
554
+ * names is synced once):
553
555
  * `syncSchema(db, Object.values(models).filter(isDbEntityType))`.
554
556
  * @since 0.1.141
555
557
  */
@@ -558,4 +560,4 @@ function isDbEntityType(value) {
558
560
  return DB_ENTITY_ANNOTATIONS.some((name) => value.metadata.has(name));
559
561
  }
560
562
  //#endregion
561
- export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, ADAPTER_FILTER_REASON, ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_ISOLATION_LEVELS, ALL_VIEW_CAPABILITIES, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, BaseDbAdapter, CasExhaustedError, CasMismatchError, DEFAULT_DB_SPACE, DbEncryption, DbError, DbSpace, DocumentFieldMapper, ENCRYPTED_REASON, FieldMappingStrategy, INTEGER_REGEX_OP, IntegrityStrategy, NativeIntegrity, NoopLogger, RELATION_OPS, REL_FILTER_MAX_DEPTH, REL_FILTER_MAX_NODES, RelationalFieldMapper, ResolvedRelationFilter, SERVER_STAMPED_ON_UPDATE_KEYS, TableMetadata, UniquSelect, acceptedOperatorsHint, aggregateExpressionsNotSupported, aggregateFailure, aliasTargetOf, andFilters, assertGeoPoint, assertNoVersionWrites, bucketSourceVerdict, bucketTimeZoneUnavailable, buildDbValidator, buildValidationContext, canFilterLeaf, checkHavingKeys, classifyQueryPath, collectQueryPaths, computeInsights, containsRelationFilter, createChangeRecorder, createDbValidatorPlugin, createFailureCollector, decomposePatch, defaultFulltextIndex, deletePath, describeFulltext, evaluateExpr, findAncestorInSet, fkColumns, fkKey, forEachResolvedRelation, forceNavNonOptional, geoIndexNotFoundMessage, getDbFieldOp, getKeyProps, getPath, groupSourceVerdict, guardAggregate, guardFilter, guardPath, guardPaths, guardQuery, hasRelationOp, isAtscriptDbView, isBucketableField, isColumnTypeChanged, isConflict, isDbEntityType, isDbFieldOp, isEmptyObject, isFieldRef, isGeoIndexableType, isGeoPointType, isJsonValueField, isNavRelation, isPlainObject, isPrimitive, isRelationOp, isResolvedRelationFilter, isRetryableDbError, isServerStampedOnUpdate, isVersionExemptPatch, isViewType, jsonValueAncestor, narrowerFilterOps, normalizeComputedSelect, numericOperandProblem, queryReadColumns, reconcileCas, relationStaticFilter, resolveCalendarBuckets, resolveDesignType, searchIndexNotFoundMessage, searchMemberKind, searchTermInteger, selfOrAncestor, separateCas, separateFieldOps, sortFieldNames, splitFulltextFields, stableJson, tableNameOf, translateQueryTree, uniqueKeyTuple, unsupportedOperatorMessage, vectorIndexNotFoundMessage, walkFilter, withOptimisticRetry };
563
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, ADAPTER_FILTER_REASON, ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_ISOLATION_LEVELS, ALL_VIEW_CAPABILITIES, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, BaseDbAdapter, CasExhaustedError, CasMismatchError, DEFAULT_DB_SPACE, DbEncryption, DbError, DbSpace, DocumentFieldMapper, ENCRYPTED_REASON, FieldMappingStrategy, INTEGER_REGEX_OP, IntegrityStrategy, NativeIntegrity, NoopLogger, RELATION_OPS, REL_FILTER_MAX_DEPTH, REL_FILTER_MAX_NODES, RelationalFieldMapper, ResolvedRelationFilter, SERVER_STAMPED_ON_UPDATE_KEYS, TableMetadata, UniquSelect, acceptedOperatorsHint, aggregateExpressionsNotSupported, aggregateFailure, aliasTargetOf, andFilters, assertGeoPoint, assertNoVersionWrites, bucketSourceVerdict, bucketTimeZoneUnavailable, buildDbValidator, buildValidationContext, canFilterLeaf, checkHavingKeys, classifyQueryPath, collectQueryPaths, computeInsights, constraintViolation, containsRelationFilter, createChangeRecorder, createDbValidatorPlugin, createFailureCollector, decomposePatch, defaultFulltextIndex, deletePath, describeFulltext, evaluateExpr, findAncestorInSet, fkColumns, fkKey, forEachResolvedRelation, forceNavNonOptional, geoIndexNotFoundMessage, getDbFieldOp, getKeyProps, getPath, groupSourceVerdict, guardAggregate, guardFilter, guardPath, guardPaths, guardQuery, hasRelationOp, isAtscriptDbView, isBucketableField, isColumnTypeChanged, isConflict, isDbEntityType, isDbFieldOp, isEmptyObject, isFieldRef, isGeoIndexableType, isGeoPointType, isJsonValueField, isNavRelation, isPlainObject, isPrimitive, isRelationOp, isResolvedRelationFilter, isRetryableDbError, isServerStampedOnUpdate, isVersionExemptPatch, isViewType, jsonValueAncestor, narrowerFilterOps, nestDottedWith, normalizeComputedSelect, numericOperandProblem, queryReadColumns, reconcileCas, relationStaticFilter, resolveCalendarBuckets, resolveDesignType, resolveFulltextIndex, searchIndexNotFoundMessage, searchMemberKind, searchTermInteger, selfOrAncestor, separateCas, separateFieldOps, soleUnionMember, sortFieldNames, splitFulltextFields, stableJson, tableNameOf, translateQueryTree, uniqueKeyTuple, unsupportedOperatorMessage, vectorIndexNotFoundMessage, walkFilter, withOptimisticRetry };
@@ -1,4 +1,4 @@
1
- import { c as invalidQuery, i as DepthLimitExceededError, r as DbError } from "./db-error-hraC12B9.mjs";
1
+ import { i as DepthLimitExceededError, l as invalidQuery, r as DbError } from "./db-error-BMy4-OWq.mjs";
2
2
  import { a as isPlainObject, r as getPath } from "./object-PQ1J8jri.mjs";
3
3
  import { ValidatorError } from "@atscript/typescript/utils";
4
4
  import { isRelationOp } from "@uniqu/core";
@@ -740,10 +740,18 @@ function keyTupleString(source, fields, plain = false) {
740
740
  * get paths like `comments[0].authorId` instead of just `authorId`.
741
741
  */
742
742
  function prefixErrorPaths(errors, prefix) {
743
- return errors.map((err) => ({
744
- ...err,
745
- path: err.path ? `${prefix}.${err.path}` : prefix
746
- }));
743
+ return errors.map((err) => {
744
+ const out = {
745
+ ...err,
746
+ path: err.path ? `${prefix}.${err.path}` : prefix
747
+ };
748
+ const params = err.params;
749
+ if (params && Array.isArray(params.fields)) out.params = {
750
+ ...params,
751
+ fields: params.fields.map((f) => `${prefix}.${String(f)}`)
752
+ };
753
+ return out;
754
+ });
747
755
  }
748
756
  /**
749
757
  * Wraps an async nested operation and prefixes error paths with the nav field context.
@@ -758,41 +766,184 @@ async function wrapNestedError(navField, fn) {
758
766
  }
759
767
  }
760
768
  /**
761
- * Catches `DbError('FK_VIOLATION')` with empty paths (from adapters that
762
- * enforce FKs natively but can't report which field failed) and enriches
763
- * the error with all FK field names from table metadata.
769
+ * Rethrows a write's constraint error normalized against the table's
770
+ * metadata (see {@link normalizeWriteError}); anything else as is.
764
771
  */
765
- async function enrichFkViolation(meta, fn) {
772
+ async function normalizeWriteErrors(meta, tableName, fn) {
766
773
  try {
767
774
  return await fn();
768
775
  } catch (error) {
769
- if (error instanceof DbError && error.code === "FK_VIOLATION" && error.errors.every((err) => !err.path)) {
770
- const msg = error.errors[0]?.message ?? error.message;
771
- const errors = [];
772
- for (const [, fk] of meta.foreignKeys) for (const field of fk.fields) errors.push({
773
- path: field,
774
- message: msg
775
- });
776
- throw new DbError("FK_VIOLATION", errors.length > 0 ? errors : error.errors);
777
- }
778
- throw error;
776
+ throw normalizeWriteError(meta, tableName, error);
779
777
  }
780
778
  }
781
779
  /**
782
- * Wraps a delete operation: catches native `FK_VIOLATION` errors (e.g. SQLite
783
- * RESTRICT) and re-throws as `CONFLICT` (409) with a descriptive message.
780
+ * The table layer's constraint-error normaliser (since 0.1.165). An adapter
781
+ * reports a unique / foreign-key violation with whatever the engine told it
782
+ * ({@link constraintViolation}: columns, constraint / index name); this
783
+ * resolves that against the table's indexes and foreign keys into ONE entry
784
+ * per logical field of the violated constraint, each with a `code`
785
+ * (`UNIQUE` / `FK_MISSING` / `FK_RESTRICT`), `params` and the engine's raw
786
+ * text in `details`.
787
+ *
788
+ * An `FK_VIOLATION` with no hint and no path (an adapter that can't tell
789
+ * which key failed) lists every foreign-key field, `params.ambiguous: true`.
790
+ * Errors already normalized, or of other codes, pass through unchanged.
791
+ */
792
+ function normalizeWriteError(meta, tableName, error) {
793
+ if (!(error instanceof DbError)) return error;
794
+ const raw = rawText(error);
795
+ if (error.code === "FK_VIOLATION" && error.hint?.referencedBy) return new DbError("FK_VIOLATION", [fkRestrictEntry(tableName, {
796
+ op: "update",
797
+ referencedBy: error.hint.referencedBy,
798
+ details: raw
799
+ })]);
800
+ if (error.hint && error.code === "CONFLICT") return new DbError("CONFLICT", uniqueEntries(meta, error.hint, raw));
801
+ if (error.code === "FK_VIOLATION" && (error.hint || error.errors.every((e) => !e.path))) return new DbError("FK_VIOLATION", fkEntries(meta, error.hint, raw));
802
+ return error;
803
+ }
804
+ /**
805
+ * Wraps a delete: a native foreign-key refusal (`FK_VIOLATION`, e.g. a
806
+ * RESTRICT) is rethrown as `CONFLICT` (409) with one `FK_RESTRICT` entry —
807
+ * `params { table, referencedBy? }`, the engine text in `details`.
784
808
  */
785
809
  async function remapDeleteFkViolation(tableName, fn) {
786
810
  try {
787
811
  return await fn();
788
812
  } catch (error) {
789
- if (error instanceof DbError && error.code === "FK_VIOLATION") throw new DbError("CONFLICT", [{
790
- path: tableName,
791
- message: `Cannot delete from "${tableName}": referenced by child records (RESTRICT)`
792
- }]);
813
+ if (error instanceof DbError && error.code === "FK_VIOLATION") throw new DbError("CONFLICT", [fkRestrictEntry(tableName, {
814
+ referencedBy: error.hint?.referencedBy,
815
+ details: rawText(error)
816
+ })]);
793
817
  throw error;
794
818
  }
795
819
  }
820
+ /** The engine text an adapter error carries (its first entry's `details`, else its message). */
821
+ function rawText(error) {
822
+ return error.errors[0]?.details ?? error.errors[0]?.message ?? error.message;
823
+ }
824
+ /** Whether `names` is exactly the column `set` (in any order). */
825
+ function sameColumns(names, set) {
826
+ return names.length === set.size && names.every((n) => set.has(n));
827
+ }
828
+ /** A physical column's logical path (the column itself when unmapped). */
829
+ function logicalPath(meta, column) {
830
+ return meta.physicalToPath.get(column) ?? column;
831
+ }
832
+ /**
833
+ * The `FK_RESTRICT` entry of a write blocked by referencing rows — a delete
834
+ * (default) or an update of a referenced key. One source for the native
835
+ * refusal and the application-level RESTRICT check.
836
+ * @internal
837
+ */
838
+ function fkRestrictEntry(tableName, opts = {}) {
839
+ const { referencedBy, count, details, fields } = opts;
840
+ return {
841
+ path: tableName,
842
+ message: `Cannot ${opts.op === "update" ? "update" : "delete from"} "${tableName}": referenced by ${referencedBy === void 0 ? "child records" : `${count === void 0 ? "records" : `${count} record(s)`} in "${referencedBy}"${fields ? ` (${fields.join(", ")})` : ""}`} (RESTRICT)`,
843
+ code: "FK_RESTRICT",
844
+ params: {
845
+ table: tableName,
846
+ ...referencedBy === void 0 ? {} : { referencedBy },
847
+ ...fields ? { fields: [...fields] } : {}
848
+ },
849
+ ...details === void 0 ? {} : { details }
850
+ };
851
+ }
852
+ /**
853
+ * The `FK_MISSING` entries of one foreign key: one per field, the same
854
+ * message on each. @internal
855
+ */
856
+ function fkMissingEntries(fields, target, details, ambiguous) {
857
+ const message = fields.length === 1 ? `References a non-existent record in "${target}"` : `"${fields.join("\", \"")}" reference a non-existent record in "${target}"`;
858
+ return fields.map((path) => ({
859
+ path,
860
+ message,
861
+ code: "FK_MISSING",
862
+ params: {
863
+ fields: [...fields],
864
+ target,
865
+ ...ambiguous ? { ambiguous: true } : {}
866
+ },
867
+ ...details === void 0 ? {} : { details }
868
+ }));
869
+ }
870
+ function uniqueEntries(meta, hint, raw) {
871
+ const resolved = resolveUnique(meta, hint);
872
+ const fields = resolved?.fields ?? [];
873
+ if (fields.length === 0) return [{
874
+ path: "",
875
+ message: "Value must be unique",
876
+ code: "UNIQUE",
877
+ params: hint.name ? { index: hint.name } : {},
878
+ details: raw
879
+ }];
880
+ const message = fields.length === 1 ? "Value must be unique" : `The combination of "${fields.join("\", \"")}" must be unique`;
881
+ const params = {
882
+ ...resolved?.index ? { index: resolved.index } : {},
883
+ fields: [...fields]
884
+ };
885
+ return fields.map((path) => ({
886
+ path,
887
+ message,
888
+ code: "UNIQUE",
889
+ params,
890
+ details: raw
891
+ }));
892
+ }
893
+ /** The unique key a hint names — by name, then by its exact column set. */
894
+ function resolveUnique(meta, hint) {
895
+ const logical = (column) => logicalPath(meta, column);
896
+ if (hint.primaryKey) return {
897
+ index: "primaryKey",
898
+ fields: [...meta.primaryKeys]
899
+ };
900
+ const uniques = [...meta.indexes.values()].filter((i) => i.type === "unique");
901
+ if (hint.name) {
902
+ const named = uniques.find((i) => i.key === hint.name || i.name === hint.name);
903
+ if (named) return {
904
+ index: named.name,
905
+ fields: named.fields.map((f) => logical(f.name))
906
+ };
907
+ }
908
+ if (!hint.columns?.length) return void 0;
909
+ const columns = new Set(hint.columns);
910
+ const sameSet = (names) => sameColumns(names, columns);
911
+ const byColumns = uniques.find((i) => sameSet(i.fields.map((f) => f.name)));
912
+ if (byColumns) return {
913
+ index: byColumns.name,
914
+ fields: byColumns.fields.map((f) => logical(f.name))
915
+ };
916
+ if (meta.primaryKeys.length > 0 && sameSet(meta.primaryKeys.map((k) => meta.physicalPath(k)))) return {
917
+ index: "primaryKey",
918
+ fields: [...meta.primaryKeys]
919
+ };
920
+ return { fields: hint.columns.map(logical) };
921
+ }
922
+ function fkEntries(meta, hint, raw) {
923
+ const fks = [...meta.foreignKeys.values()];
924
+ const physical = (fk) => fk.physicalFields ?? fk.fields;
925
+ const columns = hint?.columns;
926
+ if (columns?.length) {
927
+ const set = new Set(columns);
928
+ const fk = fks.find((f) => sameColumns(physical(f), set)) ?? fks.find((f) => physical(f).includes(columns[0]));
929
+ if (fk) return fkMissingEntries(fk.fields, fk.targetTable, raw);
930
+ const fields = columns.map((c) => logicalPath(meta, c));
931
+ return fields.map((path) => ({
932
+ path,
933
+ message: "References a non-existent record",
934
+ code: "FK_MISSING",
935
+ params: { fields },
936
+ details: raw
937
+ }));
938
+ }
939
+ const all = fks.flatMap((fk) => fkMissingEntries(fk.fields, fk.targetTable, raw, fks.length > 1));
940
+ return all.length > 0 ? all : [{
941
+ path: "",
942
+ message: "References a non-existent record",
943
+ code: "FK_MISSING",
944
+ details: raw
945
+ }];
946
+ }
796
947
  //#endregion
797
948
  //#region src/rel/nested-writer.ts
798
949
  /** A TO relation's target table and the local → target foreign key. */
@@ -1581,4 +1732,4 @@ async function viaReplace(via, targets, parentPK, maxDepth, depth) {
1581
1732
  await viaLinkTargets(via, targets, parentPK, maxDepth, depth);
1582
1733
  }
1583
1734
  //#endregion
1584
- export { hasRelationOp as A, resolveRelationTargetTable as B, REL_FILTER_MAX_DEPTH as C, containsRelationFilter as D, andFilters as E, resolveRelationFilterTree as F, evaluateExpr as G, tableNameOf as H, compositeKey as I, viewExprNullable as J, isFieldRef as K, findFKEntryForRelation as L, noteRelationFilter as M, relGuardState as N, createRelationFilterHost as O, relationStaticFilter as P, findFKForRelation as R, uniqueKeyTuple as S, ResolvedRelationFilter as T, computeOf as U, slicePerGroup as V, computedOperands as W, walkViewExpr as Y, remapDeleteFkViolation as _, batchPatchNestedFrom as a, rowMatchesKey as b, batchReplaceNestedFrom as c, checkDepthOverflow as d, planNestedFromVia as f, enrichFkViolation as g, validateBatch as h, batchInsertNestedVia as i, isResolvedRelationFilter as j, forEachResolvedRelation as k, batchReplaceNestedTo as l, preValidateNestedFrom as m, batchInsertNestedFrom as n, batchPatchNestedTo as o, planPatchNestedTo as p, translateQueryTree as q, batchInsertNestedTo as r, batchPatchNestedVia as s, applyPatchNestedTo as t, batchReplaceNestedVia as u, findRowsByKeys as v, REL_FILTER_MAX_NODES as w, sameKey as x, pkTupleKey as y, findRemoteFK as z };
1735
+ export { createRelationFilterHost as A, findFKForRelation as B, sameKey as C, ResolvedRelationFilter as D, REL_FILTER_MAX_NODES as E, relGuardState as F, computeOf as G, resolveRelationTargetTable as H, relationStaticFilter as I, isFieldRef as J, computedOperands as K, resolveRelationFilterTree as L, hasRelationOp as M, isResolvedRelationFilter as N, andFilters as O, noteRelationFilter as P, compositeKey as R, rowMatchesKey as S, REL_FILTER_MAX_DEPTH as T, slicePerGroup as U, findRemoteFK as V, tableNameOf as W, viewExprNullable as X, translateQueryTree as Y, walkViewExpr as Z, fkRestrictEntry as _, batchPatchNestedFrom as a, findRowsByKeys as b, batchReplaceNestedFrom as c, checkDepthOverflow as d, planNestedFromVia as f, fkMissingEntries as g, validateBatch as h, batchInsertNestedVia as i, forEachResolvedRelation as j, containsRelationFilter as k, batchReplaceNestedTo as l, preValidateNestedFrom as m, batchInsertNestedFrom as n, batchPatchNestedTo as o, planPatchNestedTo as p, evaluateExpr as q, batchInsertNestedTo as r, batchPatchNestedVia as s, applyPatchNestedTo as t, batchReplaceNestedVia as u, normalizeWriteErrors as v, uniqueKeyTuple as w, pkTupleKey as x, remapDeleteFkViolation as y, findFKEntryForRelation as z };
@@ -1,4 +1,4 @@
1
- const require_db_error = require("./db-error-DmF1_xrz.cjs");
1
+ const require_db_error = require("./db-error-G3sgvL87.cjs");
2
2
  const require_object = require("./object-GjsuPNqo.cjs");
3
3
  let _atscript_typescript_utils = require("@atscript/typescript/utils");
4
4
  let _uniqu_core = require("@uniqu/core");
@@ -740,10 +740,18 @@ function keyTupleString(source, fields, plain = false) {
740
740
  * get paths like `comments[0].authorId` instead of just `authorId`.
741
741
  */
742
742
  function prefixErrorPaths(errors, prefix) {
743
- return errors.map((err) => ({
744
- ...err,
745
- path: err.path ? `${prefix}.${err.path}` : prefix
746
- }));
743
+ return errors.map((err) => {
744
+ const out = {
745
+ ...err,
746
+ path: err.path ? `${prefix}.${err.path}` : prefix
747
+ };
748
+ const params = err.params;
749
+ if (params && Array.isArray(params.fields)) out.params = {
750
+ ...params,
751
+ fields: params.fields.map((f) => `${prefix}.${String(f)}`)
752
+ };
753
+ return out;
754
+ });
747
755
  }
748
756
  /**
749
757
  * Wraps an async nested operation and prefixes error paths with the nav field context.
@@ -758,41 +766,184 @@ async function wrapNestedError(navField, fn) {
758
766
  }
759
767
  }
760
768
  /**
761
- * Catches `DbError('FK_VIOLATION')` with empty paths (from adapters that
762
- * enforce FKs natively but can't report which field failed) and enriches
763
- * the error with all FK field names from table metadata.
769
+ * Rethrows a write's constraint error normalized against the table's
770
+ * metadata (see {@link normalizeWriteError}); anything else as is.
764
771
  */
765
- async function enrichFkViolation(meta, fn) {
772
+ async function normalizeWriteErrors(meta, tableName, fn) {
766
773
  try {
767
774
  return await fn();
768
775
  } catch (error) {
769
- if (error instanceof require_db_error.DbError && error.code === "FK_VIOLATION" && error.errors.every((err) => !err.path)) {
770
- const msg = error.errors[0]?.message ?? error.message;
771
- const errors = [];
772
- for (const [, fk] of meta.foreignKeys) for (const field of fk.fields) errors.push({
773
- path: field,
774
- message: msg
775
- });
776
- throw new require_db_error.DbError("FK_VIOLATION", errors.length > 0 ? errors : error.errors);
777
- }
778
- throw error;
776
+ throw normalizeWriteError(meta, tableName, error);
779
777
  }
780
778
  }
781
779
  /**
782
- * Wraps a delete operation: catches native `FK_VIOLATION` errors (e.g. SQLite
783
- * RESTRICT) and re-throws as `CONFLICT` (409) with a descriptive message.
780
+ * The table layer's constraint-error normaliser (since 0.1.165). An adapter
781
+ * reports a unique / foreign-key violation with whatever the engine told it
782
+ * ({@link constraintViolation}: columns, constraint / index name); this
783
+ * resolves that against the table's indexes and foreign keys into ONE entry
784
+ * per logical field of the violated constraint, each with a `code`
785
+ * (`UNIQUE` / `FK_MISSING` / `FK_RESTRICT`), `params` and the engine's raw
786
+ * text in `details`.
787
+ *
788
+ * An `FK_VIOLATION` with no hint and no path (an adapter that can't tell
789
+ * which key failed) lists every foreign-key field, `params.ambiguous: true`.
790
+ * Errors already normalized, or of other codes, pass through unchanged.
791
+ */
792
+ function normalizeWriteError(meta, tableName, error) {
793
+ if (!(error instanceof require_db_error.DbError)) return error;
794
+ const raw = rawText(error);
795
+ if (error.code === "FK_VIOLATION" && error.hint?.referencedBy) return new require_db_error.DbError("FK_VIOLATION", [fkRestrictEntry(tableName, {
796
+ op: "update",
797
+ referencedBy: error.hint.referencedBy,
798
+ details: raw
799
+ })]);
800
+ if (error.hint && error.code === "CONFLICT") return new require_db_error.DbError("CONFLICT", uniqueEntries(meta, error.hint, raw));
801
+ if (error.code === "FK_VIOLATION" && (error.hint || error.errors.every((e) => !e.path))) return new require_db_error.DbError("FK_VIOLATION", fkEntries(meta, error.hint, raw));
802
+ return error;
803
+ }
804
+ /**
805
+ * Wraps a delete: a native foreign-key refusal (`FK_VIOLATION`, e.g. a
806
+ * RESTRICT) is rethrown as `CONFLICT` (409) with one `FK_RESTRICT` entry —
807
+ * `params { table, referencedBy? }`, the engine text in `details`.
784
808
  */
785
809
  async function remapDeleteFkViolation(tableName, fn) {
786
810
  try {
787
811
  return await fn();
788
812
  } catch (error) {
789
- if (error instanceof require_db_error.DbError && error.code === "FK_VIOLATION") throw new require_db_error.DbError("CONFLICT", [{
790
- path: tableName,
791
- message: `Cannot delete from "${tableName}": referenced by child records (RESTRICT)`
792
- }]);
813
+ if (error instanceof require_db_error.DbError && error.code === "FK_VIOLATION") throw new require_db_error.DbError("CONFLICT", [fkRestrictEntry(tableName, {
814
+ referencedBy: error.hint?.referencedBy,
815
+ details: rawText(error)
816
+ })]);
793
817
  throw error;
794
818
  }
795
819
  }
820
+ /** The engine text an adapter error carries (its first entry's `details`, else its message). */
821
+ function rawText(error) {
822
+ return error.errors[0]?.details ?? error.errors[0]?.message ?? error.message;
823
+ }
824
+ /** Whether `names` is exactly the column `set` (in any order). */
825
+ function sameColumns(names, set) {
826
+ return names.length === set.size && names.every((n) => set.has(n));
827
+ }
828
+ /** A physical column's logical path (the column itself when unmapped). */
829
+ function logicalPath(meta, column) {
830
+ return meta.physicalToPath.get(column) ?? column;
831
+ }
832
+ /**
833
+ * The `FK_RESTRICT` entry of a write blocked by referencing rows — a delete
834
+ * (default) or an update of a referenced key. One source for the native
835
+ * refusal and the application-level RESTRICT check.
836
+ * @internal
837
+ */
838
+ function fkRestrictEntry(tableName, opts = {}) {
839
+ const { referencedBy, count, details, fields } = opts;
840
+ return {
841
+ path: tableName,
842
+ message: `Cannot ${opts.op === "update" ? "update" : "delete from"} "${tableName}": referenced by ${referencedBy === void 0 ? "child records" : `${count === void 0 ? "records" : `${count} record(s)`} in "${referencedBy}"${fields ? ` (${fields.join(", ")})` : ""}`} (RESTRICT)`,
843
+ code: "FK_RESTRICT",
844
+ params: {
845
+ table: tableName,
846
+ ...referencedBy === void 0 ? {} : { referencedBy },
847
+ ...fields ? { fields: [...fields] } : {}
848
+ },
849
+ ...details === void 0 ? {} : { details }
850
+ };
851
+ }
852
+ /**
853
+ * The `FK_MISSING` entries of one foreign key: one per field, the same
854
+ * message on each. @internal
855
+ */
856
+ function fkMissingEntries(fields, target, details, ambiguous) {
857
+ const message = fields.length === 1 ? `References a non-existent record in "${target}"` : `"${fields.join("\", \"")}" reference a non-existent record in "${target}"`;
858
+ return fields.map((path) => ({
859
+ path,
860
+ message,
861
+ code: "FK_MISSING",
862
+ params: {
863
+ fields: [...fields],
864
+ target,
865
+ ...ambiguous ? { ambiguous: true } : {}
866
+ },
867
+ ...details === void 0 ? {} : { details }
868
+ }));
869
+ }
870
+ function uniqueEntries(meta, hint, raw) {
871
+ const resolved = resolveUnique(meta, hint);
872
+ const fields = resolved?.fields ?? [];
873
+ if (fields.length === 0) return [{
874
+ path: "",
875
+ message: "Value must be unique",
876
+ code: "UNIQUE",
877
+ params: hint.name ? { index: hint.name } : {},
878
+ details: raw
879
+ }];
880
+ const message = fields.length === 1 ? "Value must be unique" : `The combination of "${fields.join("\", \"")}" must be unique`;
881
+ const params = {
882
+ ...resolved?.index ? { index: resolved.index } : {},
883
+ fields: [...fields]
884
+ };
885
+ return fields.map((path) => ({
886
+ path,
887
+ message,
888
+ code: "UNIQUE",
889
+ params,
890
+ details: raw
891
+ }));
892
+ }
893
+ /** The unique key a hint names — by name, then by its exact column set. */
894
+ function resolveUnique(meta, hint) {
895
+ const logical = (column) => logicalPath(meta, column);
896
+ if (hint.primaryKey) return {
897
+ index: "primaryKey",
898
+ fields: [...meta.primaryKeys]
899
+ };
900
+ const uniques = [...meta.indexes.values()].filter((i) => i.type === "unique");
901
+ if (hint.name) {
902
+ const named = uniques.find((i) => i.key === hint.name || i.name === hint.name);
903
+ if (named) return {
904
+ index: named.name,
905
+ fields: named.fields.map((f) => logical(f.name))
906
+ };
907
+ }
908
+ if (!hint.columns?.length) return void 0;
909
+ const columns = new Set(hint.columns);
910
+ const sameSet = (names) => sameColumns(names, columns);
911
+ const byColumns = uniques.find((i) => sameSet(i.fields.map((f) => f.name)));
912
+ if (byColumns) return {
913
+ index: byColumns.name,
914
+ fields: byColumns.fields.map((f) => logical(f.name))
915
+ };
916
+ if (meta.primaryKeys.length > 0 && sameSet(meta.primaryKeys.map((k) => meta.physicalPath(k)))) return {
917
+ index: "primaryKey",
918
+ fields: [...meta.primaryKeys]
919
+ };
920
+ return { fields: hint.columns.map(logical) };
921
+ }
922
+ function fkEntries(meta, hint, raw) {
923
+ const fks = [...meta.foreignKeys.values()];
924
+ const physical = (fk) => fk.physicalFields ?? fk.fields;
925
+ const columns = hint?.columns;
926
+ if (columns?.length) {
927
+ const set = new Set(columns);
928
+ const fk = fks.find((f) => sameColumns(physical(f), set)) ?? fks.find((f) => physical(f).includes(columns[0]));
929
+ if (fk) return fkMissingEntries(fk.fields, fk.targetTable, raw);
930
+ const fields = columns.map((c) => logicalPath(meta, c));
931
+ return fields.map((path) => ({
932
+ path,
933
+ message: "References a non-existent record",
934
+ code: "FK_MISSING",
935
+ params: { fields },
936
+ details: raw
937
+ }));
938
+ }
939
+ const all = fks.flatMap((fk) => fkMissingEntries(fk.fields, fk.targetTable, raw, fks.length > 1));
940
+ return all.length > 0 ? all : [{
941
+ path: "",
942
+ message: "References a non-existent record",
943
+ code: "FK_MISSING",
944
+ details: raw
945
+ }];
946
+ }
796
947
  //#endregion
797
948
  //#region src/rel/nested-writer.ts
798
949
  /** A TO relation's target table and the local → target foreign key. */
@@ -1701,12 +1852,6 @@ Object.defineProperty(exports, "createRelationFilterHost", {
1701
1852
  return createRelationFilterHost;
1702
1853
  }
1703
1854
  });
1704
- Object.defineProperty(exports, "enrichFkViolation", {
1705
- enumerable: true,
1706
- get: function() {
1707
- return enrichFkViolation;
1708
- }
1709
- });
1710
1855
  Object.defineProperty(exports, "evaluateExpr", {
1711
1856
  enumerable: true,
1712
1857
  get: function() {
@@ -1737,6 +1882,18 @@ Object.defineProperty(exports, "findRowsByKeys", {
1737
1882
  return findRowsByKeys;
1738
1883
  }
1739
1884
  });
1885
+ Object.defineProperty(exports, "fkMissingEntries", {
1886
+ enumerable: true,
1887
+ get: function() {
1888
+ return fkMissingEntries;
1889
+ }
1890
+ });
1891
+ Object.defineProperty(exports, "fkRestrictEntry", {
1892
+ enumerable: true,
1893
+ get: function() {
1894
+ return fkRestrictEntry;
1895
+ }
1896
+ });
1740
1897
  Object.defineProperty(exports, "forEachResolvedRelation", {
1741
1898
  enumerable: true,
1742
1899
  get: function() {
@@ -1761,6 +1918,12 @@ Object.defineProperty(exports, "isResolvedRelationFilter", {
1761
1918
  return isResolvedRelationFilter;
1762
1919
  }
1763
1920
  });
1921
+ Object.defineProperty(exports, "normalizeWriteErrors", {
1922
+ enumerable: true,
1923
+ get: function() {
1924
+ return normalizeWriteErrors;
1925
+ }
1926
+ });
1764
1927
  Object.defineProperty(exports, "noteRelationFilter", {
1765
1928
  enumerable: true,
1766
1929
  get: function() {
package/dist/ops.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_db_error = require("./db-error-DmF1_xrz.cjs");
2
+ const require_db_error = require("./db-error-G3sgvL87.cjs");
3
3
  //#region src/ops.ts
4
4
  /** Increment a numeric field by `value` (default 1). */
5
5
  function $inc(value = 1) {
package/dist/ops.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as invalidQuery, r as DbError } from "./db-error-hraC12B9.mjs";
1
+ import { l as invalidQuery, r as DbError } from "./db-error-BMy4-OWq.mjs";
2
2
  //#region src/ops.ts
3
3
  /** Increment a numeric field by `value` (default 1). */
4
4
  function $inc(value = 1) {