# Administration And Security

Administrative functions mutate extension catalogs, build state, persisted
artifacts, sync state, or run global graph algorithms. The code checks graph
admin privileges before these operations.

## Graph Admin Check

A caller is considered a graph admin when either:

| Condition | Source |
|---|---|
| Current role is a superuser | `pg_roles.rolsuper` |
| Current role has `CREATE` privilege on schema `graph` | `has_schema_privilege(current_user, 'graph', 'CREATE')` |

Grant a non-superuser admin role:

```sql
GRANT USAGE, CREATE ON SCHEMA graph TO graph_admin;
```

## Admin-Protected Functions

Admin protection applies to catalog mutation, build/vacuum/maintenance, sync
apply, reset, and global analytics.

| Function family | Examples |
|---|---|
| Registration | `add_table`, `add_edge`, `add_filter_column`, `rename_edge`, `alter_edge`, `remove_table`, `remove_edge` |
| Build lifecycle | `build`, `vacuum`, `maintenance`, `reset` |
| Sync | `enable_sync`, `apply_sync`, `add_sync_policy`, `alter_sync_policy`, `drop_sync_policy`, `run_sync_policy`, `run_job`, `run_due_jobs`, `run_due_jobs_async`, `alter_job`, `remove_job`, `projection_compact`, `projection_gc`, `projection_repair` |
| Global analytics | `connected_components`, `component_stats`, component pagination helpers |

## Graph Privileges

Named graphs can grant graph-level privileges to PostgreSQL roles:

```sql
SELECT * FROM graph.grant_graph('customer_360', 'app_reader', 'read', namespace := 'analytics');
SELECT * FROM graph.grant_graph('customer_360', 'etl_worker', 'build', namespace := 'analytics');
SELECT * FROM graph.graph_privileges('customer_360', namespace := 'analytics');
```

| Privilege | Effect |
|---|---|
| `read` | Allows selecting/querying the graph when source-table privileges also allow access |
| `write` | Reserved for mapped graph writes; source-table write privileges remain required |
| `build` | Allows build, vacuum, and maintenance for the graph |
| `admin` | Allows grant/revoke and graph lifecycle administration |

Graph owners, graph schema admins, and graph `admin` grantees can grant and
revoke graph privileges. Graph owners and graph schema admins retain all graph
privileges.

## Graph Quotas

Graph schema admins can configure quota policies:

```sql
SELECT * FROM graph.set_graph_quota(
  'owner',
  'max_named_graphs',
  25,
  scope_key := 'app_owner',
  enforcement := 'hard'
);
SELECT * FROM graph.graph_quota_usage();
```

Quota scopes are `cluster`, `tenant`, `owner`, `namespace`, and `graph`.
Enforcement can be `hard` or `warn`. Hard `max_named_graphs` quotas check
cluster and owner limits before creating a graph metadata row. Hard
`max_loaded_graphs_per_backend` quotas check cluster and owner limits before
loading a persisted graph artifact into the current backend. Hard
`max_graph_jobs` quotas count generic jobs, build jobs, and maintenance jobs
before creating a sync policy job. Hard `max_artifact_storage_bytes` quotas
check active projection artifact bytes plus the requested ingest or compaction
write budget before publishing new derived files. Warning quotas are visible
through `graph.graph_quota_usage()` but do not block the operation.

## Reader Functions

Application roles typically need execute privileges on search/traversal/path
functions and `SELECT` privilege on the source tables they query.

```sql
GRANT USAGE ON SCHEMA graph TO app_reader;
GRANT SELECT ON public.users TO app_reader;
GRANT SELECT ON public.orders TO app_reader;
```

Then grant only the functions your application uses. PostgreSQL function grants
must match argument types; inspect installed signatures with:

```sql
SELECT p.oid::regprocedure
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'graph'
ORDER BY p.proname, p.oid::regprocedure::text;
```

## Source Table ACL Checks

Query functions check source-table `SELECT` privileges before accessing graph
data for the relevant table coordinates. Missing privilege raises `PG002`.
Mapped GQL writes also preflight the privileges they need on source tables:
`MERGE` requires `SELECT` and `INSERT`, and it requires `UPDATE` only when the
query includes `ON MATCH SET`.

Graph `read` is necessary but not sufficient for reading source data. A role
with graph `read` but no source-table `SELECT` still receives `PG002` when a
query hydrates or otherwise reads source rows. A role with source-table
`SELECT` but no graph `read` cannot select or query that graph.

## Tenant Scope

When a selected graph has a `tenant` value, pgGraph uses it as the default
tenant scope for traversal, search, GQL, and Cypher calls. An explicit tenant
argument or a session tenant from `graph.tenant_setting` must match the
selected graph tenant; mismatches return `PG005`.

Resolution order is:

1. explicit tenant argument;
2. session tenant from `graph.tenant_setting`;
3. selected graph tenant;
4. no tenant, unless `graph.enforce_tenant_scope = on` and registered tables
   have `tenant_column`.

<Callout type="info">
The graph artifact contains source primary keys and topology. ACL checks are
part of query execution so a role cannot use graph queries to bypass source
table `SELECT` permissions.
</Callout>

## Internal Catalog Tables

Internal catalog tables are implementation details. Bootstrap SQL revokes
direct `PUBLIC` read and write access to sensitive named-graph, grant, quota,
job, sync, and projection metadata tables:

```text
graph._graphs
graph._graph_grants
graph._graph_quotas
graph._jobs
graph._job_runs
graph._sync_policies
graph._sync_log
graph._projection_generations
graph._sync_buffer
```

