@atscript/db 0.1.152 → 0.1.153

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 (45) 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-CyaZyb9I.mjs → aggregate-fns-B_sgl4mB.mjs} +1 -1
  6. package/dist/{aggregate-fns-C-UJRobm.cjs → aggregate-fns-DFtKAv7Q.cjs} +1 -1
  7. package/dist/{buckets-Cb0zmhDc.d.mts → buckets-CBKL_ZWE.d.mts} +205 -7
  8. package/dist/{buckets-tzd8iIM2.d.cts → buckets-jy8BfswO.d.cts} +205 -7
  9. package/dist/{column-diff-CU7KHj9-.cjs → column-diff-CbdWi_uZ.cjs} +729 -28
  10. package/dist/{column-diff-CnyLjbQ8.mjs → column-diff-DrE4ho_m.mjs} +730 -29
  11. package/dist/{db-error-DRQH4sLY.cjs → db-error-JcfJqqpk.cjs} +29 -0
  12. package/dist/{db-error-Az85UhTX.mjs → db-error-eSsQX7rt.mjs} +24 -1
  13. package/dist/{fk-diff-Bw26lwSc.d.cts → fk-diff-BIJ8l_hF.d.mts} +1 -1
  14. package/dist/{fk-diff-lcAnwvm9.d.mts → fk-diff-D1mrLMjU.d.cts} +1 -1
  15. package/dist/index.cjs +93 -5
  16. package/dist/index.d.cts +29 -5
  17. package/dist/index.d.mts +29 -5
  18. package/dist/index.mjs +92 -6
  19. package/dist/{nested-writer-cvbVAkNo.mjs → nested-writer-BOj_c_lI.mjs} +1 -1
  20. package/dist/{nested-writer-BzbOPSUp.cjs → nested-writer-Bl1SZS7x.cjs} +1 -1
  21. package/dist/ops.cjs +1 -1
  22. package/dist/ops.mjs +1 -1
  23. package/dist/plugin.cjs +48 -1
  24. package/dist/plugin.mjs +48 -1
  25. package/dist/rel.cjs +2 -2
  26. package/dist/rel.d.cts +2 -2
  27. package/dist/rel.d.mts +2 -2
  28. package/dist/rel.mjs +2 -2
  29. package/dist/{relation-helpers-Bdo4NHPC.d.mts → relation-helpers-BUsxNNGU.d.mts} +1 -1
  30. package/dist/{relation-helpers-DxIZKvnJ.d.cts → relation-helpers-D_8bydc2.d.cts} +1 -1
  31. package/dist/{relation-loader-DYab07lw.mjs → relation-loader-CNBMPfRr.mjs} +2 -1
  32. package/dist/{relation-loader-fNjHgBMs.cjs → relation-loader-DeulLrAw.cjs} +2 -1
  33. package/dist/sync.cjs +2 -2
  34. package/dist/sync.d.cts +2 -2
  35. package/dist/sync.d.mts +2 -2
  36. package/dist/sync.mjs +2 -2
  37. package/dist/{validator-tBNvM1qc.d.cts → validator-B3xlbrWf.d.cts} +36 -2
  38. package/dist/{validator-tBNvM1qc.d.mts → validator-B3xlbrWf.d.mts} +36 -2
  39. package/dist/{validator-BTiIOTKP.mjs → validator-BACewoRj.mjs} +51 -7
  40. package/dist/{validator-UcuJxHNT.cjs → validator-BxWwh3wf.cjs} +50 -6
  41. package/dist/validator.cjs +2 -2
  42. package/dist/validator.d.cts +1 -1
  43. package/dist/validator.d.mts +1 -1
  44. package/dist/validator.mjs +2 -2
  45. package/package.json +8 -8
package/dist/plugin.cjs CHANGED
@@ -33,7 +33,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
33
33
  enumerable: true
34
34
  }) : target, mod));
35
35
  //#endregion
36
- const require_aggregate_fns = require("./aggregate-fns-C-UJRobm.cjs");
36
+ const require_aggregate_fns = require("./aggregate-fns-DFtKAv7Q.cjs");
37
37
  const require_search_fields = require("./search-fields-DwrsJUC1.cjs");
38
38
  require("./consts-BzRfCcH2.cjs");
39
39
  const require_validation_utils = require("./validation-utils-debIYpq0.cjs");
@@ -1695,6 +1695,52 @@ const dbSearchAnnotations = { search: {
1695
1695
  })
1696
1696
  } };
1697
1697
  //#endregion
1698
+ //#region src/plugin/annotations/sort.ts
1699
+ /**
1700
+ * `@db.sort.nulls` (since 0.1.153) — a field's default NULL placement when a
1701
+ * read sorts by it without a `$nulls` entry for it.
1702
+ */
1703
+ const dbSortAnnotations = { sort: { nulls: new _atscript_core.AnnotationSpec({
1704
+ description: "Default **NULL placement** when a read sorts by this field and the request names none (`$nulls` control, URL suffix `$sort=-closedAt:last`).\n\n- **`'first'`** — rows where the field is NULL (or missing) come before every value, in both sort directions\n- **`'last'`** — they come after every value, in both directions\n\nWithout it (and without `$nulls`) NULL placement is the database's own: first in ascending order on SQLite, MySQL, MongoDB and the in-memory adapter, last on PostgreSQL. Applies to `$sort` and to the `$rowOrder` of `first()` / `last()`. Only optional fields can be NULL — on a required table field it has no effect. On PostgreSQL, MySQL and MongoDB a placement the engine does not produce natively costs the index its ORDER BY (a sort step instead).\n\n**Example:**\n```atscript\n@db.sort.nulls 'last'\nclosedAt?: number.timestamp\n```\n",
1705
+ nodeType: ["prop"],
1706
+ passedWhenReferred: false,
1707
+ multiple: false,
1708
+ argument: {
1709
+ name: "placement",
1710
+ type: "string",
1711
+ values: ["first", "last"],
1712
+ description: "Where NULL goes: 'first' (before every value) or 'last' (after every value), whatever the sort direction."
1713
+ },
1714
+ validate(token, _args, doc) {
1715
+ const errors = [];
1716
+ const field = token.parentNode;
1717
+ const fail = (message, severity = 1) => errors.push({
1718
+ message,
1719
+ severity,
1720
+ range: token.range
1721
+ });
1722
+ if (field.countAnnotations("db.json") > 0) {
1723
+ fail("@db.sort.nulls on a @db.json field — JSON-stored fields are not sortable");
1724
+ return errors;
1725
+ }
1726
+ if (field.countAnnotations("db.encrypted") > 0) {
1727
+ fail("@db.sort.nulls on a @db.encrypted field — encrypted fields are not sortable");
1728
+ return errors;
1729
+ }
1730
+ const def = field.getDefinition();
1731
+ let leaf = def;
1732
+ if (def && (0, _atscript_core.isRef)(def)) leaf = doc.unwindType(def.id, def.chain)?.def;
1733
+ const base = require_validation_utils.primitiveBaseType(leaf);
1734
+ if (def && ((0, _atscript_core.isArray)(def) || (0, _atscript_core.isStructure)(def)) || leaf && ((0, _atscript_core.isArray)(leaf) || (0, _atscript_core.isStructure)(leaf) || (0, _atscript_core.isInterface)(leaf)) || leaf && (0, _atscript_core.isPrimitive)(leaf) && (base === "array" || base === "object")) {
1735
+ fail("@db.sort.nulls needs a sortable scalar field — not an object or an array");
1736
+ return errors;
1737
+ }
1738
+ const owner = require_validation_utils.getDbTableOwner(token);
1739
+ if (!field.has("optional") && owner && owner.countAnnotations("db.table") > 0 && field.ownerNode?.ownerNode === owner) fail("@db.sort.nulls on a required field has no effect — the field is never NULL; make it optional (field?: …) or remove the annotation", 2);
1740
+ return errors;
1741
+ }
1742
+ }) } };
1743
+ //#endregion
1698
1744
  //#region src/plugin/annotations/table.ts
