# 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; ``` ## Security-Definer Execution pgGraph functions that run with definer privileges pin their PostgreSQL `search_path` to `pg_catalog, pg_temp`. Listing `pg_temp` after `pg_catalog` prevents temporary objects from taking implicit precedence over system catalogs. This prevents objects in a caller-controlled schema from changing how those privileged functions resolve built-in names or catalog relations. The same policy applies to the row and truncate trigger functions created by `graph.enable_sync()` for registered source tables. Registered relation identities are stored as PostgreSQL OIDs, so renaming a registered table does not change its mapping and a drop/recreate does not silently retarget it. Applications should still schema-qualify their own database objects and grant only the graph functions each role requires. ## 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 SQLSTATE `42501` with diagnostic `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 SQLSTATE `42501` with diagnostic `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 SQLSTATE `22023` with diagnostic `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`. For a graph whose tables are registered with `tenant_column` rather than pinned to a single graph-level tenant, an explicit tenant argument is a caller-supplied value, not one verified against the calling role's own identity. While `graph.enforce_tenant_scope = on` (the default), pgGraph refuses an explicit tenant argument for such a graph with SQLSTATE `22023` instead of accepting it: tenant scope must come from the session tenant (`graph.tenant_setting`) instead of a per-query argument. This closes the most direct form of self-service tenant selection, but pgGraph still cannot verify that a session's tenant setting reflects who is actually connected; that trust boundary belongs to whatever sets the session GUC (a connection pooler, a `SECURITY DEFINER` login trigger, or another trusted middle tier) before the caller's own queries run. If different tenants must not be able to read each other's data regardless of session state, also enforce it with PostgreSQL row-level security policies on the underlying tables, or register separate graphs per tenant with separate graph grants. Setting `graph.enforce_tenant_scope = off` removes this check entirely and restores the caller-suppliable-argument behavior; do not disable it for a tenant-sensitive deployment. 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. ## Row-Level Security And Topology Reads `graph.search()` re-queries source-table properties through SQL as the calling role, so row-level security policies on the underlying tables apply exactly as they would to a direct `SELECT`. The 1.1 topology APIs build one statement-scoped visibility scope after freshness handling. For each registered source relation where PostgreSQL says RLS applies to the outer caller, pgGraph scans caller-visible source keys and intersects them with projected node and relationship identities. Hidden seeds and targets behave as nonexistent, and hidden nodes or relationship rows are rejected before they affect reachability, path selection, weighted costs, limits, or hydration. This covers single- and multi-seed BFS/DFS, inbound traversal, search-derived traversal, `get_neighbors()`, `expand()`, `find_related()`, `neighborhood()`, shortest paths, weighted shortest paths, and the direct `path()` and `connection()` wrappers. `graph.build()` now accepts RLS-enabled node and relationship source tables by default. `graph.allow_rls_tables` remains registered for 1.x configuration compatibility but is a deprecated no-op. `graph.rls_mode = 'enforce'` is the default. A superuser can select `legacy_bypass` as a temporary compatibility escape hatch; table ACL checks still apply. The same visibility scope is used by components, aggregation, GQL/Cypher, derived workflows, path enumeration, and topology-producing mapped-write matching. PostgreSQL DML then rechecks source-table RLS, ACLs, constraints, and triggers before a mapped write commits. An RLS-active projected relationship must carry its durable source-row identity. A legacy artifact that lacks that identity fails closed with SQLSTATE `55000`, diagnostic `PG023`, and a rebuild-required hint. ## Query Identity And Telemetry Authorization In pgGraph 1.1 query entry points execute as the PostgreSQL caller. Source-table SQL used for hydration and visibility checks therefore sees the application role and its normal ACL/RLS environment. pgGraph does not switch Rust user IDs. Selected-graph resolution and private catalog maintenance use small `SECURITY DEFINER` mediators that capture the outer role, pin `search_path` to `pg_catalog, pg_temp`, and enforce graph grants before accessing internal catalogs. Operational status reports physical projection state; their counts are not RLS-filtered query results. `graph.status()`, build/maintenance job status, `graph.loaded_graphs()`, and `graph.graph_runtime_status()` require read access to every graph row they expose. `graph.projection_status()` and `graph.build_resource_status()` require selected-graph `admin` access. Cluster-wide generation counts and backend-local resource telemetry require a graph-schema administrator (superuser or a role with `CREATE` on schema `graph`). Build and maintenance job status uses pinned catalog-only mediators that capture the outer caller and authorize the selected or named graph. The raw job tables are not a supported authorization bypass. ## 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._build_jobs graph._maintenance_jobs graph._jobs graph._job_runs graph._sync_policies graph._sync_log graph._projection_generations graph._sync_watermarks 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. ## Durable Generation Publication Persisted projections publish as immutable generation manifests. pgGraph writes and fsyncs each candidate manifest, validates every active artifact reference, and then atomically replaces a checksummed current-generation pointer. Sync, compaction, repair, and rebuild publishers compare that pointer with the generation they planned from; a competing publisher returns SQLSTATE `55P03` with diagnostic `PG006` instead of replacing newer state. Readers continue using the immutable generation already installed in their backend while a replacement is prepared. Backend generation heartbeats and `graph.projection_retention_generations` protect active and rollback generations. `graph.projection_gc()` follows the current generation's ancestry, removes unreferenced obsolete artifacts, and bounds old or abandoned manifest files without moving the serving pointer. The default retention of two keeps the current and previous valid generation when no older reader pin requires more. An interrupted or failed publication can leave an unreferenced staging file or generation manifest, but it cannot make that candidate current. The next GC pass removes abandoned manifests that are neither retained nor pinned. If the current pointer or its referenced manifest fails checksum validation, pgGraph fails closed; rebuild or use `graph.projection_repair()` from authoritative PostgreSQL source tables. ## Error Codes The extension emits standard PostgreSQL SQLSTATEs. Its stable diagnostic code appears in `DETAIL` as `pgGraph diagnostic: PGxxx`, allowing applications and operators to distinguish pgGraph conditions that share a standard SQLSTATE. | SQLSTATE | Diagnostic | Error | Common fix | |---|---|---|---| | `53200` | `PG001` | Memory limit exceeded | Raise `graph.memory_limit_mb` or reduce registered graph size | | `42501` | `PG002` | ACL denied | Grant `SELECT` on the relevant source table or graph admin privilege for admin functions | | `55000` | `PG003` | Graph not built | Run `SELECT * FROM graph.build();` | | `54000` | `PG004` | Edge type limit exceeded | Reduce distinct edge labels; max user labels are 254 | | `22023` | `PG005` | Invalid filter | Register the filter column and use supported operators/types | | `55P03` | `PG006` | Build locked | Wait for current build/vacuum/maintenance | | `54000` | `PG007` | Resource limit exceeded | Reduce query/work size or raise the relevant query, maintenance, disk, work, or elapsed limit | | `54000` | `PG008` | Edge buffer full | Run `graph.vacuum()`/`graph.maintenance()` or increase `graph.edge_buffer_size` | | `XX001` | `PG009` | Corrupt graph file | Rebuild persisted artifact | | `P0002` | `PG010` | Node not found | Check table/primary key and rebuild/apply sync if data changed | | `0A000` | `PG011` | Incompatible graph file version | Rebuild artifact with current extension version | | `55000` | `PG012` | Read-only graph state | Inspect `graph.status().read_only_reason`, then rebuild, vacuum, or adjust memory policy | | `42601` | `PG013` | GQL syntax error | Check the query text against the documented GQL subset | | `0A000` | `PG014` | Unsupported GQL feature | Rewrite the query using supported GQL compatibility-matrix features | | `22023` | `PG015` | GQL semantic error | Verify labels, relationship types, aliases, and return bindings | | `22023` | `PG016` | GQL parameter error | Pass a JSON object containing every `$parameter` referenced by the query | | `22000` | `PG017` | GQL execution error | Fix invalid runtime values, reduce result cardinality, or rebuild/apply sync if a graph coordinate can no longer hydrate its source row | | `0A000` | `PG018` | Unsupported graph operation | Use a supported query shape or merge pending overlays before retrying | | `54000` | `PG019` | Overlay limit exceeded | Commit or roll back the transaction, or raise the relevant overlay GUC | | `55000` | `PG020` | Extension disabled | `SET graph.enabled = on` | | `55000` | `PG021` | Legacy 1.0 row-level security build boundary | Upgrade to the 1.1 behavior; `graph.allow_rls_tables` is retained only as a no-op compatibility setting | | `55000` | `PG022` | Sync replay position predates a prune | Run `graph.build()` or `graph.vacuum()` for a fresh consistent base, then resume normal sync | | `55000` | `PG023` | RLS relationship identity missing | Rebuild the projection so every relationship has a durable source-row identity | | `XX000` | `PG000` | 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. PostgreSQL relation OIDs are local to a database, so a logical restore into a new database does not automatically rebind restored graph registrations to relations that happen to have the same names. This fail-closed behavior prevents a dropped and recreated relation from silently taking over an existing registration. After a logical restore into a new database, reset the restored derived graph state, reapply the reviewed table, edge, and filter registrations against the restored relations, then rebuild and re-enable synchronization. Re-enabling synchronization replaces any logically restored triggers whose function names refer to relation OIDs from the source database: ```sql SELECT graph.reset(); -- Reapply graph.add_table(...), graph.add_edge(...), and -- graph.add_filter_column(...) calls for the restored source schema. SELECT * FROM graph.build(); SELECT graph.enable_sync(); ``` Keep the registration SQL with your schema migrations so this procedure is repeatable. A physical recovery that preserves the PostgreSQL data directory and relation OIDs may retain registrations, but derived persisted graph files still require validation; rebuild from the source tables whenever those files were not restored or cannot be validated. ## 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'); ```