flatsql 3.0.0 → 3.2.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.
|
@@ -109,14 +109,15 @@ FLATSQL_PS_WASMEDGE=1 npm test -- test/ps-parity-vectors.test.ts # + the C run
|
|
|
109
109
|
|
|
110
110
|
Machine: the owner's Mac Studio (Darwin 25.3.0, arm64, 28 hardware threads,
|
|
111
111
|
APFS), Node 25.4.0, shared with other lanes: 1-minute load average 18–25
|
|
112
|
-
during these runs
|
|
112
|
+
during these runs (37–41 for the release rerun, §4.2). Native builds
|
|
113
|
+
RelWithDebInfo; wasm builds as §1.
|
|
113
114
|
|
|
114
115
|
| # | Acceptance | Result |
|
|
115
116
|
|---|---|---|
|
|
116
117
|
| 1 | Imports exactly WASI p1 + `wasi.thread-spawn` + shared `env.memory` (≤ 32768 pages) + the 7 `env.flatsql_io_*`; exports `wasi_thread_start`; 0 emscripten imports; passes `assertPthreadArtifact` | **Pass.** §1 table; `scripts/check-wasm-imports.mjs`, `test/standalone-wasm-imports.test.ts` (the SDK guard runs in a child process). |
|
|
117
118
|
| 2 | The T1 and T2 suites, including 1,000 fault crashes, pass under the Node host with distinct OS thread ids ≥ writers + lanes | **Pass.** `scripts/ps-wasm-suite.mjs`: all 53 default-suite tests, each in its own guest process, plus `crash_faults_T1_1 --crash-trials=1000` (1,000 trials over the five modes, 298,923 acked records verified, 41.8 s). `readers_under_saturating_writers_T2_1` (8 writers, 16 lanes) ran 54 guest threads at once on 54 distinct worker (OS) threads. `test/ps-node-threads.test.ts`: the shipped artifact as a writer instance (4 writers) and a reader instance (4 lanes), each running thread on its own worker. `writer_backpressure_flood_T1_5` runs with `--bp-samples=40` (deviation 3). `crash_faults_T1_1_full` (10,000 trials, 64 types, 256 partitions): §4.1. |
|
|
118
119
|
| 3 | Parity: canonical dumps (by (pid, pseq)) and query results identical native vs Node; deterministic mode byte-identical across native, Node and the SDK WasmEdge C runner (patched 0.16.4) | **Pass.** Canonical dump, 4 writers, 11,313 lines (rows, counters, lane counters and six queries, partition and type level): sha256 `f3b419a2…` natively and under Node, three runs each. Deterministic mode, 122 files: in memory `e7ab8592…` natively, under Node and under the C runner (18.7 s, interpreted); on the host's files `c7e34c55…` natively and under Node. `test/ps-parity-vectors.test.ts`. |
|
|
119
|
-
| 4 | A published npm version with provenance; the wasm sha256 in `integrity.json`; `npm ls sql.js` empty; the package root exports only the wasm bindings |
|
|
120
|
+
| 4 | A published npm version with provenance; the wasm sha256 in `integrity.json`; `npm ls sql.js` empty; the package root exports only the wasm bindings | **Pass.** `flatsql@3.0.0`, §4.2 and §6. |
|
|
120
121
|
|
|
121
122
|
Host-level crashes (the overlay, §2): `test/ps-host-fault.test.ts` kills the
|
|
122
123
|
guest mid-ingest at a random 0.4–2.0 s, crashes the store in the three modes
|
|
@@ -140,7 +141,27 @@ journal. A negative control (half a data segment cut off) fails the verifier.
|
|
|
140
141
|
|
|
141
142
|
### 4.2 Release
|
|
142
143
|
|
|
143
|
-
|
|
144
|
+
- `flatsql@3.0.0`, published by `npm-publish.yml` from tag `v3.0.0` =
|
|
145
|
+
`79fd694` (run 36486543061) with a signed provenance statement
|
|
146
|
+
(`npm audit signatures`: verified attestation); `gitHead` 79fd694, dist-tag
|
|
147
|
+
`latest`, `dependencies` `{}`.
|
|
148
|
+
- `wasm/flatsql-ps-threads.wasm`: 2,074,707 bytes, sha256
|
|
149
|
+
`572a4bc38345d07ff91c345fc8d81e28873d72efa0fb8864a2edc5242145f9f5`, the same
|
|
150
|
+
in `wasm/integrity.json`, from the Linux arm64 Docker build, the x86_64
|
|
151
|
+
publish runner and the installed tarball.
|
|
152
|
+
- A fresh `npm install flatsql@3.0.0`: `npm ls sql.js` is empty; the root's
|
|
153
|
+
exports equal `flatsql/wasm`'s; `loadFlatSQLPsThreads()` checks the bytes
|
|
154
|
+
against `integrity.json`.
|
|
155
|
+
- At the release sources (`dac8d89` engine), under the Node host: the 55
|
|
156
|
+
default-suite tests and `crash_faults_T1_1 --crash-trials=1000` pass (1,000
|
|
157
|
+
trials, 403,798 acked records verified, 47.3 s; load 37–41). Main CI green
|
|
158
|
+
on `79fd694` (run 36482613908).
|
|
159
|
+
- Two engine defects the Node host found before the release, fixed in the
|
|
160
|
+
sources the artifact builds: the open path dropped the lane checkpoint
|
|
161
|
+
pointer of a head with more than 32 live lanes (`5c7e769`; the 1,000-trial
|
|
162
|
+
crash run, trial 317), and the type owner never merged its L0 directory
|
|
163
|
+
when `mergeL0Blocks` exceeded the directory cap, so labeling, partition
|
|
164
|
+
merges and acks stopped (`dac8d89`).
|
|
144
165
|
|
|
145
166
|
## 5. Design deviations
|
|
146
167
|
|
package/docs/PARTITION-STORE.md
CHANGED
|
@@ -4,13 +4,16 @@ The engine of the partition store program. Part I (§1–§11) is T1: append-onl
|
|
|
4
4
|
partition logs, one writer per partition on a pinned pool, durable acks,
|
|
5
5
|
per-commit indexes, type owners, and durable-tail open. Part II (§12–§21) is
|
|
6
6
|
T2: reader instances and lanes, the snapshot protocol, the SQL surface, the
|
|
7
|
-
fan-out merge, admission and results.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
fan-out merge, admission and results. Part III (§23–§30) is T3: compaction,
|
|
8
|
+
reclamation, meta-segment retirement, disk accounting and quota. Part IV
|
|
9
|
+
(§31–) is T3b: store-migrate gseqs, the entry-size setting, hot-partition
|
|
10
|
+
splitting and arrivals compaction. The design is
|
|
11
|
+
the stack's `docs/architecture/flatsql-partition-store.md` (§22 overrides
|
|
12
|
+
§0–§21; §22.4 holds the owner rulings). This file records what was built, how
|
|
13
|
+
to run it, the measured acceptance, and every place the build departs from the
|
|
14
|
+
design.
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
integration are T6/T10.
|
|
16
|
+
The wasm artifact is T4; SDN and browser integration are T6/T10.
|
|
14
17
|
|
|
15
18
|
## 1. Code map
|
|
16
19
|
|
|
@@ -633,3 +636,445 @@ engine. If the bound fails there, the next steps are the design's own levers:
|
|
|
633
636
|
fewer writer threads than cores (design §5.1: `clamp(cores−2, 1, 16)`), interactive
|
|
634
637
|
lanes at a higher priority than writers (design A28 gives nice −5 only on ≤ 2 vCPU),
|
|
635
638
|
and idle-lane sub-cursors (§21 deviation 1).
|
|
639
|
+
|
|
640
|
+
# Part III: compaction, reclamation and quota (T3)
|
|
641
|
+
|
|
642
|
+
## 23. Code map
|
|
643
|
+
|
|
644
|
+
| Path | Contents |
|
|
645
|
+
|---|---|
|
|
646
|
+
| `ps/compaction.h`, `src/ps/compaction.cpp` | Manifest version 2, the compacted rows format, retire items; the compaction pipeline (plan, build, SWAP, apply, abort). |
|
|
647
|
+
| `src/ps/reclaim.cpp` | The per-partition file ledger (`disk_bytes`), the RETIRE and UNLINKED records, reader-gated unlinking, meta-segment retirement (A9), open-time cleanup; the same for the type logs (meta log rotation and retirement, retired catalog runs and manifests, the type ledger). |
|
|
648
|
+
| `ps/quota.h`, `src/ps/quota.cpp` | The quota planner, the space emergency, the ballast. |
|
|
649
|
+
| `src/ps/sources-t3.cmake` | Registers the T3 sources and tests on the targets `cpp/CMakeLists.txt` defines. |
|
|
650
|
+
| `cpp/test/ps/{compaction,orphan,quota}_test.cpp` | T3 tests. `checkPartitionDir` and `checkTypeDir` (fixtures) walk a partition's or a type's directory against the files its head and manifest name. |
|
|
651
|
+
|
|
652
|
+
## 24. Compaction (§11, A11, minor 1)
|
|
653
|
+
|
|
654
|
+
A compaction rewrites a run of sealed, fully merged segments `[seg, segEnd]`
|
|
655
|
+
into one new file set, `c-<seg>-<gen>.{fsd,fsr,fsa}` plus
|
|
656
|
+
`x-<seg>-<gen>.fsx` and `mf-<gen>.fsm`:
|
|
657
|
+
|
|
658
|
+
1. The owner plans. `INTENT_COMPACT{seg, segEnd, gen}` rides the next batch; no
|
|
659
|
+
output exists before it is durable. The head carries the intent.
|
|
660
|
+
2. A compaction thread builds. It reads the DEAD and TAG_DEAD postings over the
|
|
661
|
+
inputs' pseq range from every run and L0 block at or after the inputs, keeps
|
|
662
|
+
a killer only when it is at or below the plan's kill bound (the type owner's
|
|
663
|
+
`labeled_through`), and decides per row:
|
|
664
|
+
- PUT, LICENCE and CTL rows with a killer go;
|
|
665
|
+
- RETAG rows go when their instance is TAG_DEAD, or their PUT is dead or gone;
|
|
666
|
+
- TOMB, CTL_TOMB and TAG_TOMB rows go only when an earlier SWAP removed their
|
|
667
|
+
target (minor 1): outside the inputs, or VOID within them.
|
|
668
|
+
|
|
669
|
+
It copies the surviving frames, attributes and rows in 4 MiB slices, filters
|
|
670
|
+
the inputs' L1 runs down to the surviving rows' postings (every partition
|
|
671
|
+
posting's value is the pseq of the row that emitted it), fsyncs everything,
|
|
672
|
+
and re-checks the owner word and an abort flag before every mutation (A26).
|
|
673
|
+
3. The owner, once no merge is in flight, writes the manifest (its fsync joins
|
|
674
|
+
the next round's first sync phase) and queues `SWAP{seg, gen, oldCgen,
|
|
675
|
+
segEnd}`; the SWAP, the RETIRE set and the counters' adjustment ride one
|
|
676
|
+
batch. Publish attaches the new files.
|
|
677
|
+
|
|
678
|
+
Rows keep their pseqs. `c-*.fsr` holds the surviving rows (dense by rank) and a
|
|
679
|
+
presence directory (a bit per pseq, a rank per 512); a pseq the directory
|
|
680
|
+
lacks reads as a `VOID` row (kind 8, never stored, never live). A segment with
|
|
681
|
+
no survivor is `EMPTY` in the manifest and has no files.
|
|
682
|
+
|
|
683
|
+
Candidates (maintenance, every 50 ms per warm partition, and at once after a
|
|
684
|
+
SWAP): the sealed segment with the largest dead share at or over
|
|
685
|
+
`compactDeadRatio` (15%; dead frame bytes counted as kills commit), joined by
|
|
686
|
+
its adjacent segments past the ratio too (one output per wave, within the
|
|
687
|
+
coalescing limits); adjacent sealed segments under 8 MiB, coalesced up to 16
|
|
688
|
+
at a time and 64 MiB of output into one segment named by the first (`lastSeg`
|
|
689
|
+
in the manifest; rows' `seg` is rewritten); after a restart, when the
|
|
690
|
+
partition's dead share is over the ratio and the per-segment counts are
|
|
691
|
+
unknown, the oldest
|
|
692
|
+
segment not yet surveyed (a build that would save under 10% is abandoned and
|
|
693
|
+
its count remembered). `Engine::swapSegment` requests one (targeted, or the
|
|
694
|
+
oldest-generation compactable segment).
|
|
695
|
+
|
|
696
|
+
Kill bound and readers. A type-level reader's visibility is `V_p =
|
|
697
|
+
min(pseq_hi, labeled_through[p])`. A compacted segment records `killThrough`
|
|
698
|
+
(the largest killer behind a removed row, the inputs' own, and the bound of
|
|
699
|
+
the SWAP that removed a dropped tombstone's target) and `prevGen` (the manifest
|
|
700
|
+
before its SWAP). A type-level snapshot whose `V_p` is below a segment's
|
|
701
|
+
`killThrough` reads that segment through `prevGen`, repeatedly for a segment
|
|
702
|
+
compacted more than once; the reader gate keeps those files for any statement
|
|
703
|
+
older than the SWAP. Partition-level readers see `pseq_hi`, which is never
|
|
704
|
+
below a SWAP's bound.
|
|
705
|
+
|
|
706
|
+
Counters: `total_count`, `total_bytes` and `tomb_count` count the rows the
|
|
707
|
+
files hold (a SWAP subtracts what it removed), so they equal a recount;
|
|
708
|
+
`min_epoch`, `max_epoch` and `latest_arrival` stay bounds over everything ever
|
|
709
|
+
appended.
|
|
710
|
+
|
|
711
|
+
## 25. Reclamation (A12) and meta segments (A9)
|
|
712
|
+
|
|
713
|
+
- **Retire.** A MERGE_DONE retires the folded runs and the manifest it
|
|
714
|
+
replaces; a SWAP retires the inputs' files, their runs and the manifest. The
|
|
715
|
+
whole outstanding set is a `RETIRE` ctl record in the same batch (the head
|
|
716
|
+
points at it: `retireSeg/retireOff/retireN`); consecutive manifests collapse
|
|
717
|
+
into one range item.
|
|
718
|
+
- **Unlink.** Maintenance unlinks a retired file with `UNLINK_IF_UNUSED` once
|
|
719
|
+
the reader gate (the start of the oldest running reader statement) is past
|
|
720
|
+
its retirement time, checked twice a grace apart (60 s by
|
|
721
|
+
default). The retirement time is that of the first head write after the
|
|
722
|
+
batch commits (the head that stops naming the file): a statement that
|
|
723
|
+
starts earlier may read the previous head. In journal mode also once every
|
|
724
|
+
journal record older than the
|
|
725
|
+
retirement is checkpointed; a meta segment once a DURABLE_CKPT head names a
|
|
726
|
+
later `first_live_m_seg` (a quiet partition writes one; later items do not
|
|
727
|
+
wait for it, and a partition with retired files never goes cold). Each pass
|
|
728
|
+
takes the first check of every file past the gate, so a backlog goes about
|
|
729
|
+
one grace after its statement ends (`reclaim_backlog_goes_in_one_grace`:
|
|
730
|
+
133 files in 0.31 s at a 0.3 s grace). BUSY (a handle is open) skips to
|
|
731
|
+
later items and retries after 50 ms; idle
|
|
732
|
+
reader lanes close cached handles after 5 s. The next batch carries
|
|
733
|
+
`UNLINKED` and the shrunken set; `disk_bytes` drops then. A set past 2,048
|
|
734
|
+
items unlinks its oldest regardless of the gate (a statement older than
|
|
735
|
+
hours of retirements gets the retryable SNAPSHOT_GONE instead of a writer
|
|
736
|
+
waiting on it).
|
|
737
|
+
- **Open** unlinks every file of the persisted set (a file a surviving reader
|
|
738
|
+
instance still holds stays retired), the outputs of an INTENT_COMPACT
|
|
739
|
+
without SWAP, and r/a files a first merge created without MERGE_DONE, then
|
|
740
|
+
writes a durable head.
|
|
741
|
+
- **Meta segments (A9).** A sealed `m-<seg>` whose batches are all merged is
|
|
742
|
+
retired by a batch that also re-emits the full lane table (LANE_CKPT) and the
|
|
743
|
+
RETIRE set, whose header carries the manifest generation, `merged_through`,
|
|
744
|
+
`first_live_m_seg` and `next_gen`; `first_live_m_seg` advances when that
|
|
745
|
+
batch commits. A rebuild of a head whose both slots are torn probes for the
|
|
746
|
+
first existing meta segment and replays from its first batch.
|
|
747
|
+
- **Type logs.** A type's MERGE_DONE retires the catalog runs it folds and
|
|
748
|
+
the manifest it replaces. The new manifest persists the whole outstanding
|
|
749
|
+
set (these and any still waiting) in an appendix after its CRC trailer,
|
|
750
|
+
`[magic FSTR][n][n x RetireItem][crc][pad]`, which readers that predate it
|
|
751
|
+
never read. Unlinking follows the partition rules (reader gate twice a
|
|
752
|
+
grace apart, journal safe time, BUSY retried after 50 ms). Open unlinks the
|
|
753
|
+
appendix's files, the outputs of a merge a crash cut short (it sweeps the
|
|
754
|
+
generations after the manifest's to `next_gen + 64`, and 16 past the last
|
|
755
|
+
leftover it finds, since a head's `next_gen` can lag), and anything below
|
|
756
|
+
`first_live_m_seg`. A failed merge unlinks its outputs at once (durably;
|
|
757
|
+
one that cannot go yet is retired like the rest) and retries with the same
|
|
758
|
+
generation. At stop a merge in flight closes its handles; an unfinished
|
|
759
|
+
build's outputs go, a finished one's are left to open (a MERGE_DONE already
|
|
760
|
+
in the log may name them).
|
|
761
|
+
- **Type meta segments.** The type meta log starts `m-<seg+1>` once it passes
|
|
762
|
+
`typeMetaSegBytes` (64 MiB): the sealed segment is cut at the end of its
|
|
763
|
+
last batch and synced, the new one created, and a durable head names it.
|
|
764
|
+
Open follows the batch chain from a segment's end into the next one, so a
|
|
765
|
+
stale head slot loses nothing. With more than 128 partitions the new
|
|
766
|
+
segment's first batch is a FULL_LABELS checkpoint, and readers read label
|
|
767
|
+
deltas across segments. The oldest sealed segment retires once no unmerged
|
|
768
|
+
L0 block, merge input or label checkpoint lives in it: `first_live_m_seg`
|
|
769
|
+
(a type head field) is written to both head slots and synced, then the file
|
|
770
|
+
waits for readers like any retired file.
|
|
771
|
+
- **Dead copies leave the catalog (A15, catalog half).** A type merge drops the
|
|
772
|
+
CID, LABEL and REPEAT entries of every copy killed within its inputs whose
|
|
773
|
+
first posting (FIRST or REPEAT) is also among them: a copy never leaves
|
|
774
|
+
DEAD, and no older run holds any of its postings. A full fold drops every
|
|
775
|
+
dead copy. GONE and REHOME entries stay: they are keyed by gseq for
|
|
776
|
+
arrivals order, and arrivals are not compacted yet (§30).
|
|
777
|
+
- **Reader gate in production.** The writer is its own wasm instance: the host
|
|
778
|
+
reads every reader instance's lane announcements
|
|
779
|
+
(`FlatsqlPsReaderLayout.laneAnnounce`) and passes the minimum through
|
|
780
|
+
`flatsql_ps_reader_gate` (T6 wires it). Natively, `Engine::setReaderGate`.
|
|
781
|
+
|
|
782
|
+
## 26. Disk bytes (§13)
|
|
783
|
+
|
|
784
|
+
`disk_bytes` is the exact size of the files a partition names: a ledger of the
|
|
785
|
+
stable files (sealed segments' d and m, r/a, c-*, runs, manifests, the next
|
|
786
|
+
segment's pre-created files, retired files until UNLINKED) plus the extents of
|
|
787
|
+
the files still growing (h, l, the active d and m, zero-fill included). Open
|
|
788
|
+
rebuilds it from the head, the manifest and file sizes; the owner maintains it
|
|
789
|
+
on every append, merge, seal, SWAP and unlink. After every reopen in the crash
|
|
790
|
+
tests, a walk of each partition directory finds exactly the named files and a
|
|
791
|
+
total equal to `disk_bytes`. Head files are written as whole slots until they
|
|
792
|
+
hold both, so a head write never grows the file again.
|
|
793
|
+
|
|
794
|
+
A type's disk bytes are kept the same way: its head, meta segments from
|
|
795
|
+
`first_live_m_seg`, arrivals segments and fence, catalog runs, manifest, a
|
|
796
|
+
built merge's outputs until MERGE_DONE, and retired files until they are
|
|
797
|
+
unlinked, maintained on every write, seal, rotation, merge and unlink and
|
|
798
|
+
rebuilt at open from file sizes. Type configs (`s-*.fsc`) are registration
|
|
799
|
+
and not counted. The crash tests check the type directory the same way.
|
|
800
|
+
|
|
801
|
+
## 27. Quota and the full disk (§13, A13, §22.4-3)
|
|
802
|
+
|
|
803
|
+
- **Planner** (writer 0's maintenance, every 100 ms): usage = Σ partition
|
|
804
|
+
`disk_bytes` + Σ type log bytes, less retired files waiting only for
|
|
805
|
+
readers. Over the cap
|
|
806
|
+
(`EngineConfig::quotaBytes`, `Engine::setQuota`, `flatsql_ps_set_quota`), it
|
|
807
|
+
evicts whole sealed segments in arrival order across partitions (their
|
|
808
|
+
`minArrival` zone) down to 0.85 of the cap: a `TOMB_RANGE{seg, all}` on the
|
|
809
|
+
owner (bounded 512-row steps; supersede-lane heads and control kinds are
|
|
810
|
+
spared; each step also stops after 4 ms of CPU), then, once the tombstones
|
|
811
|
+
are durable, a compaction of the segment. One wave at a time.
|
|
812
|
+
- **ENOSPC.** A commit that fails NOSPACE starts an emergency: record entries
|
|
813
|
+
stop being consumed (producers see zero credits; nothing is acked), the
|
|
814
|
+
ballast file (`fsql2/ballast`, `ballastBytes`) is released, and waves evict one
|
|
815
|
+
segment at a time down to 0.85 of the usage at the failure, and at least the
|
|
816
|
+
ballast's size, but never more than half the store in one episode. Once
|
|
817
|
+
usage is under that cap the ballast is recreated and ingest resumes. NOSPACE
|
|
818
|
+
in a merge, a compaction, a head write or a segment creation is transient;
|
|
819
|
+
a merge that failed retries after 100 ms (a type merge with the same
|
|
820
|
+
generation when it left nothing behind). A partition whose L0 directory is
|
|
821
|
+
full stages no rows at all (entries, mailbox kills, TOMB_RANGE steps) until
|
|
822
|
+
a merge frees a slot.
|
|
823
|
+
|
|
824
|
+
## 28. Acceptance (§18 T3 as amended)
|
|
825
|
+
|
|
826
|
+
Machines: **Mac** = the owner's Mac Studio (Darwin 25.3.0, arm64, 28 hardware
|
|
827
|
+
threads), shared with other lanes (1-minute load recorded per run). **Mac mem**
|
|
828
|
+
runs use the in-memory fault host (no fsync). Numbers come from `MEASURED`
|
|
829
|
+
lines of `flatsql_ps_test` (RelWithDebInfo) on the task-branch build landed on
|
|
830
|
+
2026-09-28, run one after another at load 28–38.
|
|
831
|
+
|
|
832
|
+
| # | Acceptance | Result | Where |
|
|
833
|
+
|---|---|---|---|
|
|
834
|
+
| 1 | A partition with 50% dead bytes compacts to ≤ 55% of its prior disk bytes; owner commit p99 and interactive lane p99 ≤ 1.5× baseline during compaction; 1,000 pre-SWAP statements complete correctly | **Size and statements pass; the latency ratio is not met on the Mac (needs a quiet Linux-8 box).** Mac mem, load 31–35: 4 partitions × 20,000 OMM records, half killed: 106.4 MB → 51.9 MB (0.488×); 1,003 bulk statements that started before a SWAP returned exactly the live rows with their bytes, 0 errors (2,177 SWAPs). Latency over the whole SWAP period (the compaction plus the re-compactions that produced those statements; 16,093 samples): ack p99 0.133 → 18.5 ms, interactive lane p99 0.339 → 3.75 ms, commit round p99 1.05 ms. | `compaction_half_dead_T3_1_full` |
|
|
835
|
+
| 2 | CAT supersede, 20% changed per cycle, 100 cycles: disk plateaus ≤ 1.3× live bytes; A9: `m` bytes ≤ one active plus one sealed segment | **Pass with the 15% trigger (deviation 24), "live bytes" read as the live set's compacted footprint.** Mac mem, load 31–33: 20,000 CAT objects, 100 cycles (each updates a random 20%), the store measured at rest after each cycle: disk peaked at 1.120× the footprint of the same live set fully compacted (19.5 MB); 346 compactions, 520 meta segments retired, 5,291 files unlinked; at most 1 meta segment on disk (A9). With §11's 25% trigger it peaks at 1.31–1.38× (six runs). Against the records' own bytes (Σ len − 4 of live PUTs, 4.9 MB) the disk is 4.5×: a fully compacted CAT record costs about 4× its frame in rows, attributes and postings, which no compaction removes | `compaction_cat_supersede_plateau_T3_2_full` |
|
|
836
|
+
| 3 | Quota at 80% of current bytes: oldest records evicted first; no supersede-lane head evicted; disk ≤ cap within 3 passes; each eviction step ≤ 10 ms | **Pass (arrival order, §22.4-3).** Mac mem, load 33–35: the store with its type logs 14.1 MB, cap 11.3 MB: under the cap after 1 pass (14 segments), 0 of 256 CAT heads evicted, in each partition the evicted records are exactly its oldest (0 holes); step max 3.4 ms over 15 steps. Across partitions eviction is by whole segment, so a survivor can be older than an evicted record of another partition (288 of 6,367 records here). The 10 ms bound is enforced on a quiet box (the wasm suite at load 41 measured past it). | `quota_arrival_order_heads_spared_T3_3` |
|
|
837
|
+
| 4 | 1,000 crash points during compaction: exactly one of the old or new set is live, 0 orphans, 0 acked records lost; A12: crash points inside the grace window, Σ on-disk sizes = Σ `disk_bytes` after each reopen | **Pass.** Mac mem, load 28–38: 3,224 trials, 1,000 with a compaction planned but not applied at the crash, 1,744 with retired files waiting; all five crash modes (664 drop-all, 637 drop-subset, 658 torn, 623 reordered, 642 kill -9); 41,825 compactions, 291,864 files unlinked, 24,719 meta segments retired (partitions and the type's), 723,969 dead catalog entries folded out; after every reopen each partition directory and the type directory held exactly the named files with sizes equal to the engine's disk bytes, every acked live record live with its bytes, every acked kill dead, counters equal to a recount; once labels caught up, `first_live_count` equal to the live records and 16 catalog lookups right. 507 s. The T1 crash harness (merges, A9, reclamation; compaction off: its oracle keeps dead bytes) checks the partition directories after every reopen too; the wasm build runs it ×1000. | `orphan_crash_points_during_compaction_T3_4_full`, `crash_faults_T1_1` |
|
|
838
|
+
| 5 | Hot split at 4× single-writer capacity with 4 helpers | **Not built** (§30). | – |
|
|
839
|
+
| 6 | Fill the disk to ENOSPC under ingest: ingest resumes with 0 acked losses and no operator action | **Pass.** Mac mem, load 34–35, a 16 MiB device, 1 MiB ballast, two OMM producers for 4 s: 17 episodes, the ballast released 17 times and restored 16, 41,112 records acked after a recovery, 6,318 alive at the end; in each partition the surviving acked records are exactly its newest (0 holes); after a reopen (before the engine starts) the partition and type directories hold exactly the named files at the engine's disk bytes. Before type-log reclamation the type logs filled 16 of the device's 17 MB and the run ended in the emergency with no survivor. | `quota_disk_full_resumes_without_operator_T3_6` |
|
|
840
|
+
|
|
841
|
+
§22 amendments naming T3:
|
|
842
|
+
|
|
843
|
+
| Amendment | Status |
|
|
844
|
+
|---|---|
|
|
845
|
+
| A9 meta-segment retirement | Built (§25). |
|
|
846
|
+
| A11 intents | INTENT_COMPACT built; merge and compaction intents coexist in one batch. |
|
|
847
|
+
| A12 reclamation lifecycle | Built (§25): RETIRE/UNLINKED, reader gate twice a grace apart, open replays the set. |
|
|
848
|
+
| A13 ENOSPC, ballast, arrival order | Built with deviations 12–13. |
|
|
849
|
+
| A15 arrivals compaction | **Half built**: dead catalog copies are folded out of the type's runs and the type logs are reclaimed and counted (§25–§27); arrivals entries and GONE postings of dead gseqs stay (§30). |
|
|
850
|
+
| A26 stage-1 prep states | Not built (with #5). |
|
|
851
|
+
|
|
852
|
+
## 29. Design deviations (T3)
|
|
853
|
+
|
|
854
|
+
1. **Compaction builds on its own threads** (§11: the owner in 4 MiB / 10 ms
|
|
855
|
+
slices; helpers only for split partitions). As T1 deviation 1 did for
|
|
856
|
+
merges: a build on the writer stalls every partition it owns. The builder
|
|
857
|
+
re-checks the owner word and an abort flag before every mutation, and a
|
|
858
|
+
HANDOFF waits for it to stop (one slice).
|
|
859
|
+
2. **The owner writes the manifest at SWAP time**, synced in the next round's
|
|
860
|
+
first phase, instead of the builder: merges of the active segment continue
|
|
861
|
+
while a build runs; only the SWAP waits for no merge in flight.
|
|
862
|
+
3. **Kill bound and `prevGen` fallback** (not in the design): a row is removed
|
|
863
|
+
only when its killer is labeled, and a type-level snapshot below a
|
|
864
|
+
segment's bound reads the previous file set. Without it, a type-level
|
|
865
|
+
statement whose type snapshot predates a death could miss a live copy
|
|
866
|
+
(A14).
|
|
867
|
+
4. **Counters are physical** (§24): recounts equal them after compaction.
|
|
868
|
+
5. **Coalescing** adjacent small segments (§11 "adjacent sealed segments under
|
|
869
|
+
8 MiB each") into one output named by the first segment.
|
|
870
|
+
6. **Minor 1 extended**: a tombstone whose target is VOID in the same inputs
|
|
871
|
+
(an earlier SWAP of that segment removed it) goes too. Without it the
|
|
872
|
+
supersede tombstones of a partition compacted in one piece never went
|
|
873
|
+
(measured: the CAT plateau grew by 77 KB a cycle).
|
|
874
|
+
7. **Tombstone-only segments seal by age** (T1 sealed only segments with
|
|
875
|
+
frames), so a partition that stops ingesting can drop its tombstones.
|
|
876
|
+
8. **Reader gate by statement start time**, not by announced manifest
|
|
877
|
+
generations (A12 text): T2 built announcements as start times
|
|
878
|
+
(`oldestActiveStart`); the host passes the minimum
|
|
879
|
+
(`flatsql_ps_reader_gate`).
|
|
880
|
+
9. **RETIRE is the whole set** per changing batch, not incremental records, so
|
|
881
|
+
open reads one record (head pointer) plus the tail.
|
|
882
|
+
10. **UNLINK_IF_UNUSED BUSY skips** to later items (A12: "retry later");
|
|
883
|
+
reader lanes close idle handles after 5 s.
|
|
884
|
+
11. **Disk bytes include zero-fill and pre-created files** (the file sizes);
|
|
885
|
+
open stats the files it names.
|
|
886
|
+
12. **ENOSPC evicts with tombstones, one segment per wave** (A13: "first unlink
|
|
887
|
+
whole eligible sealed segments, oldest first ... needs no new space").
|
|
888
|
+
Dropping a segment without per-record tombstones would leave the type
|
|
889
|
+
owner unaware of the deaths (A14: FIRST copies must be labeled DEAD and
|
|
890
|
+
REPEATs promoted), and those catalog updates need space too. The ballast
|
|
891
|
+
must hold one segment's tombstones: about 128 B of row plus postings per
|
|
892
|
+
record, twice until the tombstones' own segment is compacted.
|
|
893
|
+
13. **Ballast is off in the engine default**; hosts set it (servers 256 MiB,
|
|
894
|
+
browsers one seal plus one commit), so tests and benchmarks are unaffected.
|
|
895
|
+
14. **Quota counts partitions and type logs**, not the registry (a few KB)
|
|
896
|
+
or type configs (registration).
|
|
897
|
+
15. **A rebalance of a partition already handed off is ignored** (a T1 race
|
|
898
|
+
the compaction timing exposed: a second release before the target adopted
|
|
899
|
+
could leave two owners).
|
|
900
|
+
16. **RecordAttr bytes**: `buildRecordAttr` created its strings inside one
|
|
901
|
+
argument list, whose evaluation order C++ leaves unspecified; GCC on x86_64
|
|
902
|
+
built different bytes from clang. Each string is now created in field order;
|
|
903
|
+
`format_record_attr_golden_bytes` checks `vectors/record_attr.hex`
|
|
904
|
+
(22.3a-6).
|
|
905
|
+
17. **T3 #2 "live bytes" is the live set's compacted footprint**: the same
|
|
906
|
+
records fully compacted, rows, attributes and postings included. Every
|
|
907
|
+
record carries about four times its frame in index structure, so a ratio
|
|
908
|
+
to frame bytes alone would measure the index, not garbage; both ratios are
|
|
909
|
+
reported (§28).
|
|
910
|
+
18. **Type logs persist their retired set in the manifest** (not in ctl
|
|
911
|
+
records): a type has no ctl-record log, and the manifest is the one file a
|
|
912
|
+
MERGE_DONE names. The appendix sits after the CRC trailer, so readers and
|
|
913
|
+
opens that predate it parse the manifest unchanged.
|
|
914
|
+
19. **A type meta segment retires with two synced head writes** instead of a
|
|
915
|
+
LANE_CKPT batch and a later DURABLE_CKPT head (A9): a type head has no
|
|
916
|
+
lane table, and after the second write neither slot names the segment,
|
|
917
|
+
whichever one a crash leaves torn.
|
|
918
|
+
20. **A compaction plan pins its files**: anything retired after the plan was
|
|
919
|
+
made stays until the SWAP or the abort (a build reads the runs and meta
|
|
920
|
+
segments current at planning).
|
|
921
|
+
21. **A failed build never quarantines**: the inputs are untouched, the
|
|
922
|
+
partition carries on, and requests end with an error (NOSPACE starts the
|
|
923
|
+
emergency). A quarantined partition ends its compaction requests at once.
|
|
924
|
+
22. **TOMB_RANGE steps are also bounded in time** (4 ms of CPU, besides 512
|
|
925
|
+
rows): a kill looks up the row's tag instances in every run, so rows cost
|
|
926
|
+
more as a partition grows, and more under wasm (T3 #3: ≤ 10 ms a step).
|
|
927
|
+
23. **Dead catalog copies are dropped by merges** (A15 says "type-log
|
|
928
|
+
compaction"): the type merge already reads every entry it could drop, so
|
|
929
|
+
no separate pass rewrites the runs; a copy goes once its first posting and
|
|
930
|
+
its DEAD posting meet in one merge (at the latest, the next full fold).
|
|
931
|
+
24. **The dead-share trigger is 15%, not 25%** (§11). At rest a segment can
|
|
932
|
+
hold up to the trigger's share of dead rows, and a dead row costs as much
|
|
933
|
+
as a live one in rows, attributes and postings, so a store at rest sits
|
|
934
|
+
at up to 1/(1 − trigger) of its compacted footprint: 1.33× at 25%, above
|
|
935
|
+
T3 #2's 1.3. Measured on the T3 #2 run (20,000 CAT objects, 20% updated
|
|
936
|
+
per cycle, 100 cycles; Mac mem, load 6–26): 25%: 1.31–1.38 over six
|
|
937
|
+
runs; 20%: 1.35, 1.38; 15%: 1.115, 1.120; 10%: 1.19, with about the
|
|
938
|
+
same number of compactions (334–361). The price is write
|
|
939
|
+
amplification: a compaction at 15% rewrites 5.7 bytes per byte it
|
|
940
|
+
reclaims (3 at 25%). `EngineConfig::compactDeadRatio` sets it.
|
|
941
|
+
25. **A compaction wave is one output**: segments next to the chosen one that
|
|
942
|
+
are past the trigger too join it, and the next candidate is looked for
|
|
943
|
+
right after a SWAP (not 50 ms later).
|
|
944
|
+
|
|
945
|
+
## 30. Not built yet; running the T3 tests
|
|
946
|
+
|
|
947
|
+
Remaining T3 scope:
|
|
948
|
+
|
|
949
|
+
- **#5 hot split** with stage-1 helpers and sealed-segment re-homing (§12), and
|
|
950
|
+
A26's ring prep states (FREE → CLAIMED(epoch) → PREPARED). Sealed segments'
|
|
951
|
+
merges and compactions already build off the owner (deviation 1 here, T1
|
|
952
|
+
deviation 1); what is missing is stage-1 (verify, sha256, key extraction
|
|
953
|
+
claimed in ranges by idle writers) and its trigger. Its acceptance (≥ 2.5×
|
|
954
|
+
the unsplit rate with 4 helpers) is a throughput ratio that needs a quiet
|
|
955
|
+
many-core box to mean anything.
|
|
956
|
+
- **A15 arrivals compaction**: arrivals entries (24 B) and the GONE posting
|
|
957
|
+
of every dead gseq stay, about 40 B per record ever ingested; the quota
|
|
958
|
+
counts them but nothing shrinks them (the type-log test: 2.4 MB of catalog
|
|
959
|
+
runs, mostly GONE, and 2.5 MB of arrivals after 104,100 records with 835
|
|
960
|
+
alive). The type-log half (catalog runs, manifests, meta segments, dead
|
|
961
|
+
catalog copies) is built (§25). The arrivals half must drop an entry and its
|
|
962
|
+
GONE posting in one commit, or offset paging (live = entries − GONE, T2)
|
|
963
|
+
miscounts: the plan is to rewrite sealed arrivals segments inside the type
|
|
964
|
+
merge that drops their GONE postings, named by the merge's generation in
|
|
965
|
+
its manifest.
|
|
966
|
+
- **#1 latency ratio** on a quiet Linux-8 box.
|
|
967
|
+
|
|
968
|
+
```
|
|
969
|
+
cpp/build/flatsql_ps_test --test=compaction_ # basic, T3 #1 and #2 (short forms)
|
|
970
|
+
cpp/build/flatsql_ps_test --test=compaction_half_dead_T3_1_full [--dir=<dir>]
|
|
971
|
+
cpp/build/flatsql_ps_test --test=compaction_cat_supersede_plateau_T3_2_full
|
|
972
|
+
cpp/build/flatsql_ps_test --test=orphan_crash_points_during_compaction_T3_4_full # 1,000 crash points
|
|
973
|
+
cpp/build/flatsql_ps_test --test=quota_ # T3 #3, #6
|
|
974
|
+
cpp/build/flatsql_ps_test --test=type_logs_reclaimed_under_readers
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
**T3 #1 on a quiet Linux-8 box** (8 cores, ext4, nothing else running), from a
|
|
978
|
+
checkout at the landed commit; the test enforces the 1.5× bounds on such a box:
|
|
979
|
+
|
|
980
|
+
```
|
|
981
|
+
cpp/build/flatsql_ps_test --test=compaction_half_dead_T3_1_full --dir=<ext4 dir>
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
# Part IV: T3b
|
|
985
|
+
|
|
986
|
+
## 31. store-migrate gseqs, the entry-size setting, tag conditions
|
|
987
|
+
|
|
988
|
+
store-migrate (T6, design §16.1-5) gives every migrated FIRST copy gseq = its
|
|
989
|
+
legacy `sdn_record_index.rowid`, so per-schema MaxRowID and the FTS rowids
|
|
990
|
+
carry over.
|
|
991
|
+
|
|
992
|
+
- **Input.** `RecordAttr.migrated_gseq` (`ulong`, field 6, appended to
|
|
993
|
+
`schemas/flatsql_attr.fbs`; absent = 0, so older attributes are
|
|
994
|
+
unchanged byte for byte). `buildRecordAttr(..., migratedGseq)` writes it.
|
|
995
|
+
- **Row.** A new PUT whose attribute carries one gets row flag
|
|
996
|
+
`MIGRATED_GSEQ` (0x80). The gseq stays in the attribute (in the same meta
|
|
997
|
+
batch, `ATTR_IN_M`): the RecRow has no spare u64 (offsets 108–127 hold
|
|
998
|
+
`supersedeHash`, `tagHash` and `aux`), and the attribute is as durable as
|
|
999
|
+
the row. Deviation from the T6 proposal "(b) the RecRow reserved u64 at
|
|
1000
|
+
offset 116", which T1's layout had already given to `tagHash`.
|
|
1001
|
+
- **Labeling.** The type owner reads the flagged rows' attributes in windows
|
|
1002
|
+
over the chunk it labels (one read per chunk of up to 4,096 rows when the
|
|
1003
|
+
attributes fit in 1 MiB). A flagged copy labeled FIRST keeps its gseq when it
|
|
1004
|
+
is above the type's committed `gseq_hi` and no arrival of the same type
|
|
1005
|
+
commit holds it; the engine's gseq counter is then raised to at least gseq
|
|
1006
|
+
+ 1. Otherwise it gets an allocated gseq and counts a fallback; a flagged
|
|
1007
|
+
copy labeled REPEAT counts a fallback too. A type commit's new arrivals are
|
|
1008
|
+
sorted by gseq before they are appended, so arrivals stay strictly
|
|
1009
|
+
increasing whatever order partitions are labeled in.
|
|
1010
|
+
- **Counters.** `EngineStats::migratedGseqs` and `migratedGseqFallbacks`,
|
|
1011
|
+
appended to `flatsql_ps_stats` (entries 24 and 25). store-migrate's
|
|
1012
|
+
verification (§16.1-7) needs fallbacks = 0; they are counted per labeling
|
|
1013
|
+
decision, so a type commit that fails and is relabeled counts again.
|
|
1014
|
+
- **Crash.** The gseq is read from durable rows and attributes, so a crash
|
|
1015
|
+
that loses the type commit relabels the same copies with the same gseqs.
|
|
1016
|
+
An allocated fallback gseq that was never published can be reassigned
|
|
1017
|
+
(as for every allocated gseq).
|
|
1018
|
+
- **What T6 must do** (its (d)): feed each type's FIRST copies in rowid
|
|
1019
|
+
order and wait for `labeled_through` before crossing partitions, and run
|
|
1020
|
+
with fallbacks = 0. gseqs are unique per type; across types a fallback could
|
|
1021
|
+
reuse a legacy rowid another type migrates later, which is one more reason
|
|
1022
|
+
a fallback fails the migration.
|
|
1023
|
+
- **Entry size.** Writer TLV 19 = `maxEntryBytes` (u64; default 1 MiB +
|
|
1024
|
+
4 KiB, clamped to [64 KiB, 1 GiB]): the largest ring entry (header,
|
|
1025
|
+
RecordAttr and frame). Each writer's arena grows to at least 4 ×
|
|
1026
|
+
(maxEntryBytes + 4 KiB), so the frames half holds two entries. Frames
|
|
1027
|
+
larger than a segment (A27 jumbo segments) are still not built.
|
|
1028
|
+
|
|
1029
|
+
Tests: `type_migrated_gseq_preserved_fallback_and_stats` (kept, fallback
|
|
1030
|
+
below `gseq_hi`, REPEAT copy, ordinary allocation continues above, flags on
|
|
1031
|
+
disk, reopen), `type_migrated_gseq_one_commit_sorted_and_crash_reopen` (two
|
|
1032
|
+
partitions' interleaved gseqs in one type commit come out sorted; a crash
|
|
1033
|
+
before and after the type commit gives the same arrivals),
|
|
1034
|
+
`writer_max_entry_bytes_configurable` (a 9 MiB frame refused by default,
|
|
1035
|
+
stored and read back at 10 MiB), `writer_capi_tlv19_max_entry_and_stats`
|
|
1036
|
+
(tag 19 through `flatsql_ps_init`, the 26-entry stats array).
|
|
1037
|
+
|
|
1038
|
+
### 31.1 Tag conditions match any live tag instance (A2)
|
|
1039
|
+
|
|
1040
|
+
`_provider`, `_batch`, `_peer_id`, `_source` and `_source_name` conditions
|
|
1041
|
+
have the legacy tag table's ANY-row semantics (`indexedRecordWindowSQL`: a
|
|
1042
|
+
CID matches when one of its tag rows satisfies every requested condition).
|
|
1043
|
+
In format 2 a record's tag instances are its PUT's own tag and its RETAG rows
|
|
1044
|
+
(A2). As T2 built it, the vtab scanned tag postings (which name every
|
|
1045
|
+
instance, RETAGs included) but left each constraint for SQLite to re-check
|
|
1046
|
+
against the projected column, which shows only the PUT's own tag, so every
|
|
1047
|
+
record matched through a RETAG was dropped (T6 #7 on a migrated copy: a
|
|
1048
|
+
provider window of 200 read as 100; a source + batch window lost the
|
|
1049
|
+
re-tagged records).
|
|
1050
|
+
|
|
1051
|
+
Now `xBestIndex` consumes every tag condition (`omit = 1`) and the vtab
|
|
1052
|
+
evaluates them all on one instance:
|
|
1053
|
+
|
|
1054
|
+
- A tag or source posting scan (the plan's key) checks the remaining
|
|
1055
|
+
conditions on the instance the posting names (its RecordAttr), then emits
|
|
1056
|
+
the instance's record. A single condition that is the scanned key needs no
|
|
1057
|
+
attribute read unless the key was capped (over 512 bytes).
|
|
1058
|
+
- Any other plan (CID, column index, epoch, arrivals order, full scan)
|
|
1059
|
+
keeps a record when one of its live tag instances matches every condition
|
|
1060
|
+
(`RowFilter::hasLiveTag`, the TAG_OF postings), as the alias table's source
|
|
1061
|
+
filter already did.
|
|
1062
|
+
- An untagged record matches no tag condition (as the index-driven plans
|
|
1063
|
+
already behaved, T2 deviation 8), and conditions that contradict each other
|
|
1064
|
+
(two sources) return no rows.
|
|
1065
|
+
- `_source = '<TYPE>@<name>'` is compared as the column projects it: the type
|
|
1066
|
+
prefix is case-sensitive (a lower-case prefix returned no rows before
|
|
1067
|
+
either, after SQLite's re-check).
|
|
1068
|
+
- `_peer_id` conditions match the tag's producer peer (the TAG_PEER postings
|
|
1069
|
+
the T2 plan scanned); the `_peer_id` column still projects the storing
|
|
1070
|
+
call's `RecordAttr.peer_id`. Before, SQLite compared that BLOB column with
|
|
1071
|
+
the argument, so a text argument never matched.
|
|
1072
|
+
- `<TYPE>_current` groups first and filters after, as SQL says: its tag
|
|
1073
|
+
conditions stay SQLite's (the current version's own tag).
|
|
1074
|
+
- Projected columns are unchanged: they show the PUT's own tag.
|
|
1075
|
+
|
|
1076
|
+
Test: `lane_tag_conditions_match_any_instance_A2` (two partitions, PUT tags,
|
|
1077
|
+
RETAGs and REPEAT copies; eleven condition sets at type and partition level
|
|
1078
|
+
against an ANY-instance oracle, the full `_source` form, the alias, an
|
|
1079
|
+
epoch-ordered window, column and CID plans). With `omit = 0` restored it
|
|
1080
|
+
fails 18 checks.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flatsql",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "SQL queries over raw FlatBuffer storage: the SQLite-backed wasm engine and the multithreaded partition store (flatsql-ps-threads.wasm)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "wasm/index.js",
|
|
Binary file
|
package/wasm/integrity.json
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
"size": 2139350,
|
|
6
6
|
"files": {
|
|
7
7
|
"flatsql-ps-threads.wasm": {
|
|
8
|
-
"sha256": "
|
|
9
|
-
"sri": "sha384-
|
|
10
|
-
"size":
|
|
8
|
+
"sha256": "a90d9488187e527bccd3f02ce3f68be6443690bc1a2ee6c6786c64c907376e36",
|
|
9
|
+
"sri": "sha384-zzvS8qbCe33AJCklxaZX4xr1+QajJAaz0G0FuBv9WyEXh2rSKAz9+Rq4o2XksFaF",
|
|
10
|
+
"size": 2261085
|
|
11
11
|
},
|
|
12
12
|
"flatsql-wasi-noeh.wasm": {
|
|
13
13
|
"sha256": "fd744d4c502310669bc74ab555a36de1692030a0fa6e22fdb1df744809eeacbe",
|