1699
1745
  const dbTableAnnotations = {
1700
1746
  table: {
@@ -2286,6 +2332,7 @@ const dbPlugin = (options) => ({
2286
2332
  agg: dbAggAnnotations.agg,
2287
2333
  compute: dbComputeAnnotations.compute,
2288
2334
  search: dbSearchAnnotations.search,
2335
+ sort: dbSortAnnotations.sort,
2289
2336
  amount: dbAmountAnnotations.amount,
2290
2337
  unit: dbUnitAnnotations.unit
2291
2338
  } },
package/dist/plugin.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as NULL_WHEN_EMPTY_AGGREGATE_FNS, o as SUPPORTED_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-CyaZyb9I.mjs";
1
+ import { i as NULL_WHEN_EMPTY_AGGREGATE_FNS, o as SUPPORTED_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-B_sgl4mB.mjs";
2
2
  import { a as DERIVED_INCOMPATIBLE, i as DB_ENTITY_ANNOTATIONS, o as JSON_LEAF_TYPES, r as numericTypeProblem } from "./search-fields-DBIZKwyb.mjs";
3
3
  import "./consts-C_-5_pFq.mjs";
4
4
  import { C as validateFieldBaseType, S as validateExclusiveWith, _ as getParentTypeName, a as hasAnyViewAnnotation, b as refActionAnnotation, d as viewJoins, f as viewScopeTypes, g as getParentStruct, h as getNavTargetTypeName, i as forEachFieldRef, l as validateQueryScope, m as getDbTableOwner, n as findFKFieldsPointingTo, o as isAliasDecl, p as getAnnotationAlias, r as findViewCycle, s as isDbSourceDecl, t as earlierJoinTargets, u as validateRefArgument, v as integerMemberIndexProblem, w as validateSiblingStringField, x as searchFieldVerdict, y as primitiveBaseType } from "./validation-utils-DqZAH6aT.mjs";
@@ -1659,6 +1659,52 @@ const dbSearchAnnotations = { search: {
1659
1659
  })
1660
1660
  } };
1661
1661
  //#endregion
1662
+ //#region src/plugin/annotations/sort.ts
1663
+ /**
1664
+ * `@db.sort.nulls` (since 0.1.153) — a field's default NULL placement when a
1665
+ * read sorts by it without a `$nulls` entry for it.
1666
+ */
1667
+ const dbSortAnnotations = { sort: { nulls: new AnnotationSpec({
1668
+ description: "Default **NULL placement** when a read sorts by this field and the request names none (`$nulls` control, URL suffix `$sort=-closedAt:last`).\n\n- **`'first'`** — rows where the field is NULL (or missing) come before every value, in both sort directions\n- **`'last'`** — they come after every value, in both directions\n\nWithout it (and without `$nulls`) NULL placement is the database's own: first in ascending order on SQLite, MySQL, MongoDB and the in-memory adapter, last on PostgreSQL. Applies to `$sort` and to the `$rowOrder` of `first()` / `last()`. Only optional fields can be NULL — on a required table field it has no effect. On PostgreSQL, MySQL and MongoDB a placement the engine does not produce natively costs the index its ORDER BY (a sort step instead).\n\n**Example:**\n```atscript\n@db.sort.nulls 'last'\nclosedAt?: number.timestamp\n```\n",
1669
+ nodeType: ["prop"],
1670
+ passedWhenReferred: false,
1671
+ multiple: false,
1672
+ argument: {
1673
+ name: "placement",
1674
+ type: "string",
1675
+ values: ["first", "last"],
1676
+ description: "Where NULL goes: 'first' (before every value) or 'last' (after every value), whatever the sort direction."
1677
+ },
1678
+ validate(token, _args, doc) {
1679
+ const errors = [];
1680
+ const field = token.parentNode;
1681
+ const fail = (message, severity = 1) => errors.push({
1682
+ message,
1683
+ severity,
1684
+ range: token.range
1685
+ });
1686
+ if (field.countAnnotations("db.json") > 0) {
1687
+ fail("@db.sort.nulls on a @db.json field — JSON-stored fields are not sortable");
1688
+ return errors;
1689
+ }
1690
+ if (field.countAnnotations("db.encrypted") > 0) {
1691
+ fail("@db.sort.nulls on a @db.encrypted field — encrypted fields are not sortable");
1692
+ return errors;
1693
+ }
1694
+ const def = field.getDefinition();
1695
+ let leaf = def;
1696
+ if (def && isRef(def)) leaf = doc.unwindType(def.id, def.chain)?.def;
1697
+ const base = primitiveBaseType(leaf);
1698
+ if (def && (isArray(def) || isStructure(def)) || leaf && (isArray(leaf) || isStructure(leaf) || isInterface(leaf)) || leaf && isPrimitive(leaf) && (base === "array" || base === "object")) {
1699
+ fail("@db.sort.nulls needs a sortable scalar field — not an object or an array");
1700
+ return errors;
1701
+ }
1702
+ const owner = getDbTableOwner(token);
1703
+ if (!field.has("optional") && owner && owner.countAnnotations("db.table") > 0 && field.ownerNode?.ownerNode === owner) fail("@db.sort.nulls on a required field has no effect — the field is never NULL; make it optional (field?: …) or remove the annotation", 2);
1704
+ return errors;
1705
+ }
1706
+ }) } };
1707
+ //#endregion
1662
1708
  //#region src/plugin/annotations/table.ts
1663
1709
  const dbTableAnnotations = {
1664
1710
  table: {
@@ -2250,6 +2296,7 @@ const dbPlugin = (options) => ({
2250
2296
  agg: dbAggAnnotations.agg,
2251
2297
  compute: dbComputeAnnotations.compute,
2252
2298
  search: dbSearchAnnotations.search,
2299
+ sort: dbSortAnnotations.sort,
2253
2300
  amount: dbAmountAnnotations.amount,
2254
2301
  unit: dbUnitAnnotations.unit
2255
2302
  } },
package/dist/rel.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_nested_writer = require("./nested-writer-BzbOPSUp.cjs");
3
- const require_relation_loader = require("./relation-loader-fNjHgBMs.cjs");
2
+ const require_nested_writer = require("./nested-writer-Bl1SZS7x.cjs");
3
+ const require_relation_loader = require("./relation-loader-DeulLrAw.cjs");
4
4
  exports.applyPatchNestedTo = require_nested_writer.applyPatchNestedTo;
5
5
  exports.batchInsertNestedFrom = require_nested_writer.batchInsertNestedFrom;
6
6
  exports.batchInsertNestedTo = require_nested_writer.batchInsertNestedTo;
package/dist/rel.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { Hn as TGenericLogger, Pt as TTableResolver, U as TDbForeignKey, Wt as TableMetadata, Yt as BaseDbAdapter, et as TDbRelation, h as AtscriptDbWritable, m as AtscriptDbTableLike, zt as TWriteTableResolver } from "./buckets-tzd8iIM2.cjs";
1
+ import { Gn as TGenericLogger, It as TTableResolver, Kt as TableMetadata, Vt as TWriteTableResolver, W as TDbForeignKey, Zt as BaseDbAdapter, h as AtscriptDbWritable, m as AtscriptDbTableLike, nt as TDbRelation } from "./buckets-jy8BfswO.cjs";
2
2
  import { t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.cjs";
3
- import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-DxIZKvnJ.cjs";
3
+ import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-D_8bydc2.cjs";
4
4
  import { FilterExpr, WithRelation } from "@uniqu/core";
5
5
  import { Validator } from "@atscript/typescript/utils";
6
6
 
package/dist/rel.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { Hn as TGenericLogger, Pt as TTableResolver, U as TDbForeignKey, Wt as TableMetadata, Yt as BaseDbAdapter, et as TDbRelation, h as AtscriptDbWritable, m as AtscriptDbTableLike, zt as TWriteTableResolver } from "./buckets-Cb0zmhDc.mjs";
1
+ import { Gn as TGenericLogger, It as TTableResolver, Kt as TableMetadata, Vt as TWriteTableResolver, W as TDbForeignKey, Zt as BaseDbAdapter, h as AtscriptDbWritable, m as AtscriptDbTableLike, nt as TDbRelation } from "./buckets-CBKL_ZWE.mjs";
2
2
  import { t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.mjs";
3
- import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-Bdo4NHPC.mjs";
3
+ import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-BUsxNNGU.mjs";
4
4
  import { Validator } from "@atscript/typescript/utils";
5
5
  import { FilterExpr, WithRelation } from "@uniqu/core";
6
6
 
package/dist/rel.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { t as loadRelationsImpl } from "./relation-loader-DYab07lw.mjs";
2
- import { B as resolveRelationTargetTable, R as findFKForRelation, a as batchPatchNestedFrom, c as batchReplaceNestedFrom, d as checkDepthOverflow, f as planNestedFromVia, h as validateBatch, i as batchInsertNestedVia, l as batchReplaceNestedTo, m as preValidateNestedFrom, n as batchInsertNestedFrom, o as batchPatchNestedTo, p as planPatchNestedTo, r as batchInsertNestedTo, s as batchPatchNestedVia, t as applyPatchNestedTo, u as batchReplaceNestedVia, z as findRemoteFK } from "./nested-writer-cvbVAkNo.mjs";
1
+ import { t as loadRelationsImpl } from "./relation-loader-CNBMPfRr.mjs";
2
+ import { B as resolveRelationTargetTable, R as findFKForRelation, a as batchPatchNestedFrom, c as batchReplaceNestedFrom, d as checkDepthOverflow, f as planNestedFromVia, h as validateBatch, i as batchInsertNestedVia, l as batchReplaceNestedTo, m as preValidateNestedFrom, n as batchInsertNestedFrom, o as batchPatchNestedTo, p as planPatchNestedTo, r as batchInsertNestedTo, s as batchPatchNestedVia, t as applyPatchNestedTo, u as batchReplaceNestedVia, z as findRemoteFK } from "./nested-writer-BOj_c_lI.mjs";
3
3
  export { applyPatchNestedTo, batchInsertNestedFrom, batchInsertNestedTo, batchInsertNestedVia, batchPatchNestedFrom, batchPatchNestedTo, batchPatchNestedVia, batchReplaceNestedFrom, batchReplaceNestedTo, batchReplaceNestedVia, checkDepthOverflow, findFKForRelation, findRemoteFK, loadRelationsImpl, planNestedFromVia, planPatchNestedTo, preValidateNestedFrom, resolveRelationTargetTable, validateBatch };
@@ -1,4 +1,4 @@
1
- import { U as TDbForeignKey, et as TDbRelation } from "./buckets-Cb0zmhDc.mjs";
1
+ import { W as TDbForeignKey, nt as TDbRelation } from "./buckets-CBKL_ZWE.mjs";
2
2
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
3
3
 
4
4
  //#region src/rel/relation-helpers.d.ts
@@ -1,4 +1,4 @@
1
- import { U as TDbForeignKey, et as TDbRelation } from "./buckets-tzd8iIM2.cjs";
1
+ import { W as TDbForeignKey, nt as TDbRelation } from "./buckets-jy8BfswO.cjs";
2
2
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
3
3
 
4
4
  //#region src/rel/relation-helpers.d.ts
@@ -1,4 +1,4 @@
1
- import { B as resolveRelationTargetTable, E as andFilters, I as compositeKey, P as relationStaticFilter, R as findFKForRelation, V as slicePerGroup, z as findRemoteFK } from "./nested-writer-cvbVAkNo.mjs";
1
+ import { B as resolveRelationTargetTable, E as andFilters, I as compositeKey, P as relationStaticFilter, R as findFKForRelation, V as slicePerGroup, z as findRemoteFK } from "./nested-writer-BOj_c_lI.mjs";
2
2
  //#region \0rolldown/runtime.js
3
3
  var __defProp = Object.defineProperty;
4
4
  var __exportAll = (all, no_symbols) => {
@@ -42,6 +42,7 @@ async function loadRelationsImpl(rows, withRelations, host) {
42
42
  const flatRel = withRel;
43
43
  const controls = { ...withRel.controls || {} };
44
44
  if (flatRel.$sort && !controls.$sort) controls.$sort = flatRel.$sort;
45
+ if (flatRel.$nulls && !controls.$nulls) controls.$nulls = flatRel.$nulls;
45
46
  if (flatRel.$limit !== null && flatRel.$limit !== void 0 && (controls.$limit === null || controls.$limit === void 0)) controls.$limit = flatRel.$limit;
46
47
  if (flatRel.$skip !== null && flatRel.$skip !== void 0 && (controls.$skip === null || controls.$skip === void 0)) controls.$skip = flatRel.$skip;
47
48
  if (flatRel.$select && !controls.$select) controls.$select = flatRel.$select;
@@ -1,4 +1,4 @@
1
- const require_nested_writer = require("./nested-writer-BzbOPSUp.cjs");
1
+ const require_nested_writer = require("./nested-writer-Bl1SZS7x.cjs");
2
2
  //#region src/rel/relation-loader.ts
3
3
  var relation_loader_exports = /* @__PURE__ */ require("./plugin.cjs").__exportAll({
4
4
  ensureSelectIncludesFields: () => ensureSelectIncludesFields,
@@ -30,6 +30,7 @@ async function loadRelationsImpl(rows, withRelations, host) {
30
30
  const flatRel = withRel;
31
31
  const controls = { ...withRel.controls || {} };
32
32
  if (flatRel.$sort && !controls.$sort) controls.$sort = flatRel.$sort;
33
+ if (flatRel.$nulls && !controls.$nulls) controls.$nulls = flatRel.$nulls;
33
34
  if (flatRel.$limit !== null && flatRel.$limit !== void 0 && (controls.$limit === null || controls.$limit === void 0)) controls.$limit = flatRel.$limit;
34
35
  if (flatRel.$skip !== null && flatRel.$skip !== void 0 && (controls.$skip === null || controls.$skip === void 0)) controls.$skip = flatRel.$skip;
35
36
  if (flatRel.$select && !controls.$select) controls.$select = flatRel.$select;
package/dist/sync.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_db_error = require("./db-error-DRQH4sLY.cjs");
3
- const require_column_diff = require("./column-diff-CU7KHj9-.cjs");
2
+ const require_db_error = require("./db-error-JcfJqqpk.cjs");
3
+ const require_column_diff = require("./column-diff-CbdWi_uZ.cjs");
4
4
  //#region src/schema/table-option-diff.ts
5
5
  /**
6
6
  * Computes the difference between desired and existing table options.
package/dist/sync.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { Dt as TPrimaryKeyChange, H as TDbFieldMeta, Hn as TGenericLogger, Nt as TTableOptionDiff, Ot as TReferencingForeignKey, Q as TDbObjectKind, Zt as DbSpace, _t as TExistingTableOption, ft as TDerivedChangeReason, gt as TExistingForeignKey, mt as TEnsureTableOptions } from "./buckets-tzd8iIM2.cjs";
2
- import { _ as computeTableSnapshot, a as hasForeignKeyChanges, b as snapshotToExistingTableOptions, c as TFieldSnapshot, d as TViewColumnSnapshot, f as TViewJoinSnapshot, g as computeTableHash, h as computeSchemaHash, i as fkKey, l as TForeignKeySnapshot, m as canonicalizeQueryNode, n as computeForeignKeyDiff, o as computeColumnDiff, p as TViewSnapshot, t as TForeignKeyDiff, u as TTableSnapshot, v as computeViewSnapshot, y as snapshotToExistingColumns } from "./fk-diff-Bw26lwSc.cjs";
1
+ import { $t as DbSpace, At as TReferencingForeignKey, Ft as TTableOptionDiff, Gn as TGenericLogger, U as TDbFieldMeta, et as TDbObjectKind, gt as TEnsureTableOptions, kt as TPrimaryKeyChange, mt as TDerivedChangeReason, vt as TExistingForeignKey, yt as TExistingTableOption } from "./buckets-jy8BfswO.cjs";
2
+ import { _ as computeTableSnapshot, a as hasForeignKeyChanges, b as snapshotToExistingTableOptions, c as TFieldSnapshot, d as TViewColumnSnapshot, f as TViewJoinSnapshot, g as computeTableHash, h as computeSchemaHash, i as fkKey, l as TForeignKeySnapshot, m as canonicalizeQueryNode, n as computeForeignKeyDiff, o as computeColumnDiff, p as TViewSnapshot, t as TForeignKeyDiff, u as TTableSnapshot, v as computeViewSnapshot, y as snapshotToExistingColumns } from "./fk-diff-D1mrLMjU.cjs";
3
3
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/schema/sync-entry.d.ts
package/dist/sync.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { Dt as TPrimaryKeyChange, H as TDbFieldMeta, Hn as TGenericLogger, Nt as TTableOptionDiff, Ot as TReferencingForeignKey, Q as TDbObjectKind, Zt as DbSpace, _t as TExistingTableOption, ft as TDerivedChangeReason, gt as TExistingForeignKey, mt as TEnsureTableOptions } from "./buckets-Cb0zmhDc.mjs";
2
- import { _ as computeTableSnapshot, a as hasForeignKeyChanges, b as snapshotToExistingTableOptions, c as TFieldSnapshot, d as TViewColumnSnapshot, f as TViewJoinSnapshot, g as computeTableHash, h as computeSchemaHash, i as fkKey, l as TForeignKeySnapshot, m as canonicalizeQueryNode, n as computeForeignKeyDiff, o as computeColumnDiff, p as TViewSnapshot, t as TForeignKeyDiff, u as TTableSnapshot, v as computeViewSnapshot, y as snapshotToExistingColumns } from "./fk-diff-lcAnwvm9.mjs";
1
+ import { $t as DbSpace, At as TReferencingForeignKey, Ft as TTableOptionDiff, Gn as TGenericLogger, U as TDbFieldMeta, et as TDbObjectKind, gt as TEnsureTableOptions, kt as TPrimaryKeyChange, mt as TDerivedChangeReason, vt as TExistingForeignKey, yt as TExistingTableOption } from "./buckets-CBKL_ZWE.mjs";
2
+ import { _ as computeTableSnapshot, a as hasForeignKeyChanges, b as snapshotToExistingTableOptions, c as TFieldSnapshot, d as TViewColumnSnapshot, f as TViewJoinSnapshot, g as computeTableHash, h as computeSchemaHash, i as fkKey, l as TForeignKeySnapshot, m as canonicalizeQueryNode, n as computeForeignKeyDiff, o as computeColumnDiff, p as TViewSnapshot, t as TForeignKeyDiff, u as TTableSnapshot, v as computeViewSnapshot, y as snapshotToExistingColumns } from "./fk-diff-BIJ8l_hF.mjs";
3
3
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/schema/sync-entry.d.ts
package/dist/sync.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { r as DbError } from "./db-error-Az85UhTX.mjs";
2
- import { _t as NoopLogger, a as computeTableHash, c as snapshotToExistingColumns, d as computeForeignKeyDiff, dt as aliasTargetOf, f as fkColumns, i as computeSchemaHash, l as snapshotToExistingTableOptions, m as hasForeignKeyChanges, o as computeTableSnapshot, p as fkKey, r as canonicalizeQueryNode, s as computeViewSnapshot, t as computeColumnDiff, u as viewSnapshotSources } from "./column-diff-CnyLjbQ8.mjs";
1
+ import { r as DbError } from "./db-error-eSsQX7rt.mjs";
2
+ import { _t as NoopLogger, a as computeTableHash, c as snapshotToExistingColumns, d as computeForeignKeyDiff, dt as aliasTargetOf, f as fkColumns, i as computeSchemaHash, l as snapshotToExistingTableOptions, m as hasForeignKeyChanges, o as computeTableSnapshot, p as fkKey, r as canonicalizeQueryNode, s as computeViewSnapshot, t as computeColumnDiff, u as viewSnapshotSources } from "./column-diff-DrE4ho_m.mjs";
3
3
  //#region src/schema/table-option-diff.ts
4
4
  /**
5
5
  * Computes the difference between desired and existing table options.
@@ -14,7 +14,28 @@ type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" |
14
14
  * table lives on a different adapter, or the table was built without a
15
15
  * `DbSpace` (moost-db: 400). @since 0.1.147
16
16
  */
17
- | "REL_FILTER_NOT_SUPPORTED" /** The `DbSpace` was closed; the table/view handle can no longer be used (moost-db: 503). @since 0.1.148 */ | "SPACE_CLOSED" /** The adapter does not implement `insertManyIgnore` (moost-db: 400). @since 0.1.148 */ | "ON_CONFLICT_NOT_SUPPORTED" | "TX_WAIT_TIMEOUT";
17
+ | "REL_FILTER_NOT_SUPPORTED" /** The `DbSpace` was closed; the table/view handle can no longer be used (moost-db: 503). @since 0.1.148 */ | "SPACE_CLOSED" /** The adapter does not implement `insertManyIgnore` (moost-db: 400). @since 0.1.148 */ | "ON_CONFLICT_NOT_SUPPORTED" | "TX_WAIT_TIMEOUT"
18
+ /**
19
+ * The engine chose this transaction as a deadlock victim (MySQL errno 1213,
20
+ * PostgreSQL 40P01) and rolled it back. Retry the WHOLE transaction.
21
+ * `retryable` is `true`. @since 0.1.153
22
+ */
23
+ | "DEADLOCK"
24
+ /**
25
+ * A row lock was not granted in time (MySQL errno 1205
26
+ * `innodb_lock_wait_timeout`, PostgreSQL 55P03 `lock_timeout` / `NOWAIT`).
27
+ * The statement failed; MySQL keeps the transaction open unless
28
+ * `innodb_rollback_on_timeout` is set, PostgreSQL aborts it — retry the
29
+ * whole transaction. `retryable` is `true`. @since 0.1.153
30
+ */
31
+ | "LOCK_TIMEOUT"
32
+ /**
33
+ * PostgreSQL could not serialize the transaction (`40001`: under
34
+ * `REPEATABLE READ` / `SERIALIZABLE`, e.g. a row locked or updated after it
35
+ * changed since the snapshot). The transaction is aborted — retry it whole.
36
+ * `retryable` is `true`. @since 0.1.153
37
+ */
38
+ | "SERIALIZATION_FAILURE";
18
39
  declare class DbError extends Error {
19
40
  readonly code: DbErrorCode;
20
41
  readonly errors: Array<{
@@ -26,7 +47,20 @@ declare class DbError extends Error {
26
47
  path: string;
27
48
  message: string;
28
49
  }>, message?: string);
50
+ /**
51
+ * `true` when retrying the whole transaction may succeed — a transient
52
+ * concurrency condition (`DEADLOCK`, `LOCK_TIMEOUT`,
53
+ * `SERIALIZATION_FAILURE`), not a fault of the
54
+ * request. @since 0.1.153
55
+ */
56
+ get retryable(): boolean;
29
57
  }
58
+ /**
59
+ * Whether `error` is a retryable `DbError` (`DEADLOCK` / `LOCK_TIMEOUT` /
60
+ * `SERIALIZATION_FAILURE`):
61
+ * re-running the whole transaction may succeed. @since 0.1.153
62
+ */
63
+ declare function isRetryableDbError(error: unknown): error is DbError;
30
64
  /** Whether `error` is a `DbError` with code `CONFLICT` (unique / primary-key violation). */
31
65
  declare function isConflict(error: unknown): error is DbError;
32
66
  /**
@@ -162,4 +196,4 @@ interface ValidationContext {
162
196
  */
163
197
  declare function buildValidationContext(type: TAtscriptAnnotatedType<TAtscriptTypeObject>): ValidationContext;
164
198
  //#endregion
165
- export { aggregateFailure as _, buildValidationContext as a, isNavRelation as c, getKeyProps as d, CasExhaustedError as f, aggregateExpressionsNotSupported as g, DbErrorCode as h, buildPatchPartial as i, TArrayPatch as l, DbError as m, ValidatorMode as n, dbPlugin as o, CasMismatchError as p, buildDbValidator as r, forceNavNonOptional as s, ValidationContext as t, TDbPatch as u, bucketTimeZoneUnavailable as v, isConflict as y };
199
+ export { aggregateFailure as _, buildValidationContext as a, isRetryableDbError as b, isNavRelation as c, getKeyProps as d, CasExhaustedError as f, aggregateExpressionsNotSupported as g, DbErrorCode as h, buildPatchPartial as i, TArrayPatch as l, DbError as m, ValidatorMode as n, dbPlugin as o, CasMismatchError as p, buildDbValidator as r, forceNavNonOptional as s, ValidationContext as t, TDbPatch as u, bucketTimeZoneUnavailable as v, isConflict as y };
@@ -14,7 +14,28 @@ type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" |
14
14
  * table lives on a different adapter, or the table was built without a
15
15
  * `DbSpace` (moost-db: 400). @since 0.1.147
16
16
  */
17
- | "REL_FILTER_NOT_SUPPORTED" /** The `DbSpace` was closed; the table/view handle can no longer be used (moost-db: 503). @since 0.1.148 */ | "SPACE_CLOSED" /** The adapter does not implement `insertManyIgnore` (moost-db: 400). @since 0.1.148 */ | "ON_CONFLICT_NOT_SUPPORTED" | "TX_WAIT_TIMEOUT";
17
+ | "REL_FILTER_NOT_SUPPORTED" /** The `DbSpace` was closed; the table/view handle can no longer be used (moost-db: 503). @since 0.1.148 */ | "SPACE_CLOSED" /** The adapter does not implement `insertManyIgnore` (moost-db: 400). @since 0.1.148 */ | "ON_CONFLICT_NOT_SUPPORTED" | "TX_WAIT_TIMEOUT"
18
+ /**
19
+ * The engine chose this transaction as a deadlock victim (MySQL errno 1213,
20
+ * PostgreSQL 40P01) and rolled it back. Retry the WHOLE transaction.
21
+ * `retryable` is `true`. @since 0.1.153
22
+ */
23
+ | "DEADLOCK"
24
+ /**
25
+ * A row lock was not granted in time (MySQL errno 1205
26
+ * `innodb_lock_wait_timeout`, PostgreSQL 55P03 `lock_timeout` / `NOWAIT`).
27
+ * The statement failed; MySQL keeps the transaction open unless
28
+ * `innodb_rollback_on_timeout` is set, PostgreSQL aborts it — retry the
29
+ * whole transaction. `retryable` is `true`. @since 0.1.153
30
+ */
31
+ | "LOCK_TIMEOUT"
32
+ /**
33
+ * PostgreSQL could not serialize the transaction (`40001`: under
34
+ * `REPEATABLE READ` / `SERIALIZABLE`, e.g. a row locked or updated after it
35
+ * changed since the snapshot). The transaction is aborted — retry it whole.
36
+ * `retryable` is `true`. @since 0.1.153
37
+ */
38
+ | "SERIALIZATION_FAILURE";
18
39
  declare class DbError extends Error {
19
40
  readonly code: DbErrorCode;
20
41
  readonly errors: Array<{
@@ -26,7 +47,20 @@ declare class DbError extends Error {
26
47
  path: string;
27
48
  message: string;
28
49
  }>, message?: string);
50
+ /**
51
+ * `true` when retrying the whole transaction may succeed — a transient
52
+ * concurrency condition (`DEADLOCK`, `LOCK_TIMEOUT`,
53
+ * `SERIALIZATION_FAILURE`), not a fault of the
54
+ * request. @since 0.1.153
55
+ */
56
+ get retryable(): boolean;
29
57
  }
58
+ /**
59
+ * Whether `error` is a retryable `DbError` (`DEADLOCK` / `LOCK_TIMEOUT` /
60
+ * `SERIALIZATION_FAILURE`):
61
+ * re-running the whole transaction may succeed. @since 0.1.153
62
+ */
63
+ declare function isRetryableDbError(error: unknown): error is DbError;
30
64
  /** Whether `error` is a `DbError` with code `CONFLICT` (unique / primary-key violation). */
31
65
  declare function isConflict(error: unknown): error is DbError;
32
66
  /**
@@ -162,4 +196,4 @@ interface ValidationContext {
162
196
  */
163
197
  declare function buildValidationContext(type: TAtscriptAnnotatedType<TAtscriptTypeObject>): ValidationContext;
164
198
  //#endregion
165
- export { aggregateFailure as _, buildValidationContext as a, isNavRelation as c, getKeyProps as d, CasExhaustedError as f, aggregateExpressionsNotSupported as g, DbErrorCode as h, buildPatchPartial as i, TArrayPatch as l, DbError as m, ValidatorMode as n, dbPlugin as o, CasMismatchError as p, buildDbValidator as r, forceNavNonOptional as s, ValidationContext as t, TDbPatch as u, bucketTimeZoneUnavailable as v, isConflict as y };
199
+ export { aggregateFailure as _, buildValidationContext as a, isRetryableDbError as b, isNavRelation as c, getKeyProps as d, CasExhaustedError as f, aggregateExpressionsNotSupported as g, DbErrorCode as h, buildPatchPartial as i, TArrayPatch as l, DbError as m, ValidatorMode as n, dbPlugin as o, CasMismatchError as p, buildDbValidator as r, forceNavNonOptional as s, ValidationContext as t, TDbPatch as u, bucketTimeZoneUnavailable as v, isConflict as y };
@@ -1,6 +1,6 @@
1
- import { i as DepthLimitExceededError } from "./db-error-Az85UhTX.mjs";
1
+ import { i as DepthLimitExceededError } from "./db-error-eSsQX7rt.mjs";
2
2
  import { getDbFieldOp } from "./ops.mjs";
3
- import { flattenAnnotatedType } from "@atscript/typescript/utils";
3
+ import { createAnnotatedTypeNode, flattenAnnotatedType } from "@atscript/typescript/utils";
4
4
  //#region src/patch/patch-types.ts
5
5
  /**
6
6
  * Extracts `@expect.array.key` properties from an array-of-objects type.
@@ -40,6 +40,16 @@ function isPatchOperatorObject(value) {
40
40
  return keys.every((k) => PATCH_OPS.has(k));
41
41
  }
42
42
  /**
43
+ * Items of union / tuple / intersection types the plugin has visited. The
44
+ * validator visits a complex type before its items, so an item is known here
45
+ * by the time it is validated.
46
+ */
47
+ const complexItems = /* @__PURE__ */ new WeakSet();
48
+ function markComplexItems(def) {
49
+ const kind = def.type.kind;
50
+ if (kind === "union" || kind === "tuple" || kind === "intersection") for (const item of def.type.items) complexItems.add(item);
51
+ }
52
+ /**
43
53
  * Validator plugin for database operations.
44
54
  *
45
55
  * Handles navigation field constraints and delegates to the standard validator
@@ -54,6 +64,7 @@ function createDbValidatorPlugin() {
54
64
  return (ctx, def, value) => {
55
65
  const dbCtx = ctx.context;
56
66
  if (!dbCtx) return;
67
+ markComplexItems(def);
57
68
  if (def.type.tags?.has("geoPoint") && value !== void 0 && value !== null) {
58
69
  if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== "number" || typeof value[1] !== "number" || !Number.isFinite(value[0]) || !Number.isFinite(value[1])) {
59
70
  ctx.error("Expected a [lng, lat] tuple of two finite numbers (GeoJSON order)");
@@ -73,7 +84,7 @@ function createDbValidatorPlugin() {
73
84
  const isFrom = def.metadata.has("db.rel.from");
74
85
  const isVia = def.metadata.has("db.rel.via");
75
86
  if (isTo || isFrom || isVia) return handleNavField(ctx, def, value, dbCtx, isTo, isFrom, isVia);
76
- if (value === void 0 && (dbCtx.mode === "insert" || dbCtx.mode === "replace")) {
87
+ if (value === void 0 && (dbCtx.mode === "insert" || dbCtx.mode === "replace") && !complexItems.has(def)) {
77
88
  const meta = def.metadata;
78
89
  const serverManaged = meta.has("db.default") || meta.has("db.default.increment") || meta.has("db.default.uuid") || meta.has("db.default.now") || meta.has("db.column.version") || meta.has("db.column.derived");
79
90
  const hasFK = meta.has("db.rel.FK");
@@ -90,7 +101,7 @@ function createDbValidatorPlugin() {
90
101
  return false;
91
102
  }
92
103
  if (dbCtx.flatMap && !isFieldOpAllowed(ctx.path, dbCtx.flatMap, dbCtx.navFields)) {
93
- ctx.error("Field operations ($inc/$dec/$mul) are not supported inside @db.json fields or nested objects without @db.patch.strategy \"merge\"");
104
+ ctx.error("Field operations ($inc/$dec/$mul) are not supported inside @db.json fields, array items, or nested objects without @db.patch.strategy \"merge\"");
94
105
  return false;
95
106
  }
96
107
  if (!(def.type.kind === "" && def.type.designType === "number")) {
@@ -118,6 +129,8 @@ function createDbValidatorPlugin() {
118
129
  * - The field itself is `@db.json` (ops on opaque blobs are meaningless).
119
130
  * - Any ancestor is `@db.json`.
120
131
  * - Any ancestor is a nested object without `@db.patch.strategy "merge"`.
132
+ * - Any ancestor is an embedded array (arrays are written whole, so an op
133
+ * inside an item would be stored verbatim).
121
134
  *
122
135
  * Accepts immediately when an ancestor is a navigation field (TO/FROM/VIA) —
123
136
  * the nested data is extracted and validated against its own table separately.
@@ -131,6 +144,7 @@ function isFieldOpAllowed(path, flatMap, navFields) {
131
144
  const entry = flatMap.get(ancestor);
132
145
  if (!entry) continue;
133
146
  if (entry.metadata.has("db.json")) return false;
147
+ if (entry.type.kind === "array") return false;
134
148
  if (entry.type.kind === "object" && entry.metadata.get("db.patch.strategy") !== "merge") return false;
135
149
  }
136
150
  return true;
@@ -233,7 +247,10 @@ function validatePartialItems(ctx, arrayDef, items, op, isMerge) {
233
247
  return false;
234
248
  }
235
249
  const elementDef = arrayDef.type.of;
236
- if (elementDef.type.kind !== "object") return ctx.validateAnnotatedType(arrayDef, items);
250
+ if (elementDef.type.kind !== "object") {
251
+ for (let i = 0; i < items.length; i++) if (!validateAt(ctx, `${op}[${i}]`, elementDef, items[i])) return false;
252
+ return true;
253
+ }
237
254
  const keyProps = getKeyProps(arrayDef);
238
255
  for (let i = 0; i < items.length; i++) {
239
256
  const item = items[i];
@@ -249,12 +266,24 @@ function validatePartialItems(ctx, arrayDef, items, op, isMerge) {
249
266
  }
250
267
  }
251
268
  const objType = elementDef.type;
252
- for (const [key, val] of Object.entries(rec)) {
269
+ let provided;
270
+ let providedValue;
271
+ for (const key of Object.keys(rec)) {
253
272
  const propDef = objType.props.get(key);
254
273
  if (propDef) {
255
- if (!ctx.validateAnnotatedType(propDef, val)) return false;
274
+ (provided ??= /* @__PURE__ */ new Map()).set(key, propDef);
275
+ (providedValue ??= {})[key] = rec[key];
256
276
  }
257
277
  }
278
+ if (provided) {
279
+ const itemDef = createAnnotatedTypeNode({
280
+ kind: "object",
281
+ props: provided,
282
+ propsPatterns: [],
283
+ tags: objType.tags
284
+ }, elementDef.metadata);
285
+ if (!validateAt(ctx, `${op}[${i}]`, itemDef, providedValue)) return false;
286
+ }
258
287
  if (op === "$update" && isMerge !== true) {
259
288
  for (const [propName, propDef] of objType.props) if (!propDef.optional && !keyProps.has(propName) && rec[propName] === void 0) {
260
289
  ctx.error(`${op}[${i}]: field '${propName}' is required (replace strategy)`);
@@ -265,6 +294,21 @@ function validatePartialItems(ctx, arrayDef, items, op, isMerge) {
265
294
  return true;
266
295
  }
267
296
  /**
297
+ * Validates `value` against `def` as if it sat under `segment` of the current
298
+ * path (`<path>.<segment>`), through the validator's full `validateSafe` walk
299
+ * (optional/null shortcut, `replace`, plugins). The plugin context has no
300
+ * public path push, so the value is wrapped in a single-prop object.
301
+ */
302
+ function validateAt(ctx, segment, def, value) {
303
+ const wrapper = createAnnotatedTypeNode({
304
+ kind: "object",
305
+ props: new Map([[segment, def]]),
306
+ propsPatterns: [],
307
+ tags: /* @__PURE__ */ new Set()
308
+ }, /* @__PURE__ */ new Map());
309
+ return ctx.validateAnnotatedType(wrapper, { [segment]: value });
310
+ }
311
+ /**
268
312
  * Returns `true` when every char in `[start, end)` is an ASCII digit.
269
313
  * Matches segments that represent array indices in dot-notation paths.
270
314
  */
@@ -1,4 +1,4 @@
1
- const require_db_error = require("./db-error-DRQH4sLY.cjs");
1
+ const require_db_error = require("./db-error-JcfJqqpk.cjs");
2
2
  const require_ops = require("./ops.cjs");
3
3
  let _atscript_typescript_utils = require("@atscript/typescript/utils");
4
4
  //#region src/patch/patch-types.ts
@@ -40,6 +40,16 @@ function isPatchOperatorObject(value) {
40
40
  return keys.every((k) => PATCH_OPS.has(k));
41
41
  }
42
42
  /**
43
+ * Items of union / tuple / intersection types the plugin has visited. The
44
+ * validator visits a complex type before its items, so an item is known here
45
+ * by the time it is validated.
46
+ */
47
+ const complexItems = /* @__PURE__ */ new WeakSet();
48
+ function markComplexItems(def) {
49
+ const kind = def.type.kind;
50
+ if (kind === "union" || kind === "tuple" || kind === "intersection") for (const item of def.type.items) complexItems.add(item);
51
+ }
52
+ /**
43
53
  * Validator plugin for database operations.
44
54
  *
45
55
  * Handles navigation field constraints and delegates to the standard validator
@@ -54,6 +64,7 @@ function createDbValidatorPlugin() {
54
64
  return (ctx, def, value) => {
55
65
  const dbCtx = ctx.context;
56
66
  if (!dbCtx) return;
67
+ markComplexItems(def);
57
68
  if (def.type.tags?.has("geoPoint") && value !== void 0 && value !== null) {
58
69
  if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== "number" || typeof value[1] !== "number" || !Number.isFinite(value[0]) || !Number.isFinite(value[1])) {
59
70
  ctx.error("Expected a [lng, lat] tuple of two finite numbers (GeoJSON order)");
@@ -73,7 +84,7 @@ function createDbValidatorPlugin() {
73
84
  const isFrom = def.metadata.has("db.rel.from");
74
85
  const isVia = def.metadata.has("db.rel.via");
75
86
  if (isTo || isFrom || isVia) return handleNavField(ctx, def, value, dbCtx, isTo, isFrom, isVia);
76
- if (value === void 0 && (dbCtx.mode === "insert" || dbCtx.mode === "replace")) {
87
+ if (value === void 0 && (dbCtx.mode === "insert" || dbCtx.mode === "replace") && !complexItems.has(def)) {
77
88
  const meta = def.metadata;
78
89
  const serverManaged = meta.has("db.default") || meta.has("db.default.increment") || meta.has("db.default.uuid") || meta.has("db.default.now") || meta.has("db.column.version") || meta.has("db.column.derived");
79
90
  const hasFK = meta.has("db.rel.FK");
@@ -90,7 +101,7 @@ function createDbValidatorPlugin() {
90
101
  return false;
91
102
  }
92
103
  if (dbCtx.flatMap && !isFieldOpAllowed(ctx.path, dbCtx.flatMap, dbCtx.navFields)) {
93
- ctx.error("Field operations ($inc/$dec/$mul) are not supported inside @db.json fields or nested objects without @db.patch.strategy \"merge\"");
104
+ ctx.error("Field operations ($inc/$dec/$mul) are not supported inside @db.json fields, array items, or nested objects without @db.patch.strategy \"merge\"");
94
105
  return false;
95
106
  }
96
107
  if (!(def.type.kind === "" && def.type.designType === "number")) {
@@ -118,6 +129,8 @@ function createDbValidatorPlugin() {
118
129
  * - The field itself is `@db.json` (ops on opaque blobs are meaningless).
119
130
  * - Any ancestor is `@db.json`.
120
131
  * - Any ancestor is a nested object without `@db.patch.strategy "merge"`.
132
+ * - Any ancestor is an embedded array (arrays are written whole, so an op
133
+ * inside an item would be stored verbatim).
121
134
  *
122
135
  * Accepts immediately when an ancestor is a navigation field (TO/FROM/VIA) —
123
136
  * the nested data is extracted and validated against its own table separately.
@@ -131,6 +144,7 @@ function isFieldOpAllowed(path, flatMap, navFields) {
131
144
  const entry = flatMap.get(ancestor);
132
145
  if (!entry) continue;
133
146
  if (entry.metadata.has("db.json")) return false;
147
+ if (entry.type.kind === "array") return false;
134
148
  if (entry.type.kind === "object" && entry.metadata.get("db.patch.strategy") !== "merge") return false;
135
149
  }
136
150
  return true;
@@ -233,7 +247,10 @@ function validatePartialItems(ctx, arrayDef, items, op, isMerge) {
233
247
  return false;
234
248
  }
235
249
  const elementDef = arrayDef.type.of;
236
- if (elementDef.type.kind !== "object") return ctx.validateAnnotatedType(arrayDef, items);
250
+ if (elementDef.type.kind !== "object") {
251
+ for (let i = 0; i < items.length; i++) if (!validateAt(ctx, `${op}[${i}]`, elementDef, items[i])) return false;
252
+ return true;
253
+ }
237
254
  const keyProps = getKeyProps(arrayDef);
238
255
  for (let i = 0; i < items.length; i++) {
239
256
  const item = items[i];
@@ -249,12 +266,24 @@ function validatePartialItems(ctx, arrayDef, items, op, isMerge) {
249
266
  }
250
267
  }
251
268
  const objType = elementDef.type;
252
- for (const [key, val] of Object.entries(rec)) {
269
+ let provided;
270
+ let providedValue;
271
+ for (const key of Object.keys(rec)) {
253
272
  const propDef = objType.props.get(key);
254
273
  if (propDef) {
255
- if (!ctx.validateAnnotatedType(propDef, val)) return false;
274
+ (provided ??= /* @__PURE__ */ new Map()).set(key, propDef);
275
+ (providedValue ??= {})[key] = rec[key];
256
276
  }
257
277
  }
278
+ if (provided) {
279
+ const itemDef = (0, _atscript_typescript_utils.createAnnotatedTypeNode)({
280
+ kind: "object",
281
+ props: provided,
282
+ propsPatterns: [],
283
+ tags: objType.tags
284
+ }, elementDef.metadata);
285
+ if (!validateAt(ctx, `${op}[${i}]`, itemDef, providedValue)) return false;
286
+ }
258
287
  if (op === "$update" && isMerge !== true) {
259
288
  for (const [propName, propDef] of objType.props) if (!propDef.optional && !keyProps.has(propName) && rec[propName] === void 0) {
260
289
  ctx.error(`${op}[${i}]: field '${propName}' is required (replace strategy)`);
@@ -265,6 +294,21 @@ function validatePartialItems(ctx, arrayDef, items, op, isMerge) {
265
294
  return true;
266
295
  }
267
296
  /**
297
+ * Validates `value` against `def` as if it sat under `segment` of the current
298
+ * path (`<path>.<segment>`), through the validator's full `validateSafe` walk
299
+ * (optional/null shortcut, `replace`, plugins). The plugin context has no
300
+ * public path push, so the value is wrapped in a single-prop object.
301
+ */
302
+ function validateAt(ctx, segment, def, value) {
303
+ const wrapper = (0, _atscript_typescript_utils.createAnnotatedTypeNode)({
304
+ kind: "object",
305
+ props: new Map([[segment, def]]),
306
+ propsPatterns: [],
307
+ tags: /* @__PURE__ */ new Set()
308
+ }, /* @__PURE__ */ new Map());
309
+ return ctx.validateAnnotatedType(wrapper, { [segment]: value });
310
+ }
311
+ /**
268
312
  * Returns `true` when every char in `[start, end)` is an ASCII digit.
269
313
  * Matches segments that represent array indices in dot-notation paths.
270
314
  */