Skip to content

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

Description

@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):

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:125execute_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:225validate_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdiscuss

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions