Contents
- Rust Type Safety, Unsafe, And pgrx Boundary Plan
- Outcome
- Review Baseline
- Release-Blocking Findings
- Non-Negotiable Type And Safety Invariants
- Target Boundary
- Canonical Type Model
- pgrx-First Integration Matrix
- Work Packages
- RUST-0: Close Soundness And Security Blockers
- RUST-1: Fix Transaction And Adapter Semantics
- RUST-2: Replace Stringly Closed State
- RUST-3: Production Identity, Capacity, And Unit Types
- RUST-4: Exact GQL And Property Value Algebra
- RUST-5: Typed Persistent Formats
- RUST-6: Unsafe Isolation And Proof
- RUST-7: Streaming pgrx SQL Boundaries
- RUST-8: Verification And Continuous Enforcement
- Build-Order Integration
- Overall Definition Of Done
Rust Type Safety, Unsafe, And pgrx Boundary Plan
Outcome
pgGraph’s Rust core uses domain types for graph identity, state, values, capacity, and units. Closed vocabularies are enums after one boundary conversion; raw strings, integers, PostgreSQL datums, and pointers do not flow through the engine. Safe Rust APIs cannot reach undefined behavior for any input. PostgreSQL integration uses supported pgrx facilities first, with a small reviewed raw-FFI allowlist only where pgrx has no suitable safe API.
This is a correctness plan, not cosmetic cleanup. Exact value semantics, stable relation identity, safe mapped storage, transaction behavior, and security-definer hardening are prerequisites for full GQL and PostgreSQL 19 property-graph support.
Review Baseline
The 2026-07-09 review used the Rust reviewing checklist across production types, GUCs, catalog rows, GQL values, persistent segments, mmap access, unsafe blocks, PostgreSQL FFI, background workers, and SQL facades.
Inventory at review time:
- 54 explicit unsafe blocks: 31 in production paths and 23 in test fixtures;
- 7 unsafe declarations: four mapped-store constructors, two PostgreSQL callbacks, and one direct PostgreSQL error-reporting extern block;
- 44 security-definer SQL facades and no explicit pgrx
#[search_path]; - 12
oid::integerand 41graph_id::textsource occurrences; - 178 opt-in clippy possible-truncation, sign-loss, or possible-wrap warnings;
- no repository-owned
unsafe impl, manualSend/Sync,transmute, orMaybeUninit.
Current positive foundations to preserve:
- SPI parameters,
connect_mut, PostgreSQL quoting helpers,JsonB, temporal datums,#[pg_guard], andBackgroundWorkerBuilderalready use pgrx; unsafe_op_in_unsafe_fnand undocumented-unsafe-block lints are denied;- core control flow already has useful enums such as
SyncStatus,ReadOnlyReason,SchemaState, andMutationKind.
The actual raw PostgreSQL call/global surface is small enough to own:
pg_class_aclcheck, permanent xact/subxact callback registration, MyProcPid,
DataDir, and the private errstart/errcode/errmsg/errdetail/errhint/
errfinish family. Typed pg_sys ABI values such as Oid, Datum, callback
events, and error-report structs are not unsafe access by themselves.
The current warnings-denied clippy gate passes. That does not prove soundness: clippy cannot detect the safe mmap access paths below.
Release-Blocking Findings
| ID | Finding | Evidence | Required result |
|---|---|---|---|
| RUST-00A | Safe node lookup can dereference an mmap pointer out of bounds. | graph/src/node_store.rs:315-322 |
A local bounds proof dominates every dereference; invalid indexes return a typed error/None. |
| RUST-00B | The mmap edge constructor does not require validated CSR value invariants, while safe accessors reuse unchecked offsets. | graph/src/edge_store.rs:68-141, graph/src/edge_store.rs:522-597 |
Only an unforgeable validated layout can construct mapped stores. |
| RUST-00C | Custom SQLSTATE emission calls PostgreSQL errfinish() below the pgrx guard, which can longjmp across live Rust frames. |
graph/src/safety.rs:238-280 |
Error reporting unwinds Rust through the supported pgrx boundary before PostgreSQL ERROR. |
| RUST-00D | Durable filter deltas narrow typed values to Option<u32>; signed, temporal, large, and UUID updates can be changed or dropped. |
graph/src/projection/ingest.rs:32-48, graph/src/sql_sync.rs:1383-1391, graph/src/projection/segment.rs:116-126 |
A versioned tagged codec preserves every registered filter value across sync, segment, and reload. |
| RUST-00E | Security-definer SQL functions do not declare a hardened search path. | graph/src/sql_facade/ (security_definer entrypoints) |
Every definer function has a minimal explicit pgrx #[search_path(...)] and a catalog/attack regression. |
| RUST-00F | Registered relation identity is stored and later resolved through names/search path. | graph/sql/bootstrap.sql:75-91, graph/src/catalog/validate.rs:132-190, graph/src/builder.rs:341 |
Catalog identity is OID/regclass based; names are display data only. |
Do not expand the unsafe surface or make production-readiness claims until RUST-00A through RUST-00F have failing regressions, fixes, and release-matrix evidence.
Non-Negotiable Type And Safety Invariants
- A public or crate-visible safe function is sound for every value its type permits. Comments about expected callers do not repair an unsafe safe API.
- A closed vocabulary is one canonical Rust enum. Strings exist only at SQL, JSON, config-file, log, or artifact compatibility boundaries.
- An identifier or quantity with different semantics has a distinct newtype. Raw primitives exist only in ABI and serialized representations.
- All narrowing, sign-changing, count, offset, and byte arithmetic is checked. Representation ceilings fail before allocation or persistence.
- PostgreSQL OIDs enter through pgrx’s
pg_sys::Oid, UUIDs through pgrxUuid, and PostgreSQL values through their native datum types. Text roundtrips are not an internal transport. - Exact GQL/PostgreSQL numeric and temporal semantics never pass through JSON
or
f64unless the source type is explicitly floating point. - Raw
pg_syscalls are denied by default and confined to named adapter functions with an owner, safety contract, version matrix, and tests. - pgrx memory contexts are used only for PostgreSQL-owned lifetimes. They do not replace accounting for Rust heap, mmap, PostgreSQL executor memory, or spill.
- PostgreSQL source tables remain authoritative. Stronger Rust typing must not bypass constraints, triggers, ACLs, RLS, snapshots, or transaction rules.
Target Boundary
SQL / GUC / catalog / background-worker entrypoints
|
v
postgres/{relation,guc,error,acl,callbacks,bgworker_tx,datum}
|
one checked conversion into domain types
|
v
graph identity + GraphValue + typed IR + runtime + storage ports
|
v
persistence/{format,validate,mapped_artifact,reader,writer}
|
validated serialized primitives only
Core graph modules do not accept Datum, pg_sys::Oid, enum-like String, or
unvalidated offsets. The PostgreSQL adapter may use pgrx; a future pure core
crate is considered only after this boundary is real.
Canonical Type Model
| Concept | Canonical Rust representation | Boundary rule |
|---|---|---|
| Graph identity | GraphId(pgrx::Uuid) initially; distinct BuildId, JobId, PolicyId, RunId |
Bind/read UUID directly; format only for text APIs and safe path components. |
| Relation identity | TableOid/RelationId constructed from pgrx pg_sys::Oid |
Ban oid::integer, signed casts, and internal name re-resolution. |
| Graph indexes | NodeIdx, EdgeTypeId, FilterColumnId, RelationshipId |
Production types with private fields and checked constructors. Model untyped/reserved cases as enum variants, not magic bytes. |
| Planner indexes | NodeSlotId, RelationshipSlotId, PathSlotId, PatternId, ColumnIndex |
Typed arenas/indexed vectors prevent cross-family indexing. |
| Generations/order | GenerationId, SyncId, SourceWatermark, SegmentId |
Checked ordering and no cross-domain integer comparison. |
| Counts/units | NodeCount, EdgeCount, ByteCount, MemoryBudget, DiskBudget, WorkUnits |
Integer-byte arithmetic; MiB/JSON conversion is display only. |
| Edge weights | EdgeWeight with an explicit exact/integral domain |
Represent or reject negative, fractional, zero, NULL, and overflow explicitly under one documented contract; never clamp. |
| Engine access | EngineAccess::{Writable, ReadOnly(ReadOnlyReason)} |
Contradictory boolean/reason/status combinations cannot be represented. |
| Errors | Typed reason/resource/capacity enums inside GraphError |
Convert to message/detail/hint and SQLSTATE only at postgres/error. |
| GQL values | GraphValue algebra |
JSON is an output/input facade, not the execution value system. |
GraphValue must cover null/missing semantics, boolean, signed and unsigned
integers, exact numeric, explicit floating point, string, byte string, date,
time, timestamp, timestamptz, interval, UUID, list, record/map, node,
relationship, and path values. Use pgrx native datums such as AnyNumeric,
Date, timestamps, intervals, bytea, and Uuid, or delegate coercion and
comparison to PostgreSQL where that is the authoritative semantic boundary.
pgrx-First Integration Matrix
| Boundary | Required default | Raw exception policy |
|---|---|---|
| Closed GUCs | PostgresGucEnum, typed GucSetting<T>, define_enum_guc |
None. Paths and tenant-setting names remain string GUCs. |
| SQL-facing enum types | Parse once into canonical Rust enums; evaluate PostgresEnum for private catalogs after upgrade testing |
Retain text SQL signatures/checks temporarily where changing them would break upgrades. Unknown/NULL catalog values fail closed. |
| UUID/OID | pgrx Uuid, pg_sys::Oid, and PgRelation where locking/lifetime fits |
Convert once into domain wrappers; never roundtrip through text/signed integer. |
| SQL execution | pgrx SPI with bound parameters and connect_mut for writes |
Raw executor/catalog calls require a measured, documented need. |
| Security-definer path | pgrx #[search_path(...)] |
No implicit caller path. Dynamic object names still require OID identity and safe rendering. |
| SQL arrays/results | Lazy pgrx Array<'_, T> and genuinely incremental TableIterator/batches |
Collection requires a budget lease and explicit maximum. |
| Transactions | pgrx transaction helpers with explicit success/error semantics | Permanent xact/subxact callback registration may remain raw because pgrx’s safe callbacks are per transaction; callbacks must be panic-free. |
| ACL checks | One adapter, preferring a supported pgrx/SPI privilege path when semantics and cost are equivalent | pg_class_aclcheck may remain only if the adapter tests prove no safe equivalent preserves required behavior. |
| Background workers | BackgroundWorkerBuilder and one transaction policy adapter |
MyProcPid is an accepted isolated raw global required by pgrx’s worker API. |
| Data directory | Supported pgrx/SPI setting access when legal in the current backend phase | Raw DataDir may remain in one adapter where SPI is unavailable, with null/lifetime/version tests. |
| Error reporting | pgrx ErrorReport/guard boundary and built-in SQLSTATEs |
Never transmute a custom code into PgSqlErrorCode. If custom wire SQLSTATEs remain contractual, obtain/upstream a safe pgrx arbitrary-SQLSTATE API before release. |
| Memory | Rust budget governor plus bounded batches/spill; pgrx memory contexts for PostgreSQL-owned transient values | Memory contexts are not treated as a cap on Rust heap or mapped pages. |
Work Packages
RUST-0: Close Soundness And Security Blockers
- Add an out-of-range mmap node regression, then make access take a validated
NodeIdxor returnOption/GraphResult. Make owned and mapped behavior identical. - Replace public raw mmap parts with a private parser that returns
ValidatedGraphLayout. Validate section ranges, alignment, monotonic CSR offsets, terminal offsets, target bounds, section lengths, UTF-8, integer endianness, and overflow before a store exists. - Make
MappedGraphArtifactown theMmapplus typed validated ranges. Store offsets/ranges rather than free pointers; no store may outlive its owner. - Replace direct deep
errfinish()use. Prefer standard SQLSTATE plus a typedGraphDiagnosticCodein detail/metadata. If preservingPGxxxwire codes is mandatory, block release until pgrx exposes a sound validated-code path. - Add a minimal explicit search path to every security-definer entrypoint and
test shadow schemas/functions/operators plus
pg_proc.proconfigdrift. - Store relation OIDs/regclass identity at registration. Test table/schema rename, search-path changes, drop/recreate, concurrent DDL, and stale OIDs.
- Replace
Option<u32>durable filter payloads with a taggedPersistedFilterValue; migrate/rebuild segments and differential-test every supported type across restart.
Exit: KI-020 through KI-024 are closed, no safe accessor can reach unchecked pointer arithmetic, error reporting respects the pgrx guard, and the security/reload matrix is green on supported PostgreSQL versions.
RUST-1: Fix Transaction And Adapter Semantics
- Create
postgres/bgworker_tx.rs. A RustGraphResult::Errmust not accidentally return normally from a pgrx transaction closure and commit partial job state. Define atomic units versus intentionally committed progress checkpoints. - Put permanent transaction callbacks in one adapter. Replace panicking
RefCell::borrow_mut()paths with non-panicking state transitions; callbacks do not allocate, use SPI, or report PostgreSQL errors. - Specify and test commit, abort, savepoint, PL exception subtransactions,
parallel events, and
PREPARE TRANSACTIONbehavior. - Consolidate ACL,
MyProcPid, and earlyDataDiraccess in an explicit raw FFI allowlist. Remove replaceable raw access only after semantic tests.
Exit: injected worker failures cannot partially commit unintentionally or leave jobs stuck; callback paths are panic-free and version-tested.
RUST-2: Replace Stringly Closed State
- Define one canonical enum each for graph kind, residency, materialization, projection mode, graph privilege, quota scope/dimension/enforcement, job status/phase, validation status, search mode, sync mode, query freshness, OOM action, and build scan mode.
- Convert search, sync, freshness, OOM, scan, and projection GUCs to
PostgresGucEnum. Remove the obsolete pgrx 0.18 string-GUC comment and all invalid-value fallback parsing. - Make catalog/SPI adapters use
TryFromonce. Unknown, NULL, or corrupt closed state is a typed error, never"unknown","not_found", or a default mode. - Keep human diagnostic text and genuinely open names/paths as strings.
- Add one drift test comparing Rust spellings, SQL CHECK/enum vocabulary, configuration docs, and compatibility aliases.
Exit: no enum-like CString GUC or internal enum-like String field
remains, and invalid values fail at SET or the catalog boundary.
RUST-3: Production Identity, Capacity, And Unit Types
- Remove the test/development cfg from
NodeIdxandEdgeTypeId; make fields private and add checked constructors/conversions. - Introduce the identity, planner-index, generation, count, budget, and work types in the canonical model above. Replace tuple-shaped overlay rows with named structs and one shared overlay contract.
- Decide artifact capacity explicitly: either bounded
u32local indexes or segmented/64-bit global IDs. Do not widen silently. - Replace
ascasts in counts, offsets, status, and persistence with checked conversions. Return SQLbigintfor counts that can exceedi32. - Use pgrx
pg_sys::Oidat every SPI/catalog boundary and banoid::integer. - Enable sign-loss, possible-wrap, and possible-truncation clippy lints as deny-by-default after the inventory is burned down.
Exit: compile-fail tests prevent cross-domain ID/slot use; boundary tests
at i32::MAX/u32::MAX do not wrap; raw IDs remain only in ABI/format code.
RUST-4: Exact GQL And Property Value Algebra
- Replace GQL decimal
f64and internalserde_json::Valuetransport withGraphValueand typed binding rows. - Preserve exact numeric scale/precision, temporal types, UUID, bytea,
null/missing, and overflow behavior. No failed
from_f64conversion may silently produce JSON null. - Convert JSONB only at explicitly JSON-facing SQL functions. Prefer typed SQL rows for future standard GQL/SQL-PGQ surfaces.
- Replace
GraphId(String)and internalgraph_id::textqueries with native UUID binding. Add distinct ID types for jobs, builds, policies, and runs. - Define
EdgeWeight; reject unsupported values during registration/build/ sync instead of casting to bigint and clamping.
Exit: differential tests match PostgreSQL for large integers, high-scale numeric, SUM/AVG, mixed numeric comparisons, date/time/timestamps/intervals, UUID, bytea, null/unknown, overflow, and non-finite cases.
RUST-5: Typed Persistent Formats
- Make format structs use checked serialized primitives and convert to domain types only after validation.
- Version the filter-value codec and any index-width change. Document old reader, rebuild, rollback, and mixed-generation behavior.
- Fuzz every section offset/count, malformed CSR, PK UTF-8, filter tag/value, segment, manifest, and compatibility branch.
- Define the endian policy: decoded fallback or explicit unsupported-target rejection. Native typed loads must not reinterpret little-endian bytes on a big-endian host.
Exit: corrupt artifacts cannot construct mapped stores; old/new artifacts have deterministic accept/rebuild/reject behavior with no partial publish.
RUST-6: Unsafe Isolation And Proof
- Set
unsafe_code = "deny"at crate policy and permit it only in named mmap and PostgreSQL adapter modules. CI rejects a new unsafe file/function. - Keep
unsafe_op_in_unsafe_fn = "deny"andclippy::undocumented_unsafe_blocks = "deny"; require# Safetydocs on every unsafe function and a local invariant comment on every block. - Maintain an inventory recording owner, caller proof, lifetime/provenance, version support, and tests for each unsafe site.
- Replace test fixtures that pretend unrelated stack allocations are one mapped region with one aligned owned backing allocation.
- Remove ABI-incompatible fuzz stubs by extracting pure validation code from pgrx linkage; typed generated stubs are interim only.
Exit: the allowlist is minimal and stable, every unsafe precondition is encoded or checked at the closest boundary, and no raw pointer escapes it.
RUST-7: Streaming pgrx SQL Boundaries
- Accept lazy
Array<'_, T>for large SQL arrays, validate NULL/length, and collect only under an explicit budget. - Replace
TableIteratorwrappers over completeVecresults with resumable or bounded-batch execution. PostgreSQLLIMIT, iterator drop,statement_timeout, and cancellation stop graph work and release leases. - Keep SPI values and SQL rows typed until the outer facade. Delay JSON and display-string conversion.
- Do not move ordinary graph allocations into PostgreSQL memory contexts; integrate pgrx contexts only where PostgreSQL must own a datum lifetime.
Exit: large-array and LIMIT 1 tests stay within peak-RSS/work gates and
cancellation does not leave frontier/result memory live.
RUST-8: Verification And Continuous Enforcement
Required gates:
- unit/property tests for all enum parsers, IDs, checked arithmetic, slot
preservation, state machines, and
GraphValuecoercions; - compile-fail tests for mixed identity/slot types;
- pgrx tests for enum GUCs, native UUID/OID roundtrips, search path, relation rename/drop/recreate, worker transactions, SQLSTATEs, and callbacks;
- an in-memory aligned mapped backing for Miri, because OS file mappings are not Miri-supported;
- ASan/LSan or Valgrind against a PostgreSQL process loading the extension, covering load/traversal/reload/replacement/corruption/callback/worker paths;
- scheduled/release fuzzing for validated layouts and typed decoders without ABI-incompatible PostgreSQL stubs;
- PG14 through PG19 matrix coverage where the locked pgrx version exposes the feature;
- CI source guards for unsafe allowlist drift,
oid::integer, internalgraph_id::text, enum-likeCStringGUCs, unchecked narrowing casts, andResult<_, String>in production modules.
Every fix starts with a failing regression and closes only with its exact
command, PostgreSQL versions, compatibility impact, and artifact/SQL migration
evidence recorded in progress.md.
Build-Order Integration
- Run RUST-0 in checkpoint 0 alongside the existing correctness/security reproductions. These are blockers, not checkpoint-4 cleanup.
- Run RUST-1, budget/unit types from RUST-3, and persistent types from RUST-5 while implementing checkpoints 1A through 3. Do not create an untyped new artifact and plan to repair it later.
- Complete RUST-2, the remaining RUST-3 work, and RUST-6 during checkpoint 4 before canonical IR is declared stable.
- Implement RUST-4 as the first foundation of checkpoint 6 full-GQL slices.
- Apply RUST-7 vertically as each streaming operator/facade migrates.
- PostgreSQL 19 adapters reuse the same types and pgrx boundary; they do not introduce a parallel stringly catalog model.
- RUST-8 is continuous and becomes release evidence, not a final cleanup phase.
Overall Definition Of Done
- All release-blocking findings are closed with supported-version regressions.
- Every closed state in core/runtime/storage is a canonical enum.
- Every graph identity, planner slot, count, offset, byte quantity, generation, and watermark uses the correct domain type.
- No safe API can trigger UB for a value its signature accepts.
- Unsafe code exists only in the reviewed allowlist and passes its Miri, sanitizer, fuzz, and PostgreSQL matrix gates.
- pgrx-native UUID, OID, enum GUC, search-path, SPI, datum, guard, and worker features are used where they fit; raw PostgreSQL access has explicit proof that no suitable pgrx API exists.
- Full GQL execution uses exact typed values rather than JSON/
f64as its semantic core. - Public SQL and artifact changes have upgrade, rebuild, rollback, and release documentation; PostgreSQL source-of-truth behavior is unchanged.