AGENTS.template.md is what crafter-build writes AGENTS.md from on every build and test run: the partition map, the toolchain facts the code leans on, build and test commands, test conventions, code style, the design rules with their reasons, commit guidelines and the gotchas found in the storage and file layers. AGENTS.consumer.md is the section crafter-build copies into the AGENTS.md of every project that depends on this library: declaring the dependency, the table-struct rules, locking and durability, writing and querying, migrations, an errors table and how to test a consumer. Both were fact-checked claim by claim against the interfaces, the implementation units and the tests. AGENTS.md is build output and is now gitignored, as in the sibling libraries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
31 KiB
AGENTS.md
Guidance for coding agents and contributors working on Crafter.DB itself. The README is the user-facing manual, and AGENTS.consumer.md is what agents working in a consumer of this library read: crafter-build copies it into the dependency-notes section of the consumer's AGENTS.md on every build. When any of the three disagrees with the code, the code wins; the interfaces under interfaces/ are the contract, and tests/ShouldKeepLayoutGolden/main.cpp is the contract for the bytes on disk.
Project overview
Crafter.DB is the Catcrafts embedded table store: one C++26 module, Crafter.DB, with twelve partitions behind the umbrella (thirteen interface files) and two implementation units, no dependencies, Linux only. A table is a plain struct with a version; a file is a memory-mapped array of fixed-width rows plus a heap for strings and blobs; reads are zero-copy views; queries are lambdas or column expressions; schema changes are C++ migrations run on open. It replaces a hand-rolled, iostream-based transaction log in the first Catcrafts project that needed one (that port is still to do) and serves any Crafter project that keeps rows in a file.
| Path | What lives there |
|---|---|
interfaces/Crafter.DB.cppm |
The umbrella: export imports every partition. |
interfaces/Crafter.DB-<Partition>.cppm |
One partition per file, each but :Error opening with a comment that says what the partition owns. All but :File, :Storage and two plain functions in :Migration (DescribeColumns, PeekVersion) are templates and inline code. |
implementations/Crafter.DB-File.cpp, -Storage.cpp |
The two implementation units, Crafter.DB:File_impl and Crafter.DB:Storage_impl. File is the only unit that includes a C header. |
project.cpp |
The build description: the static library Crafter.DB and, for *-linux-gnu targets, the fourteen tests. No dependencies, no lint rules. |
tests/<Name>/main.cpp |
One test program per directory, declared in project.cpp. |
AGENTS.template.md, AGENTS.consumer.md |
The source of this file, and the notes for consumers. Every build and test run writes AGENTS.md from the template; the library has no dependencies, so the dependency-notes section never appears here. AGENTS.md is gitignored: edit the template. |
bin/, build/ |
Generated and gitignored. bin/Crafter.DB-<target>-<march>-<mtune>-<hash>/ holds the PCMs, std.pcm and libCrafter.DB.a, build/<same>/ the objects, and each test has a pair of its own. build/test-logs/<Name>.log keeps the output of failed tests. |
Partitions, from the bottom up (each imports only the ones above it):
:Error:Error : std::runtime_errorand its subclassesIOError(withcodeandpath),CorruptError,SchemaError,MigrationError,LockedError,OrderError,ReadOnlyError. Everything the library throws derives fromError, except thestd::out_of_rangeofAt()and friends and thestd::length_errorof aFixedStringthat does not fit.:Reflect: compile-time introspection of aggregates without P2996:memberCount,Member<I>,MemberType,MemberName<I, T>()(parsed out ofstd::source_location::function_name()),MemberIndex<T, Ptr>(),MemberIndexOf,TypeName<T>(),ForEachMember, theAggregateconcept. Thestatic_asserts at the bottom fail the library's own build if a compiler changes the shape offunction_name().:Format: the on-disk PODs (FileHeader,CommitRecord,Extent,SchemaBlobHeader,ColumnDescriptor,Var),Kind, the column flags, FNV-1a, CRC-32C,SchemaInfo(serialise, parse with range checks, fingerprint). Every struct size and the load-bearing offsets are pinned bystatic_asserts.:Formatter:Formatter<T>, the customisation point, with theScalar,FixedFormatterandVarFormatterconcepts;HeapandHeapWriter;EncodeVar/VarBytes;Key,StringRef,BlobRef<E>,FixedString<N>; the built-in formatters forstd::string,std::vector<Trivial>,std::chrono::durationandtime_point.:Schema:Layout<T>, computedconsteval: members classified byShape, nested aggregates flattened to dotted names, leaves placed by ascending alignment after the flags byte, the fingerprint;TableType,HasOrderedBy,HasPrevious,HasMigrate,OrderableKey;CheckTable<T>()with the diagnostics for the struct and its members (the size limits sit inLayoutandArrange);MakeSchemaInfo<T>(); theSchemaTestsnamespace of compile-time checks.:File: everything that talks to the OS:OpenOptions,OpenMode,Durability;File(descriptors,flock,fallocatewith theposix_fallocatefallback,pread/pwrite,fdatasync/fsync);MappedRegion(aPROT_NONEreservation filled withMAP_FIXEDextents);MappedPages; theOsnamespace (privatemkdir -p, rename without replace, hard link, directory sync, random bytes, page size, pid). Nothing here knows what a table is.:Storage: the type-erased engine.TableFilecreates, opens, validates, recovers, grows, flushes, compacts and verifies a table knowing rows only asrowSizebytes and columns only through the file's descriptors;InspectionandTableFile::Inspect,DynamicRow,TableStats,VerifyResult,CreateParams.:Codec: typed encode/decode overLayout<T>:EncodeRow,EmplaceRow,EncodeMember,DecodeRow,DecodeMember,View<M>,CopyLeaves/ZeroLeaves, the ordered-column helpers (LowerBoundSlot,UpperBoundSlot,CheckOrder) andAppendRows.:Row:RowId,Column<Ptr>,Row<T>(the 16-byte view),LiveIterator/LiveRange(sentinel-terminated, skips tombstones),SlotView(random access over every slot),DescribeRow,std::formatter<Row<T>>andstd::formatter<StringRef>.:Query: the expression typesCompare,And,Or,Notand their operators onColumntags,BoundsandCollectBounds(bound extraction from a top-level&&), the chainableQuery<T, R>.:Migration:CopyByName,ConvertChain,RequireFingerprint,WriteConverted/RewriteFrom/WalkChain,MigrateTable, and the exportedMigrate<T>(path)andPeekVersion(path).:Table:Table<T>, the user-facing handle: open/create/migrate, every write, iteration,Where/Select, the ordered-column searches, andCompact/ClearthroughRebuild.
Numbers the code and the tests pin (tests/ShouldKeepLayoutGolden, the SchemaTests namespace): the README's Transaction is 88 bytes, 8-aligned, with currency at offset 2, name at 8, bic at 24, iban at 40, description at 56, amount at 72 and timePoint at 80; a Var handle is 16 bytes with 12 inline; the header is one 4 KiB page followed by at least one schema page; the first extent of each region is 16 KiB, extents double up to 1 GiB, and a region holds at most 32 of them (roughly 17 GiB); 32 GiB of address space is reserved per region; a row is at most 65535 bytes and a table at most 65535 columns; a value is below 4 GiB. A fresh table is 24 KiB without string or blob columns and 40 KiB with.
Dev environment
crafter-build is the only tool, from its Flatpak (alias crafter-build='flatpak run net.catcrafts.CrafterBuild'; this machine has its stable and beta branches, both 4.0.0, and a build installed from a Crafter.Build checkout is branch local, chosen with --branch=). It compiles with its bundled clang 23.1.2 and libc++ 23.1.2, -std=c++26, import std;. The host's /usr/bin/clang++ is not used and is not a supported compiler for this library. The project has no dependencies, so nothing is fetched and no network is needed. The host is Linux x86_64.
What that compiler does and does not have, checked on 2026-10-17 (the library leans on all of it):
- No P2996 reflection:
-freflectionis accepted and ignored, and there is no<meta>.:Reflectexists because of that and is the only partition to replace when a clang shipsstd::meta. - Structured-binding packs (
auto&& [...m] = t;), pack indexing (m...[I]), user-generatedstatic_assertmessages, deducingthis,[[clang::no_destroy]],std::atomic_ref,std::ranges::fold_left,std::formatandstd::println: all in use. - libc++ 23 lacks
std::start_lifetime_as,std::inplace_vectorandstd::simd, so row fields are read with constant-sizememcpy(which folds to a load) and SIMD scans are not attempted. - libc++'s
system_clock::durationis microseconds (libstdc++'s is nanoseconds). The chrono period is part of the fingerprint for that reason, and the golden test pinsparamDen == 1000000.
The Flatpak sandbox sees the host filesystem but has its own /tmp (fs::temp_directory_path() inside a test is a per-app directory under $XDG_RUNTIME_DIR/.flatpak/net.catcrafts.CrafterBuild/tmp, shared by instances running at the same time and gone once none is), keeps its caches under ~/.var/app/net.catcrafts.CrafterBuild/cache/crafter.build/, and a binary it built runs only inside it: flatpak run --command=$PWD/bin/<Name>-<variant>/<Name> net.catcrafts.CrafterBuild. That is how you see a passing test's output, which the runner does not keep, the Benchmark's table in particular. touch tests/<Name>/main.cpp && crafter-build -v test <Name> prints the exact compiler command, the model for a scratch program against the built PCMs.
std.pcm is built once per target, --march and compiler into ~/.var/app/net.catcrafts.CrafterBuild/cache/crafter.build/<target>-<march>-<clang version>-<hash>/ and copied into each variant's directory; a toolchain update rebuilds it once, and a warm run of the whole suite takes about 2.5 seconds. The variant hash includes the compiler, so the directories of earlier compilers, and of the stub this repository started as (bin/Crafter.DB-…-97c53ceb1aa99864/, with colon-named PCMs), stay until crafter-build clean; glob for a directory rather than predicting its hash.
Build, test and style commands
crafter-build # bin/Crafter.DB-<variant>/libCrafter.DB.a and the PCMs
crafter-build test # build and run the fourteen tests, Benchmark included
crafter-build test 'ShouldM*' # name glob
crafter-build test --list
crafter-build test --jobs=1 --timeout=120
crafter-build -v test ShouldQuery # with the compiler commands
crafter-build clean # delete bin/ and build/
project.cpp registers no lint rules and does not pull in Crafter.LintRules (a #pragma crafter dependency would git pull and rebuild it on every load of project.cpp), so crafter-build lint and crafter-build format exit 1 with "No lint rules defined." Style is checked by eye, see "Code style". There is no CI. Before a change is done, crafter-build test passes: 14 passed as of 2026-10-31. Failed, crashed and timed-out tests write build/test-logs/<Name>.log; a later passing run does not delete the file (three from the first day are still there), so check its timestamp before trusting it.
Testing instructions
Tests are declared, not discovered: one cfg.AddTest("<Name>").Dependencies({ &cfg }); line in project.cpp, inside the if (cfg.target.ends_with("-linux-gnu")) block (the engine uses flock, fallocate and fixed-address mmap; the musl target gets the library and no tests). The source is tests/<Name>/main.cpp, an int main() linked against the local libCrafter.DB.a: exit 0 passes, nonzero fails, 77 skips (unused here). The runner gives each test 60 s in its own process, in parallel.
Conventions the fourteen follow:
- Names are
Should<Verb><What>;Benchmarkis the exception. Openmain.cppwith a comment that states the behaviour under test and the failure it guards against;tests/ShouldRecoverCommits/main.cppandtests/ShouldReadWhileAppending/main.cppshow the shape, and a regression test names the bug. Check(condition, what)andCheckEqual<T>(actual, expected, what)in an anonymous namespace printFAIL: …(with both values forCheckEqual) and count;mainprintsokand returns 0 only when nothing failed. Do not stop at the first failure unless later steps are meaningless without it.- Each test works under
fs::temp_directory_path() / "crafter-db-<name>",remove_alled first and last, with a name of its own because the runner is parallel. Table structs are declared per test: the README'sTransactionwhere the reference layout matters, a one- or two-member struct where it does not (Item,Entry,Tick). - Process-level behaviour is tested with
fork().ShouldLockAcrossProcessesopens the table in a child and maps the outcome to an exit code;ShouldRecoverCommitsruns the writer in a child that ends with_exit(0)while itsTableis still in scope, so no destructor and noFlushruns, which is the only honest way to simulate a crash. Those two#include <sys/wait.h>and<unistd.h>before their imports. Damage to a file is done with aPatch(path, offset, bytes)helper overstd::fstreaminShouldRecoverCommits,ShouldRejectSchemaChangeandShouldVerify. - Concurrency is tested with
std::jthreadreaders scanning while the main thread appends and erases (ShouldReadWhileAppending), and such a run ends withtable.Verify().Ok(). Benchmarknever fails. It prints the legacy format (a cleaned-up copy of the hand-rolled iostream log Crafter.DB replaces) against Crafter.DB on 100 000 rows with a warm cache; raiserowCountfor a serious run, and quote its numbers with the machine (the README's are a Xeon w7-3465X).
What is covered: layout and fingerprint (ShouldKeepLayoutGolden), encode/decode and reopen (ShouldRoundTrip), pointer stability across extents (ShouldKeepViewsStableAcrossGrowth), StringRef/BlobRef semantics (ShouldCompareStringRefs), every query form and the ordered column (ShouldQuery), tombstones and Compact (ShouldEraseAndCompact), every member shape and both formatter shapes (ShouldFormatCustomTypes), the migration chain and its refusals (ShouldMigrate), schema and corruption refusals (ShouldRejectSchemaChange), the commit protocol under crashes (ShouldRecoverCommits), locking (ShouldLockAcrossProcesses), Verify and DynamicRow (ShouldVerify), readers against a writer (ShouldReadWhileAppending). Not covered: a table on a 16 KiB-page kernel, reserveBytes exhaustion, the posix_fallocate fallback, prefetch.
A bug fix ships with a test that fails before it and passes after. A change to the bytes on disk makes ShouldKeepLayoutGolden fail: that is the point of it, so read design rule 4 before updating its expectations.
Code style
Nothing is enforced by tooling; match the file you are in. The conventions are those of Crafter.LintRules' full set, applied by hand.
- The first two lines of every source are
// SPDX-License-Identifier: LGPL-3.0-onlyand// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®, with a space after//(as Crafter.Build spells it; Crafter.Math, Network and Asset do not), then a blank line. Every interface but:Errorthen carries a comment block saying what the partition owns, beforeexport module Crafter.DB:<P>;; keep it true when the partition changes. - Four-space indentation, no tabs, LF, a final newline, K&R braces,
if (x) return;on one line when it fits, one declaration per statement,if (andfor (with a space and no space before a call's(. import std;everywhere. The only C headers are in the global module fragment ofimplementations/Crafter.DB-File.cpp(aftermodule;, with#define _GNU_SOURCE 1) and the two POSIX headers the forking tests include before their imports.__builtin_offsetofrather than<cstddef>.PascalCasefor types, functions and concepts;camelCasefor variables, members and constants alike (inlineVarBytes,fnvOffset,rowFlagDeleted), which is why the table statics areversionandorderedBywhilePreviousandMigrateare a type and a function. Internal namespaces areReflect,Detail,OsandSchemaTests.enum classonly. Fixed-width integers; plainintandlongappear only where POSIX or the language demand them (main,waitpid'sstatus,operator++(int), whatsysconfreturns) and in the tests'failurescounters.std::formatandstd::println; iostream appears only in the Benchmark's copy of the legacy format and in the tests'Patchhelpers and fixture writers. Exception messages start with the path where one is known ({}: …;Heap::Read,EncodeVar,CompactInto, the reservation and themsyncfailure carry none). Thestatic_asserts ofCheckTable,CopyByNameandCheckEmplaceArgumentstart withCrafter.DB:and name the table type and the member (MessageandMemberMessagein:Schema); a handful of internal limits assert without the prefix.- Scalars and handles are read with
std::memcpyinto a local, and the flags byte throughstd::atomic_ref<std::uint8_t>because readers race withErase; the only reinterpreted pointers are the zero-copy views (StringRef::View,BlobRef::View,DynamicRow::Text) and the schema name pool. - Hot paths carry no indirection: predicates are template parameters, column offsets are
constexpr, andHeapWriter::Growis the only virtual call, reached only when an extent fills. A change to the scan loop,StringRef's comparisons,Row::operator[]or the encoders comes with evidence, the Benchmark before and after or the-O3assembly of the loop; the maintainer reviews performance claims at that level. - Comments say why: the failure mode, the race, the rejected alternative (
MADV_DONTFORKin-File.cpp, theexternal<T>object in:Reflect, the sentinel iterator in:Row). The code carries long comments on purpose; do not strip them, write them for new non-obvious code, and give commit bodies the same treatment.
Design rules
Decided deliberately; change them only with the maintainer's say-so.
- Structs are the schema. No macros, registration, traits to specialise or code generation. Every rule about a table struct's members is a
static_assertinCheckTable<T>()with a message naming the table and member, the size limits arestatic_asserts and consteval throws inLayoutandArrange, and a type with noFormatteris a clean concept failure. Misuse is a compile error, never a runtime surprise. - One file per table, two regions, fixed addresses. Rows and heap are chains of doubling extents appended at the end of the file and mapped
MAP_FIXEDinto aPROT_NONEreservation, so rowiisbase + i * rowSizeand no pointer moves while the table is open. No free-space management:Compact()right-sizes. Region slots 2 and 3 are reserved for a persisted index and a WAL. - Fixed-width rows, 16-byte handles, a bump-arena heap. Flags byte at offset 0, leaves stable-sorted by alignment, row size rounded to
rowAlign. Strings and blobs are Umbra handles (length, 4-byte prefix, then 8 inline bytes or a heap offset), so values of up to 12 bytes live inline (strings, and blobs whose elements are aligned to 4 or less). The heap has no per-entry header;Update,SetandEraseorphan bytes that onlyCompact()reclaims. Little-endian only. - The file format is a contract pinned twice: by the
static_asserts on every struct size and the load-bearing offsets in:Format, and bytests/ShouldKeepLayoutGoldenwith an independent spelling of the fingerprint and the exact handle bytes.Kindis append-only. The fingerprint covers names, kinds, offsets, sizes, flags, chrono period, blob element type, clock and the ordered column, and what it covers is what aversionbump is required for. Changing any byte an existing file holds, or the fingerprint's inputs, is a format change: bumpformatVersion(1 today, exact match required) and say so; never reinterpret old files silently. - Durability by two commit records, not a journal. An append is visible to in-process readers at once (release stores of the published counts) and durable at
Flush():fdatasync, the inactive commit slot with its CRC,msync. On open the newest valid commit wins and everything past it, uncommitted extents included, is discarded; an unclean close recounts tombstones.UpdateandSetcommit their new heap bytes before the row points at them, and the row bytes themselves are not atomic against a power loss; that limitation is documented, and the WAL region is the v2 answer, not anfsyncper row. - A type-erased engine under a thin typed layer.
TableFileknows rows asrowSizebytes and columns through the file's own descriptors, soInspect,Verify,Compact,DynamicRowand a migration from a struct that no longer exists work without the C++ type;Table<T>adds compile-time offsets and encoders on top;:Fileknows nothing about tables at all. Keep each layer ignorant of the one above it. - Queries are C++ and inline. A predicate is a template parameter stored by value in the view: no
std::function, no virtual call, no per-row call. Search keys are encoded once (Key, or automatically in a column expression). Bound extraction reads only comparisons on theorderedBycolumn in a top-level&&chain;||and!leave the whole predicate as a filter, which is always correct and only ever a narrowing. No SQL and no planner; hash indexes (indexedBy) are deferred, not refused. - One writer, many in-process readers, a standard container's contract plus a little.
flockexclusive for a writer, shared forreadOnly, between processes and between twoTableobjects in one.Append,EmplaceandEraseare safe while other threads read (published counts, a monotonic tombstone flag, sentinel-terminated iteration that never compares against a moving end);Update,Set,CompactandClearneed exclusive access from the caller.Rowviews andStringRefs surviveAppendandErase. - Private by default, and never replace. Files
0600in0700directories,O_NOFOLLOW,MADV_DONTDUMP, creation through anO_EXCLtemp file and a rename that fails when the target exists (racing creators share one file), rewrites through a temp file andrename(a visible table file is always valid).MADV_DONTFORKwas tried and rejected: a forked child may read an inherited table, and its writes,Flushand destructor flush are refused by the owner-process check instead. - Every byte from a file is checked before it is used: header fields, both commit CRCs, extents against the committed length, every descriptor field in
SchemaInfo::Parse, every heap handle against the published heap length (Heap::Read), the page-size granule against the running kernel. A new field read from a file gets its check and a case inShouldRejectSchemaChangeorShouldVerify. - Linux, no dependencies, no host split. The library needs
flock,fallocateand fixed-addressmmapand says so; there is no#ifdeffor another platform and no portability layer to grow. It depends on nothing, so a consumer's build fetches nothing for it.
Commit and PR guidelines
- Branch off
master(claude/<topic>is the pattern for agent work in the sibling repositories) and open the PR againstmasteron the project's Forgejo. The repository isCatcrafts/Crafter.DBon the project's Forgejo:forgejo@forgejo.catcrafts.net:Catcrafts/Crafter.DB.gitover SSH,https://forgejo.catcrafts.net/Catcrafts/Crafter.DB.gitfor consumers. A checkout whoseoriginstill names the personal namespace the repository moved from can fetch (HTTPS redirects) but not push over SSH ("Push to create is not enabled"); repointorigin. Pushes go over SSH. - The history has one commit, so take the voice from the sibling repositories:
type(scope): imperative summarywithfeat,fix,refactor,docsortestand the partition as the scope (storage,schema,query,migration,file,format,reflect). Body: what was wrong, why, and why the fix has this shape, wrapped at about 75 columns, with the numbers when something was measured. Add aCo-Authored-By:trailer when an agent wrote it. - Separate commits for the fix, its test and the README change when each stands on its own. The README is the manual: keep its module table, examples and "Performance" table true to the code. Update
AGENTS.consumer.mdin the same change when a signature, a default, a limit or an error behaviour consumers lean on changes; consumers pick it up on their next build. - A format change (design rule 4) updates
ShouldKeepLayoutGoldenin the same commit, and the commit message says it is one. crafter-build testgreen before pushing. There is no CI.- Never commit
bin/,build/orAGENTS.md.
Security considerations
- A table file is untrusted input: anything on disk may have been truncated, bit-flipped or crafted. Design rule 10 is the defence, and
ShouldRejectSchemaChangeandShouldVerifyare its regression suite.Verify()is the deep check and returns problems rather than throwing, so a tool can print them all. There is no per-row checksum: a flipped byte inside a scalar column goes unnoticed. - The defaults keep data private (
0600,0700,O_NOFOLLOWagainst a swapped symlink,MADV_DONTDUMPagainst core dumps). Do not loosen a default; a consumer that wants a readable file setsfilePermissions. - The lock is advisory (
flock). A process that ignores it, orcp, can read the file mid-write, and what it reads is valid up to the writer's lastFlushbecause only committed rows count on open. reserveBytes(32 GiB per region, two regions per table) is address space, not memory, but a tightulimit -vor hundreds of open tables turns it into anIOErrorat open.project.cpphas no dependencies, so loading it executes nothing fetched from the network; keep it that way, or pin acommiton anything added.- Tests write only under
fs::temp_directory_path(), fork only to exercise locking and crashes, and never touch the network.
Gotchas
- Adding a partition is three edits: the interface file, the
ifacesarray inproject.cppand the umbrellaCrafter.DB.cppm; an implementation unit also goes into theimplsarray. Keep the order of the "Project overview" list: a partition imports only what is above it. MemberNamedepends on clang spellingfunction_name()as… [P = &external.iban]andTypeNameon… [T = <qualified type name>]; thestatic_asserts at the bottom of:Reflectcatch a change at the library's build.Reflect::external<T>is[[clang::no_destroy]] inline const Ton purpose: an undefinedexternwould be cheaper but warns at every instantiation.- Leaf indices follow declaration order, depth first; column offsets do not. A
std::optionalcontributes a presence leaf namedname?before its payload, a nested aggregate flattens toouter.inner, astd::arrayof scalars toname[i], and the flags byte is column 0 but no leaf.Layout<T>::Offset(leaf)andColumn(leaf)translate. - Reordering two members of the same alignment class changes offsets and therefore the fingerprint; the README's
Transactionhascurrencyat offset 2, ahead of every string. Both follow from rule 3, and aSchemaErroris the symptom in a consumer that forgot theversionbump. charis alwaysKind::Int8whatever the platform's signedness,wchar_tdepends on its size,long doubleis not storable,std::optional<std::optional<…>>is rejected (no distinct column names), and aFormattermay overridekindonly when itsStoredis a single column.- The
TransactioninsideSchemaTestsand theProbeinside:Reflectare compile-time tests that run on every build of the library, not examples to copy from. TableFile::Inspectis a static member; the README's module table calls itInspect(path). The tests spell itDB::TableFile::Inspect(path).- Extents are aligned to the larger of 4 KiB and the running kernel's page size at creation, and
header.pageSizerecords it. A file created on a 4 KiB kernel is refused on a 16 KiB one with anIOError(not_supported) rather than mapped badly; nothing tests that path. - A region tops out at 32 extents (
IOError,file_too_large, "Compact() it");Compactsizes the first extent of the new file to the live payload, which resets the chain. A reservation that fills throwsIOError(not_enough_memory) namingOpenOptions::reserveBytes. Table::Openis a loop for a reason: the first open fails withno_such_file_or_directory, the create may lose a race and fail withfile_exists, and the second pass opens the other process's file. Both halves of the race are needed, andTableFile::Createnever replaces.RemoveStaleTempsruns on every writable open, after the exclusive lock is taken, and deletes the<file>.tmp-*siblings it can lock exclusively; the probe spares a temp another process is still writing. The twelve-hex-digit suffix comes fromOs::RandomBytes.- A migration's backup
<file>.v<old>.bakis a hard link to the old inode taken before the rename, so it costs nothing until the original is replaced, and it fails on a filesystem without hard links. Deleted()reads the flags byte through a relaxedatomic_ref,MarkDeletedraises the monotonictombstonesflag before the byte, andLiveRange::begin()reads that flag when iteration starts rather than when the range was built, so aQuerybuilt before anErasestill skips the row. A scan that started before theErasemay yield it; that is documented, not a bug.LowerBound/UpperBoundsearch every slot, tombstones included, because an erased row keeps its key; onlyCompactdrops keys. AWherewith bounds on the ordered column does the same.- A
QueryoverRows,Range,Fromand the like can be iterated as aconstobject; one built withWherecannot, becausestd::ranges::filter_viewhas no const iteration (the deducing-thisrequiresclauses inQueryare what makes the first case work).Query::Selectis rvalue-only:std::movea named query first. Emplaceconverts arguments implicitly, never withstatic_cast, so anintfor an enum member or swapped arguments fail to compile with a message naming the member.Appendof a range that is not both sized and random-access buffers it first, so the order check covers the whole batch before anything is written.Set<&T::member>copies only that member's leaves back into the slot (CopyLeaves) and leaves the flags byte alone, so anErasethat raced with it is not undone even though the contract givesSetexclusive access. Do not "simplify" it to a whole-rowmemcpy.- After
fork()the child'sTablereads fine and refuses every write,Flushand its destructor's flush (OwnedByThisProcess): the parent's counters in the child are stale and must never reach the file. Do not make the destructor flush there. - The Benchmark's
last timestamp (1000x open+read)row is the one operation where the legacy format wins (about 2 ms against 30 ms): a mapped open costs onemmapper extent. The answer is to open once per process, which the README and the consumer notes say; do not try to win that row by changing the format. crafter-build testprints "Building std PCM" once per configuration (the library and each test) on every run; the step returns at once when the cached PCM is current, and only a toolchain update rebuilds it.