# Schema Registration 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 IDs. A graph can use up to 254 distinct user-facing labels. The untyped slot and internal sentinel reserve the remaining `u8` values, so registering or building a graph that would require another label fails cleanly with `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'); ``` `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 `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' ); ```