@minnowdb/core 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -32
- 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 +2 -1
- package/dist/engine/artifact-cache.js +5 -1
- package/dist/engine/batch.d.ts +11 -3
- package/dist/engine/batch.js +17 -7
- 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 +24 -5
- package/dist/engine/catalog.js +34 -7
- package/dist/engine/client.d.ts +29 -7
- package/dist/engine/client.js +216 -40
- package/dist/engine/database.d.ts +224 -39
- package/dist/engine/database.js +7475 -1539
- package/dist/engine/defaults.d.ts +4 -8
- package/dist/engine/defaults.js +46 -22
- 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 +41 -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 -10
- package/dist/engine/index.js +12 -9
- 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 +504 -126
- 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 +262 -27
- package/dist/engine/query-api.d.ts +3 -0
- package/dist/engine/query-api.js +3 -0
- package/dist/engine/query-cache.d.ts +1 -2
- package/dist/engine/query-cache.js +7 -5
- package/dist/engine/query.d.ts +134 -29
- package/dist/engine/query.js +1668 -372
- package/dist/engine/result-wire.d.ts +0 -1
- package/dist/engine/result-wire.js +2 -2
- package/dist/engine/schema-wire.d.ts +6 -3
- package/dist/engine/schema-wire.js +40 -34
- package/dist/engine/schema.d.ts +156 -80
- package/dist/engine/schema.js +570 -108
- package/dist/engine/sort-keys.d.ts +4 -4
- package/dist/engine/sort-keys.js +44 -29
- package/dist/engine/sql-domains.d.ts +31 -0
- package/dist/engine/sql-domains.js +585 -0
- package/dist/engine/sql-driver.d.ts +19 -0
- package/dist/engine/sql-driver.js +1 -0
- package/dist/engine/sql-json.d.ts +7 -9
- package/dist/engine/sql-json.js +75 -15
- package/dist/engine/sql-semantics.d.ts +8 -3
- package/dist/engine/sql-semantics.js +458 -30
- package/dist/engine/typed-live.d.ts +67 -0
- package/dist/engine/typed-live.js +349 -0
- package/dist/engine/vector.d.ts +12 -2
- package/dist/engine/vector.js +635 -119
- package/dist/engine/worker-host.d.ts +9 -2
- package/dist/engine/worker-host.js +620 -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 +3 -4
- package/dist/plan/index.js +3 -4
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/storage/indexeddb.d.ts +99 -51
- package/dist/storage/indexeddb.js +13448 -2660
- package/dist/storage/memory.d.ts +64 -57
- package/dist/storage/memory.js +1042 -128
- 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 +409 -75
- package/dist/storage/opfs/leader.js +4621 -650
- package/dist/storage/opfs/rpc.d.ts +15 -3
- package/dist/storage/opfs/rpc.js +224 -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 +33 -99
- package/dist/storage/opfs/store.js +453 -364
- 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 -85
- package/dist/storage/snapshot.js +33 -265
- package/dist/storage/toolkit/extents.d.ts +41 -7
- package/dist/storage/toolkit/extents.js +402 -39
- package/dist/storage/toolkit/index.d.ts +4 -5
- package/dist/storage/toolkit/index.js +28 -4
- package/dist/storage/toolkit/record-core.d.ts +182 -55
- package/dist/storage/toolkit/record-core.js +6158 -1331
- 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 +15 -2
- package/dist/storage/toolkit/wire.js +199 -10
- package/dist/storage/types.d.ts +1439 -204
- package/dist/storage/types.js +1745 -141
- package/dist/testing/block-store-conformance.d.ts +0 -1
- package/dist/testing/block-store-conformance.js +1009 -119
- package/dist/testing/index.d.ts +46 -26
- package/dist/testing/index.js +112 -57
- package/dist/testing/opfs-shim.d.ts +13 -3
- package/dist/testing/opfs-shim.js +40 -6
- package/dist/testing/simulator.d.ts +97 -0
- package/dist/testing/simulator.js +591 -0
- package/dist/testing/sqllogictest.d.ts +89 -0
- package/dist/testing/sqllogictest.js +434 -0
- package/dist/transactions/index.d.ts +92 -16
- package/dist/transactions/index.js +1074 -236
- package/dist/worker-protocol/index.d.ts +3 -2
- package/dist/worker-protocol/index.js +2 -5
- package/package.json +53 -3
- package/postgres-feature-profile.json +223 -0
- package/sql-feature-matrix.json +172 -252
- 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-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/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, 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
|
},
|
|
@@ -216,6 +379,38 @@ export function blockStoreConformanceCases() {
|
|
|
216
379
|
store.close();
|
|
217
380
|
},
|
|
218
381
|
},
|
|
382
|
+
{
|
|
383
|
+
name: "secondary-index metadata must describe one direction per indexed column",
|
|
384
|
+
async run(target) {
|
|
385
|
+
const store = await target.create();
|
|
386
|
+
const record = table("bad-secondary");
|
|
387
|
+
try {
|
|
388
|
+
await store.addTable({
|
|
389
|
+
...record,
|
|
390
|
+
secondaryIndexes: {
|
|
391
|
+
bad: {
|
|
392
|
+
name: "bad_direction_arity_index",
|
|
393
|
+
columnId: "col-v",
|
|
394
|
+
columnIds: ["col-v"],
|
|
395
|
+
directions: ["asc", "desc"],
|
|
396
|
+
termEncoding: "tuple-v1",
|
|
397
|
+
storage: "postings-v1",
|
|
398
|
+
storageColumnId: "secondary-index:bad",
|
|
399
|
+
locator: "row-id",
|
|
400
|
+
state: "ready",
|
|
401
|
+
buildFromVersion: 0,
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
throw new Error("secondary-index metadata with mismatched direction arity was accepted");
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
check(error instanceof TypeError, "invalid secondary-index metadata must be rejected");
|
|
409
|
+
}
|
|
410
|
+
checkEqual(await store.listTables(), [], "a rejected catalog record must leave no table");
|
|
411
|
+
store.close();
|
|
412
|
+
},
|
|
413
|
+
},
|
|
219
414
|
{
|
|
220
415
|
name: "removing a table removes everything keyed to it",
|
|
221
416
|
async run(target) {
|
|
@@ -226,19 +421,144 @@ export function blockStoreConformanceCases() {
|
|
|
226
421
|
tableId: "table-t",
|
|
227
422
|
});
|
|
228
423
|
await store.reserveRowIds("table-t", 10);
|
|
229
|
-
await store.
|
|
230
|
-
|
|
424
|
+
const dropped = await store.dropTable({
|
|
425
|
+
tableId: "table-t",
|
|
426
|
+
expectedTableRevision: 0,
|
|
427
|
+
expectedManifestVersion: 0,
|
|
428
|
+
expectedCatalogEpoch: (await store.getCatalogProbe()).catalogEpoch,
|
|
429
|
+
committedAt: LATER,
|
|
430
|
+
});
|
|
431
|
+
check(dropped.version === 1, "dropTable must publish one successor manifest");
|
|
432
|
+
checkEqual(await currentManifestBlockIds(store), [], "dropTable must retire the table's manifest blocks");
|
|
433
|
+
checkEqual(await tableSegments(store, "table-t"), [], "segments must go with the table");
|
|
231
434
|
checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1"]), [], "unique keys must go with the table");
|
|
435
|
+
await store.addTable(table("t", true));
|
|
232
436
|
const range = await store.reserveRowIds("table-t", 1);
|
|
233
437
|
check(range.start === 1n, "row-id counters must reset with the table");
|
|
234
438
|
check((await store.getBlock(blockId)) !== undefined, "blocks are not the catalog's to delete");
|
|
235
439
|
store.close();
|
|
236
440
|
},
|
|
237
441
|
},
|
|
442
|
+
{
|
|
443
|
+
name: "fused table drop rejects without change and resolves without partial state",
|
|
444
|
+
async run(target) {
|
|
445
|
+
const store = await target.create();
|
|
446
|
+
await store.addTable(table("t"));
|
|
447
|
+
const source = await commitOne(store, "source");
|
|
448
|
+
const catalogEpoch = (await store.getCatalogProbe()).catalogEpoch;
|
|
449
|
+
const drop = () => store.dropTable({
|
|
450
|
+
tableId: "table-t",
|
|
451
|
+
expectedTableRevision: 0,
|
|
452
|
+
expectedManifestVersion: source.version,
|
|
453
|
+
expectedCatalogEpoch: catalogEpoch,
|
|
454
|
+
committedAt: LATER,
|
|
455
|
+
});
|
|
456
|
+
await checkThrows(() => store.dropTable({
|
|
457
|
+
tableId: "table-t",
|
|
458
|
+
expectedTableRevision: 9,
|
|
459
|
+
expectedManifestVersion: source.version,
|
|
460
|
+
expectedCatalogEpoch: catalogEpoch,
|
|
461
|
+
committedAt: LATER,
|
|
462
|
+
}), TableRecordConflictError, "dropping with a stale table revision");
|
|
463
|
+
await checkThrows(() => store.dropTable({
|
|
464
|
+
tableId: "table-t",
|
|
465
|
+
expectedTableRevision: 0,
|
|
466
|
+
expectedManifestVersion: source.version - 1,
|
|
467
|
+
expectedCatalogEpoch: catalogEpoch,
|
|
468
|
+
committedAt: LATER,
|
|
469
|
+
}), WriteConflictError, "dropping with a stale manifest revision");
|
|
470
|
+
check((await store.getTable("table-t")) !== undefined &&
|
|
471
|
+
(await store.getCurrentManifestVersion()) === source.version, "a fused drop CAS refusal changed the catalog or manifest");
|
|
472
|
+
const active = transaction("txn-active-table", source.version);
|
|
473
|
+
await store.createTransaction(active);
|
|
474
|
+
const activeBlockId = "table/t/segment/active/part/000000";
|
|
475
|
+
const activeSegment = segment("segment-active-table", "table-t", active.id, activeBlockId);
|
|
476
|
+
const staged = await store.stageTransactionArtifacts({
|
|
477
|
+
transactionId: active.id,
|
|
478
|
+
expectedRevision: 0,
|
|
479
|
+
blocks: [{ id: activeBlockId, bytes: Uint8Array.of(9, 8, 7, 6) }],
|
|
480
|
+
segments: [activeSegment],
|
|
481
|
+
updatedAt: LATER,
|
|
482
|
+
});
|
|
483
|
+
const activeRefusal = await checkThrows(drop, TableInUseError, "dropping a table with an active transaction");
|
|
484
|
+
checkEqual({
|
|
485
|
+
tableId: activeRefusal.tableId,
|
|
486
|
+
ownerKind: activeRefusal.ownerKind,
|
|
487
|
+
ownerId: activeRefusal.ownerId,
|
|
488
|
+
}, { tableId: "table-t", ownerKind: "transaction", ownerId: active.id }, "an active-transaction refusal must identify its exact owner");
|
|
489
|
+
check((await store.getTable("table-t")) !== undefined &&
|
|
490
|
+
(await store.getSegment(activeSegment.id)) !== undefined &&
|
|
491
|
+
(await store.getBlock(activeBlockId)) !== undefined &&
|
|
492
|
+
(await store.getCurrentManifestVersion()) === source.version, "a refused table drop must leave catalog, manifest, and active artifacts unchanged");
|
|
493
|
+
await store.updateTransaction(active.id, staged.revision, {
|
|
494
|
+
status: "aborted",
|
|
495
|
+
updatedAt: LATER,
|
|
496
|
+
});
|
|
497
|
+
await store.createCompactionJob({
|
|
498
|
+
id: "job-table-in-use",
|
|
499
|
+
tableId: "table-t",
|
|
500
|
+
sourceManifestVersion: source.version,
|
|
501
|
+
sourceSegmentIds: ["segment-source"],
|
|
502
|
+
sourceBlockIds: [source.blockId],
|
|
503
|
+
outputBlockIds: [],
|
|
504
|
+
cursor: { sourceSegmentIndex: 0, sourceBlockIndex: 0 },
|
|
505
|
+
processedRows: 0,
|
|
506
|
+
sourceStoredBytes: 0,
|
|
507
|
+
outputStoredBytes: 0,
|
|
508
|
+
logicalBytes: 0,
|
|
509
|
+
rewritePlan: { kind: "copy-v1" },
|
|
510
|
+
outputCursor: null,
|
|
511
|
+
memoryBudgetBytes: 0,
|
|
512
|
+
minimumMemoryBytes: 0,
|
|
513
|
+
level0SourceStoredBytes: 0,
|
|
514
|
+
anchorSourceStoredBytes: 0,
|
|
515
|
+
peakWorkingBytes: 0,
|
|
516
|
+
outputLogicalBytes: 0,
|
|
517
|
+
targetLevel: 1,
|
|
518
|
+
state: "planned",
|
|
519
|
+
transactionId: null,
|
|
520
|
+
outputSegmentId: null,
|
|
521
|
+
publishedVersion: null,
|
|
522
|
+
revision: 0,
|
|
523
|
+
createdAt: T0,
|
|
524
|
+
updatedAt: T0,
|
|
525
|
+
});
|
|
526
|
+
const jobRefusal = await checkThrows(drop, TableInUseError, "dropping a table with a nonterminal compaction job");
|
|
527
|
+
checkEqual({
|
|
528
|
+
tableId: jobRefusal.tableId,
|
|
529
|
+
ownerKind: jobRefusal.ownerKind,
|
|
530
|
+
ownerId: jobRefusal.ownerId,
|
|
531
|
+
}, {
|
|
532
|
+
tableId: "table-t",
|
|
533
|
+
ownerKind: "compaction job",
|
|
534
|
+
ownerId: "job-table-in-use",
|
|
535
|
+
}, "a compaction refusal must identify its exact owner");
|
|
536
|
+
check((await store.getTable("table-t")) !== undefined &&
|
|
537
|
+
(await store.getCurrentManifestVersion()) === source.version &&
|
|
538
|
+
(await store.hasManifestBlocks(source.version, [source.blockId]))[0] === true, "a compaction refusal changed the catalog or manifest");
|
|
539
|
+
await store.cancelCompactionJob("job-table-in-use", 0, LATER);
|
|
540
|
+
const published = await drop();
|
|
541
|
+
check(published.version === source.version + 1, "a fused table drop must publish exactly one successor manifest");
|
|
542
|
+
check((await store.getTable("table-t")) === undefined, "a terminal owner blocked removal");
|
|
543
|
+
checkEqual(await tableSegments(store, "table-t"), [], "a fused drop left table segments");
|
|
544
|
+
checkEqual(await currentManifestBlockIds(store), [], "a fused drop left table blocks live");
|
|
545
|
+
check((await store.getBlock(source.blockId)) !== undefined, "a fused drop physically deleted a block pinned readers may still need");
|
|
546
|
+
store.close();
|
|
547
|
+
},
|
|
548
|
+
},
|
|
238
549
|
{
|
|
239
550
|
name: "counter reservations never overlap, even issued concurrently",
|
|
240
551
|
async run(target) {
|
|
241
552
|
const store = await target.create();
|
|
553
|
+
const counterTable = table("t", true);
|
|
554
|
+
const counterColumn = counterTable.columns[0];
|
|
555
|
+
if (counterColumn === undefined)
|
|
556
|
+
throw new Error("counter fixture column is missing");
|
|
557
|
+
counterTable.columns[0] = {
|
|
558
|
+
...counterColumn,
|
|
559
|
+
defaultValue: { kind: "autoincrement" },
|
|
560
|
+
};
|
|
561
|
+
await store.addTable(counterTable);
|
|
242
562
|
const ranges = await Promise.all(Array.from({ length: 8 }, () => store.reserveRowIds("table-t", 5)));
|
|
243
563
|
const starts = new Set(ranges.map((range) => range.start));
|
|
244
564
|
check(starts.size === 8, "concurrent reservations returned overlapping ranges");
|
|
@@ -247,6 +567,96 @@ export function blockStoreConformanceCases() {
|
|
|
247
567
|
}
|
|
248
568
|
const bumped = await store.reserveAutoIncrement("table-t", "col-id", 3, 100n);
|
|
249
569
|
check(bumped.start >= 100n, "reserveAutoIncrement must honor atLeast");
|
|
570
|
+
const maximumExclusive = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
|
|
571
|
+
const { snapshotVersion: _invalidSnapshot, ...invalidRecord } = transaction("txn-invalid-counter", null);
|
|
572
|
+
void _invalidSnapshot;
|
|
573
|
+
await checkThrows(() => store.beginTransaction({
|
|
574
|
+
record: invalidRecord,
|
|
575
|
+
reserveAutoIncrement: {
|
|
576
|
+
tableId: "table-t",
|
|
577
|
+
columnId: "col-id",
|
|
578
|
+
count: 0,
|
|
579
|
+
atLeast: maximumExclusive + 1n,
|
|
580
|
+
},
|
|
581
|
+
}), RangeError, "an invalid fused auto-increment reservation");
|
|
582
|
+
check((await store.getTransaction(invalidRecord.id)) === undefined, "a refused fused reservation must not create its transaction");
|
|
583
|
+
const { snapshotVersion: _validSnapshot, ...validRecord } = transaction("txn-valid-counter", null);
|
|
584
|
+
void _validSnapshot;
|
|
585
|
+
const valid = await store.beginTransaction({
|
|
586
|
+
record: validRecord,
|
|
587
|
+
reserveAutoIncrement: {
|
|
588
|
+
tableId: "table-t",
|
|
589
|
+
columnId: "col-id",
|
|
590
|
+
count: 1,
|
|
591
|
+
atLeast: 150n,
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
check(valid.autoIncrementValues?.start === 150n &&
|
|
595
|
+
valid.autoIncrementValues.endExclusive === 151n, "a valid fused reservation after refusal must use the unchanged counter");
|
|
596
|
+
await store.updateTransaction(valid.record.id, valid.record.revision, {
|
|
597
|
+
status: "aborted",
|
|
598
|
+
updatedAt: LATER,
|
|
599
|
+
});
|
|
600
|
+
await checkThrows(() => store.reserveAutoIncrement("table-t", "col-id", 0, maximumExclusive + 1n), RangeError, "an auto-increment bump outside the safe-integer range");
|
|
601
|
+
const afterRefusal = await store.reserveAutoIncrement("table-t", "col-id", 1, 200n);
|
|
602
|
+
check(afterRefusal.start === 200n && afterRefusal.endExclusive === 201n, "a refused auto-increment bump must not mutate the counter");
|
|
603
|
+
const boundary = await store.reserveAutoIncrement("table-t", "col-id", 1, maximumExclusive - 1n);
|
|
604
|
+
check(boundary.endExclusive === maximumExclusive, "the final safe auto-increment value must remain reservable");
|
|
605
|
+
const exhausted = await store.reserveAutoIncrement("table-t", "col-id", 0, maximumExclusive);
|
|
606
|
+
check(exhausted.start === maximumExclusive && exhausted.endExclusive === maximumExclusive, "a zero-count bump to the exclusive boundary must be representable");
|
|
607
|
+
await checkThrows(() => store.reserveAutoIncrement("table-t", "col-id", 1), RangeError, "an auto-increment range crossing the safe-integer boundary");
|
|
608
|
+
store.close();
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: "transaction ownership renewal races expiry abort atomically",
|
|
613
|
+
async run(target) {
|
|
614
|
+
const store = await target.create();
|
|
615
|
+
const record = transaction("txn-live", null);
|
|
616
|
+
record.expiresAt = MIDDLE;
|
|
617
|
+
await store.createTransaction(record);
|
|
618
|
+
check(!(await store.renewTransaction({
|
|
619
|
+
transactionId: record.id,
|
|
620
|
+
ownerId: record.ownerId,
|
|
621
|
+
expiresAtCutoff: MIDDLE,
|
|
622
|
+
expiresAt: LATER,
|
|
623
|
+
})), "ownership expired exactly at the cutoff must not be resurrected");
|
|
624
|
+
check((await store.getTransaction(record.id))?.expiresAt === MIDDLE, "a refused expiry-boundary renewal mutated the record");
|
|
625
|
+
check(!(await store.renewTransaction({
|
|
626
|
+
transactionId: record.id,
|
|
627
|
+
ownerId: "wrong-owner",
|
|
628
|
+
expiresAtCutoff: T0,
|
|
629
|
+
expiresAt: LATER,
|
|
630
|
+
})), "another owner must not renew a transaction");
|
|
631
|
+
check((await store.getTransaction(record.id))?.revision === 0, "ownership renewal must not contend on the data revision");
|
|
632
|
+
const [renewed, aborted] = await Promise.all([
|
|
633
|
+
store.renewTransaction({
|
|
634
|
+
transactionId: record.id,
|
|
635
|
+
ownerId: record.ownerId,
|
|
636
|
+
expiresAtCutoff: T0,
|
|
637
|
+
expiresAt: LATER,
|
|
638
|
+
}),
|
|
639
|
+
store.abortTransactionIfExpired({
|
|
640
|
+
transactionId: record.id,
|
|
641
|
+
expectedOwnerId: record.ownerId,
|
|
642
|
+
expiresAtCutoff: MIDDLE,
|
|
643
|
+
updatedAt: LATER,
|
|
644
|
+
}),
|
|
645
|
+
]);
|
|
646
|
+
const final = await store.getTransaction(record.id);
|
|
647
|
+
check(final !== undefined, "the liveness race lost its transaction record");
|
|
648
|
+
check((renewed && aborted === undefined && final.status === "active") ||
|
|
649
|
+
(!renewed && aborted?.status === "aborted" && final.status === "aborted"), "renewal and expiry abort must have one linearized winner");
|
|
650
|
+
if (final.status === "active") {
|
|
651
|
+
check(final.expiresAt === LATER, "the winning renewal did not extend ownership");
|
|
652
|
+
const expired = await store.abortTransactionIfExpired({
|
|
653
|
+
transactionId: final.id,
|
|
654
|
+
expectedOwnerId: final.ownerId,
|
|
655
|
+
expiresAtCutoff: "9999-12-31T23:59:59.999Z",
|
|
656
|
+
updatedAt: LATER,
|
|
657
|
+
});
|
|
658
|
+
check(expired?.status === "aborted", "an expired owner was not atomically aborted");
|
|
659
|
+
}
|
|
250
660
|
store.close();
|
|
251
661
|
},
|
|
252
662
|
},
|
|
@@ -260,7 +670,8 @@ export function blockStoreConformanceCases() {
|
|
|
260
670
|
});
|
|
261
671
|
checkEqual(await store.getCurrentManifestVersion(), version, "the version must advance");
|
|
262
672
|
const manifest = await store.getCurrentManifest();
|
|
263
|
-
check(manifest
|
|
673
|
+
check(manifest !== undefined &&
|
|
674
|
+
(await store.hasManifestBlocks(manifest.version, [blockId]))[0] === true, "the manifest must include the committed block");
|
|
264
675
|
const record = await store.getTransaction("txn-one");
|
|
265
676
|
check(record?.status === "committed" && record.committedVersion === version, "the transaction record must flip to committed with the published version");
|
|
266
677
|
checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1", "number:2"]), ["number:1"], "committed unique keys must be queryable");
|
|
@@ -302,9 +713,11 @@ export function blockStoreConformanceCases() {
|
|
|
302
713
|
await store.addTable(table("t", true));
|
|
303
714
|
const { version } = await commitOne(store, "one", { keyTokens: ["number:1"] });
|
|
304
715
|
await store.createTransaction(transaction("txn-dup", version));
|
|
305
|
-
await store.
|
|
306
|
-
|
|
307
|
-
|
|
716
|
+
await store.stageTransactionArtifacts({
|
|
717
|
+
transactionId: "txn-dup",
|
|
718
|
+
expectedRevision: 0,
|
|
719
|
+
blocks: [{ id: "block-dup", bytes: Uint8Array.of(9) }],
|
|
720
|
+
segments: [],
|
|
308
721
|
updatedAt: T0,
|
|
309
722
|
});
|
|
310
723
|
await checkThrows(() => store.commitTransaction({
|
|
@@ -320,32 +733,6 @@ export function blockStoreConformanceCases() {
|
|
|
320
733
|
store.close();
|
|
321
734
|
},
|
|
322
735
|
},
|
|
323
|
-
{
|
|
324
|
-
name: "publishManifest compare-and-swaps and refuses missing blocks",
|
|
325
|
-
async run(target) {
|
|
326
|
-
const store = await target.create();
|
|
327
|
-
await store.addBlock("block-a", Uint8Array.of(1));
|
|
328
|
-
await checkThrows(() => store.publishManifest({ expectedVersion: 3, blockIds: ["block-a"], createdAt: T0 }), WriteConflictError, "publishing against the wrong expected version");
|
|
329
|
-
try {
|
|
330
|
-
await store.publishManifest({
|
|
331
|
-
expectedVersion: null,
|
|
332
|
-
blockIds: ["missing"],
|
|
333
|
-
createdAt: T0,
|
|
334
|
-
});
|
|
335
|
-
throw new Error("a manifest referencing a missing block was published");
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
338
|
-
check(!(error instanceof WriteConflictError), "missing blocks are not a version conflict");
|
|
339
|
-
}
|
|
340
|
-
const manifest = await store.publishManifest({
|
|
341
|
-
expectedVersion: null,
|
|
342
|
-
blockIds: ["block-a"],
|
|
343
|
-
createdAt: T0,
|
|
344
|
-
});
|
|
345
|
-
check(manifest.version === 0, "the first published version must be 0");
|
|
346
|
-
store.close();
|
|
347
|
-
},
|
|
348
|
-
},
|
|
349
736
|
{
|
|
350
737
|
name: "leases pin versions, renew by CAS, and expire honestly",
|
|
351
738
|
async run(target) {
|
|
@@ -357,14 +744,47 @@ export function blockStoreConformanceCases() {
|
|
|
357
744
|
kind: "reader",
|
|
358
745
|
manifestVersion: version,
|
|
359
746
|
ownerId: "owner-a",
|
|
360
|
-
|
|
747
|
+
createdAt: T0,
|
|
748
|
+
expiresAt: LATER,
|
|
361
749
|
revision: 0,
|
|
362
750
|
});
|
|
363
|
-
await checkThrows(() => store.renewLease(
|
|
364
|
-
|
|
751
|
+
await checkThrows(() => store.renewLease({
|
|
752
|
+
id: "lease-a",
|
|
753
|
+
expectedRevision: 9,
|
|
754
|
+
expiresAtCutoff: T0,
|
|
755
|
+
expiresAt: LATER,
|
|
756
|
+
}), LeaseConflictError, "renewing with a stale revision");
|
|
757
|
+
const renewed = await store.renewLease({
|
|
758
|
+
id: "lease-a",
|
|
759
|
+
expectedRevision: 0,
|
|
760
|
+
expiresAtCutoff: T0,
|
|
761
|
+
expiresAt: LATER,
|
|
762
|
+
});
|
|
365
763
|
check(renewed.revision === 1, "a renewal must advance the revision");
|
|
764
|
+
await checkThrows(() => store.removeLease({ id: "lease-a", ownerId: "wrong-owner" }), LeaseOwnerConflictError, "releasing another owner's lease");
|
|
765
|
+
check((await store.getLease("lease-a"))?.revision === 1, "a refused lease release mutated the record");
|
|
366
766
|
checkEqual(await store.removeLeaseIfExpired("lease-a", 1, PAST), false, "an unexpired lease must not be removed");
|
|
367
767
|
checkEqual(await store.removeLeaseIfExpired("lease-a", 1, "3000-01-01T00:00:00.000Z"), true, "an expired lease must be removed");
|
|
768
|
+
await store.createLease({
|
|
769
|
+
id: "lease-race",
|
|
770
|
+
kind: "reader",
|
|
771
|
+
manifestVersion: version,
|
|
772
|
+
ownerId: "owner-a",
|
|
773
|
+
createdAt: T0,
|
|
774
|
+
expiresAt: LATER,
|
|
775
|
+
revision: 0,
|
|
776
|
+
});
|
|
777
|
+
const [, release] = await Promise.allSettled([
|
|
778
|
+
store.renewLease({
|
|
779
|
+
id: "lease-race",
|
|
780
|
+
expectedRevision: 0,
|
|
781
|
+
expiresAtCutoff: T0,
|
|
782
|
+
expiresAt: LATER,
|
|
783
|
+
}),
|
|
784
|
+
store.removeLease({ id: "lease-race", ownerId: "owner-a" }),
|
|
785
|
+
]);
|
|
786
|
+
check(release.status === "fulfilled", "a same-owner renewal made release unsafe");
|
|
787
|
+
check((await store.getLease("lease-race")) === undefined, "same-owner release did not linearize after/before renewal");
|
|
368
788
|
store.close();
|
|
369
789
|
},
|
|
370
790
|
},
|
|
@@ -374,31 +794,30 @@ export function blockStoreConformanceCases() {
|
|
|
374
794
|
const store = await target.create();
|
|
375
795
|
await store.addTable(table("t"));
|
|
376
796
|
const first = await commitOne(store, "one");
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
await store.addSegment(segment("segment-two", "table-t", transactionId, "block-two"));
|
|
382
|
-
await store.updateTransaction(transactionId, 0, {
|
|
383
|
-
pendingBlockIds: ["block-two"],
|
|
384
|
-
pendingSegmentIds: ["segment-two"],
|
|
385
|
-
updatedAt: T0,
|
|
386
|
-
});
|
|
387
|
-
await store.commitTransaction({
|
|
388
|
-
transactionId,
|
|
389
|
-
expectedTransactionRevision: 1,
|
|
797
|
+
// A fused table drop is the ordinary, intent-bound way to retire visible table bytes.
|
|
798
|
+
await store.dropTable({
|
|
799
|
+
tableId: "table-t",
|
|
800
|
+
expectedTableRevision: 0,
|
|
390
801
|
expectedManifestVersion: first.version,
|
|
391
|
-
|
|
802
|
+
expectedCatalogEpoch: (await store.getCatalogProbe()).catalogEpoch,
|
|
392
803
|
committedAt: LATER,
|
|
393
804
|
});
|
|
805
|
+
const retiredBeforeCollection = await store.listRetiredManifestBlockPage({
|
|
806
|
+
removedThroughVersion: first.version + 1,
|
|
807
|
+
afterBlockId: null,
|
|
808
|
+
limit: 1,
|
|
809
|
+
});
|
|
810
|
+
checkEqual(retiredBeforeCollection.records.map((record) => record.blockId), [first.blockId], "retired provenance must remain independently pageable before collection");
|
|
394
811
|
const job = await store.createGarbageCollectionJob({
|
|
395
812
|
id: "gc-1",
|
|
396
813
|
candidateManifestVersions: [first.version],
|
|
397
|
-
candidateSegmentIds: [
|
|
814
|
+
candidateSegmentIds: [],
|
|
398
815
|
candidateBlockIds: [first.blockId],
|
|
399
816
|
leaseCutoff: LATER,
|
|
400
817
|
createdAt: LATER,
|
|
401
818
|
});
|
|
819
|
+
await checkThrows(() => store.removeGarbageCollectionJob(job.id), Error, "removing a non-completed garbage-collection job");
|
|
820
|
+
check((await store.getGarbageCollectionJob(job.id))?.state === "planned", "a refused garbage-collection job removal mutated the record");
|
|
402
821
|
const step = await store.runGarbageCollectionStep({
|
|
403
822
|
jobId: job.id,
|
|
404
823
|
expectedRevision: job.revision,
|
|
@@ -408,9 +827,16 @@ export function blockStoreConformanceCases() {
|
|
|
408
827
|
checkEqual(step.prunedManifestVersions, [first.version], "the superseded version must prune");
|
|
409
828
|
checkEqual(step.reclaimedBlockIds, [first.blockId], "the superseded block must reclaim");
|
|
410
829
|
checkEqual(await store.getBlock(first.blockId), undefined, "reclaimed bytes must be gone");
|
|
411
|
-
|
|
830
|
+
checkEqual((await store.listRetiredManifestBlockPage({
|
|
831
|
+
removedThroughVersion: first.version + 1,
|
|
832
|
+
afterBlockId: null,
|
|
833
|
+
limit: 1,
|
|
834
|
+
})).records, [], "payload reclamation must atomically remove its retired provenance");
|
|
835
|
+
checkEqual(await store.getCurrentManifestVersion(), 1, "the successor manifest was lost");
|
|
412
836
|
const pruned = await store.getManifest(first.version);
|
|
413
837
|
check(pruned?.prunedAt !== undefined, "pruned manifests are tombstoned, not deleted");
|
|
838
|
+
await store.removeGarbageCollectionJob(job.id);
|
|
839
|
+
check((await store.getGarbageCollectionJob(job.id)) === undefined, "a completed garbage-collection job was retained");
|
|
414
840
|
store.close();
|
|
415
841
|
},
|
|
416
842
|
},
|
|
@@ -418,6 +844,12 @@ export function blockStoreConformanceCases() {
|
|
|
418
844
|
name: "temp spill pages are isolated scratch; owner records conflict by exact class",
|
|
419
845
|
async run(target) {
|
|
420
846
|
const store = await target.create();
|
|
847
|
+
await store.createTempOwner({
|
|
848
|
+
ownerId: "o1",
|
|
849
|
+
createdAt: T0,
|
|
850
|
+
expiresAt: LATER,
|
|
851
|
+
revision: 0,
|
|
852
|
+
});
|
|
421
853
|
await store.putTempRunPage({
|
|
422
854
|
ownerId: "o1",
|
|
423
855
|
runId: "r1",
|
|
@@ -434,8 +866,12 @@ export function blockStoreConformanceCases() {
|
|
|
434
866
|
}
|
|
435
867
|
await store.removeTempRun("o1", "r1");
|
|
436
868
|
checkEqual(await store.getTempRunPage("o1", "r1", 0), undefined, "a removed run must be gone");
|
|
437
|
-
await store.
|
|
438
|
-
|
|
869
|
+
await checkThrows(() => store.renewTempOwner({
|
|
870
|
+
ownerId: "o1",
|
|
871
|
+
expectedRevision: 9,
|
|
872
|
+
expiresAtCutoff: T0,
|
|
873
|
+
expiresAt: LATER,
|
|
874
|
+
}), TempOwnerConflictError, "renewing a temp owner with a stale revision");
|
|
439
875
|
checkEqual(await store.removeTempOwnerIfExpired("o1", PAST), false, "an unexpired owner must survive the sweep");
|
|
440
876
|
const listed = await store.listTempOwnerIdsPage(null, 10);
|
|
441
877
|
check(listed.records.includes("o1"), "owners must be listable");
|
|
@@ -460,6 +896,14 @@ export function blockStoreConformanceCases() {
|
|
|
460
896
|
sourceStoredBytes: 0,
|
|
461
897
|
outputStoredBytes: 0,
|
|
462
898
|
logicalBytes: 0,
|
|
899
|
+
rewritePlan: { kind: "copy-v1" },
|
|
900
|
+
outputCursor: null,
|
|
901
|
+
memoryBudgetBytes: 0,
|
|
902
|
+
minimumMemoryBytes: 0,
|
|
903
|
+
level0SourceStoredBytes: 0,
|
|
904
|
+
anchorSourceStoredBytes: 0,
|
|
905
|
+
peakWorkingBytes: 0,
|
|
906
|
+
outputLogicalBytes: 0,
|
|
463
907
|
targetLevel: 1,
|
|
464
908
|
state: "planned",
|
|
465
909
|
transactionId: null,
|
|
@@ -469,17 +913,61 @@ export function blockStoreConformanceCases() {
|
|
|
469
913
|
createdAt: T0,
|
|
470
914
|
updatedAt: T0,
|
|
471
915
|
});
|
|
916
|
+
await checkThrows(() => store.removeCompactionJob("job-1"), Error, "removing a nonterminal compaction job");
|
|
917
|
+
check((await store.getCompactionJob("job-1"))?.state === "planned", "a refused compaction-job removal mutated the record");
|
|
472
918
|
await checkThrows(() => store.updateCompactionJob("job-1", 9, {
|
|
473
919
|
state: "running",
|
|
474
920
|
transactionId: "txn-one",
|
|
475
921
|
updatedAt: LATER,
|
|
476
922
|
}), CompactionJobConflictError, "updating a compaction job with a stale revision");
|
|
477
|
-
const updated = await store.updateCompactionJob("job-1", 0, {
|
|
478
|
-
|
|
479
|
-
|
|
923
|
+
const updated = await store.updateCompactionJob("job-1", 0, { updatedAt: LATER });
|
|
924
|
+
check(updated.revision === 1, "a job update must advance the revision");
|
|
925
|
+
await store.cancelCompactionJob("job-1", updated.revision, LATER);
|
|
926
|
+
check(await store.removeCompactionJob("job-1"), "terminal job was not removed");
|
|
927
|
+
check((await store.getCompactionJob("job-1")) === undefined, "terminal job was retained");
|
|
928
|
+
await store.createTransaction(transaction("txn-orphan-output", version));
|
|
929
|
+
await store.stageTransactionArtifacts({
|
|
930
|
+
transactionId: "txn-orphan-output",
|
|
931
|
+
expectedRevision: 0,
|
|
932
|
+
blocks: [{ id: "orphan-compaction-output", bytes: Uint8Array.of(9) }],
|
|
933
|
+
segments: [],
|
|
934
|
+
updatedAt: T0,
|
|
935
|
+
});
|
|
936
|
+
await store.updateTransaction("txn-orphan-output", 1, {
|
|
937
|
+
status: "aborted",
|
|
480
938
|
updatedAt: LATER,
|
|
481
939
|
});
|
|
482
|
-
|
|
940
|
+
await store.createCompactionJob({
|
|
941
|
+
id: "job-orphan-output",
|
|
942
|
+
tableId: "table-t",
|
|
943
|
+
sourceManifestVersion: version,
|
|
944
|
+
sourceSegmentIds: ["segment-one"],
|
|
945
|
+
sourceBlockIds: [blockId],
|
|
946
|
+
outputBlockIds: ["orphan-compaction-output"],
|
|
947
|
+
cursor: { sourceSegmentIndex: 1, sourceBlockIndex: 0 },
|
|
948
|
+
processedRows: 0,
|
|
949
|
+
sourceStoredBytes: 0,
|
|
950
|
+
outputStoredBytes: 1,
|
|
951
|
+
logicalBytes: 1,
|
|
952
|
+
rewritePlan: { kind: "copy-v1" },
|
|
953
|
+
outputCursor: null,
|
|
954
|
+
memoryBudgetBytes: 0,
|
|
955
|
+
minimumMemoryBytes: 0,
|
|
956
|
+
level0SourceStoredBytes: 0,
|
|
957
|
+
anchorSourceStoredBytes: 0,
|
|
958
|
+
peakWorkingBytes: 0,
|
|
959
|
+
outputLogicalBytes: 1,
|
|
960
|
+
targetLevel: 1,
|
|
961
|
+
state: "cancelled",
|
|
962
|
+
transactionId: null,
|
|
963
|
+
outputSegmentId: null,
|
|
964
|
+
publishedVersion: null,
|
|
965
|
+
revision: 0,
|
|
966
|
+
createdAt: T0,
|
|
967
|
+
updatedAt: LATER,
|
|
968
|
+
});
|
|
969
|
+
check(await store.removeCompactionJob("job-orphan-output"), "terminal compaction metadata with independent transaction provenance was retained");
|
|
970
|
+
check((await store.getBlock("orphan-compaction-output")) !== undefined, "removing terminal metadata deleted an independently proven output");
|
|
483
971
|
store.close();
|
|
484
972
|
},
|
|
485
973
|
},
|
|
@@ -487,6 +975,17 @@ export function blockStoreConformanceCases() {
|
|
|
487
975
|
name: "full-text bases serve exact and prefix candidates with honest coverage",
|
|
488
976
|
async run(target) {
|
|
489
977
|
const store = await target.create();
|
|
978
|
+
await store.addTable({
|
|
979
|
+
...table("t"),
|
|
980
|
+
ftsColumns: {
|
|
981
|
+
"col-v": {
|
|
982
|
+
storage: "fts-chunks-v1",
|
|
983
|
+
tokenizerVersion: 1,
|
|
984
|
+
state: "ready",
|
|
985
|
+
buildFromVersion: 0,
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
});
|
|
490
989
|
await store.writeFtsBase("table-t", "col-v", {
|
|
491
990
|
coversVersion: 0,
|
|
492
991
|
chunks: [
|
|
@@ -500,22 +999,215 @@ export function blockStoreConformanceCases() {
|
|
|
500
999
|
});
|
|
501
1000
|
const exact = await store.readFtsCandidates("table-t", "col-v", [{ term: "minnow", prefix: false }], 10);
|
|
502
1001
|
checkEqual(exact.rowIdsByTerm, [[1n, 3n]], "exact terms must match exactly");
|
|
503
|
-
check(exact.coversVersion === 0 && exact.totalTokens === 5, "coverage and token totals must report the stored base");
|
|
1002
|
+
check(exact.hasBase && exact.coversVersion === 0 && exact.totalTokens === 5, "coverage and token totals must report the stored base");
|
|
504
1003
|
const prefix = await store.readFtsCandidates("table-t", "col-v", [{ term: "minnow", prefix: true }], 10);
|
|
505
1004
|
checkEqual(prefix.rowIdsByTerm, [[1n, 2n, 3n]], "prefix terms must match the term range, row ids ascending and unique");
|
|
1005
|
+
const ordered = await store.readFtsPostings("table-t", "col-v", 10);
|
|
1006
|
+
checkEqual(ordered.postings, [
|
|
1007
|
+
{ term: "minnow", rowIds: [1n, 3n], tf: [1, 2] },
|
|
1008
|
+
{ term: "minnows", rowIds: [2n], tf: [1] },
|
|
1009
|
+
{ term: "shark", rowIds: [4n], tf: [1] },
|
|
1010
|
+
], "ordered postings must merge into canonical term and row-ID order");
|
|
506
1011
|
store.close();
|
|
507
1012
|
},
|
|
508
1013
|
},
|
|
509
1014
|
{
|
|
510
|
-
name: "
|
|
1015
|
+
name: "postings builds publish bounded generations atomically and reclaim replacements",
|
|
1016
|
+
async run(target) {
|
|
1017
|
+
let store = await target.create();
|
|
1018
|
+
await store.addTable({
|
|
1019
|
+
...table("postings"),
|
|
1020
|
+
ftsColumns: {
|
|
1021
|
+
"col-v": {
|
|
1022
|
+
storage: "fts-chunks-v1",
|
|
1023
|
+
tokenizerVersion: 1,
|
|
1024
|
+
state: "building",
|
|
1025
|
+
buildFromVersion: -1,
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
});
|
|
1029
|
+
await store.beginFtsBaseBuild({
|
|
1030
|
+
tableId: "table-postings",
|
|
1031
|
+
columnId: "col-v",
|
|
1032
|
+
buildId: "abandoned",
|
|
1033
|
+
ownerId: "owner-abandoned",
|
|
1034
|
+
createdAt: "2099-01-01T00:00:00.000Z",
|
|
1035
|
+
expiresAt: "2099-01-01T00:10:00.000Z",
|
|
1036
|
+
});
|
|
1037
|
+
await store.writeFtsBaseBuildChunk({
|
|
1038
|
+
tableId: "table-postings",
|
|
1039
|
+
columnId: "col-v",
|
|
1040
|
+
buildId: "abandoned",
|
|
1041
|
+
ownerId: "owner-abandoned",
|
|
1042
|
+
expiresAtCutoff: "2099-01-01T00:00:01.000Z",
|
|
1043
|
+
expiresAt: "2099-01-01T00:10:00.000Z",
|
|
1044
|
+
updatedAt: "2099-01-01T00:00:01.000Z",
|
|
1045
|
+
ordinal: 0,
|
|
1046
|
+
chunk: [{ term: "old", rowIds: [1n], tf: [1] }],
|
|
1047
|
+
});
|
|
1048
|
+
await store.beginFtsBaseBuild({
|
|
1049
|
+
tableId: "table-postings",
|
|
1050
|
+
columnId: "col-v",
|
|
1051
|
+
buildId: "replacement",
|
|
1052
|
+
ownerId: "owner-replacement",
|
|
1053
|
+
createdAt: "2099-01-01T00:10:00.000Z",
|
|
1054
|
+
expiresAt: "2099-01-01T00:20:00.000Z",
|
|
1055
|
+
});
|
|
1056
|
+
const firstChunk = {
|
|
1057
|
+
tableId: "table-postings",
|
|
1058
|
+
columnId: "col-v",
|
|
1059
|
+
buildId: "replacement",
|
|
1060
|
+
ownerId: "owner-replacement",
|
|
1061
|
+
expiresAtCutoff: "2099-01-01T00:10:01.000Z",
|
|
1062
|
+
expiresAt: "2099-01-01T00:20:00.000Z",
|
|
1063
|
+
updatedAt: "2099-01-01T00:10:01.000Z",
|
|
1064
|
+
ordinal: 0,
|
|
1065
|
+
chunk: [{ term: "alpha", rowIds: [2n], tf: [1] }],
|
|
1066
|
+
};
|
|
1067
|
+
await store.writeFtsBaseBuildChunk(firstChunk);
|
|
1068
|
+
// Lost-ack replay is idempotent: retrying the identical chunk neither duplicates it nor
|
|
1069
|
+
// advances the ordinal, while changing acknowledged bytes is an ownership conflict.
|
|
1070
|
+
await store.writeFtsBaseBuildChunk(firstChunk);
|
|
1071
|
+
await checkThrows(() => store.writeFtsBaseBuildChunk({
|
|
1072
|
+
...firstChunk,
|
|
1073
|
+
chunk: [{ term: "alpha", rowIds: [999n], tf: [1] }],
|
|
1074
|
+
}), PostingBuildConflictError, "replaying an acknowledged postings ordinal with changed bytes");
|
|
1075
|
+
if (target.reopen !== undefined) {
|
|
1076
|
+
store = await target.reopen(store);
|
|
1077
|
+
// The active owner and completed ordinal are durable too. Replaying after a process
|
|
1078
|
+
// restart must still be a no-op, not a duplicate allocation or a vanished build.
|
|
1079
|
+
await store.writeFtsBaseBuildChunk(firstChunk);
|
|
1080
|
+
}
|
|
1081
|
+
await store.writeFtsBaseBuildChunk({
|
|
1082
|
+
tableId: "table-postings",
|
|
1083
|
+
columnId: "col-v",
|
|
1084
|
+
buildId: "replacement",
|
|
1085
|
+
ownerId: "owner-replacement",
|
|
1086
|
+
expiresAtCutoff: "2099-01-01T00:10:02.000Z",
|
|
1087
|
+
expiresAt: "2099-01-01T00:20:00.000Z",
|
|
1088
|
+
updatedAt: "2099-01-01T00:10:02.000Z",
|
|
1089
|
+
ordinal: 1,
|
|
1090
|
+
chunk: [{ term: "omega", rowIds: [3n], tf: [1] }],
|
|
1091
|
+
});
|
|
1092
|
+
await store.finishFtsBaseBuild({
|
|
1093
|
+
tableId: "table-postings",
|
|
1094
|
+
columnId: "col-v",
|
|
1095
|
+
buildId: "replacement",
|
|
1096
|
+
ownerId: "owner-replacement",
|
|
1097
|
+
expiresAtCutoff: "2099-01-01T00:10:03.000Z",
|
|
1098
|
+
coversVersion: 4,
|
|
1099
|
+
chunkCount: 2,
|
|
1100
|
+
totalTokens: 2,
|
|
1101
|
+
completedAt: "2099-01-01T00:10:03.000Z",
|
|
1102
|
+
});
|
|
1103
|
+
let candidates = await store.readFtsCandidates("table-postings", "col-v", [
|
|
1104
|
+
{ term: "old", prefix: false },
|
|
1105
|
+
{ lower: "alpha", lowerInclusive: true, upper: "omega", upperInclusive: true },
|
|
1106
|
+
], 4);
|
|
1107
|
+
checkEqual(candidates.rowIdsByTerm, [[], [2n, 3n]], "only a finished replacement generation may become visible");
|
|
1108
|
+
check(candidates.hasBase, "a finished generation must report a published base");
|
|
1109
|
+
if (target.reopen !== undefined) {
|
|
1110
|
+
store = await target.reopen(store);
|
|
1111
|
+
candidates = await store.readFtsCandidates("table-postings", "col-v", [{ term: "omega", prefix: false }], 4);
|
|
1112
|
+
checkEqual(candidates.rowIdsByTerm, [[3n]], "a finished generation must survive reopen");
|
|
1113
|
+
check(candidates.hasBase, "a reopened generation must still report its base");
|
|
1114
|
+
}
|
|
1115
|
+
await checkThrows(() => store.removeFtsColumn("table-postings", "col-v"), Error, "removing postings still owned by a ready catalog accelerator");
|
|
1116
|
+
const indexed = await store.getTable("table-postings");
|
|
1117
|
+
check(indexed !== undefined, "the postings table disappeared");
|
|
1118
|
+
await store.updateTable(indexed.id, indexed.revision, {
|
|
1119
|
+
ftsColumns: {
|
|
1120
|
+
...indexed.ftsColumns,
|
|
1121
|
+
"col-v": {
|
|
1122
|
+
...(indexed.ftsColumns?.["col-v"] ?? {
|
|
1123
|
+
storage: "fts-chunks-v1",
|
|
1124
|
+
tokenizerVersion: 1,
|
|
1125
|
+
buildFromVersion: -1,
|
|
1126
|
+
}),
|
|
1127
|
+
state: "invalid",
|
|
1128
|
+
},
|
|
1129
|
+
},
|
|
1130
|
+
});
|
|
1131
|
+
await store.removeFtsColumn("table-postings", "col-v");
|
|
1132
|
+
candidates = await store.readFtsCandidates("table-postings", "col-v", [{ term: "alpha", prefix: false }], 4);
|
|
1133
|
+
checkEqual(candidates.rowIdsByTerm, [[]], "removing an index must remove its candidates");
|
|
1134
|
+
check(!candidates.hasBase, "a removed base must be distinguishable from an empty base");
|
|
1135
|
+
store.close();
|
|
1136
|
+
},
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
name: "UNIQUE-index membership seeds and drops atomically with its catalog record",
|
|
511
1140
|
async run(target) {
|
|
512
1141
|
const store = await target.create();
|
|
1142
|
+
const record = table("unique-secondary");
|
|
1143
|
+
const indexId = "unique-v";
|
|
1144
|
+
const namespaceId = secondaryUniqueKeyNamespace(record.id, indexId);
|
|
1145
|
+
const building = {
|
|
1146
|
+
name: "unique_secondary_v",
|
|
1147
|
+
columnId: "col-v",
|
|
1148
|
+
columnIds: ["col-v"],
|
|
1149
|
+
directions: ["asc"],
|
|
1150
|
+
unique: true,
|
|
1151
|
+
termEncoding: "tuple-v1",
|
|
1152
|
+
storage: "postings-v1",
|
|
1153
|
+
storageColumnId: "secondary-index:unique-v",
|
|
1154
|
+
locator: "row-id",
|
|
1155
|
+
state: "building",
|
|
1156
|
+
buildId: "builder",
|
|
1157
|
+
buildFromVersion: -1,
|
|
1158
|
+
};
|
|
1159
|
+
const { buildId: _buildId, ...withoutBuilder } = building;
|
|
1160
|
+
void _buildId;
|
|
1161
|
+
const ready = {
|
|
1162
|
+
...withoutBuilder,
|
|
1163
|
+
state: "ready",
|
|
1164
|
+
uniqueEnforced: true,
|
|
1165
|
+
};
|
|
1166
|
+
await store.addTable({ ...record, secondaryIndexes: { [indexId]: building } });
|
|
1167
|
+
try {
|
|
1168
|
+
await store.updateTable(record.id, 0, {
|
|
1169
|
+
secondaryIndexes: { [indexId]: ready },
|
|
1170
|
+
expectedManifestVersion: { value: null },
|
|
1171
|
+
uniqueKeySeed: { namespaceId: `${namespaceId}-wrong`, keyTokens: ["x"] },
|
|
1172
|
+
});
|
|
1173
|
+
throw new Error("a seed for the wrong namespace was accepted");
|
|
1174
|
+
}
|
|
1175
|
+
catch (error) {
|
|
1176
|
+
check(error instanceof TypeError, "a mismatched UNIQUE seed must be rejected");
|
|
1177
|
+
}
|
|
1178
|
+
check((await store.getTable(record.id))?.revision === 0, "a mismatched seed must not mutate the catalog");
|
|
1179
|
+
await checkThrows(() => store.updateTable(record.id, 0, {
|
|
1180
|
+
secondaryIndexes: {
|
|
1181
|
+
[indexId]: ready,
|
|
1182
|
+
},
|
|
1183
|
+
expectedManifestVersion: { value: null },
|
|
1184
|
+
uniqueKeySeed: { namespaceId, keyTokens: ["x", "x"] },
|
|
1185
|
+
}), UniqueKeyConflictError, "a duplicate UNIQUE seed");
|
|
1186
|
+
check((await store.getTable(record.id))?.secondaryIndexes?.[indexId]?.state === "building", "a refused seed must not publish the ready catalog state");
|
|
1187
|
+
await store.updateTable(record.id, 0, {
|
|
1188
|
+
secondaryIndexes: {
|
|
1189
|
+
[indexId]: ready,
|
|
1190
|
+
},
|
|
1191
|
+
expectedManifestVersion: { value: null },
|
|
1192
|
+
uniqueKeySeed: { namespaceId, keyTokens: ["x", "y"] },
|
|
1193
|
+
});
|
|
1194
|
+
checkEqual(await store.getExistingUniqueKeys(namespaceId, ["x", "z"]), ["x"], "the ready catalog and membership must publish together");
|
|
1195
|
+
await store.updateTable(record.id, 1, { secondaryIndexes: null });
|
|
1196
|
+
checkEqual(await store.getExistingUniqueKeys(namespaceId, ["x", "y"]), [], "dropping the index must reclaim its membership namespace");
|
|
1197
|
+
store.close();
|
|
1198
|
+
},
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
name: "atomic staging and rollback keep journals and artifacts inseparable",
|
|
1202
|
+
async run(target) {
|
|
1203
|
+
let store = await target.create();
|
|
513
1204
|
await store.addTable(table("t"));
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const begun = await begin({
|
|
1205
|
+
{
|
|
1206
|
+
const begun = await store.beginTransaction({
|
|
517
1207
|
record: {
|
|
518
1208
|
id: "txn-b",
|
|
1209
|
+
ownerId: "txn-b/owner",
|
|
1210
|
+
expiresAt: LATER,
|
|
519
1211
|
pendingBlockIds: [],
|
|
520
1212
|
pendingSegmentIds: [],
|
|
521
1213
|
status: "active",
|
|
@@ -529,33 +1221,97 @@ export function blockStoreConformanceCases() {
|
|
|
529
1221
|
check(begun.record.snapshotVersion === (await store.getCurrentManifestVersion()), "beginTransaction must pin the current version");
|
|
530
1222
|
check(begun.rowIds !== undefined && begun.rowIds.endExclusive - begun.rowIds.start === 4n, "beginTransaction must honor the reservation");
|
|
531
1223
|
}
|
|
532
|
-
const stage = store.stageTransactionArtifacts
|
|
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
|
-
|
|
1224
|
+
const stage = store.stageTransactionArtifacts.bind(store);
|
|
1225
|
+
await store.createTransaction(transaction("txn-s", await store.getCurrentManifestVersion()));
|
|
1226
|
+
const duplicate = segment("segment-duplicate", "table-t", "txn-s", "block-none");
|
|
1227
|
+
await checkThrows(() => stage({
|
|
1228
|
+
transactionId: "txn-s",
|
|
1229
|
+
expectedRevision: 0,
|
|
1230
|
+
blocks: [],
|
|
1231
|
+
segments: [duplicate, duplicate],
|
|
1232
|
+
updatedAt: LATER,
|
|
1233
|
+
}), Error, "staging duplicate segment IDs");
|
|
1234
|
+
await checkThrows(() => stage({
|
|
1235
|
+
transactionId: "txn-s",
|
|
1236
|
+
expectedRevision: 0,
|
|
1237
|
+
blocks: [],
|
|
1238
|
+
segments: [segment("segment-foreign", "table-t", "another-txn", "block-none")],
|
|
1239
|
+
updatedAt: LATER,
|
|
1240
|
+
}), Error, "staging a segment owned by another transaction");
|
|
1241
|
+
check((await store.getSegment("segment-duplicate")) === undefined &&
|
|
1242
|
+
(await store.getSegment("segment-foreign")) === undefined &&
|
|
1243
|
+
(await store.getTransaction("txn-s"))?.revision === 0, "refused staging must leave the transaction and segment store unchanged");
|
|
1244
|
+
const first = await stage({
|
|
1245
|
+
transactionId: "txn-s",
|
|
1246
|
+
expectedRevision: 0,
|
|
1247
|
+
blocks: [{ id: "block-z", bytes: Uint8Array.of(7) }],
|
|
1248
|
+
segments: [segment("segment-z", "table-t", "txn-s", "block-z")],
|
|
1249
|
+
updatedAt: LATER,
|
|
1250
|
+
});
|
|
1251
|
+
await checkThrows(() => stage({
|
|
1252
|
+
transactionId: "txn-s",
|
|
1253
|
+
expectedRevision: first.revision,
|
|
1254
|
+
blocks: [{ id: "block-gap", bytes: Uint8Array.of(9) }],
|
|
1255
|
+
segments: [segment("segment-gap", "table-t", "txn-s", "block-gap", 0)],
|
|
1256
|
+
updatedAt: LATER,
|
|
1257
|
+
}), TypeError, "staging a segment whose commit ordinal does not continue the journal");
|
|
1258
|
+
check((await store.getTransaction("txn-s"))?.revision === first.revision &&
|
|
1259
|
+
(await store.getBlock("block-gap")) === undefined &&
|
|
1260
|
+
(await store.getSegment("segment-gap")) === undefined, "an ordinal refusal must leave the record, block, and segment unchanged");
|
|
1261
|
+
const staged = await stage({
|
|
1262
|
+
transactionId: "txn-s",
|
|
1263
|
+
expectedRevision: first.revision,
|
|
1264
|
+
blocks: [{ id: "block-a", bytes: Uint8Array.of(8) }],
|
|
1265
|
+
segments: [segment("segment-a", "table-t", "txn-s", "block-a", 1)],
|
|
1266
|
+
updatedAt: LATER,
|
|
1267
|
+
});
|
|
1268
|
+
checkEqual(staged.pendingBlockIds, ["block-z", "block-a"], "staging journals blocks");
|
|
1269
|
+
checkEqual(staged.pendingSegmentIds, ["segment-z", "segment-a"], "staging preserves the canonical segment journal order");
|
|
1270
|
+
check((await store.getBlock("block-a")) !== undefined &&
|
|
1271
|
+
(await store.getSegment("segment-a")) !== undefined, "staging must write bytes and segments");
|
|
1272
|
+
const rollbackInput = {
|
|
1273
|
+
transactionId: "txn-s",
|
|
1274
|
+
expectedRevision: staged.revision,
|
|
1275
|
+
pendingBlockIds: ["block-z"],
|
|
1276
|
+
pendingSegmentIds: ["segment-z"],
|
|
1277
|
+
removeBlockIds: ["block-a"],
|
|
1278
|
+
removeSegmentIds: ["segment-a"],
|
|
1279
|
+
updatedAt: LATER,
|
|
1280
|
+
};
|
|
1281
|
+
await checkThrows(() => store.rollbackTransactionArtifacts({
|
|
1282
|
+
...rollbackInput,
|
|
1283
|
+
expectedRevision: staged.revision + 1,
|
|
1284
|
+
}), TransactionRecordConflictError, "rollback with a stale transaction revision");
|
|
1285
|
+
await checkThrows(() => store.rollbackTransactionArtifacts({
|
|
1286
|
+
...rollbackInput,
|
|
1287
|
+
removeBlockIds: [],
|
|
1288
|
+
}), TypeError, "rollback whose lists do not exactly partition the journal");
|
|
1289
|
+
await checkThrows(() => store.rollbackTransactionArtifacts({
|
|
1290
|
+
...rollbackInput,
|
|
1291
|
+
pendingBlockIds: ["block-a"],
|
|
1292
|
+
pendingSegmentIds: ["segment-a"],
|
|
1293
|
+
removeBlockIds: ["block-z"],
|
|
1294
|
+
removeSegmentIds: ["segment-z"],
|
|
1295
|
+
}), TypeError, "rollback that retains a non-prefix journal partition");
|
|
1296
|
+
check((await store.getBlock("block-a")) !== undefined &&
|
|
1297
|
+
(await store.getSegment("segment-a")) !== undefined, "a refused rollback must leave every artifact intact");
|
|
1298
|
+
const rolledBack = await store.rollbackTransactionArtifacts(rollbackInput);
|
|
1299
|
+
checkEqual(rolledBack.pendingBlockIds, ["block-z"], "rollback must retain checkpoint block");
|
|
1300
|
+
check((await store.getBlock("block-a")) === undefined &&
|
|
1301
|
+
(await store.getSegment("segment-a")) === undefined &&
|
|
1302
|
+
(await store.getBlock("block-z")) !== undefined &&
|
|
1303
|
+
(await store.getSegment("segment-z")) !== undefined, "rollback must delete only its exact removed partition");
|
|
1304
|
+
if (target.reopen !== undefined) {
|
|
1305
|
+
const reopened = await target.reopen(store);
|
|
1306
|
+
checkEqual((await reopened.getTransaction("txn-s"))?.pendingBlockIds, ["block-z"], "rollback journal must survive reopen");
|
|
1307
|
+
check((await reopened.getBlock("block-a")) === undefined &&
|
|
1308
|
+
(await reopened.getBlock("block-z")) !== undefined, "rollback artifact removal must survive reopen");
|
|
1309
|
+
store = reopened;
|
|
558
1310
|
}
|
|
1311
|
+
const before = await store.getCatalogProbe();
|
|
1312
|
+
await store.addTable(table("probe-check"));
|
|
1313
|
+
const after = await store.getCatalogProbe();
|
|
1314
|
+
check(after.catalogEpoch > before.catalogEpoch, "a catalog mutation must advance the probe's epoch");
|
|
559
1315
|
store.close();
|
|
560
1316
|
},
|
|
561
1317
|
},
|
|
@@ -569,10 +1325,11 @@ export function blockStoreConformanceCases() {
|
|
|
569
1325
|
return;
|
|
570
1326
|
}
|
|
571
1327
|
await store.addTable(table("t", true));
|
|
1328
|
+
const schemaEpoch = (await store.getCatalogProbe()).schemaEpoch;
|
|
572
1329
|
const fresh = (id) => {
|
|
573
1330
|
const { snapshotVersion: _pinned, ...record } = transaction(id, null);
|
|
574
1331
|
void _pinned;
|
|
575
|
-
return record;
|
|
1332
|
+
return { ...record, schemaEpochGuard: schemaEpoch };
|
|
576
1333
|
};
|
|
577
1334
|
// A fresh record: begun, staged, and committed in the one step.
|
|
578
1335
|
const first = await write({
|
|
@@ -580,6 +1337,7 @@ export function blockStoreConformanceCases() {
|
|
|
580
1337
|
blocks: [{ id: "block-w1", bytes: Uint8Array.of(1) }],
|
|
581
1338
|
segments: [segment("segment-w1", "table-t", "txn-w1", "block-w1")],
|
|
582
1339
|
expectedManifestVersion: null,
|
|
1340
|
+
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
583
1341
|
uniqueKeyChanges: [{ tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true }],
|
|
584
1342
|
committedAt: LATER,
|
|
585
1343
|
});
|
|
@@ -599,6 +1357,7 @@ export function blockStoreConformanceCases() {
|
|
|
599
1357
|
blocks: [{ id: "block-stale", bytes: Uint8Array.of(2) }],
|
|
600
1358
|
segments: [segment("segment-stale", "table-t", "txn-stale", "block-stale")],
|
|
601
1359
|
expectedManifestVersion: null,
|
|
1360
|
+
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
602
1361
|
committedAt: LATER,
|
|
603
1362
|
}), WriteConflictError, "a single-shot write against a moved manifest");
|
|
604
1363
|
check((await store.getTransaction("txn-stale")) === undefined &&
|
|
@@ -610,6 +1369,7 @@ export function blockStoreConformanceCases() {
|
|
|
610
1369
|
blocks: [{ id: "block-dup", bytes: Uint8Array.of(3) }],
|
|
611
1370
|
segments: [segment("segment-dup", "table-t", "txn-dup", "block-dup")],
|
|
612
1371
|
expectedManifestVersion: 0,
|
|
1372
|
+
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
613
1373
|
uniqueKeyChanges: [
|
|
614
1374
|
{ tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true },
|
|
615
1375
|
],
|
|
@@ -625,6 +1385,7 @@ export function blockStoreConformanceCases() {
|
|
|
625
1385
|
blocks: [{ id: "block-w2", bytes: Uint8Array.of(4) }],
|
|
626
1386
|
segments: [segment("segment-w2", "table-t", "txn-w2", "block-w2")],
|
|
627
1387
|
expectedManifestVersion: 0,
|
|
1388
|
+
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
628
1389
|
committedAt: LATER,
|
|
629
1390
|
}), TransactionRecordConflictError, "a single-shot write with a stale transaction revision");
|
|
630
1391
|
check((await store.getBlock("block-w2")) === undefined, "a revision conflict must leave no block");
|
|
@@ -633,18 +1394,114 @@ export function blockStoreConformanceCases() {
|
|
|
633
1394
|
blocks: [{ id: "block-w2", bytes: Uint8Array.of(4) }],
|
|
634
1395
|
segments: [segment("segment-w2", "table-t", "txn-w2", "block-w2")],
|
|
635
1396
|
expectedManifestVersion: 0,
|
|
636
|
-
|
|
1397
|
+
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
637
1398
|
changedTableIds: ["table-t"],
|
|
638
1399
|
committedAt: LATER,
|
|
639
1400
|
});
|
|
640
1401
|
checkEqual(second.version, 1, "the continued write must publish the next version");
|
|
641
1402
|
checkEqual(second.changedTableIds, ["table-t"], "the summary must carry the change set");
|
|
642
|
-
|
|
643
|
-
checkEqual(manifest?.blockIds, ["block-w2"], "the continued write must add its blocks and apply supersessions");
|
|
1403
|
+
checkEqual(await manifestBlockIds(store, 1), ["block-w1", "block-w2"], "the continued write must preserve visible blocks and add its own");
|
|
644
1404
|
check((await store.getTransaction("txn-w2"))?.status === "committed", "the begun record must end committed");
|
|
645
1405
|
store.close();
|
|
646
1406
|
},
|
|
647
1407
|
},
|
|
1408
|
+
{
|
|
1409
|
+
name: "compaction commit proves exact output provenance before publication",
|
|
1410
|
+
async run(target) {
|
|
1411
|
+
const store = await target.create();
|
|
1412
|
+
await store.addTable({
|
|
1413
|
+
id: "table-compact",
|
|
1414
|
+
name: "compact",
|
|
1415
|
+
columns: [{ id: "col-value", name: "value", type: "number", nullable: false }],
|
|
1416
|
+
managed: false,
|
|
1417
|
+
revision: 0,
|
|
1418
|
+
createdAt: T0,
|
|
1419
|
+
});
|
|
1420
|
+
await store.createTransaction(transaction("txn-compact-source", null));
|
|
1421
|
+
const source = await store.stageTransactionArtifacts({
|
|
1422
|
+
transactionId: "txn-compact-source",
|
|
1423
|
+
expectedRevision: 0,
|
|
1424
|
+
blocks: [{ id: "compact-source-block", bytes: Uint8Array.of(1) }],
|
|
1425
|
+
segments: [
|
|
1426
|
+
{
|
|
1427
|
+
...segment("compact-source-segment", "table-compact", "txn-compact-source", "compact-source-block"),
|
|
1428
|
+
columnBlockIds: { "col-value": ["compact-source-block"] },
|
|
1429
|
+
},
|
|
1430
|
+
],
|
|
1431
|
+
updatedAt: T0,
|
|
1432
|
+
});
|
|
1433
|
+
await store.commitTransaction({
|
|
1434
|
+
transactionId: source.id,
|
|
1435
|
+
expectedTransactionRevision: source.revision,
|
|
1436
|
+
expectedManifestVersion: null,
|
|
1437
|
+
levelZeroSegmentLimits: [{ tableId: "table-compact", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
1438
|
+
committedAt: T0,
|
|
1439
|
+
});
|
|
1440
|
+
const jobId = "compact-proof-job";
|
|
1441
|
+
const outputBlockId = `${jobId}/output/segment/000000/column/000000/part/000000`;
|
|
1442
|
+
await store.createTransaction(transaction("txn-compact-output", 0));
|
|
1443
|
+
const output = await store.stageTransactionArtifacts({
|
|
1444
|
+
transactionId: "txn-compact-output",
|
|
1445
|
+
expectedRevision: 0,
|
|
1446
|
+
blocks: [{ id: outputBlockId, bytes: Uint8Array.of(2) }],
|
|
1447
|
+
segments: [
|
|
1448
|
+
{
|
|
1449
|
+
...segment("compact-output-segment", "table-compact", "txn-compact-output", outputBlockId),
|
|
1450
|
+
columnBlockIds: { "col-value": [outputBlockId] },
|
|
1451
|
+
// Structurally valid, but copy-v1 is required to publish an insert segment.
|
|
1452
|
+
kind: "base",
|
|
1453
|
+
level: 1,
|
|
1454
|
+
},
|
|
1455
|
+
],
|
|
1456
|
+
updatedAt: LATER,
|
|
1457
|
+
});
|
|
1458
|
+
const job = {
|
|
1459
|
+
id: jobId,
|
|
1460
|
+
tableId: "table-compact",
|
|
1461
|
+
sourceManifestVersion: 0,
|
|
1462
|
+
sourceSegmentIds: ["compact-source-segment"],
|
|
1463
|
+
sourceBlockIds: ["compact-source-block"],
|
|
1464
|
+
outputBlockIds: [outputBlockId],
|
|
1465
|
+
cursor: { sourceSegmentIndex: 1, sourceBlockIndex: 0 },
|
|
1466
|
+
processedRows: 2,
|
|
1467
|
+
sourceStoredBytes: 1,
|
|
1468
|
+
outputStoredBytes: 1,
|
|
1469
|
+
logicalBytes: 1,
|
|
1470
|
+
rewritePlan: { kind: "copy-v1" },
|
|
1471
|
+
outputCursor: null,
|
|
1472
|
+
memoryBudgetBytes: 0,
|
|
1473
|
+
minimumMemoryBytes: 0,
|
|
1474
|
+
level0SourceStoredBytes: 1,
|
|
1475
|
+
anchorSourceStoredBytes: 0,
|
|
1476
|
+
peakWorkingBytes: 0,
|
|
1477
|
+
outputLogicalBytes: 1,
|
|
1478
|
+
targetLevel: 1,
|
|
1479
|
+
state: "ready",
|
|
1480
|
+
transactionId: output.id,
|
|
1481
|
+
outputSegmentId: "compact-output-segment",
|
|
1482
|
+
publishedVersion: null,
|
|
1483
|
+
revision: 0,
|
|
1484
|
+
createdAt: T0,
|
|
1485
|
+
updatedAt: LATER,
|
|
1486
|
+
};
|
|
1487
|
+
await store.createCompactionJob(job);
|
|
1488
|
+
await checkThrows(() => store.commitTransaction({
|
|
1489
|
+
transactionId: output.id,
|
|
1490
|
+
expectedTransactionRevision: output.revision,
|
|
1491
|
+
expectedManifestVersion: 0,
|
|
1492
|
+
compactionJobId: job.id,
|
|
1493
|
+
removedBlockIds: ["compact-source-block"],
|
|
1494
|
+
committedAt: LATER,
|
|
1495
|
+
}), Error, "a structurally valid compaction segment that differs from the immutable plan");
|
|
1496
|
+
checkEqual(await store.getCurrentManifestVersion(), 0, "a provenance refusal must not publish a manifest");
|
|
1497
|
+
check((await store.getTransaction(output.id))?.status === "active" &&
|
|
1498
|
+
(await store.getSegment("compact-output-segment"))?.kind === "base" &&
|
|
1499
|
+
(await store.getBlock(outputBlockId)) !== undefined &&
|
|
1500
|
+
(await store.getCompactionJob(job.id))?.state === "ready", "a provenance refusal must leave the transaction, artifacts, and job unchanged");
|
|
1501
|
+
checkEqual(await manifestBlockIds(store, 0), ["compact-source-block"], "a provenance refusal must retain every source block");
|
|
1502
|
+
store.close();
|
|
1503
|
+
},
|
|
1504
|
+
},
|
|
648
1505
|
{
|
|
649
1506
|
name: "the lease move, when present, re-pins in place and refuses typed",
|
|
650
1507
|
async run(target) {
|
|
@@ -662,26 +1519,47 @@ export function blockStoreConformanceCases() {
|
|
|
662
1519
|
kind: "reader",
|
|
663
1520
|
manifestVersion: first.version,
|
|
664
1521
|
ownerId: "owner-m",
|
|
1522
|
+
createdAt: T0,
|
|
665
1523
|
expiresAt: LATER,
|
|
666
1524
|
revision: 0,
|
|
667
1525
|
});
|
|
668
|
-
await checkThrows(() => move(
|
|
669
|
-
|
|
1526
|
+
await checkThrows(() => move({
|
|
1527
|
+
id: "lease-m",
|
|
1528
|
+
expectedRevision: 9,
|
|
1529
|
+
manifestVersion: second.version,
|
|
1530
|
+
expiresAtCutoff: T0,
|
|
1531
|
+
expiresAt: LATER,
|
|
1532
|
+
}), LeaseConflictError, "moving with a stale revision");
|
|
1533
|
+
await checkThrows(() => move({
|
|
1534
|
+
id: "lease-m",
|
|
1535
|
+
expectedRevision: 0,
|
|
1536
|
+
manifestVersion: 999,
|
|
1537
|
+
expiresAtCutoff: T0,
|
|
1538
|
+
expiresAt: LATER,
|
|
1539
|
+
}), SnapshotManifestMissingError, "moving to a version with no manifest");
|
|
670
1540
|
checkEqual(await store.getLease("lease-m"), {
|
|
671
1541
|
id: "lease-m",
|
|
672
1542
|
kind: "reader",
|
|
673
1543
|
manifestVersion: first.version,
|
|
674
1544
|
ownerId: "owner-m",
|
|
1545
|
+
createdAt: T0,
|
|
675
1546
|
expiresAt: LATER,
|
|
676
1547
|
revision: 0,
|
|
677
1548
|
}, "a refused move must leave the lease exactly as it was");
|
|
678
|
-
const moved = await move(
|
|
1549
|
+
const moved = await move({
|
|
1550
|
+
id: "lease-m",
|
|
1551
|
+
expectedRevision: 0,
|
|
1552
|
+
manifestVersion: second.version,
|
|
1553
|
+
expiresAtCutoff: T0,
|
|
1554
|
+
expiresAt: LATER,
|
|
1555
|
+
});
|
|
679
1556
|
checkEqual(moved, {
|
|
680
1557
|
id: "lease-m",
|
|
681
1558
|
kind: "reader",
|
|
682
1559
|
manifestVersion: second.version,
|
|
683
1560
|
ownerId: "owner-m",
|
|
684
|
-
|
|
1561
|
+
createdAt: T0,
|
|
1562
|
+
expiresAt: LATER,
|
|
685
1563
|
revision: 1,
|
|
686
1564
|
}, "a move must re-pin, renew, and advance the revision");
|
|
687
1565
|
checkEqual((await store.listLeases()).map((lease) => lease.id), ["lease-m"], "a move must keep the one record, not create a second");
|
|
@@ -703,10 +1581,16 @@ export function blockStoreConformanceCases() {
|
|
|
703
1581
|
const { snapshotVersion: _pinned, ...record } = transaction("txn-single", null);
|
|
704
1582
|
void _pinned;
|
|
705
1583
|
const summary = await write({
|
|
706
|
-
transaction: {
|
|
1584
|
+
transaction: {
|
|
1585
|
+
record: {
|
|
1586
|
+
...record,
|
|
1587
|
+
schemaEpochGuard: (await store.getCatalogProbe()).schemaEpoch,
|
|
1588
|
+
},
|
|
1589
|
+
},
|
|
707
1590
|
blocks: [{ id: "block-single", bytes: Uint8Array.of(1, 2, 3, 4) }],
|
|
708
1591
|
segments: [segment("segment-single", "table-t", "txn-single", "block-single")],
|
|
709
1592
|
expectedManifestVersion: version,
|
|
1593
|
+
levelZeroSegmentLimits: [{ tableId: "table-t", limit: MAX_LEVEL_ZERO_SEGMENTS }],
|
|
710
1594
|
uniqueKeyChanges: [
|
|
711
1595
|
{ tableId: "table-t", keyTokens: ["number:1"], requireAbsent: false },
|
|
712
1596
|
],
|
|
@@ -722,10 +1606,17 @@ export function blockStoreConformanceCases() {
|
|
|
722
1606
|
kind: "reader",
|
|
723
1607
|
manifestVersion: null,
|
|
724
1608
|
ownerId: "owner",
|
|
725
|
-
|
|
1609
|
+
createdAt: T0,
|
|
1610
|
+
expiresAt: LATER,
|
|
726
1611
|
revision: 0,
|
|
727
1612
|
});
|
|
728
|
-
await move(
|
|
1613
|
+
await move({
|
|
1614
|
+
id: "lease-durable",
|
|
1615
|
+
expectedRevision: 0,
|
|
1616
|
+
manifestVersion: version,
|
|
1617
|
+
expiresAtCutoff: T0,
|
|
1618
|
+
expiresAt: LATER,
|
|
1619
|
+
});
|
|
729
1620
|
}
|
|
730
1621
|
store = await target.reopen(store);
|
|
731
1622
|
checkEqual(await store.getCurrentManifestVersion(), version, "the committed version must survive a reopen");
|
|
@@ -742,4 +1633,3 @@ export function blockStoreConformanceCases() {
|
|
|
742
1633
|
},
|
|
743
1634
|
];
|
|
744
1635
|
}
|
|
745
|
-
//# sourceMappingURL=block-store-conformance.js.map
|