# Configuration

All extension settings are PostgreSQL GUCs with the `graph.` prefix. They are
registered in `_PG_init()` by `graph/src/config.rs`.

String-valued settings such as `graph.oom_action`, `graph.sync_mode`,
`graph.query_freshness`, and `graph.build_scan_mode` are parsed into typed Rust
enums at the extension boundary. They remain string GUCs for compatibility with
existing configuration files, aliases such as `readonly`/`read_only`, and
current warning or reserved-mode behavior. A future PostgreSQL enum-GUC change
would require an explicit migration plan for those aliases and error messages.

## Setting Values

Use session-local settings for experiments:

```sql
SET graph.default_max_depth = 4;
SET graph.max_nodes = 50000;
```

Use database or role settings for application defaults:

```sql
ALTER DATABASE mydb SET graph.default_search_mode = 'exact';
ALTER ROLE app_user SET graph.default_hydrate = false;
```

Use `ALTER SYSTEM` or `postgresql.conf` for `SUSET` operational settings:

```sql
ALTER SYSTEM SET graph.memory_limit_mb = 4096;
SELECT pg_reload_conf();
```

## Query Defaults

| GUC | Type | Default | Context | Range or values | Used by |
|---|---:|---:|---|---|---|
| `graph.enabled` | bool | `true` | `USERSET` | `on`, `off` | Query kill switch |
| `graph.default_max_depth` | int | `5` | `USERSET` | `1` to `100` | `graph.traverse()`, workflow wrappers |
| `graph.default_search_mode` | string | `contains` | `USERSET` | `contains`, `exact`, `prefix`, `token` | `graph.traverse_search()`, workflow wrappers |
| `graph.default_case_sensitive` | bool | `false` | `USERSET` | `on`, `off` | Search wrappers |
| `graph.default_hydrate` | bool | `true` | `USERSET` | `on`, `off` | `graph.traverse_search()` hydrate default |
| `graph.query_freshness` | string | `apply_pending_sync` | `USERSET` | `apply_pending_sync`, `off`, `error_on_pending` | Topology-read pending sync policy |
| `graph.max_nodes` | int | `100000` | `USERSET` | `1` to `10000000` | Traversal circuit breaker |
| `graph.max_frontier` | int | `100000` | `USERSET` | `1` to `10000000` | BFS/DFS frontier circuit breaker |
| `graph.max_exact_path_count` | int | `100000` | `USERSET` | `1` to `10000000` | `graph.path_count_estimate()`, `graph.aggregate()` |
| `graph.enforce_tenant_scope` | bool | `true` | `USERSET` | `on`, `off` | Tenanted graph queries |
| `graph.tenant_setting` | string | empty | `USERSET` | Any session GUC name | Tenant fallback |
| `graph.build_scan_mode` | string | `select` | `USERSET` | `select`; `copy` is reserved | Build scanner |
| `graph.default_projection_mode` | string | `csr_readonly` | `USERSET` | `csr_readonly`, `mutable_overlay` | Default `graph.build()` projection mode |
| `graph.mutable_enabled` | bool | `false` | `USERSET` | `on`, `off` | Allows `mutable_overlay` builds |

<Callout type="warning">
`graph.build_scan_mode = 'copy'` is present as a reserved mode, but the current
code returns an error because safe server-side COPY hooks are not available
through the pgrx path used here. Use `select`.
</Callout>

`graph.default_projection_mode = 'csr_readonly'` preserves the immutable
read-mostly engine. `mutable_overlay` is an opt-in build mode for
transaction-local GQL writes such as single-node mapped `CREATE`,
registered-edge-row relationship `CREATE`, mapped property `SET`/`REMOVE`,
mapped edge-row `DELETE`, and mapped node `DETACH DELETE`/`MERGE`; selecting it
requires `graph.mutable_enabled = on`.

## Memory And Build Settings

| GUC | Type | Default | Context | Range or values | Effect |
|---|---:|---:|---|---|---|
| `graph.memory_limit_mb` | int | `2048` | `SUSET` | `64` to `32768` | Pre-build OOM guard and status limit |
| `graph.build_batch_size` | int | `10000` | `SUSET` | `1` to `1000000` | SPI cursor fetch size and spool batch size |
| `graph.oom_action` | string | `error` | `SUSET` | `error`, `readonly`, `read_only`, `read-only` | Behavior when estimated build memory exceeds the limit |
| `graph.max_loaded_graphs_per_backend` | int | `1` | `SUSET` | `0` to `1` | Runtime loaded-graph slot cap for each PostgreSQL backend |
| `graph.max_tx_delta_nodes` | int | `100000` | `USERSET` | `0` to `10000000` | Maximum transaction-local node deltas before a mapped GQL write aborts |
| `graph.max_tx_delta_edges` | int | `100000` | `USERSET` | `0` to `10000000` | Maximum transaction-local edge deltas before a mapped GQL write aborts |
| `graph.max_overlay_memory_mb` | int | `256` | `USERSET` | `1` to `32768` | Maximum estimated transaction-overlay heap before a mapped GQL write aborts |
| `graph.compaction_threshold` | int | `50000` | `USERSET` | `1` to `10000000` | Delta or tombstone count at which status surfaces recommend compaction |
| `graph.projection_retention_generations` | int | `2` | `USERSET` | `1` to `1000` | Minimum valid projection manifest generations retained by GC |

