Skip to content

Text graphs end to end (M10 step 4) - #60

Merged
wfaithfull merged 16 commits into
mainfrom
m10-text-graphs
Oct 23, 2026
Merged

wfaithfull merged 16 commits into
mainfrom
m10-text-graphs

Conversation

@wfaithfull

Copy link
Copy Markdown
Contributor

M10 step 4 (DESIGN.md §5.7, §14 item 41): text node ids work everywhere an external id is mapped, built on the TextKeys id map that merged in #58. Design decisions and deviations are recorded in §5.7, "Built (step 4)".

What changes

  • Id types. pg_warren is generic over the id map's key storage: FixedKeys<i32>, FixedKeys<i64>, FixedKeys<[u8; 16]> and TextKeys. The old IdDatum trait is split by concern into four:
    • source::IdDatum
    • source::InPlaceId
    • segment::StoredIds
    • cache::CachedIds
  • Storage. A text version's id map is stored as the id_bytes, id_offsets and id_order segments. In a shared-cache region these are arrays 7–9; the region format is still 1.
  • Build intake. For every id type, edge rows are copied into batches (EdgeBatch, 65,536 rows or 1/32 of the build budget). Each batch's endpoints are mapped on the compute pool, through a TextIndex charged to the build budget. A property test checks that this gives exactly the row-by-row edge list.
    • The compute pool moved from compute_sql to interrupts::pool, so build can use it. CLAUDE.md's dependency list gains interrupts → build.
  • Bulk lookups in one call. The first 65,536 lookups use binary search. After that, a text graph builds a lookup index charged to warren.max_compute_memory (80 MB at 10M nodes). This applies to:
    • vector_from_query
    • gather's messages
    • similarity's pairs
  • Encoding. An id is checked against the database encoding when its text value is built: 22021 if it is invalid, 54000 if it is longer than a text can hold. A load checks only the id_bytes header's encoding (55000), so a version restored into a database with another encoding is refused.

Tests

  • test/sql/text_ids.sql: runs every function that maps ids on a text graph and on the same graph with bigint ids, and compares the results.
  • a4_typed_retrieval_iris.sql: the Appendix A.4 use case written with IRIs, checked against recursive CTEs.
  • shared_cache_text.sql: shares a text version across backends.
  • Dump and upgrade: dump.py and upgrade.py now cover a text graph.
  • Soak: soak.py --ids text is new.

Run locally:

  • run.py: 55/55 plain, 55/55 --trusted, 55/55 --shared-cache preload.
  • pgrx: 721 on PG 16, 721 on PG 18, 719 on PG 15.
  • dump.py and upgrade.py pass.
  • Miri, with CI's flags: 415 passed. The new pool tests are kept out of Miri because of crossbeam-epoch's known report, as the crate already does for its other pool tests.
  • fmt, clippy and rustdoc are clean.

Soak: 5M nodes, 50M edges (half scale for disk, §12)

text bigint
version size (id map) 719.5 MB (238.9 MB) 560.6 MB (80 MB)
refresh 56.4 s 41.8 s
cold load (private) 1.03 s 0.77 s
bfs depth 6 0.95 s 0.49 s
similarity, 100k pairs 1.31 s 0.41 s
pagerank 9.1 s 6.7 s
  • The build takes about 35% longer with text ids.
  • Calls that return one row per id take about twice as long, because each text value is built and its encoding checked.
  • Kernels that return no ids are unchanged. The pagerank gap is probably noise: the machine had a load average of 2–4.
  • The full 10M text soak needs about 16 GB of free disk. It is still to do.

Generated by Claude Code

claude added 16 commits October 23, 2026 17:23
…the pool

pg_warren's generic parameter is now the id map's key storage
(FixedKeys<E> or TextKeys): IdDatum converts datums and keys, StoredIds
writes and reads an id map's segments (a text id map's id_bytes,
id_offsets and id_order), and CachedIds places and views them in a
region. The build maps each batch of edge rows on the compute pool
(warren-core's EdgeBatch), its text index charged to the build budget;
per-call bulk lookups index a text id map past 65,536 ids, charged to the
call's compute memory. The compute pool moves to interrupts::pool so the
build can use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
…er a text graph

test/sql/text_ids.sql runs every id-mapping function on a text graph and
on the same graph with bigint ids, whose ids sort alike, and compares the
results; pg_tests cover create_graph's text checks, a text build's
segments and stable ids, and the datum's encoding check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
…shared cache

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
…pts::pool

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
The map runs on the rayon pool, whose work stealing runs through
crossbeam-epoch, which Miri's Stacked Borrows model rejects (as for the
compute pool's other tests); under Miri the batch tests take the
unmapped path only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
Case a with IRIs and with bigint ids at 5M nodes and 50M edges, private
and shared: the full scale needs about 16 GB of free disk, which the
machine did not have.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
The collector compared the batch's capacity, which the rows' fixed
columns already filled, with its whole share of the budget, so a text
build below about 124 MiB, or an int8 build at a budget its rows filled
exactly, mapped every row once one text id had grown the batch. Half the
share is now the rows', the rest their text ids', compared by the bytes
the ids hold; an emptied batch frees text room past that share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
Past 65,536 lookups a call was refused with 53400 when its compute
memory had no room for the hash index, though the binary search it used
until then needs none; it now keeps the search for the rest of the call.
An index that fails to build is raised with its GraphError's SQLSTATE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
…ed ids within a batch

id_in_place and text_in_place were safe functions whose results could
outlive the rows' memory context, which holds what detoasting them
allocated. for_each_tuple now lends each row as a source::Row whose
in-place reads borrow it for one call, and resets the context within a
batch once it holds 64 MB, so long toasted ids no longer pile up
uncharged until the batch's 10,000 rows end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
Errors that name an id (a node with two rows in vector_from_query, a
negative weight in a community detection) embedded the whole id, and
naming a text id built a datum, which checks its encoding and length:
an invalid id would have hidden the error it was named in. IdDatum::named
clips a text id on a character boundary and checks nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
A bigint graph's char(5) endpoint was refused with a hint to cast it to
text; it is now refused as of the wrong type, as any other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
…oss encodings

shared_cache_parallel_text.sql runs a text graph's PARALLEL SAFE reads
in a worker and compares them with the leader's; text_ids_sql_ascii.sql
maps ids that are not UTF-8 in a SQL_ASCII database; dump.py restores a
text graph from a UTF8 database into a LATIN1 one and checks that
reading it is refused with 55000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
@wfaithfull
wfaithfull merged commit 7934941 into main Oct 23, 2026
17 checks passed
wfaithfull pushed a commit that referenced this pull request Oct 23, 2026
Conflicts: the loader's fill takes main's per-id-type sinks and still
reports the bytes copied; the build's imports and intake keep main's
batched mapping, with the row counts and CSR reporting around it;
interrupts keeps main's pool module and the Progress adapter; imports of
CachedIds beside activity's; DESIGN.md's "built so far" lists step 4 and
step 9. sql/pg_warren--0.6.0.sql regenerated with sql/schema-drift.sh
--update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rxfe2L3iVjSwSBPdUu1Ke
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants