flatsql 3.0.0 → 3.1.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. Native builds RelWithDebInfo; wasm builds as §1.
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 | See §6 and the release record in §4.2. |
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
- Filled in when the release is published.
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
 
@@ -4,13 +4,14 @@ 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. The design is the stack's
8
- `docs/architecture/flatsql-partition-store.md` (§22 overrides §0–§21; §22.4
9
- holds the owner rulings). This file records what was built, how to run it, the
10
- measured acceptance, and every place the build departs from the design.
7
+ fan-out merge, admission and results. Part III (§23–§30) is T3: compaction,
8
+ reclamation, meta-segment retirement, disk accounting and quota. The design is
9
+ the stack's `docs/architecture/flatsql-partition-store.md` (§22 overrides
10
+ §0–§21; §22.4 holds the owner rulings). This file records what was built, how
11
+ to run it, the measured acceptance, and every place the build departs from the
12
+ design.
11
13
 
12
- Compaction, splits and quota are T3; the wasm artifact is T4; SDN and browser
13
- integration are T6/T10.
14
+ The wasm artifact is T4; SDN and browser integration are T6/T10.
14
15
 
15
16
  ## 1. Code map
16
17
 
@@ -633,3 +634,347 @@ engine. If the bound fails there, the next steps are the design's own levers:
633
634
  fewer writer threads than cores (design §5.1: `clamp(cores−2, 1, 16)`), interactive
634
635
  lanes at a higher priority than writers (design A28 gives nice −5 only on ≤ 2 vCPU),
635
636
  and idle-lane sub-cursors (§21 deviation 1).
637
+
638
+ # Part III: compaction, reclamation and quota (T3)
639
+
640
+ ## 23. Code map
641
+
642
+ | Path | Contents |
643
+ |---|---|
644
+ | `ps/compaction.h`, `src/ps/compaction.cpp` | Manifest version 2, the compacted rows format, retire items; the compaction pipeline (plan, build, SWAP, apply, abort). |
645
+ | `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). |
646
+ | `ps/quota.h`, `src/ps/quota.cpp` | The quota planner, the space emergency, the ballast. |
647
+ | `src/ps/sources-t3.cmake` | Registers the T3 sources and tests on the targets `cpp/CMakeLists.txt` defines. |
648
+ | `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. |
649
+
650
+ ## 24. Compaction (§11, A11, minor 1)
651
+
652
+ A compaction rewrites a run of sealed, fully merged segments `[seg, segEnd]`
653
+ into one new file set, `c-<seg>-<gen>.{fsd,fsr,fsa}` plus
654
+ `x-<seg>-<gen>.fsx` and `mf-<gen>.fsm`:
655
+
656
+ 1. The owner plans. `INTENT_COMPACT{seg, segEnd, gen}` rides the next batch; no
657
+ output exists before it is durable. The head carries the intent.
658
+ 2. A compaction thread builds. It reads the DEAD and TAG_DEAD postings over the
659
+ inputs' pseq range from every run and L0 block at or after the inputs, keeps
660
+ a killer only when it is at or below the plan's kill bound (the type owner's
661
+ `labeled_through`), and decides per row:
662
+ - PUT, LICENCE and CTL rows with a killer go;
663
+ - RETAG rows go when their instance is TAG_DEAD, or their PUT is dead or gone;
664
+ - TOMB, CTL_TOMB and TAG_TOMB rows go only when an earlier SWAP removed their
665
+ target (minor 1): outside the inputs, or VOID within them.
666
+
667
+ It copies the surviving frames, attributes and rows in 4 MiB slices, filters
668
+ the inputs' L1 runs down to the surviving rows' postings (every partition
669
+ posting's value is the pseq of the row that emitted it), fsyncs everything,
670
+ and re-checks the owner word and an abort flag before every mutation (A26).
671
+ 3. The owner, once no merge is in flight, writes the manifest (its fsync joins
672
+ the next round's first sync phase) and queues `SWAP{seg, gen, oldCgen,
673
+ segEnd}`; the SWAP, the RETIRE set and the counters' adjustment ride one
674
+ batch. Publish attaches the new files.
675
+
676
+ Rows keep their pseqs. `c-*.fsr` holds the surviving rows (dense by rank) and a
677
+ presence directory (a bit per pseq, a rank per 512); a pseq the directory
678
+ lacks reads as a `VOID` row (kind 8, never stored, never live). A segment with
679
+ no survivor is `EMPTY` in the manifest and has no files.
680
+
681
+ Candidates (maintenance, every 50 ms per warm partition, and at once after a
682
+ SWAP): the sealed segment with the largest dead share at or over
683
+ `compactDeadRatio` (15%; dead frame bytes counted as kills commit), joined by
684
+ its adjacent segments past the ratio too (one output per wave, within the
685
+ coalescing limits); adjacent sealed segments under 8 MiB, coalesced up to 16
686
+ at a time and 64 MiB of output into one segment named by the first (`lastSeg`
687
+ in the manifest; rows' `seg` is rewritten); after a restart, when the
688
+ partition's dead share is over the ratio and the per-segment counts are
689
+ unknown, the oldest
690
+ segment not yet surveyed (a build that would save under 10% is abandoned and
691
+ its count remembered). `Engine::swapSegment` requests one (targeted, or the
692
+ oldest-generation compactable segment).
693
+
694
+ Kill bound and readers. A type-level reader's visibility is `V_p =
695
+ min(pseq_hi, labeled_through[p])`. A compacted segment records `killThrough`
696
+ (the largest killer behind a removed row, the inputs' own, and the bound of
697
+ the SWAP that removed a dropped tombstone's target) and `prevGen` (the manifest
698
+ before its SWAP). A type-level snapshot whose `V_p` is below a segment's
699
+ `killThrough` reads that segment through `prevGen`, repeatedly for a segment
700
+ compacted more than once; the reader gate keeps those files for any statement
701
+ older than the SWAP. Partition-level readers see `pseq_hi`, which is never
702
+ below a SWAP's bound.
703
+
704
+ Counters: `total_count`, `total_bytes` and `tomb_count` count the rows the
705
+ files hold (a SWAP subtracts what it removed), so they equal a recount;
706
+ `min_epoch`, `max_epoch` and `latest_arrival` stay bounds over everything ever
707
+ appended.
708
+
709
+ ## 25. Reclamation (A12) and meta segments (A9)
710
+
711
+ - **Retire.** A MERGE_DONE retires the folded runs and the manifest it
712
+ replaces; a SWAP retires the inputs' files, their runs and the manifest. The
713
+ whole outstanding set is a `RETIRE` ctl record in the same batch (the head
714
+ points at it: `retireSeg/retireOff/retireN`); consecutive manifests collapse
715
+ into one range item.
716
+ - **Unlink.** Maintenance unlinks a retired file with `UNLINK_IF_UNUSED` once
717
+ the reader gate (the start of the oldest running reader statement) is past
718
+ its retirement time, checked twice a grace apart (60 s by
719
+ default). The retirement time is that of the first head write after the
720
+ batch commits (the head that stops naming the file): a statement that
721
+ starts earlier may read the previous head. In journal mode also once every
722
+ journal record older than the
723
+ retirement is checkpointed; a meta segment once a DURABLE_CKPT head names a
724
+ later `first_live_m_seg` (a quiet partition writes one; later items do not
725
+ wait for it, and a partition with retired files never goes cold). Each pass
726
+ takes the first check of every file past the gate, so a backlog goes about
727
+ one grace after its statement ends (`reclaim_backlog_goes_in_one_grace`:
728
+ 133 files in 0.31 s at a 0.3 s grace). BUSY (a handle is open) skips to
729
+ later items and retries after 50 ms; idle
730
+ reader lanes close cached handles after 5 s. The next batch carries
731
+ `UNLINKED` and the shrunken set; `disk_bytes` drops then. A set past 2,048
732
+ items unlinks its oldest regardless of the gate (a statement older than
733
+ hours of retirements gets the retryable SNAPSHOT_GONE instead of a writer
734
+ waiting on it).
735
+ - **Open** unlinks every file of the persisted set (a file a surviving reader
736
+ instance still holds stays retired), the outputs of an INTENT_COMPACT
737
+ without SWAP, and r/a files a first merge created without MERGE_DONE, then
738
+ writes a durable head.
739
+ - **Meta segments (A9).** A sealed `m-<seg>` whose batches are all merged is
740
+ retired by a batch that also re-emits the full lane table (LANE_CKPT) and the
741
+ RETIRE set, whose header carries the manifest generation, `merged_through`,
742
+ `first_live_m_seg` and `next_gen`; `first_live_m_seg` advances when that
743
+ batch commits. A rebuild of a head whose both slots are torn probes for the
744
+ first existing meta segment and replays from its first batch.
745
+ - **Type logs.** A type's MERGE_DONE retires the catalog runs it folds and
746
+ the manifest it replaces. The new manifest persists the whole outstanding
747
+ set (these and any still waiting) in an appendix after its CRC trailer,
748
+ `[magic FSTR][n][n x RetireItem][crc][pad]`, which readers that predate it
749
+ never read. Unlinking follows the partition rules (reader gate twice a
750
+ grace apart, journal safe time, BUSY retried after 50 ms). Open unlinks the
751
+ appendix's files, the outputs of a merge a crash cut short (it sweeps the
752
+ generations after the manifest's to `next_gen + 64`, and 16 past the last
753
+ leftover it finds, since a head's `next_gen` can lag), and anything below
754
+ `first_live_m_seg`. A failed merge unlinks its outputs at once (durably;
755
+ one that cannot go yet is retired like the rest) and retries with the same
756
+ generation. At stop a merge in flight closes its handles; an unfinished
757
+ build's outputs go, a finished one's are left to open (a MERGE_DONE already
758
+ in the log may name them).
759
+ - **Type meta segments.** The type meta log starts `m-<seg+1>` once it passes
760
+ `typeMetaSegBytes` (64 MiB): the sealed segment is cut at the end of its
761
+ last batch and synced, the new one created, and a durable head names it.
762
+ Open follows the batch chain from a segment's end into the next one, so a
763
+ stale head slot loses nothing. With more than 128 partitions the new
764
+ segment's first batch is a FULL_LABELS checkpoint, and readers read label
765
+ deltas across segments. The oldest sealed segment retires once no unmerged
766
+ L0 block, merge input or label checkpoint lives in it: `first_live_m_seg`
767
+ (a type head field) is written to both head slots and synced, then the file
768
+ waits for readers like any retired file.
769
+ - **Dead copies leave the catalog (A15, catalog half).** A type merge drops the
770
+ CID, LABEL and REPEAT entries of every copy killed within its inputs whose
771
+ first posting (FIRST or REPEAT) is also among them: a copy never leaves
772
+ DEAD, and no older run holds any of its postings. A full fold drops every
773
+ dead copy. GONE and REHOME entries stay: they are keyed by gseq for
774
+ arrivals order, and arrivals are not compacted yet (§30).
775
+ - **Reader gate in production.** The writer is its own wasm instance: the host
776
+ reads every reader instance's lane announcements
777
+ (`FlatsqlPsReaderLayout.laneAnnounce`) and passes the minimum through
778
+ `flatsql_ps_reader_gate` (T6 wires it). Natively, `Engine::setReaderGate`.
779
+
780
+ ## 26. Disk bytes (§13)
781
+
782
+ `disk_bytes` is the exact size of the files a partition names: a ledger of the
783
+ stable files (sealed segments' d and m, r/a, c-*, runs, manifests, the next
784
+ segment's pre-created files, retired files until UNLINKED) plus the extents of
785
+ the files still growing (h, l, the active d and m, zero-fill included). Open
786
+ rebuilds it from the head, the manifest and file sizes; the owner maintains it
787
+ on every append, merge, seal, SWAP and unlink. After every reopen in the crash
788
+ tests, a walk of each partition directory finds exactly the named files and a
789
+ total equal to `disk_bytes`. Head files are written as whole slots until they
790
+ hold both, so a head write never grows the file again.
791
+
792
+ A type's disk bytes are kept the same way: its head, meta segments from
793
+ `first_live_m_seg`, arrivals segments and fence, catalog runs, manifest, a
794
+ built merge's outputs until MERGE_DONE, and retired files until they are
795
+ unlinked, maintained on every write, seal, rotation, merge and unlink and
796
+ rebuilt at open from file sizes. Type configs (`s-*.fsc`) are registration
797
+ and not counted. The crash tests check the type directory the same way.
798
+
799
+ ## 27. Quota and the full disk (§13, A13, §22.4-3)
800
+
801
+ - **Planner** (writer 0's maintenance, every 100 ms): usage = Σ partition
802
+ `disk_bytes` + Σ type log bytes, less retired files waiting only for
803
+ readers. Over the cap
804
+ (`EngineConfig::quotaBytes`, `Engine::setQuota`, `flatsql_ps_set_quota`), it
805
+ evicts whole sealed segments in arrival order across partitions (their
806
+ `minArrival` zone) down to 0.85 of the cap: a `TOMB_RANGE{seg, all}` on the
807
+ owner (bounded 512-row steps; supersede-lane heads and control kinds are
808
+ spared; each step also stops after 4 ms of CPU), then, once the tombstones
809
+ are durable, a compaction of the segment. One wave at a time.
810
+ - **ENOSPC.** A commit that fails NOSPACE starts an emergency: record entries
811
+ stop being consumed (producers see zero credits; nothing is acked), the
812
+ ballast file (`fsql2/ballast`, `ballastBytes`) is released, and waves evict one
813
+ segment at a time down to 0.85 of the usage at the failure, and at least the
814
+ ballast's size, but never more than half the store in one episode. Once
815
+ usage is under that cap the ballast is recreated and ingest resumes. NOSPACE
816
+ in a merge, a compaction, a head write or a segment creation is transient;
817
+ a merge that failed retries after 100 ms (a type merge with the same
818
+ generation when it left nothing behind). A partition whose L0 directory is
819
+ full stages no rows at all (entries, mailbox kills, TOMB_RANGE steps) until
820
+ a merge frees a slot.
821
+
822
+ ## 28. Acceptance (§18 T3 as amended)
823
+
824
+ Machines: **Mac** = the owner's Mac Studio (Darwin 25.3.0, arm64, 28 hardware
825
+ threads), shared with other lanes (1-minute load recorded per run). **Mac mem**
826
+ runs use the in-memory fault host (no fsync). Numbers come from `MEASURED`
827
+ lines of `flatsql_ps_test` (RelWithDebInfo) on the task-branch build landed on
828
+ 2026-09-28, run one after another at load 28–38.
829
+
830
+ | # | Acceptance | Result | Where |
831
+ |---|---|---|---|
832
+ | 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` |
833
+ | 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` |
834
+ | 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` |
835
+ | 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` |
836
+ | 5 | Hot split at 4× single-writer capacity with 4 helpers | **Not built** (§30). | – |
837
+ | 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` |
838
+
839
+ §22 amendments naming T3:
840
+
841
+ | Amendment | Status |
842
+ |---|---|
843
+ | A9 meta-segment retirement | Built (§25). |
844
+ | A11 intents | INTENT_COMPACT built; merge and compaction intents coexist in one batch. |
845
+ | A12 reclamation lifecycle | Built (§25): RETIRE/UNLINKED, reader gate twice a grace apart, open replays the set. |
846
+ | A13 ENOSPC, ballast, arrival order | Built with deviations 12–13. |
847
+ | 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). |
848
+ | A26 stage-1 prep states | Not built (with #5). |
849
+
850
+ ## 29. Design deviations (T3)
851
+
852
+ 1. **Compaction builds on its own threads** (§11: the owner in 4 MiB / 10 ms
853
+ slices; helpers only for split partitions). As T1 deviation 1 did for
854
+ merges: a build on the writer stalls every partition it owns. The builder
855
+ re-checks the owner word and an abort flag before every mutation, and a
856
+ HANDOFF waits for it to stop (one slice).
857
+ 2. **The owner writes the manifest at SWAP time**, synced in the next round's
858
+ first phase, instead of the builder: merges of the active segment continue
859
+ while a build runs; only the SWAP waits for no merge in flight.
860
+ 3. **Kill bound and `prevGen` fallback** (not in the design): a row is removed
861
+ only when its killer is labeled, and a type-level snapshot below a
862
+ segment's bound reads the previous file set. Without it, a type-level
863
+ statement whose type snapshot predates a death could miss a live copy
864
+ (A14).
865
+ 4. **Counters are physical** (§24): recounts equal them after compaction.
866
+ 5. **Coalescing** adjacent small segments (§11 "adjacent sealed segments under
867
+ 8 MiB each") into one output named by the first segment.
868
+ 6. **Minor 1 extended**: a tombstone whose target is VOID in the same inputs
869
+ (an earlier SWAP of that segment removed it) goes too. Without it the
870
+ supersede tombstones of a partition compacted in one piece never went
871
+ (measured: the CAT plateau grew by 77 KB a cycle).
872
+ 7. **Tombstone-only segments seal by age** (T1 sealed only segments with
873
+ frames), so a partition that stops ingesting can drop its tombstones.
874
+ 8. **Reader gate by statement start time**, not by announced manifest
875
+ generations (A12 text): T2 built announcements as start times
876
+ (`oldestActiveStart`); the host passes the minimum
877
+ (`flatsql_ps_reader_gate`).
878
+ 9. **RETIRE is the whole set** per changing batch, not incremental records, so
879
+ open reads one record (head pointer) plus the tail.
880
+ 10. **UNLINK_IF_UNUSED BUSY skips** to later items (A12: "retry later");
881
+ reader lanes close idle handles after 5 s.
882
+ 11. **Disk bytes include zero-fill and pre-created files** (the file sizes);
883
+ open stats the files it names.
884
+ 12. **ENOSPC evicts with tombstones, one segment per wave** (A13: "first unlink
885
+ whole eligible sealed segments, oldest first ... needs no new space").
886
+ Dropping a segment without per-record tombstones would leave the type
887
+ owner unaware of the deaths (A14: FIRST copies must be labeled DEAD and
888
+ REPEATs promoted), and those catalog updates need space too. The ballast
889
+ must hold one segment's tombstones: about 128 B of row plus postings per
890
+ record, twice until the tombstones' own segment is compacted.
891
+ 13. **Ballast is off in the engine default**; hosts set it (servers 256 MiB,
892
+ browsers one seal plus one commit), so tests and benchmarks are unaffected.
893
+ 14. **Quota counts partitions and type logs**, not the registry (a few KB)
894
+ or type configs (registration).
895
+ 15. **A rebalance of a partition already handed off is ignored** (a T1 race
896
+ the compaction timing exposed: a second release before the target adopted
897
+ could leave two owners).
898
+ 16. **RecordAttr bytes**: `buildRecordAttr` created its strings inside one
899
+ argument list, whose evaluation order C++ leaves unspecified; GCC on x86_64
900
+ built different bytes from clang. Each string is now created in field order;
901
+ `format_record_attr_golden_bytes` checks `vectors/record_attr.hex`
902
+ (22.3a-6).
903
+ 17. **T3 #2 "live bytes" is the live set's compacted footprint**: the same
904
+ records fully compacted, rows, attributes and postings included. Every
905
+ record carries about four times its frame in index structure, so a ratio
906
+ to frame bytes alone would measure the index, not garbage; both ratios are
907
+ reported (§28).
908
+ 18. **Type logs persist their retired set in the manifest** (not in ctl
909
+ records): a type has no ctl-record log, and the manifest is the one file a
910
+ MERGE_DONE names. The appendix sits after the CRC trailer, so readers and
911
+ opens that predate it parse the manifest unchanged.
912
+ 19. **A type meta segment retires with two synced head writes** instead of a
913
+ LANE_CKPT batch and a later DURABLE_CKPT head (A9): a type head has no
914
+ lane table, and after the second write neither slot names the segment,
915
+ whichever one a crash leaves torn.
916
+ 20. **A compaction plan pins its files**: anything retired after the plan was
917
+ made stays until the SWAP or the abort (a build reads the runs and meta
918
+ segments current at planning).
919
+ 21. **A failed build never quarantines**: the inputs are untouched, the
920
+ partition carries on, and requests end with an error (NOSPACE starts the
921
+ emergency). A quarantined partition ends its compaction requests at once.
922
+ 22. **TOMB_RANGE steps are also bounded in time** (4 ms of CPU, besides 512
923
+ rows): a kill looks up the row's tag instances in every run, so rows cost
924
+ more as a partition grows, and more under wasm (T3 #3: ≤ 10 ms a step).
925
+ 23. **Dead catalog copies are dropped by merges** (A15 says "type-log
926
+ compaction"): the type merge already reads every entry it could drop, so
927
+ no separate pass rewrites the runs; a copy goes once its first posting and
928
+ its DEAD posting meet in one merge (at the latest, the next full fold).
929
+ 24. **The dead-share trigger is 15%, not 25%** (§11). At rest a segment can
930
+ hold up to the trigger's share of dead rows, and a dead row costs as much
931
+ as a live one in rows, attributes and postings, so a store at rest sits
932
+ at up to 1/(1 − trigger) of its compacted footprint: 1.33× at 25%, above
933
+ T3 #2's 1.3. Measured on the T3 #2 run (20,000 CAT objects, 20% updated
934
+ per cycle, 100 cycles; Mac mem, load 6–26): 25%: 1.31–1.38 over six
935
+ runs; 20%: 1.35, 1.38; 15%: 1.115, 1.120; 10%: 1.19, with about the
936
+ same number of compactions (334–361). The price is write
937
+ amplification: a compaction at 15% rewrites 5.7 bytes per byte it
938
+ reclaims (3 at 25%). `EngineConfig::compactDeadRatio` sets it.
939
+ 25. **A compaction wave is one output**: segments next to the chosen one that
940
+ are past the trigger too join it, and the next candidate is looked for
941
+ right after a SWAP (not 50 ms later).
942
+
943
+ ## 30. Not built yet; running the T3 tests
944
+
945
+ Remaining T3 scope:
946
+
947
+ - **#5 hot split** with stage-1 helpers and sealed-segment re-homing (§12), and
948
+ A26's ring prep states (FREE → CLAIMED(epoch) → PREPARED). Sealed segments'
949
+ merges and compactions already build off the owner (deviation 1 here, T1
950
+ deviation 1); what is missing is stage-1 (verify, sha256, key extraction
951
+ claimed in ranges by idle writers) and its trigger. Its acceptance (≥ 2.5×
952
+ the unsplit rate with 4 helpers) is a throughput ratio that needs a quiet
953
+ many-core box to mean anything.
954
+ - **A15 arrivals compaction**: arrivals entries (24 B) and the GONE posting
955
+ of every dead gseq stay, about 40 B per record ever ingested; the quota
956
+ counts them but nothing shrinks them (the type-log test: 2.4 MB of catalog
957
+ runs, mostly GONE, and 2.5 MB of arrivals after 104,100 records with 835
958
+ alive). The type-log half (catalog runs, manifests, meta segments, dead
959
+ catalog copies) is built (§25). The arrivals half must drop an entry and its
960
+ GONE posting in one commit, or offset paging (live = entries − GONE, T2)
961
+ miscounts: the plan is to rewrite sealed arrivals segments inside the type
962
+ merge that drops their GONE postings, named by the merge's generation in
963
+ its manifest.
964
+ - **#1 latency ratio** on a quiet Linux-8 box.
965
+
966
+ ```
967
+ cpp/build/flatsql_ps_test --test=compaction_ # basic, T3 #1 and #2 (short forms)
968
+ cpp/build/flatsql_ps_test --test=compaction_half_dead_T3_1_full [--dir=<dir>]
969
+ cpp/build/flatsql_ps_test --test=compaction_cat_supersede_plateau_T3_2_full
970
+ cpp/build/flatsql_ps_test --test=orphan_crash_points_during_compaction_T3_4_full # 1,000 crash points
971
+ cpp/build/flatsql_ps_test --test=quota_ # T3 #3, #6
972
+ cpp/build/flatsql_ps_test --test=type_logs_reclaimed_under_readers
973
+ ```
974
+
975
+ **T3 #1 on a quiet Linux-8 box** (8 cores, ext4, nothing else running), from a
976
+ checkout at the landed commit; the test enforces the 1.5× bounds on such a box:
977
+
978
+ ```
979
+ cpp/build/flatsql_ps_test --test=compaction_half_dead_T3_1_full --dir=<ext4 dir>
980
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flatsql",
3
- "version": "3.0.0",
3
+ "version": "3.1.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
@@ -5,9 +5,9 @@
5
5
  "size": 2139350,
6
6
  "files": {
7
7
  "flatsql-ps-threads.wasm": {
8
- "sha256": "572a4bc38345d07ff91c345fc8d81e28873d72efa0fb8864a2edc5242145f9f5",
9
- "sri": "sha384-ewHpK0cJ1AAFg2xqLIWtGWyAE9W0A+4Vq6THCJ2WJpPcKxHqabdf7bbxt9Ngf7oZ",
10
- "size": 2074707
8
+ "sha256": "075dd0a104694df39b2776c34dc6444be224442717967d9cea5dd9af15964379",
9
+ "sri": "sha384-6COvEIcsSPYQ2gaha+/n5bdRTDysR84pfSw92d4F+Sxss3kCxYj2XNkPIfA9zEhC",
10
+ "size": 2237656
11
11
  },
12
12
  "flatsql-wasi-noeh.wasm": {
13
13
  "sha256": "fd744d4c502310669bc74ab555a36de1692030a0fa6e22fdb1df744809eeacbe",