# 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`, `graph.rls_mode`, 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. Any representation change follows
the published 1.x migration and deprecation policy.

The release contract records the complete GUC name set and a normalized hash of
the Rust registration source. PostgreSQL integration tests verify documented
defaults, ranges, contexts, and invalid-value behavior against `pg_settings`.

## 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.rls_mode` | string | `enforce` | `SUSET` | `enforce`, `legacy_bypass` | Caller-scoped topology RLS; `legacy_bypass` is a superuser-only temporary compatibility mode |
| `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.allow_rls_tables` | bool | `false` | `USERSET` | `on`, `off` | Deprecated no-op retained for configuration compatibility |
| `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.query_memory_mb` | int | `64` | `USERSET` | `1` to `32768` | Per-statement graph query workspace cap |
| `graph.maintenance_memory_mb` | int | `256` | `SUSET` | `1` to `32768` | Per-operation sync, compaction, and analytics workspace cap |
| `graph.spill_disk_limit_mb` | int | `4096` | `SUSET` | `1` to `1048576` | Per-operation staging and spill-disk cap |
| `graph.query_work_limit` | int | `10000000` | `USERSET` | `1` to `2000000000` | Expansion and blocking-operator work cap |
| `graph.operation_timeout_ms` | int | `0` | `USERSET` | `0` to `86400000` | Graph-operation elapsed breaker; `0` uses PostgreSQL cancellation only |
| `graph.build_batch_size` | int | `10000` | `SUSET` | `1` to `1000000` | Maximum SPI cursor/spool rows; byte pressure can flush a batch earlier |
| `graph.low_memory_build` | bool | `false` | `SUSET` | `on`, `off` | Allow rebuilds to unload the current backend-local graph before constructing the replacement |
| `graph.oom_action` | string | `error` | `SUSET` | `error`; deprecated aliases `readonly`, `read_only`, `read-only` | Compatibility setting; every over-budget build is rejected before allocation |
| `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 in checked integer bytes from registered
tables and edges before constructing the active engine. Missing or zero table
statistics trigger an exact row-count preflight. Every over-budget build raises
SQLSTATE `53200` with diagnostic `PG001` before engine allocation.

The `readonly`, `read_only`, and `read-only` values remain readable for 1.x
configuration compatibility but are deprecated and have the same safe
over-budget rejection behavior as `error`.

The runtime build budget subtracts current backend-local serving residency and
a safety reserve of one sixteenth of the configured limit, capped at 64 MiB.
Node, filter, resolution, relationship, CSR, and persistence phases reserve
against the remainder. Adaptive spool batches use one sixteenth of the
remainder, with a 64 KiB minimum and 8 MiB maximum when that much memory is
available.

Set `graph.low_memory_build = on` when rebuilds fail only because the current
backend already has a graph loaded. In that mode, `graph.build()` may unload the
current backend-local graph before constructing the replacement. This lowers
peak memory for rebuilds at the cost of losing the old loaded graph in the
building backend if the build fails. Other PostgreSQL backends are unaffected.
It does not make a final graph that exceeds `graph.memory_limit_mb` fit; reduce
the graph size or raise the limit for that case.

`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 includes each backend's immutable artifact snapshot in private
memory, then projects an instance total for the supplied backend count. The
shared-memory columns remain present for 1.x compatibility and currently
return zero.

Load, query, sync, compaction, and analytics operations also reserve their
workspaces before growing large graph-owned structures. Their effective memory
allowance is the smaller of the operation-specific setting and the remaining
`graph.memory_limit_mb` after the selected graph's backend-private residency.
Queries count candidate scans, neighbor expansions, path work, hydration, and
blocking projection work against `graph.query_work_limit`. Maintenance paths
also enforce row and disk budgets derived from their public operation inputs.

When a breaker is reached, pgGraph cancels the operation with SQLSTATE `54000`
and diagnostic `PG007`. PostgreSQL statement cancellation and
`statement_timeout` remain authoritative; `graph.operation_timeout_ms` adds an
optional graph-local elapsed limit. The failed operation does not publish a
partially validated graph or projection generation.

```sql
SET graph.query_memory_mb = 128;
SET graph.query_work_limit = 20000000;
SET graph.operation_timeout_ms = 5000;

SELECT * FROM graph.resource_status();
```

`graph.resource_status()` reports the most recent governed operation in the
current PostgreSQL backend. It is diagnostic telemetry, not cluster-wide
accounting.

`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 SQLSTATE
`54000` and diagnostic `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 root and compatibility path:

```text
$PGDATA/graph/00000000-0000-0000-0000-000000000001/
  main.pggraph                                      logical compatibility path
  projection-current.json                          current-generation pointer
  projection-generation-<generation>.json          immutable manifest
  projection-generation-<generation>-base.pggraph  immutable base artifact
```

When a valid current pointer and manifest exist, the manifest's base artifact
takes precedence over `main.pggraph`. A store without projection metadata can
still load `main.pggraph` as a compatibility fallback. Operators should use
`graph.graph_runtime_status()` and `graph.projection_status()` for presence and
health checks instead of inferring the serving file from directory names.

## 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.query_memory_mb = 64
graph.maintenance_memory_mb = 256
graph.spill_disk_limit_mb = 4096
graph.query_work_limit = 10000000
graph.operation_timeout_ms = 0
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>
