Bug: graph.build() fails with column "id" does not exist when an edge's from_table is a junction table (composite PK)
Version
- pgGraph:
0.1.5
- Postgres:
17.10 (Debian, also reproduced on 17-bookworm Docker image)
- OS: any
Summary
graph.build() fails with ERROR: column "id" does not exist whenever a registered edge has a from_table whose primary key is composite (i.e. the table is a many-to-many junction). The README and the graph.auto_discover output both treat composite-PK junction tables as "first-class" (notice: "table 'X' has composite PK (...) where all columns are foreign keys — treated as a junction table (edges only, not a node)"), and the add_table overload with id_columns is documented and exposed. So the surface looks supported, but the build path does not handle this case.
A secondary issue: when the edge is registered in the other direction (single-PK table → composite-PK table), the build completes but graph.traverse from the single-PK root only returns the root node — the edges are stored in the CSR but not walked, suggesting the resolution index is not built correctly for edges into composite-ID nodes.
To reproduce
Smallest possible reproducer (single edge, two columns on the junction, single-column PK on the target):
CREATE EXTENSION graph;
CREATE TABLE users (id text PRIMARY KEY, name text);
CREATE TABLE follows (follower text REFERENCES users(id),
followee text REFERENCES users(id),
PRIMARY KEY (follower, followee));
INSERT INTO users VALUES ('u1','Alice'),('u2','Bob');
INSERT INTO follows VALUES ('u1','u2');
-- 1) Junction-as-source (fails to build)
SELECT graph.add_table('public.users' ::regclass, 'id'::text, ARRAY['name']);
SELECT graph.add_edge ('public.follows' ::regclass, 'follower',
'public.users' ::regclass, 'id',
label := 'follows', bidirectional := true);
SELECT * FROM graph.build();
-- ERROR: column "id" does not exist
Inverse direction (succeeds at build, but traversal is broken):
TRUNCATE graph._registered_tables, graph._registered_edges, graph._registered_filter_columns RESTART IDENTITY CASCADE;
SELECT graph.add_table('public.users' ::regclass, 'id'::text, ARRAY['name']);
SELECT graph.add_table('public.follows' ::regclass, ARRAY['follower','followee'], ARRAY[]::text[]);
SELECT graph.add_edge ('public.users' ::regclass, 'id',
'public.follows' ::regclass, 'follower',
label := 'follows', bidirectional := true);
SELECT * FROM graph.build();
-- nodes_loaded = 12, edges_loaded = 6 -- no error
SELECT depth, node->>'name'
FROM graph.traverse('public.users'::regclass, 'u1', 3);
-- depth | name
-- 0 | Alice
-- (1 row) <-- expected u1→follows(u1,u2)→u2, plus more; actually nothing
Expected behavior
graph.build() should accept edges whose from-table is a junction table (composite PK) — that is the natural representation of a many-to-many edge, and graph.auto_discover already produces these registrations.
graph.traverse from a single-PK root through a composite-PK intermediate should walk the chain and return all reachable nodes.
Actual behavior
- Build aborts with
ERROR: column "id" does not exist (no table OID in the message, making it look like a misleading Postgres system error rather than a friendly validation error from the extension).
- When the edge direction is reversed, build succeeds but traversal returns only the root node.
Impact
- Severity: High — this is the canonical many-to-many case, which is one of the main reasons people reach for a graph layer on top of relational tables. Friends-of-friends, products-bought-together, follower graphs, co-authorship, etc. all hit this.
- Reproducibility: Always (with current
0.1.5).
Evidence / where I looked
graph/src/sql_build.rs:125 — execute_build_inner calls build_replacement_engine which probably probes from_column against a hard-coded id lookup on from_table rather than using the registered id_columns / id_column from the catalog.
graph/src/catalog/validate.rs:225 — validate_column_exists raises "column 'id' does not exist on table OID {}" with no SQLSTATE-friendly context; this is the error string users see.
- The README's "Quickstart" examples only use 1-to-many FK relations, so this case is not exercised in the documented quickstart path.
Environment
- Deployed via:
ghcr.io/evokoa/pggraph:0.1.5 (Docker image, prebuilt)
- Connection: standard TCP, plain
psql 14 client
- No custom GUCs set; default
csr_readonly projection mode, default trigger sync mode
Additional context
I encountered this while building a social-network demo (Alice → follows → Bob/Carol) on a Railway deploy of the 0.1.5 image. 1-to-many demos (org chart, accounts/transfers) work fine, which is how I narrowed the failure to the composite-PK-from-table case.
If helpful, I can provide a cargo pgrx test fixture that fails today and would pass once the build path is fixed.
Bug:
graph.build()fails withcolumn "id" does not existwhen an edge'sfrom_tableis a junction table (composite PK)Version
0.1.517.10(Debian, also reproduced on 17-bookworm Docker image)Summary
graph.build()fails withERROR: column "id" does not existwhenever a registered edge has afrom_tablewhose primary key is composite (i.e. the table is a many-to-many junction). The README and thegraph.auto_discoveroutput both treat composite-PK junction tables as "first-class" (notice: "table 'X' has composite PK (...) where all columns are foreign keys — treated as a junction table (edges only, not a node)"), and theadd_tableoverload withid_columnsis documented and exposed. So the surface looks supported, but the build path does not handle this case.A secondary issue: when the edge is registered in the other direction (single-PK table → composite-PK table), the build completes but
graph.traversefrom the single-PK root only returns the root node — the edges are stored in the CSR but not walked, suggesting the resolution index is not built correctly for edges into composite-ID nodes.To reproduce
Smallest possible reproducer (single edge, two columns on the junction, single-column PK on the target):
Inverse direction (succeeds at build, but traversal is broken):
Expected behavior
graph.build()should accept edges whose from-table is a junction table (composite PK) — that is the natural representation of a many-to-many edge, andgraph.auto_discoveralready produces these registrations.graph.traversefrom a single-PK root through a composite-PK intermediate should walk the chain and return all reachable nodes.Actual behavior
ERROR: column "id" does not exist(no table OID in the message, making it look like a misleading Postgres system error rather than a friendly validation error from the extension).Impact
0.1.5).Evidence / where I looked
graph/src/sql_build.rs:125—execute_build_innercallsbuild_replacement_enginewhich probably probesfrom_columnagainst a hard-codedidlookup onfrom_tablerather than using the registeredid_columns/id_columnfrom the catalog.graph/src/catalog/validate.rs:225—validate_column_existsraises"column 'id' does not exist on table OID {}"with no SQLSTATE-friendly context; this is the error string users see.Environment
ghcr.io/evokoa/pggraph:0.1.5(Docker image, prebuilt)psql14 clientcsr_readonlyprojection mode, defaulttriggersync modeAdditional context
I encountered this while building a social-network demo (Alice → follows → Bob/Carol) on a Railway deploy of the
0.1.5image. 1-to-many demos (org chart, accounts/transfers) work fine, which is how I narrowed the failure to the composite-PK-from-table case.If helpful, I can provide a
cargo pgrxtest fixture that fails today and would pass once the build path is fixed.