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