Contents
Developing pg_lion
Building, testing and finding your way around the source. Section numbers (§) refer to DESIGN.md.
Supported versions and CI
Builds against PostgreSQL 16, 17, 18, 19 and master (20devel); the version differences live in
src/lion_compat.h. It needs a POSIX mmap(): LionOrdered’s set, past hash_mem, lives in a
temporary file mapped into memory (src/lion_arena.c, DESIGN.md §30.4, “Spilling”). CI (.github/workflows/ci.yml) builds and tests, on
each pull request, 16 to 19 as the PGDG packages and 18 built from source
with assertions and injection points, and on every one of them
runs the unit tests (as built, and under ASan and UBSan), the SQL regression files in test/sql
and the isolation specs in test/isolation in both WAL modes, make hookcheck, and the
crash-recovery/hot-standby harness (test/recovery/run.sh) in both WAL modes with
wal_consistency_checking. The other source builds - 16, 17, 19 and master, with assertions,
and from 17 with injection points - run when .github/workflows/optional.yml is started by hand
from the Actions tab, which builds all five by default; master is tested only there. The same
workflow runs 18’s packages on arm64, which is not a first-class target. A pull request that changes only
documentation - Markdown files, docs/, bench/ and LICENSE, as .github/workflows/changes.yml
says - skips the build and test jobs and CodeQL; the CI result check still reports, and passes. On the source builds pg_lion itself is compiled with UBSan
(COPT=-fsanitize=undefined -fno-sanitize-recover=all), so the whole suite runs under it and a
report aborts the backend that met it. What parks a backend on an injection point - the isolation
specs that do (grep -l injection_points test/isolation/*.spec), which are the race tests of the
VACUUM/count/split interlocks, and the harness’s phases 1b-1e, 1g and 3 - needs a server configured
with --enable-injection-points and the injection_points test module. PostgreSQL 16 has no
injection points, and the PGDG packages ship without the module, so CI runs those specs and phases
on its 18 source build (and the 17, 19 and master ones of a run by hand), where a missing module
fails the job
(INJECTION_POINTS=1); on 16 and the packages the Makefile leaves the specs out and says so, and
the harness runs its phases 1, 1f, 2, 4 and 5. The concurrent soak (test/soak) runs weekly on
18’s packages, and by hand from the Actions tab (.github/workflows/soak.yml).
lionvec’s tests need pgvector, which every job installs or builds; a failed install or build fails
the job, and PGVECTOR=1 keeps the Makefile and the harness from skipping them, except on 19’s
packages while apt.postgresql.org has none for a beta. The source builds are also the only ones
with assertions, so debug_discard_caches runs there (discard_caches.sql, on 16, 19 and master, in a run by
hand), and make hookcheck on 16 is where the refusal to read a lion index under old_snapshot_threshold is tested (16 alone
has the setting, DESIGN.md §9).
Build and test
./dev.sh reset # initdb a private cluster in .local/data (needs .local/pg)
make PG_CONFIG=.local/pg/bin/pg_config && make PG_CONFIG=.local/pg/bin/pg_config install
eval "$(./dev.sh env)"
make PG_CONFIG=.local/pg/bin/pg_config installcheck # regress files + isolation specs
make unit PG_CONFIG=.local/pg/bin/pg_config # bit-planes, containers, segments, positions,
# entry layout, lionvec's arithmetic, the read
# stream ring; no server
make planes-bench PG_CONFIG=.local/pg/bin/pg_config # the bit-plane kernels timed, at -O2
make vecplanes-eval PG_CONFIG=.local/pg/bin/pg_config # lionvec's codes as bit-planes 2-8, evaluated
# and not adopted (DESIGN.md §44.14); minutes
No -march is needed for speed: on x86-64, GCC and clang builds carry AVX2 and POPCNT versions of
the container library’s passes over whole bitsets, AVX-512 (VPOPCNTDQ), AVX2 and POPCNT ones of the
bit-plane kernels - lionvec’s plane 1 among them - and AVX2 (and F16C) ones of lionvec’s others,
compiled with target attributes, and pick one at run time by what the CPU has - one choice for the
extension, lion_simd_current() in src/lion_planes.h, which every one of them follows; other
targets and compilers get a portable one, and make LION_NO_SIMD=1 leaves the x86-64 ones out
(DESIGN.md §3, “Whole-bitset kernels”; §44.3, “Kernels”; §50). make unit runs the bit-plane,
container and lionvec tests against each version the CPU has, and once more built without the
x86-64 ones (test/unit/planes_test_nosimd, test/unit/container_test_nosimd,
test/unit/vec_test_nosimd).
Lion logs through one of two WAL resource managers (DESIGN.md §25), chosen per index at CREATE INDEX, so the suite has to pass in both:
make PG_CONFIG=.local/pg/bin/pg_config installcheck # generic WAL (no preload)
make PG_CONFIG=.local/pg/bin/pg_config installcheck-rmgr # the custom resource manager
make PG_CONFIG=.local/pg/bin/pg_config recovery-check RECOVERY_PREFIX=<prefix>
make PG_CONFIG=.local/pg/bin/pg_config recovery-check-rmgr RECOVERY_PREFIX=<prefix>
installcheck-rmgr restarts the dev cluster with shared_preload_libraries = 'pg_lion' on
pg_ctl’s command line, proves the restart took - the server lists pg_lion as a WAL resource manager
and a freshly built index reports rmgr - and only then runs the same suite (walrecords has an
expected file for each mode, so the suite alone would pass in either), and afterwards puts the
cluster back as it found it: restarted in generic mode, or stopped. Nothing is written into
postgresql.conf, so an interrupted run leaves no trace; make hookcheck works the same way.
recovery-check-rmgr is the recovery
harness with the resource manager registered AND wal_consistency_checking = 'pg_lion', which is
where “replay reproduces every page” is actually proved - the comparison only happens during
replay, so turning it on for a primary that never replays proves nothing.
.local/pg can be any PostgreSQL 16 or later install. What the suites need from it:
- contrib:
citext,pageinspect,pg_buffercache,pg_freespacemapandpg_walinspect. The regression suite creates all five. Withoutpageinspectthebuild,corrupt,corrupt_items,corrupt_recordsandsummaryfiles fail, withoutpg_buffercachethecorrupt,corrupt_items,pinbudgetandrangefiles and thecount_batch_raceandgettuple_pausespecs fail, withoutpg_freespacemapdir_deletefails, withoutpg_walinspectwalrecordsfails, and most files usecitext.installcheckchecks for all five before it starts and stops with one message if any is missing. The PGDG packages (postgresql-N) include contrib; a source build needsmake -C contrib install. - pgvector 0.7 or later (for
halfvec) forvector.sql,vector_filter.sql,vector_half.sqland thelionvec_*isolation specs, lionvec’s files (DESIGN.md §44): the PGDG packagepostgresql-N-pgvector, or a build of github.com/pgvector/pgvector against the samepg_config. Where it is missing those files are skipped and the run says so (PGVECTOR=1forces them;PGVECTOR=0skips them), as is the recovery harness’s phase 5, which underPGVECTOR=1fails instead. pg_lion itself builds and loads without it. - Session settings the expected plans assume (
work_mem,maintenance_work_mem, no parallel workers, no JIT) are passed to every connection the suite makes throughPGOPTIONS(LION_TEST_OPTIONSin the Makefile), so a cluster other than./dev.sh’s gives the same plans. pg_isolation_regressfor the isolation specs: a source build installs it withmake -C src/test/isolation install, and the packages ship it inpostgresql-server-dev-N.injection_pointsfor the specs that park a backend on an injection point: a server configured with--enable-injection-points(17 or later) and the test module installed withmake -C src/test/modules/injection_points install. Where the module is missing those specs are skipped and the run says which (INJECTION_POINTS=1forces them, and makes a missing module an error instead).- The recovery harness (
make recovery-check) needs a whole installation to initdb clusters in, named byRECOVERY_PREFIX, which it builds and installs the extension into (RECOVERY_SKIP_INSTALL=1uses the one already installed there instead); as root it also needsRECOVERY_RUN_AS=<unprivileged user>(test/recovery/README.md). Its phases 1b-1e, 1g and 3 needinjection_points, and are skipped without it unlessINJECTION_POINTS=1makes that an error.
The regression files VACUUM with FREEZE wherever what they print depends on the VACUUM having
done all its work - index statistics after a DELETE, a plan the visibility map prices, a count
that must skip every heap block. A plain VACUUM that cannot get a heap page’s cleanup lock at
once (the checkpointer and the background writer pin the pages they write) checks that page
without pruning it: its dead TIDs stay in every index and its visibility-map bit stays clear. An
aggressive VACUUM waits for the lock instead. The isolation specs keep plain VACUUMs, whose waits
are part of what they test.
make soak (test/soak/README.md) is the long-running check the suites are not: four pgbench
writers (inserts, updates, deletes, rollbacks, savepoints, upserts, and a phase of writes that all
roll back), a VACUUM loop, and readers that compare every lion-answered query - counts, GROUP BYs,
count(DISTINCT), the aggregates over keys, the direct SQL counts, LionOrdered, bitmap and plain
scans - with a sequential scan inside the same REPEATABLE READ or SERIALIZABLE snapshot, some of
them holding it across VACUUMs, plus periodic lion_index_verify(idx, true); ten minutes in each
WAL mode, with a hot standby whose readers compare the same way in rmgr mode. It restarts the dev
cluster as installcheck-rmgr does and puts it back; test/soak/soak.sh runs it against any
running cluster. Races it found are kept as reproducers in test/soak/findings/, whose expected
outputs hold the correct answers (test/soak/findings/run-spec.sh).
dev.sh puts its cluster’s socket in $XDG_RUNTIME_DIR/pg_lion-<user> (or /tmp/pg_lion-<user>)
on port 54329, and LION_SOCK / LION_PORT move it. The cluster trusts local connections, so
dev.sh makes a missing socket directory mode 0700, and refuses the default one if it is a symlink,
belongs to someone else or is open to others. The cluster runs with wal_level = replica, so
that index builds and the write paths of indexes created in the same transaction are WAL-logged in
the suite as they are in production.
Changing the SQL scripts
An installed extension’s functions and operator classes change only through ALTER EXTENSION
... UPDATE, so a change to pg_lion--<version>.sql (or pg_lion_citext’s, or pg_lion_vector’s)
is a new version (§23, “Extension versions”):
git mv pg_lion--<version>.sql pg_lion--<next>.sql, make the change there, and setdefault_versioninpg_lion.controlandprovides.pg_lion.filein META.json to match.- Add
pg_lion--<version>--<next>.sqlmaking the same change to an installed copy:CREATE OR REPLACE FUNCTIONwhere the arguments and result stay the same,DROP FUNCTIONthenCREATE FUNCTIONwhere they or the OUT columns change,ALTER OPERATOR FAMILY ... ADDfor new strategies and support functions,CREATE OPERATOR CLASSfor new classes, and the sameCOMMENT,GRANTandREVOKE. Never edit an upgrade script that is on main. Run the checks, which CI runs too (
.github/workflows/upgrade.yml, on 18’s packages):./test/upgrade/lint.py origin/main # versions, script names, META.json, no edits to old scripts make upgradecheck # every earlier version updated, against a fresh installmake upgradecheckneeds the build installed, the dev cluster running, write access to the installation’s extension directory (it puts each old install script there during its test) and the full git history.
<next> is X.Y.Z-devN+1 after X.Y.Z-devN, and after a release the next release’s -dev1
(after 0.3.0, 0.3.1-dev1). A release turns X.Y.Z-devN into X.Y.Z
the same way, with an upgrade script that changes nothing. Versions before 0.3.0 have no upgrade
path (test/upgrade/floor).
Calibrating the cost model
Two scripts in bench/calib/, both Python’s standard library and psql, best run on a release
build with nothing else on the machine:
python3 bench/calib/ops.py --setup && python3 bench/calib/ops.py # lion's operations against core's
python3 bench/calib/matrix.py --setup-only && python3 bench/calib/matrix.py --out run.json # plan choices
ops.py times each lion-only operation the planner prices against the core operation it stands in
for, on the same build, and prices it at that operation’s own rate - its time over core’s price of
it, in cpu_operator_costs - printed beside the setting that charges it (DESIGN.md §49,
“Calibration”): a nested loop’s B-tree probe against core’s Index Scan, apportioned between
pg_lion.probe_rescan_cost and pg_lion.probe_entry_cost. It prints a sequential scan’s rate
too, the slope of its time over its quals, for comparison. matrix.py plans and times a fixed set of queries under
every arm and reports the planner’s mispicks (DESIGN.md §39, “The matrix”).
--setup-file bench/calib/joins.sql --queries bench/calib/joins.py swaps in the joins of DESIGN.md
§40.10 and §46 to §49 at scale (a 4.7 GB fixture at --rows 4000000), --arms plans under the
named arms alone (probejoin, ordjoin and probe force lion’s joins), --sweep A+B=v+w,...
sweeps settings together, and --cold CMD runs CMD - one that stops the server, drops the
database’s files from the OS cache and starts it - before each timed run.
Source layout
src/lion_tid.h TID <-> (container key, 15-bit lo) encoding; 9 offset bits at 8K pages
src/lion_planes.[ch] bit-plane arithmetic - popcounts of a bit vector AND each of k planes, interleaved
or each its own array, their weighted sum, the 64x64 bit transpose and values to
and from planes - and the extension's one choice of SIMD implementation
(portable, POPCNT, AVX2, AVX-512); below the container library and lionvec,
frontend-safe and unit-tested standalone (test/unit/planes_test.c)
src/lion_container.[ch] container library (array/bitset/run/narrow), set algebra, AVX2/POPCNT bitset kernels
picked by that choice at run time, unit-tested standalone
src/lion_sparse.[ch] sparse (container key, offset) segments, unit-tested standalone
src/lion.h on-disk structs, the relation state, and the page layer's and the AM's functions
src/lion_entryfmt.h the directory entry tuple's byte layout and accessors, unit-tested standalone
src/lion_pages.c index pages: initializing, deleting, recycling; the items of container pages
src/lion_meta.c the meta page
src/lion_state.c per-relation state: columns, opclass procedures and order; usability checks; keys
src/lion_entry.c entry tuples, and the max_entries cardinality guard
src/lion_posting_put.c writing container chains: putting items, spilling an INLINE entry, page splits
src/lion_compat.h the differences between PostgreSQL 16, 17, 18, 19 and master
src/lion_wal.[ch] the WAL shim every write path calls, and the custom resource manager
src/lion_dir.c the sorted entry directory: a Lehman & Yao B-tree keyed by the index key
src/lion_posting.c the per-key posting tree: a B-tree over container keys, GIN's shape
src/lion_am.c handler, reloptions, amvalidate, table AMs, buildempty, _PG_init hook/GUC
src/lion_amcost.c amcostestimate: the cost of plain and bitmap scans
src/lion_build.c ambuild via tuplesort (hash, key, tid code); INLINE entries or per-key posting trees
src/lion_scan.c amgetbitmap, and amgettuple for plain index scans
src/lion_insert.c aminsert (serialised on the directory leaf; bitset in-place fast path; a row's
bit slices a walk of the leaf a column, their in-place adds several to a record)
src/lion_vacuum.c ambulkdelete with cleanup locks on every page, two-pass cancellable protocol
src/lion_funcs.[ch] lion_index_stats() and the other diagnostics; lion_funcs.h is private to them
src/lion_verify*.c lion_index_verify(): pages and posting sets, the directory, rechecks and
heapallindexed, summary posting sets
src/lion_count.h the count engine's interface - lion_count_keys(): VM-interlocked counting,
per-block batched heap recheck - and lion_count_int.h what its files share:
src/lion_set.c locating posting sets, the list pin budget, the lookup walk
src/lion_set_copy.c private copies of posting sets, spilled to a file when too big
src/lion_cursor.c container cursors over one posting set
src/lion_expr.c expression cursors over boolean expressions of sets; the stream the scans read
src/lion_vis.c the visibility map a container at a time, and the per-query visibility cache
src/lion_count.c counting a container against the map and the heap; the merge
src/lion_count_groups.c the groups of a walk counted together
src/lion_count_shared.c the copy the participants of a parallel plan share
src/lion_rangesrc.c a range as a source: collected, or probed at the other sources' rows
src/lion_range.c range restrictions, and iterating every entry of an index
src/lion_count_sql.c the SQL count functions (lion_index_count() and the others)
src/lion_facets.c lion_index_facets(): the rows under each key of a column, under a filter
src/lion_slice.c a bit-sliced column's counts and descents (LionSliceAgg), lion_index_slice_agg()
src/lion_slice_decode.c a sliced column's aggregates for a batch of groups, decoded key by key
src/lion_customscan.h the LionCount custom scan's private header, shared by its planner half:
src/lion_plan_*.c create_upper_paths_hook -> CustomPath/CustomScan "LionCount"; set_join_pathlist_hook
-> "LionSemiJoin"/"LionAntiJoin"; index matching, partitions, the cost model
src/lion_exec_*.c and its executor half: begin, locating the WHERE sets, counting, the FK-side
join, the run and parallel DSM, EXPLAIN
src/lion_costs.[ch] the cost model's constants as planner settings (pg_lion.*_cost)
src/lion_fkjoin.[ch] the FK-side joins a LionCount answers (fact JOIN dim, EXISTS / NOT EXISTS), and the semi/anti join paths
src/lion_ordered.c CustomScans "LionOrdered" and "LionBtreeScan": lion-filtered walks of a btree (its
values from the index tuples when it covers the query) or of a lion column's order
src/lion_probejoin.c CustomScan "LionProbeJoin": a GROUP BY of a join to a dimension by its primary
key, each fact row's key looked up in the B-tree and tested against lion sets
src/lion_ordjoin.c CustomScans "LionOrderedJoin": a fact's btree walked in order across a join to a
dimension's unique key, both sides filtered by lion sets, key-only tables as hash sets;
and "LionOrderedSplit": that join run once per entry of a dimension column that leads
the ORDER BY, the dimension narrowed to the entry's rows
src/lion_multikey.c array_ops/tsvector_ops: GIN-style extraction and tsquery key trees
src/lion_jsonb.c jsonb_contains_ops: path keys for jsonb @>, ?, ?|, ?&, @? and @@
src/lion_vec.[ch] lionvec, the nearest-neighbour AM of pgvector's vector and halfvec (created by
the pg_lion_vector extension): pages, build, inserts, VACUUM, costs, handler;
lion_vec.h is what LionOrdered uses to restrict its scan to a lion set
src/lion_vec_scan.c its scan: passes over the 1-bit codes, the lazy queue of bounds, refines and
exact distances
src/lion_vec_loc.c its locator: where vectors in TOAST lie, so that the scan reads them from their
chunks' pages directly - the lookups, the checked reads, LOC and LOCDIR pages
src/lion_vec_math.[ch] its rotation, bit-plane codes, kernels and error bounds - plane 1's kernel
lion_planes.c's, and every kernel run by lion_planes' choice - frontend-safe
and unit-tested standalone (test/unit/vec_test.c)
src/lion_readahead.[ch] reading a relation's blocks in an order its reader predicts: a read stream
over the blocks it pushes, or over a list it knows whole (17 and later), asks
of the kernel, or plain reads
src/lion_readahead_ring.[ch] the read stream's bookkeeping: blocks pushed, given, held; frontend-safe
and unit-tested standalone (test/unit/readahead_test.c)
test/sql, test/isolation, test/unit, test/recovery, test/modules