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.