@minnowdb/core 0.3.0 → 0.4.0

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 (276) hide show
  1. package/README.md +4 -2
  2. package/dist/block-format/block.d.ts +32 -13
  3. package/dist/block-format/block.js +179 -55
  4. package/dist/block-format/checksum.d.ts +2 -1
  5. package/dist/block-format/checksum.js +5 -2
  6. package/dist/block-format/codecs.d.ts +7 -2
  7. package/dist/block-format/codecs.js +53 -12
  8. package/dist/block-format/column.d.ts +3 -2
  9. package/dist/block-format/column.js +96 -30
  10. package/dist/block-format/index.d.ts +2 -2
  11. package/dist/block-format/index.js +2 -2
  12. package/dist/block-format/physical.d.ts +8 -2
  13. package/dist/block-format/physical.js +14 -6
  14. package/dist/block-format/types.d.ts +7 -3
  15. package/dist/block-format/types.js +0 -1
  16. package/dist/block-format/unicode.d.ts +11 -0
  17. package/dist/block-format/unicode.js +46 -0
  18. package/dist/date-value.d.ts +15 -0
  19. package/dist/date-value.js +64 -0
  20. package/dist/engine/artifact-cache.d.ts +0 -1
  21. package/dist/engine/artifact-cache.js +0 -1
  22. package/dist/engine/batch.d.ts +5 -2
  23. package/dist/engine/batch.js +0 -1
  24. package/dist/engine/buffered-writer.d.ts +2 -1
  25. package/dist/engine/buffered-writer.js +34 -9
  26. package/dist/engine/cache-limits.d.ts +25 -0
  27. package/dist/engine/cache-limits.js +25 -0
  28. package/dist/engine/catalog.d.ts +5 -6
  29. package/dist/engine/catalog.js +5 -5
  30. package/dist/engine/client.d.ts +27 -7
  31. package/dist/engine/client.js +197 -31
  32. package/dist/engine/database.d.ts +161 -38
  33. package/dist/engine/database.js +4320 -1523
  34. package/dist/engine/defaults.d.ts +0 -1
  35. package/dist/engine/defaults.js +2 -4
  36. package/dist/engine/errors.d.ts +19 -1
  37. package/dist/engine/errors.js +32 -2
  38. package/dist/engine/fts.d.ts +0 -6
  39. package/dist/engine/fts.js +39 -14
  40. package/dist/engine/group-index.d.ts +0 -1
  41. package/dist/engine/group-index.js +6 -11
  42. package/dist/engine/index.d.ts +16 -11
  43. package/dist/engine/index.js +12 -10
  44. package/dist/engine/join-index.d.ts +0 -1
  45. package/dist/engine/join-index.js +2 -2
  46. package/dist/engine/keyed-live.d.ts +57 -0
  47. package/dist/engine/keyed-live.js +226 -0
  48. package/dist/engine/live-api.d.ts +4 -0
  49. package/dist/engine/live-api.js +4 -0
  50. package/dist/engine/live.d.ts +43 -26
  51. package/dist/engine/live.js +499 -129
  52. package/dist/engine/memory.d.ts +2 -1
  53. package/dist/engine/memory.js +3 -2
  54. package/dist/engine/optimizer.d.ts +0 -1
  55. package/dist/engine/optimizer.js +4 -4
  56. package/dist/engine/query-api.d.ts +3 -0
  57. package/dist/engine/query-api.js +3 -0
  58. package/dist/engine/query-cache.d.ts +0 -1
  59. package/dist/engine/query-cache.js +4 -4
  60. package/dist/engine/query.d.ts +4 -7
  61. package/dist/engine/query.js +348 -155
  62. package/dist/engine/result-wire.d.ts +0 -1
  63. package/dist/engine/result-wire.js +2 -2
  64. package/dist/engine/schema-wire.d.ts +1 -2
  65. package/dist/engine/schema-wire.js +0 -1
  66. package/dist/engine/schema.d.ts +3 -8
  67. package/dist/engine/schema.js +41 -24
  68. package/dist/engine/sort-keys.d.ts +0 -1
  69. package/dist/engine/sort-keys.js +2 -2
  70. package/dist/engine/sql-domains.d.ts +6 -1
  71. package/dist/engine/sql-domains.js +209 -45
  72. package/dist/engine/sql-driver.d.ts +2 -2
  73. package/dist/engine/sql-driver.js +0 -1
  74. package/dist/engine/sql-json.d.ts +0 -1
  75. package/dist/engine/sql-json.js +53 -7
  76. package/dist/engine/sql-semantics.d.ts +8 -5
  77. package/dist/engine/sql-semantics.js +427 -65
  78. package/dist/engine/typed-live.d.ts +67 -0
  79. package/dist/engine/typed-live.js +349 -0
  80. package/dist/engine/vector.d.ts +7 -1
  81. package/dist/engine/vector.js +202 -58
  82. package/dist/engine/worker-host.d.ts +8 -2
  83. package/dist/engine/worker-host.js +616 -152
  84. package/dist/engine/worker.d.ts +0 -1
  85. package/dist/engine/worker.js +3 -3
  86. package/dist/engine/write-block-planner.d.ts +18 -0
  87. package/dist/engine/write-block-planner.js +112 -0
  88. package/dist/index.d.ts +0 -1
  89. package/dist/index.js +0 -1
  90. package/dist/plan/index.d.ts +0 -1
  91. package/dist/plan/index.js +0 -1
  92. package/dist/storage/index.d.ts +1 -1
  93. package/dist/storage/index.js +1 -1
  94. package/dist/storage/indexeddb.d.ts +74 -65
  95. package/dist/storage/indexeddb.js +12301 -2076
  96. package/dist/storage/memory.d.ts +56 -66
  97. package/dist/storage/memory.js +1004 -139
  98. package/dist/storage/opfs/files.d.ts +18 -7
  99. package/dist/storage/opfs/files.js +115 -28
  100. package/dist/storage/opfs/index.d.ts +1 -1
  101. package/dist/storage/opfs/index.js +1 -1
  102. package/dist/storage/opfs/leader.d.ts +403 -91
  103. package/dist/storage/opfs/leader.js +4306 -602
  104. package/dist/storage/opfs/rpc.d.ts +15 -3
  105. package/dist/storage/opfs/rpc.js +223 -2
  106. package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
  107. package/dist/storage/opfs/snapshot-ledger.js +281 -0
  108. package/dist/storage/opfs/store.d.ts +26 -121
  109. package/dist/storage/opfs/store.js +436 -388
  110. package/dist/storage/persistence.d.ts +37 -0
  111. package/dist/storage/persistence.js +78 -0
  112. package/dist/storage/snapshot-stream.d.ts +24 -0
  113. package/dist/storage/snapshot-stream.js +897 -0
  114. package/dist/storage/snapshot.d.ts +9 -99
  115. package/dist/storage/snapshot.js +33 -309
  116. package/dist/storage/toolkit/extents.d.ts +41 -7
  117. package/dist/storage/toolkit/extents.js +402 -39
  118. package/dist/storage/toolkit/index.d.ts +3 -4
  119. package/dist/storage/toolkit/index.js +27 -3
  120. package/dist/storage/toolkit/record-core.d.ts +174 -61
  121. package/dist/storage/toolkit/record-core.js +5401 -849
  122. package/dist/storage/toolkit/sync-file.d.ts +10 -1
  123. package/dist/storage/toolkit/sync-file.js +50 -2
  124. package/dist/storage/toolkit/wal.d.ts +23 -5
  125. package/dist/storage/toolkit/wal.js +89 -27
  126. package/dist/storage/toolkit/wire.d.ts +4 -6
  127. package/dist/storage/toolkit/wire.js +80 -25
  128. package/dist/storage/types.d.ts +1271 -218
  129. package/dist/storage/types.js +1471 -138
  130. package/dist/testing/block-store-conformance.d.ts +0 -1
  131. package/dist/testing/block-store-conformance.js +877 -138
  132. package/dist/testing/index.d.ts +42 -31
  133. package/dist/testing/index.js +100 -68
  134. package/dist/testing/opfs-shim.d.ts +13 -3
  135. package/dist/testing/opfs-shim.js +40 -6
  136. package/dist/testing/simulator.d.ts +0 -1
  137. package/dist/testing/simulator.js +43 -12
  138. package/dist/testing/sqllogictest.d.ts +0 -1
  139. package/dist/testing/sqllogictest.js +0 -1
  140. package/dist/transactions/index.d.ts +81 -20
  141. package/dist/transactions/index.js +1050 -268
  142. package/dist/worker-protocol/index.d.ts +3 -2
  143. package/dist/worker-protocol/index.js +2 -5
  144. package/package.json +49 -1
  145. package/dist/block-format/block.d.ts.map +0 -1
  146. package/dist/block-format/block.js.map +0 -1
  147. package/dist/block-format/checksum.d.ts.map +0 -1
  148. package/dist/block-format/checksum.js.map +0 -1
  149. package/dist/block-format/codecs.d.ts.map +0 -1
  150. package/dist/block-format/codecs.js.map +0 -1
  151. package/dist/block-format/column.d.ts.map +0 -1
  152. package/dist/block-format/column.js.map +0 -1
  153. package/dist/block-format/index.d.ts.map +0 -1
  154. package/dist/block-format/index.js.map +0 -1
  155. package/dist/block-format/physical.d.ts.map +0 -1
  156. package/dist/block-format/physical.js.map +0 -1
  157. package/dist/block-format/types.d.ts.map +0 -1
  158. package/dist/block-format/types.js.map +0 -1
  159. package/dist/engine/artifact-cache.d.ts.map +0 -1
  160. package/dist/engine/artifact-cache.js.map +0 -1
  161. package/dist/engine/batch.d.ts.map +0 -1
  162. package/dist/engine/batch.js.map +0 -1
  163. package/dist/engine/buffered-writer.d.ts.map +0 -1
  164. package/dist/engine/buffered-writer.js.map +0 -1
  165. package/dist/engine/catalog.d.ts.map +0 -1
  166. package/dist/engine/catalog.js.map +0 -1
  167. package/dist/engine/client.d.ts.map +0 -1
  168. package/dist/engine/client.js.map +0 -1
  169. package/dist/engine/coordinator.d.ts +0 -17
  170. package/dist/engine/coordinator.d.ts.map +0 -1
  171. package/dist/engine/coordinator.js +0 -60
  172. package/dist/engine/coordinator.js.map +0 -1
  173. package/dist/engine/database.d.ts.map +0 -1
  174. package/dist/engine/database.js.map +0 -1
  175. package/dist/engine/defaults.d.ts.map +0 -1
  176. package/dist/engine/defaults.js.map +0 -1
  177. package/dist/engine/errors.d.ts.map +0 -1
  178. package/dist/engine/errors.js.map +0 -1
  179. package/dist/engine/fts.d.ts.map +0 -1
  180. package/dist/engine/fts.js.map +0 -1
  181. package/dist/engine/group-index.d.ts.map +0 -1
  182. package/dist/engine/group-index.js.map +0 -1
  183. package/dist/engine/index.d.ts.map +0 -1
  184. package/dist/engine/index.js.map +0 -1
  185. package/dist/engine/join-index.d.ts.map +0 -1
  186. package/dist/engine/join-index.js.map +0 -1
  187. package/dist/engine/live.d.ts.map +0 -1
  188. package/dist/engine/live.js.map +0 -1
  189. package/dist/engine/memory.d.ts.map +0 -1
  190. package/dist/engine/memory.js.map +0 -1
  191. package/dist/engine/optimizer.d.ts.map +0 -1
  192. package/dist/engine/optimizer.js.map +0 -1
  193. package/dist/engine/query-cache.d.ts.map +0 -1
  194. package/dist/engine/query-cache.js.map +0 -1
  195. package/dist/engine/query.d.ts.map +0 -1
  196. package/dist/engine/query.js.map +0 -1
  197. package/dist/engine/result-wire.d.ts.map +0 -1
  198. package/dist/engine/result-wire.js.map +0 -1
  199. package/dist/engine/schema-wire.d.ts.map +0 -1
  200. package/dist/engine/schema-wire.js.map +0 -1
  201. package/dist/engine/schema.d.ts.map +0 -1
  202. package/dist/engine/schema.js.map +0 -1
  203. package/dist/engine/sort-keys.d.ts.map +0 -1
  204. package/dist/engine/sort-keys.js.map +0 -1
  205. package/dist/engine/sql-domains.d.ts.map +0 -1
  206. package/dist/engine/sql-domains.js.map +0 -1
  207. package/dist/engine/sql-driver.d.ts.map +0 -1
  208. package/dist/engine/sql-driver.js.map +0 -1
  209. package/dist/engine/sql-json.d.ts.map +0 -1
  210. package/dist/engine/sql-json.js.map +0 -1
  211. package/dist/engine/sql-semantics.d.ts.map +0 -1
  212. package/dist/engine/sql-semantics.js.map +0 -1
  213. package/dist/engine/vector.d.ts.map +0 -1
  214. package/dist/engine/vector.js.map +0 -1
  215. package/dist/engine/worker-host.d.ts.map +0 -1
  216. package/dist/engine/worker-host.js.map +0 -1
  217. package/dist/engine/worker.d.ts.map +0 -1
  218. package/dist/engine/worker.js.map +0 -1
  219. package/dist/index.d.ts.map +0 -1
  220. package/dist/index.js.map +0 -1
  221. package/dist/plan/index.d.ts.map +0 -1
  222. package/dist/plan/index.js.map +0 -1
  223. package/dist/storage/fixture-shape.d.ts +0 -42
  224. package/dist/storage/fixture-shape.d.ts.map +0 -1
  225. package/dist/storage/fixture-shape.js +0 -146
  226. package/dist/storage/fixture-shape.js.map +0 -1
  227. package/dist/storage/index.d.ts.map +0 -1
  228. package/dist/storage/index.js.map +0 -1
  229. package/dist/storage/indexeddb.d.ts.map +0 -1
  230. package/dist/storage/indexeddb.js.map +0 -1
  231. package/dist/storage/memory.d.ts.map +0 -1
  232. package/dist/storage/memory.js.map +0 -1
  233. package/dist/storage/opfs/files.d.ts.map +0 -1
  234. package/dist/storage/opfs/files.js.map +0 -1
  235. package/dist/storage/opfs/index.d.ts.map +0 -1
  236. package/dist/storage/opfs/index.js.map +0 -1
  237. package/dist/storage/opfs/leader.d.ts.map +0 -1
  238. package/dist/storage/opfs/leader.js.map +0 -1
  239. package/dist/storage/opfs/rpc.d.ts.map +0 -1
  240. package/dist/storage/opfs/rpc.js.map +0 -1
  241. package/dist/storage/opfs/store.d.ts.map +0 -1
  242. package/dist/storage/opfs/store.js.map +0 -1
  243. package/dist/storage/snapshot.d.ts.map +0 -1
  244. package/dist/storage/snapshot.js.map +0 -1
  245. package/dist/storage/toolkit/extents.d.ts.map +0 -1
  246. package/dist/storage/toolkit/extents.js.map +0 -1
  247. package/dist/storage/toolkit/index.d.ts.map +0 -1
  248. package/dist/storage/toolkit/index.js.map +0 -1
  249. package/dist/storage/toolkit/record-core.d.ts.map +0 -1
  250. package/dist/storage/toolkit/record-core.js.map +0 -1
  251. package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
  252. package/dist/storage/toolkit/sync-file.js.map +0 -1
  253. package/dist/storage/toolkit/wal.d.ts.map +0 -1
  254. package/dist/storage/toolkit/wal.js.map +0 -1
  255. package/dist/storage/toolkit/wire.d.ts.map +0 -1
  256. package/dist/storage/toolkit/wire.js.map +0 -1
  257. package/dist/storage/types.d.ts.map +0 -1
  258. package/dist/storage/types.js.map +0 -1
  259. package/dist/testing/block-store-conformance.d.ts.map +0 -1
  260. package/dist/testing/block-store-conformance.js.map +0 -1
  261. package/dist/testing/index.d.ts.map +0 -1
  262. package/dist/testing/index.js.map +0 -1
  263. package/dist/testing/opfs-shim.d.ts.map +0 -1
  264. package/dist/testing/opfs-shim.js.map +0 -1
  265. package/dist/testing/seeds.d.ts +0 -11
  266. package/dist/testing/seeds.d.ts.map +0 -1
  267. package/dist/testing/seeds.js +0 -50
  268. package/dist/testing/seeds.js.map +0 -1
  269. package/dist/testing/simulator.d.ts.map +0 -1
  270. package/dist/testing/simulator.js.map +0 -1
  271. package/dist/testing/sqllogictest.d.ts.map +0 -1
  272. package/dist/testing/sqllogictest.js.map +0 -1
  273. package/dist/transactions/index.d.ts.map +0 -1
  274. package/dist/transactions/index.js.map +0 -1
  275. package/dist/worker-protocol/index.d.ts.map +0 -1
  276. package/dist/worker-protocol/index.js.map +0 -1
