@minnowdb/core 0.2.1 → 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 (270) hide show
  1. package/README.md +23 -32
  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 +2 -1
  21. package/dist/engine/artifact-cache.js +5 -1
  22. package/dist/engine/batch.d.ts +11 -3
  23. package/dist/engine/batch.js +17 -7
  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 +24 -5
  29. package/dist/engine/catalog.js +34 -7
  30. package/dist/engine/client.d.ts +29 -7
  31. package/dist/engine/client.js +216 -40
  32. package/dist/engine/database.d.ts +224 -39
  33. package/dist/engine/database.js +7475 -1539
  34. package/dist/engine/defaults.d.ts +4 -8
  35. package/dist/engine/defaults.js +46 -22
  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 +41 -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 -10
  43. package/dist/engine/index.js +12 -9
  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 +504 -126
  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 +262 -27
  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 +1 -2
  59. package/dist/engine/query-cache.js +7 -5
  60. package/dist/engine/query.d.ts +134 -29
  61. package/dist/engine/query.js +1668 -372
  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 +6 -3
  65. package/dist/engine/schema-wire.js +40 -34
  66. package/dist/engine/schema.d.ts +156 -80
  67. package/dist/engine/schema.js +570 -108
  68. package/dist/engine/sort-keys.d.ts +4 -4
  69. package/dist/engine/sort-keys.js +44 -29
  70. package/dist/engine/sql-domains.d.ts +31 -0
  71. package/dist/engine/sql-domains.js +585 -0
  72. package/dist/engine/sql-driver.d.ts +19 -0
  73. package/dist/engine/sql-driver.js +1 -0
  74. package/dist/engine/sql-json.d.ts +7 -9
  75. package/dist/engine/sql-json.js +75 -15
  76. package/dist/engine/sql-semantics.d.ts +8 -3
  77. package/dist/engine/sql-semantics.js +458 -30
  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 +12 -2
  81. package/dist/engine/vector.js +635 -119
  82. package/dist/engine/worker-host.d.ts +9 -2
  83. package/dist/engine/worker-host.js +620 -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 +3 -4
  91. package/dist/plan/index.js +3 -4
  92. package/dist/storage/index.d.ts +1 -1
  93. package/dist/storage/index.js +1 -1
  94. package/dist/storage/indexeddb.d.ts +99 -51
  95. package/dist/storage/indexeddb.js +13448 -2660
  96. package/dist/storage/memory.d.ts +64 -57
  97. package/dist/storage/memory.js +1042 -128
  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 +409 -75
  103. package/dist/storage/opfs/leader.js +4621 -650
  104. package/dist/storage/opfs/rpc.d.ts +15 -3
  105. package/dist/storage/opfs/rpc.js +224 -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 +33 -99
  109. package/dist/storage/opfs/store.js +453 -364
  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 -85
  115. package/dist/storage/snapshot.js +33 -265
  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 +4 -5
  119. package/dist/storage/toolkit/index.js +28 -4
  120. package/dist/storage/toolkit/record-core.d.ts +182 -55
  121. package/dist/storage/toolkit/record-core.js +6158 -1331
  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 +15 -2
  127. package/dist/storage/toolkit/wire.js +199 -10
  128. package/dist/storage/types.d.ts +1439 -204
  129. package/dist/storage/types.js +1745 -141
  130. package/dist/testing/block-store-conformance.d.ts +0 -1
  131. package/dist/testing/block-store-conformance.js +1009 -119
  132. package/dist/testing/index.d.ts +46 -26
  133. package/dist/testing/index.js +112 -57
  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 +97 -0
  137. package/dist/testing/simulator.js +591 -0
  138. package/dist/testing/sqllogictest.d.ts +89 -0
  139. package/dist/testing/sqllogictest.js +434 -0
  140. package/dist/transactions/index.d.ts +92 -16
  141. package/dist/transactions/index.js +1074 -236
  142. package/dist/worker-protocol/index.d.ts +3 -2
  143. package/dist/worker-protocol/index.js +2 -5
  144. package/package.json +53 -3
  145. package/postgres-feature-profile.json +223 -0
  146. package/sql-feature-matrix.json +172 -252
  147. package/dist/block-format/block.d.ts.map +0 -1
  148. package/dist/block-format/block.js.map +0 -1
  149. package/dist/block-format/checksum.d.ts.map +0 -1
  150. package/dist/block-format/checksum.js.map +0 -1
  151. package/dist/block-format/codecs.d.ts.map +0 -1
  152. package/dist/block-format/codecs.js.map +0 -1
  153. package/dist/block-format/column.d.ts.map +0 -1
  154. package/dist/block-format/column.js.map +0 -1
  155. package/dist/block-format/index.d.ts.map +0 -1
  156. package/dist/block-format/index.js.map +0 -1
  157. package/dist/block-format/physical.d.ts.map +0 -1
  158. package/dist/block-format/physical.js.map +0 -1
  159. package/dist/block-format/types.d.ts.map +0 -1
  160. package/dist/block-format/types.js.map +0 -1
  161. package/dist/engine/artifact-cache.d.ts.map +0 -1
  162. package/dist/engine/artifact-cache.js.map +0 -1
  163. package/dist/engine/batch.d.ts.map +0 -1
  164. package/dist/engine/batch.js.map +0 -1
  165. package/dist/engine/buffered-writer.d.ts.map +0 -1
  166. package/dist/engine/buffered-writer.js.map +0 -1
  167. package/dist/engine/catalog.d.ts.map +0 -1
  168. package/dist/engine/catalog.js.map +0 -1
  169. package/dist/engine/client.d.ts.map +0 -1
  170. package/dist/engine/client.js.map +0 -1
  171. package/dist/engine/coordinator.d.ts +0 -17
  172. package/dist/engine/coordinator.d.ts.map +0 -1
  173. package/dist/engine/coordinator.js +0 -60
  174. package/dist/engine/coordinator.js.map +0 -1
  175. package/dist/engine/database.d.ts.map +0 -1
  176. package/dist/engine/database.js.map +0 -1
  177. package/dist/engine/defaults.d.ts.map +0 -1
  178. package/dist/engine/defaults.js.map +0 -1
  179. package/dist/engine/errors.d.ts.map +0 -1
  180. package/dist/engine/errors.js.map +0 -1
  181. package/dist/engine/fts.d.ts.map +0 -1
  182. package/dist/engine/fts.js.map +0 -1
  183. package/dist/engine/group-index.d.ts.map +0 -1
  184. package/dist/engine/group-index.js.map +0 -1
  185. package/dist/engine/index.d.ts.map +0 -1
  186. package/dist/engine/index.js.map +0 -1
  187. package/dist/engine/join-index.d.ts.map +0 -1
  188. package/dist/engine/join-index.js.map +0 -1
  189. package/dist/engine/live.d.ts.map +0 -1
  190. package/dist/engine/live.js.map +0 -1
  191. package/dist/engine/memory.d.ts.map +0 -1
  192. package/dist/engine/memory.js.map +0 -1
  193. package/dist/engine/optimizer.d.ts.map +0 -1
  194. package/dist/engine/optimizer.js.map +0 -1
  195. package/dist/engine/query-cache.d.ts.map +0 -1
  196. package/dist/engine/query-cache.js.map +0 -1
  197. package/dist/engine/query.d.ts.map +0 -1
  198. package/dist/engine/query.js.map +0 -1
  199. package/dist/engine/result-wire.d.ts.map +0 -1
  200. package/dist/engine/result-wire.js.map +0 -1
  201. package/dist/engine/schema-wire.d.ts.map +0 -1
  202. package/dist/engine/schema-wire.js.map +0 -1
  203. package/dist/engine/schema.d.ts.map +0 -1
  204. package/dist/engine/schema.js.map +0 -1
  205. package/dist/engine/sort-keys.d.ts.map +0 -1
  206. package/dist/engine/sort-keys.js.map +0 -1
  207. package/dist/engine/sql-json.d.ts.map +0 -1
  208. package/dist/engine/sql-json.js.map +0 -1
  209. package/dist/engine/sql-semantics.d.ts.map +0 -1
  210. package/dist/engine/sql-semantics.js.map +0 -1
  211. package/dist/engine/vector.d.ts.map +0 -1
  212. package/dist/engine/vector.js.map +0 -1
  213. package/dist/engine/worker-host.d.ts.map +0 -1
  214. package/dist/engine/worker-host.js.map +0 -1
  215. package/dist/engine/worker.d.ts.map +0 -1
  216. package/dist/engine/worker.js.map +0 -1
  217. package/dist/index.d.ts.map +0 -1
  218. package/dist/index.js.map +0 -1
  219. package/dist/plan/index.d.ts.map +0 -1
  220. package/dist/plan/index.js.map +0 -1
  221. package/dist/storage/fixture-shape.d.ts +0 -42
  222. package/dist/storage/fixture-shape.d.ts.map +0 -1
  223. package/dist/storage/fixture-shape.js +0 -146
  224. package/dist/storage/fixture-shape.js.map +0 -1
  225. package/dist/storage/index.d.ts.map +0 -1
  226. package/dist/storage/index.js.map +0 -1
  227. package/dist/storage/indexeddb.d.ts.map +0 -1
  228. package/dist/storage/indexeddb.js.map +0 -1
  229. package/dist/storage/memory.d.ts.map +0 -1
  230. package/dist/storage/memory.js.map +0 -1
  231. package/dist/storage/opfs/files.d.ts.map +0 -1
  232. package/dist/storage/opfs/files.js.map +0 -1
  233. package/dist/storage/opfs/index.d.ts.map +0 -1
  234. package/dist/storage/opfs/index.js.map +0 -1
  235. package/dist/storage/opfs/leader.d.ts.map +0 -1
  236. package/dist/storage/opfs/leader.js.map +0 -1
  237. package/dist/storage/opfs/rpc.d.ts.map +0 -1
  238. package/dist/storage/opfs/rpc.js.map +0 -1
  239. package/dist/storage/opfs/store.d.ts.map +0 -1
  240. package/dist/storage/opfs/store.js.map +0 -1
  241. package/dist/storage/snapshot.d.ts.map +0 -1
  242. package/dist/storage/snapshot.js.map +0 -1
  243. package/dist/storage/toolkit/extents.d.ts.map +0 -1
  244. package/dist/storage/toolkit/extents.js.map +0 -1
  245. package/dist/storage/toolkit/index.d.ts.map +0 -1
  246. package/dist/storage/toolkit/index.js.map +0 -1
  247. package/dist/storage/toolkit/record-core.d.ts.map +0 -1
  248. package/dist/storage/toolkit/record-core.js.map +0 -1
  249. package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
  250. package/dist/storage/toolkit/sync-file.js.map +0 -1
  251. package/dist/storage/toolkit/wal.d.ts.map +0 -1
  252. package/dist/storage/toolkit/wal.js.map +0 -1
  253. package/dist/storage/toolkit/wire.d.ts.map +0 -1
  254. package/dist/storage/toolkit/wire.js.map +0 -1
  255. package/dist/storage/types.d.ts.map +0 -1
  256. package/dist/storage/types.js.map +0 -1
  257. package/dist/testing/block-store-conformance.d.ts.map +0 -1
  258. package/dist/testing/block-store-conformance.js.map +0 -1
  259. package/dist/testing/index.d.ts.map +0 -1
  260. package/dist/testing/index.js.map +0 -1
  261. package/dist/testing/opfs-shim.d.ts.map +0 -1
  262. package/dist/testing/opfs-shim.js.map +0 -1
  263. package/dist/testing/seeds.d.ts +0 -11
  264. package/dist/testing/seeds.d.ts.map +0 -1
  265. package/dist/testing/seeds.js +0 -50
  266. package/dist/testing/seeds.js.map +0 -1
  267. package/dist/transactions/index.d.ts.map +0 -1
  268. package/dist/transactions/index.js.map +0 -1
  269. package/dist/worker-protocol/index.d.ts.map +0 -1
  270. package/dist/worker-protocol/index.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { CompactionJobConflictError, LeaseConflictError, SnapshotManifestMissingError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, 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
  },
