# Limitations And Fit

## Relationship type limits

- Maximum distinct relationship types: 1,000,000 per graph.
- Maximum relationship type label bytes: 1,024 UTF-8 bytes.
- Maximum relationship type dictionary bytes: 256 MiB of label payload.

These hard ceilings are also constrained by the configured build and
maintenance memory budgets. They apply to rebuilt bases, committed incremental
dictionary growth, and transaction-local provisional labels.

pgGraph is a PostgreSQL-local graph acceleration layer. It is a good fit when
your source data already lives in PostgreSQL and you need SQL-accessible
neighborhood traversal, path explanation, and source-row search without moving
data into another system.

## Good Fits

| Use case | Why pgGraph fits |
|---|---|
| Local relationship traversal | Bounded BFS/DFS with depth, node, and frontier circuit breakers |
| Path explanation | Shortest path functions return source table/primary-key coordinates and edge labels |
| SQL-first applications | The current public API is PostgreSQL SQL functions in the `graph` schema, including the `graph.gql()` SQL function for the documented GQL subset |
| Existing relational schemas | Tables, primary keys, unique keys, and foreign keys are enough to model many graphs |
| Operational Postgres teams | Source tables, ACLs, RLS, backups, and observability remain PostgreSQL concerns |
| Validated persisted startup | Both graph directions, filters, identities, and resolution bytes load into one compact immutable mapping per backend |

## Not The Target

| Need | Better fit |
|---|---|
| Distributed graph execution across databases | A distributed graph system or application-level federation |
| Full GQL or SQL/PGQ compatibility | A database that implements those query languages completely today |
| Cypher, Gremlin, or SPARQL compatibility | A database that implements that query language |
| External graph storage as source of truth | A graph database or graph service |
| Online mutable graph topology with no rebuild/maintenance phase today | A mutable graph store |
| Large global analytics in the OLTP query path | A read replica, batch job, or dedicated analytics engine |
| Cross-database graph joins | ETL/materialization into one database or a federation layer |

## Current Feature Boundaries

| Area | Current boundary |
|---|---|
| WAL sync | `graph.sync_mode = 'wal'` is parsed but reserved; use `manual` or `trigger` |
| Build scanner | `graph.build_scan_mode = 'copy'` is reserved; use `select` |
| GQL and openCypher | `graph.gql()` supports the documented read subset, including node-only `MATCH`, top-level single-relationship `OPTIONAL MATCH`, multi-pattern joins over concrete labels/types with node-property `WHERE` predicates, bounded variable-length join path variables, relationship variable returns for fixed single-hop join patterns, variable-length relationship variables as path values, path variable returns, path functions, ordering, and projected-row `RETURN DISTINCT`, fixed path variables with optional named nodes and concrete labels/types plus compatible path-node property predicates, bounded wildcard variable-length path variables including unnamed multi-segment variable-length segments, relationship type alternation, and named endpoint variables plus relationship variables as path values on single bounded segments, projection-stage `WITH`, bounded `DISTINCT`, `RETURN` aggregates, JSONB list/map property paths, bounded relationship path values and path functions, single-node mapped `CREATE`, single-relationship mapped `CREATE` for registered edge-row mappings, single-node mapped property `SET`, mapped property `REMOVE`, single-edge mapped `DELETE` including wildcard relationship patterns that resolve to exactly one static-label registered edge-row mapping, single-node `DETACH DELETE` over static-label registered edge-row relationships, and single-node mapped `MERGE`; eligible fixed one-hop equality on a registered relationship label column lowers to exact type filtering; `graph.cypher()` accepts the overlapping openCypher-shaped subset through the same planner and explicitly does not claim full openCypher compatibility; dynamic-label `DETACH DELETE`, general relationship-property expressions, and broader writes are future work |
| SQL/PGQ | Internal typed adapter seam only; no public SQL/PGQ API is exposed until PostgreSQL provides stable graph-pattern hooks pgGraph can consume |
| Projection modes | `graph.build()` defaults to `csr_readonly`; `mutable_overlay` can be selected when `graph.mutable_enabled = on` and is required for GQL writes |
| Edge labels | Rebuilt v7 bases use adaptive relationship type IDs under the documented policy limits |
| Base topology | The base CSR stores are immutable; edge mutations use overlays until vacuum/maintenance rebuild |
| Backend state | Each PostgreSQL backend owns its own active `Engine`; Rust heap state is not shared across connections |
| mmap residency | Formats v6 and v7 load both CSR directions, relationship IDs, filters, dictionaries, identities, tenant data, and resolution as a backend-local read-only mapped snapshot; sparse mutable deltas remain backend-local heap |
| Query catch-up | Topology reads auto-apply pending trigger sync rows by default up to a captured high-water mark; set `graph.query_freshness = 'off'` for compatibility/manual catch-up |
| Search | Search runs against registered source-table columns through PostgreSQL, not against a full-text graph artifact |
| Hydration | Hydrated rows come from source tables and require source-table privileges; required GQL hydration fails closed with SQLSTATE `22000` and diagnostic `PG017` if a graph coordinate no longer has a readable source row |
| Components | Connected component functions are global admin operations and touch the whole active graph |

## Data Freshness Model

pgGraph does not replace your source tables. The graph is derived state.

<TreeDiagram>
  <TreeNode title="source table write">
    <TreeBranch>
      <TreeNode title="no sync mode: rebuild with graph.build()" />
      <TreeNode title="trigger mode: row enters graph._sync_log">
        <TreeBranch>
          <TreeNode title="graph.apply_sync() for backend-local overlays" />
          <TreeNode title="graph.maintenance() for rebuilt base graph" />
        </TreeBranch>
      </TreeNode>
    </TreeBranch>
  </TreeNode>
</TreeDiagram>

For read paths that need immediate consistency after writes, run the appropriate
sync or maintenance step in the same operational workflow. For read paths that
can tolerate scheduled refresh, use periodic build or maintenance jobs.

## Memory Expectations

The persisted `.pggraph` file reduces repeated startup cost, but it does not make
every structure shared:

<Callout type="info">
pgGraph uses mmap for immutable, rebuildable graph artifacts, not as a mutable
database storage engine or PostgreSQL buffer-pool replacement. PostgreSQL remains
responsible for authoritative table storage, WAL, MVCC, durability, and crash
recovery.
</Callout>

| Structure | Sharing behavior |
|---|---|
| Node active bits, table OIDs, primary-key bytes | Backend-local immutable mapped snapshot after load |
| Forward CSR arrays | Backend-local immutable mapped snapshot after load |
| Inbound CSR arrays | Backend-local immutable mapped snapshot after load |
| Resolution index | Backend-local immutable mapped snapshot after load |
| Filter values and text dictionaries | Backend-local immutable mapped snapshot plus sparse heap deltas |
| Relationship identities | Backend-local immutable mapped snapshot plus an owned suffix for post-load identities |
| Edge type registry | Bounded backend-local decoded labels |
| Durable projection segments | Shared files loaded from the graph artifact directory |
| Transaction-local and legacy sync overlays | backend-local heap |

Use `graph.estimate()` before large builds and watch `memory_used_mb`,
`edge_buffer_used`, `needs_vacuum`, and `read_only` in `graph.status()`. For
instance sizing, run `graph.memory_profile(concurrent_backends := N)` with the
number of sessions that may actively use the graph; pgGraph includes the
immutable artifact snapshot in each backend's private-memory projection.

## Operational Guidance

| Pattern | Recommendation |
|---|---|
| Small or mostly static graph | Use manual builds and persisted auto-load |
| Moderate writes with graph reads | Use trigger sync, persisted `mutable_overlay`, scheduled `graph.apply_sync()`, and scheduled `graph.maintenance()` |
| High write rate topology | Keep graph queries on a replica or schedule rebuild windows |
| Multi-tenant data | Register tenant columns and keep `graph.enforce_tenant_scope = on` |
| User-facing traversal | Set conservative `max_depth`, `max_nodes`, `max_frontier`, and pagination defaults |
| Admin analytics | Run connected component functions as graph admins, preferably away from OLTP peaks |

## Before Production

1. Confirm source-table `SELECT` grants and RLS policies for query roles.
2. Decide whether `graph.persist_on_build` and `graph.auto_load` should be on.
3. Set memory and traversal circuit breakers for your workload.
4. Choose `manual` or `trigger` sync mode.
5. Define a rebuild/maintenance schedule.
6. Exercise backup/restore and rebuild from source tables.
7. Monitor `graph.status()` after build, after sync apply, and after maintenance.
