# 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

<FlowDiagram
  steps={[
    { title: 'Choose source tables' },
    { title: 'Register node tables' },
    { title: 'Register edges' },
    { title: 'Register filter columns' },
    { title: 'Inspect graph.registered_*()' },
    { title: 'graph.build()', isResult: true }
  ]}
/>

## 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 |

<Callout type="warning">
Search only considers columns registered in `columns`. Traversal filter pushdown
uses `graph.add_filter_column()` separately.
</Callout>

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 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');
```

`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'
);
```

<Callout type="info">
Use auto-discovery for quick starts and formal FK schemas. Use manual
registration when relationship tables, dynamic labels, weights, or custom
tenant behavior matter.
</Callout>

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'
);
```