@@ -216,6 +379,38 @@ export function blockStoreConformanceCases() {
216
379
  store.close();
217
380
  },
218
381
  },
382
+ {
383
+ name: "secondary-index metadata must describe one direction per indexed column",
384
+ async run(target) {
385
+ const store = await target.create();
386
+ const record = table("bad-secondary");
387
+ try {
388
+ await store.addTable({
389
+ ...record,
390
+ secondaryIndexes: {
391
+ bad: {
392
+ name: "bad_direction_arity_index",
393
+ columnId: "col-v",
394
+ columnIds: ["col-v"],
395
+ directions: ["asc", "desc"],
396
+ termEncoding: "tuple-v1",
397
+ storage: "postings-v1",
398
+ storageColumnId: "secondary-index:bad",
399
+ locator: "row-id",
400
+ state: "ready",
401
+ buildFromVersion: 0,
402
+ },
403
+ },
404
+ });
405
+ throw new Error("secondary-index metadata with mismatched direction arity was accepted");
406
+ }
407
+ catch (error) {
408
+ check(error instanceof TypeError, "invalid secondary-index metadata must be rejected");
409
+ }
410
+ checkEqual(await store.listTables(), [], "a rejected catalog record must leave no table");
411
+ store.close();
412
+ },
413
+ },
219
414
  {
220
415
  name: "removing a table removes everything keyed to it",
221
416
  async run(target) {
@@ -226,19 +421,144 @@ export function blockStoreConformanceCases() {
226
421
  tableId: "table-t",
227
422
  });
228
423
  await store.reserveRowIds("table-t", 10);
229
- await store.removeTable("table-t", 0);
230
- 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");
231
434
  checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1"]), [], "unique keys must go with the table");
435
+ await store.addTable(table("t", true));
232
436
  const range = await store.reserveRowIds("table-t", 1);
233
437
  check(range.start === 1n, "row-id counters must reset with the table");
234
438
  check((await store.getBlock(blockId)) !== undefined, "blocks are not the catalog's to delete");
235
439
  store.close();
236
440
  },
237
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
+ },
238
549
  {
239
550
  name: "counter reservations never overlap, even issued concurrently",
240
551
  async run(target) {
241
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);
242
562
  const ranges = await Promise.all(Array.from({ length: 8 }, () => store.reserveRowIds("table-t", 5)));
243
563
  const starts = new Set(ranges.map((range) => range.start));
244
564
  check(starts.size === 8, "concurrent reservations returned overlapping ranges");
@@ -247,6 +567,96 @@ export function blockStoreConformanceCases() {
247
567
  }
248
568
  const bumped = await store.reserveAutoIncrement("table-t", "col-id", 3, 100n);
249
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
+ }
250
660
  store.close();
251
661
  },
252
662
  },
@@ -260,7 +670,8 @@ export function blockStoreConformanceCases() {
260
670
  });
261
671
  checkEqual(await store.getCurrentManifestVersion(), version, "the version must advance");
262
672
  const manifest = await store.getCurrentManifest();
263
- 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");
264
675
  const record = await store.getTransaction("txn-one");
265
676
  check(record?.status === "committed" && record.committedVersion === version, "the transaction record must flip to committed with the published version");
266
677
  checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1", "number:2"]), ["number:1"], "committed unique keys must be queryable");
@@ -302,9 +713,11 @@ export function blockStoreConformanceCases() {
302
713
  await store.addTable(table("t", true));
303
714
  const { version } = await commitOne(store, "one", { keyTokens: ["number:1"] });
304
715
  await store.createTransaction(transaction("txn-dup", version));
305
- await store.addBlock("block-dup", Uint8Array.of(9));
306
- await store.updateTransaction("txn-dup", 0, {
307
- 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: [],
308
721
  updatedAt: T0,
309
722
  });
310
723
  await checkThrows(() => store.commitTransaction({
@@ -320,32 +733,6 @@ export function blockStoreConformanceCases() {
320
733
  store.close();
321
734
  },
322
735
  },
323
- {
324
- name: "publishManifest compare-and-swaps and refuses missing blocks",
325
- async run(target) {
326
- const store = await target.create();
327
- await store.addBlock("block-a", Uint8Array.of(1));
328
- await checkThrows(() => store.publishManifest({ expectedVersion: 3, blockIds: ["block-a"], createdAt: T0 }), WriteConflictError, "publishing against the wrong expected version");
329
- try {
330
- await store.publishManifest({
331
- expectedVersion: null,
332
- blockIds: ["missing"],
333
- createdAt: T0,
334
- });
335
- throw new Error("a manifest referencing a missing block was published");
336
- }
337
- catch (error) {
338
- check(!(error instanceof WriteConflictError), "missing blocks are not a version conflict");
339
- }
340
- const manifest = await store.publishManifest({
341
- expectedVersion: null,
342
- blockIds: ["block-a"],
343
- createdAt: T0,
344
- });
345
- check(manifest.version === 0, "the first published version must be 0");
346
- store.close();
347
- },
348
- },
349
736
  {
350
737
  name: "leases pin versions, renew by CAS, and expire honestly",
351
738
  async run(target) {
@@ -357,14 +744,47 @@ export function blockStoreConformanceCases() {
357
744
  kind: "reader",
358
745
  manifestVersion: version,
359
746
  ownerId: "owner-a",
360
- expiresAt: FAR_FUTURE,
747
+ createdAt: T0,
748
+ expiresAt: LATER,
361
749
  revision: 0,
362
750
  });
363
- await checkThrows(() => store.renewLease("lease-a", 9, FAR_FUTURE), LeaseConflictError, "renewing with a stale revision");
364
- 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
+ });
365
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");
366
766
  checkEqual(await store.removeLeaseIfExpired("lease-a", 1, PAST), false, "an unexpired lease must not be removed");
367
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");
368
788
  store.close();
369
789
  },
370
790
  },
