graph.build() fails with "column 'id' does not exist" for edges from a composite-PK (junction) table

#11 · closed · 3 comments

View on GitHub ↗

svngoku

## 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): ```sql 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): ```sql 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.

Comments

evokoa-admin

Hey thanks for reporting! Will investigate this now. I think it might be with the way we are handling composite keys.

evokoa-admin

Thanks again for the super detailed reproduction steps! I dug into this, and the good news is that composite primary keys actually work fine. The crash is just coming from a minor mix-up in how the edge is being registered. When you use a junction table (like follows), the system expects both column names you provide to be columns that exist on that junction table itself. So, in your first example, instead of pointing to the id column on the users table, you just need to point to the followee column on the follows table. Here is how it should look: ```sql SELECT graph.add_edge('public.follows'::regclass, 'follower', 'public.users'::regclass, 'followee', -- <-- Changed 'id' to 'followee' label := 'follows', bidirectional := true); ``` Basically, public.users tells it where to look, and followee tells it which column in your junction table holds that target user's info. That being said, this is absolutely still a bug on our end. Our system shouldn't have let you use id in the first place. Because id does exist on the users table, our validation mistakenly let it slide. But when it went to actually build the graph, it got confused, tried to look for an id column on the follows table, and threw that completely misleading column "id" does not exist error. We need to catch this earlier and give a much clearer validation error, so I'm going to add tests, update docs, and better validation to prevent this. Can you try changing that one line from id to followee and let me know if it unblocks your Railway demo or does the issue persist?

evokoa-admin

Hey! Did the fix above resolve the issue? Would love to know if it worked. Thanks :)