Application code should not depend on raw internal reads. Use filtered public
functions such as `graph.list_graphs()`, `graph.graph_privileges(...)`,
`graph.graph_quotas()`, `graph.graph_quota_usage()`, `graph.jobs(...)`,
`graph.job_runs(...)`, `graph.sync_health()`, and `graph.projection_status()`.
Extension owners and superusers can still inspect internals for maintenance
when needed.

## Error Codes

The extension emits custom SQLSTATEs through PostgreSQL's error reporting API.

| SQLSTATE | Error | Common fix |
|---|---|---|
| `PG001` | Memory limit exceeded | Raise `graph.memory_limit_mb`, reduce registered graph size, or change OOM policy |
| `PG002` | ACL denied | Grant `SELECT` on the relevant source table or graph admin privilege for admin functions |
| `PG003` | Graph not built | Run `SELECT * FROM graph.build();` |
| `PG004` | Edge type limit exceeded | Reduce distinct edge labels; max user labels are 254 |
| `PG005` | Invalid filter | Register the filter column and use supported operators/types |
| `PG006` | Build locked | Wait for current build/vacuum/maintenance |
| `PG008` | Edge buffer full | Run `graph.vacuum()`/`graph.maintenance()` or increase `graph.edge_buffer_size` |
| `PG009` | Corrupt graph file | Rebuild persisted artifact |
| `PG010` | Node not found | Check table/primary key and rebuild/apply sync if data changed |
| `PG011` | Incompatible graph file version | Rebuild artifact with current extension version |
| `PG012` | Read-only graph state | Inspect `graph.status().read_only_reason`, then rebuild, vacuum, or adjust memory policy |
| `PG013` | GQL syntax error | Check the query text against the documented GQL subset |
| `PG014` | Unsupported GQL feature | Rewrite the query using supported GQL compatibility-matrix features |
| `PG015` | GQL semantic error | Verify labels, relationship types, aliases, and return bindings |
| `PG016` | GQL parameter error | Pass a JSON object containing every `$parameter` referenced by the query |
| `PG017` | GQL execution error | Reduce result cardinality, fix invalid runtime values, or rebuild/apply sync if a graph coordinate can no longer hydrate its source row |
| `PG018` | Unsupported graph operation | Use a supported query shape or merge pending overlays before retrying |
| `PG019` | Overlay limit exceeded | Commit or roll back the transaction, or raise the relevant overlay GUC |
| `55000` | Extension disabled | `SET graph.enabled = on` |
| `XX000` | Internal error | Report with full message and reproduction |

## Disable Query Functions

```sql
SET graph.enabled = off;
```

When disabled, query functions fail with SQLSTATE `55000`. Administrative
functions such as `status`, `build`, and `reset` are not intended as regular
query paths and are not all gated by the kill switch.

## Backup And Restore

The bootstrap SQL marks extension-owned operational tables for config dump with
`pg_extension_config_dump`. This preserves registration, jobs, and unapplied
sync rows across dump/restore.

Source tables remain authoritative for graph contents. After restore:

```sql
SELECT * FROM graph.status();
SELECT * FROM graph.build();
```

If the persisted file was not restored with `$PGDATA`, rebuild from source
tables.

## Admin SQL Examples

Use the named-graph APIs when you want to work on a graph without changing the session default.

**Create, register, and build**
```sql
SELECT * FROM graph.create_graph('customer_360', namespace := 'analytics');
SELECT * FROM graph.add_table_to_graph(
  'customer_360',
  'public.customers'::regclass,
  'id',
  ARRAY['name', 'email'],
  graph_namespace := 'analytics'
);
SELECT * FROM graph.add_edge_to_graph(
  'customer_360',
  'public.orders'::regclass,
  'customer_id',
  'public.customers'::regclass,
  'id',
  'placed_order',
  graph_namespace := 'analytics'
);
SELECT * FROM graph.build_graph('customer_360', force_persist := true, graph_namespace := 'analytics');
```

**Select, sync, and run jobs**
```sql
SELECT * FROM graph.select_graph('customer_360', namespace := 'analytics');
SELECT graph.enable_sync();
SELECT * FROM graph.add_sync_policy('customer_360', schedule_interval_secs := 300, graph_namespace := 'analytics');
SELECT * FROM graph.jobs('customer_360', graph_namespace := 'analytics');
SELECT * FROM graph.run_due_jobs();
```

**Set residency and quotas**
```sql
SELECT * FROM graph.set_graph_residency('customer_360', 'warm', namespace := 'analytics');
SELECT * FROM graph.set_graph_quota('owner', 'max_named_graphs', 25, current_user::text, 'hard');
SELECT * FROM graph.graph_quota_usage();
```

**Inspect storage, status, and memory**
```sql
SELECT artifact_bytes, segment_count, artifact_validation_state
FROM graph.projection_status();
SELECT node_count, edge_count, schema_status, sync_status, sync_lag
FROM graph.status();
SELECT * FROM graph.memory_profile(concurrent_backends := 4);
```

**Grant access and export the map**
```sql
SELECT * FROM graph.grant_graph('customer_360', 'app_reader', 'read', namespace := 'analytics');
SELECT * FROM graph.graph_map('customer_360', graph_namespace := 'analytics');
```

**Direct node lookup and search**
```sql
SELECT * FROM graph.get_node('customer_360', 'customers', 'cust_123', graph_namespace := 'analytics');
SELECT * FROM graph.search('name', 'Ada');
```
