@minnowdb/core 0.6.9 → 0.7.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.
- package/dist/block-format/block.js +254 -325
- package/dist/block-format/checksum.js +30 -52
- package/dist/block-format/codecs.js +112 -125
- package/dist/block-format/column.js +187 -205
- package/dist/block-format/index.js +5 -1
- package/dist/block-format/physical.js +371 -387
- package/dist/block-format/types.js +4 -1
- package/dist/block-format/unicode.js +30 -39
- package/dist/date-value.d.ts +5 -0
- package/dist/date-value.js +71 -41
- package/dist/engine/artifact-cache.js +106 -116
- package/dist/engine/batch.js +36 -44
- package/dist/engine/buffered-writer.js +156 -158
- package/dist/engine/byte-estimates.js +20 -22
- package/dist/engine/cache-limits.js +24 -25
- package/dist/engine/cancellation.js +5 -3
- package/dist/engine/catalog.js +64 -73
- package/dist/engine/client.js +715 -780
- package/dist/engine/database.js +15952 -18465
- package/dist/engine/defaults.js +104 -111
- package/dist/engine/errors.js +113 -128
- package/dist/engine/fts.js +233 -280
- package/dist/engine/group-index.js +287 -321
- package/dist/engine/index.js +23 -12
- package/dist/engine/join-index.js +177 -186
- package/dist/engine/keyed-live.js +168 -173
- package/dist/engine/live-api.js +0 -1
- package/dist/engine/live-equal.js +32 -37
- package/dist/engine/live.js +603 -647
- package/dist/engine/memory.js +109 -111
- package/dist/engine/optimizer.d.ts +19 -1
- package/dist/engine/optimizer.js +2642 -2515
- package/dist/engine/point-read.d.ts +4 -2
- package/dist/engine/point-read.js +121 -171
- package/dist/engine/query-api.js +12 -3
- package/dist/engine/query-cache.js +61 -69
- package/dist/engine/query.d.ts +45 -0
- package/dist/engine/query.js +8506 -9375
- package/dist/engine/result-wire.js +221 -252
- package/dist/engine/schema-wire.js +97 -112
- package/dist/engine/schema.js +1099 -1254
- package/dist/engine/sort-keys.js +315 -389
- package/dist/engine/sql-domains.d.ts +7 -1
- package/dist/engine/sql-domains.js +648 -734
- package/dist/engine/sql-driver.js +0 -1
- package/dist/engine/sql-functions.d.ts +11 -0
- package/dist/engine/sql-functions.js +1092 -0
- package/dist/engine/sql-json.js +199 -218
- package/dist/engine/sql-semantics.d.ts +25 -4
- package/dist/engine/sql-semantics.js +579 -513
- package/dist/engine/typed-live.js +271 -293
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +4513 -4580
- package/dist/engine/worker-host.d.ts +8 -63
- package/dist/engine/worker-host.js +28 -1130
- package/dist/engine/worker-indexeddb.d.ts +1 -0
- package/dist/engine/worker-indexeddb.js +3 -0
- package/dist/engine/worker-memory.d.ts +1 -0
- package/dist/engine/worker-memory.js +3 -0
- package/dist/engine/worker-opfs.d.ts +1 -0
- package/dist/engine/worker-opfs.js +3 -0
- package/dist/engine/worker-server.d.ts +93 -0
- package/dist/engine/worker-server.js +999 -0
- package/dist/engine/worker-store-indexeddb.d.ts +2 -0
- package/dist/engine/worker-store-indexeddb.js +11 -0
- package/dist/engine/worker-store-memory.d.ts +2 -0
- package/dist/engine/worker-store-memory.js +6 -0
- package/dist/engine/worker-store-opfs.d.ts +2 -0
- package/dist/engine/worker-store-opfs.js +9 -0
- package/dist/engine/worker.js +0 -12
- package/dist/engine/write-block-planner.js +93 -100
- package/dist/plan/index.js +15 -16
- package/dist/plan/model.d.ts +9 -1
- package/dist/plan/model.js +20 -27
- package/dist/storage/index.js +0 -12
- package/dist/storage/indexeddb.js +11776 -13171
- package/dist/storage/memory.js +1198 -1241
- package/dist/storage/opfs/files.js +238 -295
- package/dist/storage/opfs/index.js +9 -3
- package/dist/storage/opfs/leader.js +4386 -4856
- package/dist/storage/opfs/rpc.js +223 -259
- package/dist/storage/opfs/snapshot-ledger.js +219 -260
- package/dist/storage/opfs/store.js +884 -1003
- package/dist/storage/persistence.js +59 -74
- package/dist/storage/snapshot-stream.js +788 -826
- package/dist/storage/snapshot.js +36 -37
- package/dist/storage/toolkit/extents.js +444 -520
- package/dist/storage/toolkit/index.js +28 -29
- package/dist/storage/toolkit/record-core.js +5792 -6377
- package/dist/storage/toolkit/sync-file.js +34 -42
- package/dist/storage/toolkit/wal.js +92 -127
- package/dist/storage/toolkit/wire.js +232 -256
- package/dist/storage/types.js +2952 -3228
- package/dist/testing/block-store-conformance.js +1556 -1629
- package/dist/testing/index.js +310 -291
- package/dist/testing/opfs-shim.js +280 -312
- package/dist/testing/simulator.js +496 -534
- package/dist/testing/sqllogictest.js +425 -365
- package/dist/transactions/index.js +1464 -1712
- package/dist/worker-protocol/index.js +70 -69
- package/package.json +20 -2
- package/postgres-feature-profile.json +15 -5
- package/sql-feature-matrix.json +252 -2
|
@@ -1,1663 +1,1590 @@
|
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
10
|
-
throw new Error(`[${conformanceCase.name}] ${message}`, { cause: error });
|
|
11
|
-
}
|
|
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
|
+
async function runBlockStoreConformance(target) {
|
|
3
|
+
for (const conformanceCase of blockStoreConformanceCases()) {
|
|
4
|
+
try {
|
|
5
|
+
await conformanceCase.run(target);
|
|
6
|
+
} catch (error) {
|
|
7
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8
|
+
throw new Error(`[${conformanceCase.name}] ${message}`, { cause: error });
|
|
12
9
|
}
|
|
10
|
+
}
|
|
13
11
|
}
|
|
14
|
-
// ------------------------------------------------------------------------------------------
|
|
15
|
-
// Assertions — deliberately tiny and dependency-free.
|
|
16
|
-
// ------------------------------------------------------------------------------------------
|
|
17
12
|
function check(condition, message) {
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
if (!condition)
|
|
14
|
+
throw new Error(message);
|
|
20
15
|
}
|
|
21
16
|
function checkEqual(actual, expected, message) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
if (!deepEqual(actual, expected)) {
|
|
18
|
+
throw new Error(`${message}
|
|
19
|
+
expected: ${render(expected)}
|
|
20
|
+
actual: ${render(actual)}`);
|
|
21
|
+
}
|
|
25
22
|
}
|
|
26
23
|
async function checkThrows(work, errorClass, message) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return error;
|
|
36
|
-
}
|
|
37
|
-
throw new Error(`${message}: resolved instead of throwing ${errorClass.name}`);
|
|
24
|
+
try {
|
|
25
|
+
await work();
|
|
26
|
+
} catch (error) {
|
|
27
|
+
check(error instanceof errorClass, `${message}: threw ${render(error)} instead of ${errorClass.name}. Conflicts must be thrown as the exact exported classes \u2014 the engine's retry loops match on them.`);
|
|
28
|
+
check(error.name === errorClass.name, `${message}: the error's own name is "${error.name}", not "${errorClass.name}" \u2014 the worker client rehydrates errors by constructor name.`);
|
|
29
|
+
return error;
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`${message}: resolved instead of throwing ${errorClass.name}`);
|
|
38
32
|
}
|
|
39
33
|
function deepEqual(left, right) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const leftKeys = Object.keys(left).sort();
|
|
55
|
-
const rightKeys = Object.keys(right).sort();
|
|
56
|
-
return (deepEqual(leftKeys, rightKeys) &&
|
|
57
|
-
leftKeys.every((key) => deepEqual(left[key], right[key])));
|
|
58
|
-
}
|
|
59
|
-
return false;
|
|
34
|
+
if (Object.is(left, right))
|
|
35
|
+
return true;
|
|
36
|
+
if (left instanceof Uint8Array && right instanceof Uint8Array) {
|
|
37
|
+
return left.byteLength === right.byteLength && left.every((byte, at) => right[at] === byte);
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
40
|
+
return left.length === right.length && left.every((entry, at) => deepEqual(entry, right[at]));
|
|
41
|
+
}
|
|
42
|
+
if (typeof left === "object" && typeof right === "object" && left !== null && right !== null && !Array.isArray(left) && !Array.isArray(right)) {
|
|
43
|
+
const leftKeys = Object.keys(left).sort();
|
|
44
|
+
const rightKeys = Object.keys(right).sort();
|
|
45
|
+
return deepEqual(leftKeys, rightKeys) && leftKeys.every((key) => deepEqual(left[key], right[key]));
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
60
48
|
}
|
|
61
49
|
function render(value) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
: entry);
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return String(value);
|
|
73
|
-
}
|
|
50
|
+
if (value instanceof Error)
|
|
51
|
+
return `${value.name}: ${value.message}`;
|
|
52
|
+
try {
|
|
53
|
+
return JSON.stringify(value, (_key, entry) => typeof entry === "bigint" ? `${entry.toString()}n` : entry instanceof Uint8Array ? `Uint8Array(${String(entry.byteLength)})` : entry);
|
|
54
|
+
} catch {
|
|
55
|
+
return String(value);
|
|
56
|
+
}
|
|
74
57
|
}
|
|
75
58
|
async function manifestBlockIds(store, version) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
59
|
+
const ids = [];
|
|
60
|
+
let afterBlockId = null;
|
|
61
|
+
for (; ; ) {
|
|
62
|
+
const page = await store.listManifestBlockPage({ version, afterBlockId, limit: 256 });
|
|
63
|
+
ids.push(...page.records.map(({ blockId }) => blockId));
|
|
64
|
+
if (page.nextCursor === null)
|
|
65
|
+
return ids;
|
|
66
|
+
afterBlockId = page.nextCursor;
|
|
67
|
+
}
|
|
85
68
|
}
|
|
86
69
|
async function currentManifestBlockIds(store) {
|
|
87
|
-
|
|
88
|
-
|
|
70
|
+
const version = await store.getCurrentManifestVersion();
|
|
71
|
+
return version === null ? [] : manifestBlockIds(store, version);
|
|
89
72
|
}
|
|
90
73
|
async function tableSegments(store, tableId) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
74
|
+
const records = [];
|
|
75
|
+
let afterId = null;
|
|
76
|
+
for (; ; ) {
|
|
77
|
+
const page = await store.listTableSegmentPage(tableId, afterId, 256);
|
|
78
|
+
records.push(...page.records);
|
|
79
|
+
if (page.nextCursor === null)
|
|
80
|
+
return records;
|
|
81
|
+
afterId = page.nextCursor;
|
|
82
|
+
}
|
|
100
83
|
}
|
|
101
|
-
// ------------------------------------------------------------------------------------------
|
|
102
|
-
// Fixtures — the smallest records that exercise each rule.
|
|
103
|
-
// ------------------------------------------------------------------------------------------
|
|
104
84
|
const T0 = "2026-01-01T00:00:00.000Z";
|
|
105
85
|
const MIDDLE = "2026-01-01T00:30:00.000Z";
|
|
106
86
|
const LATER = "2026-01-01T01:00:00.000Z";
|
|
107
87
|
const PAST = "2000-01-01T00:00:00.000Z";
|
|
108
88
|
function table(name, keyed = false) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
89
|
+
return {
|
|
90
|
+
id: `table-${name}`,
|
|
91
|
+
name,
|
|
92
|
+
columns: [
|
|
93
|
+
{ id: "col-id", name: "id", type: "number", nullable: false },
|
|
94
|
+
{ id: "col-v", name: "v", type: "string", nullable: true }
|
|
95
|
+
],
|
|
96
|
+
managed: false,
|
|
97
|
+
revision: 0,
|
|
98
|
+
createdAt: T0,
|
|
99
|
+
...keyed ? { uniqueKeyColumnId: "col-id" } : {}
|
|
100
|
+
};
|
|
121
101
|
}
|
|
122
102
|
function transaction(id, snapshotVersion) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
ownerId: `owner-${id}`,
|
|
106
|
+
expiresAt: LATER,
|
|
107
|
+
snapshotVersion,
|
|
108
|
+
pendingBlockIds: [],
|
|
109
|
+
pendingSegmentIds: [],
|
|
110
|
+
status: "active",
|
|
111
|
+
revision: 0,
|
|
112
|
+
startedAt: T0,
|
|
113
|
+
updatedAt: T0,
|
|
114
|
+
committedVersion: null
|
|
115
|
+
};
|
|
136
116
|
}
|
|
137
117
|
function segment(id, tableId, transactionId, blockId, commitOrdinal = 0) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
118
|
+
return {
|
|
119
|
+
id,
|
|
120
|
+
tableId,
|
|
121
|
+
transactionId,
|
|
122
|
+
rowCount: 2,
|
|
123
|
+
rowIdStart: 1n,
|
|
124
|
+
rowIdEndExclusive: 3n,
|
|
125
|
+
columnBlockIds: { "col-id": [blockId], "col-v": [blockId] },
|
|
126
|
+
kind: "insert",
|
|
127
|
+
level: 0,
|
|
128
|
+
logicalOrder: 0,
|
|
129
|
+
commitOrdinal,
|
|
130
|
+
rowIdSpans: [],
|
|
131
|
+
createdAt: T0
|
|
132
|
+
};
|
|
153
133
|
}
|
|
154
|
-
/** Stages one block + one segment on a fresh transaction and commits it. */
|
|
155
134
|
async function commitOne(store, suffix, options = {}) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
: {
|
|
178
|
-
uniqueKeyChanges: [
|
|
179
|
-
{
|
|
180
|
-
tableId: options.tableId ?? "table-t",
|
|
181
|
-
keyTokens: options.keyTokens,
|
|
182
|
-
requireAbsent: true,
|
|
183
|
-
},
|
|
184
|
-
],
|
|
185
|
-
}),
|
|
186
|
-
});
|
|
187
|
-
return { version: summary.version, blockId };
|
|
188
|
-
}
|
|
189
|
-
// ------------------------------------------------------------------------------------------
|
|
190
|
-
// The cases.
|
|
191
|
-
// ------------------------------------------------------------------------------------------
|
|
192
|
-
export function blockStoreConformanceCases() {
|
|
193
|
-
return [
|
|
194
|
-
{
|
|
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",
|
|
261
|
-
async run(target) {
|
|
262
|
-
const store = await target.create();
|
|
263
|
-
await store.createTransaction(transaction("txn-block-copy", null));
|
|
264
|
-
const bytes = Uint8Array.of(1, 2, 3);
|
|
265
|
-
await store.stageTransactionArtifacts({
|
|
266
|
-
transactionId: "txn-block-copy",
|
|
267
|
-
expectedRevision: 0,
|
|
268
|
-
blocks: [{ id: "block-a", bytes }],
|
|
269
|
-
segments: [],
|
|
270
|
-
updatedAt: T0,
|
|
271
|
-
});
|
|
272
|
-
bytes[0] = 99; // The store must not alias the caller's buffer.
|
|
273
|
-
const first = await store.getBlock("block-a");
|
|
274
|
-
check(first?.[0] === 1, "stored bytes changed with the caller's buffer");
|
|
275
|
-
first[1] = 99; // Nor may the caller's mutations reach the store.
|
|
276
|
-
const second = await store.getBlock("block-a");
|
|
277
|
-
check(second?.[1] === 2, "returned bytes alias the store's copy");
|
|
278
|
-
try {
|
|
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
|
-
});
|
|
286
|
-
throw new Error("a duplicate block id was accepted");
|
|
287
|
-
}
|
|
288
|
-
catch (error) {
|
|
289
|
-
check(error instanceof Error, "duplicate rejection must be an Error");
|
|
290
|
-
}
|
|
291
|
-
checkEqual(await store.getBlocks(["missing", "block-a"]).then((all) => all[0]), undefined, "getBlocks must be positional with undefined for missing ids");
|
|
292
|
-
store.close();
|
|
293
|
-
},
|
|
294
|
-
},
|
|
295
|
-
{
|
|
296
|
-
name: "an artifact batch with any duplicate writes nothing at all",
|
|
297
|
-
async run(target) {
|
|
298
|
-
const store = await target.create();
|
|
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
|
-
});
|
|
307
|
-
try {
|
|
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
|
-
});
|
|
319
|
-
throw new Error("a batch containing an existing id was accepted");
|
|
320
|
-
}
|
|
321
|
-
catch {
|
|
322
|
-
// Expected; what matters is that nothing landed.
|
|
323
|
-
}
|
|
324
|
-
checkEqual(await store.getBlock("block-b"), undefined, "a failed batch leaked block-b");
|
|
325
|
-
checkEqual(await store.getBlock("block-c"), undefined, "a failed batch leaked block-c");
|
|
326
|
-
checkEqual(await store.getBlock("block-a"), Uint8Array.of(1), "the first block changed");
|
|
327
|
-
store.close();
|
|
328
|
-
},
|
|
329
|
-
},
|
|
135
|
+
const version = await store.getCurrentManifestVersion();
|
|
136
|
+
const transactionId = `txn-${suffix}`;
|
|
137
|
+
const blockId = `table/t/segment/${suffix}/part/000000`;
|
|
138
|
+
await store.createTransaction(transaction(transactionId, version));
|
|
139
|
+
await store.stageTransactionArtifacts({
|
|
140
|
+
transactionId,
|
|
141
|
+
expectedRevision: 0,
|
|
142
|
+
blocks: [{ id: blockId, bytes: Uint8Array.of(1, 2, 3, 4) }],
|
|
143
|
+
segments: [segment(`segment-${suffix}`, options.tableId ?? "table-t", transactionId, blockId)],
|
|
144
|
+
updatedAt: T0
|
|
145
|
+
});
|
|
146
|
+
const summary = await store.commitTransaction({
|
|
147
|
+
transactionId,
|
|
148
|
+
expectedTransactionRevision: 1,
|
|
149
|
+
expectedManifestVersion: version,
|
|
150
|
+
levelZeroSegmentLimits: [
|
|
151
|
+
{ tableId: options.tableId ?? "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }
|
|
152
|
+
],
|
|
153
|
+
committedAt: LATER,
|
|
154
|
+
...options.keyTokens === void 0 ? {} : {
|
|
155
|
+
uniqueKeyChanges: [
|
|
330
156
|
{
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
{
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
{
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
const schemaEpoch = (await store.getCatalogProbe()).schemaEpoch;
|
|
1357
|
-
const fresh = (id) => {
|
|
1358
|
-
const { snapshotVersion: _pinned, ...record } = transaction(id, null);
|
|
1359
|
-
void _pinned;
|
|
1360
|
-
return { ...record, schemaEpochGuard: schemaEpoch };
|
|
1361
|
-
};
|
|
1362
|
-
// A fresh record: begun, staged, and committed in the one step.
|
|
1363
|
-
const first = await write({
|
|
1364
|
-
transaction: { record: fresh("txn-w1") },
|
|
1365
|
-
blocks: [{ id: "block-w1", bytes: Uint8Array.of(1) }],
|
|
1366
|
-
segments: [segment("segment-w1", "table-t", "txn-w1", "block-w1")],
|
|
1367
|
-
expectedManifestVersion: null,
|
|
1368
|
-
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1369
|
-
uniqueKeyChanges: [{ tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true }],
|
|
1370
|
-
committedAt: LATER,
|
|
1371
|
-
});
|
|
1372
|
-
checkEqual(first.version, 0, "the first single-shot write must publish version 0");
|
|
1373
|
-
const committed = await store.getTransaction("txn-w1");
|
|
1374
|
-
check(committed?.status === "committed" &&
|
|
1375
|
-
committed.committedVersion === 0 &&
|
|
1376
|
-
committed.snapshotVersion === null, "the fresh record must end committed, pinned at the expected version");
|
|
1377
|
-
checkEqual(committed.pendingBlockIds, ["block-w1"], "the single-shot write must journal its blocks on the record");
|
|
1378
|
-
check((await store.getBlock("block-w1")) !== undefined &&
|
|
1379
|
-
(await store.getSegment("segment-w1"))?.logicalOrder === 0, "the single-shot write must land bytes and finalized segments");
|
|
1380
|
-
checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1"]), ["number:1"], "the single-shot write must apply unique-key changes");
|
|
1381
|
-
// A stale version refuses as WriteConflictError and leaves nothing behind — not even
|
|
1382
|
-
// the record.
|
|
1383
|
-
await checkThrows(() => write({
|
|
1384
|
-
transaction: { record: fresh("txn-stale") },
|
|
1385
|
-
blocks: [{ id: "block-stale", bytes: Uint8Array.of(2) }],
|
|
1386
|
-
segments: [segment("segment-stale", "table-t", "txn-stale", "block-stale")],
|
|
1387
|
-
expectedManifestVersion: null,
|
|
1388
|
-
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1389
|
-
committedAt: LATER,
|
|
1390
|
-
}), WriteConflictError, "a single-shot write against a moved manifest");
|
|
1391
|
-
check((await store.getTransaction("txn-stale")) === undefined &&
|
|
1392
|
-
(await store.getBlock("block-stale")) === undefined &&
|
|
1393
|
-
(await store.getSegment("segment-stale")) === undefined, "a refused single-shot write must leave no record, block, or segment");
|
|
1394
|
-
// So does a duplicate unique key.
|
|
1395
|
-
await checkThrows(() => write({
|
|
1396
|
-
transaction: { record: fresh("txn-dup") },
|
|
1397
|
-
blocks: [{ id: "block-dup", bytes: Uint8Array.of(3) }],
|
|
1398
|
-
segments: [segment("segment-dup", "table-t", "txn-dup", "block-dup")],
|
|
1399
|
-
expectedManifestVersion: 0,
|
|
1400
|
-
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1401
|
-
uniqueKeyChanges: [
|
|
1402
|
-
{ tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true },
|
|
1403
|
-
],
|
|
1404
|
-
committedAt: LATER,
|
|
1405
|
-
}), UniqueKeyConflictError, "a single-shot write inserting an existing unique key");
|
|
1406
|
-
check((await store.getTransaction("txn-dup")) === undefined &&
|
|
1407
|
-
(await store.getBlock("block-dup")) === undefined, "a key conflict must leave no record or block");
|
|
1408
|
-
checkEqual(await store.getCurrentManifestVersion(), 0, "refusals must not advance");
|
|
1409
|
-
// A begun transaction — the shape a reservation needs — continues into the same step.
|
|
1410
|
-
await store.createTransaction(transaction("txn-w2", 0));
|
|
1411
|
-
await checkThrows(() => write({
|
|
1412
|
-
transaction: { id: "txn-w2", expectedRevision: 4 },
|
|
1413
|
-
blocks: [{ id: "block-w2", bytes: Uint8Array.of(4) }],
|
|
1414
|
-
segments: [segment("segment-w2", "table-t", "txn-w2", "block-w2")],
|
|
1415
|
-
expectedManifestVersion: 0,
|
|
1416
|
-
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1417
|
-
committedAt: LATER,
|
|
1418
|
-
}), TransactionRecordConflictError, "a single-shot write with a stale transaction revision");
|
|
1419
|
-
check((await store.getBlock("block-w2")) === undefined, "a revision conflict must leave no block");
|
|
1420
|
-
const second = await write({
|
|
1421
|
-
transaction: { id: "txn-w2", expectedRevision: 0 },
|
|
1422
|
-
blocks: [{ id: "block-w2", bytes: Uint8Array.of(4) }],
|
|
1423
|
-
segments: [segment("segment-w2", "table-t", "txn-w2", "block-w2")],
|
|
1424
|
-
expectedManifestVersion: 0,
|
|
1425
|
-
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1426
|
-
changedTableIds: ["table-t"],
|
|
1427
|
-
committedAt: LATER,
|
|
1428
|
-
});
|
|
1429
|
-
checkEqual(second.version, 1, "the continued write must publish the next version");
|
|
1430
|
-
checkEqual(second.changedTableIds, ["table-t"], "the summary must carry the change set");
|
|
1431
|
-
checkEqual(await manifestBlockIds(store, 1), ["block-w1", "block-w2"], "the continued write must preserve visible blocks and add its own");
|
|
1432
|
-
check((await store.getTransaction("txn-w2"))?.status === "committed", "the begun record must end committed");
|
|
1433
|
-
store.close();
|
|
1434
|
-
},
|
|
1435
|
-
},
|
|
1436
|
-
{
|
|
1437
|
-
name: "compaction commit proves exact output provenance before publication",
|
|
1438
|
-
async run(target) {
|
|
1439
|
-
const store = await target.create();
|
|
1440
|
-
await store.addTable({
|
|
1441
|
-
id: "table-compact",
|
|
1442
|
-
name: "compact",
|
|
1443
|
-
columns: [{ id: "col-value", name: "value", type: "number", nullable: false }],
|
|
1444
|
-
managed: false,
|
|
1445
|
-
revision: 0,
|
|
1446
|
-
createdAt: T0,
|
|
1447
|
-
});
|
|
1448
|
-
await store.createTransaction(transaction("txn-compact-source", null));
|
|
1449
|
-
const source = await store.stageTransactionArtifacts({
|
|
1450
|
-
transactionId: "txn-compact-source",
|
|
1451
|
-
expectedRevision: 0,
|
|
1452
|
-
blocks: [{ id: "compact-source-block", bytes: Uint8Array.of(1) }],
|
|
1453
|
-
segments: [
|
|
1454
|
-
{
|
|
1455
|
-
...segment("compact-source-segment", "table-compact", "txn-compact-source", "compact-source-block"),
|
|
1456
|
-
columnBlockIds: { "col-value": ["compact-source-block"] },
|
|
1457
|
-
},
|
|
1458
|
-
],
|
|
1459
|
-
updatedAt: T0,
|
|
1460
|
-
});
|
|
1461
|
-
await store.commitTransaction({
|
|
1462
|
-
transactionId: source.id,
|
|
1463
|
-
expectedTransactionRevision: source.revision,
|
|
1464
|
-
expectedManifestVersion: null,
|
|
1465
|
-
levelZeroSegmentLimits: [{ tableId: "table-compact", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1466
|
-
committedAt: T0,
|
|
1467
|
-
});
|
|
1468
|
-
const jobId = "compact-proof-job";
|
|
1469
|
-
const outputBlockId = `${jobId}/output/segment/000000/column/000000/part/000000`;
|
|
1470
|
-
await store.createTransaction(transaction("txn-compact-output", 0));
|
|
1471
|
-
const output = await store.stageTransactionArtifacts({
|
|
1472
|
-
transactionId: "txn-compact-output",
|
|
1473
|
-
expectedRevision: 0,
|
|
1474
|
-
blocks: [{ id: outputBlockId, bytes: Uint8Array.of(2) }],
|
|
1475
|
-
segments: [
|
|
1476
|
-
{
|
|
1477
|
-
...segment("compact-output-segment", "table-compact", "txn-compact-output", outputBlockId),
|
|
1478
|
-
columnBlockIds: { "col-value": [outputBlockId] },
|
|
1479
|
-
// Structurally valid, but copy-v1 is required to publish an insert segment.
|
|
1480
|
-
kind: "base",
|
|
1481
|
-
level: 1,
|
|
1482
|
-
},
|
|
1483
|
-
],
|
|
1484
|
-
updatedAt: LATER,
|
|
1485
|
-
});
|
|
1486
|
-
const job = {
|
|
1487
|
-
id: jobId,
|
|
1488
|
-
tableId: "table-compact",
|
|
1489
|
-
sourceManifestVersion: 0,
|
|
1490
|
-
sourceSegmentIds: ["compact-source-segment"],
|
|
1491
|
-
sourceBlockIds: ["compact-source-block"],
|
|
1492
|
-
outputBlockIds: [outputBlockId],
|
|
1493
|
-
cursor: { sourceSegmentIndex: 1, sourceBlockIndex: 0 },
|
|
1494
|
-
processedRows: 2,
|
|
1495
|
-
sourceStoredBytes: 1,
|
|
1496
|
-
outputStoredBytes: 1,
|
|
1497
|
-
logicalBytes: 1,
|
|
1498
|
-
rewritePlan: { kind: "copy-v1" },
|
|
1499
|
-
outputCursor: null,
|
|
1500
|
-
memoryBudgetBytes: 0,
|
|
1501
|
-
minimumMemoryBytes: 0,
|
|
1502
|
-
level0SourceStoredBytes: 1,
|
|
1503
|
-
anchorSourceStoredBytes: 0,
|
|
1504
|
-
peakWorkingBytes: 0,
|
|
1505
|
-
outputLogicalBytes: 1,
|
|
1506
|
-
targetLevel: 1,
|
|
1507
|
-
state: "ready",
|
|
1508
|
-
transactionId: output.id,
|
|
1509
|
-
outputSegmentId: "compact-output-segment",
|
|
1510
|
-
publishedVersion: null,
|
|
1511
|
-
revision: 0,
|
|
1512
|
-
createdAt: T0,
|
|
1513
|
-
updatedAt: LATER,
|
|
1514
|
-
};
|
|
1515
|
-
await store.createCompactionJob(job);
|
|
1516
|
-
await checkThrows(() => store.commitTransaction({
|
|
1517
|
-
transactionId: output.id,
|
|
1518
|
-
expectedTransactionRevision: output.revision,
|
|
1519
|
-
expectedManifestVersion: 0,
|
|
1520
|
-
compactionJobId: job.id,
|
|
1521
|
-
removedBlockIds: ["compact-source-block"],
|
|
1522
|
-
committedAt: LATER,
|
|
1523
|
-
}), Error, "a structurally valid compaction segment that differs from the immutable plan");
|
|
1524
|
-
checkEqual(await store.getCurrentManifestVersion(), 0, "a provenance refusal must not publish a manifest");
|
|
1525
|
-
check((await store.getTransaction(output.id))?.status === "active" &&
|
|
1526
|
-
(await store.getSegment("compact-output-segment"))?.kind === "base" &&
|
|
1527
|
-
(await store.getBlock(outputBlockId)) !== undefined &&
|
|
1528
|
-
(await store.getCompactionJob(job.id))?.state === "ready", "a provenance refusal must leave the transaction, artifacts, and job unchanged");
|
|
1529
|
-
checkEqual(await manifestBlockIds(store, 0), ["compact-source-block"], "a provenance refusal must retain every source block");
|
|
1530
|
-
store.close();
|
|
1531
|
-
},
|
|
1532
|
-
},
|
|
157
|
+
tableId: options.tableId ?? "table-t",
|
|
158
|
+
keyTokens: options.keyTokens,
|
|
159
|
+
requireAbsent: true
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return { version: summary.version, blockId };
|
|
165
|
+
}
|
|
166
|
+
function blockStoreConformanceCases() {
|
|
167
|
+
return [
|
|
168
|
+
{
|
|
169
|
+
name: "pending tables stay invisible across multi-batch reopen and publish only at commit",
|
|
170
|
+
async run(target) {
|
|
171
|
+
let store = await target.create();
|
|
172
|
+
const pending = table("pending");
|
|
173
|
+
const epoch = (await store.getCatalogProbe()).catalogEpoch;
|
|
174
|
+
const begun = await store.beginTransaction({
|
|
175
|
+
record: {
|
|
176
|
+
id: "pending-owner",
|
|
177
|
+
ownerId: "owner-pending",
|
|
178
|
+
expiresAt: LATER,
|
|
179
|
+
pendingBlockIds: [],
|
|
180
|
+
pendingSegmentIds: [],
|
|
181
|
+
status: "active",
|
|
182
|
+
revision: 0,
|
|
183
|
+
startedAt: T0,
|
|
184
|
+
updatedAt: T0,
|
|
185
|
+
committedVersion: null
|
|
186
|
+
},
|
|
187
|
+
pendingTable: { record: pending, nextRowId: 1n, expectedCatalogEpoch: epoch }
|
|
188
|
+
});
|
|
189
|
+
check(begun.record.pendingTable?.id === pending.id, "pending reservation was not recorded");
|
|
190
|
+
check(await store.getTable(pending.id) === void 0, "pending table became public early");
|
|
191
|
+
checkEqual(await store.listTables(), [], "pending table appeared in the public catalog");
|
|
192
|
+
const firstBlock = "pending/block/1";
|
|
193
|
+
await store.stageTransactionArtifacts({
|
|
194
|
+
transactionId: begun.record.id,
|
|
195
|
+
expectedRevision: begun.record.revision,
|
|
196
|
+
blocks: [{ id: firstBlock, bytes: Uint8Array.of(1) }],
|
|
197
|
+
segments: [segment("pending-segment-1", pending.id, begun.record.id, firstBlock)],
|
|
198
|
+
updatedAt: MIDDLE
|
|
199
|
+
});
|
|
200
|
+
if (target.reopen !== void 0)
|
|
201
|
+
store = await target.reopen(store);
|
|
202
|
+
check(await store.getTable(pending.id) === void 0, "reopen exposed a pending table");
|
|
203
|
+
const resumed = await store.getTransaction(begun.record.id);
|
|
204
|
+
check(resumed?.revision === 1, "reopen lost the first pending-table stage");
|
|
205
|
+
const secondBlock = "pending/block/2";
|
|
206
|
+
await store.stageTransactionArtifacts({
|
|
207
|
+
transactionId: begun.record.id,
|
|
208
|
+
expectedRevision: resumed.revision,
|
|
209
|
+
blocks: [{ id: secondBlock, bytes: Uint8Array.of(2) }],
|
|
210
|
+
segments: [
|
|
211
|
+
{
|
|
212
|
+
...segment("pending-segment-2", pending.id, begun.record.id, secondBlock),
|
|
213
|
+
rowIdStart: 3n,
|
|
214
|
+
rowIdEndExclusive: 5n,
|
|
215
|
+
commitOrdinal: 1
|
|
216
|
+
}
|
|
217
|
+
],
|
|
218
|
+
updatedAt: LATER
|
|
219
|
+
});
|
|
220
|
+
const manifest = await store.commitTransaction({
|
|
221
|
+
transactionId: begun.record.id,
|
|
222
|
+
expectedTransactionRevision: 2,
|
|
223
|
+
expectedManifestVersion: null,
|
|
224
|
+
levelZeroSegmentLimits: [{ tableId: pending.id, limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
225
|
+
committedAt: LATER
|
|
226
|
+
});
|
|
227
|
+
check(manifest.version === 0, "pending-table commit did not publish the first manifest");
|
|
228
|
+
check((await store.getTable(pending.id))?.name === pending.name, "commit lost pending table");
|
|
229
|
+
check((await store.getTransaction(begun.record.id))?.pendingTable === void 0, "commit retained the pending catalog reservation");
|
|
230
|
+
store.close();
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "staged blocks are immutable, copied both directions, and duplicates are refused",
|
|
235
|
+
async run(target) {
|
|
236
|
+
const store = await target.create();
|
|
237
|
+
await store.createTransaction(transaction("txn-block-copy", null));
|
|
238
|
+
const bytes = Uint8Array.of(1, 2, 3);
|
|
239
|
+
await store.stageTransactionArtifacts({
|
|
240
|
+
transactionId: "txn-block-copy",
|
|
241
|
+
expectedRevision: 0,
|
|
242
|
+
blocks: [{ id: "block-a", bytes }],
|
|
243
|
+
segments: [],
|
|
244
|
+
updatedAt: T0
|
|
245
|
+
});
|
|
246
|
+
bytes[0] = 99;
|
|
247
|
+
const first = await store.getBlock("block-a");
|
|
248
|
+
check(first?.[0] === 1, "stored bytes changed with the caller's buffer");
|
|
249
|
+
first[1] = 99;
|
|
250
|
+
const second = await store.getBlock("block-a");
|
|
251
|
+
check(second?.[1] === 2, "returned bytes alias the store's copy");
|
|
252
|
+
try {
|
|
253
|
+
await store.stageTransactionArtifacts({
|
|
254
|
+
transactionId: "txn-block-copy",
|
|
255
|
+
expectedRevision: 1,
|
|
256
|
+
blocks: [{ id: "block-a", bytes: Uint8Array.of(9) }],
|
|
257
|
+
segments: [],
|
|
258
|
+
updatedAt: LATER
|
|
259
|
+
});
|
|
260
|
+
throw new Error("a duplicate block id was accepted");
|
|
261
|
+
} catch (error) {
|
|
262
|
+
check(error instanceof Error, "duplicate rejection must be an Error");
|
|
263
|
+
}
|
|
264
|
+
checkEqual(await store.getBlocks(["missing", "block-a"]).then((all) => all[0]), void 0, "getBlocks must be positional with undefined for missing ids");
|
|
265
|
+
store.close();
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: "an artifact batch with any duplicate writes nothing at all",
|
|
270
|
+
async run(target) {
|
|
271
|
+
const store = await target.create();
|
|
272
|
+
await store.createTransaction(transaction("txn-block-batch", null));
|
|
273
|
+
await store.stageTransactionArtifacts({
|
|
274
|
+
transactionId: "txn-block-batch",
|
|
275
|
+
expectedRevision: 0,
|
|
276
|
+
blocks: [{ id: "block-a", bytes: Uint8Array.of(1) }],
|
|
277
|
+
segments: [],
|
|
278
|
+
updatedAt: T0
|
|
279
|
+
});
|
|
280
|
+
try {
|
|
281
|
+
await store.stageTransactionArtifacts({
|
|
282
|
+
transactionId: "txn-block-batch",
|
|
283
|
+
expectedRevision: 1,
|
|
284
|
+
blocks: [
|
|
285
|
+
{ id: "block-b", bytes: Uint8Array.of(2) },
|
|
286
|
+
{ id: "block-a", bytes: Uint8Array.of(3) },
|
|
287
|
+
{ id: "block-c", bytes: Uint8Array.of(4) }
|
|
288
|
+
],
|
|
289
|
+
segments: [],
|
|
290
|
+
updatedAt: LATER
|
|
291
|
+
});
|
|
292
|
+
throw new Error("a batch containing an existing id was accepted");
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
checkEqual(await store.getBlock("block-b"), void 0, "a failed batch leaked block-b");
|
|
296
|
+
checkEqual(await store.getBlock("block-c"), void 0, "a failed batch leaked block-c");
|
|
297
|
+
checkEqual(await store.getBlock("block-a"), Uint8Array.of(1), "the first block changed");
|
|
298
|
+
store.close();
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: "version-scoped block reads are exact, bounded, and reject missing history",
|
|
303
|
+
async run(target) {
|
|
304
|
+
const store = await target.create();
|
|
305
|
+
await store.addTable(table("t"));
|
|
306
|
+
const first = await commitOne(store, "manifest-read-first");
|
|
307
|
+
const second = await commitOne(store, "manifest-read-second");
|
|
308
|
+
checkEqual(await store.hasManifestBlocks(first.version, [first.blockId, "not-a-member"]), [true, false], "manifest membership must be positional and exact");
|
|
309
|
+
checkEqual(await store.readManifestBlock(first.version, first.blockId), Uint8Array.of(1, 2, 3, 4), "a readable manifest member must return its payload");
|
|
310
|
+
checkEqual(await store.readManifestBlock(first.version, second.blockId), void 0, "a non-member block must not be readable through an older manifest");
|
|
311
|
+
await checkThrows(() => store.hasManifestBlocks(second.version, Array.from({ length: MAX_MANIFEST_BLOCK_PRESENCE_IDS + 1 }, (_, index) => `presence-${String(index)}`)), RangeError, "manifest membership cap+1");
|
|
312
|
+
await checkThrows(() => store.getBlocks(Array.from({ length: MAX_STORAGE_BULK_READ_ITEMS + 1 }, (_, index) => `block-${String(index)}`)), RangeError, "block read cap+1");
|
|
313
|
+
const oversizedIds = Array.from({ length: MAX_STORAGE_BULK_READ_ITEMS + 1 }, (_, index) => `id-${String(index)}`);
|
|
314
|
+
await checkThrows(() => store.getTransactions(oversizedIds), RangeError, "transaction read cap+1");
|
|
315
|
+
await checkThrows(() => store.getExistingUniqueKeys("table-t", oversizedIds), RangeError, "unique-key lookup cap+1");
|
|
316
|
+
let job = await store.createGarbageCollectionJob({
|
|
317
|
+
id: "gc-prune-manifest-read",
|
|
318
|
+
candidateManifestVersions: [first.version],
|
|
319
|
+
candidateSegmentIds: [],
|
|
320
|
+
candidateBlockIds: [],
|
|
321
|
+
leaseCutoff: LATER,
|
|
322
|
+
createdAt: T0
|
|
323
|
+
});
|
|
324
|
+
while (job.state !== "completed") {
|
|
325
|
+
job = (await store.runGarbageCollectionStep({
|
|
326
|
+
jobId: job.id,
|
|
327
|
+
expectedRevision: job.revision,
|
|
328
|
+
maxItems: 16,
|
|
329
|
+
updatedAt: LATER
|
|
330
|
+
})).job;
|
|
331
|
+
}
|
|
332
|
+
checkEqual(await store.readManifestBlock(first.version, first.blockId), void 0, "a pruned manifest must not expose its former member");
|
|
333
|
+
store.close();
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: "table records round-trip, list sorted by name, and conflict by exact class",
|
|
338
|
+
async run(target) {
|
|
339
|
+
const store = await target.create();
|
|
340
|
+
await store.addTable(table("bravo"));
|
|
341
|
+
await store.addTable(table("alpha"));
|
|
342
|
+
const names = (await store.listTables()).map((record) => record.name);
|
|
343
|
+
checkEqual(names, ["alpha", "bravo"], "listTables must sort by name");
|
|
344
|
+
const byName = await store.getTableByName("alpha");
|
|
345
|
+
check(byName?.id === "table-alpha", "getTableByName must resolve the record");
|
|
346
|
+
await checkThrows(() => store.updateTable("table-alpha", 7, {}), TableRecordConflictError, "updateTable with a stale revision");
|
|
347
|
+
await checkThrows(() => store.removeTable("table-alpha", 7), TableRecordConflictError, "removeTable with a stale revision");
|
|
348
|
+
const updated = await store.updateTable("table-alpha", 0, {});
|
|
349
|
+
check(updated.revision === 1, "updateTable must advance the revision");
|
|
350
|
+
store.close();
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: "secondary-index metadata must describe one direction per indexed column",
|
|
355
|
+
async run(target) {
|
|
356
|
+
const store = await target.create();
|
|
357
|
+
const record = table("bad-secondary");
|
|
358
|
+
try {
|
|
359
|
+
await store.addTable({
|
|
360
|
+
...record,
|
|
361
|
+
secondaryIndexes: {
|
|
362
|
+
bad: {
|
|
363
|
+
name: "bad_direction_arity_index",
|
|
364
|
+
columnId: "col-v",
|
|
365
|
+
columnIds: ["col-v"],
|
|
366
|
+
directions: ["asc", "desc"],
|
|
367
|
+
termEncoding: "tuple-v1",
|
|
368
|
+
storage: "postings-v1",
|
|
369
|
+
storageColumnId: "secondary-index:bad",
|
|
370
|
+
locator: "row-id",
|
|
371
|
+
state: "ready",
|
|
372
|
+
buildFromVersion: 0
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
throw new Error("secondary-index metadata with mismatched direction arity was accepted");
|
|
377
|
+
} catch (error) {
|
|
378
|
+
check(error instanceof TypeError, "invalid secondary-index metadata must be rejected");
|
|
379
|
+
}
|
|
380
|
+
checkEqual(await store.listTables(), [], "a rejected catalog record must leave no table");
|
|
381
|
+
store.close();
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: "removing a table removes everything keyed to it",
|
|
386
|
+
async run(target) {
|
|
387
|
+
const store = await target.create();
|
|
388
|
+
await store.addTable(table("t", true));
|
|
389
|
+
const { blockId } = await commitOne(store, "one", {
|
|
390
|
+
keyTokens: ["number:1"],
|
|
391
|
+
tableId: "table-t"
|
|
392
|
+
});
|
|
393
|
+
await store.reserveRowIds("table-t", 10);
|
|
394
|
+
const dropped = await store.dropTable({
|
|
395
|
+
tableId: "table-t",
|
|
396
|
+
expectedTableRevision: 0,
|
|
397
|
+
expectedManifestVersion: 0,
|
|
398
|
+
expectedCatalogEpoch: (await store.getCatalogProbe()).catalogEpoch,
|
|
399
|
+
committedAt: LATER
|
|
400
|
+
});
|
|
401
|
+
check(dropped.version === 1, "dropTable must publish one successor manifest");
|
|
402
|
+
checkEqual(await currentManifestBlockIds(store), [], "dropTable must retire the table's manifest blocks");
|
|
403
|
+
checkEqual(await tableSegments(store, "table-t"), [], "segments must go with the table");
|
|
404
|
+
checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1"]), [], "unique keys must go with the table");
|
|
405
|
+
await store.addTable(table("t", true));
|
|
406
|
+
const range = await store.reserveRowIds("table-t", 1);
|
|
407
|
+
check(range.start === 1n, "row-id counters must reset with the table");
|
|
408
|
+
check(await store.getBlock(blockId) !== void 0, "blocks are not the catalog's to delete");
|
|
409
|
+
store.close();
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "fused table drop rejects without change and resolves without partial state",
|
|
414
|
+
async run(target) {
|
|
415
|
+
const store = await target.create();
|
|
416
|
+
await store.addTable(table("t"));
|
|
417
|
+
const source = await commitOne(store, "source");
|
|
418
|
+
const catalogEpoch = (await store.getCatalogProbe()).catalogEpoch;
|
|
419
|
+
const drop = () => store.dropTable({
|
|
420
|
+
tableId: "table-t",
|
|
421
|
+
expectedTableRevision: 0,
|
|
422
|
+
expectedManifestVersion: source.version,
|
|
423
|
+
expectedCatalogEpoch: catalogEpoch,
|
|
424
|
+
committedAt: LATER
|
|
425
|
+
});
|
|
426
|
+
await checkThrows(() => store.dropTable({
|
|
427
|
+
tableId: "table-t",
|
|
428
|
+
expectedTableRevision: 9,
|
|
429
|
+
expectedManifestVersion: source.version,
|
|
430
|
+
expectedCatalogEpoch: catalogEpoch,
|
|
431
|
+
committedAt: LATER
|
|
432
|
+
}), TableRecordConflictError, "dropping with a stale table revision");
|
|
433
|
+
await checkThrows(() => store.dropTable({
|
|
434
|
+
tableId: "table-t",
|
|
435
|
+
expectedTableRevision: 0,
|
|
436
|
+
expectedManifestVersion: source.version - 1,
|
|
437
|
+
expectedCatalogEpoch: catalogEpoch,
|
|
438
|
+
committedAt: LATER
|
|
439
|
+
}), WriteConflictError, "dropping with a stale manifest revision");
|
|
440
|
+
check(await store.getTable("table-t") !== void 0 && await store.getCurrentManifestVersion() === source.version, "a fused drop CAS refusal changed the catalog or manifest");
|
|
441
|
+
const active = transaction("txn-active-table", source.version);
|
|
442
|
+
await store.createTransaction(active);
|
|
443
|
+
const activeBlockId = "table/t/segment/active/part/000000";
|
|
444
|
+
const activeSegment = segment("segment-active-table", "table-t", active.id, activeBlockId);
|
|
445
|
+
const staged = await store.stageTransactionArtifacts({
|
|
446
|
+
transactionId: active.id,
|
|
447
|
+
expectedRevision: 0,
|
|
448
|
+
blocks: [{ id: activeBlockId, bytes: Uint8Array.of(9, 8, 7, 6) }],
|
|
449
|
+
segments: [activeSegment],
|
|
450
|
+
updatedAt: LATER
|
|
451
|
+
});
|
|
452
|
+
const activeRefusal = await checkThrows(drop, TableInUseError, "dropping a table with an active transaction");
|
|
453
|
+
checkEqual({
|
|
454
|
+
tableId: activeRefusal.tableId,
|
|
455
|
+
ownerKind: activeRefusal.ownerKind,
|
|
456
|
+
ownerId: activeRefusal.ownerId
|
|
457
|
+
}, { tableId: "table-t", ownerKind: "transaction", ownerId: active.id }, "an active-transaction refusal must identify its exact owner");
|
|
458
|
+
check(await store.getTable("table-t") !== void 0 && await store.getSegment(activeSegment.id) !== void 0 && await store.getBlock(activeBlockId) !== void 0 && await store.getCurrentManifestVersion() === source.version, "a refused table drop must leave catalog, manifest, and active artifacts unchanged");
|
|
459
|
+
await store.updateTransaction(active.id, staged.revision, {
|
|
460
|
+
status: "aborted",
|
|
461
|
+
updatedAt: LATER
|
|
462
|
+
});
|
|
463
|
+
await store.createCompactionJob({
|
|
464
|
+
id: "job-table-in-use",
|
|
465
|
+
tableId: "table-t",
|
|
466
|
+
sourceManifestVersion: source.version,
|
|
467
|
+
sourceSegmentIds: ["segment-source"],
|
|
468
|
+
sourceBlockIds: [source.blockId],
|
|
469
|
+
outputBlockIds: [],
|
|
470
|
+
cursor: { sourceSegmentIndex: 0, sourceBlockIndex: 0 },
|
|
471
|
+
processedRows: 0,
|
|
472
|
+
sourceStoredBytes: 0,
|
|
473
|
+
outputStoredBytes: 0,
|
|
474
|
+
logicalBytes: 0,
|
|
475
|
+
rewritePlan: { kind: "copy-v1" },
|
|
476
|
+
outputCursor: null,
|
|
477
|
+
memoryBudgetBytes: 0,
|
|
478
|
+
minimumMemoryBytes: 0,
|
|
479
|
+
level0SourceStoredBytes: 0,
|
|
480
|
+
anchorSourceStoredBytes: 0,
|
|
481
|
+
peakWorkingBytes: 0,
|
|
482
|
+
outputLogicalBytes: 0,
|
|
483
|
+
targetLevel: 1,
|
|
484
|
+
state: "planned",
|
|
485
|
+
transactionId: null,
|
|
486
|
+
outputSegmentId: null,
|
|
487
|
+
publishedVersion: null,
|
|
488
|
+
revision: 0,
|
|
489
|
+
createdAt: T0,
|
|
490
|
+
updatedAt: T0
|
|
491
|
+
});
|
|
492
|
+
const jobRefusal = await checkThrows(drop, TableInUseError, "dropping a table with a nonterminal compaction job");
|
|
493
|
+
checkEqual({
|
|
494
|
+
tableId: jobRefusal.tableId,
|
|
495
|
+
ownerKind: jobRefusal.ownerKind,
|
|
496
|
+
ownerId: jobRefusal.ownerId
|
|
497
|
+
}, {
|
|
498
|
+
tableId: "table-t",
|
|
499
|
+
ownerKind: "compaction job",
|
|
500
|
+
ownerId: "job-table-in-use"
|
|
501
|
+
}, "a compaction refusal must identify its exact owner");
|
|
502
|
+
check(await store.getTable("table-t") !== void 0 && await store.getCurrentManifestVersion() === source.version && (await store.hasManifestBlocks(source.version, [source.blockId]))[0] === true, "a compaction refusal changed the catalog or manifest");
|
|
503
|
+
await store.cancelCompactionJob("job-table-in-use", 0, LATER);
|
|
504
|
+
const published = await drop();
|
|
505
|
+
check(published.version === source.version + 1, "a fused table drop must publish exactly one successor manifest");
|
|
506
|
+
check(await store.getTable("table-t") === void 0, "a terminal owner blocked removal");
|
|
507
|
+
checkEqual(await tableSegments(store, "table-t"), [], "a fused drop left table segments");
|
|
508
|
+
checkEqual(await currentManifestBlockIds(store), [], "a fused drop left table blocks live");
|
|
509
|
+
check(await store.getBlock(source.blockId) !== void 0, "a fused drop physically deleted a block pinned readers may still need");
|
|
510
|
+
store.close();
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: "counter reservations never overlap, even issued concurrently",
|
|
515
|
+
async run(target) {
|
|
516
|
+
const store = await target.create();
|
|
517
|
+
const counterTable = table("t", true);
|
|
518
|
+
const counterColumn = counterTable.columns[0];
|
|
519
|
+
if (counterColumn === void 0)
|
|
520
|
+
throw new Error("counter fixture column is missing");
|
|
521
|
+
counterTable.columns[0] = {
|
|
522
|
+
...counterColumn,
|
|
523
|
+
defaultValue: { kind: "autoincrement" }
|
|
524
|
+
};
|
|
525
|
+
await store.addTable(counterTable);
|
|
526
|
+
const ranges = await Promise.all(Array.from({ length: 8 }, () => store.reserveRowIds("table-t", 5)));
|
|
527
|
+
const starts = new Set(ranges.map((range) => range.start));
|
|
528
|
+
check(starts.size === 8, "concurrent reservations returned overlapping ranges");
|
|
529
|
+
for (const range of ranges) {
|
|
530
|
+
check(range.endExclusive - range.start === 5n, "a reservation returned the wrong span");
|
|
531
|
+
}
|
|
532
|
+
const bumped = await store.reserveAutoIncrement("table-t", "col-id", 3, 100n);
|
|
533
|
+
check(bumped.start >= 100n, "reserveAutoIncrement must honor atLeast");
|
|
534
|
+
const maximumExclusive = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
|
|
535
|
+
const { snapshotVersion: _invalidSnapshot, ...invalidRecord } = transaction("txn-invalid-counter", null);
|
|
536
|
+
void _invalidSnapshot;
|
|
537
|
+
await checkThrows(() => store.beginTransaction({
|
|
538
|
+
record: invalidRecord,
|
|
539
|
+
reserveAutoIncrement: {
|
|
540
|
+
tableId: "table-t",
|
|
541
|
+
columnId: "col-id",
|
|
542
|
+
count: 0,
|
|
543
|
+
atLeast: maximumExclusive + 1n
|
|
544
|
+
}
|
|
545
|
+
}), RangeError, "an invalid fused auto-increment reservation");
|
|
546
|
+
check(await store.getTransaction(invalidRecord.id) === void 0, "a refused fused reservation must not create its transaction");
|
|
547
|
+
const { snapshotVersion: _validSnapshot, ...validRecord } = transaction("txn-valid-counter", null);
|
|
548
|
+
void _validSnapshot;
|
|
549
|
+
const valid = await store.beginTransaction({
|
|
550
|
+
record: validRecord,
|
|
551
|
+
reserveAutoIncrement: {
|
|
552
|
+
tableId: "table-t",
|
|
553
|
+
columnId: "col-id",
|
|
554
|
+
count: 1,
|
|
555
|
+
atLeast: 150n
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
check(valid.autoIncrementValues?.start === 150n && valid.autoIncrementValues.endExclusive === 151n, "a valid fused reservation after refusal must use the unchanged counter");
|
|
559
|
+
await store.updateTransaction(valid.record.id, valid.record.revision, {
|
|
560
|
+
status: "aborted",
|
|
561
|
+
updatedAt: LATER
|
|
562
|
+
});
|
|
563
|
+
await checkThrows(() => store.reserveAutoIncrement("table-t", "col-id", 0, maximumExclusive + 1n), RangeError, "an auto-increment bump outside the safe-integer range");
|
|
564
|
+
const afterRefusal = await store.reserveAutoIncrement("table-t", "col-id", 1, 200n);
|
|
565
|
+
check(afterRefusal.start === 200n && afterRefusal.endExclusive === 201n, "a refused auto-increment bump must not mutate the counter");
|
|
566
|
+
const boundary = await store.reserveAutoIncrement("table-t", "col-id", 1, maximumExclusive - 1n);
|
|
567
|
+
check(boundary.endExclusive === maximumExclusive, "the final safe auto-increment value must remain reservable");
|
|
568
|
+
const exhausted = await store.reserveAutoIncrement("table-t", "col-id", 0, maximumExclusive);
|
|
569
|
+
check(exhausted.start === maximumExclusive && exhausted.endExclusive === maximumExclusive, "a zero-count bump to the exclusive boundary must be representable");
|
|
570
|
+
await checkThrows(() => store.reserveAutoIncrement("table-t", "col-id", 1), RangeError, "an auto-increment range crossing the safe-integer boundary");
|
|
571
|
+
store.close();
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: "transaction ownership renewal races expiry abort atomically",
|
|
576
|
+
async run(target) {
|
|
577
|
+
const store = await target.create();
|
|
578
|
+
const record = transaction("txn-live", null);
|
|
579
|
+
record.expiresAt = MIDDLE;
|
|
580
|
+
await store.createTransaction(record);
|
|
581
|
+
check(!await store.renewTransaction({
|
|
582
|
+
transactionId: record.id,
|
|
583
|
+
ownerId: record.ownerId,
|
|
584
|
+
expiresAtCutoff: MIDDLE,
|
|
585
|
+
expiresAt: LATER
|
|
586
|
+
}), "ownership expired exactly at the cutoff must not be resurrected");
|
|
587
|
+
check((await store.getTransaction(record.id))?.expiresAt === MIDDLE, "a refused expiry-boundary renewal mutated the record");
|
|
588
|
+
check(!await store.renewTransaction({
|
|
589
|
+
transactionId: record.id,
|
|
590
|
+
ownerId: "wrong-owner",
|
|
591
|
+
expiresAtCutoff: T0,
|
|
592
|
+
expiresAt: LATER
|
|
593
|
+
}), "another owner must not renew a transaction");
|
|
594
|
+
check((await store.getTransaction(record.id))?.revision === 0, "ownership renewal must not contend on the data revision");
|
|
595
|
+
const [renewed, aborted] = await Promise.all([
|
|
596
|
+
store.renewTransaction({
|
|
597
|
+
transactionId: record.id,
|
|
598
|
+
ownerId: record.ownerId,
|
|
599
|
+
expiresAtCutoff: T0,
|
|
600
|
+
expiresAt: LATER
|
|
601
|
+
}),
|
|
602
|
+
store.abortTransactionIfExpired({
|
|
603
|
+
transactionId: record.id,
|
|
604
|
+
expectedOwnerId: record.ownerId,
|
|
605
|
+
expiresAtCutoff: MIDDLE,
|
|
606
|
+
updatedAt: LATER
|
|
607
|
+
})
|
|
608
|
+
]);
|
|
609
|
+
const final = await store.getTransaction(record.id);
|
|
610
|
+
check(final !== void 0, "the liveness race lost its transaction record");
|
|
611
|
+
check(renewed && aborted === void 0 && final.status === "active" || !renewed && aborted?.status === "aborted" && final.status === "aborted", "renewal and expiry abort must have one linearized winner");
|
|
612
|
+
if (final.status === "active") {
|
|
613
|
+
check(final.expiresAt === LATER, "the winning renewal did not extend ownership");
|
|
614
|
+
const expired = await store.abortTransactionIfExpired({
|
|
615
|
+
transactionId: final.id,
|
|
616
|
+
expectedOwnerId: final.ownerId,
|
|
617
|
+
expiresAtCutoff: "9999-12-31T23:59:59.999Z",
|
|
618
|
+
updatedAt: LATER
|
|
619
|
+
});
|
|
620
|
+
check(expired?.status === "aborted", "an expired owner was not atomically aborted");
|
|
621
|
+
}
|
|
622
|
+
store.close();
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: "a commit is atomic and visible: manifest, record, segments, keys together",
|
|
627
|
+
async run(target) {
|
|
628
|
+
const store = await target.create();
|
|
629
|
+
await store.addTable(table("t", true));
|
|
630
|
+
const { version, blockId } = await commitOne(store, "one", {
|
|
631
|
+
keyTokens: ["number:1"]
|
|
632
|
+
});
|
|
633
|
+
checkEqual(await store.getCurrentManifestVersion(), version, "the version must advance");
|
|
634
|
+
const manifest = await store.getCurrentManifest();
|
|
635
|
+
check(manifest !== void 0 && (await store.hasManifestBlocks(manifest.version, [blockId]))[0] === true, "the manifest must include the committed block");
|
|
636
|
+
const record = await store.getTransaction("txn-one");
|
|
637
|
+
check(record?.status === "committed" && record.committedVersion === version, "the transaction record must flip to committed with the published version");
|
|
638
|
+
checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1", "number:2"]), ["number:1"], "committed unique keys must be queryable");
|
|
639
|
+
const finalized = await store.getSegment("segment-one");
|
|
640
|
+
check(finalized?.level === 0 && finalized.logicalOrder === version, "committed segments must be finalized with level and logical order");
|
|
641
|
+
store.close();
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: "commit conflicts are typed exactly, and a refused commit changes nothing",
|
|
646
|
+
async run(target) {
|
|
647
|
+
const store = await target.create();
|
|
648
|
+
await store.addTable(table("t", true));
|
|
649
|
+
await commitOne(store, "one");
|
|
650
|
+
const staleVersion = null;
|
|
651
|
+
await store.createTransaction(transaction("txn-stale", staleVersion));
|
|
652
|
+
await checkThrows(() => store.commitTransaction({
|
|
653
|
+
transactionId: "txn-stale",
|
|
654
|
+
expectedTransactionRevision: 0,
|
|
655
|
+
expectedManifestVersion: staleVersion,
|
|
656
|
+
committedAt: LATER
|
|
657
|
+
}), WriteConflictError, "a commit against a moved manifest");
|
|
658
|
+
const movedVersion = await store.getCurrentManifestVersion();
|
|
659
|
+
await checkThrows(() => store.commitTransaction({
|
|
660
|
+
transactionId: "txn-stale",
|
|
661
|
+
expectedTransactionRevision: 41,
|
|
662
|
+
expectedManifestVersion: movedVersion,
|
|
663
|
+
committedAt: LATER
|
|
664
|
+
}), TransactionRecordConflictError, "a commit with a stale transaction revision");
|
|
665
|
+
const still = await store.getTransaction("txn-stale");
|
|
666
|
+
check(still?.status === "active", "a refused commit must leave the transaction active");
|
|
667
|
+
store.close();
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: "duplicate unique keys refuse with UniqueKeyConflictError and commit nothing",
|
|
672
|
+
async run(target) {
|
|
673
|
+
const store = await target.create();
|
|
674
|
+
await store.addTable(table("t", true));
|
|
675
|
+
const { version } = await commitOne(store, "one", { keyTokens: ["number:1"] });
|
|
676
|
+
await store.createTransaction(transaction("txn-dup", version));
|
|
677
|
+
await store.stageTransactionArtifacts({
|
|
678
|
+
transactionId: "txn-dup",
|
|
679
|
+
expectedRevision: 0,
|
|
680
|
+
blocks: [{ id: "block-dup", bytes: Uint8Array.of(9) }],
|
|
681
|
+
segments: [],
|
|
682
|
+
updatedAt: T0
|
|
683
|
+
});
|
|
684
|
+
await checkThrows(() => store.commitTransaction({
|
|
685
|
+
transactionId: "txn-dup",
|
|
686
|
+
expectedTransactionRevision: 1,
|
|
687
|
+
expectedManifestVersion: version,
|
|
688
|
+
committedAt: LATER,
|
|
689
|
+
uniqueKeyChanges: [
|
|
690
|
+
{ tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true }
|
|
691
|
+
]
|
|
692
|
+
}), UniqueKeyConflictError, "a commit inserting an existing unique key");
|
|
693
|
+
checkEqual(await store.getCurrentManifestVersion(), version, "a key conflict must not advance the version");
|
|
694
|
+
store.close();
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
name: "leases pin versions, renew by CAS, and expire honestly",
|
|
699
|
+
async run(target) {
|
|
700
|
+
const store = await target.create();
|
|
701
|
+
await store.addTable(table("t"));
|
|
702
|
+
const { version } = await commitOne(store, "one");
|
|
703
|
+
await store.createLease({
|
|
704
|
+
id: "lease-a",
|
|
705
|
+
kind: "reader",
|
|
706
|
+
manifestVersion: version,
|
|
707
|
+
ownerId: "owner-a",
|
|
708
|
+
createdAt: T0,
|
|
709
|
+
expiresAt: LATER,
|
|
710
|
+
revision: 0
|
|
711
|
+
});
|
|
712
|
+
await checkThrows(() => store.renewLease({
|
|
713
|
+
id: "lease-a",
|
|
714
|
+
expectedRevision: 9,
|
|
715
|
+
expiresAtCutoff: T0,
|
|
716
|
+
expiresAt: LATER
|
|
717
|
+
}), LeaseConflictError, "renewing with a stale revision");
|
|
718
|
+
const renewed = await store.renewLease({
|
|
719
|
+
id: "lease-a",
|
|
720
|
+
expectedRevision: 0,
|
|
721
|
+
expiresAtCutoff: T0,
|
|
722
|
+
expiresAt: LATER
|
|
723
|
+
});
|
|
724
|
+
check(renewed.revision === 1, "a renewal must advance the revision");
|
|
725
|
+
await checkThrows(() => store.removeLease({ id: "lease-a", ownerId: "wrong-owner" }), LeaseOwnerConflictError, "releasing another owner's lease");
|
|
726
|
+
check((await store.getLease("lease-a"))?.revision === 1, "a refused lease release mutated the record");
|
|
727
|
+
checkEqual(await store.removeLeaseIfExpired("lease-a", 1, PAST), false, "an unexpired lease must not be removed");
|
|
728
|
+
checkEqual(await store.removeLeaseIfExpired("lease-a", 1, "3000-01-01T00:00:00.000Z"), true, "an expired lease must be removed");
|
|
729
|
+
await store.createLease({
|
|
730
|
+
id: "lease-race",
|
|
731
|
+
kind: "reader",
|
|
732
|
+
manifestVersion: version,
|
|
733
|
+
ownerId: "owner-a",
|
|
734
|
+
createdAt: T0,
|
|
735
|
+
expiresAt: LATER,
|
|
736
|
+
revision: 0
|
|
737
|
+
});
|
|
738
|
+
const [, release] = await Promise.allSettled([
|
|
739
|
+
store.renewLease({
|
|
740
|
+
id: "lease-race",
|
|
741
|
+
expectedRevision: 0,
|
|
742
|
+
expiresAtCutoff: T0,
|
|
743
|
+
expiresAt: LATER
|
|
744
|
+
}),
|
|
745
|
+
store.removeLease({ id: "lease-race", ownerId: "owner-a" })
|
|
746
|
+
]);
|
|
747
|
+
check(release.status === "fulfilled", "a same-owner renewal made release unsafe");
|
|
748
|
+
check(await store.getLease("lease-race") === void 0, "same-owner release did not linearize after/before renewal");
|
|
749
|
+
store.close();
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
name: "garbage collection reclaims only unpinned, superseded state \u2014 atomically",
|
|
754
|
+
async run(target) {
|
|
755
|
+
const store = await target.create();
|
|
756
|
+
await store.addTable(table("t"));
|
|
757
|
+
const first = await commitOne(store, "one");
|
|
758
|
+
await store.dropTable({
|
|
759
|
+
tableId: "table-t",
|
|
760
|
+
expectedTableRevision: 0,
|
|
761
|
+
expectedManifestVersion: first.version,
|
|
762
|
+
expectedCatalogEpoch: (await store.getCatalogProbe()).catalogEpoch,
|
|
763
|
+
committedAt: LATER
|
|
764
|
+
});
|
|
765
|
+
const retiredBeforeCollection = await store.listRetiredManifestBlockPage({
|
|
766
|
+
removedThroughVersion: first.version + 1,
|
|
767
|
+
afterBlockId: null,
|
|
768
|
+
limit: 1
|
|
769
|
+
});
|
|
770
|
+
checkEqual(retiredBeforeCollection.records.map((record) => record.blockId), [first.blockId], "retired provenance must remain independently pageable before collection");
|
|
771
|
+
const job = await store.createGarbageCollectionJob({
|
|
772
|
+
id: "gc-1",
|
|
773
|
+
candidateManifestVersions: [first.version],
|
|
774
|
+
candidateSegmentIds: [],
|
|
775
|
+
candidateBlockIds: [first.blockId],
|
|
776
|
+
leaseCutoff: LATER,
|
|
777
|
+
createdAt: LATER
|
|
778
|
+
});
|
|
779
|
+
await checkThrows(() => store.removeGarbageCollectionJob(job.id), Error, "removing a non-completed garbage-collection job");
|
|
780
|
+
check((await store.getGarbageCollectionJob(job.id))?.state === "planned", "a refused garbage-collection job removal mutated the record");
|
|
781
|
+
const step = await store.runGarbageCollectionStep({
|
|
782
|
+
jobId: job.id,
|
|
783
|
+
expectedRevision: job.revision,
|
|
784
|
+
maxItems: 100,
|
|
785
|
+
updatedAt: LATER
|
|
786
|
+
});
|
|
787
|
+
checkEqual(step.prunedManifestVersions, [first.version], "the superseded version must prune");
|
|
788
|
+
checkEqual(step.reclaimedBlockIds, [first.blockId], "the superseded block must reclaim");
|
|
789
|
+
checkEqual(await store.getBlock(first.blockId), void 0, "reclaimed bytes must be gone");
|
|
790
|
+
checkEqual((await store.listRetiredManifestBlockPage({
|
|
791
|
+
removedThroughVersion: first.version + 1,
|
|
792
|
+
afterBlockId: null,
|
|
793
|
+
limit: 1
|
|
794
|
+
})).records, [], "payload reclamation must atomically remove its retired provenance");
|
|
795
|
+
checkEqual(await store.getCurrentManifestVersion(), 1, "the successor manifest was lost");
|
|
796
|
+
const pruned = await store.getManifest(first.version);
|
|
797
|
+
check(pruned?.prunedAt !== void 0, "pruned manifests are tombstoned, not deleted");
|
|
798
|
+
await store.removeGarbageCollectionJob(job.id);
|
|
799
|
+
check(await store.getGarbageCollectionJob(job.id) === void 0, "a completed garbage-collection job was retained");
|
|
800
|
+
store.close();
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
name: "temp spill pages are isolated scratch; owner records conflict by exact class",
|
|
805
|
+
async run(target) {
|
|
806
|
+
const store = await target.create();
|
|
807
|
+
await store.createTempOwner({
|
|
808
|
+
ownerId: "o1",
|
|
809
|
+
createdAt: T0,
|
|
810
|
+
expiresAt: LATER,
|
|
811
|
+
revision: 0
|
|
812
|
+
});
|
|
813
|
+
await store.putTempRunPage({
|
|
814
|
+
ownerId: "o1",
|
|
815
|
+
runId: "r1",
|
|
816
|
+
pageIndex: 0,
|
|
817
|
+
bytes: Uint8Array.of(1)
|
|
818
|
+
});
|
|
819
|
+
const batched = store.putTempRunPages?.bind(store);
|
|
820
|
+
if (batched !== void 0) {
|
|
821
|
+
await batched([
|
|
822
|
+
{ ownerId: "o1", runId: "r1", pageIndex: 1, bytes: Uint8Array.of(2) },
|
|
823
|
+
{ ownerId: "o1", runId: "r2", pageIndex: 0, bytes: Uint8Array.of(3) }
|
|
824
|
+
]);
|
|
825
|
+
checkEqual(await store.getTempRunPage("o1", "r1", 1), Uint8Array.of(2), "batched pages must read back identically");
|
|
826
|
+
}
|
|
827
|
+
await store.removeTempRun("o1", "r1");
|
|
828
|
+
checkEqual(await store.getTempRunPage("o1", "r1", 0), void 0, "a removed run must be gone");
|
|
829
|
+
await checkThrows(() => store.renewTempOwner({
|
|
830
|
+
ownerId: "o1",
|
|
831
|
+
expectedRevision: 9,
|
|
832
|
+
expiresAtCutoff: T0,
|
|
833
|
+
expiresAt: LATER
|
|
834
|
+
}), TempOwnerConflictError, "renewing a temp owner with a stale revision");
|
|
835
|
+
checkEqual(await store.removeTempOwnerIfExpired("o1", PAST), false, "an unexpired owner must survive the sweep");
|
|
836
|
+
const listed = await store.listTempOwnerIdsPage(null, 10);
|
|
837
|
+
check(listed.records.includes("o1"), "owners must be listable");
|
|
838
|
+
store.close();
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
name: "compaction job records update by CAS with the exact conflict class",
|
|
843
|
+
async run(target) {
|
|
844
|
+
const store = await target.create();
|
|
845
|
+
await store.addTable(table("t"));
|
|
846
|
+
const { version, blockId } = await commitOne(store, "one");
|
|
847
|
+
await store.createCompactionJob({
|
|
848
|
+
id: "job-1",
|
|
849
|
+
tableId: "table-t",
|
|
850
|
+
sourceManifestVersion: version,
|
|
851
|
+
sourceSegmentIds: ["segment-one"],
|
|
852
|
+
sourceBlockIds: [blockId],
|
|
853
|
+
outputBlockIds: [],
|
|
854
|
+
cursor: { sourceSegmentIndex: 0, sourceBlockIndex: 0 },
|
|
855
|
+
processedRows: 0,
|
|
856
|
+
sourceStoredBytes: 0,
|
|
857
|
+
outputStoredBytes: 0,
|
|
858
|
+
logicalBytes: 0,
|
|
859
|
+
rewritePlan: { kind: "copy-v1" },
|
|
860
|
+
outputCursor: null,
|
|
861
|
+
memoryBudgetBytes: 0,
|
|
862
|
+
minimumMemoryBytes: 0,
|
|
863
|
+
level0SourceStoredBytes: 0,
|
|
864
|
+
anchorSourceStoredBytes: 0,
|
|
865
|
+
peakWorkingBytes: 0,
|
|
866
|
+
outputLogicalBytes: 0,
|
|
867
|
+
targetLevel: 1,
|
|
868
|
+
state: "planned",
|
|
869
|
+
transactionId: null,
|
|
870
|
+
outputSegmentId: null,
|
|
871
|
+
publishedVersion: null,
|
|
872
|
+
revision: 0,
|
|
873
|
+
createdAt: T0,
|
|
874
|
+
updatedAt: T0
|
|
875
|
+
});
|
|
876
|
+
await checkThrows(() => store.removeCompactionJob("job-1"), Error, "removing a nonterminal compaction job");
|
|
877
|
+
check((await store.getCompactionJob("job-1"))?.state === "planned", "a refused compaction-job removal mutated the record");
|
|
878
|
+
await checkThrows(() => store.updateCompactionJob("job-1", 9, {
|
|
879
|
+
state: "running",
|
|
880
|
+
transactionId: "txn-one",
|
|
881
|
+
updatedAt: LATER
|
|
882
|
+
}), CompactionJobConflictError, "updating a compaction job with a stale revision");
|
|
883
|
+
const updated = await store.updateCompactionJob("job-1", 0, { updatedAt: LATER });
|
|
884
|
+
check(updated.revision === 1, "a job update must advance the revision");
|
|
885
|
+
await store.cancelCompactionJob("job-1", updated.revision, LATER);
|
|
886
|
+
check(await store.removeCompactionJob("job-1"), "terminal job was not removed");
|
|
887
|
+
check(await store.getCompactionJob("job-1") === void 0, "terminal job was retained");
|
|
888
|
+
await store.createTransaction(transaction("txn-orphan-output", version));
|
|
889
|
+
await store.stageTransactionArtifacts({
|
|
890
|
+
transactionId: "txn-orphan-output",
|
|
891
|
+
expectedRevision: 0,
|
|
892
|
+
blocks: [{ id: "orphan-compaction-output", bytes: Uint8Array.of(9) }],
|
|
893
|
+
segments: [],
|
|
894
|
+
updatedAt: T0
|
|
895
|
+
});
|
|
896
|
+
await store.updateTransaction("txn-orphan-output", 1, {
|
|
897
|
+
status: "aborted",
|
|
898
|
+
updatedAt: LATER
|
|
899
|
+
});
|
|
900
|
+
await store.createCompactionJob({
|
|
901
|
+
id: "job-orphan-output",
|
|
902
|
+
tableId: "table-t",
|
|
903
|
+
sourceManifestVersion: version,
|
|
904
|
+
sourceSegmentIds: ["segment-one"],
|
|
905
|
+
sourceBlockIds: [blockId],
|
|
906
|
+
outputBlockIds: ["orphan-compaction-output"],
|
|
907
|
+
cursor: { sourceSegmentIndex: 1, sourceBlockIndex: 0 },
|
|
908
|
+
processedRows: 0,
|
|
909
|
+
sourceStoredBytes: 0,
|
|
910
|
+
outputStoredBytes: 1,
|
|
911
|
+
logicalBytes: 1,
|
|
912
|
+
rewritePlan: { kind: "copy-v1" },
|
|
913
|
+
outputCursor: null,
|
|
914
|
+
memoryBudgetBytes: 0,
|
|
915
|
+
minimumMemoryBytes: 0,
|
|
916
|
+
level0SourceStoredBytes: 0,
|
|
917
|
+
anchorSourceStoredBytes: 0,
|
|
918
|
+
peakWorkingBytes: 0,
|
|
919
|
+
outputLogicalBytes: 1,
|
|
920
|
+
targetLevel: 1,
|
|
921
|
+
state: "cancelled",
|
|
922
|
+
transactionId: null,
|
|
923
|
+
outputSegmentId: null,
|
|
924
|
+
publishedVersion: null,
|
|
925
|
+
revision: 0,
|
|
926
|
+
createdAt: T0,
|
|
927
|
+
updatedAt: LATER
|
|
928
|
+
});
|
|
929
|
+
check(await store.removeCompactionJob("job-orphan-output"), "terminal compaction metadata with independent transaction provenance was retained");
|
|
930
|
+
check(await store.getBlock("orphan-compaction-output") !== void 0, "removing terminal metadata deleted an independently proven output");
|
|
931
|
+
store.close();
|
|
932
|
+
}
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
name: "full-text bases serve exact and prefix candidates with honest coverage",
|
|
936
|
+
async run(target) {
|
|
937
|
+
const store = await target.create();
|
|
938
|
+
await store.addTable({
|
|
939
|
+
...table("t"),
|
|
940
|
+
ftsColumns: {
|
|
941
|
+
"col-v": {
|
|
942
|
+
storage: "fts-chunks-v1",
|
|
943
|
+
tokenizerVersion: 1,
|
|
944
|
+
state: "ready",
|
|
945
|
+
buildFromVersion: 0
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
await store.writeFtsBase("table-t", "col-v", {
|
|
950
|
+
coversVersion: 0,
|
|
951
|
+
chunks: [
|
|
952
|
+
[
|
|
953
|
+
{ term: "minnow", rowIds: [1n, 3n], tf: [1, 2] },
|
|
954
|
+
{ term: "minnows", rowIds: [2n], tf: [1] }
|
|
955
|
+
],
|
|
956
|
+
[{ term: "shark", rowIds: [4n], tf: [1] }]
|
|
957
|
+
],
|
|
958
|
+
totalTokens: 5
|
|
959
|
+
});
|
|
960
|
+
const exact = await store.readFtsCandidates("table-t", "col-v", [{ term: "minnow", prefix: false }], 10);
|
|
961
|
+
checkEqual(exact.rowIdsByTerm, [[1n, 3n]], "exact terms must match exactly");
|
|
962
|
+
check(exact.hasBase && exact.coversVersion === 0 && exact.totalTokens === 5, "coverage and token totals must report the stored base");
|
|
963
|
+
const prefix = await store.readFtsCandidates("table-t", "col-v", [{ term: "minnow", prefix: true }], 10);
|
|
964
|
+
checkEqual(prefix.rowIdsByTerm, [[1n, 2n, 3n]], "prefix terms must match the term range, row ids ascending and unique");
|
|
965
|
+
const ordered = await store.readFtsPostings("table-t", "col-v", 10);
|
|
966
|
+
checkEqual(ordered.postings, [
|
|
967
|
+
{ term: "minnow", rowIds: [1n, 3n], tf: [1, 2] },
|
|
968
|
+
{ term: "minnows", rowIds: [2n], tf: [1] },
|
|
969
|
+
{ term: "shark", rowIds: [4n], tf: [1] }
|
|
970
|
+
], "ordered postings must merge into canonical term and row-ID order");
|
|
971
|
+
const baseOnly = await store.readFtsPostings("table-t", "col-v", -1);
|
|
972
|
+
check(baseOnly.hasBase && baseOnly.postings.length === 3, "an upToVersion of -1 must serve the base view alone");
|
|
973
|
+
for (const version of [-2, Number.NaN, 1.5]) {
|
|
974
|
+
try {
|
|
975
|
+
await store.readFtsCandidates("table-t", "col-v", [{ term: "minnow", prefix: false }], version);
|
|
976
|
+
throw new Error(`candidate read accepted invalid version ${String(version)}`);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
check(error instanceof RangeError, "invalid candidate versions must throw RangeError");
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
await store.readFtsPostings("table-t", "col-v", version);
|
|
982
|
+
throw new Error(`postings read accepted invalid version ${String(version)}`);
|
|
983
|
+
} catch (error) {
|
|
984
|
+
check(error instanceof RangeError, "invalid postings versions must throw RangeError");
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
await store.readFtsCandidates("table-t", "col-v", [{ term: "minnow", prefix: false }], 10, 0);
|
|
989
|
+
throw new Error("candidate read accepted a zero row-ID limit");
|
|
990
|
+
} catch (error) {
|
|
991
|
+
check(error instanceof RangeError, "invalid candidate limits must throw RangeError");
|
|
992
|
+
}
|
|
993
|
+
store.close();
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
name: "postings builds publish bounded generations atomically and reclaim replacements",
|
|
998
|
+
async run(target) {
|
|
999
|
+
let store = await target.create();
|
|
1000
|
+
await store.addTable({
|
|
1001
|
+
...table("postings"),
|
|
1002
|
+
ftsColumns: {
|
|
1003
|
+
"col-v": {
|
|
1004
|
+
storage: "fts-chunks-v1",
|
|
1005
|
+
tokenizerVersion: 1,
|
|
1006
|
+
state: "building",
|
|
1007
|
+
buildFromVersion: -1
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
await store.beginFtsBaseBuild({
|
|
1012
|
+
tableId: "table-postings",
|
|
1013
|
+
columnId: "col-v",
|
|
1014
|
+
buildId: "abandoned",
|
|
1015
|
+
ownerId: "owner-abandoned",
|
|
1016
|
+
createdAt: "2099-01-01T00:00:00.000Z",
|
|
1017
|
+
expiresAt: "2099-01-01T00:10:00.000Z"
|
|
1018
|
+
});
|
|
1019
|
+
await store.writeFtsBaseBuildChunk({
|
|
1020
|
+
tableId: "table-postings",
|
|
1021
|
+
columnId: "col-v",
|
|
1022
|
+
buildId: "abandoned",
|
|
1023
|
+
ownerId: "owner-abandoned",
|
|
1024
|
+
expiresAtCutoff: "2099-01-01T00:00:01.000Z",
|
|
1025
|
+
expiresAt: "2099-01-01T00:10:00.000Z",
|
|
1026
|
+
updatedAt: "2099-01-01T00:00:01.000Z",
|
|
1027
|
+
ordinal: 0,
|
|
1028
|
+
chunk: [{ term: "old", rowIds: [1n], tf: [1] }]
|
|
1029
|
+
});
|
|
1030
|
+
await store.beginFtsBaseBuild({
|
|
1031
|
+
tableId: "table-postings",
|
|
1032
|
+
columnId: "col-v",
|
|
1033
|
+
buildId: "replacement",
|
|
1034
|
+
ownerId: "owner-replacement",
|
|
1035
|
+
createdAt: "2099-01-01T00:10:00.000Z",
|
|
1036
|
+
expiresAt: "2099-01-01T00:20:00.000Z"
|
|
1037
|
+
});
|
|
1038
|
+
const firstChunk = {
|
|
1039
|
+
tableId: "table-postings",
|
|
1040
|
+
columnId: "col-v",
|
|
1041
|
+
buildId: "replacement",
|
|
1042
|
+
ownerId: "owner-replacement",
|
|
1043
|
+
expiresAtCutoff: "2099-01-01T00:10:01.000Z",
|
|
1044
|
+
expiresAt: "2099-01-01T00:20:00.000Z",
|
|
1045
|
+
updatedAt: "2099-01-01T00:10:01.000Z",
|
|
1046
|
+
ordinal: 0,
|
|
1047
|
+
chunk: [{ term: "alpha", rowIds: [2n], tf: [1] }]
|
|
1048
|
+
};
|
|
1049
|
+
await store.writeFtsBaseBuildChunk(firstChunk);
|
|
1050
|
+
await store.writeFtsBaseBuildChunk(firstChunk);
|
|
1051
|
+
await checkThrows(() => store.writeFtsBaseBuildChunk({
|
|
1052
|
+
...firstChunk,
|
|
1053
|
+
chunk: [{ term: "alpha", rowIds: [999n], tf: [1] }]
|
|
1054
|
+
}), PostingBuildConflictError, "replaying an acknowledged postings ordinal with changed bytes");
|
|
1055
|
+
if (target.reopen !== void 0) {
|
|
1056
|
+
store = await target.reopen(store);
|
|
1057
|
+
await store.writeFtsBaseBuildChunk(firstChunk);
|
|
1058
|
+
}
|
|
1059
|
+
await store.writeFtsBaseBuildChunk({
|
|
1060
|
+
tableId: "table-postings",
|
|
1061
|
+
columnId: "col-v",
|
|
1062
|
+
buildId: "replacement",
|
|
1063
|
+
ownerId: "owner-replacement",
|
|
1064
|
+
expiresAtCutoff: "2099-01-01T00:10:02.000Z",
|
|
1065
|
+
expiresAt: "2099-01-01T00:20:00.000Z",
|
|
1066
|
+
updatedAt: "2099-01-01T00:10:02.000Z",
|
|
1067
|
+
ordinal: 1,
|
|
1068
|
+
chunk: [{ term: "omega", rowIds: [3n], tf: [1] }]
|
|
1069
|
+
});
|
|
1070
|
+
await store.finishFtsBaseBuild({
|
|
1071
|
+
tableId: "table-postings",
|
|
1072
|
+
columnId: "col-v",
|
|
1073
|
+
buildId: "replacement",
|
|
1074
|
+
ownerId: "owner-replacement",
|
|
1075
|
+
expiresAtCutoff: "2099-01-01T00:10:03.000Z",
|
|
1076
|
+
coversVersion: 4,
|
|
1077
|
+
chunkCount: 2,
|
|
1078
|
+
totalTokens: 2,
|
|
1079
|
+
completedAt: "2099-01-01T00:10:03.000Z"
|
|
1080
|
+
});
|
|
1081
|
+
let candidates = await store.readFtsCandidates("table-postings", "col-v", [
|
|
1082
|
+
{ term: "old", prefix: false },
|
|
1083
|
+
{ lower: "alpha", lowerInclusive: true, upper: "omega", upperInclusive: true }
|
|
1084
|
+
], 4);
|
|
1085
|
+
checkEqual(candidates.rowIdsByTerm, [[], [2n, 3n]], "only a finished replacement generation may become visible");
|
|
1086
|
+
check(candidates.hasBase, "a finished generation must report a published base");
|
|
1087
|
+
if (target.reopen !== void 0) {
|
|
1088
|
+
store = await target.reopen(store);
|
|
1089
|
+
candidates = await store.readFtsCandidates("table-postings", "col-v", [{ term: "omega", prefix: false }], 4);
|
|
1090
|
+
checkEqual(candidates.rowIdsByTerm, [[3n]], "a finished generation must survive reopen");
|
|
1091
|
+
check(candidates.hasBase, "a reopened generation must still report its base");
|
|
1092
|
+
}
|
|
1093
|
+
await checkThrows(() => store.removeFtsColumn("table-postings", "col-v"), Error, "removing postings still owned by a ready catalog accelerator");
|
|
1094
|
+
const indexed = await store.getTable("table-postings");
|
|
1095
|
+
check(indexed !== void 0, "the postings table disappeared");
|
|
1096
|
+
await store.updateTable(indexed.id, indexed.revision, {
|
|
1097
|
+
ftsColumns: {
|
|
1098
|
+
...indexed.ftsColumns,
|
|
1099
|
+
"col-v": {
|
|
1100
|
+
...indexed.ftsColumns?.["col-v"] ?? {
|
|
1101
|
+
storage: "fts-chunks-v1",
|
|
1102
|
+
tokenizerVersion: 1,
|
|
1103
|
+
buildFromVersion: -1
|
|
1104
|
+
},
|
|
1105
|
+
state: "invalid"
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
await store.removeFtsColumn("table-postings", "col-v");
|
|
1110
|
+
candidates = await store.readFtsCandidates("table-postings", "col-v", [{ term: "alpha", prefix: false }], 4);
|
|
1111
|
+
checkEqual(candidates.rowIdsByTerm, [[]], "removing an index must remove its candidates");
|
|
1112
|
+
check(!candidates.hasBase, "a removed base must be distinguishable from an empty base");
|
|
1113
|
+
store.close();
|
|
1114
|
+
}
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
name: "UNIQUE-index membership seeds and drops atomically with its catalog record",
|
|
1118
|
+
async run(target) {
|
|
1119
|
+
const store = await target.create();
|
|
1120
|
+
const record = table("unique-secondary");
|
|
1121
|
+
const indexId = "unique-v";
|
|
1122
|
+
const namespaceId = secondaryUniqueKeyNamespace(record.id, indexId);
|
|
1123
|
+
const building = {
|
|
1124
|
+
name: "unique_secondary_v",
|
|
1125
|
+
columnId: "col-v",
|
|
1126
|
+
columnIds: ["col-v"],
|
|
1127
|
+
directions: ["asc"],
|
|
1128
|
+
unique: true,
|
|
1129
|
+
termEncoding: "tuple-v1",
|
|
1130
|
+
storage: "postings-v1",
|
|
1131
|
+
storageColumnId: "secondary-index:unique-v",
|
|
1132
|
+
locator: "row-id",
|
|
1133
|
+
state: "building",
|
|
1134
|
+
buildId: "builder",
|
|
1135
|
+
buildFromVersion: -1
|
|
1136
|
+
};
|
|
1137
|
+
const { buildId: _buildId, ...withoutBuilder } = building;
|
|
1138
|
+
void _buildId;
|
|
1139
|
+
const ready = {
|
|
1140
|
+
...withoutBuilder,
|
|
1141
|
+
state: "ready",
|
|
1142
|
+
uniqueEnforced: true
|
|
1143
|
+
};
|
|
1144
|
+
await store.addTable({ ...record, secondaryIndexes: { [indexId]: building } });
|
|
1145
|
+
try {
|
|
1146
|
+
await store.updateTable(record.id, 0, {
|
|
1147
|
+
secondaryIndexes: { [indexId]: ready },
|
|
1148
|
+
expectedManifestVersion: { value: null },
|
|
1149
|
+
uniqueKeySeed: { namespaceId: `${namespaceId}-wrong`, keyTokens: ["x"] }
|
|
1150
|
+
});
|
|
1151
|
+
throw new Error("a seed for the wrong namespace was accepted");
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
check(error instanceof TypeError, "a mismatched UNIQUE seed must be rejected");
|
|
1154
|
+
}
|
|
1155
|
+
check((await store.getTable(record.id))?.revision === 0, "a mismatched seed must not mutate the catalog");
|
|
1156
|
+
await checkThrows(() => store.updateTable(record.id, 0, {
|
|
1157
|
+
secondaryIndexes: {
|
|
1158
|
+
[indexId]: ready
|
|
1159
|
+
},
|
|
1160
|
+
expectedManifestVersion: { value: null },
|
|
1161
|
+
uniqueKeySeed: { namespaceId, keyTokens: ["x", "x"] }
|
|
1162
|
+
}), UniqueKeyConflictError, "a duplicate UNIQUE seed");
|
|
1163
|
+
check((await store.getTable(record.id))?.secondaryIndexes?.[indexId]?.state === "building", "a refused seed must not publish the ready catalog state");
|
|
1164
|
+
await store.updateTable(record.id, 0, {
|
|
1165
|
+
secondaryIndexes: {
|
|
1166
|
+
[indexId]: ready
|
|
1167
|
+
},
|
|
1168
|
+
expectedManifestVersion: { value: null },
|
|
1169
|
+
uniqueKeySeed: { namespaceId, keyTokens: ["x", "y"] }
|
|
1170
|
+
});
|
|
1171
|
+
checkEqual(await store.getExistingUniqueKeys(namespaceId, ["x", "z"]), ["x"], "the ready catalog and membership must publish together");
|
|
1172
|
+
await store.updateTable(record.id, 1, { secondaryIndexes: null });
|
|
1173
|
+
checkEqual(await store.getExistingUniqueKeys(namespaceId, ["x", "y"]), [], "dropping the index must reclaim its membership namespace");
|
|
1174
|
+
store.close();
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
name: "atomic staging and rollback keep journals and artifacts inseparable",
|
|
1179
|
+
async run(target) {
|
|
1180
|
+
let store = await target.create();
|
|
1181
|
+
await store.addTable(table("t"));
|
|
1533
1182
|
{
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
id: "lease-m",
|
|
1547
|
-
kind: "reader",
|
|
1548
|
-
manifestVersion: first.version,
|
|
1549
|
-
ownerId: "owner-m",
|
|
1550
|
-
createdAt: T0,
|
|
1551
|
-
expiresAt: LATER,
|
|
1552
|
-
revision: 0,
|
|
1553
|
-
});
|
|
1554
|
-
await checkThrows(() => move({
|
|
1555
|
-
id: "lease-m",
|
|
1556
|
-
expectedRevision: 9,
|
|
1557
|
-
manifestVersion: second.version,
|
|
1558
|
-
expiresAtCutoff: T0,
|
|
1559
|
-
expiresAt: LATER,
|
|
1560
|
-
}), LeaseConflictError, "moving with a stale revision");
|
|
1561
|
-
await checkThrows(() => move({
|
|
1562
|
-
id: "lease-m",
|
|
1563
|
-
expectedRevision: 0,
|
|
1564
|
-
manifestVersion: 999,
|
|
1565
|
-
expiresAtCutoff: T0,
|
|
1566
|
-
expiresAt: LATER,
|
|
1567
|
-
}), SnapshotManifestMissingError, "moving to a version with no manifest");
|
|
1568
|
-
checkEqual(await store.getLease("lease-m"), {
|
|
1569
|
-
id: "lease-m",
|
|
1570
|
-
kind: "reader",
|
|
1571
|
-
manifestVersion: first.version,
|
|
1572
|
-
ownerId: "owner-m",
|
|
1573
|
-
createdAt: T0,
|
|
1574
|
-
expiresAt: LATER,
|
|
1575
|
-
revision: 0,
|
|
1576
|
-
}, "a refused move must leave the lease exactly as it was");
|
|
1577
|
-
const moved = await move({
|
|
1578
|
-
id: "lease-m",
|
|
1579
|
-
expectedRevision: 0,
|
|
1580
|
-
manifestVersion: second.version,
|
|
1581
|
-
expiresAtCutoff: T0,
|
|
1582
|
-
expiresAt: LATER,
|
|
1583
|
-
});
|
|
1584
|
-
checkEqual(moved, {
|
|
1585
|
-
id: "lease-m",
|
|
1586
|
-
kind: "reader",
|
|
1587
|
-
manifestVersion: second.version,
|
|
1588
|
-
ownerId: "owner-m",
|
|
1589
|
-
createdAt: T0,
|
|
1590
|
-
expiresAt: LATER,
|
|
1591
|
-
revision: 1,
|
|
1592
|
-
}, "a move must re-pin, renew, and advance the revision");
|
|
1593
|
-
checkEqual((await store.listLeases()).map((lease) => lease.id), ["lease-m"], "a move must keep the one record, not create a second");
|
|
1594
|
-
store.close();
|
|
1183
|
+
const begun = await store.beginTransaction({
|
|
1184
|
+
record: {
|
|
1185
|
+
id: "txn-b",
|
|
1186
|
+
ownerId: "txn-b/owner",
|
|
1187
|
+
expiresAt: LATER,
|
|
1188
|
+
pendingBlockIds: [],
|
|
1189
|
+
pendingSegmentIds: [],
|
|
1190
|
+
status: "active",
|
|
1191
|
+
revision: 0,
|
|
1192
|
+
startedAt: T0,
|
|
1193
|
+
updatedAt: T0,
|
|
1194
|
+
committedVersion: null
|
|
1595
1195
|
},
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1196
|
+
reserveRowIds: { tableId: "table-t", count: 4 }
|
|
1197
|
+
});
|
|
1198
|
+
check(begun.record.snapshotVersion === await store.getCurrentManifestVersion(), "beginTransaction must pin the current version");
|
|
1199
|
+
check(begun.rowIds !== void 0 && begun.rowIds.endExclusive - begun.rowIds.start === 4n, "beginTransaction must honor the reservation");
|
|
1200
|
+
}
|
|
1201
|
+
const stage = store.stageTransactionArtifacts.bind(store);
|
|
1202
|
+
await store.createTransaction(transaction("txn-s", await store.getCurrentManifestVersion()));
|
|
1203
|
+
const duplicate = segment("segment-duplicate", "table-t", "txn-s", "block-none");
|
|
1204
|
+
await checkThrows(() => stage({
|
|
1205
|
+
transactionId: "txn-s",
|
|
1206
|
+
expectedRevision: 0,
|
|
1207
|
+
blocks: [],
|
|
1208
|
+
segments: [duplicate, duplicate],
|
|
1209
|
+
updatedAt: LATER
|
|
1210
|
+
}), Error, "staging duplicate segment IDs");
|
|
1211
|
+
await checkThrows(() => stage({
|
|
1212
|
+
transactionId: "txn-s",
|
|
1213
|
+
expectedRevision: 0,
|
|
1214
|
+
blocks: [],
|
|
1215
|
+
segments: [segment("segment-foreign", "table-t", "another-txn", "block-none")],
|
|
1216
|
+
updatedAt: LATER
|
|
1217
|
+
}), Error, "staging a segment owned by another transaction");
|
|
1218
|
+
check(await store.getSegment("segment-duplicate") === void 0 && await store.getSegment("segment-foreign") === void 0 && (await store.getTransaction("txn-s"))?.revision === 0, "refused staging must leave the transaction and segment store unchanged");
|
|
1219
|
+
const first = await stage({
|
|
1220
|
+
transactionId: "txn-s",
|
|
1221
|
+
expectedRevision: 0,
|
|
1222
|
+
blocks: [{ id: "block-z", bytes: Uint8Array.of(7) }],
|
|
1223
|
+
segments: [segment("segment-z", "table-t", "txn-s", "block-z")],
|
|
1224
|
+
updatedAt: LATER
|
|
1225
|
+
});
|
|
1226
|
+
await checkThrows(() => stage({
|
|
1227
|
+
transactionId: "txn-s",
|
|
1228
|
+
expectedRevision: first.revision,
|
|
1229
|
+
blocks: [{ id: "block-gap", bytes: Uint8Array.of(9) }],
|
|
1230
|
+
segments: [segment("segment-gap", "table-t", "txn-s", "block-gap", 0)],
|
|
1231
|
+
updatedAt: LATER
|
|
1232
|
+
}), TypeError, "staging a segment whose commit ordinal does not continue the journal");
|
|
1233
|
+
check((await store.getTransaction("txn-s"))?.revision === first.revision && await store.getBlock("block-gap") === void 0 && await store.getSegment("segment-gap") === void 0, "an ordinal refusal must leave the record, block, and segment unchanged");
|
|
1234
|
+
const staged = await stage({
|
|
1235
|
+
transactionId: "txn-s",
|
|
1236
|
+
expectedRevision: first.revision,
|
|
1237
|
+
blocks: [{ id: "block-a", bytes: Uint8Array.of(8) }],
|
|
1238
|
+
segments: [segment("segment-a", "table-t", "txn-s", "block-a", 1)],
|
|
1239
|
+
updatedAt: LATER
|
|
1240
|
+
});
|
|
1241
|
+
checkEqual(staged.pendingBlockIds, ["block-z", "block-a"], "staging journals blocks");
|
|
1242
|
+
checkEqual(staged.pendingSegmentIds, ["segment-z", "segment-a"], "staging preserves the canonical segment journal order");
|
|
1243
|
+
check(await store.getBlock("block-a") !== void 0 && await store.getSegment("segment-a") !== void 0, "staging must write bytes and segments");
|
|
1244
|
+
const rollbackInput = {
|
|
1245
|
+
transactionId: "txn-s",
|
|
1246
|
+
expectedRevision: staged.revision,
|
|
1247
|
+
pendingBlockIds: ["block-z"],
|
|
1248
|
+
pendingSegmentIds: ["segment-z"],
|
|
1249
|
+
removeBlockIds: ["block-a"],
|
|
1250
|
+
removeSegmentIds: ["segment-a"],
|
|
1251
|
+
updatedAt: LATER
|
|
1252
|
+
};
|
|
1253
|
+
await checkThrows(() => store.rollbackTransactionArtifacts({
|
|
1254
|
+
...rollbackInput,
|
|
1255
|
+
expectedRevision: staged.revision + 1
|
|
1256
|
+
}), TransactionRecordConflictError, "rollback with a stale transaction revision");
|
|
1257
|
+
await checkThrows(() => store.rollbackTransactionArtifacts({
|
|
1258
|
+
...rollbackInput,
|
|
1259
|
+
removeBlockIds: []
|
|
1260
|
+
}), TypeError, "rollback whose lists do not exactly partition the journal");
|
|
1261
|
+
await checkThrows(() => store.rollbackTransactionArtifacts({
|
|
1262
|
+
...rollbackInput,
|
|
1263
|
+
pendingBlockIds: ["block-a"],
|
|
1264
|
+
pendingSegmentIds: ["segment-a"],
|
|
1265
|
+
removeBlockIds: ["block-z"],
|
|
1266
|
+
removeSegmentIds: ["segment-z"]
|
|
1267
|
+
}), TypeError, "rollback that retains a non-prefix journal partition");
|
|
1268
|
+
check(await store.getBlock("block-a") !== void 0 && await store.getSegment("segment-a") !== void 0, "a refused rollback must leave every artifact intact");
|
|
1269
|
+
const rolledBack = await store.rollbackTransactionArtifacts(rollbackInput);
|
|
1270
|
+
checkEqual(rolledBack.pendingBlockIds, ["block-z"], "rollback must retain checkpoint block");
|
|
1271
|
+
check(await store.getBlock("block-a") === void 0 && await store.getSegment("segment-a") === void 0 && await store.getBlock("block-z") !== void 0 && await store.getSegment("segment-z") !== void 0, "rollback must delete only its exact removed partition");
|
|
1272
|
+
if (target.reopen !== void 0) {
|
|
1273
|
+
const reopened = await target.reopen(store);
|
|
1274
|
+
checkEqual((await reopened.getTransaction("txn-s"))?.pendingBlockIds, ["block-z"], "rollback journal must survive reopen");
|
|
1275
|
+
check(await reopened.getBlock("block-a") === void 0 && await reopened.getBlock("block-z") !== void 0, "rollback artifact removal must survive reopen");
|
|
1276
|
+
store = reopened;
|
|
1277
|
+
}
|
|
1278
|
+
const before = await store.getCatalogProbe();
|
|
1279
|
+
await store.addTable(table("probe-check"));
|
|
1280
|
+
const after = await store.getCatalogProbe();
|
|
1281
|
+
check(after.catalogEpoch > before.catalogEpoch, "a catalog mutation must advance the probe's epoch");
|
|
1282
|
+
store.close();
|
|
1283
|
+
}
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
name: "the single-shot write, when present, commits atomically and refuses typed",
|
|
1287
|
+
async run(target) {
|
|
1288
|
+
const store = await target.create();
|
|
1289
|
+
const write = store.writeTransaction?.bind(store);
|
|
1290
|
+
if (write === void 0) {
|
|
1291
|
+
store.close();
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
await store.addTable(table("t", true));
|
|
1295
|
+
const schemaEpoch = (await store.getCatalogProbe()).schemaEpoch;
|
|
1296
|
+
const fresh = (id) => {
|
|
1297
|
+
const { snapshotVersion: _pinned, ...record } = transaction(id, null);
|
|
1298
|
+
void _pinned;
|
|
1299
|
+
return { ...record, schemaEpochGuard: schemaEpoch };
|
|
1300
|
+
};
|
|
1301
|
+
const first = await write({
|
|
1302
|
+
transaction: { record: fresh("txn-w1") },
|
|
1303
|
+
blocks: [{ id: "block-w1", bytes: Uint8Array.of(1) }],
|
|
1304
|
+
segments: [segment("segment-w1", "table-t", "txn-w1", "block-w1")],
|
|
1305
|
+
expectedManifestVersion: null,
|
|
1306
|
+
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1307
|
+
uniqueKeyChanges: [{ tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true }],
|
|
1308
|
+
committedAt: LATER
|
|
1309
|
+
});
|
|
1310
|
+
checkEqual(first.version, 0, "the first single-shot write must publish version 0");
|
|
1311
|
+
const committed = await store.getTransaction("txn-w1");
|
|
1312
|
+
check(committed?.status === "committed" && committed.committedVersion === 0 && committed.snapshotVersion === null, "the fresh record must end committed, pinned at the expected version");
|
|
1313
|
+
checkEqual(committed.pendingBlockIds, ["block-w1"], "the single-shot write must journal its blocks on the record");
|
|
1314
|
+
check(await store.getBlock("block-w1") !== void 0 && (await store.getSegment("segment-w1"))?.logicalOrder === 0, "the single-shot write must land bytes and finalized segments");
|
|
1315
|
+
checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1"]), ["number:1"], "the single-shot write must apply unique-key changes");
|
|
1316
|
+
await checkThrows(() => write({
|
|
1317
|
+
transaction: { record: fresh("txn-stale") },
|
|
1318
|
+
blocks: [{ id: "block-stale", bytes: Uint8Array.of(2) }],
|
|
1319
|
+
segments: [segment("segment-stale", "table-t", "txn-stale", "block-stale")],
|
|
1320
|
+
expectedManifestVersion: null,
|
|
1321
|
+
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1322
|
+
committedAt: LATER
|
|
1323
|
+
}), WriteConflictError, "a single-shot write against a moved manifest");
|
|
1324
|
+
check(await store.getTransaction("txn-stale") === void 0 && await store.getBlock("block-stale") === void 0 && await store.getSegment("segment-stale") === void 0, "a refused single-shot write must leave no record, block, or segment");
|
|
1325
|
+
await checkThrows(() => write({
|
|
1326
|
+
transaction: { record: fresh("txn-dup") },
|
|
1327
|
+
blocks: [{ id: "block-dup", bytes: Uint8Array.of(3) }],
|
|
1328
|
+
segments: [segment("segment-dup", "table-t", "txn-dup", "block-dup")],
|
|
1329
|
+
expectedManifestVersion: 0,
|
|
1330
|
+
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1331
|
+
uniqueKeyChanges: [
|
|
1332
|
+
{ tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true }
|
|
1333
|
+
],
|
|
1334
|
+
committedAt: LATER
|
|
1335
|
+
}), UniqueKeyConflictError, "a single-shot write inserting an existing unique key");
|
|
1336
|
+
check(await store.getTransaction("txn-dup") === void 0 && await store.getBlock("block-dup") === void 0, "a key conflict must leave no record or block");
|
|
1337
|
+
checkEqual(await store.getCurrentManifestVersion(), 0, "refusals must not advance");
|
|
1338
|
+
await store.createTransaction(transaction("txn-w2", 0));
|
|
1339
|
+
await checkThrows(() => write({
|
|
1340
|
+
transaction: { id: "txn-w2", expectedRevision: 4 },
|
|
1341
|
+
blocks: [{ id: "block-w2", bytes: Uint8Array.of(4) }],
|
|
1342
|
+
segments: [segment("segment-w2", "table-t", "txn-w2", "block-w2")],
|
|
1343
|
+
expectedManifestVersion: 0,
|
|
1344
|
+
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1345
|
+
committedAt: LATER
|
|
1346
|
+
}), TransactionRecordConflictError, "a single-shot write with a stale transaction revision");
|
|
1347
|
+
check(await store.getBlock("block-w2") === void 0, "a revision conflict must leave no block");
|
|
1348
|
+
const second = await write({
|
|
1349
|
+
transaction: { id: "txn-w2", expectedRevision: 0 },
|
|
1350
|
+
blocks: [{ id: "block-w2", bytes: Uint8Array.of(4) }],
|
|
1351
|
+
segments: [segment("segment-w2", "table-t", "txn-w2", "block-w2")],
|
|
1352
|
+
expectedManifestVersion: 0,
|
|
1353
|
+
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1354
|
+
changedTableIds: ["table-t"],
|
|
1355
|
+
committedAt: LATER
|
|
1356
|
+
});
|
|
1357
|
+
checkEqual(second.version, 1, "the continued write must publish the next version");
|
|
1358
|
+
checkEqual(second.changedTableIds, ["table-t"], "the summary must carry the change set");
|
|
1359
|
+
checkEqual(await manifestBlockIds(store, 1), ["block-w1", "block-w2"], "the continued write must preserve visible blocks and add its own");
|
|
1360
|
+
check((await store.getTransaction("txn-w2"))?.status === "committed", "the begun record must end committed");
|
|
1361
|
+
store.close();
|
|
1362
|
+
}
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
name: "compaction commit proves exact output provenance before publication",
|
|
1366
|
+
async run(target) {
|
|
1367
|
+
const store = await target.create();
|
|
1368
|
+
await store.addTable({
|
|
1369
|
+
id: "table-compact",
|
|
1370
|
+
name: "compact",
|
|
1371
|
+
columns: [{ id: "col-value", name: "value", type: "number", nullable: false }],
|
|
1372
|
+
managed: false,
|
|
1373
|
+
revision: 0,
|
|
1374
|
+
createdAt: T0
|
|
1375
|
+
});
|
|
1376
|
+
await store.createTransaction(transaction("txn-compact-source", null));
|
|
1377
|
+
const source = await store.stageTransactionArtifacts({
|
|
1378
|
+
transactionId: "txn-compact-source",
|
|
1379
|
+
expectedRevision: 0,
|
|
1380
|
+
blocks: [{ id: "compact-source-block", bytes: Uint8Array.of(1) }],
|
|
1381
|
+
segments: [
|
|
1382
|
+
{
|
|
1383
|
+
...segment("compact-source-segment", "table-compact", "txn-compact-source", "compact-source-block"),
|
|
1384
|
+
columnBlockIds: { "col-value": ["compact-source-block"] }
|
|
1385
|
+
}
|
|
1386
|
+
],
|
|
1387
|
+
updatedAt: T0
|
|
1388
|
+
});
|
|
1389
|
+
await store.commitTransaction({
|
|
1390
|
+
transactionId: source.id,
|
|
1391
|
+
expectedTransactionRevision: source.revision,
|
|
1392
|
+
expectedManifestVersion: null,
|
|
1393
|
+
levelZeroSegmentLimits: [{ tableId: "table-compact", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1394
|
+
committedAt: T0
|
|
1395
|
+
});
|
|
1396
|
+
const jobId = "compact-proof-job";
|
|
1397
|
+
const outputBlockId = `${jobId}/output/segment/000000/column/000000/part/000000`;
|
|
1398
|
+
await store.createTransaction(transaction("txn-compact-output", 0));
|
|
1399
|
+
const output = await store.stageTransactionArtifacts({
|
|
1400
|
+
transactionId: "txn-compact-output",
|
|
1401
|
+
expectedRevision: 0,
|
|
1402
|
+
blocks: [{ id: outputBlockId, bytes: Uint8Array.of(2) }],
|
|
1403
|
+
segments: [
|
|
1404
|
+
{
|
|
1405
|
+
...segment("compact-output-segment", "table-compact", "txn-compact-output", outputBlockId),
|
|
1406
|
+
columnBlockIds: { "col-value": [outputBlockId] },
|
|
1407
|
+
kind: "base",
|
|
1408
|
+
level: 1
|
|
1409
|
+
}
|
|
1410
|
+
],
|
|
1411
|
+
updatedAt: LATER
|
|
1412
|
+
});
|
|
1413
|
+
const job = {
|
|
1414
|
+
id: jobId,
|
|
1415
|
+
tableId: "table-compact",
|
|
1416
|
+
sourceManifestVersion: 0,
|
|
1417
|
+
sourceSegmentIds: ["compact-source-segment"],
|
|
1418
|
+
sourceBlockIds: ["compact-source-block"],
|
|
1419
|
+
outputBlockIds: [outputBlockId],
|
|
1420
|
+
cursor: { sourceSegmentIndex: 1, sourceBlockIndex: 0 },
|
|
1421
|
+
processedRows: 2,
|
|
1422
|
+
sourceStoredBytes: 1,
|
|
1423
|
+
outputStoredBytes: 1,
|
|
1424
|
+
logicalBytes: 1,
|
|
1425
|
+
rewritePlan: { kind: "copy-v1" },
|
|
1426
|
+
outputCursor: null,
|
|
1427
|
+
memoryBudgetBytes: 0,
|
|
1428
|
+
minimumMemoryBytes: 0,
|
|
1429
|
+
level0SourceStoredBytes: 1,
|
|
1430
|
+
anchorSourceStoredBytes: 0,
|
|
1431
|
+
peakWorkingBytes: 0,
|
|
1432
|
+
outputLogicalBytes: 1,
|
|
1433
|
+
targetLevel: 1,
|
|
1434
|
+
state: "ready",
|
|
1435
|
+
transactionId: output.id,
|
|
1436
|
+
outputSegmentId: "compact-output-segment",
|
|
1437
|
+
publishedVersion: null,
|
|
1438
|
+
revision: 0,
|
|
1439
|
+
createdAt: T0,
|
|
1440
|
+
updatedAt: LATER
|
|
1441
|
+
};
|
|
1442
|
+
await store.createCompactionJob(job);
|
|
1443
|
+
await checkThrows(() => store.commitTransaction({
|
|
1444
|
+
transactionId: output.id,
|
|
1445
|
+
expectedTransactionRevision: output.revision,
|
|
1446
|
+
expectedManifestVersion: 0,
|
|
1447
|
+
compactionJobId: job.id,
|
|
1448
|
+
removedBlockIds: ["compact-source-block"],
|
|
1449
|
+
committedAt: LATER
|
|
1450
|
+
}), Error, "a structurally valid compaction segment that differs from the immutable plan");
|
|
1451
|
+
checkEqual(await store.getCurrentManifestVersion(), 0, "a provenance refusal must not publish a manifest");
|
|
1452
|
+
check((await store.getTransaction(output.id))?.status === "active" && (await store.getSegment("compact-output-segment"))?.kind === "base" && await store.getBlock(outputBlockId) !== void 0 && (await store.getCompactionJob(job.id))?.state === "ready", "a provenance refusal must leave the transaction, artifacts, and job unchanged");
|
|
1453
|
+
checkEqual(await manifestBlockIds(store, 0), ["compact-source-block"], "a provenance refusal must retain every source block");
|
|
1454
|
+
store.close();
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
name: "the lease move, when present, re-pins in place and refuses typed",
|
|
1459
|
+
async run(target) {
|
|
1460
|
+
const store = await target.create();
|
|
1461
|
+
const move = store.moveLease?.bind(store);
|
|
1462
|
+
if (move === void 0) {
|
|
1463
|
+
store.close();
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
await store.addTable(table("t"));
|
|
1467
|
+
const first = await commitOne(store, "one");
|
|
1468
|
+
const second = await commitOne(store, "two");
|
|
1469
|
+
await store.createLease({
|
|
1470
|
+
id: "lease-m",
|
|
1471
|
+
kind: "reader",
|
|
1472
|
+
manifestVersion: first.version,
|
|
1473
|
+
ownerId: "owner-m",
|
|
1474
|
+
createdAt: T0,
|
|
1475
|
+
expiresAt: LATER,
|
|
1476
|
+
revision: 0
|
|
1477
|
+
});
|
|
1478
|
+
await checkThrows(() => move({
|
|
1479
|
+
id: "lease-m",
|
|
1480
|
+
expectedRevision: 9,
|
|
1481
|
+
manifestVersion: second.version,
|
|
1482
|
+
expiresAtCutoff: T0,
|
|
1483
|
+
expiresAt: LATER
|
|
1484
|
+
}), LeaseConflictError, "moving with a stale revision");
|
|
1485
|
+
await checkThrows(() => move({
|
|
1486
|
+
id: "lease-m",
|
|
1487
|
+
expectedRevision: 0,
|
|
1488
|
+
manifestVersion: 999,
|
|
1489
|
+
expiresAtCutoff: T0,
|
|
1490
|
+
expiresAt: LATER
|
|
1491
|
+
}), SnapshotManifestMissingError, "moving to a version with no manifest");
|
|
1492
|
+
checkEqual(await store.getLease("lease-m"), {
|
|
1493
|
+
id: "lease-m",
|
|
1494
|
+
kind: "reader",
|
|
1495
|
+
manifestVersion: first.version,
|
|
1496
|
+
ownerId: "owner-m",
|
|
1497
|
+
createdAt: T0,
|
|
1498
|
+
expiresAt: LATER,
|
|
1499
|
+
revision: 0
|
|
1500
|
+
}, "a refused move must leave the lease exactly as it was");
|
|
1501
|
+
const moved = await move({
|
|
1502
|
+
id: "lease-m",
|
|
1503
|
+
expectedRevision: 0,
|
|
1504
|
+
manifestVersion: second.version,
|
|
1505
|
+
expiresAtCutoff: T0,
|
|
1506
|
+
expiresAt: LATER
|
|
1507
|
+
});
|
|
1508
|
+
checkEqual(moved, {
|
|
1509
|
+
id: "lease-m",
|
|
1510
|
+
kind: "reader",
|
|
1511
|
+
manifestVersion: second.version,
|
|
1512
|
+
ownerId: "owner-m",
|
|
1513
|
+
createdAt: T0,
|
|
1514
|
+
expiresAt: LATER,
|
|
1515
|
+
revision: 1
|
|
1516
|
+
}, "a move must re-pin, renew, and advance the revision");
|
|
1517
|
+
checkEqual((await store.listLeases()).map((lease) => lease.id), ["lease-m"], "a move must keep the one record, not create a second");
|
|
1518
|
+
store.close();
|
|
1519
|
+
}
|
|
1520
|
+
},
|
|
1521
|
+
{
|
|
1522
|
+
name: "everything committed survives a reopen",
|
|
1523
|
+
async run(target) {
|
|
1524
|
+
if (target.reopen === void 0)
|
|
1525
|
+
return;
|
|
1526
|
+
let store = await target.create();
|
|
1527
|
+
await store.addTable(table("t", true));
|
|
1528
|
+
let { version, blockId } = await commitOne(store, "one", { keyTokens: ["number:1"] });
|
|
1529
|
+
const reserved = await store.reserveRowIds("table-t", 10);
|
|
1530
|
+
const write = store.writeTransaction?.bind(store);
|
|
1531
|
+
if (write !== void 0) {
|
|
1532
|
+
const { snapshotVersion: _pinned, ...record } = transaction("txn-single", null);
|
|
1533
|
+
void _pinned;
|
|
1534
|
+
const summary = await write({
|
|
1535
|
+
transaction: {
|
|
1536
|
+
record: {
|
|
1537
|
+
...record,
|
|
1538
|
+
schemaEpochGuard: (await store.getCatalogProbe()).schemaEpoch
|
|
1539
|
+
}
|
|
1660
1540
|
},
|
|
1661
|
-
|
|
1662
|
-
|
|
1541
|
+
blocks: [{ id: "block-single", bytes: Uint8Array.of(1, 2, 3, 4) }],
|
|
1542
|
+
segments: [segment("segment-single", "table-t", "txn-single", "block-single")],
|
|
1543
|
+
expectedManifestVersion: version,
|
|
1544
|
+
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1545
|
+
uniqueKeyChanges: [
|
|
1546
|
+
{ tableId: "table-t", keyTokens: ["number:1"], requireAbsent: false }
|
|
1547
|
+
],
|
|
1548
|
+
committedAt: LATER
|
|
1549
|
+
});
|
|
1550
|
+
version = summary.version;
|
|
1551
|
+
blockId = "block-single";
|
|
1552
|
+
}
|
|
1553
|
+
const move = store.moveLease?.bind(store);
|
|
1554
|
+
if (move !== void 0) {
|
|
1555
|
+
await store.createLease({
|
|
1556
|
+
id: "lease-durable",
|
|
1557
|
+
kind: "reader",
|
|
1558
|
+
manifestVersion: null,
|
|
1559
|
+
ownerId: "owner",
|
|
1560
|
+
createdAt: T0,
|
|
1561
|
+
expiresAt: LATER,
|
|
1562
|
+
revision: 0
|
|
1563
|
+
});
|
|
1564
|
+
await move({
|
|
1565
|
+
id: "lease-durable",
|
|
1566
|
+
expectedRevision: 0,
|
|
1567
|
+
manifestVersion: version,
|
|
1568
|
+
expiresAtCutoff: T0,
|
|
1569
|
+
expiresAt: LATER
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
store = await target.reopen(store);
|
|
1573
|
+
checkEqual(await store.getCurrentManifestVersion(), version, "the committed version must survive a reopen");
|
|
1574
|
+
if (move !== void 0) {
|
|
1575
|
+
checkEqual((await store.getLease("lease-durable"))?.manifestVersion, version, "a moved lease must survive a reopen at its new version");
|
|
1576
|
+
}
|
|
1577
|
+
checkEqual(await store.getBlock(blockId), Uint8Array.of(1, 2, 3, 4), "committed bytes must survive a reopen");
|
|
1578
|
+
checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1"]), ["number:1"], "unique-key membership must survive a reopen");
|
|
1579
|
+
check(await store.getTableByName("t") !== void 0, "the catalog must survive a reopen");
|
|
1580
|
+
const next = await store.reserveRowIds("table-t", 1);
|
|
1581
|
+
check(next.start >= reserved.endExclusive, "counters must continue past the reserved high-water mark after a reopen");
|
|
1582
|
+
store.close();
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
];
|
|
1663
1586
|
}
|
|
1587
|
+
export {
|
|
1588
|
+
blockStoreConformanceCases,
|
|
1589
|
+
runBlockStoreConformance
|
|
1590
|
+
};
|