@@ -374,31 +794,30 @@ export function blockStoreConformanceCases() {
374
794
  const store = await target.create();
375
795
  await store.addTable(table("t"));
376
796
  const first = await commitOne(store, "one");
377
- // Supersede the first commit's block so version 0 becomes collectable.
378
- const transactionId = "txn-two";
379
- await store.createTransaction(transaction(transactionId, first.version));
380
- await store.addBlock("block-two", Uint8Array.of(5));
381
- await store.addSegment(segment("segment-two", "table-t", transactionId, "block-two"));
382
- await store.updateTransaction(transactionId, 0, {
383
- pendingBlockIds: ["block-two"],
384
- pendingSegmentIds: ["segment-two"],
385
- updatedAt: T0,
386
- });
387
- await store.commitTransaction({
388
- transactionId,
389
- 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,
390
801
  expectedManifestVersion: first.version,
391
- removedBlockIds: [first.blockId],
802
+ expectedCatalogEpoch: (await store.getCatalogProbe()).catalogEpoch,
392
803
  committedAt: LATER,
393
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");
394
811
  const job = await store.createGarbageCollectionJob({
395
812
  id: "gc-1",
396
813
  candidateManifestVersions: [first.version],
397
- candidateSegmentIds: ["segment-one"],
814
+ candidateSegmentIds: [],
398
815
  candidateBlockIds: [first.blockId],
399
816
  leaseCutoff: LATER,
400
817
  createdAt: LATER,
401
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");
402
821
  const step = await store.runGarbageCollectionStep({
403
822
  jobId: job.id,
404
823
  expectedRevision: job.revision,
@@ -408,9 +827,16 @@ export function blockStoreConformanceCases() {
408
827
  checkEqual(step.prunedManifestVersions, [first.version], "the superseded version must prune");
409
828
  checkEqual(step.reclaimedBlockIds, [first.blockId], "the superseded block must reclaim");
410
829
  checkEqual(await store.getBlock(first.blockId), undefined, "reclaimed bytes must be gone");
411
- 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");
412
836
  const pruned = await store.getManifest(first.version);
413
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");
414
840
  store.close();
415
841
  },
416
842
  },
@@ -418,6 +844,12 @@ export function blockStoreConformanceCases() {
418
844
  name: "temp spill pages are isolated scratch; owner records conflict by exact class",
419
845
  async run(target) {
420
846
  const store = await target.create();
847
+ await store.createTempOwner({
848
+ ownerId: "o1",
849
+ createdAt: T0,
850
+ expiresAt: LATER,
851
+ revision: 0,
852
+ });
421
853
  await store.putTempRunPage({
422
854
  ownerId: "o1",
423
855
  runId: "r1",
@@ -434,8 +866,12 @@ export function blockStoreConformanceCases() {
434
866
  }
435
867
  await store.removeTempRun("o1", "r1");
436
868
  checkEqual(await store.getTempRunPage("o1", "r1", 0), undefined, "a removed run must be gone");
437
- await store.createTempOwner({ ownerId: "o1", expiresAt: FAR_FUTURE, revision: 0 });
438
- 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");
439
875
  checkEqual(await store.removeTempOwnerIfExpired("o1", PAST), false, "an unexpired owner must survive the sweep");
440
876
  const listed = await store.listTempOwnerIdsPage(null, 10);
441
877
  check(listed.records.includes("o1"), "owners must be listable");
@@ -460,6 +896,14 @@ export function blockStoreConformanceCases() {
460
896
  sourceStoredBytes: 0,
461
897
  outputStoredBytes: 0,
462
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,
463
907
  targetLevel: 1,
464
908
  state: "planned",
465
909
  transactionId: null,
@@ -469,17 +913,61 @@ export function blockStoreConformanceCases() {
469
913
  createdAt: T0,
470
914
  updatedAt: T0,
471
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");
472
918
  await checkThrows(() => store.updateCompactionJob("job-1", 9, {
473
919
  state: "running",
474
920
  transactionId: "txn-one",
475
921
  updatedAt: LATER,
476
922
  }), CompactionJobConflictError, "updating a compaction job with a stale revision");
477
- const updated = await store.updateCompactionJob("job-1", 0, {
478
- state: "running",
479
- 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",
480
938
  updatedAt: LATER,
481
939
  });
482
- 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");
483
971
  store.close();
484
972
  },
485
973
  },
@@ -487,6 +975,17 @@ export function blockStoreConformanceCases() {
487
975
  name: "full-text bases serve exact and prefix candidates with honest coverage",
488
976
  async run(target) {
489
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
+ });
490
989
  await store.writeFtsBase("table-t", "col-v", {
491
990
  coversVersion: 0,
492
991
  chunks: [
@@ -500,22 +999,215 @@ export function blockStoreConformanceCases() {
500
999
  });
501
1000
  const exact = await store.readFtsCandidates("table-t", "col-v", [{ term: "minnow", prefix: false }], 10);
502
1001
  checkEqual(exact.rowIdsByTerm, [[1n, 3n]], "exact terms must match exactly");
503
- check(exact.coversVersion === 0 && exact.totalTokens === 5, "coverage and token totals must report the stored base");
1002
+ check(exact.hasBase && exact.coversVersion === 0 && exact.totalTokens === 5, "coverage and token totals must report the stored base");
504
1003
  const prefix = await store.readFtsCandidates("table-t", "col-v", [{ term: "minnow", prefix: true }], 10);
505
1004
  checkEqual(prefix.rowIdsByTerm, [[1n, 2n, 3n]], "prefix terms must match the term range, row ids ascending and unique");
1005
+ const ordered = await store.readFtsPostings("table-t", "col-v", 10);
1006
+ checkEqual(ordered.postings, [
1007
+ { term: "minnow", rowIds: [1n, 3n], tf: [1, 2] },
1008
+ { term: "minnows", rowIds: [2n], tf: [1] },
1009
+ { term: "shark", rowIds: [4n], tf: [1] },
1010
+ ], "ordered postings must merge into canonical term and row-ID order");
506
1011
  store.close();
507
1012
  },
508
1013
  },
509
1014
  {
510
- name: "optional atomic methods, when present, equal the sequential calls",
1015
+ name: "postings builds publish bounded generations atomically and reclaim replacements",
1016
+ async run(target) {
1017
+ let store = await target.create();
1018
+ await store.addTable({
1019
+ ...table("postings"),
1020
+ ftsColumns: {
1021
+ "col-v": {
1022
+ storage: "fts-chunks-v1",
1023
+ tokenizerVersion: 1,
1024
+ state: "building",
1025
+ buildFromVersion: -1,
1026
+ },
1027
+ },
1028
+ });
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",
1098
+ coversVersion: 4,
1099
+ chunkCount: 2,
1100
+ totalTokens: 2,
1101
+ completedAt: "2099-01-01T00:10:03.000Z",
1102
+ });
1103
+ let candidates = await store.readFtsCandidates("table-postings", "col-v", [
1104
+ { term: "old", prefix: false },
1105
+ { lower: "alpha", lowerInclusive: true, upper: "omega", upperInclusive: true },
1106
+ ], 4);
1107
+ checkEqual(candidates.rowIdsByTerm, [[], [2n, 3n]], "only a finished replacement generation may become visible");
1108
+ check(candidates.hasBase, "a finished generation must report a published base");
1109
+ if (target.reopen !== undefined) {
1110
+ store = await target.reopen(store);
1111
+ candidates = await store.readFtsCandidates("table-postings", "col-v", [{ term: "omega", prefix: false }], 4);
1112
+ checkEqual(candidates.rowIdsByTerm, [[3n]], "a finished generation must survive reopen");
1113
+ check(candidates.hasBase, "a reopened generation must still report its base");
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
+ });
1131
+ await store.removeFtsColumn("table-postings", "col-v");
1132
+ candidates = await store.readFtsCandidates("table-postings", "col-v", [{ term: "alpha", prefix: false }], 4);
1133
+ checkEqual(candidates.rowIdsByTerm, [[]], "removing an index must remove its candidates");
1134
+ check(!candidates.hasBase, "a removed base must be distinguishable from an empty base");
1135
+ store.close();
1136
+ },
1137
+ },
1138
+ {
1139
+ name: "UNIQUE-index membership seeds and drops atomically with its catalog record",
511
1140
  async run(target) {
512
1141
  const store = await target.create();
1142
+ const record = table("unique-secondary");
1143
+ const indexId = "unique-v";
1144
+ const namespaceId = secondaryUniqueKeyNamespace(record.id, indexId);
1145
+ const building = {
1146
+ name: "unique_secondary_v",
1147
+ columnId: "col-v",
1148
+ columnIds: ["col-v"],
1149
+ directions: ["asc"],
1150
+ unique: true,
1151
+ termEncoding: "tuple-v1",
1152
+ storage: "postings-v1",
1153
+ storageColumnId: "secondary-index:unique-v",
1154
+ locator: "row-id",
1155
+ state: "building",
1156
+ buildId: "builder",
1157
+ buildFromVersion: -1,
1158
+ };
1159
+ const { buildId: _buildId, ...withoutBuilder } = building;
1160
+ void _buildId;
1161
+ const ready = {
1162
+ ...withoutBuilder,
1163
+ state: "ready",
1164
+ uniqueEnforced: true,
1165
+ };
1166
+ await store.addTable({ ...record, secondaryIndexes: { [indexId]: building } });
1167
+ try {
1168
+ await store.updateTable(record.id, 0, {
1169
+ secondaryIndexes: { [indexId]: ready },
1170
+ expectedManifestVersion: { value: null },
1171
+ uniqueKeySeed: { namespaceId: `${namespaceId}-wrong`, keyTokens: ["x"] },
1172
+ });
1173
+ throw new Error("a seed for the wrong namespace was accepted");
1174
+ }
1175
+ catch (error) {
1176
+ check(error instanceof TypeError, "a mismatched UNIQUE seed must be rejected");
1177
+ }
1178
+ check((await store.getTable(record.id))?.revision === 0, "a mismatched seed must not mutate the catalog");
1179
+ await checkThrows(() => store.updateTable(record.id, 0, {
1180
+ secondaryIndexes: {
1181
+ [indexId]: ready,
1182
+ },
1183
+ expectedManifestVersion: { value: null },
1184
+ uniqueKeySeed: { namespaceId, keyTokens: ["x", "x"] },
1185
+ }), UniqueKeyConflictError, "a duplicate UNIQUE seed");
1186
+ check((await store.getTable(record.id))?.secondaryIndexes?.[indexId]?.state === "building", "a refused seed must not publish the ready catalog state");
1187
+ await store.updateTable(record.id, 0, {
1188
+ secondaryIndexes: {
1189
+ [indexId]: ready,
1190
+ },
1191
+ expectedManifestVersion: { value: null },
1192
+ uniqueKeySeed: { namespaceId, keyTokens: ["x", "y"] },
1193
+ });
1194
+ checkEqual(await store.getExistingUniqueKeys(namespaceId, ["x", "z"]), ["x"], "the ready catalog and membership must publish together");
1195
+ await store.updateTable(record.id, 1, { secondaryIndexes: null });
1196
+ checkEqual(await store.getExistingUniqueKeys(namespaceId, ["x", "y"]), [], "dropping the index must reclaim its membership namespace");
1197
+ store.close();
1198
+ },
1199
+ },
1200
+ {
1201
+ name: "atomic staging and rollback keep journals and artifacts inseparable",
1202
+ async run(target) {
1203
+ let store = await target.create();
513
1204
  await store.addTable(table("t"));
514
- const begin = store.beginTransaction?.bind(store);
515
- if (begin !== undefined) {
516
- const begun = await begin({
1205
+ {
1206
+ const begun = await store.beginTransaction({
517
1207
  record: {
518
1208
  id: "txn-b",
1209
+ ownerId: "txn-b/owner",
1210
+ expiresAt: LATER,
519
1211
  pendingBlockIds: [],
520
1212
  pendingSegmentIds: [],
521
1213
  status: "active",
@@ -529,33 +1221,97 @@ export function blockStoreConformanceCases() {
529
1221
  check(begun.record.snapshotVersion === (await store.getCurrentManifestVersion()), "beginTransaction must pin the current version");
530
1222
  check(begun.rowIds !== undefined && begun.rowIds.endExclusive - begun.rowIds.start === 4n, "beginTransaction must honor the reservation");
531
1223
  }
532
- const stage = store.stageTransactionArtifacts?.bind(store);
533
- if (stage !== undefined) {
534
- await store.createTransaction(transaction("txn-s", await store.getCurrentManifestVersion()));
535
- const staged = await stage({
536
- transactionId: "txn-s",
537
- expectedRevision: 0,
538
- blocks: [{ id: "block-staged", bytes: Uint8Array.of(7) }],
539
- segments: [segment("segment-staged", "table-t", "txn-s", "block-staged")],
540
- updatedAt: LATER,
541
- });
542
- checkEqual(staged.pendingBlockIds, ["block-staged"], "staging must journal the block");
543
- check((await store.getBlock("block-staged")) !== undefined, "staging must write the bytes");
544
- check((await store.getSegment("segment-staged")) !== undefined, "staging must write the segment");
545
- }
546
- const probe = store.getCatalogProbe?.bind(store);
547
- if (probe !== undefined) {
548
- const before = await probe();
549
- await store.addTable(table("probe-check"));
550
- const after = await probe();
551
- check(after.catalogEpoch > before.catalogEpoch, "a catalog mutation must advance the probe's epoch");
552
- }
553
- const catalogState = store.getQueryCatalogState?.bind(store);
554
- if (catalogState !== undefined) {
555
- const state = await catalogState(["t", "missing-table"]);
556
- check(state.tables.length === 2 && state.tables[1] === undefined, "getQueryCatalogState must be positional with undefined for missing tables");
557
- checkEqual(state.tables[0]?.id, (await store.getTableByName("t"))?.id, "getQueryCatalogState must return the same records the individual reads do");
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;
558
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");
559
1315
  store.close();
560
1316
  },
561
1317
  },
@@ -569,10 +1325,11 @@ export function blockStoreConformanceCases() {
569
1325
  return;
570
1326
  }
571
1327
  await store.addTable(table("t", true));
1328
+ const schemaEpoch = (await store.getCatalogProbe()).schemaEpoch;
572
1329
  const fresh = (id) => {
573
1330
  const { snapshotVersion: _pinned, ...record } = transaction(id, null);
574
1331
  void _pinned;
575
- return record;
1332
+ return { ...record, schemaEpochGuard: schemaEpoch };
576
1333
  };
577
1334
  // A fresh record: begun, staged, and committed in the one step.
578
1335
  const first = await write({
@@ -580,6 +1337,7 @@ export function blockStoreConformanceCases() {
580
1337
  blocks: [{ id: "block-w1", bytes: Uint8Array.of(1) }],
581
1338
  segments: [segment("segment-w1", "table-t", "txn-w1", "block-w1")],
582
1339
  expectedManifestVersion: null,
1340
+ levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
583
1341
  uniqueKeyChanges: [{ tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true }],
584
1342
  committedAt: LATER,
585
1343
  });
@@ -599,6 +1357,7 @@ export function blockStoreConformanceCases() {
599
1357
  blocks: [{ id: "block-stale", bytes: Uint8Array.of(2) }],
600
1358
  segments: [segment("segment-stale", "table-t", "txn-stale", "block-stale")],
601
1359
  expectedManifestVersion: null,
1360
+ levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
602
1361
  committedAt: LATER,
603
1362
  }), WriteConflictError, "a single-shot write against a moved manifest");
604
1363
  check((await store.getTransaction("txn-stale")) === undefined &&
@@ -610,6 +1369,7 @@ export function blockStoreConformanceCases() {
610
1369
  blocks: [{ id: "block-dup", bytes: Uint8Array.of(3) }],
611
1370
  segments: [segment("segment-dup", "table-t", "txn-dup", "block-dup")],
612
1371
  expectedManifestVersion: 0,
1372
+ levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
613
1373
  uniqueKeyChanges: [
614
1374
  { tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true },
615
1375
  ],
@@ -625,6 +1385,7 @@ export function blockStoreConformanceCases() {
625
1385
  blocks: [{ id: "block-w2", bytes: Uint8Array.of(4) }],
626
1386
  segments: [segment("segment-w2", "table-t", "txn-w2", "block-w2")],
627
1387
  expectedManifestVersion: 0,
1388
+ levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
628
1389
  committedAt: LATER,
629
1390
  }), TransactionRecordConflictError, "a single-shot write with a stale transaction revision");
630
1391
  check((await store.getBlock("block-w2")) === undefined, "a revision conflict must leave no block");
@@ -633,18 +1394,114 @@ export function blockStoreConformanceCases() {
633
1394
  blocks: [{ id: "block-w2", bytes: Uint8Array.of(4) }],
634
1395
  segments: [segment("segment-w2", "table-t", "txn-w2", "block-w2")],
635
1396
  expectedManifestVersion: 0,
636
- removedBlockIds: ["block-w1"],
1397
+ levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
637
1398
  changedTableIds: ["table-t"],
638
1399
  committedAt: LATER,
639
1400
  });
640
1401
  checkEqual(second.version, 1, "the continued write must publish the next version");
641
1402
  checkEqual(second.changedTableIds, ["table-t"], "the summary must carry the change set");
642
- const manifest = await store.getManifest(1);
643
- 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");
644
1404
  check((await store.getTransaction("txn-w2"))?.status === "committed", "the begun record must end committed");
645
1405
  store.close();
646
1406
  },
647
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
+ },
648
1505
  {
649
1506
  name: "the lease move, when present, re-pins in place and refuses typed",
650
1507
  async run(target) {
@@ -662,26 +1519,47 @@ export function blockStoreConformanceCases() {
662
1519
  kind: "reader",
663
1520
  manifestVersion: first.version,
664
1521
  ownerId: "owner-m",
1522
+ createdAt: T0,
665
1523
  expiresAt: LATER,
666
1524
  revision: 0,
667
1525
  });
668
- await checkThrows(() => move("lease-m", 9, second.version, FAR_FUTURE), LeaseConflictError, "moving with a stale revision");
669
- 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");
670
1540
  checkEqual(await store.getLease("lease-m"), {
671
1541
  id: "lease-m",
672
1542
  kind: "reader",
673
1543
  manifestVersion: first.version,
674
1544
  ownerId: "owner-m",
1545
+ createdAt: T0,
675
1546
  expiresAt: LATER,
676
1547
  revision: 0,
677
1548
  }, "a refused move must leave the lease exactly as it was");
678
- 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
+ });
679
1556
  checkEqual(moved, {
680
1557
  id: "lease-m",
681
1558
  kind: "reader",
682
1559
  manifestVersion: second.version,
683
1560
  ownerId: "owner-m",
684
- expiresAt: FAR_FUTURE,
1561
+ createdAt: T0,
1562
+ expiresAt: LATER,
685
1563
  revision: 1,
686
1564
  }, "a move must re-pin, renew, and advance the revision");
687
1565
  checkEqual((await store.listLeases()).map((lease) => lease.id), ["lease-m"], "a move must keep the one record, not create a second");
@@ -703,10 +1581,16 @@ export function blockStoreConformanceCases() {
703
1581
  const { snapshotVersion: _pinned, ...record } = transaction("txn-single", null);
704
1582
  void _pinned;
705
1583
  const summary = await write({
706
- transaction: { record },
1584
+ transaction: {
1585
+ record: {
1586
+ ...record,
1587
+ schemaEpochGuard: (await store.getCatalogProbe()).schemaEpoch,
1588
+ },
1589
+ },
707
1590
  blocks: [{ id: "block-single", bytes: Uint8Array.of(1, 2, 3, 4) }],
708
1591
  segments: [segment("segment-single", "table-t", "txn-single", "block-single")],
709
1592
  expectedManifestVersion: version,
1593
+ levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
710
1594
  uniqueKeyChanges: [
711
1595
  { tableId: "table-t", keyTokens: ["number:1"], requireAbsent: false },
712
1596
  ],
@@ -722,10 +1606,17 @@ export function blockStoreConformanceCases() {
722
1606
  kind: "reader",
723
1607
  manifestVersion: null,
724
1608
  ownerId: "owner",
725
- expiresAt: FAR_FUTURE,
1609
+ createdAt: T0,
1610
+ expiresAt: LATER,
726
1611
  revision: 0,
727
1612
  });
728
- 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
+ });
729
1620
  }
730
1621
  store = await target.reopen(store);
731
1622
  checkEqual(await store.getCurrentManifestVersion(), version, "the committed version must survive a reopen");
@@ -742,4 +1633,3 @@ export function blockStoreConformanceCases() {
742
1633
  },
743
1634
  ];
744
1635
  }
745
- //# sourceMappingURL=block-store-conformance.js.map