@quereus/store 4.11.0 → 4.12.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 +131 -10
- package/dist/src/common/cached-kv-store.d.ts +16 -0
- package/dist/src/common/cached-kv-store.d.ts.map +1 -1
- package/dist/src/common/cached-kv-store.js +43 -0
- package/dist/src/common/cached-kv-store.js.map +1 -1
- package/dist/src/common/index.d.ts +1 -0
- package/dist/src/common/index.d.ts.map +1 -1
- package/dist/src/common/index.js +2 -0
- package/dist/src/common/index.js.map +1 -1
- package/dist/src/common/key-builder.d.ts +9 -6
- package/dist/src/common/key-builder.d.ts.map +1 -1
- package/dist/src/common/key-builder.js +17 -6
- package/dist/src/common/key-builder.js.map +1 -1
- package/dist/src/common/kv-store.d.ts +89 -3
- package/dist/src/common/kv-store.d.ts.map +1 -1
- package/dist/src/common/kv-store.js +21 -1
- package/dist/src/common/kv-store.js.map +1 -1
- package/dist/src/common/memory-store.d.ts +1 -0
- package/dist/src/common/memory-store.d.ts.map +1 -1
- package/dist/src/common/memory-store.js +7 -0
- package/dist/src/common/memory-store.js.map +1 -1
- package/dist/src/common/pk-key-resolution.d.ts +17 -4
- package/dist/src/common/pk-key-resolution.d.ts.map +1 -1
- package/dist/src/common/pk-key-resolution.js +23 -8
- package/dist/src/common/pk-key-resolution.js.map +1 -1
- package/dist/src/common/store-module-access-plan.d.ts +1 -1
- package/dist/src/common/store-module-access-plan.d.ts.map +1 -1
- package/dist/src/common/store-module-access-plan.js +434 -105
- package/dist/src/common/store-module-access-plan.js.map +1 -1
- package/dist/src/common/store-module-alter-column.d.ts.map +1 -1
- package/dist/src/common/store-module-alter-column.js +25 -2
- package/dist/src/common/store-module-alter-column.js.map +1 -1
- package/dist/src/common/store-module-alter.d.ts.map +1 -1
- package/dist/src/common/store-module-alter.js +36 -13
- package/dist/src/common/store-module-alter.js.map +1 -1
- package/dist/src/common/store-module-catalog.d.ts +6 -4
- package/dist/src/common/store-module-catalog.d.ts.map +1 -1
- package/dist/src/common/store-module-catalog.js +8 -6
- package/dist/src/common/store-module-catalog.js.map +1 -1
- package/dist/src/common/store-module-index.d.ts +98 -7
- package/dist/src/common/store-module-index.d.ts.map +1 -1
- package/dist/src/common/store-module-index.js +236 -81
- package/dist/src/common/store-module-index.js.map +1 -1
- package/dist/src/common/store-module.d.ts.map +1 -1
- package/dist/src/common/store-module.js +7 -3
- package/dist/src/common/store-module.js.map +1 -1
- package/dist/src/common/store-table-base.d.ts +30 -0
- package/dist/src/common/store-table-base.d.ts.map +1 -1
- package/dist/src/common/store-table-base.js +71 -12
- package/dist/src/common/store-table-base.js.map +1 -1
- package/dist/src/common/store-table-constraints.js +3 -3
- package/dist/src/common/store-table-constraints.js.map +1 -1
- package/dist/src/common/store-table-scan.d.ts +208 -98
- package/dist/src/common/store-table-scan.d.ts.map +1 -1
- package/dist/src/common/store-table-scan.js +353 -105
- package/dist/src/common/store-table-scan.js.map +1 -1
- package/dist/src/testing/index.d.ts +17 -5
- package/dist/src/testing/index.d.ts.map +1 -1
- package/dist/src/testing/index.js +16 -5
- package/dist/src/testing/index.js.map +1 -1
- package/dist/src/testing/kv-conformance.d.ts +49 -0
- package/dist/src/testing/kv-conformance.d.ts.map +1 -1
- package/dist/src/testing/kv-conformance.js +125 -0
- package/dist/src/testing/kv-conformance.js.map +1 -1
- package/dist/src/testing/kv-lifecycle.d.ts +22 -0
- package/dist/src/testing/kv-lifecycle.d.ts.map +1 -0
- package/dist/src/testing/kv-lifecycle.js +12 -0
- package/dist/src/testing/kv-lifecycle.js.map +1 -0
- package/dist/src/testing/kv-naming-conformance.d.ts +77 -0
- package/dist/src/testing/kv-naming-conformance.d.ts.map +1 -0
- package/dist/src/testing/kv-naming-conformance.js +208 -0
- package/dist/src/testing/kv-naming-conformance.js.map +1 -0
- package/dist/src/testing/kv-reclaim-conformance.d.ts +52 -0
- package/dist/src/testing/kv-reclaim-conformance.d.ts.map +1 -0
- package/dist/src/testing/kv-reclaim-conformance.js +287 -0
- package/dist/src/testing/kv-reclaim-conformance.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared physical-store-name distinctness battery — test-support; run under Mocha.
|
|
3
|
+
*
|
|
4
|
+
* Third sibling of {@link runKVStoreConformance} (the {@link KVStore} contract) and
|
|
5
|
+
* {@link runKVProviderConformance} (the atomic multi-store commit). This one asserts a
|
|
6
|
+
* single PROVIDER-level property that every backend must hold:
|
|
7
|
+
*
|
|
8
|
+
* Two logical stores the engine considers distinct never share one physical store.
|
|
9
|
+
*
|
|
10
|
+
* Why it exists as a shared battery rather than a per-plugin test: the engine's only
|
|
11
|
+
* collision guard, `StoreModule.assertStoreNameFree`, compares LOGICAL store names
|
|
12
|
+
* (`{schema}.{table}` from `buildDataStoreName`) as plain JS strings. That guard stands
|
|
13
|
+
* in for a physical collision check only while each provider's translation from the
|
|
14
|
+
* logical name to its own native name (a LevelDB sublevel, an IndexedDB object store, a
|
|
15
|
+
* SQLite table, an on-device LevelDB file) is INJECTIVE — two different logical names can
|
|
16
|
+
* never come out the same. A provider that lowercases, strips punctuation, or otherwise
|
|
17
|
+
* folds characters breaks that silently: two `create table`s both succeed and their rows
|
|
18
|
+
* then interleave in one physical store. Nothing in the type system prevents the next
|
|
19
|
+
* provider from reintroducing it, so the property is pinned behaviorally here.
|
|
20
|
+
*
|
|
21
|
+
* The battery needs nothing beyond the public {@link KVStoreProvider} surface, so it runs
|
|
22
|
+
* for every backend: it writes a distinct marker into each store under ONE shared key and
|
|
23
|
+
* checks each store still reads back its own marker. Two entries sharing a physical store
|
|
24
|
+
* show up as one marker having overwritten another.
|
|
25
|
+
*
|
|
26
|
+
* A provider MAY reject a name its native namespace cannot represent — a throw from
|
|
27
|
+
* `getStore`/`getIndexStore` passes that entry, and the rejected entries are reported so
|
|
28
|
+
* the corpus cannot silently shrink to nothing. Silently folding two names onto one store
|
|
29
|
+
* never passes. Entries marked {@link StoreSpec.mustAccept} (logical names using only
|
|
30
|
+
* `[a-z0-9_.]`, which any KV namespace can represent) may not be rejected at all.
|
|
31
|
+
*
|
|
32
|
+
* Dependency and Mocha-global handling mirror kv-conformance.ts — see its header.
|
|
33
|
+
*
|
|
34
|
+
* NOTE: this asserts distinctness, not decodability. An injective encoding is what the
|
|
35
|
+
* property needs; nothing here requires a provider to be able to recover the logical name
|
|
36
|
+
* from the physical one, and no shipped provider does.
|
|
37
|
+
*
|
|
38
|
+
* NOTE: the marker check reads back through the very handles it wrote through, so a
|
|
39
|
+
* provider that (a) caches one read-your-own-writes handle PER LOGICAL NAME — as
|
|
40
|
+
* `IndexedDBProvider` does, wrapping each store in `CachedKVStore` — and (b) folds two
|
|
41
|
+
* logical names onto one physical store BELOW that cache would serve each marker from its
|
|
42
|
+
* own cache and pass. No shipped provider does both: the only caching provider names its
|
|
43
|
+
* object stores verbatim, so its folding (if any) would happen at the cache key and be
|
|
44
|
+
* caught. If a provider ever gains a lossy escape underneath a per-name cache, re-read the
|
|
45
|
+
* markers through handles reopened after `closeStore`/`closeIndexStore` instead.
|
|
46
|
+
*/
|
|
47
|
+
import type { KVStoreProvider } from '../common/kv-store.js';
|
|
48
|
+
import type { KVProviderLifecycle } from './kv-lifecycle.js';
|
|
49
|
+
/**
|
|
50
|
+
* Per-backend lifecycle adapter — the shared provider-battery shape, so a plugin's spec can
|
|
51
|
+
* hand one closure to this battery and to {@link runStoreReclaimConformance} alike. The
|
|
52
|
+
* suite drives its own per-test lifecycle and calls {@link makeNamingBackend} fresh for
|
|
53
|
+
* every test, so state never leaks between tests.
|
|
54
|
+
*/
|
|
55
|
+
export type KVNamingBackend = KVProviderLifecycle;
|
|
56
|
+
/**
|
|
57
|
+
* Assert `provider` gives every logical store in {@link DISTINCT_STORES} its OWN physical
|
|
58
|
+
* store: write one marker per store under a single shared key, then check each store still
|
|
59
|
+
* reads back its own. A shared physical store shows up as one marker having replaced
|
|
60
|
+
* another.
|
|
61
|
+
*
|
|
62
|
+
* Exported standalone (like {@link assertBoundedIterate}) so a spec can drive it against a
|
|
63
|
+
* provider double built to violate the property and prove it rejects — a guard nobody has
|
|
64
|
+
* watched fail is not a guard. See `test/store-name-distinctness.spec.ts`.
|
|
65
|
+
*
|
|
66
|
+
* @param provider - A provider over a fresh, EMPTY physical keyspace.
|
|
67
|
+
* @param report - Where to record names the provider refused. Refusal is allowed (a
|
|
68
|
+
* namespace that genuinely cannot represent a name must reject it rather than fold it),
|
|
69
|
+
* but never silently: the battery logs whatever lands here.
|
|
70
|
+
*/
|
|
71
|
+
export declare function assertStoreNamesDistinct(provider: KVStoreProvider, report?: string[]): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Register the store-name distinctness battery under `describe(name)`. Call once per
|
|
74
|
+
* backend, from the same spec file that registers {@link runKVStoreConformance}.
|
|
75
|
+
*/
|
|
76
|
+
export declare function runStoreNameDistinctness(name: string, makeNamingBackend: () => KVNamingBackend): void;
|
|
77
|
+
//# sourceMappingURL=kv-naming-conformance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kv-naming-conformance.d.ts","sourceRoot":"","sources":["../../../src/testing/kv-naming-conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAGH,OAAO,KAAK,EAAW,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAU7D;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAgFlD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,wBAAwB,CAC7C,QAAQ,EAAE,eAAe,EACzB,MAAM,GAAE,MAAM,EAAO,GACnB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,eAAe,GAAG,IAAI,CAoFrG"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared physical-store-name distinctness battery — test-support; run under Mocha.
|
|
3
|
+
*
|
|
4
|
+
* Third sibling of {@link runKVStoreConformance} (the {@link KVStore} contract) and
|
|
5
|
+
* {@link runKVProviderConformance} (the atomic multi-store commit). This one asserts a
|
|
6
|
+
* single PROVIDER-level property that every backend must hold:
|
|
7
|
+
*
|
|
8
|
+
* Two logical stores the engine considers distinct never share one physical store.
|
|
9
|
+
*
|
|
10
|
+
* Why it exists as a shared battery rather than a per-plugin test: the engine's only
|
|
11
|
+
* collision guard, `StoreModule.assertStoreNameFree`, compares LOGICAL store names
|
|
12
|
+
* (`{schema}.{table}` from `buildDataStoreName`) as plain JS strings. That guard stands
|
|
13
|
+
* in for a physical collision check only while each provider's translation from the
|
|
14
|
+
* logical name to its own native name (a LevelDB sublevel, an IndexedDB object store, a
|
|
15
|
+
* SQLite table, an on-device LevelDB file) is INJECTIVE — two different logical names can
|
|
16
|
+
* never come out the same. A provider that lowercases, strips punctuation, or otherwise
|
|
17
|
+
* folds characters breaks that silently: two `create table`s both succeed and their rows
|
|
18
|
+
* then interleave in one physical store. Nothing in the type system prevents the next
|
|
19
|
+
* provider from reintroducing it, so the property is pinned behaviorally here.
|
|
20
|
+
*
|
|
21
|
+
* The battery needs nothing beyond the public {@link KVStoreProvider} surface, so it runs
|
|
22
|
+
* for every backend: it writes a distinct marker into each store under ONE shared key and
|
|
23
|
+
* checks each store still reads back its own marker. Two entries sharing a physical store
|
|
24
|
+
* show up as one marker having overwritten another.
|
|
25
|
+
*
|
|
26
|
+
* A provider MAY reject a name its native namespace cannot represent — a throw from
|
|
27
|
+
* `getStore`/`getIndexStore` passes that entry, and the rejected entries are reported so
|
|
28
|
+
* the corpus cannot silently shrink to nothing. Silently folding two names onto one store
|
|
29
|
+
* never passes. Entries marked {@link StoreSpec.mustAccept} (logical names using only
|
|
30
|
+
* `[a-z0-9_.]`, which any KV namespace can represent) may not be rejected at all.
|
|
31
|
+
*
|
|
32
|
+
* Dependency and Mocha-global handling mirror kv-conformance.ts — see its header.
|
|
33
|
+
*
|
|
34
|
+
* NOTE: this asserts distinctness, not decodability. An injective encoding is what the
|
|
35
|
+
* property needs; nothing here requires a provider to be able to recover the logical name
|
|
36
|
+
* from the physical one, and no shipped provider does.
|
|
37
|
+
*
|
|
38
|
+
* NOTE: the marker check reads back through the very handles it wrote through, so a
|
|
39
|
+
* provider that (a) caches one read-your-own-writes handle PER LOGICAL NAME — as
|
|
40
|
+
* `IndexedDBProvider` does, wrapping each store in `CachedKVStore` — and (b) folds two
|
|
41
|
+
* logical names onto one physical store BELOW that cache would serve each marker from its
|
|
42
|
+
* own cache and pass. No shipped provider does both: the only caching provider names its
|
|
43
|
+
* object stores verbatim, so its folding (if any) would happen at the cache key and be
|
|
44
|
+
* caught. If a provider ever gains a lossy escape underneath a per-name cache, re-read the
|
|
45
|
+
* markers through handles reopened after `closeStore`/`closeIndexStore` instead.
|
|
46
|
+
*/
|
|
47
|
+
import assert from 'node:assert/strict';
|
|
48
|
+
import { assertBytes, b } from './kv-assert.js';
|
|
49
|
+
const encoder = new TextEncoder();
|
|
50
|
+
const decoder = new TextDecoder();
|
|
51
|
+
/** The single key every store's marker is written under. */
|
|
52
|
+
const MARKER_KEY = b(0x01);
|
|
53
|
+
/**
|
|
54
|
+
* Logical stores that must all land on DIFFERENT physical stores.
|
|
55
|
+
*
|
|
56
|
+
* Every name here is one the shared builders accept (no unpaired surrogates — those are
|
|
57
|
+
* refused above every provider by `assertKeyableIdentifiers`). The awkward ones are chosen
|
|
58
|
+
* to break a specific plausible encoding:
|
|
59
|
+
*
|
|
60
|
+
* - `a-b` / `a b` / `a.b` / `a_b` — collapse onto each other under any "fold everything
|
|
61
|
+
* outside `[a-zA-Z0-9_]` to `_`" sanitizer.
|
|
62
|
+
* - `café` / `weißbier` — non-ASCII, which a byte-range filter drops or folds.
|
|
63
|
+
* - `o'brien` — the quote character a SQL-identifier encoding has to deal with.
|
|
64
|
+
* - index `x` on `t` vs. index `x` on `t2` — an index store name must carry its table.
|
|
65
|
+
* - `other.t` vs `main.t` — same table name, different schema.
|
|
66
|
+
*
|
|
67
|
+
* Two near-miss pairs are deliberately ABSENT, because they compose to ONE logical name at
|
|
68
|
+
* the BUILDER — so no provider can tell them apart and asserting distinctness here would
|
|
69
|
+
* pin a failure nobody can fix. Both are guarded above the provider instead:
|
|
70
|
+
*
|
|
71
|
+
* - schema `x` + table `y.z` vs. schema `x.y` + table `z` (both `x.y.z`) — a documented
|
|
72
|
+
* limitation of the `.`-joined name; see the NOTE on `buildDataStoreName`.
|
|
73
|
+
* - a table literally named `t_idx_x` vs. index `x` on table `t` (both `main.t_idx_x`) —
|
|
74
|
+
* `_idx_` is a legal substring of an ordinary identifier, so the join is likewise not
|
|
75
|
+
* boundary-safe. `StoreModuleBase.assertStoreNameFree` rejects whichever of the two is
|
|
76
|
+
* created second, which is what keeps them from ever coexisting.
|
|
77
|
+
*/
|
|
78
|
+
const DISTINCT_STORES = [
|
|
79
|
+
{ label: 'main.t', schema: 'main', table: 't', mustAccept: true },
|
|
80
|
+
{ label: 'main.t2', schema: 'main', table: 't2', mustAccept: true },
|
|
81
|
+
{ label: 'main.a_b', schema: 'main', table: 'a_b', mustAccept: true },
|
|
82
|
+
{ label: 'other.t', schema: 'other', table: 't', mustAccept: true },
|
|
83
|
+
{ label: 'main.t_idx_x (index x on table t)', schema: 'main', table: 't', index: 'x', mustAccept: true },
|
|
84
|
+
{ label: 'main.t2_idx_x (index x on table t2)', schema: 'main', table: 't2', index: 'x', mustAccept: true },
|
|
85
|
+
{ label: 'main.a-b', schema: 'main', table: 'a-b' },
|
|
86
|
+
{ label: 'main.a b', schema: 'main', table: 'a b' },
|
|
87
|
+
{ label: 'main.a.b', schema: 'main', table: 'a.b' },
|
|
88
|
+
{ label: 'main.café', schema: 'main', table: 'café' },
|
|
89
|
+
{ label: "main.o'brien", schema: 'main', table: "o'brien" },
|
|
90
|
+
{ label: 'main.weißbier', schema: 'main', table: 'weißbier' },
|
|
91
|
+
];
|
|
92
|
+
/** Open the store a spec names, through the provider's public surface. */
|
|
93
|
+
function openSpec(provider, spec) {
|
|
94
|
+
return spec.index === undefined
|
|
95
|
+
? provider.getStore(spec.schema, spec.table)
|
|
96
|
+
: provider.getIndexStore(spec.schema, spec.table, spec.index);
|
|
97
|
+
}
|
|
98
|
+
/** Read a store's marker back, or `'<none>'` when the key is absent. */
|
|
99
|
+
async function readMarker(store) {
|
|
100
|
+
const value = await store.get(MARKER_KEY);
|
|
101
|
+
return value === undefined ? '<none>' : decoder.decode(value);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Assert `provider` gives every logical store in {@link DISTINCT_STORES} its OWN physical
|
|
105
|
+
* store: write one marker per store under a single shared key, then check each store still
|
|
106
|
+
* reads back its own. A shared physical store shows up as one marker having replaced
|
|
107
|
+
* another.
|
|
108
|
+
*
|
|
109
|
+
* Exported standalone (like {@link assertBoundedIterate}) so a spec can drive it against a
|
|
110
|
+
* provider double built to violate the property and prove it rejects — a guard nobody has
|
|
111
|
+
* watched fail is not a guard. See `test/store-name-distinctness.spec.ts`.
|
|
112
|
+
*
|
|
113
|
+
* @param provider - A provider over a fresh, EMPTY physical keyspace.
|
|
114
|
+
* @param report - Where to record names the provider refused. Refusal is allowed (a
|
|
115
|
+
* namespace that genuinely cannot represent a name must reject it rather than fold it),
|
|
116
|
+
* but never silently: the battery logs whatever lands here.
|
|
117
|
+
*/
|
|
118
|
+
export async function assertStoreNamesDistinct(provider, report = []) {
|
|
119
|
+
const opened = [];
|
|
120
|
+
for (const spec of DISTINCT_STORES) {
|
|
121
|
+
try {
|
|
122
|
+
opened.push({ spec, store: await openSpec(provider, spec) });
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
// A provider whose native namespace genuinely cannot represent this name is
|
|
126
|
+
// allowed to refuse it — that is the safe outcome. Folding it onto another
|
|
127
|
+
// store is not, and is what the marker check below catches.
|
|
128
|
+
assert.ok(!spec.mustAccept, `${spec.label} uses only [a-z0-9_.] and must be representable, but the provider rejected it: ${String(e)}`);
|
|
129
|
+
report.push(`${spec.label} (${String(e)})`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// No explicit "did enough survive?" check is needed: the `mustAccept` entries above
|
|
133
|
+
// cannot be refused, so the compared set never shrinks below them.
|
|
134
|
+
for (const { spec, store } of opened) {
|
|
135
|
+
await store.put(MARKER_KEY, encoder.encode(spec.label));
|
|
136
|
+
}
|
|
137
|
+
for (const { spec, store } of opened) {
|
|
138
|
+
assert.strictEqual(await readMarker(store), spec.label, `store "${spec.label}" reads back another store's marker — the two share one physical store`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Register the store-name distinctness battery under `describe(name)`. Call once per
|
|
143
|
+
* backend, from the same spec file that registers {@link runKVStoreConformance}.
|
|
144
|
+
*/
|
|
145
|
+
export function runStoreNameDistinctness(name, makeNamingBackend) {
|
|
146
|
+
describe(name, () => {
|
|
147
|
+
let backend;
|
|
148
|
+
let provider;
|
|
149
|
+
beforeEach(async () => {
|
|
150
|
+
backend = makeNamingBackend();
|
|
151
|
+
provider = await backend.open();
|
|
152
|
+
});
|
|
153
|
+
afterEach(async () => {
|
|
154
|
+
await backend.teardown();
|
|
155
|
+
});
|
|
156
|
+
it('gives every distinct logical store its own physical store', async () => {
|
|
157
|
+
const rejected = [];
|
|
158
|
+
await assertStoreNamesDistinct(provider, rejected);
|
|
159
|
+
if (rejected.length > 0) {
|
|
160
|
+
// Visible on a passing run too: a provider that quietly grew its rejection set
|
|
161
|
+
// would otherwise look identical to one that represents every name.
|
|
162
|
+
console.warn(`${name}: names refused by the provider (allowed, but recorded): ${rejected.join('; ')}`);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
it('opens ONE physical store for either spelling of a case-insensitive identifier', async () => {
|
|
166
|
+
// SQL identifiers are case-insensitive, and the shared builders lowercase, so two
|
|
167
|
+
// spellings of one logical store must land on the same physical store — the mirror
|
|
168
|
+
// image of the distinctness property, and just as easy to break (a provider that
|
|
169
|
+
// lowercases only part of the name gets two stores for one index).
|
|
170
|
+
const upperData = await provider.getStore('MAIN', 'T');
|
|
171
|
+
await upperData.put(MARKER_KEY, encoder.encode('data'));
|
|
172
|
+
const lowerData = await provider.getStore('main', 't');
|
|
173
|
+
assertBytes(await lowerData.get(MARKER_KEY), encoder.encode('data'), 'getStore("MAIN","T") and getStore("main","t") must be the same physical store');
|
|
174
|
+
const upperIndex = await provider.getIndexStore('main', 't', 'IdxName');
|
|
175
|
+
await upperIndex.put(MARKER_KEY, encoder.encode('index'));
|
|
176
|
+
const lowerIndex = await provider.getIndexStore('MAIN', 'T', 'idxname');
|
|
177
|
+
assertBytes(await lowerIndex.get(MARKER_KEY), encoder.encode('index'), 'getIndexStore(…,"IdxName") and getIndexStore(…,"idxname") must be the same physical store');
|
|
178
|
+
// …and the index store is still not the data store (a provider could satisfy both
|
|
179
|
+
// checks above by returning one store for everything).
|
|
180
|
+
assert.strictEqual(await readMarker(lowerData), 'data', 'the index write leaked into the data store');
|
|
181
|
+
});
|
|
182
|
+
it('keeps the reserved catalog and stats stores disjoint from same-named tables', async () => {
|
|
183
|
+
const catalog = await provider.getCatalogStore();
|
|
184
|
+
const stats = await provider.getStatsStore('main', 't');
|
|
185
|
+
await catalog.put(MARKER_KEY, encoder.encode('catalog'));
|
|
186
|
+
await stats.put(MARKER_KEY, encoder.encode('stats'));
|
|
187
|
+
assert.strictEqual(await readMarker(catalog), 'catalog', 'the catalog and stats stores are the same physical store');
|
|
188
|
+
// A user table may legally be named `__catalog__` or `__stats__`. Its data store
|
|
189
|
+
// must not land on the reserved store of the same name — providers keep the
|
|
190
|
+
// reserved names in a namespace their encoded table names cannot reach.
|
|
191
|
+
const tableStores = new Map();
|
|
192
|
+
for (const table of ['__catalog__', '__stats__']) {
|
|
193
|
+
const store = await provider.getStore('main', table);
|
|
194
|
+
tableStores.set(table, store);
|
|
195
|
+
await store.put(MARKER_KEY, encoder.encode(`table ${table}`));
|
|
196
|
+
}
|
|
197
|
+
assert.strictEqual(await readMarker(catalog), 'catalog', 'a table named __catalog__ landed on the reserved catalog store');
|
|
198
|
+
assert.strictEqual(await readMarker(stats), 'stats', 'a table named __stats__ landed on the reserved stats store');
|
|
199
|
+
// …and the two user tables are distinct from EACH OTHER, not just from the
|
|
200
|
+
// reserved pair — a provider that folded both onto one store would otherwise
|
|
201
|
+
// leave the reserved markers intact and pass.
|
|
202
|
+
for (const [table, store] of tableStores) {
|
|
203
|
+
assert.strictEqual(await readMarker(store), `table ${table}`, `the user table "${table}" reads back another store's marker`);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=kv-naming-conformance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kv-naming-conformance.js","sourceRoot":"","sources":["../../../src/testing/kv-naming-conformance.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared store-reclaim battery — test-support; run under Mocha.
|
|
3
|
+
*
|
|
4
|
+
* Fourth sibling of {@link runKVStoreConformance} (the {@link KVStore} contract),
|
|
5
|
+
* {@link runKVProviderConformance} (the atomic multi-store commit) and
|
|
6
|
+
* {@link runStoreNameDistinctness} (no two logical stores fold onto one physical store).
|
|
7
|
+
* This one asserts a single PROVIDER-level property that every backend must hold:
|
|
8
|
+
*
|
|
9
|
+
* After a provider is told to delete a table's stores, re-opening those stores by the
|
|
10
|
+
* same names must find them EMPTY — and every other store must be untouched.
|
|
11
|
+
*
|
|
12
|
+
* Why it exists as a shared battery rather than a per-plugin test: `deleteTableStores` /
|
|
13
|
+
* `deleteIndexStore` are how the engine RECLAIMS a dropped table's physical storage
|
|
14
|
+
* (`StoreModuleBase.tearDownTableStorage` on `drop table`, and the generic rename fallback
|
|
15
|
+
* after it has copied the rows to the new name). Store handles are re-opened lazily by
|
|
16
|
+
* name, so a provider that merely closes its handle leaves the bytes in place and the next
|
|
17
|
+
* table created under that name opens the old table's rows — `drop table t; create table t`
|
|
18
|
+
* comes back full. Two of the four shipped providers implemented exactly that, and nothing
|
|
19
|
+
* in the type system stops the next one, so the property is pinned behaviorally here.
|
|
20
|
+
*
|
|
21
|
+
* The battery needs nothing beyond the public {@link KVStoreProvider} surface, so it runs
|
|
22
|
+
* for every backend: it writes a marker into each store, deletes, and re-opens by the same
|
|
23
|
+
* name. One case seeds {@link BULK_ROW_COUNT} rows instead of a marker, because a provider
|
|
24
|
+
* with no native "empty this store" has to walk the keyspace in bounded chunks and only a
|
|
25
|
+
* store bigger than one chunk exercises resuming between passes.
|
|
26
|
+
*
|
|
27
|
+
* Both delete hooks are OPTIONAL in {@link KVStoreProvider}; a provider that omits one
|
|
28
|
+
* is skipped rather than crashed — but the skip is printed, so a provider cannot silently
|
|
29
|
+
* opt out of the property.
|
|
30
|
+
*
|
|
31
|
+
* Dependency and Mocha-global handling mirror kv-conformance.ts — see its header.
|
|
32
|
+
*
|
|
33
|
+
* NOTE: "empty" is asserted through the two read paths a caller can use — a point `get` of
|
|
34
|
+
* the marker key and a full `iterate()` — because a backend could plausibly satisfy one and
|
|
35
|
+
* not the other (an index-only delete, a tombstone a point read honors but a scan does not).
|
|
36
|
+
* Nothing here asserts that the physical storage was RELEASED: a provider is free to leave
|
|
37
|
+
* an empty database directory or unshrunk file behind, which is what the LevelDB-family
|
|
38
|
+
* providers do. Reclaiming bytes on disk is a per-provider concern; not serving stale rows
|
|
39
|
+
* is the contract.
|
|
40
|
+
*/
|
|
41
|
+
import type { KVProviderLifecycle } from './kv-lifecycle.js';
|
|
42
|
+
/**
|
|
43
|
+
* Per-backend lifecycle adapter — the same shape {@link runStoreNameDistinctness} takes, so
|
|
44
|
+
* a plugin's spec can hand one closure to both batteries.
|
|
45
|
+
*/
|
|
46
|
+
export type KVReclaimBackend = KVProviderLifecycle;
|
|
47
|
+
/**
|
|
48
|
+
* Register the store-reclaim battery under `describe(name)`. Call once per backend, from
|
|
49
|
+
* the same spec file that registers {@link runKVStoreConformance}.
|
|
50
|
+
*/
|
|
51
|
+
export declare function runStoreReclaimConformance(name: string, makeReclaimBackend: () => KVReclaimBackend): void;
|
|
52
|
+
//# sourceMappingURL=kv-reclaim-conformance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kv-reclaim-conformance.d.ts","sourceRoot":"","sources":["../../../src/testing/kv-reclaim-conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAKH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAU7D;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AA+InD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,GAAG,IAAI,CA0LzG"}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared store-reclaim battery — test-support; run under Mocha.
|
|
3
|
+
*
|
|
4
|
+
* Fourth sibling of {@link runKVStoreConformance} (the {@link KVStore} contract),
|
|
5
|
+
* {@link runKVProviderConformance} (the atomic multi-store commit) and
|
|
6
|
+
* {@link runStoreNameDistinctness} (no two logical stores fold onto one physical store).
|
|
7
|
+
* This one asserts a single PROVIDER-level property that every backend must hold:
|
|
8
|
+
*
|
|
9
|
+
* After a provider is told to delete a table's stores, re-opening those stores by the
|
|
10
|
+
* same names must find them EMPTY — and every other store must be untouched.
|
|
11
|
+
*
|
|
12
|
+
* Why it exists as a shared battery rather than a per-plugin test: `deleteTableStores` /
|
|
13
|
+
* `deleteIndexStore` are how the engine RECLAIMS a dropped table's physical storage
|
|
14
|
+
* (`StoreModuleBase.tearDownTableStorage` on `drop table`, and the generic rename fallback
|
|
15
|
+
* after it has copied the rows to the new name). Store handles are re-opened lazily by
|
|
16
|
+
* name, so a provider that merely closes its handle leaves the bytes in place and the next
|
|
17
|
+
* table created under that name opens the old table's rows — `drop table t; create table t`
|
|
18
|
+
* comes back full. Two of the four shipped providers implemented exactly that, and nothing
|
|
19
|
+
* in the type system stops the next one, so the property is pinned behaviorally here.
|
|
20
|
+
*
|
|
21
|
+
* The battery needs nothing beyond the public {@link KVStoreProvider} surface, so it runs
|
|
22
|
+
* for every backend: it writes a marker into each store, deletes, and re-opens by the same
|
|
23
|
+
* name. One case seeds {@link BULK_ROW_COUNT} rows instead of a marker, because a provider
|
|
24
|
+
* with no native "empty this store" has to walk the keyspace in bounded chunks and only a
|
|
25
|
+
* store bigger than one chunk exercises resuming between passes.
|
|
26
|
+
*
|
|
27
|
+
* Both delete hooks are OPTIONAL in {@link KVStoreProvider}; a provider that omits one
|
|
28
|
+
* is skipped rather than crashed — but the skip is printed, so a provider cannot silently
|
|
29
|
+
* opt out of the property.
|
|
30
|
+
*
|
|
31
|
+
* Dependency and Mocha-global handling mirror kv-conformance.ts — see its header.
|
|
32
|
+
*
|
|
33
|
+
* NOTE: "empty" is asserted through the two read paths a caller can use — a point `get` of
|
|
34
|
+
* the marker key and a full `iterate()` — because a backend could plausibly satisfy one and
|
|
35
|
+
* not the other (an index-only delete, a tombstone a point read honors but a scan does not).
|
|
36
|
+
* Nothing here asserts that the physical storage was RELEASED: a provider is free to leave
|
|
37
|
+
* an empty database directory or unshrunk file behind, which is what the LevelDB-family
|
|
38
|
+
* providers do. Reclaiming bytes on disk is a per-provider concern; not serving stale rows
|
|
39
|
+
* is the contract.
|
|
40
|
+
*/
|
|
41
|
+
import assert from 'node:assert/strict';
|
|
42
|
+
import { buildDataStoreName, buildIndexStoreName } from '../common/key-builder.js';
|
|
43
|
+
import { b } from './kv-assert.js';
|
|
44
|
+
/** The schema every store in this battery lives in. */
|
|
45
|
+
const SCHEMA = 'main';
|
|
46
|
+
const encoder = new TextEncoder();
|
|
47
|
+
const decoder = new TextDecoder();
|
|
48
|
+
/** The key each store's marker is written under before a delete. */
|
|
49
|
+
const MARKER_KEY = b(0x01);
|
|
50
|
+
/** A second key, written into a store RE-CREATED after a delete. */
|
|
51
|
+
const REUSE_KEY = b(0x02);
|
|
52
|
+
/**
|
|
53
|
+
* Rows seeded into one store by the bulk case, chosen to exceed the largest per-pass chunk
|
|
54
|
+
* any shipped provider erases in (`@quereus/plugin-react-native-leveldb` walks the keyspace
|
|
55
|
+
* 512 keys at a time), so a chunked erase has to make several passes and resume correctly
|
|
56
|
+
* between them. A provider that erases natively is unaffected by the size.
|
|
57
|
+
*/
|
|
58
|
+
const BULK_ROW_COUNT = 1100;
|
|
59
|
+
/** The `i`-th bulk key — two bytes, so it can never collide with the 1-byte markers. */
|
|
60
|
+
function bulkKey(i) {
|
|
61
|
+
return b((i >> 8) & 0xff, i & 0xff);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Battery+hook pairs already reported as skipped, so one omitted hook prints once rather
|
|
65
|
+
* than once per test.
|
|
66
|
+
*/
|
|
67
|
+
const reportedSkips = new Set();
|
|
68
|
+
/**
|
|
69
|
+
* True when `provider` implements `hook`. A provider that omits one is skipped rather than
|
|
70
|
+
* failed — the hook is genuinely optional — but the skip is printed, because a silent skip
|
|
71
|
+
* is indistinguishable from a passing provider.
|
|
72
|
+
*/
|
|
73
|
+
function hasHook(name, provider, hook) {
|
|
74
|
+
if (typeof provider[hook] === 'function')
|
|
75
|
+
return true;
|
|
76
|
+
const key = `${name}:${hook}`;
|
|
77
|
+
if (!reportedSkips.has(key)) {
|
|
78
|
+
reportedSkips.add(key);
|
|
79
|
+
console.warn(`${name}: SKIPPED — the provider does not implement ${hook}(), so its reclaim property is unenforced`);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
/** Call `deleteTableStores`, which the caller has already checked is present. */
|
|
84
|
+
async function deleteTableStores(provider, tableName, indexNames) {
|
|
85
|
+
const hook = provider.deleteTableStores;
|
|
86
|
+
assert.ok(hook, 'deleteTableStores must be present here — callers check hasHook first');
|
|
87
|
+
await hook.call(provider, SCHEMA, tableName, indexNames);
|
|
88
|
+
}
|
|
89
|
+
/** Call `deleteIndexStore`, which the caller has already checked is present. */
|
|
90
|
+
async function deleteIndexStore(provider, tableName, indexName) {
|
|
91
|
+
const hook = provider.deleteIndexStore;
|
|
92
|
+
assert.ok(hook, 'deleteIndexStore must be present here — callers check hasHook first');
|
|
93
|
+
await hook.call(provider, SCHEMA, tableName, indexName);
|
|
94
|
+
}
|
|
95
|
+
/** Lowercase hex of a key, for a legible dump of a store that should have been emptied. */
|
|
96
|
+
function hex(bytes) {
|
|
97
|
+
let out = '';
|
|
98
|
+
for (const byte of bytes)
|
|
99
|
+
out += byte.toString(16).padStart(2, '0');
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/** Every entry in a store as `{key hex}={marker}`, in iteration order. */
|
|
103
|
+
async function dump(store) {
|
|
104
|
+
const entries = [];
|
|
105
|
+
for await (const { key, value } of store.iterate()) {
|
|
106
|
+
entries.push(`${hex(key)}=${decoder.decode(value)}`);
|
|
107
|
+
}
|
|
108
|
+
return entries;
|
|
109
|
+
}
|
|
110
|
+
/** How many entries a store yields on a full scan. */
|
|
111
|
+
async function countEntries(store) {
|
|
112
|
+
let count = 0;
|
|
113
|
+
for await (const _entry of store.iterate())
|
|
114
|
+
count++;
|
|
115
|
+
return count;
|
|
116
|
+
}
|
|
117
|
+
/** Read a store's marker back, or `undefined` when the key is absent. */
|
|
118
|
+
async function readMarker(store) {
|
|
119
|
+
const value = await store.get(MARKER_KEY);
|
|
120
|
+
return value === undefined ? undefined : decoder.decode(value);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Write one table's marker into its data store and into each of its index stores. The
|
|
124
|
+
* marker is the store's own logical name, so a failure names the store that leaked.
|
|
125
|
+
*/
|
|
126
|
+
async function seedTable(provider, tableName, indexNames = []) {
|
|
127
|
+
const data = await provider.getStore(SCHEMA, tableName);
|
|
128
|
+
await data.put(MARKER_KEY, encoder.encode(buildDataStoreName(SCHEMA, tableName)));
|
|
129
|
+
for (const indexName of indexNames) {
|
|
130
|
+
const index = await provider.getIndexStore(SCHEMA, tableName, indexName);
|
|
131
|
+
await index.put(MARKER_KEY, encoder.encode(buildIndexStoreName(SCHEMA, tableName, indexName)));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Assert a store re-opened after a delete reads back empty through BOTH read paths. */
|
|
135
|
+
async function assertEmpty(store, label) {
|
|
136
|
+
assert.strictEqual(await readMarker(store), undefined, `${label} still serves its pre-delete row on a point read — the delete did not erase it`);
|
|
137
|
+
assert.deepStrictEqual(await dump(store), [], `${label} still yields entries on a scan — the delete did not erase it`);
|
|
138
|
+
}
|
|
139
|
+
/** Assert a store the delete must not have touched still holds its own marker. */
|
|
140
|
+
async function assertMarkerIntact(store, expected) {
|
|
141
|
+
assert.strictEqual(await readMarker(store), expected, `store "${expected}" lost its row to an unrelated delete`);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Register the store-reclaim battery under `describe(name)`. Call once per backend, from
|
|
145
|
+
* the same spec file that registers {@link runKVStoreConformance}.
|
|
146
|
+
*/
|
|
147
|
+
export function runStoreReclaimConformance(name, makeReclaimBackend) {
|
|
148
|
+
describe(name, () => {
|
|
149
|
+
let backend;
|
|
150
|
+
let provider;
|
|
151
|
+
beforeEach(async () => {
|
|
152
|
+
backend = makeReclaimBackend();
|
|
153
|
+
provider = await backend.open();
|
|
154
|
+
});
|
|
155
|
+
afterEach(async () => {
|
|
156
|
+
await backend.teardown();
|
|
157
|
+
});
|
|
158
|
+
it('erases the data store and every named index store on deleteTableStores', async () => {
|
|
159
|
+
if (!hasHook(name, provider, 'deleteTableStores'))
|
|
160
|
+
return;
|
|
161
|
+
await seedTable(provider, 't', ['x', 'y']);
|
|
162
|
+
await deleteTableStores(provider, 't', ['x', 'y']);
|
|
163
|
+
// Re-open by the SAME names — the whole defect is that a lazily re-opened store
|
|
164
|
+
// serves the deleted table's rows.
|
|
165
|
+
await assertEmpty(await provider.getStore(SCHEMA, 't'), 'the data store of main.t');
|
|
166
|
+
await assertEmpty(await provider.getIndexStore(SCHEMA, 't', 'x'), 'index x of main.t');
|
|
167
|
+
await assertEmpty(await provider.getIndexStore(SCHEMA, 't', 'y'), 'index y of main.t');
|
|
168
|
+
});
|
|
169
|
+
it('gives a table re-created under a dropped name a store holding only its OWN rows', async () => {
|
|
170
|
+
if (!hasHook(name, provider, 'deleteTableStores'))
|
|
171
|
+
return;
|
|
172
|
+
await seedTable(provider, 't', ['x']);
|
|
173
|
+
await deleteTableStores(provider, 't', ['x']);
|
|
174
|
+
// The user-visible corruption scenario: `drop table t; create table t (...)`
|
|
175
|
+
// followed by an insert must leave exactly the new row — not the new row on top
|
|
176
|
+
// of the old table's.
|
|
177
|
+
const data = await provider.getStore(SCHEMA, 't');
|
|
178
|
+
await data.put(REUSE_KEY, encoder.encode('new data row'));
|
|
179
|
+
assert.deepStrictEqual(await dump(data), [`${hex(REUSE_KEY)}=new data row`], 'a table re-created under a dropped name reads back the dropped table\'s rows');
|
|
180
|
+
const index = await provider.getIndexStore(SCHEMA, 't', 'x');
|
|
181
|
+
await index.put(REUSE_KEY, encoder.encode('new index row'));
|
|
182
|
+
assert.deepStrictEqual(await dump(index), [`${hex(REUSE_KEY)}=new index row`], 'an index re-created under a dropped name reads back the dropped index\'s entries');
|
|
183
|
+
});
|
|
184
|
+
it('erases a store holding more rows than a provider clears in one pass', async () => {
|
|
185
|
+
if (!hasHook(name, provider, 'deleteTableStores'))
|
|
186
|
+
return;
|
|
187
|
+
// A provider with no native "empty this store" has to walk the keyspace in bounded
|
|
188
|
+
// chunks, so the whole resume-between-passes path only runs above its chunk size —
|
|
189
|
+
// a walk that restarts (or stops early) after each pass is invisible below it.
|
|
190
|
+
const data = await provider.getStore(SCHEMA, 't');
|
|
191
|
+
const seed = data.batch();
|
|
192
|
+
for (let i = 0; i < BULK_ROW_COUNT; i++) {
|
|
193
|
+
seed.put(bulkKey(i), encoder.encode(String(i)));
|
|
194
|
+
}
|
|
195
|
+
await seed.write();
|
|
196
|
+
assert.strictEqual(await countEntries(data), BULK_ROW_COUNT, 'the bulk seed itself did not land');
|
|
197
|
+
await deleteTableStores(provider, 't', []);
|
|
198
|
+
assert.strictEqual(await countEntries(await provider.getStore(SCHEMA, 't')), 0, `the data store of main.t still holds rows after a delete of ${BULK_ROW_COUNT} of them — a chunked erase that does not resume past its last key leaves a tail behind`);
|
|
199
|
+
});
|
|
200
|
+
it('erases only the named index store on deleteIndexStore', async () => {
|
|
201
|
+
if (!hasHook(name, provider, 'deleteIndexStore'))
|
|
202
|
+
return;
|
|
203
|
+
await seedTable(provider, 't', ['x', 'y']);
|
|
204
|
+
await deleteIndexStore(provider, 't', 'x');
|
|
205
|
+
await assertEmpty(await provider.getIndexStore(SCHEMA, 't', 'x'), 'index x of main.t');
|
|
206
|
+
await assertMarkerIntact(await provider.getStore(SCHEMA, 't'), buildDataStoreName(SCHEMA, 't'));
|
|
207
|
+
await assertMarkerIntact(await provider.getIndexStore(SCHEMA, 't', 'y'), buildIndexStoreName(SCHEMA, 't', 'y'));
|
|
208
|
+
});
|
|
209
|
+
it('erases a store whose handle the caller already closed', async () => {
|
|
210
|
+
if (!hasHook(name, provider, 'deleteIndexStore'))
|
|
211
|
+
return;
|
|
212
|
+
// The order every `drop index` actually takes: `StoreModuleIndex.tearDownIndexStore`
|
|
213
|
+
// closes the table's cached index handle (`StoreTableBase.releaseIndexStore`) and
|
|
214
|
+
// THEN asks the provider to delete that store. A provider whose delete reads or
|
|
215
|
+
// writes through its own cached handle finds it dead at that point, so this is the
|
|
216
|
+
// path a battery driving only live handles would never exercise.
|
|
217
|
+
await seedTable(provider, 't', ['x']);
|
|
218
|
+
const index = await provider.getIndexStore(SCHEMA, 't', 'x');
|
|
219
|
+
await index.close();
|
|
220
|
+
await deleteIndexStore(provider, 't', 'x');
|
|
221
|
+
await assertEmpty(await provider.getIndexStore(SCHEMA, 't', 'x'), 'index x of main.t');
|
|
222
|
+
});
|
|
223
|
+
it('leaves sibling tables untouched when a table is deleted', async () => {
|
|
224
|
+
if (!hasHook(name, provider, 'deleteTableStores'))
|
|
225
|
+
return;
|
|
226
|
+
await seedTable(provider, 't', ['x']);
|
|
227
|
+
await seedTable(provider, 't2');
|
|
228
|
+
// `_idx_` is a legal substring of an ordinary table name, which is exactly why the
|
|
229
|
+
// contract forbids reclaiming by a `{table}_idx_` prefix scan: this sibling table's
|
|
230
|
+
// DATA store is named `main.t_idx_y`, which such a scan would sweep up along with
|
|
231
|
+
// table t's index stores. (The near-miss `t_idx_x` is deliberately not used — it
|
|
232
|
+
// composes to the same logical name as index `x` on table `t`, so no provider can
|
|
233
|
+
// tell the two apart; `StoreModuleBase.assertStoreNameFree` is what keeps that pair
|
|
234
|
+
// from ever coexisting. Same exclusion as `runStoreNameDistinctness`.)
|
|
235
|
+
await seedTable(provider, 't_idx_y');
|
|
236
|
+
await deleteTableStores(provider, 't', ['x']);
|
|
237
|
+
await assertMarkerIntact(await provider.getStore(SCHEMA, 't2'), buildDataStoreName(SCHEMA, 't2'));
|
|
238
|
+
await assertMarkerIntact(await provider.getStore(SCHEMA, 't_idx_y'), buildDataStoreName(SCHEMA, 't_idx_y'));
|
|
239
|
+
});
|
|
240
|
+
it('leaves an index store the caller did not name untouched', async () => {
|
|
241
|
+
if (!hasHook(name, provider, 'deleteTableStores'))
|
|
242
|
+
return;
|
|
243
|
+
// `indexNames` is the authoritative list, and reclaiming anything outside it means
|
|
244
|
+
// the provider found the store some other way — a `{table}_idx_` prefix scan being
|
|
245
|
+
// the way that also destroys a sibling table's data (the case above). Storage for
|
|
246
|
+
// an index left out of the list leaks; that is the caller's bug to fix, not licence
|
|
247
|
+
// for the provider to guess.
|
|
248
|
+
await seedTable(provider, 't', ['x', 'unnamed']);
|
|
249
|
+
await deleteTableStores(provider, 't', ['x']);
|
|
250
|
+
await assertEmpty(await provider.getStore(SCHEMA, 't'), 'the data store of main.t');
|
|
251
|
+
await assertEmpty(await provider.getIndexStore(SCHEMA, 't', 'x'), 'index x of main.t');
|
|
252
|
+
await assertMarkerIntact(await provider.getIndexStore(SCHEMA, 't', 'unnamed'), buildIndexStoreName(SCHEMA, 't', 'unnamed'));
|
|
253
|
+
});
|
|
254
|
+
it('leaves the reserved stats and catalog stores untouched when a table is deleted', async () => {
|
|
255
|
+
if (!hasHook(name, provider, 'deleteTableStores'))
|
|
256
|
+
return;
|
|
257
|
+
const stats = await provider.getStatsStore(SCHEMA, 't');
|
|
258
|
+
const catalog = await provider.getCatalogStore();
|
|
259
|
+
await stats.put(MARKER_KEY, encoder.encode('stats'));
|
|
260
|
+
await catalog.put(MARKER_KEY, encoder.encode('catalog'));
|
|
261
|
+
await seedTable(provider, 't', ['x']);
|
|
262
|
+
await deleteTableStores(provider, 't', ['x']);
|
|
263
|
+
// Re-fetched through the provider rather than reusing the handles above: a provider
|
|
264
|
+
// that dropped the reserved store's physical storage while holding a live handle
|
|
265
|
+
// could otherwise still answer from it. (A provider is free to hand back the same
|
|
266
|
+
// cached handle — that is a pass, since nothing was deleted.)
|
|
267
|
+
await assertMarkerIntact(await provider.getStatsStore(SCHEMA, 't'), 'stats');
|
|
268
|
+
await assertMarkerIntact(await provider.getCatalogStore(), 'catalog');
|
|
269
|
+
});
|
|
270
|
+
it('treats deleting stores that were never created as a no-op', async () => {
|
|
271
|
+
// The engine calls these speculatively on its reclaim paths — dropping a table
|
|
272
|
+
// whose storage a previous drop already removed must succeed, not throw
|
|
273
|
+
// (`StoreModuleBase.reclaimDetachedTable` documents the idempotence it relies on).
|
|
274
|
+
if (hasHook(name, provider, 'deleteTableStores')) {
|
|
275
|
+
await deleteTableStores(provider, 'never-created', []);
|
|
276
|
+
await deleteTableStores(provider, 'never-created', ['nor-this-index']);
|
|
277
|
+
}
|
|
278
|
+
if (hasHook(name, provider, 'deleteIndexStore')) {
|
|
279
|
+
await seedTable(provider, 't');
|
|
280
|
+
// An index name the table never had — the store behind it does not exist.
|
|
281
|
+
await deleteIndexStore(provider, 't', 'never-created');
|
|
282
|
+
await assertMarkerIntact(await provider.getStore(SCHEMA, 't'), buildDataStoreName(SCHEMA, 't'));
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=kv-reclaim-conformance.js.map
|