@@ -1,3 +1,5 @@
1
+ import { dateIsoString, dateMilliseconds } from "../date-value.js";
2
+ import { assertWellFormedString, MAX_BLOCK_ROW_COUNT, MAX_STORED_BLOCK_BYTE_LENGTH, } from "../block-format/index.js";
1
3
  export const storeNames = [
2
4
  "catalog",
3
5
  "manifests",
@@ -9,20 +11,23 @@ export const storeNames = [
9
11
  "temp",
10
12
  "gc",
11
13
  ];
12
- /** Every this-many commits the store writes a full checkpoint instead of a delta. */
13
- export const MANIFEST_CHECKPOINT_INTERVAL = 32;
14
- /** Applies one stored record to a running block set (checkpoint replaces, delta mutates). */
15
- export function applyManifestRecord(blockIds, record) {
16
- if (record.blockIds !== undefined) {
17
- blockIds.clear();
18
- for (const id of record.blockIds)
19
- blockIds.add(id);
20
- return;
14
+ export const MAX_MANIFEST_CHANGED_TABLE_IDS = 1_024;
15
+ export function canonicalManifestChangedTableIds(ids) {
16
+ if (ids.length > MAX_MANIFEST_CHANGED_TABLE_IDS) {
17
+ throw new RangeError(`Manifest changed-table IDs cannot exceed ${String(MAX_MANIFEST_CHANGED_TABLE_IDS)}`);
18
+ }
19
+ const canonical = [...new Set(ids.map((id) => validateStorageId(id, "Changed table ID")))].sort();
20
+ if (canonical.length > MAX_MANIFEST_CHANGED_TABLE_IDS) {
21
+ throw new RangeError(`Manifest changed-table IDs cannot exceed ${String(MAX_MANIFEST_CHANGED_TABLE_IDS)}`);
22
+ }
23
+ return canonical;
24
+ }
25
+ export function validateCanonicalManifestChangedTableIds(ids) {
26
+ const canonical = canonicalManifestChangedTableIds(ids);
27
+ if (canonical.length !== ids.length || canonical.some((id, index) => id !== ids[index])) {
28
+ throw new TypeError("Manifest changed-table IDs must be unique and lexically sorted");
21
29
  }
22
- for (const id of record.removedBlockIds ?? [])
23
- blockIds.delete(id);
24
- for (const id of record.addedBlockIds ?? [])
25
- blockIds.add(id);
30
+ return canonical;
26
31
  }
27
32
  export const simpleDataTypes = ["boolean", "number", "string", "datetime"];
28
33
  /** Validates the logical metadata shared by SQL DDL, the schema DSL, and restored catalogs. */
@@ -48,6 +53,17 @@ export function validateSqlDomain(domain, context) {
48
53
  }
49
54
  return structuredClone(domain);
50
55
  }
56
+ export const MAX_TABLE_COLUMNS = 1_024;
57
+ export const MAX_ENUM_VALUES = 4_096;
58
+ export const MAX_SECONDARY_INDEXES = 1_024;
59
+ export const MAX_TABLE_TRIGGERS = 256;
60
+ export const MAX_TRIGGER_STATEMENTS = 256;
61
+ const VALID_TRIGGER_EVENTS = ["insert", "update", "delete"];
62
+ const VALID_TRIGGER_TIMINGS = ["before", "after"];
63
+ const VALID_TRIGGER_BINDING_SOURCES = ["new", "old"];
64
+ export const MAX_TABLE_CONSTRAINTS = 1_024;
65
+ export const MAX_TABLE_RECORD_CHARACTERS = 1_048_576;
66
+ export const MAX_TABLE_RECORD_ENTRIES = 65_536;
51
67
  /**
52
68
  * The single authority on which enum declarations are legal, shared by the schema DSL's
53
69
  * `column.enum()` and the engine's `createTable`: at least one value, every value a non-empty
@@ -57,6 +73,9 @@ export function validateEnumValues(values, context) {
57
73
  if (values.length === 0) {
58
74
  throw new TypeError(`An enum needs at least one value: ${context}`);
59
75
  }
76
+ if (values.length > MAX_ENUM_VALUES) {
77
+ throw new RangeError(`An enum cannot exceed ${String(MAX_ENUM_VALUES)} values: ${context}`);
78
+ }
60
79
  const seen = new Set();
61
80
  for (const value of values) {
62
81
  if (typeof value !== "string" || value.length === 0) {
@@ -75,9 +94,21 @@ export function validateEnumValues(values, context) {
75
94
  * metadata cannot enter through a less common persistence path.
76
95
  */
77
96
  export function validateTableColumns(columns) {
78
- if (columns.length === 0)
97
+ const runtimeColumns = columns;
98
+ if (!Array.isArray(runtimeColumns) || columns.length === 0) {
79
99
  throw new TypeError("A table needs at least one column");
80
- for (const column of columns) {
100
+ }
101
+ if (columns.length > MAX_TABLE_COLUMNS) {
102
+ throw new RangeError(`A table cannot exceed ${String(MAX_TABLE_COLUMNS)} columns`);
103
+ }
104
+ const runtimeEntries = runtimeColumns;
105
+ for (const entry of runtimeEntries) {
106
+ if (!hasValidColumnPrimitives(entry)) {
107
+ throw new TypeError("Table columns have invalid primitive metadata");
108
+ }
109
+ const column = entry;
110
+ validateStorageId(column.id, "Column ID");
111
+ validateCatalogName(column.name, "Column name");
81
112
  const integer = column.integer;
82
113
  if (integer !== undefined && (integer !== true || column.type !== "number")) {
83
114
  throw new TypeError(`Integer domain requires a number column: ${column.name}`);
@@ -93,6 +124,37 @@ export function validateTableColumns(columns) {
93
124
  if (column.enumValues !== undefined && column.sqlDomain !== undefined) {
94
125
  throw new TypeError(`A column cannot have both enum restrictions and a SQL domain: ${column.name}`);
95
126
  }
127
+ if (column.enumValues !== undefined) {
128
+ if (column.type !== "string") {
129
+ throw new TypeError(`Enum restrictions require a string column: ${column.name}`);
130
+ }
131
+ validateEnumValues(column.enumValues, column.name);
132
+ }
133
+ if (column.defaultValue !== undefined) {
134
+ validateColumnDefault({
135
+ name: column.name,
136
+ type: column.type,
137
+ nullable: column.nullable,
138
+ isUniqueKey: true,
139
+ ...(column.integer === undefined ? {} : { integer: column.integer }),
140
+ ...(column.sqlDomain === undefined ? {} : { sqlDomain: column.sqlDomain }),
141
+ ...(column.enumValues === undefined ? {} : { enumValues: column.enumValues }),
142
+ }, column.defaultValue);
143
+ }
144
+ if (column.backfill !== undefined) {
145
+ const value = column.backfill;
146
+ const validType = column.type === "datetime"
147
+ ? value instanceof Date && Number.isFinite(dateMilliseconds(value))
148
+ : typeof value === column.type;
149
+ if (!validType ||
150
+ (typeof value === "number" && !Number.isFinite(value)) ||
151
+ (column.integer === true && !Number.isSafeInteger(value)) ||
152
+ (column.enumValues !== undefined &&
153
+ typeof value === "string" &&
154
+ !column.enumValues.includes(value))) {
155
+ throw new TypeError(`Invalid backfill value: ${column.name}`);
156
+ }
157
+ }
96
158
  }
97
159
  const ids = new Set(columns.map(({ id }) => id));
98
160
  const names = new Set(columns.map(({ name }) => name));
@@ -100,6 +162,18 @@ export function validateTableColumns(columns) {
100
162
  throw new TypeError("Table columns must have unique IDs and names");
101
163
  }
102
164
  }
165
+ function hasValidColumnPrimitives(value) {
166
+ if (value === null || typeof value !== "object")
167
+ return false;
168
+ const column = value;
169
+ return (typeof column.id === "string" &&
170
+ column.id.length > 0 &&
171
+ typeof column.name === "string" &&
172
+ column.name.length > 0 &&
173
+ simpleDataTypes.some((type) => type === column.type) &&
174
+ typeof column.nullable === "boolean" &&
175
+ (column.hidden === undefined || column.hidden === true));
176
+ }
103
177
  /**
104
178
  * The single authority on which default declarations are legal, shared by the schema DSL's
105
179
  * `table()` and the engine's `createTable` so the two entry points (and the wire path between
@@ -127,7 +201,7 @@ export function validateColumnDefault(column, defaultValue) {
127
201
  const value = defaultValue.value;
128
202
  const numericDomain = column.sqlDomain?.kind === "numeric";
129
203
  const correctType = column.type === "datetime"
130
- ? value instanceof Date && Number.isFinite(value.getTime())
204
+ ? value instanceof Date && Number.isFinite(dateMilliseconds(value))
131
205
  : numericDomain
132
206
  ? typeof value === "number" || typeof value === "string"
133
207
  : typeof value === column.type;
@@ -157,8 +231,220 @@ export function validateColumnDefault(column, defaultValue) {
157
231
  throw new TypeError(`Unknown default kind: ${String(defaultValue.kind)}`);
158
232
  }
159
233
  }
234
+ /**
235
+ * Whether a secondary-index replacement changes the contract a staged writer must honor.
236
+ * Physical posting-build state is deliberately excluded: postings are only a reverified pruning
237
+ * accelerator. Index identity, key shape, and UNIQUE enforcement are structural.
238
+ */
239
+ export function secondaryIndexWriteContractChanged(previous, next) {
240
+ const previousEntries = Object.entries(previous ?? {}).sort(([left], [right]) => left.localeCompare(right));
241
+ const nextEntries = Object.entries(next ?? {}).sort(([left], [right]) => left.localeCompare(right));
242
+ if (previousEntries.length !== nextEntries.length)
243
+ return true;
244
+ return previousEntries.some(([id, left], position) => {
245
+ const rightEntry = nextEntries[position];
246
+ if (rightEntry?.[0] !== id)
247
+ return true;
248
+ const right = rightEntry[1];
249
+ return (left.name !== right.name ||
250
+ left.columnId !== right.columnId ||
251
+ left.columnIds.length !== right.columnIds.length ||
252
+ left.columnIds.some((columnId, index) => columnId !== right.columnIds[index]) ||
253
+ left.directions.length !== right.directions.length ||
254
+ left.directions.some((direction, index) => direction !== right.directions[index]) ||
255
+ left.unique !== right.unique ||
256
+ left.uniqueEnforced !== right.uniqueEnforced ||
257
+ left.storageColumnId !== right.storageColumnId ||
258
+ left.locator !== right.locator);
259
+ });
260
+ }
261
+ const catalogRecordTextEncoder = new TextEncoder();
262
+ function durableRecordRetainedBytes(record, label) {
263
+ // Keep this byte accounting identical to the canonical record-wire JSON codec without
264
+ // importing the toolkit codec back into this leaf contract module. Segment records always
265
+ // contain bigint row envelopes, so plain JSON.stringify is not sufficient.
266
+ let json;
267
+ try {
268
+ json = JSON.stringify(record);
269
+ }
270
+ catch {
271
+ json = JSON.stringify(record, (_key, entry) => {
272
+ if (typeof entry !== "bigint")
273
+ return entry;
274
+ if (entry < 0n || entry > MAX_ROW_ID_EXCLUSIVE_END) {
275
+ throw new RangeError("Record bigint exceeds the unsigned 64-bit persisted range");
276
+ }
277
+ return { $n: entry.toString() };
278
+ });
279
+ }
280
+ if (typeof json !== "string")
281
+ throw new TypeError(`${label} is not JSON-serializable`);
282
+ return catalogRecordTextEncoder.encode(json).byteLength;
283
+ }
284
+ /** Exact UTF-8 bytes used when the canonical record-wire JSON codec persists a table record. */
285
+ export function catalogRecordRetainedBytes(record) {
286
+ return durableRecordRetainedBytes(record, "Table record");
287
+ }
288
+ export function manifestRecordRetainedBytes(record) {
289
+ return durableRecordRetainedBytes(record, "Manifest record");
290
+ }
291
+ /**
292
+ * Admission charge for one manifest summary, including the exact canonical tombstone bytes that
293
+ * later reclamation may add. Reserving them up front prevents quota deadlock at the byte ceiling.
294
+ */
295
+ export function manifestRecordRetainedReservationBytes(record) {
296
+ return manifestRecordRetainedBytes(record.prunedAt === undefined ? { ...record, prunedAt: "1970-01-01T00:00:00.000Z" } : record);
297
+ }
298
+ export function segmentRecordRetainedBytes(record) {
299
+ return durableRecordRetainedBytes(record, "Segment record");
300
+ }
301
+ /** Hard catalog cardinality/text bounds shared by engine, adapters, checkpoints, and snapshots. */
302
+ export function validateTableRecordBounds(record) {
303
+ validateStorageId(record.id, "Table ID");
304
+ validateCatalogName(record.name, "Table name");
305
+ if (typeof record.managed !== "boolean")
306
+ throw new TypeError("Table managed flag is required");
307
+ if (!Number.isSafeInteger(record.revision) || record.revision < 0) {
308
+ throw new RangeError("Table revision must be a non-negative safe integer");
309
+ }
310
+ if (record.view !== undefined && typeof record.view.managed !== "boolean") {
311
+ throw new TypeError("View managed flag is required");
312
+ }
313
+ if ((record.primaryKeyColumnIds?.length ?? 0) > MAX_TABLE_COLUMNS) {
314
+ throw new RangeError("A primary key names too many columns");
315
+ }
316
+ if (Object.keys(record.ftsColumns ?? {}).length > MAX_TABLE_COLUMNS) {
317
+ throw new RangeError("A table has too many full-text columns");
318
+ }
319
+ if (Object.keys(record.secondaryIndexes ?? {}).length > MAX_SECONDARY_INDEXES) {
320
+ throw new RangeError(`A table cannot exceed ${String(MAX_SECONDARY_INDEXES)} secondary indexes`);
321
+ }
322
+ if ((record.triggers?.length ?? 0) > MAX_TABLE_TRIGGERS) {
323
+ throw new RangeError(`A table cannot exceed ${String(MAX_TABLE_TRIGGERS)} triggers`);
324
+ }
325
+ const triggerIds = new Set();
326
+ const triggerNames = new Set();
327
+ const columnNames = new Set(record.columns.map((column) => column.name));
328
+ for (const trigger of record.triggers ?? []) {
329
+ validateStorageId(trigger.id, "Trigger ID");
330
+ validateCatalogName(trigger.name, "Trigger name");
331
+ if (!VALID_TRIGGER_EVENTS.includes(trigger.event)) {
332
+ throw new TypeError(`Trigger event is invalid: ${trigger.name}`);
333
+ }
334
+ if (!VALID_TRIGGER_TIMINGS.includes(trigger.timing)) {
335
+ throw new TypeError(`Trigger timing is invalid: ${trigger.name}`);
336
+ }
337
+ if (triggerIds.has(trigger.id)) {
338
+ throw new TypeError(`Trigger ID already exists: ${trigger.id}`);
339
+ }
340
+ if (triggerNames.has(trigger.name)) {
341
+ throw new TypeError(`Trigger already exists: ${trigger.name}`);
342
+ }
343
+ triggerIds.add(trigger.id);
344
+ triggerNames.add(trigger.name);
345
+ if (typeof trigger.createdAt !== "string") {
346
+ throw new TypeError(`Trigger creation timestamp is invalid: ${trigger.name}`);
347
+ }
348
+ const triggerCreatedAt = Date.parse(trigger.createdAt);
349
+ if (!Number.isFinite(triggerCreatedAt) ||
350
+ dateIsoString(new Date(triggerCreatedAt)) !== trigger.createdAt) {
351
+ throw new TypeError(`Trigger creation timestamp is invalid: ${trigger.name}`);
352
+ }
353
+ if (!Array.isArray(trigger.statements) || trigger.statements.length === 0) {
354
+ throw new TypeError(`A trigger needs at least one statement: ${trigger.name}`);
355
+ }
356
+ if (trigger.statements.length > MAX_TRIGGER_STATEMENTS) {
357
+ throw new RangeError(`A trigger cannot exceed ${String(MAX_TRIGGER_STATEMENTS)} statements: ${trigger.name}`);
358
+ }
359
+ for (const statement of trigger.statements) {
360
+ if (typeof statement.sql !== "string" || statement.sql.length === 0) {
361
+ throw new TypeError(`Trigger statement SQL is invalid: ${trigger.name}`);
362
+ }
363
+ if (!Array.isArray(statement.bindings)) {
364
+ throw new TypeError(`Trigger statement bindings are invalid: ${trigger.name}`);
365
+ }
366
+ for (const binding of statement.bindings) {
367
+ if (!VALID_TRIGGER_BINDING_SOURCES.includes(binding.source)) {
368
+ throw new TypeError(`Trigger binding source is invalid: ${trigger.name}`);
369
+ }
370
+ validateCatalogName(binding.column, "Trigger binding column");
371
+ if (!columnNames.has(binding.column)) {
372
+ throw new TypeError(`Trigger binding names an unknown column: ${binding.column}`);
373
+ }
374
+ if ((trigger.event === "insert" && binding.source === "old") ||
375
+ (trigger.event === "delete" && binding.source === "new")) {
376
+ throw new TypeError(`Trigger binding source is unavailable for ${trigger.event}`);
377
+ }
378
+ }
379
+ }
380
+ }
381
+ const namedConstraints = [
382
+ ...(record.foreignKeys ?? []).map((constraint) => constraint.name),
383
+ ...(record.checks ?? []).map((constraint) => constraint.name),
384
+ ...Object.values(record.secondaryIndexes ?? {})
385
+ .filter((index) => index.unique === true)
386
+ .map((index) => index.name),
387
+ ];
388
+ if (namedConstraints.length > MAX_TABLE_CONSTRAINTS) {
389
+ throw new RangeError(`A table cannot exceed ${String(MAX_TABLE_CONSTRAINTS)} named constraints`);
390
+ }
391
+ const constraintNames = new Set();
392
+ for (const constraintName of namedConstraints) {
393
+ validateCatalogName(constraintName, "Constraint name");
394
+ if (constraintNames.has(constraintName)) {
395
+ throw new TypeError(`Constraint already exists: ${constraintName}`);
396
+ }
397
+ constraintNames.add(constraintName);
398
+ }
399
+ if ((record.enumType?.values.length ?? 0) > MAX_ENUM_VALUES) {
400
+ throw new RangeError("A catalog enum has too many values");
401
+ }
402
+ const stack = [record];
403
+ const seen = new WeakSet();
404
+ let characters = 0;
405
+ let entries = 0;
406
+ while (stack.length > 0) {
407
+ const value = stack.pop();
408
+ if (typeof value === "string") {
409
+ assertWellFormedString(value, "Table record string");
410
+ characters += value.length;
411
+ }
412
+ else if (Array.isArray(value)) {
413
+ if (seen.has(value))
414
+ throw new TypeError("A table record cannot contain cycles or aliases");
415
+ seen.add(value);
416
+ entries += value.length;
417
+ for (const item of value)
418
+ stack.push(item);
419
+ }
420
+ else if (value instanceof Date) {
421
+ if (!Number.isFinite(dateMilliseconds(value))) {
422
+ throw new TypeError("A table record date is invalid");
423
+ }
424
+ }
425
+ else if (value !== null && typeof value === "object") {
426
+ if (seen.has(value))
427
+ throw new TypeError("A table record cannot contain cycles or aliases");
428
+ seen.add(value);
429
+ const fields = Object.entries(value);
430
+ entries += fields.length;
431
+ for (const [key, item] of fields) {
432
+ assertWellFormedString(key, "Table record field name");
433
+ characters += key.length;
434
+ stack.push(item);
435
+ }
436
+ }
437
+ if (!Number.isSafeInteger(entries) || entries > MAX_TABLE_RECORD_ENTRIES) {
438
+ throw new RangeError(`A table record cannot exceed ${String(MAX_TABLE_RECORD_ENTRIES)} aggregate entries`);
439
+ }
440
+ if (!Number.isSafeInteger(characters) || characters > MAX_TABLE_RECORD_CHARACTERS) {
441
+ throw new RangeError(`A table record cannot exceed ${String(MAX_TABLE_RECORD_CHARACTERS)} modeled characters`);
442
+ }
443
+ }
444
+ }
160
445
  /** Validates the durable identities and ownership rules of one table's secondary indexes. */
161
446
  export function validateSecondaryIndexes(record) {
447
+ validateTableRecordBounds(record);
162
448
  const columnIds = new Set(record.columns.map((column) => column.id));
163
449
  const names = new Set();
164
450
  const storageIds = new Set(Object.keys(record.ftsColumns ?? {}));
@@ -166,22 +452,21 @@ export function validateSecondaryIndexes(record) {
166
452
  if (indexId.length === 0 || index.name.length === 0 || index.storageColumnId.length === 0) {
167
453
  throw new TypeError("Secondary-index IDs and names must be non-empty");
168
454
  }
169
- const indexedColumnIds = index.columnIds ?? [index.columnId];
455
+ validateStorageId(indexId, "Secondary-index ID");
456
+ validateCatalogName(index.name, "Secondary-index name");
457
+ validateStorageId(index.storageColumnId, "Secondary-index storage ID");
458
+ const indexedColumnIds = index.columnIds;
170
459
  if (indexedColumnIds.length === 0 ||
171
460
  indexedColumnIds[0] !== index.columnId ||
172
461
  new Set(indexedColumnIds).size !== indexedColumnIds.length ||
173
462
  indexedColumnIds.some((columnId) => !columnIds.has(columnId))) {
174
463
  throw new TypeError(`Secondary index ${index.name} references an unknown column`);
175
464
  }
176
- const directions = index.directions ?? indexedColumnIds.map(() => "asc");
465
+ const directions = index.directions;
177
466
  const termEncoding = index.termEncoding;
178
467
  if (directions.length !== indexedColumnIds.length ||
179
468
  directions.some((direction) => direction !== "asc" && direction !== "desc") ||
180
- (termEncoding !== undefined && termEncoding !== "tuple-v1") ||
181
- (termEncoding !== "tuple-v1" &&
182
- (indexedColumnIds.length > 1 ||
183
- directions.some((direction) => direction !== "asc") ||
184
- index.unique === true))) {
469
+ termEncoding !== "tuple-v1") {
185
470
  throw new TypeError(`Secondary index ${index.name} has invalid key metadata`);
186
471
  }
187
472
  if (index.uniqueEnforced === true && index.unique !== true) {
@@ -208,13 +493,13 @@ export function validateSecondaryIndexes(record) {
208
493
  }
209
494
  }
210
495
  }
211
- /** Ordered catalog column IDs for legacy and current secondary-index records. */
496
+ /** Ordered catalog column IDs for one canonical secondary-index record. */
212
497
  export function secondaryIndexColumnIds(index) {
213
- return index.columnIds ?? [index.columnId];
498
+ return index.columnIds;
214
499
  }
215
- /** Declared directions for legacy and current secondary-index records. */
500
+ /** Declared directions for one canonical secondary-index record. */
216
501
  export function secondaryIndexDirections(index) {
217
- return index.directions ?? secondaryIndexColumnIds(index).map(() => "asc");
502
+ return index.directions;
218
503
  }
219
504
  /** Unique-membership namespace owned by one physical secondary index. */
220
505
  export function secondaryUniqueKeyNamespace(tableId, indexId) {
@@ -232,6 +517,94 @@ export class TableRecordConflictError extends Error {
232
517
  this.actualRevision = actualRevision;
233
518
  }
234
519
  }
520
+ /** A catalog drop lost a race with durable work that still owns records for the table. */
521
+ export class TableInUseError extends Error {
522
+ tableId;
523
+ ownerKind;
524
+ ownerId;
525
+ name = "TableInUseError";
526
+ constructor(tableId, ownerKind, ownerId) {
527
+ super(`Cannot remove table ${tableId} while ${ownerKind} ${ownerId} is active`);
528
+ this.tableId = tableId;
529
+ this.ownerKind = ownerKind;
530
+ this.ownerId = ownerId;
531
+ }
532
+ }
533
+ /** Refuses a commit that would cross the persisted level-zero fragmentation safety ceiling. */
534
+ export class CompactionBacklogError extends Error {
535
+ tableName;
536
+ levelZeroSegments;
537
+ limit;
538
+ name = "CompactionBacklogError";
539
+ constructor(tableName, levelZeroSegments, limit) {
540
+ super(`Table ${tableName} has ${String(levelZeroSegments)} level-zero segments; compactTable() must reduce it below the ${String(limit)}-segment safety limit before more writes`);
541
+ this.tableName = tableName;
542
+ this.levelZeroSegments = levelZeroSegments;
543
+ this.limit = limit;
544
+ }
545
+ }
546
+ /** Absolute visible level-zero ceiling; every commit that adds L0 must enforce this or lower. */
547
+ export const MAX_LEVEL_ZERO_SEGMENTS = 4_096;
548
+ /** Largest persisted row ID/posting value; the wire format is canonical unsigned 64-bit. */
549
+ export const MAX_ROW_ID = (1n << 64n) - 1n;
550
+ /** One past `MAX_ROW_ID`, allowed only for exclusive range/counter ends. */
551
+ export const MAX_ROW_ID_EXCLUSIVE_END = 1n << 64n;
552
+ /** Auto-increment values are exposed as exact JavaScript numbers and therefore stop here. */
553
+ export const MAX_AUTO_INCREMENT_VALUE = BigInt(Number.MAX_SAFE_INTEGER);
554
+ export const MAX_AUTO_INCREMENT_EXCLUSIVE_END = MAX_AUTO_INCREMENT_VALUE + 1n;
555
+ /** A live owner renews; an abandoned durable pin expires within one hour. */
556
+ export const MAX_LEASE_TTL_MS = 60 * 60 * 1_000;
557
+ /** Durable resource ceilings; adapters sweep expired owners before atomically refusing creation. */
558
+ export const MAX_ACTIVE_LEASES = 4_096;
559
+ export const MAX_ACTIVE_TEMP_OWNERS = 1_024;
560
+ export const MAX_TEMP_RUNS_PER_OWNER = 1_024;
561
+ export const MAX_TEMP_PAGES_PER_OWNER = 16_384;
562
+ export const MAX_TEMP_RUNS_TOTAL = 65_536;
563
+ export const MAX_TEMP_PAGES_TOTAL = 262_144;
564
+ export const MAX_TEMP_BYTES_PER_OWNER = 512 * 1024 * 1024;
565
+ export const MAX_TEMP_BYTES_TOTAL = 1024 * 1024 * 1024;
566
+ export const MAX_ACTIVE_COMPACTION_JOBS = 1_024;
567
+ export const MAX_ACTIVE_GARBAGE_COLLECTION_JOBS = 1;
568
+ export const MAX_ACTIVE_UNIQUE_KEY_BUILDS = 1_024;
569
+ export const MAX_ACTIVE_FTS_BASE_BUILDS = 128;
570
+ export const MAX_ACTIVE_SECONDARY_INDEX_BUILDS = 128;
571
+ export const MAX_ACCELERATOR_BUILD_STAGED_BYTES_TOTAL = 1024 * 1024 * 1024;
572
+ export const MAX_ACCELERATOR_BUILD_STAGED_ENTRIES_TOTAL = 16_777_216;
573
+ export const MAX_ACTIVE_TRANSACTIONS = 4_096;
574
+ export const MAX_GLOBAL_STAGED_ARTIFACT_BYTES = 512 * 1024 * 1024;
575
+ export const MAX_GLOBAL_STAGED_BLOCKS = 65_536;
576
+ export const MAX_GLOBAL_STAGED_SEGMENTS = 65_536;
577
+ /** Catalog enumeration remains bounded even for cold public inspection APIs. */
578
+ export const MAX_CATALOG_RECORDS = 4_096;
579
+ /** Total canonical UTF-8 record-wire bytes retained by the durable catalog. */
580
+ export const MAX_CATALOG_RETAINED_BYTES = 64 * 1024 * 1024;
581
+ export const MAX_MANIFEST_RECORDS = 65_536;
582
+ export const MAX_MANIFEST_RETAINED_BYTES = 64 * 1024 * 1024;
583
+ export const MAX_SEGMENT_RECORDS = 1_048_576;
584
+ export const MAX_SEGMENT_RETAINED_BYTES = 512 * 1024 * 1024;
585
+ /** Durable diagnostic/provenance tails are pruned before admitting more terminal records. */
586
+ export const MAX_TERMINAL_TRANSACTION_RECORDS = 65_536;
587
+ export const MAX_TERMINAL_COMPACTION_JOB_RECORDS = 4_096;
588
+ export const MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS = 1_024;
589
+ /** An old renewable pin may not force unbounded history retention while writes continue. */
590
+ export const MAX_PINNED_MANIFEST_VERSION_LAG = 4_096;
591
+ export const MAX_PINNED_RETIRED_BLOCKS = 65_536;
592
+ export const MAX_PINNED_RETIRED_BYTES = 512 * 1024 * 1024;
593
+ /** Total obsolete physical history is refused before it can consume an origin indefinitely. */
594
+ export const MAX_RETIRED_HISTORY_BYTES = 1024 * 1024 * 1024;
595
+ /** A durable resource family reached its fixed corruption/growth safety ceiling. */
596
+ export class StorageResourceLimitError extends Error {
597
+ resource;
598
+ count;
599
+ limit;
600
+ name = "StorageResourceLimitError";
601
+ constructor(resource, count, limit) {
602
+ super(`Storage resource ${resource} would reach ${String(count)}; the fixed safety limit is ${String(limit)}`);
603
+ this.resource = resource;
604
+ this.count = count;
605
+ this.limit = limit;
606
+ }
607
+ }
235
608
  export const compactionJobStates = [
236
609
  "planned",
237
610
  "running",
@@ -242,6 +615,240 @@ export const compactionJobStates = [
242
615
  ];
243
616
  export const compactionRewritePlanKinds = ["copy-v1", "rechunk-v1", "merge-v1"];
244
617
  export const compactionOutputCompressions = ["raw", "gzip"];
618
+ /** Every segment ID owned by a compaction job, including merge partition outputs. */
619
+ export function compactionOutputSegmentIds(job) {
620
+ const outputSegmentId = job.outputSegmentId;
621
+ if (outputSegmentId === null)
622
+ return [];
623
+ const partitionCount = (job.rewritePlan.kind === "merge-v1" || job.rewritePlan.kind === "rechunk-v1") &&
624
+ job.rewritePlan.partitions !== undefined
625
+ ? job.rewritePlan.partitions.length
626
+ : 1;
627
+ return Array.from({ length: partitionCount }, (_, index) => index === 0 ? outputSegmentId : `${outputSegmentId}/${String(index)}`);
628
+ }
629
+ /**
630
+ * Proves that a compaction transaction stages exactly the immutable job output. This is kept in
631
+ * the storage contract (rather than only in the engine) so adapters reject forged, reordered, or
632
+ * partially journaled output before publication and while loading durable state.
633
+ */
634
+ export function assertCompactionOutputProvenance(job, table, transaction, sourceSegments, outputSegments, options = {}) {
635
+ if (job.transactionId !== transaction.id) {
636
+ throw new Error(`Compaction job ${job.id} belongs to another transaction`);
637
+ }
638
+ const expectedSegmentIds = compactionOutputSegmentIds(job);
639
+ const stagedExpectedSegmentIds = options.allowOutputPrefix
640
+ ? expectedSegmentIds.slice(0, transaction.pendingSegmentIds.length)
641
+ : expectedSegmentIds;
642
+ assertExactOrderedIds(transaction.pendingSegmentIds, stagedExpectedSegmentIds, `Compaction job ${job.id} segment journal`);
643
+ assertExactOrderedIds(outputSegments.map((segment) => segment.id), stagedExpectedSegmentIds, `Compaction job ${job.id} output segments`);
644
+ assertExactOrderedIds(sourceSegments.map((segment) => segment.id), job.sourceSegmentIds, `Compaction job ${job.id} source segments`);
645
+ const plan = job.rewritePlan;
646
+ if (plan.kind === "copy-v1") {
647
+ const expectedOutputBlockIds = sourceSegments.flatMap((segment, segmentIndex) => table.columns.flatMap((column, columnIndex) => (segment.columnBlockIds[column.id] ?? []).map((_blockId, part) => copyCompactionOutputBlockId(job.id, segmentIndex, columnIndex, part))));
648
+ assertExactOrderedIds(job.outputBlockIds, expectedOutputBlockIds, `Compaction job ${job.id} output block plan`);
649
+ }
650
+ else {
651
+ if (table.columns.length !== plan.columns.length ||
652
+ table.columns.some((column, index) => {
653
+ const planned = plan.columns.at(index);
654
+ if (planned?.columnId !== column.id)
655
+ return true;
656
+ return planned.type !== column.type;
657
+ })) {
658
+ throw new Error(`Compaction job ${job.id} table schema differs from its rewrite plan`);
659
+ }
660
+ if (plan.kind === "merge-v1" && plan.keyColumnId !== table.uniqueKeyColumnId) {
661
+ throw new Error(`Compaction job ${job.id} table key differs from its rewrite plan`);
662
+ }
663
+ const expectedOutputBlockIds = plan.outputs.flatMap((_output, outputIndex) => plan.columns.map((_column, columnIndex) => physicalCompactionOutputBlockId(job.id, outputIndex, columnIndex)));
664
+ assertExactOrderedIds(job.outputBlockIds, expectedOutputBlockIds, `Compaction job ${job.id} output block plan`);
665
+ }
666
+ assertExactOrderedIds(transaction.pendingBlockIds, job.outputBlockIds, `Compaction job ${job.id} block journal`);
667
+ const expectedSegments = expectedCompactionOutputSegments(job, table, sourceSegments);
668
+ for (const [index, actual] of outputSegments.entries()) {
669
+ const expected = expectedSegments[index];
670
+ if (expected === undefined) {
671
+ throw new Error(`Compaction job ${job.id} stages an unexpected output segment`);
672
+ }
673
+ assertCompactionSegmentFields(actual, expected, job.id);
674
+ }
675
+ }
676
+ function expectedCompactionOutputSegments(job, table, sourceSegments) {
677
+ const outputSegmentId = job.outputSegmentId;
678
+ if (outputSegmentId === null)
679
+ return [];
680
+ const plan = job.rewritePlan;
681
+ const common = {
682
+ tableId: table.id,
683
+ transactionId: job.transactionId ?? "",
684
+ ...(table.uniqueKeyColumnId === undefined ? {} : { keyColumnId: table.uniqueKeyColumnId }),
685
+ level: job.targetLevel,
686
+ ...(job.outputPartitionOrdinal === undefined
687
+ ? {}
688
+ : { partitionOrdinal: job.outputPartitionOrdinal }),
689
+ };
690
+ if (plan.kind === "copy-v1") {
691
+ const first = sourceSegments[0];
692
+ const last = sourceSegments[sourceSegments.length - 1];
693
+ if (first === undefined || last === undefined) {
694
+ throw new Error(`Compaction job ${job.id} has no source rows`);
695
+ }
696
+ return [
697
+ {
698
+ id: outputSegmentId,
699
+ ...common,
700
+ rowCount: safeSum(sourceSegments.map((segment) => segment.rowCount), "Copy compaction output rows"),
701
+ rowIdStart: first.rowIdStart,
702
+ rowIdEndExclusive: last.rowIdEndExclusive,
703
+ columnBlockIds: Object.fromEntries(table.columns.map((column, columnIndex) => [
704
+ column.id,
705
+ sourceSegments.flatMap((segment, segmentIndex) => (segment.columnBlockIds[column.id] ?? []).map((_blockId, part) => copyCompactionOutputBlockId(job.id, segmentIndex, columnIndex, part))),
706
+ ])),
707
+ kind: "insert",
708
+ logicalOrder: Math.min(...sourceSegments.map((segment) => segment.logicalOrder)),
709
+ commitOrdinal: 0,
710
+ rowIdSpans: [],
711
+ },
712
+ ];
713
+ }
714
+ const partitions = plan.partitions ?? [
715
+ { rowStart: 0, rowCount: plan.totalRows, logicalOrder: plan.logicalOrder },
716
+ ];
717
+ return partitions.map((partition, index) => {
718
+ const rowIdSpans = plan.kind === "merge-v1"
719
+ ? sliceCompactionRowIdSpans(plan.rowIdSpans, partition.rowStart, partition.rowCount)
720
+ : [];
721
+ const envelope = plan.kind === "merge-v1"
722
+ ? compactionRowIdSpanEnvelope(rowIdSpans)
723
+ : {
724
+ start: plan.rowIdStart + BigInt(partition.rowStart),
725
+ endExclusive: plan.rowIdStart + BigInt(partition.rowStart + partition.rowCount),
726
+ };
727
+ return {
728
+ id: index === 0 ? outputSegmentId : `${outputSegmentId}/${String(index)}`,
729
+ ...common,
730
+ rowCount: partition.rowCount,
731
+ rowIdStart: envelope.start,
732
+ rowIdEndExclusive: envelope.endExclusive,
733
+ columnBlockIds: Object.fromEntries(plan.columns.map((column, columnIndex) => [
734
+ column.columnId,
735
+ plan.outputs.flatMap((output, outputIndex) => output.rowStart >= partition.rowStart &&
736
+ output.rowStart + output.rowCount <= partition.rowStart + partition.rowCount
737
+ ? [physicalCompactionOutputBlockId(job.id, outputIndex, columnIndex)]
738
+ : []),
739
+ ])),
740
+ kind: plan.kind === "merge-v1" ? "base" : "insert",
741
+ logicalOrder: partition.logicalOrder,
742
+ commitOrdinal: index,
743
+ rowIdSpans,
744
+ };
745
+ });
746
+ }
747
+ function assertCompactionSegmentFields(actual, expected, jobId) {
748
+ for (const field of [
749
+ "id",
750
+ "tableId",
751
+ "transactionId",
752
+ "rowCount",
753
+ "rowIdStart",
754
+ "rowIdEndExclusive",
755
+ "kind",
756
+ "keyColumnId",
757
+ "level",
758
+ "logicalOrder",
759
+ "commitOrdinal",
760
+ "partitionOrdinal",
761
+ ]) {
762
+ if (actual[field] !== expected[field]) {
763
+ throw new Error(`Compaction job ${jobId} output ${actual.id} has invalid ${field}`);
764
+ }
765
+ }
766
+ const expectedColumnIds = Object.keys(expected.columnBlockIds);
767
+ assertExactOrderedIds(Object.keys(actual.columnBlockIds).sort(), [...expectedColumnIds].sort(), `Compaction job ${jobId} output ${actual.id} columns`);
768
+ for (const columnId of expectedColumnIds) {
769
+ assertExactOrderedIds(actual.columnBlockIds[columnId] ?? [], expected.columnBlockIds[columnId] ?? [], `Compaction job ${jobId} output ${actual.id} column ${columnId}`);
770
+ }
771
+ if (actual.rowIdSpans.length !== expected.rowIdSpans.length ||
772
+ actual.rowIdSpans.some((span, index) => {
773
+ const planned = expected.rowIdSpans[index];
774
+ return (planned?.rowStart !== span.rowStart ||
775
+ planned.rowCount !== span.rowCount ||
776
+ planned.rowIdStart !== span.rowIdStart);
777
+ })) {
778
+ throw new Error(`Compaction job ${jobId} output ${actual.id} has invalid row-ID spans`);
779
+ }
780
+ }
781
+ function assertExactOrderedIds(actual, expected, label) {
782
+ if (actual.length !== expected.length ||
783
+ actual.some((value, index) => value !== expected[index])) {
784
+ throw new Error(`${label} is not the canonical ordered sequence`);
785
+ }
786
+ }
787
+ function physicalCompactionOutputBlockId(jobId, outputIndex, columnIndex) {
788
+ return [
789
+ jobId,
790
+ "rewrite",
791
+ "window",
792
+ String(outputIndex).padStart(8, "0"),
793
+ "column",
794
+ String(columnIndex).padStart(8, "0"),
795
+ ].join("/");
796
+ }
797
+ function copyCompactionOutputBlockId(jobId, segmentIndex, columnIndex, part) {
798
+ return [
799
+ jobId,
800
+ "output",
801
+ "segment",
802
+ String(segmentIndex).padStart(6, "0"),
803
+ "column",
804
+ String(columnIndex).padStart(6, "0"),
805
+ "part",
806
+ String(part).padStart(6, "0"),
807
+ ].join("/");
808
+ }
809
+ function sliceCompactionRowIdSpans(spans, rowStart, rowCount) {
810
+ const result = [];
811
+ const rowEnd = rowStart + rowCount;
812
+ for (const span of spans) {
813
+ const spanEnd = span.rowStart + span.rowCount;
814
+ if (spanEnd <= rowStart || span.rowStart >= rowEnd)
815
+ continue;
816
+ const start = Math.max(span.rowStart, rowStart);
817
+ const end = Math.min(spanEnd, rowEnd);
818
+ const next = {
819
+ rowStart: start - rowStart,
820
+ rowCount: end - start,
821
+ rowIdStart: span.rowIdStart + BigInt(start - span.rowStart),
822
+ };
823
+ const previous = result[result.length - 1];
824
+ if (previous !== undefined &&
825
+ previous.rowStart + previous.rowCount === next.rowStart &&
826
+ previous.rowIdStart + BigInt(previous.rowCount) === next.rowIdStart) {
827
+ result[result.length - 1] = {
828
+ ...previous,
829
+ rowCount: previous.rowCount + next.rowCount,
830
+ };
831
+ }
832
+ else {
833
+ result.push(next);
834
+ }
835
+ }
836
+ return result;
837
+ }
838
+ function compactionRowIdSpanEnvelope(spans) {
839
+ if (spans.length === 0)
840
+ return { start: 0n, endExclusive: 0n };
841
+ let start = spans[0]?.rowIdStart ?? 0n;
842
+ let endExclusive = start + BigInt(spans[0]?.rowCount ?? 0);
843
+ for (const span of spans.slice(1)) {
844
+ if (span.rowIdStart < start)
845
+ start = span.rowIdStart;
846
+ const spanEnd = span.rowIdStart + BigInt(span.rowCount);
847
+ if (spanEnd > endExclusive)
848
+ endExclusive = spanEnd;
849
+ }
850
+ return { start, endExclusive };
851
+ }
245
852
  export class CompactionJobConflictError extends Error {
246
853
  jobId;
247
854
  expectedRevision;
@@ -255,6 +862,15 @@ export class CompactionJobConflictError extends Error {
255
862
  }
256
863
  }
257
864
  export const garbageCollectionJobStates = ["planned", "running", "completed"];
865
+ /** Absolute per-call bound for durable maintenance candidate arrays and storage transactions. */
866
+ export const MAX_MAINTENANCE_BATCH_ITEMS = 1_024;
867
+ export function boundedMaintenanceBatchItems(value, label) {
868
+ const normalized = positiveWholeNumber(value, label);
869
+ if (normalized > MAX_MAINTENANCE_BATCH_ITEMS) {
870
+ throw new RangeError(`${label} cannot exceed ${String(MAX_MAINTENANCE_BATCH_ITEMS)} items`);
871
+ }
872
+ return normalized;
873
+ }
258
874
  export class GarbageCollectionJobConflictError extends Error {
259
875
  jobId;
260
876
  expectedRevision;
@@ -287,6 +903,362 @@ export class LeaseConflictError extends Error {
287
903
  this.actualRevision = actualRevision;
288
904
  }
289
905
  }
906
+ /** A renewal/move cutoff reached the persisted expiry; expiry is irrevocable. */
907
+ export class LeaseExpiredError extends Error {
908
+ leaseId;
909
+ expiresAt;
910
+ expiresAtCutoff;
911
+ name = "LeaseExpiredError";
912
+ constructor(leaseId, expiresAt, expiresAtCutoff) {
913
+ super(`Lease ${leaseId} expired at ${expiresAt}`);
914
+ this.leaseId = leaseId;
915
+ this.expiresAt = expiresAt;
916
+ this.expiresAtCutoff = expiresAtCutoff;
917
+ }
918
+ }
919
+ /** Refuses a lease release from a different durable owner. */
920
+ export class LeaseOwnerConflictError extends Error {
921
+ leaseId;
922
+ expectedOwnerId;
923
+ actualOwnerId;
924
+ name = "LeaseOwnerConflictError";
925
+ constructor(leaseId, expectedOwnerId, actualOwnerId) {
926
+ super(`Lease ${leaseId} belongs to ${actualOwnerId}, not release owner ${expectedOwnerId}`);
927
+ this.leaseId = leaseId;
928
+ this.expectedOwnerId = expectedOwnerId;
929
+ this.actualOwnerId = actualOwnerId;
930
+ }
931
+ }
932
+ /** Maximum immutable payloads accepted by one atomic staging/WAL operation. */
933
+ export const MAX_TRANSACTION_STAGE_BLOCKS = 64;
934
+ /** Maximum segment records accepted by one atomic staging/WAL operation. */
935
+ export const MAX_TRANSACTION_STAGE_SEGMENTS = 64;
936
+ /**
937
+ * Maximum block bytes accepted by one atomic staging/WAL operation. The limit still admits one
938
+ * maximum-size physical block; callers split collections of smaller blocks into bounded calls.
939
+ */
940
+ export const MAX_TRANSACTION_STAGE_BYTES = MAX_STORED_BLOCK_BYTE_LENGTH;
941
+ /** Durable journal ceilings prevent a forgotten or hostile transaction growing without bound. */
942
+ export const MAX_TRANSACTION_PENDING_BLOCKS = 4_096;
943
+ export const MAX_TRANSACTION_PENDING_SEGMENTS = 4_096;
944
+ /** Storage-boundary preflight shared by staging and single-shot write implementations. */
945
+ export function assertTransactionArtifactBatchLimits(blocks, segments) {
946
+ if (blocks.length > MAX_TRANSACTION_STAGE_BLOCKS) {
947
+ throw new RangeError(`Transaction artifact batch exceeds ${String(MAX_TRANSACTION_STAGE_BLOCKS)} blocks`);
948
+ }
949
+ if (segments.length > MAX_TRANSACTION_STAGE_SEGMENTS) {
950
+ throw new RangeError(`Transaction artifact batch exceeds ${String(MAX_TRANSACTION_STAGE_SEGMENTS)} segments`);
951
+ }
952
+ let byteLength = 0;
953
+ for (const block of blocks) {
954
+ if (!(block.bytes instanceof Uint8Array)) {
955
+ throw new TypeError("Transaction block bytes must be a Uint8Array");
956
+ }
957
+ byteLength += block.bytes.byteLength;
958
+ if (!Number.isSafeInteger(byteLength) || byteLength > MAX_TRANSACTION_STAGE_BYTES) {
959
+ throw new RangeError(`Transaction artifact batch exceeds ${String(MAX_TRANSACTION_STAGE_BYTES)} block bytes`);
960
+ }
961
+ }
962
+ }
963
+ /** Preflight for the complete persisted journal after a bounded staging operation. */
964
+ export function assertTransactionArtifactJournalLimits(pendingBlockIds, pendingSegmentIds) {
965
+ if (pendingBlockIds.length > MAX_TRANSACTION_PENDING_BLOCKS) {
966
+ throw new RangeError(`Transaction journal exceeds ${String(MAX_TRANSACTION_PENDING_BLOCKS)} pending blocks`);
967
+ }
968
+ if (pendingSegmentIds.length > MAX_TRANSACTION_PENDING_SEGMENTS) {
969
+ throw new RangeError(`Transaction journal exceeds ${String(MAX_TRANSACTION_PENDING_SEGMENTS)} pending segments`);
970
+ }
971
+ }
972
+ /** A snapshot session is renewable but can never pin storage for more than one hour unattended. */
973
+ export const MAX_SNAPSHOT_SESSION_TTL_MS = 60 * 60 * 1_000;
974
+ /** Snapshot v1 is an ordered bounded record stream; no database-sized collection is in header. */
975
+ export const SNAPSHOT_FRAME_KINDS = [
976
+ "catalog-page",
977
+ "segment-page",
978
+ "transaction-page",
979
+ "unique-page",
980
+ "posting-page",
981
+ "block",
982
+ ];
983
+ export const MAX_SNAPSHOT_FRAME_ITEMS = 1_024;
984
+ export const MAX_SNAPSHOT_METADATA_FRAME_BYTES = 4 * 1024 * 1024;
985
+ export const MAX_SNAPSHOT_FRAME_BATCH_ITEMS = 64;
986
+ export const MAX_SNAPSHOT_METADATA_BATCH_BYTES = 16 * 1024 * 1024;
987
+ /** One block frame is permitted; metadata frames still use MAX_SNAPSHOT_METADATA_BATCH_BYTES. */
988
+ export const MAX_SNAPSHOT_FRAME_BATCH_BYTES = MAX_STORED_BLOCK_BYTE_LENGTH + 8 * 1024;
989
+ /**
990
+ * Framed restore builds accelerator generations in a disposable core before one atomic publish.
991
+ * These aggregate ceilings keep that validation heap bounded until generations become a native
992
+ * chunk-backed structure rather than one materialized in-memory collection.
993
+ */
994
+ export const MAX_SNAPSHOT_IMPORT_ACCELERATOR_RETAINED_BYTES = MAX_ACCELERATOR_BUILD_STAGED_BYTES_TOTAL;
995
+ export const MAX_SNAPSHOT_IMPORT_ACCELERATOR_RETAINED_ENTRIES = MAX_ACCELERATOR_BUILD_STAGED_ENTRIES_TOTAL;
996
+ /** Modeled heap retained by one framed accelerator chunk during atomic snapshot validation. */
997
+ export function snapshotAcceleratorItemRetainedUsage(item) {
998
+ if (item.kind === "unique-chunk") {
999
+ return {
1000
+ bytes: uniqueKeyBuildChunkRetainedBytes(item.keyTokens),
1001
+ entries: item.keyTokens.length,
1002
+ };
1003
+ }
1004
+ let bytes = 0;
1005
+ let entries = 0;
1006
+ for (const posting of item.postings) {
1007
+ bytes = safeSum([bytes, 32 + posting.term.length * 2], "Snapshot posting retained bytes");
1008
+ entries = safeSum([entries, posting.rowIds.length], "Snapshot posting retained entries");
1009
+ // Bigint row IDs, term-frequency numbers, and the two array slots all remain live until the
1010
+ // generation is validated and promoted. The fixed model is intentionally conservative.
1011
+ bytes = safeSum([bytes, posting.rowIds.length * 48], "Snapshot posting retained bytes");
1012
+ }
1013
+ return { bytes, entries };
1014
+ }
1015
+ /** Shared arithmetic boundary used by streaming adapters and allocation-free cap tests. */
1016
+ export function assertSnapshotImportAcceleratorUsage(bytes, entries) {
1017
+ if (!Number.isSafeInteger(bytes) || bytes < 0) {
1018
+ throw new RangeError("Snapshot accelerator retained bytes are invalid");
1019
+ }
1020
+ if (!Number.isSafeInteger(entries) || entries < 0) {
1021
+ throw new RangeError("Snapshot accelerator retained entries are invalid");
1022
+ }
1023
+ if (bytes > MAX_SNAPSHOT_IMPORT_ACCELERATOR_RETAINED_BYTES) {
1024
+ throw new StorageResourceLimitError("snapshot accelerator byte", bytes, MAX_SNAPSHOT_IMPORT_ACCELERATOR_RETAINED_BYTES);
1025
+ }
1026
+ if (entries > MAX_SNAPSHOT_IMPORT_ACCELERATOR_RETAINED_ENTRIES) {
1027
+ throw new StorageResourceLimitError("snapshot accelerator entry", entries, MAX_SNAPSHOT_IMPORT_ACCELERATOR_RETAINED_ENTRIES);
1028
+ }
1029
+ }
1030
+ export class SnapshotImportConflictError extends Error {
1031
+ identity;
1032
+ ownerId;
1033
+ name = "SnapshotImportConflictError";
1034
+ constructor(identity, ownerId, message) {
1035
+ super(`Snapshot import ${identity} owned by ${ownerId}: ${message}`);
1036
+ this.identity = identity;
1037
+ this.ownerId = ownerId;
1038
+ }
1039
+ }
1040
+ /**
1041
+ * Persisted bytes belong to a recognized storage family, but not to the version this reader
1042
+ * supports. This is deliberately distinct from corruption: callers must not infer that repair,
1043
+ * recreation, or deletion is safe merely because versions differ.
1044
+ */
1045
+ export class StorageFormatVersionError extends Error {
1046
+ backend;
1047
+ location;
1048
+ actualVersion;
1049
+ supportedVersion;
1050
+ relation;
1051
+ name = "StorageFormatVersionError";
1052
+ constructor(backend, location, actualVersion, supportedVersion, relation) {
1053
+ const actual = actualVersion === null ? "an unknown version" : `version ${String(actualVersion)}`;
1054
+ super(`${backend} storage format at ${location} uses ${actual}, which is ${relation} than ` +
1055
+ `the supported version ${String(supportedVersion)}`);
1056
+ this.backend = backend;
1057
+ this.location = location;
1058
+ this.actualVersion = actualVersion;
1059
+ this.supportedVersion = supportedVersion;
1060
+ this.relation = relation;
1061
+ }
1062
+ }
1063
+ /** A native IndexedDB schema upgrade is waiting for another connection to close. */
1064
+ export class IndexedDbSchemaUpgradeBlockedError extends Error {
1065
+ databaseName;
1066
+ oldVersion;
1067
+ requestedVersion;
1068
+ name = "IndexedDbSchemaUpgradeBlockedError";
1069
+ constructor(databaseName, oldVersion, requestedVersion) {
1070
+ super(`IndexedDB schema upgrade for ${databaseName} from version ${String(oldVersion)} to ` +
1071
+ `${String(requestedVersion)} is blocked by another open connection; close older tabs and retry`);
1072
+ this.databaseName = databaseName;
1073
+ this.oldVersion = oldVersion;
1074
+ this.requestedVersion = requestedVersion;
1075
+ }
1076
+ }
1077
+ /** Persisted metadata or payload failed an adapter's fail-closed integrity checks. */
1078
+ export class StorageCorruptionError extends Error {
1079
+ backend;
1080
+ location;
1081
+ name = "StorageCorruptionError";
1082
+ constructor(backend, location, message) {
1083
+ super(`${backend} storage corruption at ${location}: ${message}`);
1084
+ this.backend = backend;
1085
+ this.location = location;
1086
+ }
1087
+ }
1088
+ /**
1089
+ * A remote OPFS leader may have committed a mutation whose acknowledgement was lost.
1090
+ * Reconcile stable identities or revisions before retrying the named operation.
1091
+ */
1092
+ export class OpfsUncertainOutcomeError extends Error {
1093
+ method;
1094
+ name = "OpfsUncertainOutcomeError";
1095
+ constructor(method) {
1096
+ super(`The OPFS leader changed before ${method} was acknowledged; the mutation may have committed`);
1097
+ this.method = method;
1098
+ }
1099
+ }
1100
+ /** Bounded calls used to seed an arbitrarily large UNIQUE namespace without heap materialization. */
1101
+ export const MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK = 4_096;
1102
+ export const MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES = 4 * 1024 * 1024;
1103
+ export const MAX_UNIQUE_KEY_BUILD_TTL_MS = 60 * 60 * 1_000;
1104
+ export const MAX_UNIQUE_KEY_BUILD_STAGED_BYTES = 512 * 1024 * 1024;
1105
+ export const MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL = 1024 * 1024 * 1024;
1106
+ /** Validates one staged seed call before an adapter opens a transaction or WAL frame. */
1107
+ export function uniqueKeyBuildChunkRetainedBytes(keyTokens) {
1108
+ if (keyTokens.length === 0 || keyTokens.length > MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK) {
1109
+ throw new RangeError(`A UNIQUE build chunk must contain 1-${String(MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK)} tokens`);
1110
+ }
1111
+ const seen = new Set();
1112
+ let bytes = 0;
1113
+ for (const token of keyTokens) {
1114
+ if (typeof token !== "string" ||
1115
+ token.length === 0 ||
1116
+ token.length > MAX_FTS_POSTING_TERM_CHARACTERS) {
1117
+ throw new TypeError("A UNIQUE build token has invalid length");
1118
+ }
1119
+ assertWellFormedString(token, "UNIQUE build token");
1120
+ if (seen.has(token))
1121
+ throw new UniqueKeyConflictError("UNIQUE build", token);
1122
+ seen.add(token);
1123
+ bytes = safeSum([bytes, 16 + token.length * 2], "UNIQUE build chunk bytes");
1124
+ if (bytes > MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES) {
1125
+ throw new RangeError(`A UNIQUE build chunk cannot exceed ${String(MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES)} retained bytes`);
1126
+ }
1127
+ }
1128
+ return bytes;
1129
+ }
1130
+ export class UniqueKeyBuildConflictError extends Error {
1131
+ buildId;
1132
+ reason;
1133
+ name = "UniqueKeyBuildConflictError";
1134
+ constructor(buildId, reason) {
1135
+ super(`UNIQUE build ${buildId} cannot continue: ${reason}`);
1136
+ this.buildId = buildId;
1137
+ this.reason = reason;
1138
+ }
1139
+ }
1140
+ export const MAX_POSTING_BUILD_TTL_MS = 60 * 60 * 1_000;
1141
+ export class PostingBuildConflictError extends Error {
1142
+ buildId;
1143
+ ownerId;
1144
+ reason;
1145
+ name = "PostingBuildConflictError";
1146
+ constructor(buildId, ownerId, reason) {
1147
+ super(`Posting build ${buildId} owned by ${ownerId}: ${reason}`);
1148
+ this.buildId = buildId;
1149
+ this.ownerId = ownerId;
1150
+ this.reason = reason;
1151
+ }
1152
+ }
1153
+ /** In-memory and atomic-publish bounds for one transaction's accelerator/key deltas. */
1154
+ export const MAX_TRANSACTION_COMMIT_DELTA_BYTES = 64 * 1024 * 1024;
1155
+ export const MAX_TRANSACTION_COMMIT_DELTA_ENTRIES = 1_048_576;
1156
+ export function transactionCommitDeltaRetainedBytes(uniqueKeyChanges, ftsChanges) {
1157
+ let bytes = 0;
1158
+ let entries = 0;
1159
+ const add = (amount) => {
1160
+ bytes += amount;
1161
+ if (!Number.isSafeInteger(bytes) || bytes > MAX_TRANSACTION_COMMIT_DELTA_BYTES) {
1162
+ throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_BYTES)} retained bytes`);
1163
+ }
1164
+ };
1165
+ const addEntry = () => {
1166
+ entries += 1;
1167
+ if (entries > MAX_TRANSACTION_COMMIT_DELTA_ENTRIES) {
1168
+ throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_ENTRIES)} entries`);
1169
+ }
1170
+ };
1171
+ for (const change of uniqueKeyChanges) {
1172
+ add(48 + change.tableId.length * 2);
1173
+ for (const token of change.keyTokens) {
1174
+ addEntry();
1175
+ add(16 + token.length * 2);
1176
+ }
1177
+ }
1178
+ for (const change of ftsChanges) {
1179
+ add(32 + change.tableId.length * 2);
1180
+ for (const column of change.columns) {
1181
+ add(48 + column.columnId.length * 2);
1182
+ for (const posting of column.postings) {
1183
+ addEntry();
1184
+ add(32 + posting.term.length * 2);
1185
+ if (posting.rowIds.length !== posting.tf.length) {
1186
+ throw new TypeError("Full-text posting row and frequency counts differ");
1187
+ }
1188
+ entries += posting.rowIds.length;
1189
+ if (entries > MAX_TRANSACTION_COMMIT_DELTA_ENTRIES) {
1190
+ throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_ENTRIES)} entries`);
1191
+ }
1192
+ add(posting.rowIds.length * 16);
1193
+ }
1194
+ }
1195
+ }
1196
+ return { bytes, entries };
1197
+ }
1198
+ /** Aggregate row-id ceiling for one postings candidate read. Overflow falls back to a scan. */
1199
+ export const MAX_FTS_CANDIDATE_ROW_IDS = 65_536;
1200
+ /** Query-term cardinality shared by the parser and every postings adapter boundary. */
1201
+ export const MAX_FTS_QUERY_TERMS = 32;
1202
+ /** Retained metadata/value ceiling for one ordered postings read. Overflow falls back to a scan. */
1203
+ export const MAX_FTS_ORDERED_READ_BYTES = 64 * 1024 * 1024;
1204
+ /** Hard public mutation bounds for one streamed postings-build chunk. */
1205
+ export const MAX_FTS_POSTINGS_PER_CHUNK = 65_536;
1206
+ export const MAX_FTS_POSTING_ROW_IDS_PER_CHUNK = 1_048_576;
1207
+ export const MAX_FTS_POSTING_TERM_CHARACTERS = 65_536;
1208
+ /** Indexed string values are bounded; non-indexed block strings retain the physical block cap. */
1209
+ export const MAX_INDEXED_STRING_CHARACTERS = 16_384;
1210
+ /** Maximum normalized tokens retained from one indexed document. */
1211
+ export const MAX_FTS_TOKENS_PER_DOCUMENT = 4_096;
1212
+ /** Bounded catalog/TOC cardinality. Oversized accelerators invalidate and queries scan. */
1213
+ export const MAX_FTS_BASE_CHUNKS = 4_096;
1214
+ export const MAX_FTS_DELTA_CHUNKS = 128;
1215
+ /** Strict, bounded runtime validation shared by every adapter before any postings read. */
1216
+ export function validateFtsPostingQueries(value, label = "Full-text query terms") {
1217
+ if (!Array.isArray(value))
1218
+ throw new TypeError(`${label} must be an array`);
1219
+ if (value.length > MAX_FTS_QUERY_TERMS) {
1220
+ throw new RangeError(`${label} cannot exceed ${String(MAX_FTS_QUERY_TERMS)} items`);
1221
+ }
1222
+ for (const queryValue of value) {
1223
+ if (typeof queryValue !== "object" || queryValue === null || Array.isArray(queryValue)) {
1224
+ throw new TypeError("Full-text posting query must be an object");
1225
+ }
1226
+ const query = queryValue;
1227
+ const keys = Object.keys(query);
1228
+ if ("term" in query) {
1229
+ if (keys.length !== 2 ||
1230
+ keys.some((key) => key !== "term" && key !== "prefix") ||
1231
+ typeof query.term !== "string" ||
1232
+ query.term.length === 0 ||
1233
+ query.term.length > MAX_FTS_POSTING_TERM_CHARACTERS ||
1234
+ typeof query.prefix !== "boolean") {
1235
+ throw new TypeError("Full-text exact/prefix query is invalid");
1236
+ }
1237
+ assertWellFormedString(query.term, "Full-text query term");
1238
+ continue;
1239
+ }
1240
+ if (keys.some((key) => key !== "lower" &&
1241
+ key !== "lowerInclusive" &&
1242
+ key !== "upper" &&
1243
+ key !== "upperInclusive") ||
1244
+ (query.lower !== undefined &&
1245
+ (typeof query.lower !== "string" ||
1246
+ query.lower.length > MAX_FTS_POSTING_TERM_CHARACTERS)) ||
1247
+ (query.upper !== undefined &&
1248
+ (typeof query.upper !== "string" ||
1249
+ query.upper.length > MAX_FTS_POSTING_TERM_CHARACTERS)) ||
1250
+ (query.lowerInclusive !== undefined && typeof query.lowerInclusive !== "boolean") ||
1251
+ (query.upperInclusive !== undefined && typeof query.upperInclusive !== "boolean")) {
1252
+ throw new TypeError("Full-text range query is invalid");
1253
+ }
1254
+ if (typeof query.lower === "string") {
1255
+ assertWellFormedString(query.lower, "Full-text query lower bound");
1256
+ }
1257
+ if (typeof query.upper === "string") {
1258
+ assertWellFormedString(query.upper, "Full-text query upper bound");
1259
+ }
1260
+ }
1261
+ }
290
1262
  /** Whether a stored term belongs to one exact, prefix, or range lookup. */
291
1263
  export function ftsPostingQueryMatches(term, query) {
292
1264
  if ("term" in query)
@@ -306,8 +1278,13 @@ export function ftsPostingQueryMatches(term, query) {
306
1278
  * term-match rule (exact, or prefix as a term range) and the sorted-unique row-id shape must
307
1279
  * never drift between backends — pruning would silently differ per store.
308
1280
  */
309
- export function collectFtsCandidates(chunkLists, terms) {
1281
+ export function collectFtsCandidates(chunkLists, terms, maxRowIds = MAX_FTS_CANDIDATE_ROW_IDS) {
1282
+ validateFtsPostingQueries(terms);
1283
+ if (!Number.isSafeInteger(maxRowIds) || maxRowIds < 1 || maxRowIds > MAX_FTS_CANDIDATE_ROW_IDS) {
1284
+ throw new RangeError(`Full-text candidate limit must be between 1 and ${String(MAX_FTS_CANDIDATE_ROW_IDS)}`);
1285
+ }
310
1286
  const sets = terms.map(() => new Set());
1287
+ let retainedRowIds = 0;
311
1288
  for (const postings of chunkLists) {
312
1289
  for (const posting of postings) {
313
1290
  for (let index = 0; index < terms.length; index += 1) {
@@ -318,21 +1295,49 @@ export function collectFtsCandidates(chunkLists, terms) {
318
1295
  if (!matches)
319
1296
  continue;
320
1297
  const set = sets[index];
321
- if (set !== undefined)
322
- for (const rowId of posting.rowIds)
1298
+ if (set !== undefined) {
1299
+ for (const rowId of posting.rowIds) {
1300
+ if (set.has(rowId))
1301
+ continue;
1302
+ if (retainedRowIds === maxRowIds) {
1303
+ return { rowIdsByTerm: terms.map(() => []), overflow: true };
1304
+ }
323
1305
  set.add(rowId);
1306
+ retainedRowIds += 1;
1307
+ }
1308
+ }
324
1309
  }
325
1310
  }
326
1311
  }
327
1312
  return {
328
1313
  rowIdsByTerm: sets.map((set) => [...set].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))),
1314
+ overflow: false,
329
1315
  };
330
1316
  }
1317
+ /** Validates the fixed memory ceilings accepted by one ordered postings read. */
1318
+ export function validateFtsOrderedReadLimits(maxRowIds = MAX_FTS_CANDIDATE_ROW_IDS, maxRetainedBytes = MAX_FTS_ORDERED_READ_BYTES) {
1319
+ if (!Number.isSafeInteger(maxRowIds) || maxRowIds < 1 || maxRowIds > MAX_FTS_CANDIDATE_ROW_IDS) {
1320
+ throw new RangeError(`Ordered postings row limit must be between 1 and ${String(MAX_FTS_CANDIDATE_ROW_IDS)}`);
1321
+ }
1322
+ if (!Number.isSafeInteger(maxRetainedBytes) ||
1323
+ maxRetainedBytes < 1 ||
1324
+ maxRetainedBytes > MAX_FTS_ORDERED_READ_BYTES) {
1325
+ throw new RangeError(`Ordered postings byte limit must be between 1 and ${String(MAX_FTS_ORDERED_READ_BYTES)}`);
1326
+ }
1327
+ }
331
1328
  /**
332
1329
  * K-way merges sorted base/delta chunks into canonical term order. Only one term's row map and
333
1330
  * one cursor per chunk stay live beyond the returned postings, avoiding a second index-sized map.
334
1331
  */
335
1332
  export function collectFtsPostings(chunkLists) {
1333
+ return mergeFtsPostings(chunkLists, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY).postings;
1334
+ }
1335
+ /** Bounded ordered merge; overflow deliberately returns no partial authoritative answer. */
1336
+ export function collectFtsPostingsBounded(chunkLists, maxRowIds = MAX_FTS_CANDIDATE_ROW_IDS, maxRetainedBytes = MAX_FTS_ORDERED_READ_BYTES) {
1337
+ validateFtsOrderedReadLimits(maxRowIds, maxRetainedBytes);
1338
+ return mergeFtsPostings(chunkLists, maxRowIds, maxRetainedBytes);
1339
+ }
1340
+ function mergeFtsPostings(chunkLists, maxRowIds, maxRetainedBytes) {
336
1341
  const heap = [];
337
1342
  const before = (left, right) => {
338
1343
  const leftTerm = left.postings[left.position]?.term ?? "";
@@ -391,6 +1396,8 @@ export function collectFtsPostings(chunkLists) {
391
1396
  ordinal += 1;
392
1397
  }
393
1398
  const output = [];
1399
+ let retainedRowIds = 0;
1400
+ let retainedBytes = 0;
394
1401
  while (heap.length > 0) {
395
1402
  const term = heap[0]?.postings[heap[0].position]?.term;
396
1403
  if (term === undefined)
@@ -404,20 +1411,33 @@ export function collectFtsPostings(chunkLists) {
404
1411
  if (posting === undefined)
405
1412
  continue;
406
1413
  posting.rowIds.forEach((rowId, index) => {
407
- rows.set(rowId, Math.max(rows.get(rowId) ?? 0, posting.tf[index] ?? 1));
1414
+ const existing = rows.get(rowId);
1415
+ if (existing === undefined) {
1416
+ rows.set(rowId, posting.tf[index] ?? 1);
1417
+ }
1418
+ else {
1419
+ rows.set(rowId, Math.max(existing, posting.tf[index] ?? 1));
1420
+ }
408
1421
  });
409
1422
  cursor.position += 1;
410
1423
  if (cursor.position < cursor.postings.length)
411
1424
  push(cursor);
412
1425
  }
1426
+ const nextRetainedRowIds = retainedRowIds + rows.size;
1427
+ const nextRetainedBytes = retainedBytes + 64 + term.length * 2 + rows.size * 32;
1428
+ if (nextRetainedRowIds > maxRowIds || nextRetainedBytes > maxRetainedBytes) {
1429
+ return { postings: [], overflow: true };
1430
+ }
413
1431
  const ordered = [...rows].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
414
1432
  output.push({
415
1433
  term,
416
1434
  rowIds: ordered.map(([rowId]) => rowId),
417
1435
  tf: ordered.map(([, frequency]) => frequency),
418
1436
  });
1437
+ retainedRowIds = nextRetainedRowIds;
1438
+ retainedBytes = nextRetainedBytes;
419
1439
  }
420
- return output;
1440
+ return { postings: output, overflow: false };
421
1441
  }
422
1442
  /**
423
1443
  * Shared stale-writer policy for both stores' commit steps: a commit that adds segments to a
@@ -442,7 +1462,11 @@ export function invalidateUncoveredFtsColumns(record, coveredColumnIds) {
442
1462
  }
443
1463
  if (!invalidated)
444
1464
  return undefined;
445
- return { ...record, ftsColumns: next, revision: (record.revision ?? 0) + 1 };
1465
+ return {
1466
+ ...record,
1467
+ ftsColumns: next,
1468
+ revision: safeSum([record.revision, 1], "Table revision"),
1469
+ };
446
1470
  }
447
1471
  /**
448
1472
  * Scalar-index half of the stale-writer rule. A writer that staged table data from catalog
@@ -468,7 +1492,11 @@ export function invalidateUncoveredSecondaryIndexes(record, coveredStorageColumn
468
1492
  }
469
1493
  if (!invalidated)
470
1494
  return undefined;
471
- return { ...record, secondaryIndexes: next, revision: (record.revision ?? 0) + 1 };
1495
+ return {
1496
+ ...record,
1497
+ secondaryIndexes: next,
1498
+ revision: safeSum([record.revision, 1], "Table revision"),
1499
+ };
472
1500
  }
473
1501
  /** Physical posting IDs a current catalog record authorizes a commit to write. */
474
1502
  export function activePostingStorageColumnIds(record) {
@@ -508,6 +1536,17 @@ export class WriteConflictError extends Error {
508
1536
  this.actualVersion = actualVersion;
509
1537
  }
510
1538
  }
1539
+ /** Staged write artifacts were prepared against a structurally different catalog. */
1540
+ export class SchemaConflictError extends Error {
1541
+ expectedEpoch;
1542
+ actualEpoch;
1543
+ name = "SchemaConflictError";
1544
+ constructor(expectedEpoch, actualEpoch) {
1545
+ super(`Schema changed while the write was staged: expected epoch ${String(expectedEpoch)}, found ${String(actualEpoch)}`);
1546
+ this.expectedEpoch = expectedEpoch;
1547
+ this.actualEpoch = actualEpoch;
1548
+ }
1549
+ }
511
1550
  export class TransactionRecordConflictError extends Error {
512
1551
  transactionId;
513
1552
  expectedRevision;
@@ -520,6 +1559,37 @@ export class TransactionRecordConflictError extends Error {
520
1559
  this.actualRevision = actualRevision;
521
1560
  }
522
1561
  }
1562
+ /** Hard storage-boundary limits for one scratch-page write. */
1563
+ export const MAX_TEMP_RUN_PAGES_PER_BATCH = 64;
1564
+ export const MAX_TEMP_RUN_PAGE_BYTES = MAX_STORED_BLOCK_BYTE_LENGTH;
1565
+ export const MAX_TEMP_RUN_BATCH_BYTES = MAX_STORED_BLOCK_BYTE_LENGTH;
1566
+ /**
1567
+ * Refuses an oversized scratch write before an adapter starts a transaction or appends a WAL
1568
+ * record. Query execution splits pages and batches to these limits, so the limits bound atomic
1569
+ * storage work without limiting the total size of a spill.
1570
+ */
1571
+ export function assertTempRunPageBatchLimits(pages) {
1572
+ if (pages.length > MAX_TEMP_RUN_PAGES_PER_BATCH) {
1573
+ throw new RangeError(`Temp run page batch exceeds ${String(MAX_TEMP_RUN_PAGES_PER_BATCH)} pages`);
1574
+ }
1575
+ let totalBytes = 0;
1576
+ for (const page of pages) {
1577
+ if (!(page.bytes instanceof Uint8Array)) {
1578
+ throw new TypeError("Temp run page bytes must be a Uint8Array");
1579
+ }
1580
+ if (page.bytes.byteLength > MAX_TEMP_RUN_PAGE_BYTES) {
1581
+ throw new RangeError(`Temp run page exceeds ${String(MAX_TEMP_RUN_PAGE_BYTES)} bytes`);
1582
+ }
1583
+ totalBytes += page.bytes.byteLength;
1584
+ if (!Number.isSafeInteger(totalBytes)) {
1585
+ throw new RangeError("Temp run page batch bytes exceed the safe integer range");
1586
+ }
1587
+ if (totalBytes > MAX_TEMP_RUN_BATCH_BYTES) {
1588
+ throw new RangeError(`Temp run page batch exceeds ${String(MAX_TEMP_RUN_BATCH_BYTES)} bytes`);
1589
+ }
1590
+ }
1591
+ }
1592
+ export const MAX_TEMP_OWNER_TTL_MS = 60 * 60 * 1_000;
523
1593
  export class TempOwnerConflictError extends Error {
524
1594
  ownerId;
525
1595
  expectedRevision;
@@ -532,37 +1602,122 @@ export class TempOwnerConflictError extends Error {
532
1602
  this.actualRevision = actualRevision;
533
1603
  }
534
1604
  }
1605
+ /**
1606
+ * Bulk payload storage: immutable, opaque byte blobs keyed by structured ids.
1607
+ *
1608
+ * Blocks are written only through transaction staging, which creates their durable provenance
1609
+ * in the same atomic step and rejects duplicate ids without a partial write. Ids contain `/` separators
1610
+ * (`table/<uuid>/segment/<uuid>/part/000001`) and sort lexically; treat them as opaque keys.
1611
+ * Reads MUST return bytes the caller may mutate freely (a fresh copy or freshly deserialized
1612
+ * buffer), and writes MUST NOT alias the caller's buffer — the engine may reuse it.
1613
+ *
1614
+ * Published blocks are retired by superseding them in a commit; only the store's atomic
1615
+ * lease-aware collection step can physically delete payloads once no reader can reach them.
1616
+ */
1617
+ /** Maximum manifest-membership ids accepted by one storage call. */
1618
+ export const MAX_MANIFEST_BLOCK_PRESENCE_IDS = 1_024;
1619
+ /** Maximum UTF-16 code units in any opaque storage identity. */
1620
+ export const MAX_STORAGE_ID_CHARACTERS = 1_024;
1621
+ /** Maximum UTF-16 code units in a persisted catalog name. */
1622
+ export const MAX_CATALOG_NAME_CHARACTERS = 1_024;
1623
+ /** Public adapter database-name bound; names commonly become substrate keys or path segments. */
1624
+ export const MAX_STORAGE_DATABASE_NAME_CHARACTERS = 256;
1625
+ /** Maximum positional items accepted by one public bulk-read operation. */
1626
+ export const MAX_STORAGE_BULK_READ_ITEMS = 1_024;
1627
+ /** Maximum aggregate payload bytes returned by one getBlocks call. */
1628
+ export const MAX_BLOCK_READ_BATCH_BYTES = MAX_STORED_BLOCK_BYTE_LENGTH;
1629
+ export class BlockReadBatchTooLargeError extends RangeError {
1630
+ requestedBytes;
1631
+ limitBytes;
1632
+ name = "BlockReadBatchTooLargeError";
1633
+ constructor(requestedBytes, limitBytes = MAX_BLOCK_READ_BATCH_BYTES) {
1634
+ super(`Block read batch requires ${String(requestedBytes)} bytes; limit is ${String(limitBytes)}`);
1635
+ this.requestedBytes = requestedBytes;
1636
+ this.limitBytes = limitBytes;
1637
+ }
1638
+ }
1639
+ export function assertStorageBulkReadItems(items, label) {
1640
+ if (items.length > MAX_STORAGE_BULK_READ_ITEMS) {
1641
+ throw new RangeError(`${label} cannot exceed ${String(MAX_STORAGE_BULK_READ_ITEMS)} items`);
1642
+ }
1643
+ }
1644
+ export function validateStorageId(value, label = "Storage ID") {
1645
+ if (typeof value !== "string" || value.length === 0) {
1646
+ throw new TypeError(`${label} cannot be empty`);
1647
+ }
1648
+ if (value.length > MAX_STORAGE_ID_CHARACTERS) {
1649
+ throw new TypeError(`${label} exceeds ${String(MAX_STORAGE_ID_CHARACTERS)} characters`);
1650
+ }
1651
+ assertWellFormedString(value, label);
1652
+ return value;
1653
+ }
1654
+ export function validateCatalogName(value, label = "Catalog name") {
1655
+ if (typeof value !== "string" || value.trim().length === 0) {
1656
+ throw new TypeError(`${label} cannot be empty`);
1657
+ }
1658
+ if (value.length > MAX_CATALOG_NAME_CHARACTERS) {
1659
+ throw new TypeError(`${label} exceeds ${String(MAX_CATALOG_NAME_CHARACTERS)} characters`);
1660
+ }
1661
+ assertWellFormedString(value, label);
1662
+ return value;
1663
+ }
1664
+ export function validateStorageDatabaseName(value) {
1665
+ if (typeof value !== "string" || value.trim().length === 0) {
1666
+ throw new TypeError("Storage database name cannot be empty");
1667
+ }
1668
+ if (value.length > MAX_STORAGE_DATABASE_NAME_CHARACTERS) {
1669
+ throw new TypeError(`Storage database name exceeds ${String(MAX_STORAGE_DATABASE_NAME_CHARACTERS)} characters`);
1670
+ }
1671
+ assertWellFormedString(value, "Storage database name");
1672
+ return value;
1673
+ }
535
1674
  export function createManifest(input) {
536
1675
  return {
537
- version: input.expectedVersion === null ? 0 : input.expectedVersion + 1,
1676
+ version: input.expectedVersion === null ? 0 : safeSum([input.expectedVersion, 1], "Manifest version"),
538
1677
  previousVersion: input.expectedVersion,
539
- blockIds: [...new Set(input.blockIds)].sort(),
540
- createdAt: input.createdAt ?? new Date().toISOString(),
541
- ...(input.changedTableIds === undefined ? {} : { changedTableIds: [...input.changedTableIds] }),
1678
+ liveBlockCount: nonNegativeWholeNumber(input.liveBlockCount, "Manifest live block count"),
1679
+ liveBlockBytes: nonNegativeWholeNumber(input.liveBlockBytes, "Manifest live block bytes"),
1680
+ createdAt: input.createdAt ?? dateIsoString(new Date()),
1681
+ changedTableIds: canonicalManifestChangedTableIds(input.changedTableIds),
542
1682
  };
543
1683
  }
544
- /**
545
- * Normalizes the additive L2 partition metadata while preserving legacy segment records verbatim.
546
- */
1684
+ /** Strictly clones the one canonical v1 segment shape. */
547
1685
  export function normalizeSegmentRecord(record) {
548
- if (record.partitionOrdinal === undefined)
1686
+ switch (record.kind) {
1687
+ case "insert":
1688
+ case "upsert":
1689
+ case "update":
1690
+ case "delete":
1691
+ case "base":
1692
+ break;
1693
+ default:
1694
+ throw new TypeError("Segment kind is invalid");
1695
+ }
1696
+ const level = nonNegativeWholeNumber(record.level, "Segment level");
1697
+ if (level > 2)
1698
+ throw new RangeError("Segment level must be between zero and two");
1699
+ nonNegativeFiniteNumber(record.logicalOrder, "Segment logical order");
1700
+ nonNegativeWholeNumber(record.commitOrdinal, "Segment commit ordinal");
1701
+ if (!Array.isArray(record.rowIdSpans)) {
1702
+ throw new TypeError("Segment row ID spans must be an array");
1703
+ }
1704
+ if (record.partitionOrdinal === undefined) {
1705
+ if (level === 2)
1706
+ throw new TypeError("A level-two segment requires a partition ordinal");
549
1707
  return structuredClone(record);
1708
+ }
550
1709
  const partitionOrdinal = nonNegativeWholeNumber(record.partitionOrdinal, "Segment partition ordinal");
551
1710
  if (record.level !== 2) {
552
1711
  throw new TypeError("A partitioned segment must have explicit level two");
553
1712
  }
554
- const kind = record.kind ?? "insert";
1713
+ const kind = record.kind;
555
1714
  if (kind !== "insert" && kind !== "base") {
556
1715
  throw new TypeError("A partitioned segment must be an insert or a merged base");
557
1716
  }
558
- if (record.logicalOrder === undefined) {
559
- throw new TypeError("A partitioned segment requires an explicit logical order");
560
- }
561
- nonNegativeFiniteNumber(record.logicalOrder, "Segment logical order");
562
1717
  const rowCount = positiveWholeNumber(record.rowCount, "Segment row count");
563
1718
  if (kind === "insert") {
564
1719
  // Append-row-range partition: one contiguous positive row-ID interval, no spans.
565
- if (record.rowIdSpans !== undefined) {
1720
+ if (record.rowIdSpans.length !== 0) {
566
1721
  throw new TypeError("A partitioned segment cannot contain row ID spans");
567
1722
  }
568
1723
  if (typeof record.rowIdStart !== "bigint" || record.rowIdStart <= 0n) {
@@ -576,7 +1731,7 @@ export function normalizeSegmentRecord(record) {
576
1731
  }
577
1732
  // Keyed multi-range partition: a merged full-row base whose live rows keep their original
578
1733
  // ids, described by positive, sorted, non-overlapping spans that sum to the row count.
579
- if (record.rowIdSpans === undefined || record.rowIdSpans.length === 0) {
1734
+ if (record.rowIdSpans.length === 0) {
580
1735
  throw new TypeError("A merged partitioned segment requires row ID spans");
581
1736
  }
582
1737
  let spanRows = 0;
@@ -600,20 +1755,34 @@ export function normalizeSegmentRecord(record) {
600
1755
  return structuredClone({ ...record, partitionOrdinal });
601
1756
  }
602
1757
  export function updateTransactionRecord(record, update) {
603
- return {
1758
+ const updated = {
604
1759
  ...record,
605
1760
  ...(update.snapshotVersion === undefined ? {} : { snapshotVersion: update.snapshotVersion }),
606
1761
  ...(update.pendingBlockIds === undefined
607
1762
  ? {}
608
- : { pendingBlockIds: [...new Set(update.pendingBlockIds)].sort() }),
1763
+ : {
1764
+ pendingBlockIds: orderedUniqueIds(update.pendingBlockIds, "Transaction pending block ID"),
1765
+ }),
609
1766
  ...(update.pendingSegmentIds === undefined
610
1767
  ? {}
611
- : { pendingSegmentIds: [...new Set(update.pendingSegmentIds)].sort() }),
1768
+ : {
1769
+ pendingSegmentIds: orderedUniqueIds(update.pendingSegmentIds, "Transaction pending segment ID"),
1770
+ }),
612
1771
  ...(update.status === undefined ? {} : { status: update.status }),
613
1772
  ...(update.committedVersion === undefined ? {} : { committedVersion: update.committedVersion }),
1773
+ ...(update.pendingTableNextRowId === undefined
1774
+ ? {}
1775
+ : { pendingTableNextRowId: update.pendingTableNextRowId }),
614
1776
  updatedAt: update.updatedAt,
615
- revision: record.revision + 1,
1777
+ revision: safeSum([record.revision, 1], "Transaction revision"),
616
1778
  };
1779
+ if (updated.status !== "active") {
1780
+ delete updated.pendingTable;
1781
+ delete updated.pendingTableNextRowId;
1782
+ delete updated.catalogEpochGuard;
1783
+ delete updated.schemaEpochGuard;
1784
+ }
1785
+ return updated;
617
1786
  }
618
1787
  export function createGarbageCollectionJobRecord(input) {
619
1788
  const candidateManifestVersions = uniqueWholeNumbers(input.candidateManifestVersions, "Garbage collection candidate manifest version");
@@ -621,12 +1790,16 @@ export function createGarbageCollectionJobRecord(input) {
621
1790
  const candidateBlockIds = uniqueIds(input.candidateBlockIds, "Garbage collection candidate block ID", true);
622
1791
  const candidateTransactionIds = uniqueIds(input.candidateTransactionIds ?? [], "Garbage collection candidate transaction ID", true);
623
1792
  const createdAt = validTimestamp(input.createdAt, "Garbage collection creation timestamp");
624
- const complete = candidateManifestVersions.length === 0 &&
1793
+ const discovery = input.discovery === undefined
1794
+ ? undefined
1795
+ : normalizeGarbageCollectionDiscovery(input.discovery);
1796
+ const complete = (discovery === undefined || discovery.phase === "complete") &&
1797
+ candidateManifestVersions.length === 0 &&
625
1798
  candidateSegmentIds.length === 0 &&
626
1799
  candidateBlockIds.length === 0 &&
627
1800
  candidateTransactionIds.length === 0;
628
1801
  return {
629
- id: nonEmptyString(input.id, "Garbage collection job ID"),
1802
+ id: validateStorageId(input.id, "Garbage collection job ID"),
630
1803
  candidateManifestVersions,
631
1804
  candidateSegmentIds,
632
1805
  candidateBlockIds,
@@ -651,20 +1824,169 @@ export function createGarbageCollectionJobRecord(input) {
651
1824
  leaseCutoff: validTimestamp(input.leaseCutoff, "Garbage collection lease cutoff"),
652
1825
  createdAt,
653
1826
  updatedAt: createdAt,
1827
+ ...(discovery === undefined ? {} : { discovery }),
1828
+ };
1829
+ }
1830
+ export function normalizeGarbageCollectionDiscovery(discovery) {
1831
+ const runtime = discovery;
1832
+ if (typeof runtime !== "object" || runtime === null) {
1833
+ throw new TypeError("Garbage collection discovery must be an object");
1834
+ }
1835
+ const phase = runtime.phase;
1836
+ if (phase !== "manifests" &&
1837
+ phase !== "manifest-blocks" &&
1838
+ phase !== "segments" &&
1839
+ phase !== "transactions" &&
1840
+ phase !== "compactions" &&
1841
+ phase !== "complete") {
1842
+ throw new TypeError(`Invalid garbage collection discovery phase: ${String(phase)}`);
1843
+ }
1844
+ const stringCursor = (value, name) => value === null ? null : validateStorageId(value, name);
1845
+ const currentManifestVersion = discovery.currentManifestVersion === null
1846
+ ? null
1847
+ : nonNegativeWholeNumber(discovery.currentManifestVersion, "Garbage collection discovery manifest version");
1848
+ const manifestCursor = discovery.manifestCursor === null
1849
+ ? null
1850
+ : nonNegativeWholeNumber(discovery.manifestCursor, "Garbage collection manifest discovery cursor");
1851
+ if (!Number.isSafeInteger(discovery.retainAboveVersion)) {
1852
+ throw new RangeError("Garbage collection retained version floor must be a safe integer");
1853
+ }
1854
+ if (!Number.isSafeInteger(discovery.retainAfter)) {
1855
+ throw new RangeError("Garbage collection retained timestamp floor must be a safe integer");
1856
+ }
1857
+ const resumePhase = runtime.resumePhase;
1858
+ if (resumePhase !== undefined &&
1859
+ resumePhase !== null &&
1860
+ resumePhase !== "manifests" &&
1861
+ resumePhase !== "manifest-blocks" &&
1862
+ resumePhase !== "segments" &&
1863
+ resumePhase !== "transactions" &&
1864
+ resumePhase !== "compactions") {
1865
+ throw new TypeError("Invalid garbage collection resume phase");
1866
+ }
1867
+ const postManifestPhase = runtime.postManifestPhase;
1868
+ if (postManifestPhase !== undefined &&
1869
+ postManifestPhase !== null &&
1870
+ postManifestPhase !== "manifest-blocks" &&
1871
+ postManifestPhase !== "segments" &&
1872
+ postManifestPhase !== "transactions" &&
1873
+ postManifestPhase !== "compactions") {
1874
+ throw new TypeError("Invalid garbage collection post-manifest phase");
1875
+ }
1876
+ const artifactCursor = runtime.artifactCursor;
1877
+ let normalizedArtifactCursor;
1878
+ if (artifactCursor !== undefined && artifactCursor !== null) {
1879
+ if (typeof artifactCursor !== "object") {
1880
+ throw new TypeError("Garbage collection artifact cursor must be an object");
1881
+ }
1882
+ const rawArtifactCursor = artifactCursor;
1883
+ if (rawArtifactCursor.family !== "manifest" &&
1884
+ rawArtifactCursor.family !== "transaction" &&
1885
+ rawArtifactCursor.family !== "compaction") {
1886
+ throw new TypeError(`Invalid garbage collection artifact cursor family: ${String(rawArtifactCursor.family)}`);
1887
+ }
1888
+ normalizedArtifactCursor = {
1889
+ family: rawArtifactCursor.family,
1890
+ recordId: validateStorageId(rawArtifactCursor.recordId, "Garbage collection artifact cursor record ID"),
1891
+ blockId: rawArtifactCursor.blockId === null
1892
+ ? null
1893
+ : validateStorageId(rawArtifactCursor.blockId, "Garbage collection artifact cursor block ID"),
1894
+ blockIndex: nonNegativeWholeNumber(rawArtifactCursor.blockIndex, "Garbage collection artifact block cursor"),
1895
+ segmentIndex: nonNegativeWholeNumber(rawArtifactCursor.segmentIndex, "Garbage collection artifact segment cursor"),
1896
+ };
1897
+ }
1898
+ else {
1899
+ normalizedArtifactCursor = artifactCursor;
1900
+ }
1901
+ return {
1902
+ phase,
1903
+ currentManifestVersion,
1904
+ retainAboveVersion: discovery.retainAboveVersion,
1905
+ retainAfter: discovery.retainAfter,
1906
+ maxPlanningItems: boundedMaintenanceBatchItems(discovery.maxPlanningItems, "Garbage collection planning item limit"),
1907
+ manifestCursor,
1908
+ segmentCursor: stringCursor(discovery.segmentCursor, "Garbage collection segment discovery cursor"),
1909
+ transactionCursor: stringCursor(discovery.transactionCursor, "Garbage collection transaction discovery cursor"),
1910
+ compactionCursor: stringCursor(discovery.compactionCursor, "Garbage collection compaction discovery cursor"),
1911
+ visitedRecords: nonNegativeWholeNumber(discovery.visitedRecords, "Garbage collection discovery visited records"),
1912
+ ...(resumePhase === undefined ? {} : { resumePhase }),
1913
+ ...(postManifestPhase === undefined ? {} : { postManifestPhase }),
1914
+ ...(normalizedArtifactCursor === undefined ? {} : { artifactCursor: normalizedArtifactCursor }),
1915
+ };
1916
+ }
1917
+ export function updateGarbageCollectionPlanningRecord(record, input) {
1918
+ const current = normalizeGarbageCollectionJobRecord(record);
1919
+ if (current.id !== input.jobId || current.revision !== input.expectedRevision) {
1920
+ throw new GarbageCollectionJobConflictError(input.jobId, input.expectedRevision, current.id === input.jobId ? current.revision : null);
1921
+ }
1922
+ if (current.state !== "planned" || current.discovery?.phase === "complete") {
1923
+ throw new TypeError("Only a discovering planned garbage collection job can be updated");
1924
+ }
1925
+ const appendIds = (existing, additions, label) => {
1926
+ const normalized = uniqueIds(additions ?? [], label, true);
1927
+ const seen = new Set(existing);
1928
+ for (const id of normalized) {
1929
+ if (seen.has(id))
1930
+ throw new TypeError(`${label} is already planned: ${id}`);
1931
+ seen.add(id);
1932
+ }
1933
+ return [...existing, ...normalized];
1934
+ };
1935
+ const candidateManifestVersions = [...current.candidateManifestVersions];
1936
+ const manifestSet = new Set(candidateManifestVersions);
1937
+ for (const version of uniqueWholeNumbers(input.candidateManifestVersions ?? [], "Garbage collection candidate manifest version")) {
1938
+ if (manifestSet.has(version)) {
1939
+ throw new TypeError(`Garbage collection manifest is already planned: ${String(version)}`);
1940
+ }
1941
+ manifestSet.add(version);
1942
+ candidateManifestVersions.push(version);
1943
+ }
1944
+ candidateManifestVersions.sort((left, right) => left - right);
1945
+ const candidateSegmentIds = appendIds(current.candidateSegmentIds, input.candidateSegmentIds, "Garbage collection candidate segment ID");
1946
+ const candidateBlockIds = appendIds(current.candidateBlockIds, input.candidateBlockIds, "Garbage collection candidate block ID");
1947
+ const candidateTransactionIds = appendIds(current.candidateTransactionIds, input.candidateTransactionIds, "Garbage collection candidate transaction ID");
1948
+ const discovery = normalizeGarbageCollectionDiscovery(input.discovery);
1949
+ const fixedDiscovery = current.discovery;
1950
+ if (discovery.currentManifestVersion !== fixedDiscovery?.currentManifestVersion ||
1951
+ discovery.retainAboveVersion !== fixedDiscovery.retainAboveVersion ||
1952
+ discovery.retainAfter !== fixedDiscovery.retainAfter ||
1953
+ discovery.maxPlanningItems !== fixedDiscovery.maxPlanningItems) {
1954
+ throw new TypeError("Garbage collection discovery snapshot, retention boundary, and item limit are immutable");
1955
+ }
1956
+ if (candidateManifestVersions.length +
1957
+ candidateSegmentIds.length +
1958
+ candidateBlockIds.length +
1959
+ candidateTransactionIds.length >
1960
+ discovery.maxPlanningItems) {
1961
+ throw new RangeError("Garbage collection planning candidates exceed the persisted limit");
1962
+ }
1963
+ const updated = {
1964
+ ...current,
1965
+ candidateManifestVersions,
1966
+ candidateSegmentIds,
1967
+ candidateBlockIds,
1968
+ candidateTransactionIds,
1969
+ discovery,
1970
+ state: "planned",
1971
+ revision: safeSum([current.revision, 1], "Garbage collection job revision"),
1972
+ updatedAt: validTimestamp(input.updatedAt, "Garbage collection update timestamp"),
654
1973
  };
1974
+ if (garbageCollectionJobComplete(updated))
1975
+ updated.state = "completed";
1976
+ return normalizeGarbageCollectionJobRecord(updated);
655
1977
  }
656
1978
  export function normalizeGarbageCollectionJobRecord(record) {
657
- const legacy = record;
658
1979
  const candidateManifestVersions = uniqueWholeNumbers(record.candidateManifestVersions, "Garbage collection candidate manifest version");
659
1980
  const candidateSegmentIds = uniqueIds(record.candidateSegmentIds, "Garbage collection candidate segment ID", true);
660
1981
  const candidateBlockIds = uniqueIds(record.candidateBlockIds, "Garbage collection candidate block ID", true);
661
- // These fields were added after durable jobs shipped. Missing values are the empty fourth
662
- // phase, which lets an old planned/running job resume after an upgrade without migration.
663
- const candidateTransactionIds = uniqueIds(legacy.candidateTransactionIds ?? [], "Garbage collection candidate transaction ID", true);
1982
+ const candidateTransactionIds = uniqueIds(record.candidateTransactionIds, "Garbage collection candidate transaction ID", true);
664
1983
  const cursor = normalizeGarbageCollectionCursor(record.cursor);
1984
+ const discovery = record.discovery === undefined
1985
+ ? undefined
1986
+ : normalizeGarbageCollectionDiscovery(record.discovery);
665
1987
  const normalized = {
666
1988
  ...record,
667
- id: nonEmptyString(record.id, "Garbage collection job ID"),
1989
+ id: validateStorageId(record.id, "Garbage collection job ID"),
668
1990
  candidateManifestVersions,
669
1991
  candidateSegmentIds,
670
1992
  candidateBlockIds,
@@ -681,15 +2003,24 @@ export function normalizeGarbageCollectionJobRecord(record) {
681
2003
  retainedBlockCount: nonNegativeWholeNumber(record.retainedBlockCount, "Garbage collection retained block count"),
682
2004
  missingBlockCount: nonNegativeWholeNumber(record.missingBlockCount, "Garbage collection missing block count"),
683
2005
  reclaimedBlockBytes: nonNegativeWholeNumber(record.reclaimedBlockBytes, "Garbage collection reclaimed block bytes"),
684
- reclaimedTransactionCount: nonNegativeWholeNumber(legacy.reclaimedTransactionCount ?? 0, "Garbage collection reclaimed transaction count"),
685
- retainedTransactionCount: nonNegativeWholeNumber(legacy.retainedTransactionCount ?? 0, "Garbage collection retained transaction count"),
686
- missingTransactionCount: nonNegativeWholeNumber(legacy.missingTransactionCount ?? 0, "Garbage collection missing transaction count"),
2006
+ reclaimedTransactionCount: nonNegativeWholeNumber(record.reclaimedTransactionCount, "Garbage collection reclaimed transaction count"),
2007
+ retainedTransactionCount: nonNegativeWholeNumber(record.retainedTransactionCount, "Garbage collection retained transaction count"),
2008
+ missingTransactionCount: nonNegativeWholeNumber(record.missingTransactionCount, "Garbage collection missing transaction count"),
687
2009
  state: garbageCollectionJobState(record.state),
688
2010
  revision: nonNegativeWholeNumber(record.revision, "Garbage collection job revision"),
689
2011
  leaseCutoff: validTimestamp(record.leaseCutoff, "Garbage collection lease cutoff"),
690
2012
  createdAt: validTimestamp(record.createdAt, "Garbage collection creation timestamp"),
691
2013
  updatedAt: validTimestamp(record.updatedAt, "Garbage collection update timestamp"),
2014
+ ...(discovery === undefined ? {} : { discovery }),
692
2015
  };
2016
+ if (discovery !== undefined &&
2017
+ candidateManifestVersions.length +
2018
+ candidateSegmentIds.length +
2019
+ candidateBlockIds.length +
2020
+ candidateTransactionIds.length >
2021
+ discovery.maxPlanningItems) {
2022
+ throw new RangeError("Garbage collection planning candidates exceed the persisted limit");
2023
+ }
693
2024
  if (safeSum([
694
2025
  normalized.prunedManifestCount,
695
2026
  normalized.alreadyPrunedManifestCount,
@@ -732,6 +2063,9 @@ export function normalizeGarbageCollectionJobRecord(record) {
732
2063
  }
733
2064
  export function advanceGarbageCollectionJobRecord(record, accounting) {
734
2065
  const current = normalizeGarbageCollectionJobRecord(record);
2066
+ if (current.discovery !== undefined && current.discovery.phase !== "complete") {
2067
+ throw new TypeError("Garbage collection discovery must complete before reclamation starts");
2068
+ }
735
2069
  if (current.state === "completed")
736
2070
  return current;
737
2071
  const increments = {
@@ -805,7 +2139,7 @@ export function advanceGarbageCollectionJobRecord(record, accounting) {
805
2139
  retainedTransactionCount: safeSum([current.retainedTransactionCount, increments.retainedTransactionCount], "Garbage collection retained transaction count"),
806
2140
  missingTransactionCount: safeSum([current.missingTransactionCount, increments.missingTransactionCount], "Garbage collection missing transaction count"),
807
2141
  state: "running",
808
- revision: current.revision + 1,
2142
+ revision: safeSum([current.revision, 1], "Garbage collection revision"),
809
2143
  updatedAt: validTimestamp(accounting.updatedAt, "Garbage collection update timestamp"),
810
2144
  };
811
2145
  if (garbageCollectionJobComplete(updated))
@@ -821,22 +2155,14 @@ export function normalizeCompactionJobRecord(record) {
821
2155
  const logicalBytes = nonNegativeWholeNumber(record.logicalBytes, "Compaction logical bytes");
822
2156
  const sourceStoredBytes = nonNegativeWholeNumber(record.sourceStoredBytes, "Compaction source stored bytes");
823
2157
  const outputStoredBytes = nonNegativeWholeNumber(record.outputStoredBytes, "Compaction output stored bytes");
824
- const hasLevel0SourceStoredBytes = record.level0SourceStoredBytes !== undefined;
825
- const hasAnchorSourceStoredBytes = record.anchorSourceStoredBytes !== undefined;
826
- if (hasLevel0SourceStoredBytes !== hasAnchorSourceStoredBytes) {
827
- throw new TypeError("Compaction source-level byte accounting requires both stored byte fields");
828
- }
829
- const sourceLevelStoredBytes = hasLevel0SourceStoredBytes
830
- ? {
831
- level0SourceStoredBytes: positiveWholeNumber(record.level0SourceStoredBytes, "Compaction level-zero source stored bytes"),
832
- anchorSourceStoredBytes: nonNegativeWholeNumber(record.anchorSourceStoredBytes, "Compaction anchor source stored bytes"),
833
- }
834
- : undefined;
835
- if (sourceLevelStoredBytes !== undefined &&
836
- safeSum([
837
- sourceLevelStoredBytes.level0SourceStoredBytes,
838
- sourceLevelStoredBytes.anchorSourceStoredBytes,
839
- ], "Compaction source-level stored bytes") !== sourceStoredBytes) {
2158
+ const sourceLevelStoredBytes = {
2159
+ level0SourceStoredBytes: nonNegativeWholeNumber(record.level0SourceStoredBytes, "Compaction level-zero source stored bytes"),
2160
+ anchorSourceStoredBytes: nonNegativeWholeNumber(record.anchorSourceStoredBytes, "Compaction anchor source stored bytes"),
2161
+ };
2162
+ if (safeSum([
2163
+ sourceLevelStoredBytes.level0SourceStoredBytes,
2164
+ sourceLevelStoredBytes.anchorSourceStoredBytes,
2165
+ ], "Compaction source-level stored bytes") !== sourceStoredBytes) {
840
2166
  throw new TypeError("Compaction source-level stored bytes must equal source stored bytes");
841
2167
  }
842
2168
  const level2PolicyValues = [
@@ -863,13 +2189,10 @@ export function normalizeCompactionJobRecord(record) {
863
2189
  // Append-row-range promotions consume pure level-zero prefixes; keyed merge promotions may
864
2190
  // also fold a retained level-one anchor, whose bytes never count toward the L0 ceiling.
865
2191
  if (rewritePlan.kind === "rechunk-v1" &&
866
- (sourceLevelStoredBytes?.level0SourceStoredBytes !== sourceStoredBytes ||
2192
+ (sourceLevelStoredBytes.level0SourceStoredBytes !== sourceStoredBytes ||
867
2193
  sourceLevelStoredBytes.anchorSourceStoredBytes !== 0)) {
868
2194
  throw new TypeError("Append-row-range L2 compaction requires only level-zero source bytes");
869
2195
  }
870
- if (rewritePlan.kind === "merge-v1" && sourceLevelStoredBytes === undefined) {
871
- throw new TypeError("Keyed L2 compaction requires source-level byte accounting");
872
- }
873
2196
  const outputPartitionOrdinal = nonNegativeWholeNumber(record.outputPartitionOrdinal, "Compaction output partition ordinal");
874
2197
  const maxWriteAmplification = positiveFiniteNumber(record.maxWriteAmplification, "Compaction maximum write amplification");
875
2198
  const maximumOutputStoredBytes = positiveWholeNumber(record.maximumOutputStoredBytes, "Compaction maximum output stored bytes");
@@ -899,8 +2222,8 @@ export function normalizeCompactionJobRecord(record) {
899
2222
  }
900
2223
  const normalized = {
901
2224
  ...record,
902
- id: nonEmptyString(record.id, "Compaction job ID"),
903
- tableId: nonEmptyString(record.tableId, "Compaction job table ID"),
2225
+ id: validateStorageId(record.id, "Compaction job ID"),
2226
+ tableId: validateStorageId(record.tableId, "Compaction job table ID"),
904
2227
  sourceManifestVersion: nonNegativeWholeNumber(record.sourceManifestVersion, "Compaction source manifest version"),
905
2228
  sourceSegmentIds: rewritePlan.kind === "copy-v1"
906
2229
  ? uniqueIds(record.sourceSegmentIds, "Compaction source segment ID", false)
@@ -918,12 +2241,12 @@ export function normalizeCompactionJobRecord(record) {
918
2241
  logicalBytes,
919
2242
  rewritePlan,
920
2243
  outputCursor: normalizeCompactionOutputCursor(record.outputCursor, rewritePlan),
921
- memoryBudgetBytes: nonNegativeWholeNumber(record.memoryBudgetBytes ?? 0, "Compaction memory budget"),
922
- minimumMemoryBytes: nonNegativeWholeNumber(record.minimumMemoryBytes ?? 0, "Compaction minimum memory"),
923
- ...(sourceLevelStoredBytes ?? {}),
2244
+ memoryBudgetBytes: nonNegativeWholeNumber(record.memoryBudgetBytes, "Compaction memory budget"),
2245
+ minimumMemoryBytes: nonNegativeWholeNumber(record.minimumMemoryBytes, "Compaction minimum memory"),
2246
+ ...sourceLevelStoredBytes,
924
2247
  ...(level2Policy ?? {}),
925
- peakWorkingBytes: nonNegativeWholeNumber(record.peakWorkingBytes ?? 0, "Compaction peak working bytes"),
926
- outputLogicalBytes: nonNegativeWholeNumber(record.outputLogicalBytes ?? (rewritePlan.kind === "copy-v1" ? logicalBytes : 0), "Compaction output logical bytes"),
2248
+ peakWorkingBytes: nonNegativeWholeNumber(record.peakWorkingBytes, "Compaction peak working bytes"),
2249
+ outputLogicalBytes: nonNegativeWholeNumber(record.outputLogicalBytes, "Compaction output logical bytes"),
927
2250
  targetLevel: nonNegativeWholeNumber(record.targetLevel, "Compaction target level"),
928
2251
  state: compactionJobState(record.state),
929
2252
  transactionId: nullableId(record.transactionId, "Compaction transaction ID"),
@@ -974,7 +2297,7 @@ export function updateCompactionJobRecord(record, update) {
974
2297
  }
975
2298
  }
976
2299
  }
977
- const mirrorCopyLogicalBytes = current.rewritePlan?.kind === "copy-v1" &&
2300
+ const mirrorCopyLogicalBytes = current.rewritePlan.kind === "copy-v1" &&
978
2301
  update.logicalBytes !== undefined &&
979
2302
  update.outputLogicalBytes === undefined;
980
2303
  const updated = {
@@ -1001,7 +2324,7 @@ export function updateCompactionJobRecord(record, update) {
1001
2324
  ...(update.outputSegmentId === undefined ? {} : { outputSegmentId: update.outputSegmentId }),
1002
2325
  ...(update.publishedVersion === undefined ? {} : { publishedVersion: update.publishedVersion }),
1003
2326
  updatedAt: update.updatedAt,
1004
- revision: current.revision + 1,
2327
+ revision: safeSum([current.revision, 1], "Compaction revision"),
1005
2328
  };
1006
2329
  if (update.error === null)
1007
2330
  delete updated.error;
@@ -1013,7 +2336,7 @@ export function updateCompactionJobRecord(record, update) {
1013
2336
  return normalized;
1014
2337
  }
1015
2338
  function validateCompactionJobState(record) {
1016
- const plan = record.rewritePlan ?? { kind: "copy-v1" };
2339
+ const plan = record.rewritePlan;
1017
2340
  if (plan.kind === "merge-v1") {
1018
2341
  if (plan.totalRows === 0 && record.outputSegmentId !== null) {
1019
2342
  throw new TypeError("An empty merge compaction cannot have an output segment");
@@ -1026,7 +2349,7 @@ function validateCompactionJobState(record) {
1026
2349
  throw new TypeError("A cancelled compaction cannot contain an error");
1027
2350
  }
1028
2351
  if (record.state === "planned") {
1029
- const isCopy = record.rewritePlan?.kind === "copy-v1";
2352
+ const isCopy = record.rewritePlan.kind === "copy-v1";
1030
2353
  const hasProgress = record.cursor.sourceSegmentIndex !== 0 ||
1031
2354
  record.cursor.sourceBlockIndex !== 0 ||
1032
2355
  record.outputBlockIds.length !== 0 ||
@@ -1056,7 +2379,7 @@ function validateCompactionJobState(record) {
1056
2379
  }
1057
2380
  if (isOutputDrivenCompactionPlan(record.rewritePlan) &&
1058
2381
  expectedCompactionOutputCount(record) > 0 &&
1059
- (record.peakWorkingBytes ?? 0) < (record.minimumMemoryBytes ?? 0)) {
2382
+ record.peakWorkingBytes < record.minimumMemoryBytes) {
1060
2383
  throw new TypeError(`${record.state} compaction requires complete memory accounting`);
1061
2384
  }
1062
2385
  }
@@ -1070,11 +2393,11 @@ function validateCompactionJobState(record) {
1070
2393
  }
1071
2394
  }
1072
2395
  function validateCompactionRewrite(record) {
1073
- const plan = record.rewritePlan ?? { kind: "copy-v1" };
1074
- const outputLogicalBytes = record.outputLogicalBytes ?? 0;
1075
- const memoryBudgetBytes = record.memoryBudgetBytes ?? 0;
1076
- const minimumMemoryBytes = record.minimumMemoryBytes ?? 0;
1077
- const peakWorkingBytes = record.peakWorkingBytes ?? 0;
2396
+ const plan = record.rewritePlan;
2397
+ const outputLogicalBytes = record.outputLogicalBytes;
2398
+ const memoryBudgetBytes = record.memoryBudgetBytes;
2399
+ const minimumMemoryBytes = record.minimumMemoryBytes;
2400
+ const peakWorkingBytes = record.peakWorkingBytes;
1078
2401
  if (plan.kind === "copy-v1") {
1079
2402
  if (record.outputCursor !== null) {
1080
2403
  throw new TypeError("A copy compaction cannot have an output cursor");
@@ -1139,7 +2462,7 @@ function validateCompactionRewrite(record) {
1139
2462
  }
1140
2463
  }
1141
2464
  const cursor = record.outputCursor;
1142
- if (cursor === null || cursor === undefined) {
2465
+ if (cursor === null) {
1143
2466
  throw new TypeError("An output-driven compaction requires an output cursor");
1144
2467
  }
1145
2468
  const completedOutputs = safeSum([
@@ -1162,8 +2485,8 @@ function validateCompactionJobProgress(previous, next) {
1162
2485
  ["source stored bytes", previous.sourceStoredBytes, next.sourceStoredBytes],
1163
2486
  ["output stored bytes", previous.outputStoredBytes, next.outputStoredBytes],
1164
2487
  ["logical bytes", previous.logicalBytes, next.logicalBytes],
1165
- ["output logical bytes", previous.outputLogicalBytes ?? 0, next.outputLogicalBytes ?? 0],
1166
- ["peak working bytes", previous.peakWorkingBytes ?? 0, next.peakWorkingBytes ?? 0],
2488
+ ["output logical bytes", previous.outputLogicalBytes, next.outputLogicalBytes],
2489
+ ["peak working bytes", previous.peakWorkingBytes, next.peakWorkingBytes],
1167
2490
  ]) {
1168
2491
  if (nextValue < previousValue) {
1169
2492
  throw new RangeError(`Compaction ${label} cannot decrease`);
@@ -1199,7 +2522,7 @@ function validateCompactionJobProgress(previous, next) {
1199
2522
  }
1200
2523
  }
1201
2524
  function isInitialOutputCursor(record) {
1202
- const plan = record.rewritePlan ?? { kind: "copy-v1" };
2525
+ const plan = record.rewritePlan;
1203
2526
  if (plan.kind === "copy-v1")
1204
2527
  return record.outputCursor === null;
1205
2528
  const cursor = record.outputCursor;
@@ -1207,7 +2530,7 @@ function isInitialOutputCursor(record) {
1207
2530
  return (cursor?.outputIndex === 0 && cursor.columnIndex === 0 && cursor.rowStart === initialRowStart);
1208
2531
  }
1209
2532
  function hasCompletedCompactionCursor(record) {
1210
- const plan = record.rewritePlan ?? { kind: "copy-v1" };
2533
+ const plan = record.rewritePlan;
1211
2534
  if (plan.kind === "copy-v1") {
1212
2535
  return record.cursor.sourceSegmentIndex === record.sourceSegmentIds.length;
1213
2536
  }
@@ -1217,7 +2540,7 @@ function hasCompletedCompactionCursor(record) {
1217
2540
  cursor.rowStart === plan.totalRows);
1218
2541
  }
1219
2542
  function expectedCompactionOutputCount(record) {
1220
- const plan = record.rewritePlan ?? { kind: "copy-v1" };
2543
+ const plan = record.rewritePlan;
1221
2544
  return plan.kind === "copy-v1"
1222
2545
  ? record.sourceBlockIds.length
1223
2546
  : safeProduct(plan.outputs.length, plan.columns.length, "Rechunk output block count");
@@ -1225,7 +2548,7 @@ function expectedCompactionOutputCount(record) {
1225
2548
  function compactionOutputOrdinal(record) {
1226
2549
  const plan = record.rewritePlan;
1227
2550
  const cursor = record.outputCursor;
1228
- if (!isOutputDrivenCompactionPlan(plan) || cursor === null || cursor === undefined)
2551
+ if (!isOutputDrivenCompactionPlan(plan) || cursor === null)
1229
2552
  return 0;
1230
2553
  return safeSum([
1231
2554
  safeProduct(cursor.outputIndex, plan.columns.length, "Rechunk output cursor"),
@@ -1233,7 +2556,7 @@ function compactionOutputOrdinal(record) {
1233
2556
  ], "Rechunk output cursor");
1234
2557
  }
1235
2558
  function isOutputDrivenCompactionPlan(plan) {
1236
- return plan?.kind === "rechunk-v1" || plan?.kind === "merge-v1";
2559
+ return plan.kind === "rechunk-v1" || plan.kind === "merge-v1";
1237
2560
  }
1238
2561
  function validateCompactionJobTransition(previous, next) {
1239
2562
  const allowed = {
@@ -1258,8 +2581,6 @@ function normalizeCompactionJobCursor(value) {
1258
2581
  };
1259
2582
  }
1260
2583
  function normalizeCompactionRewritePlan(value) {
1261
- if (value === undefined)
1262
- return { kind: "copy-v1" };
1263
2584
  if (typeof value !== "object" || value === null) {
1264
2585
  throw new TypeError("Compaction rewrite plan must be an object");
1265
2586
  }
@@ -1296,7 +2617,7 @@ function normalizeCompactionRewritePlan(value) {
1296
2617
  }
1297
2618
  return {
1298
2619
  rowStart: nonNegativeWholeNumber(Reflect.get(output, "rowStart"), `Rechunk output window ${String(index)} row start`),
1299
- rowCount: positiveUint32(Reflect.get(output, "rowCount"), `Rechunk output window ${String(index)} row count`),
2620
+ rowCount: blockRowCount(Reflect.get(output, "rowCount"), `Rechunk output window ${String(index)} row count`),
1300
2621
  };
1301
2622
  });
1302
2623
  validateContiguousRows(outputs, totalRows, "Rechunk output windows");
@@ -1330,9 +2651,9 @@ function normalizeRechunkSourceColumn(value, totalRows, columnIndex) {
1330
2651
  throw new TypeError(`Rechunk source block ${String(columnIndex)}:${String(blockIndex)} must be an object`);
1331
2652
  }
1332
2653
  return {
1333
- blockId: nonEmptyString(Reflect.get(block, "blockId"), `Rechunk source block ${String(columnIndex)}:${String(blockIndex)} ID`),
2654
+ blockId: validateStorageId(Reflect.get(block, "blockId"), `Rechunk source block ${String(columnIndex)}:${String(blockIndex)} ID`),
1334
2655
  rowStart: nonNegativeWholeNumber(Reflect.get(block, "rowStart"), `Rechunk source block ${String(columnIndex)}:${String(blockIndex)} row start`),
1335
- rowCount: positiveUint32(Reflect.get(block, "rowCount"), `Rechunk source block ${String(columnIndex)}:${String(blockIndex)} row count`),
2656
+ rowCount: blockRowCount(Reflect.get(block, "rowCount"), `Rechunk source block ${String(columnIndex)}:${String(blockIndex)} row count`),
1336
2657
  storedBytes: positiveWholeNumber(Reflect.get(block, "storedBytes"), `Rechunk source block ${String(columnIndex)}:${String(blockIndex)} stored bytes`),
1337
2658
  encodedBytes: nonNegativeWholeNumber(Reflect.get(block, "encodedBytes"), `Rechunk source block ${String(columnIndex)}:${String(blockIndex)} encoded bytes`),
1338
2659
  checksum: uint32(Reflect.get(block, "checksum"), `Rechunk source block ${String(columnIndex)}:${String(blockIndex)} checksum`),
@@ -1340,7 +2661,7 @@ function normalizeRechunkSourceColumn(value, totalRows, columnIndex) {
1340
2661
  });
1341
2662
  validateContiguousRows(sourceBlocks, totalRows, `Rechunk source column ${String(columnIndex)} blocks`);
1342
2663
  return {
1343
- columnId: nonEmptyString(Reflect.get(value, "columnId"), `Rechunk source column ${String(columnIndex)} ID`),
2664
+ columnId: validateStorageId(Reflect.get(value, "columnId"), `Rechunk source column ${String(columnIndex)} ID`),
1344
2665
  type: simpleDataType(Reflect.get(value, "type")),
1345
2666
  sourceBlocks,
1346
2667
  };
@@ -1350,7 +2671,7 @@ function normalizeMergeCompactionRewritePlan(value) {
1350
2671
  const rowIdStart = nonNegativeBigInt(Reflect.get(value, "rowIdStart"), "Merge row ID start");
1351
2672
  const rowIdEndExclusive = nonNegativeBigInt(Reflect.get(value, "rowIdEndExclusive"), "Merge row ID end");
1352
2673
  const rowIdSpans = normalizeRowIdSpans(Reflect.get(value, "rowIdSpans"), totalRows, rowIdStart, rowIdEndExclusive, "Merge output row ID spans");
1353
- const keyColumnId = nonEmptyString(Reflect.get(value, "keyColumnId"), "Merge key column ID");
2674
+ const keyColumnId = validateStorageId(Reflect.get(value, "keyColumnId"), "Merge key column ID");
1354
2675
  const sourceSegmentsValue = Reflect.get(value, "sourceSegments");
1355
2676
  if (!Array.isArray(sourceSegmentsValue) || sourceSegmentsValue.length === 0) {
1356
2677
  throw new TypeError("A merge plan requires at least one source segment");
@@ -1409,7 +2730,7 @@ function normalizeMergeCompactionRewritePlan(value) {
1409
2730
  }
1410
2731
  return {
1411
2732
  rowStart: nonNegativeWholeNumber(Reflect.get(output, "rowStart"), `Merge output window ${String(index)} row start`),
1412
- rowCount: positiveUint32(Reflect.get(output, "rowCount"), `Merge output window ${String(index)} row count`),
2733
+ rowCount: blockRowCount(Reflect.get(output, "rowCount"), `Merge output window ${String(index)} row count`),
1413
2734
  };
1414
2735
  });
1415
2736
  validateContiguousRows(outputs, totalRows, "Merge output windows");
@@ -1516,8 +2837,8 @@ function normalizeMergeSourceSegment(value, segmentIndex) {
1516
2837
  throw new TypeError(`${label} cannot contain duplicate source columns`);
1517
2838
  }
1518
2839
  return {
1519
- segmentId: nonEmptyString(Reflect.get(value, "segmentId"), `${label} ID`),
1520
- transactionId: nonEmptyString(Reflect.get(value, "transactionId"), `${label} transaction ID`),
2840
+ segmentId: validateStorageId(Reflect.get(value, "segmentId"), `${label} ID`),
2841
+ transactionId: validateStorageId(Reflect.get(value, "transactionId"), `${label} transaction ID`),
1521
2842
  committedVersion: nonNegativeWholeNumber(Reflect.get(value, "committedVersion"), `${label} committed version`),
1522
2843
  kind,
1523
2844
  keyColumnId: nullableId(Reflect.get(value, "keyColumnId"), `${label} key column ID`),
@@ -1544,9 +2865,9 @@ function normalizeMergeSourceColumn(value, segmentRowCount, segmentIndex, column
1544
2865
  throw new TypeError(`${label} block ${String(blockIndex)} must be an object`);
1545
2866
  }
1546
2867
  return {
1547
- blockId: nonEmptyString(Reflect.get(block, "blockId"), `${label} block ID`),
2868
+ blockId: validateStorageId(Reflect.get(block, "blockId"), `${label} block ID`),
1548
2869
  rowStart: nonNegativeWholeNumber(Reflect.get(block, "rowStart"), `${label} block row start`),
1549
- rowCount: positiveUint32(Reflect.get(block, "rowCount"), `${label} block row count`),
2870
+ rowCount: blockRowCount(Reflect.get(block, "rowCount"), `${label} block row count`),
1550
2871
  storedBytes: positiveWholeNumber(Reflect.get(block, "storedBytes"), `${label} block stored bytes`),
1551
2872
  encodedBytes: nonNegativeWholeNumber(Reflect.get(block, "encodedBytes"), `${label} block encoded bytes`),
1552
2873
  checksum: uint32(Reflect.get(block, "checksum"), `${label} block checksum`),
@@ -1554,7 +2875,7 @@ function normalizeMergeSourceColumn(value, segmentRowCount, segmentIndex, column
1554
2875
  });
1555
2876
  validateContiguousRows(sourceBlocks, segmentRowCount, `${label} blocks`);
1556
2877
  return {
1557
- columnId: nonEmptyString(Reflect.get(value, "columnId"), `${label} ID`),
2878
+ columnId: validateStorageId(Reflect.get(value, "columnId"), `${label} ID`),
1558
2879
  type: simpleDataType(Reflect.get(value, "type")),
1559
2880
  sourceBlocks,
1560
2881
  };
@@ -1564,7 +2885,7 @@ function normalizeMergeOutputColumn(value, totalRows, sourceBlocks, columnIndex)
1564
2885
  if (typeof value !== "object" || value === null) {
1565
2886
  throw new TypeError(`${label} must be an object`);
1566
2887
  }
1567
- const columnId = nonEmptyString(Reflect.get(value, "columnId"), `${label} ID`);
2888
+ const columnId = validateStorageId(Reflect.get(value, "columnId"), `${label} ID`);
1568
2889
  const type = simpleDataType(Reflect.get(value, "type"));
1569
2890
  const rangesValue = Reflect.get(value, "sourceRanges");
1570
2891
  if (!Array.isArray(rangesValue))
@@ -1575,9 +2896,9 @@ function normalizeMergeOutputColumn(value, totalRows, sourceBlocks, columnIndex)
1575
2896
  }
1576
2897
  const normalized = {
1577
2898
  outputRowStart: nonNegativeWholeNumber(Reflect.get(range, "outputRowStart"), `${label} source range output row start`),
1578
- sourceBlockId: nonEmptyString(Reflect.get(range, "sourceBlockId"), `${label} source range block ID`),
2899
+ sourceBlockId: validateStorageId(Reflect.get(range, "sourceBlockId"), `${label} source range block ID`),
1579
2900
  sourceRowStart: nonNegativeWholeNumber(Reflect.get(range, "sourceRowStart"), `${label} source range block row start`),
1580
- rowCount: positiveUint32(Reflect.get(range, "rowCount"), `${label} source range row count`),
2901
+ rowCount: blockRowCount(Reflect.get(range, "rowCount"), `${label} source range row count`),
1581
2902
  };
1582
2903
  const source = sourceBlocks.get(normalized.sourceBlockId);
1583
2904
  if (source === undefined)
@@ -1703,16 +3024,13 @@ function compareMergeSourceSegments(left, right) {
1703
3024
  }
1704
3025
  function normalizeCompactionOutputCursor(value, plan) {
1705
3026
  if (plan.kind === "copy-v1") {
1706
- if (value !== undefined && value !== null) {
3027
+ if (value !== null) {
1707
3028
  throw new TypeError("A copy compaction cannot have an output cursor");
1708
3029
  }
1709
3030
  return null;
1710
3031
  }
1711
3032
  if (value === undefined) {
1712
- if (plan.kind === "merge-v1") {
1713
- throw new TypeError("A merge compaction requires an explicit output cursor");
1714
- }
1715
- return { outputIndex: 0, columnIndex: 0, rowStart: plan.outputs[0]?.rowStart ?? 0 };
3033
+ throw new TypeError("An output-driven compaction requires an explicit output cursor");
1716
3034
  }
1717
3035
  if (typeof value !== "object" || value === null) {
1718
3036
  throw new TypeError("Rechunk output cursor must be an object");
@@ -1793,11 +3111,12 @@ function normalizeGarbageCollectionCursor(value) {
1793
3111
  manifestIndex: nonNegativeWholeNumber(Reflect.get(value, "manifestIndex"), "Garbage collection manifest cursor"),
1794
3112
  segmentIndex: nonNegativeWholeNumber(Reflect.get(value, "segmentIndex"), "Garbage collection segment cursor"),
1795
3113
  blockIndex: nonNegativeWholeNumber(Reflect.get(value, "blockIndex"), "Garbage collection block cursor"),
1796
- transactionIndex: nonNegativeWholeNumber(Reflect.get(value, "transactionIndex") ?? 0, "Garbage collection transaction cursor"),
3114
+ transactionIndex: nonNegativeWholeNumber(Reflect.get(value, "transactionIndex"), "Garbage collection transaction cursor"),
1797
3115
  };
1798
3116
  }
1799
3117
  function garbageCollectionJobComplete(record) {
1800
- return (record.cursor.manifestIndex === record.candidateManifestVersions.length &&
3118
+ return ((record.discovery === undefined || record.discovery.phase === "complete") &&
3119
+ record.cursor.manifestIndex === record.candidateManifestVersions.length &&
1801
3120
  record.cursor.segmentIndex === record.candidateSegmentIds.length &&
1802
3121
  record.cursor.blockIndex === record.candidateBlockIds.length &&
1803
3122
  record.cursor.transactionIndex === record.candidateTransactionIds.length);
@@ -1805,13 +3124,13 @@ function garbageCollectionJobComplete(record) {
1805
3124
  function uniqueIds(ids, label, sort) {
1806
3125
  if (!Array.isArray(ids))
1807
3126
  throw new TypeError(`${label}s must be an array`);
1808
- const unique = [...new Set(ids.map((id) => nonEmptyString(id, label)))];
3127
+ const unique = [...new Set(ids.map((id) => validateStorageId(id, label)))];
1809
3128
  return sort ? unique.sort() : unique;
1810
3129
  }
1811
3130
  function orderedUniqueIds(ids, label) {
1812
3131
  if (!Array.isArray(ids))
1813
3132
  throw new TypeError(`${label}s must be an array`);
1814
- const normalized = ids.map((id) => nonEmptyString(id, label));
3133
+ const normalized = ids.map((id) => validateStorageId(id, label));
1815
3134
  if (new Set(normalized).size !== normalized.length) {
1816
3135
  throw new TypeError(`${label}s cannot contain duplicates`);
1817
3136
  }
@@ -1823,7 +3142,7 @@ function uniqueWholeNumbers(values, label) {
1823
3142
  return [...new Set(values.map((value) => nonNegativeWholeNumber(value, label)))].sort((left, right) => left - right);
1824
3143
  }
1825
3144
  function nullableId(id, label) {
1826
- return id === null ? null : nonEmptyString(id, label);
3145
+ return id === null ? null : validateStorageId(id, label);
1827
3146
  }
1828
3147
  function nonEmptyString(value, label) {
1829
3148
  if (typeof value !== "string" || value.length === 0) {
@@ -1833,8 +3152,16 @@ function nonEmptyString(value, label) {
1833
3152
  }
1834
3153
  function validTimestamp(value, label) {
1835
3154
  const timestamp = nonEmptyString(value, label);
1836
- if (!Number.isFinite(Date.parse(timestamp)))
1837
- throw new TypeError(`${label} must be valid`);
3155
+ // Storage timestamps are internal coordination metadata, not SQL date values. Keeping the
3156
+ // canonical v1 form fixed at 24 UTF-8 bytes makes byte reservations exact (notably the
3157
+ // manifest-pruning tombstone) and avoids engine-dependent Date.parse spellings.
3158
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(timestamp)) {
3159
+ throw new TypeError(`${label} must be canonical UTC ISO-8601`);
3160
+ }
3161
+ const milliseconds = Date.parse(timestamp);
3162
+ if (!Number.isFinite(milliseconds) || dateIsoString(new Date(milliseconds)) !== timestamp) {
3163
+ throw new TypeError(`${label} must be canonical UTC ISO-8601`);
3164
+ }
1838
3165
  return timestamp;
1839
3166
  }
1840
3167
  function nonNegativeWholeNumber(value, label) {
@@ -1879,6 +3206,13 @@ function positiveUint32(value, label) {
1879
3206
  throw new RangeError(`${label} must be positive`);
1880
3207
  return normalized;
1881
3208
  }
3209
+ function blockRowCount(value, label) {
3210
+ const normalized = positiveUint32(value, label);
3211
+ if (normalized > MAX_BLOCK_ROW_COUNT) {
3212
+ throw new RangeError(`${label} exceeds the block format row limit`);
3213
+ }
3214
+ return normalized;
3215
+ }
1882
3216
  function safeSum(values, label) {
1883
3217
  let total = 0;
1884
3218
  for (const value of values) {
@@ -1918,4 +3252,3 @@ export function floorWholeNumberProduct(left, right, label) {
1918
3252
  }
1919
3253
  return Number(product);
1920
3254
  }
1921
- //# sourceMappingURL=types.js.map