@minnowdb/core 0.3.0 → 0.4.1

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 +4326 -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 +4318 -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,4 +1,4 @@
1
- import { CompactionJobConflictError, LeaseConflictError, SnapshotManifestMissingError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, secondaryUniqueKeyNamespace, WriteConflictError, } from "../storage/types.js";
1
+ import { CompactionJobConflictError, LeaseConflictError, LeaseOwnerConflictError, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_STORAGE_BULK_READ_ITEMS, PostingBuildConflictError, SnapshotManifestMissingError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, secondaryUniqueKeyNamespace, WriteConflictError, } from "../storage/types.js";
2
2
  /** Runs every case in sequence; throws on the first failure, prefixed with the case name. */
3
3
  export async function runBlockStoreConformance(target) {
4
4
  for (const conformanceCase of blockStoreConformanceCases()) {
@@ -72,12 +72,38 @@ function render(value) {
72
72
  return String(value);
73
73
  }
74
74
  }
75
+ async function manifestBlockIds(store, version) {
76
+ const ids = [];
77
+ let afterBlockId = null;
78
+ for (;;) {
79
+ const page = await store.listManifestBlockPage({ version, afterBlockId, limit: 256 });
80
+ ids.push(...page.records.map(({ blockId }) => blockId));
81
+ if (page.nextCursor === null)
82
+ return ids;
83
+ afterBlockId = page.nextCursor;
84
+ }
85
+ }
86
+ async function currentManifestBlockIds(store) {
87
+ const version = await store.getCurrentManifestVersion();
88
+ return version === null ? [] : manifestBlockIds(store, version);
89
+ }
90
+ async function tableSegments(store, tableId) {
91
+ const records = [];
92
+ let afterId = null;
93
+ for (;;) {
94
+ const page = await store.listTableSegmentPage(tableId, afterId, 256);
95
+ records.push(...page.records);
96
+ if (page.nextCursor === null)
97
+ return records;
98
+ afterId = page.nextCursor;
99
+ }
100
+ }
75
101
  // ------------------------------------------------------------------------------------------
76
102
  // Fixtures — the smallest records that exercise each rule.
77
103
  // ------------------------------------------------------------------------------------------
78
104
  const T0 = "2026-01-01T00:00:00.000Z";
105
+ const MIDDLE = "2026-01-01T00:30:00.000Z";
79
106
  const LATER = "2026-01-01T01:00:00.000Z";
80
- const FAR_FUTURE = "2999-01-01T00:00:00.000Z";
81
107
  const PAST = "2000-01-01T00:00:00.000Z";
82
108
  function table(name, keyed = false) {
83
109
  return {
@@ -87,6 +113,7 @@ function table(name, keyed = false) {
87
113
  { id: "col-id", name: "id", type: "number", nullable: false },
88
114
  { id: "col-v", name: "v", type: "string", nullable: true },
89
115
  ],
116
+ managed: false,
90
117
  revision: 0,
91
118
  createdAt: T0,
92
119
  ...(keyed ? { uniqueKeyColumnId: "col-id" } : {}),
@@ -95,6 +122,8 @@ function table(name, keyed = false) {
95
122
  function transaction(id, snapshotVersion) {
96
123
  return {
97
124
  id,
125
+ ownerId: `owner-${id}`,
126
+ expiresAt: LATER,
98
127
  snapshotVersion,
99
128
  pendingBlockIds: [],
100
129
  pendingSegmentIds: [],
@@ -105,7 +134,7 @@ function transaction(id, snapshotVersion) {
105
134
  committedVersion: null,
106
135
  };
107
136
  }
108
- function segment(id, tableId, transactionId, blockId) {
137
+ function segment(id, tableId, transactionId, blockId, commitOrdinal = 0) {
109
138
  return {
110
139
  id,
111
140
  tableId,
@@ -114,6 +143,11 @@ function segment(id, tableId, transactionId, blockId) {
114
143
  rowIdStart: 1n,
115
144
  rowIdEndExclusive: 3n,
116
145
  columnBlockIds: { "col-id": [blockId], "col-v": [blockId] },
146
+ kind: "insert",
147
+ level: 0,
148
+ logicalOrder: 0,
149
+ commitOrdinal,
150
+ rowIdSpans: [],
117
151
  createdAt: T0,
118
152
  };
119
153
  }
@@ -123,17 +157,20 @@ async function commitOne(store, suffix, options = {}) {
123
157
  const transactionId = `txn-${suffix}`;
124
158
  const blockId = `table/t/segment/${suffix}/part/000000`;
125
159
  await store.createTransaction(transaction(transactionId, version));
126
- await store.addBlock(blockId, Uint8Array.of(1, 2, 3, 4));
127
- await store.addSegment(segment(`segment-${suffix}`, options.tableId ?? "table-t", transactionId, blockId));
128
- await store.updateTransaction(transactionId, 0, {
129
- pendingBlockIds: [blockId],
130
- pendingSegmentIds: [`segment-${suffix}`],
160
+ await store.stageTransactionArtifacts({
161
+ transactionId,
162
+ expectedRevision: 0,
163
+ blocks: [{ id: blockId, bytes: Uint8Array.of(1, 2, 3, 4) }],
164
+ segments: [segment(`segment-${suffix}`, options.tableId ?? "table-t", transactionId, blockId)],
131
165
  updatedAt: T0,
132
166
  });
133
167
  const summary = await store.commitTransaction({
134
168
  transactionId,
135
169
  expectedTransactionRevision: 1,
136
170
  expectedManifestVersion: version,
171
+ levelZeroSegmentLimits: [
172
+ { tableId: options.tableId ?? "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS },
173
+ ],
137
174
  committedAt: LATER,
138
175
  ...(options.keyTokens === undefined
139
176
  ? {}
@@ -155,11 +192,83 @@ async function commitOne(store, suffix, options = {}) {
155
192
  export function blockStoreConformanceCases() {
156
193
  return [
157
194
  {
158
- name: "blocks are immutable, copied both directions, and duplicates are refused",
195
+ name: "pending tables stay invisible across multi-batch reopen and publish only at commit",
196
+ async run(target) {
197
+ let store = await target.create();
198
+ const pending = table("pending");
199
+ const epoch = (await store.getCatalogProbe()).catalogEpoch;
200
+ const begun = await store.beginTransaction({
201
+ record: {
202
+ id: "pending-owner",
203
+ ownerId: "owner-pending",
204
+ expiresAt: LATER,
205
+ pendingBlockIds: [],
206
+ pendingSegmentIds: [],
207
+ status: "active",
208
+ revision: 0,
209
+ startedAt: T0,
210
+ updatedAt: T0,
211
+ committedVersion: null,
212
+ },
213
+ pendingTable: { record: pending, nextRowId: 1n, expectedCatalogEpoch: epoch },
214
+ });
215
+ check(begun.record.pendingTable?.id === pending.id, "pending reservation was not recorded");
216
+ check((await store.getTable(pending.id)) === undefined, "pending table became public early");
217
+ checkEqual(await store.listTables(), [], "pending table appeared in the public catalog");
218
+ const firstBlock = "pending/block/1";
219
+ await store.stageTransactionArtifacts({
220
+ transactionId: begun.record.id,
221
+ expectedRevision: begun.record.revision,
222
+ blocks: [{ id: firstBlock, bytes: Uint8Array.of(1) }],
223
+ segments: [segment("pending-segment-1", pending.id, begun.record.id, firstBlock)],
224
+ updatedAt: MIDDLE,
225
+ });
226
+ if (target.reopen !== undefined)
227
+ store = await target.reopen(store);
228
+ check((await store.getTable(pending.id)) === undefined, "reopen exposed a pending table");
229
+ const resumed = await store.getTransaction(begun.record.id);
230
+ check(resumed?.revision === 1, "reopen lost the first pending-table stage");
231
+ const secondBlock = "pending/block/2";
232
+ await store.stageTransactionArtifacts({
233
+ transactionId: begun.record.id,
234
+ expectedRevision: resumed.revision,
235
+ blocks: [{ id: secondBlock, bytes: Uint8Array.of(2) }],
236
+ segments: [
237
+ {
238
+ ...segment("pending-segment-2", pending.id, begun.record.id, secondBlock),
239
+ rowIdStart: 3n,
240
+ rowIdEndExclusive: 5n,
241
+ commitOrdinal: 1,
242
+ },
243
+ ],
244
+ updatedAt: LATER,
245
+ });
246
+ const manifest = await store.commitTransaction({
247
+ transactionId: begun.record.id,
248
+ expectedTransactionRevision: 2,
249
+ expectedManifestVersion: null,
250
+ levelZeroSegmentLimits: [{ tableId: pending.id, limit: MAX_LEVEL_ZERO_SEGMENTS }],
251
+ committedAt: LATER,
252
+ });
253
+ check(manifest.version === 0, "pending-table commit did not publish the first manifest");
254
+ check((await store.getTable(pending.id))?.name === pending.name, "commit lost pending table");
255
+ check((await store.getTransaction(begun.record.id))?.pendingTable === undefined, "commit retained the pending catalog reservation");
256
+ store.close();
257
+ },
258
+ },
259
+ {
260
+ name: "staged blocks are immutable, copied both directions, and duplicates are refused",
159
261
  async run(target) {
160
262
  const store = await target.create();
263
+ await store.createTransaction(transaction("txn-block-copy", null));
161
264
  const bytes = Uint8Array.of(1, 2, 3);
162
- await store.addBlock("block-a", bytes);
265
+ await store.stageTransactionArtifacts({
266
+ transactionId: "txn-block-copy",
267
+ expectedRevision: 0,
268
+ blocks: [{ id: "block-a", bytes }],
269
+ segments: [],
270
+ updatedAt: T0,
271
+ });
163
272
  bytes[0] = 99; // The store must not alias the caller's buffer.
164
273
  const first = await store.getBlock("block-a");
165
274
  check(first?.[0] === 1, "stored bytes changed with the caller's buffer");
@@ -167,7 +276,13 @@ export function blockStoreConformanceCases() {
167
276
  const second = await store.getBlock("block-a");
168
277
  check(second?.[1] === 2, "returned bytes alias the store's copy");
169
278
  try {
170
- await store.addBlock("block-a", Uint8Array.of(9));
279
+ await store.stageTransactionArtifacts({
280
+ transactionId: "txn-block-copy",
281
+ expectedRevision: 1,
282
+ blocks: [{ id: "block-a", bytes: Uint8Array.of(9) }],
283
+ segments: [],
284
+ updatedAt: LATER,
285
+ });
171
286
  throw new Error("a duplicate block id was accepted");
172
287
  }
173
288
  catch (error) {
@@ -178,16 +293,29 @@ export function blockStoreConformanceCases() {
178
293
  },
179
294
  },
180
295
  {
181
- name: "a block batch with any duplicate writes nothing at all",
296
+ name: "an artifact batch with any duplicate writes nothing at all",
182
297
  async run(target) {
183
298
  const store = await target.create();
184
- await store.addBlock("block-a", Uint8Array.of(1));
299
+ await store.createTransaction(transaction("txn-block-batch", null));
300
+ await store.stageTransactionArtifacts({
301
+ transactionId: "txn-block-batch",
302
+ expectedRevision: 0,
303
+ blocks: [{ id: "block-a", bytes: Uint8Array.of(1) }],
304
+ segments: [],
305
+ updatedAt: T0,
306
+ });
185
307
  try {
186
- await store.addBlocks([
187
- { id: "block-b", bytes: Uint8Array.of(2) },
188
- { id: "block-a", bytes: Uint8Array.of(3) },
189
- { id: "block-c", bytes: Uint8Array.of(4) },
190
- ]);
308
+ await store.stageTransactionArtifacts({
309
+ transactionId: "txn-block-batch",
310
+ expectedRevision: 1,
311
+ blocks: [
312
+ { id: "block-b", bytes: Uint8Array.of(2) },
313
+ { id: "block-a", bytes: Uint8Array.of(3) },
314
+ { id: "block-c", bytes: Uint8Array.of(4) },
315
+ ],
316
+ segments: [],
317
+ updatedAt: LATER,
318
+ });
191
319
  throw new Error("a batch containing an existing id was accepted");
192
320
  }
193
321
  catch {
@@ -195,7 +323,42 @@ export function blockStoreConformanceCases() {
195
323
  }
196
324
  checkEqual(await store.getBlock("block-b"), undefined, "a failed batch leaked block-b");
197
325
  checkEqual(await store.getBlock("block-c"), undefined, "a failed batch leaked block-c");
198
- checkEqual(await store.listBlockIds(), ["block-a"], "listBlockIds must be sorted and exact");
326
+ checkEqual(await store.getBlock("block-a"), Uint8Array.of(1), "the first block changed");
327
+ store.close();
328
+ },
329
+ },
330
+ {
331
+ name: "version-scoped block reads are exact, bounded, and reject missing history",
332
+ async run(target) {
333
+ const store = await target.create();
334
+ await store.addTable(table("t"));
335
+ const first = await commitOne(store, "manifest-read-first");
336
+ const second = await commitOne(store, "manifest-read-second");
337
+ checkEqual(await store.hasManifestBlocks(first.version, [first.blockId, "not-a-member"]), [true, false], "manifest membership must be positional and exact");
338
+ checkEqual(await store.readManifestBlock(first.version, first.blockId), Uint8Array.of(1, 2, 3, 4), "a readable manifest member must return its payload");
339
+ checkEqual(await store.readManifestBlock(first.version, second.blockId), undefined, "a non-member block must not be readable through an older manifest");
340
+ await checkThrows(() => store.hasManifestBlocks(second.version, Array.from({ length: MAX_MANIFEST_BLOCK_PRESENCE_IDS + 1 }, (_, index) => `presence-${String(index)}`)), RangeError, "manifest membership cap+1");
341
+ await checkThrows(() => store.getBlocks(Array.from({ length: MAX_STORAGE_BULK_READ_ITEMS + 1 }, (_, index) => `block-${String(index)}`)), RangeError, "block read cap+1");
342
+ const oversizedIds = Array.from({ length: MAX_STORAGE_BULK_READ_ITEMS + 1 }, (_, index) => `id-${String(index)}`);
343
+ await checkThrows(() => store.getTransactions(oversizedIds), RangeError, "transaction read cap+1");
344
+ await checkThrows(() => store.getExistingUniqueKeys("table-t", oversizedIds), RangeError, "unique-key lookup cap+1");
345
+ let job = await store.createGarbageCollectionJob({
346
+ id: "gc-prune-manifest-read",
347
+ candidateManifestVersions: [first.version],
348
+ candidateSegmentIds: [],
349
+ candidateBlockIds: [],
350
+ leaseCutoff: LATER,
351
+ createdAt: T0,
352
+ });
353
+ while (job.state !== "completed") {
354
+ job = (await store.runGarbageCollectionStep({
355
+ jobId: job.id,
356
+ expectedRevision: job.revision,
357
+ maxItems: 16,
358
+ updatedAt: LATER,
359
+ })).job;
360
+ }
361
+ checkEqual(await store.readManifestBlock(first.version, first.blockId), undefined, "a pruned manifest must not expose its former member");
199
362
  store.close();
200
363
  },
201
364
  },
@@ -217,7 +380,7 @@ export function blockStoreConformanceCases() {
217
380
  },
218
381
  },
219
382
  {
220
- name: "secondary-index metadata cannot claim ordering its term encoding does not provide",
383
+ name: "secondary-index metadata must describe one direction per indexed column",
221
384
  async run(target) {
222
385
  const store = await target.create();
223
386
  const record = table("bad-secondary");
@@ -226,9 +389,11 @@ export function blockStoreConformanceCases() {
226
389
  ...record,
227
390
  secondaryIndexes: {
228
391
  bad: {
229
- name: "bad_descending_legacy_index",
392
+ name: "bad_direction_arity_index",
230
393
  columnId: "col-v",
231
- directions: ["desc"],
394
+ columnIds: ["col-v"],
395
+ directions: ["asc", "desc"],
396
+ termEncoding: "tuple-v1",
232
397
  storage: "postings-v1",
233
398
  storageColumnId: "secondary-index:bad",
234
399
  locator: "row-id",
@@ -237,7 +402,7 @@ export function blockStoreConformanceCases() {
237
402
  },
238
403
  },
239
404
  });
240
- throw new Error("a descending legacy term encoding was accepted");
405
+ throw new Error("secondary-index metadata with mismatched direction arity was accepted");
241
406
  }
242
407
  catch (error) {
243
408
  check(error instanceof TypeError, "invalid secondary-index metadata must be rejected");
@@ -256,19 +421,144 @@ export function blockStoreConformanceCases() {
256
421
  tableId: "table-t",
257
422
  });
258
423
  await store.reserveRowIds("table-t", 10);
259
- await store.removeTable("table-t", 0);
260
- checkEqual(await store.listSegments("table-t"), [], "segments must go with the table");
424
+ const dropped = await store.dropTable({
425
+ tableId: "table-t",
426
+ expectedTableRevision: 0,
427
+ expectedManifestVersion: 0,
428
+ expectedCatalogEpoch: (await store.getCatalogProbe()).catalogEpoch,
429
+ committedAt: LATER,
430
+ });
431
+ check(dropped.version === 1, "dropTable must publish one successor manifest");
432
+ checkEqual(await currentManifestBlockIds(store), [], "dropTable must retire the table's manifest blocks");
433
+ checkEqual(await tableSegments(store, "table-t"), [], "segments must go with the table");
261
434
  checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1"]), [], "unique keys must go with the table");
435
+ await store.addTable(table("t", true));
262
436
  const range = await store.reserveRowIds("table-t", 1);
263
437
  check(range.start === 1n, "row-id counters must reset with the table");
264
438
  check((await store.getBlock(blockId)) !== undefined, "blocks are not the catalog's to delete");
265
439
  store.close();
266
440
  },
267
441
  },
442
+ {
443
+ name: "fused table drop rejects without change and resolves without partial state",
444
+ async run(target) {
445
+ const store = await target.create();
446
+ await store.addTable(table("t"));
447
+ const source = await commitOne(store, "source");
448
+ const catalogEpoch = (await store.getCatalogProbe()).catalogEpoch;
449
+ const drop = () => store.dropTable({
450
+ tableId: "table-t",
451
+ expectedTableRevision: 0,
452
+ expectedManifestVersion: source.version,
453
+ expectedCatalogEpoch: catalogEpoch,
454
+ committedAt: LATER,
455
+ });
456
+ await checkThrows(() => store.dropTable({
457
+ tableId: "table-t",
458
+ expectedTableRevision: 9,
459
+ expectedManifestVersion: source.version,
460
+ expectedCatalogEpoch: catalogEpoch,
461
+ committedAt: LATER,
462
+ }), TableRecordConflictError, "dropping with a stale table revision");
463
+ await checkThrows(() => store.dropTable({
464
+ tableId: "table-t",
465
+ expectedTableRevision: 0,
466
+ expectedManifestVersion: source.version - 1,
467
+ expectedCatalogEpoch: catalogEpoch,
468
+ committedAt: LATER,
469
+ }), WriteConflictError, "dropping with a stale manifest revision");
470
+ check((await store.getTable("table-t")) !== undefined &&
471
+ (await store.getCurrentManifestVersion()) === source.version, "a fused drop CAS refusal changed the catalog or manifest");
472
+ const active = transaction("txn-active-table", source.version);
473
+ await store.createTransaction(active);
474
+ const activeBlockId = "table/t/segment/active/part/000000";
475
+ const activeSegment = segment("segment-active-table", "table-t", active.id, activeBlockId);
476
+ const staged = await store.stageTransactionArtifacts({
477
+ transactionId: active.id,
478
+ expectedRevision: 0,
479
+ blocks: [{ id: activeBlockId, bytes: Uint8Array.of(9, 8, 7, 6) }],
480
+ segments: [activeSegment],
481
+ updatedAt: LATER,
482
+ });
483
+ const activeRefusal = await checkThrows(drop, TableInUseError, "dropping a table with an active transaction");
484
+ checkEqual({
485
+ tableId: activeRefusal.tableId,
486
+ ownerKind: activeRefusal.ownerKind,
487
+ ownerId: activeRefusal.ownerId,
488
+ }, { tableId: "table-t", ownerKind: "transaction", ownerId: active.id }, "an active-transaction refusal must identify its exact owner");
489
+ check((await store.getTable("table-t")) !== undefined &&
490
+ (await store.getSegment(activeSegment.id)) !== undefined &&
491
+ (await store.getBlock(activeBlockId)) !== undefined &&
492
+ (await store.getCurrentManifestVersion()) === source.version, "a refused table drop must leave catalog, manifest, and active artifacts unchanged");
493
+ await store.updateTransaction(active.id, staged.revision, {
494
+ status: "aborted",
495
+ updatedAt: LATER,
496
+ });
497
+ await store.createCompactionJob({
498
+ id: "job-table-in-use",
499
+ tableId: "table-t",
500
+ sourceManifestVersion: source.version,
501
+ sourceSegmentIds: ["segment-source"],
502
+ sourceBlockIds: [source.blockId],
503
+ outputBlockIds: [],
504
+ cursor: { sourceSegmentIndex: 0, sourceBlockIndex: 0 },
505
+ processedRows: 0,
506
+ sourceStoredBytes: 0,
507
+ outputStoredBytes: 0,
508
+ logicalBytes: 0,
509
+ rewritePlan: { kind: "copy-v1" },
510
+ outputCursor: null,
511
+ memoryBudgetBytes: 0,
512
+ minimumMemoryBytes: 0,
513
+ level0SourceStoredBytes: 0,
514
+ anchorSourceStoredBytes: 0,
515
+ peakWorkingBytes: 0,
516
+ outputLogicalBytes: 0,
517
+ targetLevel: 1,
518
+ state: "planned",
519
+ transactionId: null,
520
+ outputSegmentId: null,
521
+ publishedVersion: null,
522
+ revision: 0,
523
+ createdAt: T0,
524
+ updatedAt: T0,
525
+ });
526
+ const jobRefusal = await checkThrows(drop, TableInUseError, "dropping a table with a nonterminal compaction job");
527
+ checkEqual({
528
+ tableId: jobRefusal.tableId,
529
+ ownerKind: jobRefusal.ownerKind,
530
+ ownerId: jobRefusal.ownerId,
531
+ }, {
532
+ tableId: "table-t",
533
+ ownerKind: "compaction job",
534
+ ownerId: "job-table-in-use",
535
+ }, "a compaction refusal must identify its exact owner");
536
+ check((await store.getTable("table-t")) !== undefined &&
537
+ (await store.getCurrentManifestVersion()) === source.version &&
538
+ (await store.hasManifestBlocks(source.version, [source.blockId]))[0] === true, "a compaction refusal changed the catalog or manifest");
539
+ await store.cancelCompactionJob("job-table-in-use", 0, LATER);
540
+ const published = await drop();
541
+ check(published.version === source.version + 1, "a fused table drop must publish exactly one successor manifest");
542
+ check((await store.getTable("table-t")) === undefined, "a terminal owner blocked removal");
543
+ checkEqual(await tableSegments(store, "table-t"), [], "a fused drop left table segments");
544
+ checkEqual(await currentManifestBlockIds(store), [], "a fused drop left table blocks live");
545
+ check((await store.getBlock(source.blockId)) !== undefined, "a fused drop physically deleted a block pinned readers may still need");
546
+ store.close();
547
+ },
548
+ },
268
549
  {
269
550
  name: "counter reservations never overlap, even issued concurrently",
270
551
  async run(target) {
271
552
  const store = await target.create();
553
+ const counterTable = table("t", true);
554
+ const counterColumn = counterTable.columns[0];
555
+ if (counterColumn === undefined)
556
+ throw new Error("counter fixture column is missing");
557
+ counterTable.columns[0] = {
558
+ ...counterColumn,
559
+ defaultValue: { kind: "autoincrement" },
560
+ };
561
+ await store.addTable(counterTable);
272
562
  const ranges = await Promise.all(Array.from({ length: 8 }, () => store.reserveRowIds("table-t", 5)));
273
563
  const starts = new Set(ranges.map((range) => range.start));
274
564
  check(starts.size === 8, "concurrent reservations returned overlapping ranges");
@@ -277,6 +567,96 @@ export function blockStoreConformanceCases() {
277
567
  }
278
568
  const bumped = await store.reserveAutoIncrement("table-t", "col-id", 3, 100n);
279
569
  check(bumped.start >= 100n, "reserveAutoIncrement must honor atLeast");
570
+ const maximumExclusive = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
571
+ const { snapshotVersion: _invalidSnapshot, ...invalidRecord } = transaction("txn-invalid-counter", null);
572
+ void _invalidSnapshot;
573
+ await checkThrows(() => store.beginTransaction({
574
+ record: invalidRecord,
575
+ reserveAutoIncrement: {
576
+ tableId: "table-t",
577
+ columnId: "col-id",
578
+ count: 0,
579
+ atLeast: maximumExclusive + 1n,
580
+ },
581
+ }), RangeError, "an invalid fused auto-increment reservation");
582
+ check((await store.getTransaction(invalidRecord.id)) === undefined, "a refused fused reservation must not create its transaction");
583
+ const { snapshotVersion: _validSnapshot, ...validRecord } = transaction("txn-valid-counter", null);
584
+ void _validSnapshot;
585
+ const valid = await store.beginTransaction({
586
+ record: validRecord,
587
+ reserveAutoIncrement: {
588
+ tableId: "table-t",
589
+ columnId: "col-id",
590
+ count: 1,
591
+ atLeast: 150n,
592
+ },
593
+ });
594
+ check(valid.autoIncrementValues?.start === 150n &&
595
+ valid.autoIncrementValues.endExclusive === 151n, "a valid fused reservation after refusal must use the unchanged counter");
596
+ await store.updateTransaction(valid.record.id, valid.record.revision, {
597
+ status: "aborted",
598
+ updatedAt: LATER,
599
+ });
600
+ await checkThrows(() => store.reserveAutoIncrement("table-t", "col-id", 0, maximumExclusive + 1n), RangeError, "an auto-increment bump outside the safe-integer range");
601
+ const afterRefusal = await store.reserveAutoIncrement("table-t", "col-id", 1, 200n);
602
+ check(afterRefusal.start === 200n && afterRefusal.endExclusive === 201n, "a refused auto-increment bump must not mutate the counter");
603
+ const boundary = await store.reserveAutoIncrement("table-t", "col-id", 1, maximumExclusive - 1n);
604
+ check(boundary.endExclusive === maximumExclusive, "the final safe auto-increment value must remain reservable");
605
+ const exhausted = await store.reserveAutoIncrement("table-t", "col-id", 0, maximumExclusive);
606
+ check(exhausted.start === maximumExclusive && exhausted.endExclusive === maximumExclusive, "a zero-count bump to the exclusive boundary must be representable");
607
+ await checkThrows(() => store.reserveAutoIncrement("table-t", "col-id", 1), RangeError, "an auto-increment range crossing the safe-integer boundary");
608
+ store.close();
609
+ },
610
+ },
611
+ {
612
+ name: "transaction ownership renewal races expiry abort atomically",
613
+ async run(target) {
614
+ const store = await target.create();
615
+ const record = transaction("txn-live", null);
616
+ record.expiresAt = MIDDLE;
617
+ await store.createTransaction(record);
618
+ check(!(await store.renewTransaction({
619
+ transactionId: record.id,
620
+ ownerId: record.ownerId,
621
+ expiresAtCutoff: MIDDLE,
622
+ expiresAt: LATER,
623
+ })), "ownership expired exactly at the cutoff must not be resurrected");
624
+ check((await store.getTransaction(record.id))?.expiresAt === MIDDLE, "a refused expiry-boundary renewal mutated the record");
625
+ check(!(await store.renewTransaction({
626
+ transactionId: record.id,
627
+ ownerId: "wrong-owner",
628
+ expiresAtCutoff: T0,
629
+ expiresAt: LATER,
630
+ })), "another owner must not renew a transaction");
631
+ check((await store.getTransaction(record.id))?.revision === 0, "ownership renewal must not contend on the data revision");
632
+ const [renewed, aborted] = await Promise.all([
633
+ store.renewTransaction({
634
+ transactionId: record.id,
635
+ ownerId: record.ownerId,
636
+ expiresAtCutoff: T0,
637
+ expiresAt: LATER,
638
+ }),
639
+ store.abortTransactionIfExpired({
640
+ transactionId: record.id,
641
+ expectedOwnerId: record.ownerId,
642
+ expiresAtCutoff: MIDDLE,
643
+ updatedAt: LATER,
644
+ }),
645
+ ]);
646
+ const final = await store.getTransaction(record.id);
647
+ check(final !== undefined, "the liveness race lost its transaction record");
648
+ check((renewed && aborted === undefined && final.status === "active") ||
649
+ (!renewed && aborted?.status === "aborted" && final.status === "aborted"), "renewal and expiry abort must have one linearized winner");
650
+ if (final.status === "active") {
651
+ check(final.expiresAt === LATER, "the winning renewal did not extend ownership");
652
+ const expired = await store.abortTransactionIfExpired({
653
+ transactionId: final.id,
654
+ expectedOwnerId: final.ownerId,
655
+ expiresAtCutoff: "9999-12-31T23:59:59.999Z",
656
+ updatedAt: LATER,
657
+ });
658
+ check(expired?.status === "aborted", "an expired owner was not atomically aborted");
659
+ }
280
660
  store.close();
281
661
  },
282
662
  },
@@ -290,7 +670,8 @@ export function blockStoreConformanceCases() {
290
670
  });
291
671
  checkEqual(await store.getCurrentManifestVersion(), version, "the version must advance");
292
672
  const manifest = await store.getCurrentManifest();
293
- check(manifest?.blockIds.includes(blockId) === true, "the manifest must include the committed block");
673
+ check(manifest !== undefined &&
674
+ (await store.hasManifestBlocks(manifest.version, [blockId]))[0] === true, "the manifest must include the committed block");
294
675
  const record = await store.getTransaction("txn-one");
295
676
  check(record?.status === "committed" && record.committedVersion === version, "the transaction record must flip to committed with the published version");
296
677
  checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1", "number:2"]), ["number:1"], "committed unique keys must be queryable");
@@ -332,9 +713,11 @@ export function blockStoreConformanceCases() {
332
713
  await store.addTable(table("t", true));
333
714
  const { version } = await commitOne(store, "one", { keyTokens: ["number:1"] });
334
715
  await store.createTransaction(transaction("txn-dup", version));
335
- await store.addBlock("block-dup", Uint8Array.of(9));
336
- await store.updateTransaction("txn-dup", 0, {
337
- pendingBlockIds: ["block-dup"],
716
+ await store.stageTransactionArtifacts({
717
+ transactionId: "txn-dup",
718
+ expectedRevision: 0,
719
+ blocks: [{ id: "block-dup", bytes: Uint8Array.of(9) }],
720
+ segments: [],
338
721
  updatedAt: T0,
339
722
  });
340
723
  await checkThrows(() => store.commitTransaction({
@@ -350,32 +733,6 @@ export function blockStoreConformanceCases() {
350
733
  store.close();
351
734
  },
352
735
  },
353
- {
354
- name: "publishManifest compare-and-swaps and refuses missing blocks",
355
- async run(target) {
356
- const store = await target.create();
357
- await store.addBlock("block-a", Uint8Array.of(1));
358
- await checkThrows(() => store.publishManifest({ expectedVersion: 3, blockIds: ["block-a"], createdAt: T0 }), WriteConflictError, "publishing against the wrong expected version");
359
- try {
360
- await store.publishManifest({
361
- expectedVersion: null,
362
- blockIds: ["missing"],
363
- createdAt: T0,
364
- });
365
- throw new Error("a manifest referencing a missing block was published");
366
- }
367
- catch (error) {
368
- check(!(error instanceof WriteConflictError), "missing blocks are not a version conflict");
369
- }
370
- const manifest = await store.publishManifest({
371
- expectedVersion: null,
372
- blockIds: ["block-a"],
373
- createdAt: T0,
374
- });
375
- check(manifest.version === 0, "the first published version must be 0");
376
- store.close();
377
- },
378
- },
379
736
  {
380
737
  name: "leases pin versions, renew by CAS, and expire honestly",
381
738
  async run(target) {
@@ -387,14 +744,47 @@ export function blockStoreConformanceCases() {
387
744
  kind: "reader",
388
745
  manifestVersion: version,
389
746
  ownerId: "owner-a",
390
- expiresAt: FAR_FUTURE,
747
+ createdAt: T0,
748
+ expiresAt: LATER,
391
749
  revision: 0,
392
750
  });
393
- await checkThrows(() => store.renewLease("lease-a", 9, FAR_FUTURE), LeaseConflictError, "renewing with a stale revision");
394
- const renewed = await store.renewLease("lease-a", 0, FAR_FUTURE);
751
+ await checkThrows(() => store.renewLease({
752
+ id: "lease-a",
753
+ expectedRevision: 9,
754
+ expiresAtCutoff: T0,
755
+ expiresAt: LATER,
756
+ }), LeaseConflictError, "renewing with a stale revision");
757
+ const renewed = await store.renewLease({
758
+ id: "lease-a",
759
+ expectedRevision: 0,
760
+ expiresAtCutoff: T0,
761
+ expiresAt: LATER,
762
+ });
395
763
  check(renewed.revision === 1, "a renewal must advance the revision");
764
+ await checkThrows(() => store.removeLease({ id: "lease-a", ownerId: "wrong-owner" }), LeaseOwnerConflictError, "releasing another owner's lease");
765
+ check((await store.getLease("lease-a"))?.revision === 1, "a refused lease release mutated the record");
396
766
  checkEqual(await store.removeLeaseIfExpired("lease-a", 1, PAST), false, "an unexpired lease must not be removed");
397
767
  checkEqual(await store.removeLeaseIfExpired("lease-a", 1, "3000-01-01T00:00:00.000Z"), true, "an expired lease must be removed");
768
+ await store.createLease({
769
+ id: "lease-race",
770
+ kind: "reader",
771
+ manifestVersion: version,
772
+ ownerId: "owner-a",
773
+ createdAt: T0,
774
+ expiresAt: LATER,
775
+ revision: 0,
776
+ });
777
+ const [, release] = await Promise.allSettled([
778
+ store.renewLease({
779
+ id: "lease-race",
780
+ expectedRevision: 0,
781
+ expiresAtCutoff: T0,
782
+ expiresAt: LATER,
783
+ }),
784
+ store.removeLease({ id: "lease-race", ownerId: "owner-a" }),
785
+ ]);
786
+ check(release.status === "fulfilled", "a same-owner renewal made release unsafe");
787
+ check((await store.getLease("lease-race")) === undefined, "same-owner release did not linearize after/before renewal");
398
788
  store.close();
399
789
  },
400
790
  },
@@ -404,31 +794,30 @@ export function blockStoreConformanceCases() {
404
794
  const store = await target.create();
405
795
  await store.addTable(table("t"));
406
796
  const first = await commitOne(store, "one");
407
- // Supersede the first commit's block so version 0 becomes collectable.
408
- const transactionId = "txn-two";
409
- await store.createTransaction(transaction(transactionId, first.version));
410
- await store.addBlock("block-two", Uint8Array.of(5));
411
- await store.addSegment(segment("segment-two", "table-t", transactionId, "block-two"));
412
- await store.updateTransaction(transactionId, 0, {
413
- pendingBlockIds: ["block-two"],
414
- pendingSegmentIds: ["segment-two"],
415
- updatedAt: T0,
416
- });
417
- await store.commitTransaction({
418
- transactionId,
419
- expectedTransactionRevision: 1,
797
+ // A fused table drop is the ordinary, intent-bound way to retire visible table bytes.
798
+ await store.dropTable({
799
+ tableId: "table-t",
800
+ expectedTableRevision: 0,
420
801
  expectedManifestVersion: first.version,
421
- removedBlockIds: [first.blockId],
802
+ expectedCatalogEpoch: (await store.getCatalogProbe()).catalogEpoch,
422
803
  committedAt: LATER,
423
804
  });
805
+ const retiredBeforeCollection = await store.listRetiredManifestBlockPage({
806
+ removedThroughVersion: first.version + 1,
807
+ afterBlockId: null,
808
+ limit: 1,
809
+ });
810
+ checkEqual(retiredBeforeCollection.records.map((record) => record.blockId), [first.blockId], "retired provenance must remain independently pageable before collection");
424
811
  const job = await store.createGarbageCollectionJob({
425
812
  id: "gc-1",
426
813
  candidateManifestVersions: [first.version],
427
- candidateSegmentIds: ["segment-one"],
814
+ candidateSegmentIds: [],
428
815
  candidateBlockIds: [first.blockId],
429
816
  leaseCutoff: LATER,
430
817
  createdAt: LATER,
431
818
  });
819
+ await checkThrows(() => store.removeGarbageCollectionJob(job.id), Error, "removing a non-completed garbage-collection job");
820
+ check((await store.getGarbageCollectionJob(job.id))?.state === "planned", "a refused garbage-collection job removal mutated the record");
432
821
  const step = await store.runGarbageCollectionStep({
433
822
  jobId: job.id,
434
823
  expectedRevision: job.revision,
@@ -438,9 +827,16 @@ export function blockStoreConformanceCases() {
438
827
  checkEqual(step.prunedManifestVersions, [first.version], "the superseded version must prune");
439
828
  checkEqual(step.reclaimedBlockIds, [first.blockId], "the superseded block must reclaim");
440
829
  checkEqual(await store.getBlock(first.blockId), undefined, "reclaimed bytes must be gone");
441
- check((await store.getBlock("block-two")) !== undefined, "live bytes must survive");
830
+ checkEqual((await store.listRetiredManifestBlockPage({
831
+ removedThroughVersion: first.version + 1,
832
+ afterBlockId: null,
833
+ limit: 1,
834
+ })).records, [], "payload reclamation must atomically remove its retired provenance");
835
+ checkEqual(await store.getCurrentManifestVersion(), 1, "the successor manifest was lost");
442
836
  const pruned = await store.getManifest(first.version);
443
837
  check(pruned?.prunedAt !== undefined, "pruned manifests are tombstoned, not deleted");
838
+ await store.removeGarbageCollectionJob(job.id);
839
+ check((await store.getGarbageCollectionJob(job.id)) === undefined, "a completed garbage-collection job was retained");
444
840
  store.close();
445
841
  },
446
842
  },
@@ -448,6 +844,12 @@ export function blockStoreConformanceCases() {
448
844
  name: "temp spill pages are isolated scratch; owner records conflict by exact class",
449
845
  async run(target) {
450
846
  const store = await target.create();
847
+ await store.createTempOwner({
848
+ ownerId: "o1",
849
+ createdAt: T0,
850
+ expiresAt: LATER,
851
+ revision: 0,
852
+ });
451
853
  await store.putTempRunPage({
452
854
  ownerId: "o1",
453
855
  runId: "r1",
@@ -464,8 +866,12 @@ export function blockStoreConformanceCases() {
464
866
  }
465
867
  await store.removeTempRun("o1", "r1");
466
868
  checkEqual(await store.getTempRunPage("o1", "r1", 0), undefined, "a removed run must be gone");
467
- await store.createTempOwner({ ownerId: "o1", expiresAt: FAR_FUTURE, revision: 0 });
468
- await checkThrows(() => store.renewTempOwner("o1", 9, FAR_FUTURE), TempOwnerConflictError, "renewing a temp owner with a stale revision");
869
+ await checkThrows(() => store.renewTempOwner({
870
+ ownerId: "o1",
871
+ expectedRevision: 9,
872
+ expiresAtCutoff: T0,
873
+ expiresAt: LATER,
874
+ }), TempOwnerConflictError, "renewing a temp owner with a stale revision");
469
875
  checkEqual(await store.removeTempOwnerIfExpired("o1", PAST), false, "an unexpired owner must survive the sweep");
470
876
  const listed = await store.listTempOwnerIdsPage(null, 10);
471
877
  check(listed.records.includes("o1"), "owners must be listable");
@@ -490,6 +896,14 @@ export function blockStoreConformanceCases() {
490
896
  sourceStoredBytes: 0,
491
897
  outputStoredBytes: 0,
492
898
  logicalBytes: 0,
899
+ rewritePlan: { kind: "copy-v1" },
900
+ outputCursor: null,
901
+ memoryBudgetBytes: 0,
902
+ minimumMemoryBytes: 0,
903
+ level0SourceStoredBytes: 0,
904
+ anchorSourceStoredBytes: 0,
905
+ peakWorkingBytes: 0,
906
+ outputLogicalBytes: 0,
493
907
  targetLevel: 1,
494
908
  state: "planned",
495
909
  transactionId: null,
@@ -499,17 +913,61 @@ export function blockStoreConformanceCases() {
499
913
  createdAt: T0,
500
914
  updatedAt: T0,
501
915
  });
916
+ await checkThrows(() => store.removeCompactionJob("job-1"), Error, "removing a nonterminal compaction job");
917
+ check((await store.getCompactionJob("job-1"))?.state === "planned", "a refused compaction-job removal mutated the record");
502
918
  await checkThrows(() => store.updateCompactionJob("job-1", 9, {
503
919
  state: "running",
504
920
  transactionId: "txn-one",
505
921
  updatedAt: LATER,
506
922
  }), CompactionJobConflictError, "updating a compaction job with a stale revision");
507
- const updated = await store.updateCompactionJob("job-1", 0, {
508
- state: "running",
509
- transactionId: "txn-one",
923
+ const updated = await store.updateCompactionJob("job-1", 0, { updatedAt: LATER });
924
+ check(updated.revision === 1, "a job update must advance the revision");
925
+ await store.cancelCompactionJob("job-1", updated.revision, LATER);
926
+ check(await store.removeCompactionJob("job-1"), "terminal job was not removed");
927
+ check((await store.getCompactionJob("job-1")) === undefined, "terminal job was retained");
928
+ await store.createTransaction(transaction("txn-orphan-output", version));
929
+ await store.stageTransactionArtifacts({
930
+ transactionId: "txn-orphan-output",
931
+ expectedRevision: 0,
932
+ blocks: [{ id: "orphan-compaction-output", bytes: Uint8Array.of(9) }],
933
+ segments: [],
934
+ updatedAt: T0,
935
+ });
936
+ await store.updateTransaction("txn-orphan-output", 1, {
937
+ status: "aborted",
510
938
  updatedAt: LATER,
511
939
  });
512
- check(updated.revision === 1, "a job update must advance the revision");
940
+ await store.createCompactionJob({
941
+ id: "job-orphan-output",
942
+ tableId: "table-t",
943
+ sourceManifestVersion: version,
944
+ sourceSegmentIds: ["segment-one"],
945
+ sourceBlockIds: [blockId],
946
+ outputBlockIds: ["orphan-compaction-output"],
947
+ cursor: { sourceSegmentIndex: 1, sourceBlockIndex: 0 },
948
+ processedRows: 0,
949
+ sourceStoredBytes: 0,
950
+ outputStoredBytes: 1,
951
+ logicalBytes: 1,
952
+ rewritePlan: { kind: "copy-v1" },
953
+ outputCursor: null,
954
+ memoryBudgetBytes: 0,
955
+ minimumMemoryBytes: 0,
956
+ level0SourceStoredBytes: 0,
957
+ anchorSourceStoredBytes: 0,
958
+ peakWorkingBytes: 0,
959
+ outputLogicalBytes: 1,
960
+ targetLevel: 1,
961
+ state: "cancelled",
962
+ transactionId: null,
963
+ outputSegmentId: null,
964
+ publishedVersion: null,
965
+ revision: 0,
966
+ createdAt: T0,
967
+ updatedAt: LATER,
968
+ });
969
+ check(await store.removeCompactionJob("job-orphan-output"), "terminal compaction metadata with independent transaction provenance was retained");
970
+ check((await store.getBlock("orphan-compaction-output")) !== undefined, "removing terminal metadata deleted an independently proven output");
513
971
  store.close();
514
972
  },
515
973
  },
@@ -517,6 +975,17 @@ export function blockStoreConformanceCases() {
517
975
  name: "full-text bases serve exact and prefix candidates with honest coverage",
518
976
  async run(target) {
519
977
  const store = await target.create();
978
+ await store.addTable({
979
+ ...table("t"),
980
+ ftsColumns: {
981
+ "col-v": {
982
+ storage: "fts-chunks-v1",
983
+ tokenizerVersion: 1,
984
+ state: "ready",
985
+ buildFromVersion: 0,
986
+ },
987
+ },
988
+ });
520
989
  await store.writeFtsBase("table-t", "col-v", {
521
990
  coversVersion: 0,
522
991
  chunks: [
@@ -557,21 +1026,79 @@ export function blockStoreConformanceCases() {
557
1026
  },
558
1027
  },
559
1028
  });
560
- await store.beginFtsBaseBuild("table-postings", "col-v", "abandoned");
561
- await store.writeFtsBaseBuildChunk("table-postings", "col-v", "abandoned", 0, [
562
- { term: "old", rowIds: [1n], tf: [1] },
563
- ]);
564
- await store.beginFtsBaseBuild("table-postings", "col-v", "replacement");
565
- await store.writeFtsBaseBuildChunk("table-postings", "col-v", "replacement", 0, [
566
- { term: "alpha", rowIds: [2n], tf: [1] },
567
- ]);
568
- await store.writeFtsBaseBuildChunk("table-postings", "col-v", "replacement", 1, [
569
- { term: "omega", rowIds: [3n], tf: [1] },
570
- ]);
571
- await store.finishFtsBaseBuild("table-postings", "col-v", "replacement", {
1029
+ await store.beginFtsBaseBuild({
1030
+ tableId: "table-postings",
1031
+ columnId: "col-v",
1032
+ buildId: "abandoned",
1033
+ ownerId: "owner-abandoned",
1034
+ createdAt: "2099-01-01T00:00:00.000Z",
1035
+ expiresAt: "2099-01-01T00:10:00.000Z",
1036
+ });
1037
+ await store.writeFtsBaseBuildChunk({
1038
+ tableId: "table-postings",
1039
+ columnId: "col-v",
1040
+ buildId: "abandoned",
1041
+ ownerId: "owner-abandoned",
1042
+ expiresAtCutoff: "2099-01-01T00:00:01.000Z",
1043
+ expiresAt: "2099-01-01T00:10:00.000Z",
1044
+ updatedAt: "2099-01-01T00:00:01.000Z",
1045
+ ordinal: 0,
1046
+ chunk: [{ term: "old", rowIds: [1n], tf: [1] }],
1047
+ });
1048
+ await store.beginFtsBaseBuild({
1049
+ tableId: "table-postings",
1050
+ columnId: "col-v",
1051
+ buildId: "replacement",
1052
+ ownerId: "owner-replacement",
1053
+ createdAt: "2099-01-01T00:10:00.000Z",
1054
+ expiresAt: "2099-01-01T00:20:00.000Z",
1055
+ });
1056
+ const firstChunk = {
1057
+ tableId: "table-postings",
1058
+ columnId: "col-v",
1059
+ buildId: "replacement",
1060
+ ownerId: "owner-replacement",
1061
+ expiresAtCutoff: "2099-01-01T00:10:01.000Z",
1062
+ expiresAt: "2099-01-01T00:20:00.000Z",
1063
+ updatedAt: "2099-01-01T00:10:01.000Z",
1064
+ ordinal: 0,
1065
+ chunk: [{ term: "alpha", rowIds: [2n], tf: [1] }],
1066
+ };
1067
+ await store.writeFtsBaseBuildChunk(firstChunk);
1068
+ // Lost-ack replay is idempotent: retrying the identical chunk neither duplicates it nor
1069
+ // advances the ordinal, while changing acknowledged bytes is an ownership conflict.
1070
+ await store.writeFtsBaseBuildChunk(firstChunk);
1071
+ await checkThrows(() => store.writeFtsBaseBuildChunk({
1072
+ ...firstChunk,
1073
+ chunk: [{ term: "alpha", rowIds: [999n], tf: [1] }],
1074
+ }), PostingBuildConflictError, "replaying an acknowledged postings ordinal with changed bytes");
1075
+ if (target.reopen !== undefined) {
1076
+ store = await target.reopen(store);
1077
+ // The active owner and completed ordinal are durable too. Replaying after a process
1078
+ // restart must still be a no-op, not a duplicate allocation or a vanished build.
1079
+ await store.writeFtsBaseBuildChunk(firstChunk);
1080
+ }
1081
+ await store.writeFtsBaseBuildChunk({
1082
+ tableId: "table-postings",
1083
+ columnId: "col-v",
1084
+ buildId: "replacement",
1085
+ ownerId: "owner-replacement",
1086
+ expiresAtCutoff: "2099-01-01T00:10:02.000Z",
1087
+ expiresAt: "2099-01-01T00:20:00.000Z",
1088
+ updatedAt: "2099-01-01T00:10:02.000Z",
1089
+ ordinal: 1,
1090
+ chunk: [{ term: "omega", rowIds: [3n], tf: [1] }],
1091
+ });
1092
+ await store.finishFtsBaseBuild({
1093
+ tableId: "table-postings",
1094
+ columnId: "col-v",
1095
+ buildId: "replacement",
1096
+ ownerId: "owner-replacement",
1097
+ expiresAtCutoff: "2099-01-01T00:10:03.000Z",
572
1098
  coversVersion: 4,
573
1099
  chunkCount: 2,
574
1100
  totalTokens: 2,
1101
+ completedAt: "2099-01-01T00:10:03.000Z",
575
1102
  });
576
1103
  let candidates = await store.readFtsCandidates("table-postings", "col-v", [
577
1104
  { term: "old", prefix: false },
@@ -585,6 +1112,22 @@ export function blockStoreConformanceCases() {
585
1112
  checkEqual(candidates.rowIdsByTerm, [[3n]], "a finished generation must survive reopen");
586
1113
  check(candidates.hasBase, "a reopened generation must still report its base");
587
1114
  }
1115
+ await checkThrows(() => store.removeFtsColumn("table-postings", "col-v"), Error, "removing postings still owned by a ready catalog accelerator");
1116
+ const indexed = await store.getTable("table-postings");
1117
+ check(indexed !== undefined, "the postings table disappeared");
1118
+ await store.updateTable(indexed.id, indexed.revision, {
1119
+ ftsColumns: {
1120
+ ...indexed.ftsColumns,
1121
+ "col-v": {
1122
+ ...(indexed.ftsColumns?.["col-v"] ?? {
1123
+ storage: "fts-chunks-v1",
1124
+ tokenizerVersion: 1,
1125
+ buildFromVersion: -1,
1126
+ }),
1127
+ state: "invalid",
1128
+ },
1129
+ },
1130
+ });
588
1131
  await store.removeFtsColumn("table-postings", "col-v");
589
1132
  candidates = await store.readFtsCandidates("table-postings", "col-v", [{ term: "alpha", prefix: false }], 4);
590
1133
  checkEqual(candidates.rowIdsByTerm, [[]], "removing an index must remove its candidates");
@@ -655,15 +1198,16 @@ export function blockStoreConformanceCases() {
655
1198
  },
656
1199
  },
657
1200
  {
658
- name: "optional atomic methods, when present, equal the sequential calls",
1201
+ name: "atomic staging and rollback keep journals and artifacts inseparable",
659
1202
  async run(target) {
660
- const store = await target.create();
1203
+ let store = await target.create();
661
1204
  await store.addTable(table("t"));
662
- const begin = store.beginTransaction?.bind(store);
663
- if (begin !== undefined) {
664
- const begun = await begin({
1205
+ {
1206
+ const begun = await store.beginTransaction({
665
1207
  record: {
666
1208
  id: "txn-b",
1209
+ ownerId: "txn-b/owner",
1210
+ expiresAt: LATER,
667
1211
  pendingBlockIds: [],
668
1212
  pendingSegmentIds: [],
669
1213
  status: "active",
@@ -677,36 +1221,97 @@ export function blockStoreConformanceCases() {
677
1221
  check(begun.record.snapshotVersion === (await store.getCurrentManifestVersion()), "beginTransaction must pin the current version");
678
1222
  check(begun.rowIds !== undefined && begun.rowIds.endExclusive - begun.rowIds.start === 4n, "beginTransaction must honor the reservation");
679
1223
  }
680
- const stage = store.stageTransactionArtifacts?.bind(store);
681
- if (stage !== undefined) {
682
- await store.createTransaction(transaction("txn-s", await store.getCurrentManifestVersion()));
683
- const staged = await stage({
684
- transactionId: "txn-s",
685
- expectedRevision: 0,
686
- blocks: [{ id: "block-staged", bytes: Uint8Array.of(7) }],
687
- segments: [segment("segment-staged", "table-t", "txn-s", "block-staged")],
688
- updatedAt: LATER,
689
- });
690
- checkEqual(staged.pendingBlockIds, ["block-staged"], "staging must journal the block");
691
- check((await store.getBlock("block-staged")) !== undefined, "staging must write the bytes");
692
- check((await store.getSegment("segment-staged")) !== undefined, "staging must write the segment");
693
- }
694
- const probe = store.getCatalogProbe?.bind(store);
695
- if (probe !== undefined) {
696
- const before = await probe();
697
- await store.addTable(table("probe-check"));
698
- const after = await probe();
699
- check(after.catalogEpoch > before.catalogEpoch, "a catalog mutation must advance the probe's epoch");
700
- }
701
- const catalogState = store.getQueryCatalogState?.bind(store);
702
- if (catalogState !== undefined) {
703
- const state = await catalogState(["t", "missing-table"]);
704
- check(state.tables.length === 2 && state.tables[1] === undefined, "getQueryCatalogState must be positional with undefined for missing tables");
705
- checkEqual(state.tables[0]?.id, (await store.getTableByName("t"))?.id, "getQueryCatalogState must return the same records the individual reads do");
706
- checkEqual(state.segments, [], "getQueryCatalogState must omit staged segments outside the current manifest");
707
- checkEqual(state.transactions, [], "getQueryCatalogState must omit transactions used only by non-current segments");
708
- checkEqual(state.manifestBlockIds, [], "getQueryCatalogState must return the exact current manifest block set");
1224
+ const stage = store.stageTransactionArtifacts.bind(store);
1225
+ await store.createTransaction(transaction("txn-s", await store.getCurrentManifestVersion()));
1226
+ const duplicate = segment("segment-duplicate", "table-t", "txn-s", "block-none");
1227
+ await checkThrows(() => stage({
1228
+ transactionId: "txn-s",
1229
+ expectedRevision: 0,
1230
+ blocks: [],
1231
+ segments: [duplicate, duplicate],
1232
+ updatedAt: LATER,
1233
+ }), Error, "staging duplicate segment IDs");
1234
+ await checkThrows(() => stage({
1235
+ transactionId: "txn-s",
1236
+ expectedRevision: 0,
1237
+ blocks: [],
1238
+ segments: [segment("segment-foreign", "table-t", "another-txn", "block-none")],
1239
+ updatedAt: LATER,
1240
+ }), Error, "staging a segment owned by another transaction");
1241
+ check((await store.getSegment("segment-duplicate")) === undefined &&
1242
+ (await store.getSegment("segment-foreign")) === undefined &&
1243
+ (await store.getTransaction("txn-s"))?.revision === 0, "refused staging must leave the transaction and segment store unchanged");
1244
+ const first = await stage({
1245
+ transactionId: "txn-s",
1246
+ expectedRevision: 0,
1247
+ blocks: [{ id: "block-z", bytes: Uint8Array.of(7) }],
1248
+ segments: [segment("segment-z", "table-t", "txn-s", "block-z")],
1249
+ updatedAt: LATER,
1250
+ });
1251
+ await checkThrows(() => stage({
1252
+ transactionId: "txn-s",
1253
+ expectedRevision: first.revision,
1254
+ blocks: [{ id: "block-gap", bytes: Uint8Array.of(9) }],
1255
+ segments: [segment("segment-gap", "table-t", "txn-s", "block-gap", 0)],
1256
+ updatedAt: LATER,
1257
+ }), TypeError, "staging a segment whose commit ordinal does not continue the journal");
1258
+ check((await store.getTransaction("txn-s"))?.revision === first.revision &&
1259
+ (await store.getBlock("block-gap")) === undefined &&
1260
+ (await store.getSegment("segment-gap")) === undefined, "an ordinal refusal must leave the record, block, and segment unchanged");
1261
+ const staged = await stage({
1262
+ transactionId: "txn-s",
1263
+ expectedRevision: first.revision,
1264
+ blocks: [{ id: "block-a", bytes: Uint8Array.of(8) }],
1265
+ segments: [segment("segment-a", "table-t", "txn-s", "block-a", 1)],
1266
+ updatedAt: LATER,
1267
+ });
1268
+ checkEqual(staged.pendingBlockIds, ["block-z", "block-a"], "staging journals blocks");
1269
+ checkEqual(staged.pendingSegmentIds, ["segment-z", "segment-a"], "staging preserves the canonical segment journal order");
1270
+ check((await store.getBlock("block-a")) !== undefined &&
1271
+ (await store.getSegment("segment-a")) !== undefined, "staging must write bytes and segments");
1272
+ const rollbackInput = {
1273
+ transactionId: "txn-s",
1274
+ expectedRevision: staged.revision,
1275
+ pendingBlockIds: ["block-z"],
1276
+ pendingSegmentIds: ["segment-z"],
1277
+ removeBlockIds: ["block-a"],
1278
+ removeSegmentIds: ["segment-a"],
1279
+ updatedAt: LATER,
1280
+ };
1281
+ await checkThrows(() => store.rollbackTransactionArtifacts({
1282
+ ...rollbackInput,
1283
+ expectedRevision: staged.revision + 1,
1284
+ }), TransactionRecordConflictError, "rollback with a stale transaction revision");
1285
+ await checkThrows(() => store.rollbackTransactionArtifacts({
1286
+ ...rollbackInput,
1287
+ removeBlockIds: [],
1288
+ }), TypeError, "rollback whose lists do not exactly partition the journal");
1289
+ await checkThrows(() => store.rollbackTransactionArtifacts({
1290
+ ...rollbackInput,
1291
+ pendingBlockIds: ["block-a"],
1292
+ pendingSegmentIds: ["segment-a"],
1293
+ removeBlockIds: ["block-z"],
1294
+ removeSegmentIds: ["segment-z"],
1295
+ }), TypeError, "rollback that retains a non-prefix journal partition");
1296
+ check((await store.getBlock("block-a")) !== undefined &&
1297
+ (await store.getSegment("segment-a")) !== undefined, "a refused rollback must leave every artifact intact");
1298
+ const rolledBack = await store.rollbackTransactionArtifacts(rollbackInput);
1299
+ checkEqual(rolledBack.pendingBlockIds, ["block-z"], "rollback must retain checkpoint block");
1300
+ check((await store.getBlock("block-a")) === undefined &&
1301
+ (await store.getSegment("segment-a")) === undefined &&
1302
+ (await store.getBlock("block-z")) !== undefined &&
1303
+ (await store.getSegment("segment-z")) !== undefined, "rollback must delete only its exact removed partition");
1304
+ if (target.reopen !== undefined) {
1305
+ const reopened = await target.reopen(store);
1306
+ checkEqual((await reopened.getTransaction("txn-s"))?.pendingBlockIds, ["block-z"], "rollback journal must survive reopen");
1307
+ check((await reopened.getBlock("block-a")) === undefined &&
1308
+ (await reopened.getBlock("block-z")) !== undefined, "rollback artifact removal must survive reopen");
1309
+ store = reopened;
709
1310
  }
1311
+ const before = await store.getCatalogProbe();
1312
+ await store.addTable(table("probe-check"));
1313
+ const after = await store.getCatalogProbe();
1314
+ check(after.catalogEpoch > before.catalogEpoch, "a catalog mutation must advance the probe's epoch");
710
1315
  store.close();
711
1316
  },
712
1317
  },
@@ -720,10 +1325,11 @@ export function blockStoreConformanceCases() {
720
1325
  return;
721
1326
  }
722
1327
  await store.addTable(table("t", true));
1328
+ const schemaEpoch = (await store.getCatalogProbe()).schemaEpoch;
723
1329
  const fresh = (id) => {
724
1330
  const { snapshotVersion: _pinned, ...record } = transaction(id, null);
725
1331
  void _pinned;
726
- return record;
1332
+ return { ...record, schemaEpochGuard: schemaEpoch };
727
1333
  };
728
1334
  // A fresh record: begun, staged, and committed in the one step.
729
1335
  const first = await write({
@@ -731,6 +1337,7 @@ export function blockStoreConformanceCases() {
731
1337
  blocks: [{ id: "block-w1", bytes: Uint8Array.of(1) }],
732
1338
  segments: [segment("segment-w1", "table-t", "txn-w1", "block-w1")],
733
1339
  expectedManifestVersion: null,
1340
+ levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
734
1341
  uniqueKeyChanges: [{ tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true }],
735
1342
  committedAt: LATER,
736
1343
  });
@@ -750,6 +1357,7 @@ export function blockStoreConformanceCases() {
750
1357
  blocks: [{ id: "block-stale", bytes: Uint8Array.of(2) }],
751
1358
  segments: [segment("segment-stale", "table-t", "txn-stale", "block-stale")],
752
1359
  expectedManifestVersion: null,
1360
+ levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
753
1361
  committedAt: LATER,
754
1362
  }), WriteConflictError, "a single-shot write against a moved manifest");
755
1363
  check((await store.getTransaction("txn-stale")) === undefined &&
@@ -761,6 +1369,7 @@ export function blockStoreConformanceCases() {
761
1369
  blocks: [{ id: "block-dup", bytes: Uint8Array.of(3) }],
762
1370
  segments: [segment("segment-dup", "table-t", "txn-dup", "block-dup")],
763
1371
  expectedManifestVersion: 0,
1372
+ levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
764
1373
  uniqueKeyChanges: [
765
1374
  { tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true },
766
1375
  ],
@@ -776,6 +1385,7 @@ export function blockStoreConformanceCases() {
776
1385
  blocks: [{ id: "block-w2", bytes: Uint8Array.of(4) }],
777
1386
  segments: [segment("segment-w2", "table-t", "txn-w2", "block-w2")],
778
1387
  expectedManifestVersion: 0,
1388
+ levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
779
1389
  committedAt: LATER,
780
1390
  }), TransactionRecordConflictError, "a single-shot write with a stale transaction revision");
781
1391
  check((await store.getBlock("block-w2")) === undefined, "a revision conflict must leave no block");
@@ -784,18 +1394,114 @@ export function blockStoreConformanceCases() {
784
1394
  blocks: [{ id: "block-w2", bytes: Uint8Array.of(4) }],
785
1395
  segments: [segment("segment-w2", "table-t", "txn-w2", "block-w2")],
786
1396
  expectedManifestVersion: 0,
787
- removedBlockIds: ["block-w1"],
1397
+ levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
788
1398
  changedTableIds: ["table-t"],
789
1399
  committedAt: LATER,
790
1400
  });
791
1401
  checkEqual(second.version, 1, "the continued write must publish the next version");
792
1402
  checkEqual(second.changedTableIds, ["table-t"], "the summary must carry the change set");
793
- const manifest = await store.getManifest(1);
794
- checkEqual(manifest?.blockIds, ["block-w2"], "the continued write must add its blocks and apply supersessions");
1403
+ checkEqual(await manifestBlockIds(store, 1), ["block-w1", "block-w2"], "the continued write must preserve visible blocks and add its own");
795
1404
  check((await store.getTransaction("txn-w2"))?.status === "committed", "the begun record must end committed");
796
1405
  store.close();
797
1406
  },
798
1407
  },
1408
+ {
1409
+ name: "compaction commit proves exact output provenance before publication",
1410
+ async run(target) {
1411
+ const store = await target.create();
1412
+ await store.addTable({
1413
+ id: "table-compact",
1414
+ name: "compact",
1415
+ columns: [{ id: "col-value", name: "value", type: "number", nullable: false }],
1416
+ managed: false,
1417
+ revision: 0,
1418
+ createdAt: T0,
1419
+ });
1420
+ await store.createTransaction(transaction("txn-compact-source", null));
1421
+ const source = await store.stageTransactionArtifacts({
1422
+ transactionId: "txn-compact-source",
1423
+ expectedRevision: 0,
1424
+ blocks: [{ id: "compact-source-block", bytes: Uint8Array.of(1) }],
1425
+ segments: [
1426
+ {
1427
+ ...segment("compact-source-segment", "table-compact", "txn-compact-source", "compact-source-block"),
1428
+ columnBlockIds: { "col-value": ["compact-source-block"] },
1429
+ },
1430
+ ],
1431
+ updatedAt: T0,
1432
+ });
1433
+ await store.commitTransaction({
1434
+ transactionId: source.id,
1435
+ expectedTransactionRevision: source.revision,
1436
+ expectedManifestVersion: null,
1437
+ levelZeroSegmentLimits: [{ tableId: "table-compact", limit: MAX_LEVEL_ZERO_SEGMENTS }],
1438
+ committedAt: T0,
1439
+ });
1440
+ const jobId = "compact-proof-job";
1441
+ const outputBlockId = `${jobId}/output/segment/000000/column/000000/part/000000`;
1442
+ await store.createTransaction(transaction("txn-compact-output", 0));
1443
+ const output = await store.stageTransactionArtifacts({
1444
+ transactionId: "txn-compact-output",
1445
+ expectedRevision: 0,
1446
+ blocks: [{ id: outputBlockId, bytes: Uint8Array.of(2) }],
1447
+ segments: [
1448
+ {
1449
+ ...segment("compact-output-segment", "table-compact", "txn-compact-output", outputBlockId),
1450
+ columnBlockIds: { "col-value": [outputBlockId] },
1451
+ // Structurally valid, but copy-v1 is required to publish an insert segment.
1452
+ kind: "base",
1453
+ level: 1,
1454
+ },
1455
+ ],
1456
+ updatedAt: LATER,
1457
+ });
1458
+ const job = {
1459
+ id: jobId,
1460
+ tableId: "table-compact",
1461
+ sourceManifestVersion: 0,
1462
+ sourceSegmentIds: ["compact-source-segment"],
1463
+ sourceBlockIds: ["compact-source-block"],
1464
+ outputBlockIds: [outputBlockId],
1465
+ cursor: { sourceSegmentIndex: 1, sourceBlockIndex: 0 },
1466
+ processedRows: 2,
1467
+ sourceStoredBytes: 1,
1468
+ outputStoredBytes: 1,
1469
+ logicalBytes: 1,
1470
+ rewritePlan: { kind: "copy-v1" },
1471
+ outputCursor: null,
1472
+ memoryBudgetBytes: 0,
1473
+ minimumMemoryBytes: 0,
1474
+ level0SourceStoredBytes: 1,
1475
+ anchorSourceStoredBytes: 0,
1476
+ peakWorkingBytes: 0,
1477
+ outputLogicalBytes: 1,
1478
+ targetLevel: 1,
1479
+ state: "ready",
1480
+ transactionId: output.id,
1481
+ outputSegmentId: "compact-output-segment",
1482
+ publishedVersion: null,
1483
+ revision: 0,
1484
+ createdAt: T0,
1485
+ updatedAt: LATER,
1486
+ };
1487
+ await store.createCompactionJob(job);
1488
+ await checkThrows(() => store.commitTransaction({
1489
+ transactionId: output.id,
1490
+ expectedTransactionRevision: output.revision,
1491
+ expectedManifestVersion: 0,
1492
+ compactionJobId: job.id,
1493
+ removedBlockIds: ["compact-source-block"],
1494
+ committedAt: LATER,
1495
+ }), Error, "a structurally valid compaction segment that differs from the immutable plan");
1496
+ checkEqual(await store.getCurrentManifestVersion(), 0, "a provenance refusal must not publish a manifest");
1497
+ check((await store.getTransaction(output.id))?.status === "active" &&
1498
+ (await store.getSegment("compact-output-segment"))?.kind === "base" &&
1499
+ (await store.getBlock(outputBlockId)) !== undefined &&
1500
+ (await store.getCompactionJob(job.id))?.state === "ready", "a provenance refusal must leave the transaction, artifacts, and job unchanged");
1501
+ checkEqual(await manifestBlockIds(store, 0), ["compact-source-block"], "a provenance refusal must retain every source block");
1502
+ store.close();
1503
+ },
1504
+ },
799
1505
  {
800
1506
  name: "the lease move, when present, re-pins in place and refuses typed",
801
1507
  async run(target) {
@@ -813,26 +1519,47 @@ export function blockStoreConformanceCases() {
813
1519
  kind: "reader",
814
1520
  manifestVersion: first.version,
815
1521
  ownerId: "owner-m",
1522
+ createdAt: T0,
816
1523
  expiresAt: LATER,
817
1524
  revision: 0,
818
1525
  });
819
- await checkThrows(() => move("lease-m", 9, second.version, FAR_FUTURE), LeaseConflictError, "moving with a stale revision");
820
- await checkThrows(() => move("lease-m", 0, 999, FAR_FUTURE), SnapshotManifestMissingError, "moving to a version with no manifest");
1526
+ await checkThrows(() => move({
1527
+ id: "lease-m",
1528
+ expectedRevision: 9,
1529
+ manifestVersion: second.version,
1530
+ expiresAtCutoff: T0,
1531
+ expiresAt: LATER,
1532
+ }), LeaseConflictError, "moving with a stale revision");
1533
+ await checkThrows(() => move({
1534
+ id: "lease-m",
1535
+ expectedRevision: 0,
1536
+ manifestVersion: 999,
1537
+ expiresAtCutoff: T0,
1538
+ expiresAt: LATER,
1539
+ }), SnapshotManifestMissingError, "moving to a version with no manifest");
821
1540
  checkEqual(await store.getLease("lease-m"), {
822
1541
  id: "lease-m",
823
1542
  kind: "reader",
824
1543
  manifestVersion: first.version,
825
1544
  ownerId: "owner-m",
1545
+ createdAt: T0,
826
1546
  expiresAt: LATER,
827
1547
  revision: 0,
828
1548
  }, "a refused move must leave the lease exactly as it was");
829
- const moved = await move("lease-m", 0, second.version, FAR_FUTURE);
1549
+ const moved = await move({
1550
+ id: "lease-m",
1551
+ expectedRevision: 0,
1552
+ manifestVersion: second.version,
1553
+ expiresAtCutoff: T0,
1554
+ expiresAt: LATER,
1555
+ });
830
1556
  checkEqual(moved, {
831
1557
  id: "lease-m",
832
1558
  kind: "reader",
833
1559
  manifestVersion: second.version,
834
1560
  ownerId: "owner-m",
835
- expiresAt: FAR_FUTURE,
1561
+ createdAt: T0,
1562
+ expiresAt: LATER,
836
1563
  revision: 1,
837
1564
  }, "a move must re-pin, renew, and advance the revision");
838
1565
  checkEqual((await store.listLeases()).map((lease) => lease.id), ["lease-m"], "a move must keep the one record, not create a second");
@@ -854,10 +1581,16 @@ export function blockStoreConformanceCases() {
854
1581
  const { snapshotVersion: _pinned, ...record } = transaction("txn-single", null);
855
1582
  void _pinned;
856
1583
  const summary = await write({
857
- transaction: { record },
1584
+ transaction: {
1585
+ record: {
1586
+ ...record,
1587
+ schemaEpochGuard: (await store.getCatalogProbe()).schemaEpoch,
1588
+ },
1589
+ },
858
1590
  blocks: [{ id: "block-single", bytes: Uint8Array.of(1, 2, 3, 4) }],
859
1591
  segments: [segment("segment-single", "table-t", "txn-single", "block-single")],
860
1592
  expectedManifestVersion: version,
1593
+ levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
861
1594
  uniqueKeyChanges: [
862
1595
  { tableId: "table-t", keyTokens: ["number:1"], requireAbsent: false },
863
1596
  ],
@@ -873,10 +1606,17 @@ export function blockStoreConformanceCases() {
873
1606
  kind: "reader",
874
1607
  manifestVersion: null,
875
1608
  ownerId: "owner",
876
- expiresAt: FAR_FUTURE,
1609
+ createdAt: T0,
1610
+ expiresAt: LATER,
877
1611
  revision: 0,
878
1612
  });
879
- await move("lease-durable", 0, version, FAR_FUTURE);
1613
+ await move({
1614
+ id: "lease-durable",
1615
+ expectedRevision: 0,
1616
+ manifestVersion: version,
1617
+ expiresAtCutoff: T0,
1618
+ expiresAt: LATER,
1619
+ });
880
1620
  }
881
1621
  store = await target.reopen(store);
882
1622
  checkEqual(await store.getCurrentManifestVersion(), version, "the committed version must survive a reopen");
@@ -893,4 +1633,3 @@ export function blockStoreConformanceCases() {
893
1633
  },
894
1634
  ];
895
1635
  }
896
- //# sourceMappingURL=block-store-conformance.js.map