`graph.build()` estimates memory from registered tables and edges before
constructing the active engine. With `graph.oom_action = 'error'`, it raises `PG001`.
With `graph.oom_action = 'readonly'`, it logs a warning and marks the built
engine read-only.

`graph.memory_limit_mb` is enforced per PostgreSQL backend. To size a database
where multiple sessions may load or build the graph, build or load the graph in
one representative backend and run:

```sql
SELECT *
FROM graph.memory_profile(concurrent_backends := 8);
```

The result separates backend-private heap from mmap-backed bytes that are
physically shareable through the OS page cache, then projects an instance total
for the supplied backend count.

`graph.max_loaded_graphs_per_backend` is also enforced per backend. The current
runtime has one backend-local graph engine slot, so the supported range is
`0` to `1`. Set it to `0` only when you want to block runtime graph loads on a
server while still allowing metadata and build operations.

Mapped GQL writes use transaction-local overlays. If a node, edge, or overlay
memory limit would be exceeded, the current SQL statement aborts with `PG019`
and PostgreSQL rolls back the source-table DML for that statement.

```sql
ALTER SYSTEM SET graph.memory_limit_mb = 8192;
ALTER SYSTEM SET graph.oom_action = 'error';
SELECT pg_reload_conf();
```

## Persistence Settings

| GUC | Type | Default | Context | Effect |
|---|---:|---:|---|---|
| `graph.persist_on_build` | bool | `true` | `USERSET` | Writes the `.pggraph` artifact after successful build/vacuum/maintenance paths that request persistence |
| `graph.auto_load` | bool | `true` | `SUSET` | Loads an existing artifact on first query in an empty backend |
| `graph.hot_eager_load` | bool | `false` | `SUSET` | Attempts to load `hot` graphs immediately when `graph.select_graph()` selects them |
| `graph.idle_unload_secs` | int | `0` | `SUSET` | Reserved idle-unload threshold; `0` disables idle unload |
| `graph.data_dir` | string | `graph` | `SUSET` | Subdirectory under `$PGDATA` that contains one artifact root per graph id |

`graph.auto_load` applies to `hot` and `warm` graphs. `cold` graphs keep their
artifacts available for explicit `graph.load_graph()` calls but do not
auto-load during query execution. `graph.hot_eager_load` is an opt-in selection
policy for `hot` graphs that already have persisted artifacts.

Default graph artifact path:

```text
$PGDATA/graph/00000000-0000-0000-0000-000000000001/main.pggraph
$PGDATA/graph/00000000-0000-0000-0000-000000000001/main.pggraph.sync
```

## Sync Settings

| GUC | Type | Default | Context | Range or values | Effect |
|---|---:|---:|---|---|---|
| `graph.sync_mode` | string | `trigger` | `SUSET` | `manual`, `trigger`, `wal` | Sync strategy selector |
| `graph.edge_buffer_size` | int | `100000` | `SUSET` | `1000` to `10000000` | Maximum pending edge mutations before read-only mode |
| `graph.sync_batch_size` | int | `1000` | `SUSET` | `1` to `100000` | Maximum sync-log rows replayed in one internal batch |
| `graph.vacuum_interval_secs` | int | `60` | `SUSET` | `5` to `86400` | Reserved maintenance interval setting; not scheduled by current code |

<Callout type="warning">
`graph.sync_mode = 'wal'` is parsed but reserved. Current code rejects WAL mode
for active sync operations and asks callers to use `manual` or `trigger`.
</Callout>

## Recommended Starting Configuration

```conf
shared_preload_libraries = 'graph'

graph.memory_limit_mb = 4096
graph.persist_on_build = on
graph.auto_load = on
graph.build_batch_size = 10000
graph.sync_batch_size = 1000
graph.max_nodes = 100000
graph.max_frontier = 100000
graph.default_max_depth = 5
graph.default_search_mode = 'contains'
graph.default_hydrate = on
graph.default_projection_mode = 'csr_readonly'
graph.mutable_enabled = off
graph.projection_retention_generations = 2
graph.query_freshness = 'apply_pending_sync'
graph.sync_mode = 'trigger'
```

Use lower traversal circuit breakers on multi-tenant OLTP primaries, and raise
them only for trusted administrative or analytical roles.

<Callout type="info">
`graph.default_hydrate` currently affects `graph.traverse_search()` when its
`hydrate` argument is omitted. Primitive `graph.search()` and `graph.traverse()`
default their own `hydrate` arguments to `true`.
</Callout>
