# Schema Registration Dynamic relationship labels are subject to the relationship type limits in [Limitations And Fit](./limitations-and-fit). The extension builds its graph from explicit registration metadata stored in extension catalog tables. Register node tables, edge relationships, and optional filter columns before calling `graph.build()`. Registration applies to the current graph selection. If you do not call `graph.set_current_graph()`, registration uses the compatibility default graph. For one-off named graph updates, use the explicit `*_to_graph` and `*_from_graph` functions shown below. ## Registration Flow ## Register Node Tables ```sql SELECT graph.add_table( table_name := 'public.users'::regclass, id_column := 'id', columns := ARRAY['name', 'email', 'status'], tenant_column := NULL ); ``` The `id_column` must match a primary key or a unique `NOT NULL` index. The code validates this in `catalog/validate.rs` before writing the registration catalog. For composite identifiers, use the overload with `id_columns`: ```sql SELECT graph.add_table( table_name := 'public.memberships'::regclass, id_columns := ARRAY['account_id', 'user_id'], columns := ARRAY['role', 'state'] ); ``` Composite IDs are encoded as JSON array text during build. For example, `account_id = 10, user_id = 55` becomes a primary-key string similar to `["10","55"]`. ### Arguments | Argument | Type | Required | Meaning | |---|---|---:|---| | `table_name` | `REGCLASS` | yes | Source table to index as graph nodes | | `id_column` | `TEXT` | yes for scalar overload | Primary key or unique not-null column | | `id_columns` | `TEXT[]` | yes for composite overload | Ordered primary key or unique not-null columns | | `columns` | `TEXT[]` | no | Source columns available to search and GQL; dotted JSONB property paths are available to GQL | | `tenant_column` | `TEXT` | no | Per-row tenant value used for tenant-scoped traversal and search | Search only considers columns registered in `columns`. Traversal filter pushdown uses `graph.add_filter_column()` separately. For `graph.gql()`, `columns` may also include dotted paths rooted at a JSONB source column, such as `profile.plan` or `profile.tags`. The base column must exist and have type `jsonb`; typed filter indexes still use `graph.add_filter_column()` on ordinary columns. To register the same source table in a named graph without changing session selection: ```sql SELECT graph.add_table_to_graph( graph_name := 'customer_360', table_name := 'public.users'::regclass, id_column := 'id', columns := ARRAY['name', 'email'], graph_namespace := 'analytics' ); ``` ## Register FK-Style Edges FK-style registration reads source rows from `from_table`, takes the row primary key as the source node, and uses `from_column` as the target primary-key value. ```sql SELECT graph.add_edge( from_table := 'public.orders'::regclass, from_column := 'customer_id', to_table := 'public.customers'::regclass, to_column := 'id', label := 'placed_by', bidirectional := true ); ``` This models: ```text orders.id --placed_by--> customers.id source PK target PK from orders.customer_id ``` Named graphs use the same relationship shapes: ```sql SELECT graph.add_edge_to_graph( graph_name := 'customer_360', from_table := 'public.orders'::regclass, from_column := 'customer_id', to_table := 'public.customers'::regclass, to_column := 'id', label := 'placed_by', graph_namespace := 'analytics' ); ``` When `bidirectional := true`, pgGraph stores an additional traversable reverse adjacency row. GQL relationship values still report the registered edge record in the schema direction shown above: `_start` is the registered source endpoint and `_end` is the registered target endpoint, even when a pattern traverses the generated reverse adjacency. ## Register Edge-Table Relationships Edge-table style registration is used when one table contains relationship rows. The source and target values both come from the edge table. ```sql SELECT graph.add_edge( from_table := 'public.relationships'::regclass, from_column := 'node_id_start', to_table := 'public.entities'::regclass, to_column := 'node_id_end', label := 'related_to', bidirectional := false ); ``` The builder first tries table-specific endpoint resolution. If needed, it falls back to resolving the endpoint primary-key value across registered tables, ordered by table OID. Prefer unambiguous primary keys across graph tables. Junction tables use this same edge-table form. For a table such as `public.follows(follower, followee)`, register `from_column := 'follower'` and `to_column := 'followee'`. `to_table` is the preferred target node table when that table is registered; otherwise endpoint resolution falls back across the registered node tables. `to_column` still names the target endpoint column on the junction table, not the node table primary-key column. ## Dynamic Edge Labels When `label_column` is set, each edge row may provide its own label. Empty or NULL values fall back to the static `label`. ```sql SELECT graph.add_edge( from_table := 'public.weighted_edges'::regclass, from_column := 'src', to_table := 'public.nodes'::regclass, to_column := 'dst', label := 'route', bidirectional := false, label_column := 'rel_type' ); ``` Edge labels are assigned compact logical IDs within the documented count, per-label, and cumulative dictionary limits. New v7 bases select a one-, two-, or four-byte physical width. Exceeding a policy fails cleanly with SQLSTATE `54000` and diagnostic `PG004`. ## Weighted Edges Register `weight_column` to enable `graph.weighted_shortest_path()`: ```sql SELECT graph.add_edge( from_table := 'public.routes'::regclass, from_column := 'src', to_table := 'public.places'::regclass, to_column := 'dst', label := 'route', bidirectional := false, weight_column := 'cost' ); ``` The weight column must be numeric. Build clamps loaded weights into `u32`; NULL or missing weights default to `1` when a weighted store is built. ## Register Filter Columns Filter columns are separate from search columns. They are encoded into `FilterIndex` so traversal can evaluate predicates without joining back to SQL for every candidate neighbor. ```sql SELECT graph.add_filter_column( table_name := 'public.users'::regclass, column_name := 'age', column_type := 'numeric' ); SELECT graph.add_filter_column( table_name := 'public.users'::regclass, column_name := 'state', column_type := 'text' ); ``` Supported `column_type` values: | Type | Encoding | |---|---| | `numeric` | Signed integer domain | | `boolean` | Boolean domain with sparse bitmap support | | `text` | Interned text dictionary token | | `date` | Days from `DATE '2000-01-01'` | | `timestamptz` | Microseconds from Unix epoch | | `uuid` | Canonical 128-bit UUID integer | For a named graph: ```sql SELECT graph.add_filter_column_to_graph( graph_name := 'customer_360', table_name := 'public.users'::regclass, column_name := 'status', column_type := 'text', graph_namespace := 'analytics' ); ``` ## Inspect Registration ```sql SELECT * FROM graph.registered_tables(); SELECT * FROM graph.registered_edges(); ``` These functions inspect the selected graph, or the default graph when no graph has been selected. To inspect a named graph without changing session selection: ```sql SELECT * FROM graph.registered_tables_for_graph( 'customer_360', graph_namespace := 'analytics' ); SELECT * FROM graph.registered_edges_for_graph( 'customer_360', graph_namespace := 'analytics' ); ``` `graph.list_edges()` is the graph-scoped relationship inspection API for management workflows: ```sql SELECT * FROM graph.list_edges( 'customer_360', graph_namespace := 'analytics' ); ``` For a metadata-only export of the graph definition and maintenance state, use `graph.graph_map()`: ```sql SELECT graph.graph_map( graph_name := 'customer_360', graph_namespace := 'analytics' ); ``` The graph map reports registered node tables, relationship mappings, filter columns, build/sync/projection status, and warnings. It does not export source rows or graph data. ## Edit Relationships Relationship definitions can be renamed or updated without removing and re-adding the mapping: ```sql SELECT graph.rename_edge( graph_name := 'customer_360', old_label := 'placed_by', new_label := 'created_by', graph_namespace := 'analytics' ); SELECT graph.alter_edge( graph_name := 'customer_360', label := 'created_by', bidirectional := false, weight_column := 'confidence', graph_namespace := 'analytics' ); ``` Relationship edits update registration metadata. Rebuild the graph afterward so queries use the new mapping. ## Remove Registration ```sql SELECT graph.remove_edge('placed_by'); SELECT graph.remove_table('public.orders'::regclass); ``` Removing a table also removes associated filter columns and edges where that table is either endpoint. Rebuild after removal: ```sql SELECT * FROM graph.build(); ``` Named graph removals affect only that graph: ```sql SELECT graph.remove_edge_from_graph( 'customer_360', 'placed_by', graph_namespace := 'analytics' ); SELECT graph.remove_table_from_graph( 'customer_360', 'public.orders'::regclass, graph_namespace := 'analytics' ); ``` ## Auto-Discovery `graph.auto_discover(schema_name)` scans tables and foreign keys in one schema, registers discovered tables and FK edges in the selected/default graph, then calls `graph.build()` unless `build := false`. ```sql SELECT * FROM graph.auto_discover('public'); ``` Auto-discovery treats a composite-primary-key table with exactly two single-column foreign-key constraints, where both key columns are foreign-key sources and both constraints reference single-column primary keys, as a relationship table. That two-endpoint junction produces one edge mapping, not one edge per foreign key. Composite foreign keys and references to alternate unique keys are not split or guessed into endpoints. Tables with three or more endpoints retain ordinary table and foreign-key discovery; preview the result and register them manually when they represent hyperedges rather than entities. It also recognizes the common open-vocabulary relationship shape: a table with one surrogate primary key, exactly two outgoing foreign keys, and a text column named `relationship_name`, `relationship_type`, `edge_type`, or `rel_type`. That table is registered as one edge mapping and the conventional text column becomes its dynamic `label_column`. Endpoint names `source_id`, `from_id`, `start_id`, and `subject_id` take source precedence; `target_id`, `to_id`, `end_id`, and `object_id` take target precedence. Otherwise PostgreSQL catalog foreign-key order is used. The conventional label may be `text` or any width of `varchar`; its type is discovered independently of the narrower property-column heuristic. This convention is deliberately narrow. Use `graph.preview_discover_tables()` to inspect inferred mappings, and register manually when a two-FK table is a node entity, repeats one endpoint column, references an alternate unique key, or needs different direction, weight, tenant, or label semantics. `graph.auto_discover_tables()` limits discovery to selected tables and FK edges between only those tables: ```sql SELECT * FROM graph.auto_discover_tables( ARRAY[ 'public.users'::regclass, 'public.orders'::regclass ], tenant_column := 'account_id' ); ``` For a named graph, pass `graph_name` and optionally `graph_namespace`: ```sql SELECT * FROM graph.auto_discover_tables( ARRAY[ 'public.users'::regclass, 'public.orders'::regclass ], graph_name := 'customer_360', graph_namespace := 'analytics', build := false ); ``` Preview functions return the same discovery rows without writing registration catalog state: ```sql SELECT * FROM graph.preview_discover_tables( ARRAY[ 'public.users'::regclass, 'public.orders'::regclass ], graph_name := 'customer_360', graph_namespace := 'analytics' ); ``` Use auto-discovery for quick starts and formal FK schemas. Use manual registration when relationship tables, dynamic labels, weights, or custom tenant behavior matter. Auto-discovery registers bounded string-like columns as searchable properties. This keeps quick-start search predictable for common names, codes, labels, and small text attributes while avoiding unexpectedly broad graph metadata from large text, numeric, JSON, or timestamp columns. Use manual registration when the searchable surface needs to include non-text properties or a narrower set of text columns: ```sql SELECT graph.add_table( table_name := 'public.users'::regclass, key := ARRAY['id'], columns := ARRAY['name', 'email', 'status'] ); ``` Subgraph support is catalog-subset based: create a named graph and register the selected tables and relationships into that graph. Arbitrary row-predicate subgraphs are rejected with SQLSTATE `0A000` and diagnostic `PG018`; use PostgreSQL views or source tables when row filtering needs to be part of the source-of-truth model. Traversal filters are registered separately from searchable properties: ```sql SELECT graph.add_filter_column( table_name := 'public.users'::regclass, column_name := 'created_at', column_type := 'timestamptz' ); ```