@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/plugin.cjs CHANGED
@@ -33,10 +33,10 @@ 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-DupYzFD7.cjs");
36
+ const require_aggregate_fns = require("./aggregate-fns-CNtrSxGH.cjs");
37
37
  const require_search_fields = require("./search-fields-B49-RCWd.cjs");
38
38
  require("./consts-BzRfCcH2.cjs");
39
- const require_view_validation = require("./view-validation-BiDLCYMw.cjs");
39
+ const require_view_validation = require("./view-validation-C0UVLx_H.cjs");
40
40
  let _atscript_core = require("@atscript/core");
41
41
  let node_path = require("node:path");
42
42
  node_path = __toESM(node_path, 1);
@@ -1083,6 +1083,12 @@ const dbIndexAnnotations = { index: {
1083
1083
  description: (0, _atscript_core.annotationExample)("Full-text search index. Fields sharing the same index name form a composite full-text index. A string member is part of the engine's text index. An integer member (`number.int` and its sizes, `@expect.int`, `@db.default.increment`) is never part of the physical text index: when the whole search term is a whole number (`2946`, `-12`; no leading zeros), rows whose member equals it exactly also match (OR'd with the text match). An integer member must be index-backed — the primary key (first `@meta.id`) or the first field of a `@db.index.plain` / `@db.index.unique`. Floats, decimals and timestamps are refused.", "@db.index.fulltext \"ft_content\"", "title: string", "", "@db.index.fulltext \"ft_content\", 5", "bio: string", "", "@db.index.fulltext \"ft_content\"", "@db.index.unique", "refNo: number.int"),
1084
1084
  validate(token, args, doc) {
1085
1085
  const field = token.parentNode;
1086
+ const indexName = args[0]?.text;
1087
+ if (indexName?.startsWith("atscript__")) return [{
1088
+ message: `@db.index.fulltext name "${indexName}" is reserved — names starting with "atscript__" are internal index keys`,
1089
+ severity: 1,
1090
+ range: args[0].range
1091
+ }];
1086
1092
  const sealed = require_view_validation.sealedSearchProblem(token, doc, "@db.index.fulltext");
1087
1093
  if (sealed) return [sealed];
1088
1094
  const verdict = require_view_validation.searchFieldVerdict(field, doc);
@@ -1156,8 +1162,8 @@ function checkCompositeFkCoverage(token, alias, refTypeName, targetStruct, targe
1156
1162
  for (const [, sibling] of hostStruct.props) {
1157
1163
  if (sibling.countAnnotations("db.rel.FK") === 0) continue;
1158
1164
  if (require_view_validation.getAnnotationAlias(sibling, "db.rel.FK") !== alias) continue;
1159
- const siblingDef = sibling.getDefinition();
1160
- if (siblingDef && (0, _atscript_core.isRef)(siblingDef) && siblingDef.id === refTypeName) {
1165
+ const siblingDef = require_view_validation.fkChainRef(sibling.getDefinition());
1166
+ if (siblingDef && siblingDef.id === refTypeName) {
1161
1167
  if (siblingDef.chain.length === 1) covered.add(siblingDef.chain[0].text);
1162
1168
  }
1163
1169
  }
@@ -1183,16 +1189,15 @@ const dbRelAnnotations = { rel: {
1183
1189
  severity: 1,
1184
1190
  range: token.range
1185
1191
  });
1186
- const definition = field.getDefinition();
1187
- if (!definition || !(0, _atscript_core.isRef)(definition) || !definition.hasChain) {
1192
+ const ref = require_view_validation.fkChainRef(field.getDefinition());
1193
+ if (!ref) {
1188
1194
  errors.push({
1189
- message: `@db.rel.FK requires a chain reference type (e.g. User.id), got scalar type`,
1195
+ message: `@db.rel.FK requires a chain reference type (e.g. User.id or User.id | null), got scalar type`,
1190
1196
  severity: 1,
1191
1197
  range: token.range
1192
1198
  });
1193
1199
  return errors;
1194
1200
  }
1195
- const ref = definition;
1196
1201
  const refTypeName = ref.id;
1197
1202
  const chainFields = ref.chain.map((c) => c.text);
1198
1203
  const targetUnwound = doc.unwindType(refTypeName);
@@ -1236,10 +1241,8 @@ const dbRelAnnotations = { rel: {
1236
1241
  let sameTargetCount = 0;
1237
1242
  for (const [, prop] of struct.props) {
1238
1243
  if (prop.countAnnotations("db.rel.FK") === 0) continue;
1239
- const def = prop.getDefinition();
1240
- if (!def || !(0, _atscript_core.isRef)(def)) continue;
1241
- const r = def;
1242
- if (!r.hasChain) continue;
1244
+ const r = require_view_validation.fkChainRef(prop.getDefinition());
1245
+ if (!r) continue;
1243
1246
  if (r.id === refTypeName) {
1244
1247
  if (!require_view_validation.getAnnotationAlias(prop, "db.rel.FK")) sameTargetCount++;
1245
1248
  }
package/dist/plugin.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { i as NULL_WHEN_EMPTY_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-DsS9UQVB.mjs";
1
+ import { i as NULL_WHEN_EMPTY_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-CmtSNIHm.mjs";
2
2
  import { c as DERIVED_INCOMPATIBLE, l as JSON_LEAF_TYPES, o as numericTypeProblem, s as DB_ENTITY_ANNOTATIONS, t as ENCRYPTED_INCOMPATIBLE } from "./search-fields-BPNsxrCk.mjs";
3
3
  import "./consts-C_-5_pFq.mjs";
4
- import { A as getParentTypeName, B as validateSiblingStringField, C as viewJoins, D as getDbTableOwner, E as getAnnotationAlias, F as refActionAnnotation, I as searchFieldVerdict, L as validateExclusiveWith, M as isNullableUnion, O as getNavTargetTypeName, P as primitiveBaseType, R as validateFieldBaseType, S as validateRefArgument, T as effectiveAnnotationNames, _ as hasAnyViewAnnotation, c as refuseCiphertext, d as viewFieldSeals, f as viewProps, g as forEachFieldRef, h as findViewCycle, i as encryptedTypeSearchProblem, j as integerMemberIndexProblem, k as getParentStruct, l as sealedSearchProblem, m as findFKFieldsPointingTo, o as isNavProp, p as earlierJoinTargets, s as jsonChainInfo, u as validateViewInterface, v as isAliasDecl, w as viewScopeTypes, x as validateQueryScope, y as isDbSourceDecl, z as validateIncompatible } from "./view-validation-BcgDq7M9.mjs";
4
+ import { A as getParentStruct, B as validateFieldBaseType, C as validateRefArgument, D as getAnnotationAlias, E as effectiveAnnotationNames, H as validateSiblingStringField, I as primitiveBaseType, L as refActionAnnotation, M as integerMemberIndexProblem, O as getDbTableOwner, P as isNullableUnion, R as searchFieldVerdict, S as validateQueryScope, T as viewScopeTypes, V as validateIncompatible, _ as forEachFieldRef, b as isDbSourceDecl, c as refuseCiphertext, d as viewFieldSeals, f as viewProps, g as fkChainRef, h as findViewCycle, i as encryptedTypeSearchProblem, j as getParentTypeName, k as getNavTargetTypeName, l as sealedSearchProblem, m as findFKFieldsPointingTo, o as isNavProp, p as earlierJoinTargets, s as jsonChainInfo, u as validateViewInterface, v as hasAnyViewAnnotation, w as viewJoins, y as isAliasDecl, z as validateExclusiveWith } from "./view-validation-KW77xIKn.mjs";
5
5
  import { AnnotationSpec, DEFAULT_FORMAT, annotationExample, getFieldsForType, isArray, isInterface, isPrimitive, isProp, isQueryComparison, isQueryLogical, isRef, isStructure } from "@atscript/core";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
@@ -1047,6 +1047,12 @@ const dbIndexAnnotations = { index: {
1047
1047
  description: annotationExample("Full-text search index. Fields sharing the same index name form a composite full-text index. A string member is part of the engine's text index. An integer member (`number.int` and its sizes, `@expect.int`, `@db.default.increment`) is never part of the physical text index: when the whole search term is a whole number (`2946`, `-12`; no leading zeros), rows whose member equals it exactly also match (OR'd with the text match). An integer member must be index-backed — the primary key (first `@meta.id`) or the first field of a `@db.index.plain` / `@db.index.unique`. Floats, decimals and timestamps are refused.", "@db.index.fulltext \"ft_content\"", "title: string", "", "@db.index.fulltext \"ft_content\", 5", "bio: string", "", "@db.index.fulltext \"ft_content\"", "@db.index.unique", "refNo: number.int"),
1048
1048
  validate(token, args, doc) {
1049
1049
  const field = token.parentNode;
1050
+ const indexName = args[0]?.text;
1051
+ if (indexName?.startsWith("atscript__")) return [{
1052
+ message: `@db.index.fulltext name "${indexName}" is reserved — names starting with "atscript__" are internal index keys`,
1053
+ severity: 1,
1054
+ range: args[0].range
1055
+ }];
1050
1056
  const sealed = sealedSearchProblem(token, doc, "@db.index.fulltext");
1051
1057
  if (sealed) return [sealed];
1052
1058
  const verdict = searchFieldVerdict(field, doc);
@@ -1120,8 +1126,8 @@ function checkCompositeFkCoverage(token, alias, refTypeName, targetStruct, targe
1120
1126
  for (const [, sibling] of hostStruct.props) {
1121
1127
  if (sibling.countAnnotations("db.rel.FK") === 0) continue;
1122
1128
  if (getAnnotationAlias(sibling, "db.rel.FK") !== alias) continue;
1123
- const siblingDef = sibling.getDefinition();
1124
- if (siblingDef && isRef(siblingDef) && siblingDef.id === refTypeName) {
1129
+ const siblingDef = fkChainRef(sibling.getDefinition());
1130
+ if (siblingDef && siblingDef.id === refTypeName) {
1125
1131
  if (siblingDef.chain.length === 1) covered.add(siblingDef.chain[0].text);
1126
1132
  }
1127
1133
  }
@@ -1147,16 +1153,15 @@ const dbRelAnnotations = { rel: {
1147
1153
  severity: 1,
1148
1154
  range: token.range
1149
1155
  });
1150
- const definition = field.getDefinition();
1151
- if (!definition || !isRef(definition) || !definition.hasChain) {
1156
+ const ref = fkChainRef(field.getDefinition());
1157
+ if (!ref) {
1152
1158
  errors.push({
1153
- message: `@db.rel.FK requires a chain reference type (e.g. User.id), got scalar type`,
1159
+ message: `@db.rel.FK requires a chain reference type (e.g. User.id or User.id | null), got scalar type`,
1154
1160
  severity: 1,
1155
1161
  range: token.range
1156
1162
  });
1157
1163
  return errors;
1158
1164
  }
1159
- const ref = definition;
1160
1165
  const refTypeName = ref.id;
1161
1166
  const chainFields = ref.chain.map((c) => c.text);
1162
1167
  const targetUnwound = doc.unwindType(refTypeName);
@@ -1200,10 +1205,8 @@ const dbRelAnnotations = { rel: {
1200
1205
  let sameTargetCount = 0;
1201
1206
  for (const [, prop] of struct.props) {
1202
1207
  if (prop.countAnnotations("db.rel.FK") === 0) continue;
1203
- const def = prop.getDefinition();
1204
- if (!def || !isRef(def)) continue;
1205
- const r = def;
1206
- if (!r.hasChain) continue;
1208
+ const r = fkChainRef(prop.getDefinition());
1209
+ if (!r) continue;
1207
1210
  if (r.id === refTypeName) {
1208
1211
  if (!getAnnotationAlias(prop, "db.rel.FK")) sameTargetCount++;
1209
1212
  }
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-yCy6wGuN.cjs");
3
- const require_relation_loader = require("./relation-loader-DujL_Mh9.cjs");
2
+ const require_nested_writer = require("./nested-writer-DOvdbYG-.cjs");
3
+ const require_relation_loader = require("./relation-loader-BrkakVF_.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 { Jt as TTableResolver, K as TDbForeignKey, an as TableMetadata, cr as TGenericLogger, ct as TDbRelation, en as TWriteTableResolver, fn as BaseDbAdapter, h as AtscriptDbWritable, m as AtscriptDbTableLike } from "./buckets-B8UOod-v.cjs";
1
+ import { K as TDbForeignKey, Yt as TTableResolver, ct as TDbRelation, fr as TGenericLogger, h as AtscriptDbWritable, hn as BaseDbAdapter, m as AtscriptDbTableLike, nn as TWriteTableResolver, sn as TableMetadata } from "./buckets-BDxKfCNo.cjs";
2
2
  import { t as DbValidationContext } from "./db-validator-plugin-wRqMYNBI.cjs";
3
- import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-DloMywNs.cjs";
3
+ import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-C4UNWYQu.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 { Jt as TTableResolver, K as TDbForeignKey, an as TableMetadata, cr as TGenericLogger, ct as TDbRelation, en as TWriteTableResolver, fn as BaseDbAdapter, h as AtscriptDbWritable, m as AtscriptDbTableLike } from "./buckets-CHyspjYP.mjs";
1
+ import { K as TDbForeignKey, Yt as TTableResolver, ct as TDbRelation, fr as TGenericLogger, h as AtscriptDbWritable, hn as BaseDbAdapter, m as AtscriptDbTableLike, nn as TWriteTableResolver, sn as TableMetadata } from "./buckets-CHUgky8T.mjs";
2
2
  import { t as DbValidationContext } from "./db-validator-plugin-wRqMYNBI.mjs";
3
- import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-zwt19E7r.mjs";
3
+ import { n as findRemoteFK, r as resolveRelationTargetTable, t as findFKForRelation } from "./relation-helpers-hdbuqwjV.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-Qv3LrQ42.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-ClUX0DB0.mjs";
1
+ import { t as loadRelationsImpl } from "./relation-loader-BOEVhcBe.mjs";
2
+ import { B as findFKForRelation, H as resolveRelationTargetTable, V as findRemoteFK, 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 } from "./nested-writer-BraWjS8a.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 { K as TDbForeignKey, ct as TDbRelation } from "./buckets-B8UOod-v.cjs";
1
+ import { K as TDbForeignKey, ct as TDbRelation } from "./buckets-BDxKfCNo.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 { K as TDbForeignKey, ct as TDbRelation } from "./buckets-CHyspjYP.mjs";
1
+ import { K as TDbForeignKey, ct as TDbRelation } from "./buckets-CHUgky8T.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 { B as resolveRelationTargetTable, E as andFilters, I as compositeKey, P as relationStaticFilter, R as findFKForRelation, V as slicePerGroup, z as findRemoteFK } from "./nested-writer-ClUX0DB0.mjs";
1
+ import { B as findFKForRelation, H as resolveRelationTargetTable, I as relationStaticFilter, O as andFilters, R as compositeKey, U as slicePerGroup, V as findRemoteFK } from "./nested-writer-BraWjS8a.mjs";
2
2
  //#region \0rolldown/runtime.js
3
3
  var __defProp = Object.defineProperty;
4
4
  var __exportAll = (all, no_symbols) => {
@@ -1,4 +1,4 @@
1
- const require_nested_writer = require("./nested-writer-yCy6wGuN.cjs");
1
+ const require_nested_writer = require("./nested-writer-DOvdbYG-.cjs");
2
2
  //#region src/rel/relation-loader.ts
3
3
  var relation_loader_exports = /* @__PURE__ */ require("./plugin.cjs").__exportAll({
4
4
  ensureSelectIncludesFields: () => ensureSelectIncludesFields,
package/dist/shared.cjs CHANGED
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_search_fields = require("./search-fields-B49-RCWd.cjs");
3
3
  const require_object = require("./object-GjsuPNqo.cjs");
4
4
  const require_aggregate_rules = require("./aggregate-rules-D_bsCpUI.cjs");
5
- const require_view_validation = require("./view-validation-BiDLCYMw.cjs");
5
+ const require_view_validation = require("./view-validation-C0UVLx_H.cjs");
6
6
  exports.ENCRYPTED_INCOMPATIBLE = require_search_fields.ENCRYPTED_INCOMPATIBLE;
7
7
  exports.ENCRYPTED_SEARCH_FEATURES = require_search_fields.ENCRYPTED_SEARCH_FEATURES;
8
8
  exports.NAV_ANNOTATIONS = require_view_validation.NAV_ANNOTATIONS;
@@ -15,6 +15,7 @@ exports.encryptedTypeSearchProblem = require_view_validation.encryptedTypeSearch
15
15
  exports.findFKFieldsPointingTo = require_view_validation.findFKFieldsPointingTo;
16
16
  exports.findStoredField = require_view_validation.findStoredField;
17
17
  exports.findViewCycle = require_view_validation.findViewCycle;
18
+ exports.fkChainRef = require_view_validation.fkChainRef;
18
19
  exports.forEachFieldRef = require_view_validation.forEachFieldRef;
19
20
  exports.getAnnotationAlias = require_view_validation.getAnnotationAlias;
20
21
  exports.getDbTableOwner = require_view_validation.getDbTableOwner;
@@ -26,6 +27,7 @@ exports.integerMemberIndexProblem = require_view_validation.integerMemberIndexPr
26
27
  exports.isAliasDecl = require_view_validation.isAliasDecl;
27
28
  exports.isDbSourceDecl = require_view_validation.isDbSourceDecl;
28
29
  exports.isNavProp = require_view_validation.isNavProp;
30
+ exports.isNullMember = require_view_validation.isNullMember;
29
31
  exports.isNullableUnion = require_view_validation.isNullableUnion;
30
32
  exports.isStrictAggregateStruct = require_aggregate_rules.isStrictAggregateStruct;
31
33
  exports.isViewField = require_view_validation.isViewField;
package/dist/shared.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { s as stableJson } from "./object-Czia_qD4.cjs";
2
2
  import { n as ENCRYPTED_SEARCH_FEATURES, t as ENCRYPTED_INCOMPATIBLE } from "./search-fields-CHqqcC3W.cjs";
3
- import { AnnotationSpec, AtscriptDoc, SemanticInterfaceNode, SemanticNode, SemanticPropNode, SemanticQueryExprNode, SemanticQueryFieldRefNode, SemanticStructureNode, TAnnotationSpecConfig, TAnnotationTokens, TMessages, TQueryScope, Token } from "@atscript/core";
3
+ import { AnnotationSpec, AtscriptDoc, SemanticInterfaceNode, SemanticNode, SemanticPropNode, SemanticQueryExprNode, SemanticQueryFieldRefNode, SemanticRefNode, SemanticStructureNode, TAnnotationSpecConfig, TAnnotationTokens, TMessages, TQueryScope, Token } from "@atscript/core";
4
4
 
5
5
  //#region src/shared/annotation-utils.d.ts
6
6
  /**
@@ -32,6 +32,11 @@ declare function validateIncompatible(token: Token, tag: string, pairs: Readonly
32
32
  declare function isViewField(field: SemanticNode): boolean;
33
33
  /** Primitive base type (`string`, `number`, `decimal`, …) of a resolved definition, if it is a primitive. */
34
34
  declare function primitiveBaseType(def: SemanticNode | undefined): string | undefined;
35
+ /**
36
+ * `null` in a type expression: the `null` reference or the `null` primitive
37
+ * (whose `config.type` is `"null"`).
38
+ */
39
+ declare function isNullMember(node: SemanticNode): boolean;
35
40
  /**
36
41
  * Whether a definition is a union with a `null` member — `T | null`, or a
37
42
  * ref to an alias of one (compile-time mirror of the runtime nullable union,
@@ -130,6 +135,12 @@ interface TFKFieldMatch {
130
135
  field: string;
131
136
  };
132
137
  }
138
+ /**
139
+ * The chain reference (`User.id`) an `@db.rel.FK` field's type names, else
140
+ * `undefined`. `User.id | null` names it too (since 0.1.165) — the same
141
+ * optional foreign key as `field?: User.id`; any other union does not.
142
+ */
143
+ declare function fkChainRef(def: SemanticNode | undefined): SemanticRefNode | undefined;
133
144
  /**
134
145
  * Find all `@db.rel.FK` fields on a type that reference `targetTypeName`.
135
146
  * Resolves `extends` to include inherited fields.
@@ -258,4 +269,4 @@ declare function encryptedTypeSearchProblem(token: Token, doc: AtscriptDoc): TMe
258
269
  /** Name of the nearest `@db.encrypted` prop enclosing `field` (inline nested objects), if any. */
259
270
  declare function encryptedAncestorName(field: SemanticNode): string | undefined;
260
271
  //#endregion
261
- export { ENCRYPTED_INCOMPATIBLE, ENCRYPTED_SEARCH_FEATURES, NAV_ANNOTATIONS, NOT_DIMENSION_REASON, TFKFieldMatch, TRefArgumentOptions, type TViewFieldSeals, earlierJoinTargets, effectiveAnnotationNames, encryptedAncestorName, encryptedSearchProblem, encryptedTypeSearchProblem, findFKFieldsPointingTo, findStoredField, findViewCycle, forEachFieldRef, getAnnotationAlias, getDbTableOwner, getNavTargetTypeName, getParentStruct, getParentTypeName, hasAnyViewAnnotation, integerMemberIndexProblem, isAliasDecl, isDbSourceDecl, isNavProp, isNullableUnion, isStrictAggregateStruct, isViewField, joinTargets, primitiveBaseType, refActionAnnotation, refuseCiphertext, searchFieldVerdict, stableJson, validateExclusiveWith, validateFieldBaseType, validateIncompatible, validateQueryScope, validateRefArgument, validateSiblingStringField, viewFieldSeals, viewJoins, viewScopeTypes };
272
+ export { ENCRYPTED_INCOMPATIBLE, ENCRYPTED_SEARCH_FEATURES, NAV_ANNOTATIONS, NOT_DIMENSION_REASON, TFKFieldMatch, TRefArgumentOptions, type TViewFieldSeals, earlierJoinTargets, effectiveAnnotationNames, encryptedAncestorName, encryptedSearchProblem, encryptedTypeSearchProblem, findFKFieldsPointingTo, findStoredField, findViewCycle, fkChainRef, forEachFieldRef, getAnnotationAlias, getDbTableOwner, getNavTargetTypeName, getParentStruct, getParentTypeName, hasAnyViewAnnotation, integerMemberIndexProblem, isAliasDecl, isDbSourceDecl, isNavProp, isNullMember, isNullableUnion, isStrictAggregateStruct, isViewField, joinTargets, primitiveBaseType, refActionAnnotation, refuseCiphertext, searchFieldVerdict, stableJson, validateExclusiveWith, validateFieldBaseType, validateIncompatible, validateQueryScope, validateRefArgument, validateSiblingStringField, viewFieldSeals, viewJoins, viewScopeTypes };
package/dist/shared.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { s as stableJson } from "./object-Czia_qD4.mjs";
2
2
  import { n as ENCRYPTED_SEARCH_FEATURES, t as ENCRYPTED_INCOMPATIBLE } from "./search-fields-CHqqcC3W.mjs";
3
- import { AnnotationSpec, AtscriptDoc, SemanticInterfaceNode, SemanticNode, SemanticPropNode, SemanticQueryExprNode, SemanticQueryFieldRefNode, SemanticStructureNode, TAnnotationSpecConfig, TAnnotationTokens, TMessages, TQueryScope, Token } from "@atscript/core";
3
+ import { AnnotationSpec, AtscriptDoc, SemanticInterfaceNode, SemanticNode, SemanticPropNode, SemanticQueryExprNode, SemanticQueryFieldRefNode, SemanticRefNode, SemanticStructureNode, TAnnotationSpecConfig, TAnnotationTokens, TMessages, TQueryScope, Token } from "@atscript/core";
4
4
 
5
5
  //#region src/shared/annotation-utils.d.ts
6
6
  /**
@@ -32,6 +32,11 @@ declare function validateIncompatible(token: Token, tag: string, pairs: Readonly
32
32
  declare function isViewField(field: SemanticNode): boolean;
33
33
  /** Primitive base type (`string`, `number`, `decimal`, …) of a resolved definition, if it is a primitive. */
34
34
  declare function primitiveBaseType(def: SemanticNode | undefined): string | undefined;
35
+ /**
36
+ * `null` in a type expression: the `null` reference or the `null` primitive
37
+ * (whose `config.type` is `"null"`).
38
+ */
39
+ declare function isNullMember(node: SemanticNode): boolean;
35
40
  /**
36
41
  * Whether a definition is a union with a `null` member — `T | null`, or a
37
42
  * ref to an alias of one (compile-time mirror of the runtime nullable union,
@@ -130,6 +135,12 @@ interface TFKFieldMatch {
130
135
  field: string;
131
136
  };
132
137
  }
138
+ /**
139
+ * The chain reference (`User.id`) an `@db.rel.FK` field's type names, else
140
+ * `undefined`. `User.id | null` names it too (since 0.1.165) — the same
141
+ * optional foreign key as `field?: User.id`; any other union does not.
142
+ */
143
+ declare function fkChainRef(def: SemanticNode | undefined): SemanticRefNode | undefined;
133
144
  /**
134
145
  * Find all `@db.rel.FK` fields on a type that reference `targetTypeName`.
135
146
  * Resolves `extends` to include inherited fields.
@@ -258,4 +269,4 @@ declare function encryptedTypeSearchProblem(token: Token, doc: AtscriptDoc): TMe
258
269
  /** Name of the nearest `@db.encrypted` prop enclosing `field` (inline nested objects), if any. */
259
270
  declare function encryptedAncestorName(field: SemanticNode): string | undefined;
260
271
  //#endregion
261
- export { ENCRYPTED_INCOMPATIBLE, ENCRYPTED_SEARCH_FEATURES, NAV_ANNOTATIONS, NOT_DIMENSION_REASON, TFKFieldMatch, TRefArgumentOptions, type TViewFieldSeals, earlierJoinTargets, effectiveAnnotationNames, encryptedAncestorName, encryptedSearchProblem, encryptedTypeSearchProblem, findFKFieldsPointingTo, findStoredField, findViewCycle, forEachFieldRef, getAnnotationAlias, getDbTableOwner, getNavTargetTypeName, getParentStruct, getParentTypeName, hasAnyViewAnnotation, integerMemberIndexProblem, isAliasDecl, isDbSourceDecl, isNavProp, isNullableUnion, isStrictAggregateStruct, isViewField, joinTargets, primitiveBaseType, refActionAnnotation, refuseCiphertext, searchFieldVerdict, stableJson, validateExclusiveWith, validateFieldBaseType, validateIncompatible, validateQueryScope, validateRefArgument, validateSiblingStringField, viewFieldSeals, viewJoins, viewScopeTypes };
272
+ export { ENCRYPTED_INCOMPATIBLE, ENCRYPTED_SEARCH_FEATURES, NAV_ANNOTATIONS, NOT_DIMENSION_REASON, TFKFieldMatch, TRefArgumentOptions, type TViewFieldSeals, earlierJoinTargets, effectiveAnnotationNames, encryptedAncestorName, encryptedSearchProblem, encryptedTypeSearchProblem, findFKFieldsPointingTo, findStoredField, findViewCycle, fkChainRef, forEachFieldRef, getAnnotationAlias, getDbTableOwner, getNavTargetTypeName, getParentStruct, getParentTypeName, hasAnyViewAnnotation, integerMemberIndexProblem, isAliasDecl, isDbSourceDecl, isNavProp, isNullMember, isNullableUnion, isStrictAggregateStruct, isViewField, joinTargets, primitiveBaseType, refActionAnnotation, refuseCiphertext, searchFieldVerdict, stableJson, validateExclusiveWith, validateFieldBaseType, validateIncompatible, validateQueryScope, validateRefArgument, validateSiblingStringField, viewFieldSeals, viewJoins, viewScopeTypes };
package/dist/shared.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as ENCRYPTED_SEARCH_FEATURES, t as ENCRYPTED_INCOMPATIBLE } from "./search-fields-BPNsxrCk.mjs";
2
2
  import { s as stableJson } from "./object-PQ1J8jri.mjs";
3
3
  import { n as isStrictAggregateStruct, t as NOT_DIMENSION_REASON } from "./aggregate-rules-jdPrxqWa.mjs";
4
- import { A as getParentTypeName, B as validateSiblingStringField, C as viewJoins, D as getDbTableOwner, E as getAnnotationAlias, F as refActionAnnotation, I as searchFieldVerdict, L as validateExclusiveWith, M as isNullableUnion, N as isViewField, O as getNavTargetTypeName, P as primitiveBaseType, R as validateFieldBaseType, S as validateRefArgument, T as effectiveAnnotationNames, _ as hasAnyViewAnnotation, a as findStoredField, b as joinTargets, c as refuseCiphertext, d as viewFieldSeals, g as forEachFieldRef, h as findViewCycle, i as encryptedTypeSearchProblem, j as integerMemberIndexProblem, k as getParentStruct, m as findFKFieldsPointingTo, n as encryptedAncestorName, o as isNavProp, p as earlierJoinTargets, r as encryptedSearchProblem, t as NAV_ANNOTATIONS, v as isAliasDecl, w as viewScopeTypes, x as validateQueryScope, y as isDbSourceDecl, z as validateIncompatible } from "./view-validation-BcgDq7M9.mjs";
5
- export { ENCRYPTED_INCOMPATIBLE, ENCRYPTED_SEARCH_FEATURES, NAV_ANNOTATIONS, NOT_DIMENSION_REASON, earlierJoinTargets, effectiveAnnotationNames, encryptedAncestorName, encryptedSearchProblem, encryptedTypeSearchProblem, findFKFieldsPointingTo, findStoredField, findViewCycle, forEachFieldRef, getAnnotationAlias, getDbTableOwner, getNavTargetTypeName, getParentStruct, getParentTypeName, hasAnyViewAnnotation, integerMemberIndexProblem, isAliasDecl, isDbSourceDecl, isNavProp, isNullableUnion, isStrictAggregateStruct, isViewField, joinTargets, primitiveBaseType, refActionAnnotation, refuseCiphertext, searchFieldVerdict, stableJson, validateExclusiveWith, validateFieldBaseType, validateIncompatible, validateQueryScope, validateRefArgument, validateSiblingStringField, viewFieldSeals, viewJoins, viewScopeTypes };
4
+ import { A as getParentStruct, B as validateFieldBaseType, C as validateRefArgument, D as getAnnotationAlias, E as effectiveAnnotationNames, F as isViewField, H as validateSiblingStringField, I as primitiveBaseType, L as refActionAnnotation, M as integerMemberIndexProblem, N as isNullMember, O as getDbTableOwner, P as isNullableUnion, R as searchFieldVerdict, S as validateQueryScope, T as viewScopeTypes, V as validateIncompatible, _ as forEachFieldRef, a as findStoredField, b as isDbSourceDecl, c as refuseCiphertext, d as viewFieldSeals, g as fkChainRef, h as findViewCycle, i as encryptedTypeSearchProblem, j as getParentTypeName, k as getNavTargetTypeName, m as findFKFieldsPointingTo, n as encryptedAncestorName, o as isNavProp, p as earlierJoinTargets, r as encryptedSearchProblem, t as NAV_ANNOTATIONS, v as hasAnyViewAnnotation, w as viewJoins, x as joinTargets, y as isAliasDecl, z as validateExclusiveWith } from "./view-validation-KW77xIKn.mjs";
5
+ export { ENCRYPTED_INCOMPATIBLE, ENCRYPTED_SEARCH_FEATURES, NAV_ANNOTATIONS, NOT_DIMENSION_REASON, earlierJoinTargets, effectiveAnnotationNames, encryptedAncestorName, encryptedSearchProblem, encryptedTypeSearchProblem, findFKFieldsPointingTo, findStoredField, findViewCycle, fkChainRef, forEachFieldRef, getAnnotationAlias, getDbTableOwner, getNavTargetTypeName, getParentStruct, getParentTypeName, hasAnyViewAnnotation, integerMemberIndexProblem, isAliasDecl, isDbSourceDecl, isNavProp, isNullMember, isNullableUnion, isStrictAggregateStruct, isViewField, joinTargets, primitiveBaseType, refActionAnnotation, refuseCiphertext, searchFieldVerdict, stableJson, validateExclusiveWith, validateFieldBaseType, validateIncompatible, validateQueryScope, validateRefArgument, validateSiblingStringField, viewFieldSeals, viewJoins, viewScopeTypes };
package/dist/sync.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_db_error = require("./db-error-DmF1_xrz.cjs");
3
- const require_column_diff = require("./column-diff-BIGX3sRH.cjs");
4
- const require_validator = require("./validator-DK9Ijpo7.cjs");
2
+ const require_column_diff = require("./column-diff-DjeUj9kb.cjs");
3
+ const require_db_error = require("./db-error-G3sgvL87.cjs");
4
+ const require_validator = require("./validator-D7E7T0Oy.cjs");
5
5
  //#region src/schema/json-column-copy.ts
6
6
  const NONE = {
7
7
  copies: [],
@@ -323,11 +323,28 @@ var SyncStore = class {
323
323
  }
324
324
  async ensureControlTable() {
325
325
  if (!this.controlTable) {
326
- const { AtscriptControl } = await Promise.resolve().then(() => require("./control-jIVU2hkf.cjs"));
326
+ const { AtscriptControl } = await Promise.resolve().then(() => require("./control-B5BJypqL.cjs"));
327
327
  this.controlTable = this.space.getTable(AtscriptControl);
328
328
  }
329
329
  await this.controlTable.ensureTable();
330
330
  }
331
+ /**
332
+ * Runs `fn` in a transaction of the control table's adapter family — the
333
+ * space's tables on the same driver join it (since 0.1.165, data steps).
334
+ */
335
+ async inTransaction(fn) {
336
+ const { AtscriptControl } = await Promise.resolve().then(() => require("./control-B5BJypqL.cjs"));
337
+ return this.space.getAdapter(AtscriptControl).withTransaction(fn);
338
+ }
339
+ /** Which of `ids` have a control row — one query (since 0.1.165, data steps). */
340
+ async presentControlIds(ids) {
341
+ if (ids.length === 0) return /* @__PURE__ */ new Set();
342
+ const rows = await this.controlTable.findMany({
343
+ filter: { _id: { $in: [...ids] } },
344
+ controls: { $select: ["_id"] }
345
+ });
346
+ return new Set(rows.map((r) => r._id));
347
+ }
331
348
  async readControlValue(_id) {
332
349
  return (await this.controlTable.findOne({
333
350
  filter: { _id: { $eq: _id } },
@@ -495,7 +512,7 @@ function isExpired(lock, now) {
495
512
  return expiresAt > 0 && expiresAt < now;
496
513
  }
497
514
  async function readStoredSnapshot(space, tableName, _asView) {
498
- const { AtscriptControl } = await Promise.resolve().then(() => require("./control-jIVU2hkf.cjs"));
515
+ const { AtscriptControl } = await Promise.resolve().then(() => require("./control-B5BJypqL.cjs"));
499
516
  const table = space.getTable(AtscriptControl);
500
517
  await table.ensureTable();
501
518
  const value = (await table.findOne({
@@ -1571,6 +1588,33 @@ function removedBlockers(t, removedTables, removedTableSet, dropEdges) {
1571
1588
  } else for (const r of removedTables) if (r.snapshot && "foreignKeys" in r.snapshot && r.snapshot.foreignKeys.some((fk) => fk.targetTable === t.dbName)) direct.push(r.name);
1572
1589
  return reachable(direct, dropEdges);
1573
1590
  }
1591
+ /** The `__atscript_control` key a data step is recorded under. */
1592
+ function dataStepKey(id) {
1593
+ return `data_step:${id}`;
1594
+ }
1595
+ /** The steps as given, after checking their ids are non-empty and unique. */
1596
+ function checkDataSteps(steps) {
1597
+ if (!steps?.length) return [];
1598
+ const seen = /* @__PURE__ */ new Set();
1599
+ for (const step of steps) {
1600
+ if (typeof step.id !== "string" || step.id === "") throw new Error("syncSchema: every data step needs a non-empty id");
1601
+ if (seen.has(step.id)) throw new Error(`syncSchema: duplicate data step id "${step.id}"`);
1602
+ seen.add(step.id);
1603
+ }
1604
+ return steps;
1605
+ }
1606
+ /** `result` with its `dataSteps` report — unchanged when no steps were passed. */
1607
+ function withSteps(result, steps, ran) {
1608
+ if (steps.length === 0) return result;
1609
+ const done = new Set(ran);
1610
+ return {
1611
+ ...result,
1612
+ dataSteps: {
1613
+ ran: [...ran],
1614
+ pending: steps.map((s) => s.id).filter((id) => !done.has(id))
1615
+ }
1616
+ };
1617
+ }
1574
1618
  var SchemaSync = class {
1575
1619
  space;
1576
1620
  store;
@@ -1588,7 +1632,7 @@ var SchemaSync = class {
1588
1632
  const tables = [];
1589
1633
  const views = [];
1590
1634
  const externalViews = [];
1591
- for (const type of types) {
1635
+ for (const type of new Set(types)) {
1592
1636
  if (require_column_diff.aliasTargetOf(type)) continue;
1593
1637
  const readable = this.space.get(type);
1594
1638
  if (readable.isView) {
@@ -1716,7 +1760,8 @@ var SchemaSync = class {
1716
1760
  /**
1717
1761
  * Acquires the sync lock, waiting for a peer that holds it. Resolves
1718
1762
  * `"acquired"`, or `"synced-by-peer"` when a non-forced run finds, after a
1719
- * wait, that the peer stored this schema's hash. A forced run never
1763
+ * wait, that the peer stored this schema's hash (and `peerDone` agrees —
1764
+ * no data step left pending). A forced run never
1720
1765
  * short-circuits — it waits its turn and runs. Losing the race for a freed
1721
1766
  * lock to another waiter just waits again, within the same `waitTimeoutMs`.
1722
1767
  */
@@ -1724,7 +1769,7 @@ var SchemaSync = class {
1724
1769
  const deadline = Date.now() + opts.waitTimeoutMs;
1725
1770
  while (!await this.store.tryAcquireLock(podId, opts.lockTtlMs)) {
1726
1771
  if (!await this.store.waitForLock(deadline - Date.now(), opts.pollIntervalMs)) throw new Error(`Schema sync lock wait timed out after ${opts.waitTimeoutMs}ms`);
1727
- if (!opts.force && await this.store.readHash() === hash) return "synced-by-peer";
1772
+ if (!opts.force && await this.store.readHash() === hash && (!opts.peerDone || await opts.peerDone())) return "synced-by-peer";
1728
1773
  }
1729
1774
  return "acquired";
1730
1775
  }
@@ -1744,6 +1789,30 @@ var SchemaSync = class {
1744
1789
  this.logger.warn("[schema-sync] Failed to release the sync lock — peers wait until it expires:", error instanceof Error ? error.message : error);
1745
1790
  }
1746
1791
  }
1792
+ /** Ids of the steps not recorded yet, in order. */
1793
+ async pendingDataSteps(steps) {
1794
+ const done = await this.store.presentControlIds(steps.map((step) => dataStepKey(step.id)));
1795
+ return steps.filter((step) => !done.has(dataStepKey(step.id))).map((step) => step.id);
1796
+ }
1797
+ /**
1798
+ * Runs the pending steps in order, each with its record in one transaction.
1799
+ * A throwing step propagates (rolled back, not recorded). Returns the ids run.
1800
+ */
1801
+ async runDataSteps(steps, getAbortReason) {
1802
+ const ran = [];
1803
+ const pending = new Set(await this.pendingDataSteps(steps));
1804
+ for (const step of steps) {
1805
+ if (!pending.has(step.id)) continue;
1806
+ this.assertLockHeld(getAbortReason);
1807
+ await this.store.inTransaction(async () => {
1808
+ await step.run(this.space);
1809
+ await this.store.writeControlValue(dataStepKey(step.id), JSON.stringify({ ranAt: Date.now() }));
1810
+ });
1811
+ this.logger.info?.(`[schema-sync] data step "${step.id}" applied`);
1812
+ ran.push(step.id);
1813
+ }
1814
+ return ran;
1815
+ }
1747
1816
  /**
1748
1817
  * Runs schema synchronization with distributed locking, in three phases:
1749
1818
  *
@@ -1768,21 +1837,25 @@ var SchemaSync = class {
1768
1837
  const force = opts?.force ?? false;
1769
1838
  const safe = opts?.safe ?? false;
1770
1839
  const onError = opts?.onError ?? "warn";
1840
+ const steps = checkDataSteps(opts?.dataSteps);
1771
1841
  const resolved = await this.resolveAndHash(types);
1772
1842
  const { hash } = resolved;
1773
1843
  await this.store.ensureControlTable();
1774
1844
  if (!force) {
1775
- if (await this.store.readHash() === hash) return {
1776
- status: "up-to-date",
1777
- schemaHash: hash,
1778
- entries: []
1779
- };
1845
+ if (await this.store.readHash() === hash) {
1846
+ if ((await this.pendingDataSteps(steps)).length === 0) return withSteps({
1847
+ status: "up-to-date",
1848
+ schemaHash: hash,
1849
+ entries: []
1850
+ }, steps, []);
1851
+ }
1780
1852
  }
1781
1853
  if (await this.acquireLock(podId, hash, {
1782
1854
  lockTtlMs,
1783
1855
  waitTimeoutMs,
1784
1856
  pollIntervalMs,
1785
- force
1857
+ force,
1858
+ peerDone: async () => (await this.pendingDataSteps(steps)).length === 0
1786
1859
  }) === "synced-by-peer") return {
1787
1860
  status: "synced-by-peer",
1788
1861
  schemaHash: hash,
@@ -1791,11 +1864,14 @@ var SchemaSync = class {
1791
1864
  const heartbeat = this.startHeartbeat(podId, lockTtlMs);
1792
1865
  try {
1793
1866
  if (!force) {
1794
- if (await this.store.readHash() === hash) return {
1795
- status: "synced-by-peer",
1796
- schemaHash: hash,
1797
- entries: []
1798
- };
1867
+ if (await this.store.readHash() === hash) {
1868
+ const ran = await this.runDataSteps(steps, heartbeat.getAbortReason);
1869
+ return withSteps({
1870
+ status: ran.length > 0 ? "synced" : "synced-by-peer",
1871
+ schemaHash: hash,
1872
+ entries: []
1873
+ }, steps, ran);
1874
+ }
1799
1875
  }
1800
1876
  const d = await this.discover(resolved, safe);
1801
1877
  this.assertLockHeld(heartbeat.getAbortReason);
@@ -1808,11 +1884,11 @@ var SchemaSync = class {
1808
1884
  entries: this.buildPlanEntries(d, safe, refusals)
1809
1885
  };
1810
1886
  this.reportOutcome(result, onError);
1811
- return result;
1887
+ return withSteps(result, steps, []);
1812
1888
  }
1813
1889
  const result = await this.execute(d, safe, heartbeat.getAbortReason);
1814
1890
  this.reportOutcome(result, onError);
1815
- return result;
1891
+ return withSteps(result, steps, result.entries.some((e) => e.status === "error") ? [] : await this.runDataSteps(steps, heartbeat.getAbortReason));
1816
1892
  } finally {
1817
1893
  await heartbeat.stop();
1818
1894
  await this.releaseLock(podId);
@@ -2437,6 +2513,7 @@ var SchemaSync = class {
2437
2513
  * as `error` entries (`refused: true`) exactly as `run()` would report them.
2438
2514
  */
2439
2515
  async plan(types, opts) {
2516
+ const steps = checkDataSteps(opts?.dataSteps);
2440
2517
  const force = opts?.force ?? false;
2441
2518
  const safe = opts?.safe ?? false;
2442
2519
  const resolved = await this.resolveAndHash(types);
@@ -2446,18 +2523,23 @@ var SchemaSync = class {
2446
2523
  const refusals = this.preflight(d, safe);
2447
2524
  await this.planLiveWork(d, safe);
2448
2525
  const entries = this.buildPlanEntries(d, safe, refusals);
2526
+ const pending = steps.length > 0 ? await this.pendingDataSteps(steps) : void 0;
2527
+ const withPending = (plan) => pending ? {
2528
+ ...plan,
2529
+ pendingDataSteps: pending
2530
+ } : plan;
2449
2531
  if (!force) {
2450
- if (await this.store.readHash() === hash) return {
2532
+ if (await this.store.readHash() === hash && !pending?.length) return withPending({
2451
2533
  status: "up-to-date",
2452
2534
  schemaHash: hash,
2453
2535
  entries
2454
- };
2536
+ });
2455
2537
  }
2456
- return {
2538
+ return withPending({
2457
2539
  status: "changes-needed",
2458
2540
  schemaHash: hash,
2459
2541
  entries
2460
- };
2542
+ });
2461
2543
  }
2462
2544
  /** Fallback typeMapper for snapshot-based Path B: compares designType directly, skips unions. */
2463
2545
  resolveTypeMapper(adapter) {