@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.
Files changed (77) hide show
  1. package/README.md +131 -10
  2. package/dist/src/common/cached-kv-store.d.ts +16 -0
  3. package/dist/src/common/cached-kv-store.d.ts.map +1 -1
  4. package/dist/src/common/cached-kv-store.js +43 -0
  5. package/dist/src/common/cached-kv-store.js.map +1 -1
  6. package/dist/src/common/index.d.ts +1 -0
  7. package/dist/src/common/index.d.ts.map +1 -1
  8. package/dist/src/common/index.js +2 -0
  9. package/dist/src/common/index.js.map +1 -1
  10. package/dist/src/common/key-builder.d.ts +9 -6
  11. package/dist/src/common/key-builder.d.ts.map +1 -1
  12. package/dist/src/common/key-builder.js +17 -6
  13. package/dist/src/common/key-builder.js.map +1 -1
  14. package/dist/src/common/kv-store.d.ts +89 -3
  15. package/dist/src/common/kv-store.d.ts.map +1 -1
  16. package/dist/src/common/kv-store.js +21 -1
  17. package/dist/src/common/kv-store.js.map +1 -1
  18. package/dist/src/common/memory-store.d.ts +1 -0
  19. package/dist/src/common/memory-store.d.ts.map +1 -1
  20. package/dist/src/common/memory-store.js +7 -0
  21. package/dist/src/common/memory-store.js.map +1 -1
  22. package/dist/src/common/pk-key-resolution.d.ts +17 -4
  23. package/dist/src/common/pk-key-resolution.d.ts.map +1 -1
  24. package/dist/src/common/pk-key-resolution.js +23 -8
  25. package/dist/src/common/pk-key-resolution.js.map +1 -1
  26. package/dist/src/common/store-module-access-plan.d.ts +1 -1
  27. package/dist/src/common/store-module-access-plan.d.ts.map +1 -1
  28. package/dist/src/common/store-module-access-plan.js +434 -105
  29. package/dist/src/common/store-module-access-plan.js.map +1 -1
  30. package/dist/src/common/store-module-alter-column.d.ts.map +1 -1
  31. package/dist/src/common/store-module-alter-column.js +25 -2
  32. package/dist/src/common/store-module-alter-column.js.map +1 -1
  33. package/dist/src/common/store-module-alter.d.ts.map +1 -1
  34. package/dist/src/common/store-module-alter.js +36 -13
  35. package/dist/src/common/store-module-alter.js.map +1 -1
  36. package/dist/src/common/store-module-catalog.d.ts +6 -4
  37. package/dist/src/common/store-module-catalog.d.ts.map +1 -1
  38. package/dist/src/common/store-module-catalog.js +8 -6
  39. package/dist/src/common/store-module-catalog.js.map +1 -1
  40. package/dist/src/common/store-module-index.d.ts +98 -7
  41. package/dist/src/common/store-module-index.d.ts.map +1 -1
  42. package/dist/src/common/store-module-index.js +236 -81
  43. package/dist/src/common/store-module-index.js.map +1 -1
  44. package/dist/src/common/store-module.d.ts.map +1 -1
  45. package/dist/src/common/store-module.js +7 -3
  46. package/dist/src/common/store-module.js.map +1 -1
  47. package/dist/src/common/store-table-base.d.ts +30 -0
  48. package/dist/src/common/store-table-base.d.ts.map +1 -1
  49. package/dist/src/common/store-table-base.js +71 -12
  50. package/dist/src/common/store-table-base.js.map +1 -1
  51. package/dist/src/common/store-table-constraints.js +3 -3
  52. package/dist/src/common/store-table-constraints.js.map +1 -1
  53. package/dist/src/common/store-table-scan.d.ts +208 -98
  54. package/dist/src/common/store-table-scan.d.ts.map +1 -1
  55. package/dist/src/common/store-table-scan.js +353 -105
  56. package/dist/src/common/store-table-scan.js.map +1 -1
  57. package/dist/src/testing/index.d.ts +17 -5
  58. package/dist/src/testing/index.d.ts.map +1 -1
  59. package/dist/src/testing/index.js +16 -5
  60. package/dist/src/testing/index.js.map +1 -1
  61. package/dist/src/testing/kv-conformance.d.ts +49 -0
  62. package/dist/src/testing/kv-conformance.d.ts.map +1 -1
  63. package/dist/src/testing/kv-conformance.js +125 -0
  64. package/dist/src/testing/kv-conformance.js.map +1 -1
  65. package/dist/src/testing/kv-lifecycle.d.ts +22 -0
  66. package/dist/src/testing/kv-lifecycle.d.ts.map +1 -0
  67. package/dist/src/testing/kv-lifecycle.js +12 -0
  68. package/dist/src/testing/kv-lifecycle.js.map +1 -0
  69. package/dist/src/testing/kv-naming-conformance.d.ts +77 -0
  70. package/dist/src/testing/kv-naming-conformance.d.ts.map +1 -0
  71. package/dist/src/testing/kv-naming-conformance.js +208 -0
  72. package/dist/src/testing/kv-naming-conformance.js.map +1 -0
  73. package/dist/src/testing/kv-reclaim-conformance.d.ts +52 -0
  74. package/dist/src/testing/kv-reclaim-conformance.d.ts.map +1 -0
  75. package/dist/src/testing/kv-reclaim-conformance.js +287 -0
  76. package/dist/src/testing/kv-reclaim-conformance.js.map +1 -0
  77. 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