Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
pg_tviews
Transactional Materialized Views with Incremental Refresh for PostgreSQL
Core infrastructure for FraiseQL's GraphQL Cascade — automatic incremental refresh of JSONB read models with 5,000-12,000× performance gains.
By Lionel Hamayon • Part of the FraiseQL framework
Features • Quick Start • Performance • Documentation • Architecture
🍓 Part of the FraiseQL Ecosystem
pg_tviews is the performance foundation for FraiseQL's CQRS architecture:
Server Stack (PostgreSQL + Python/Rust)
| Tool | Purpose | Status | Performance Gain |
|---|---|---|---|
| pg_tviews | Incremental materialized views | Beta ⭐ | 100-500× faster |
| jsonb_delta | JSONB surgical updates | Stable | 2-7× faster |
| pgGit | Database per branch or agent, attributed DDL history | In development (v2) | Not a schema source of truth; promotes into confiture |
| confiture | PostgreSQL migrations | Stable | 300-600× faster |
| fraiseql | GraphQL framework | Stable | 7-10× faster |
| fraiseql-data | Seed data generation | Planned | Auto-dependency resolution |
Client Libraries (TypeScript/JavaScript)
| Library | Purpose | Framework Support |
|---|---|---|
| graphql-cascade | Automatic cache invalidation | Apollo, React Query, Relay, URQL |
How pg_tviews fits:
- fraiseql uses pg_tviews for GraphQL read models (tv_* tables)
- jsonb_delta optimizes JSONB updates (1.5-3× faster)
- confiture manages TVIEW schema evolution
- graphql-cascade (client-side) invalidates browser caches when mutations trigger refreshes
Stack it up:
# Install extensions
;
;
# Create incremental view
;
# Use with fraiseql GraphQL
)
📋 Version Status
Current Version: 0.1.0-beta.18 (September 2026)
- Status: Public Beta - Feature-complete, API may change
- Production Use: Suitable for evaluation, not mission-critical systems
- Support: Community support via GitHub issues
Roadmap to 1.0.0:
- ✅ Core TVIEW functionality complete
- ✅ Comprehensive documentation
- 🔄 Production hardening and testing
- 🔄 Security audit
- 🔄 Performance validation at scale
Breaking Changes: Minor API changes possible until 1.0.0. Pin to exact version in production.
🎯 The Problem
Traditional PostgreSQL materialized views require full rebuilds on every refresh—scanning entire tables and recomputing all rows. For large datasets or complex views with JOINs, this becomes prohibitively expensive:
-- Traditional approach: Full rebuild every time
REFRESH MATERIALIZED VIEW my_view; -- Scans ALL rows, recomputes EVERYTHING
Result: Minutes of downtime, high I/O, locks, and stale data between refreshes.
✨ The Solution
pg_tviews brings incremental materialized view maintenance to PostgreSQL with surgical, row-level updates that happen automatically within your transactions:
-- pg_tviews: Automatic incremental refresh
SELECT p.pk_post as pk_post, jsonb_build_object(...) as data
FROM tb_post p JOIN tb_user u ON p.fk_user = u.pk_user;
-- Just use your database normally:
INSERT INTO tb_post(title, fk_user) VALUES ('New Post', 123);
COMMIT; -- tv_post automatically updated with ONLY the affected row!
Result: Millisecond updates, no full scans, always up-to-date, zero manual intervention.
🚀 Performance Optimization
For 1.5-3× faster JSONB updates, install the optional jsonb_delta extension:
CREATE EXTENSION jsonb_delta; -- Optional: 1.5-3× faster JSONB updates
CREATE EXTENSION pg_tviews;
Without jsonb_delta, pg_tviews uses standard PostgreSQL JSONB operations (still fast, just not optimized).
CREATE EXTENSION pg_tviews says so once, with a WARNING; after that each backend notes it
once in the server log, and pg_tviews_health_check() reports it. Writes send no message.
🔑 Trinity Identifier Pattern
pg_tviews follows FraiseQL's trinity identifier conventions for optimal GraphQL Cascade performance:
id(UUID): Public identifier for GraphQL/REST APIspk_entity(integer): Primary key for efficient joins and lineage trackingfk_*(integer): Foreign keys for cascade propagationidentifier(text): Optional unique slugs for SEO-friendly URLs{parent}_id(UUID): Optional UUID FKs for FraiseQL filtering
Example TVIEW with full trinity support:
SELECT
p.pk_post, -- lineage root
p.id, -- GraphQL ID
p.identifier, -- SEO slug
p.fk_user, -- cascade FK
u.id as user_id, -- FraiseQL filtering FK
jsonb_build_object(
'id', p.id,
'identifier', p.identifier,
'title', p.title,
'author', jsonb_build_object(
'id', u.id,
'identifier', u.identifier,
'name', u.name,
'email', u.email
)
) as data
FROM tb_post p
JOIN tb_user u ON p.fk_user = u.pk_user;
🚀 Key Features
Automatic & Intelligent
- 🔍 Smart Dependency Detection: Automatically analyzes SQL to find source tables and relationships
- 🎯 Surgical Updates: Updates only affected rows—never full table scans
- 🔄 Transactional Consistency: Refresh happens atomically within your transaction
- 📊 Cascade Propagation: Automatically handles multi-level view dependencies
High Performance
- ⚡ 100-500× Faster Triggers: Statement-level triggers for bulk operations
- 💾 Query Plan Caching: 10× faster with cached prepared statements
- 📦 Bulk Optimization: N rows with just 2 queries instead of N queries
- 🎨 Smart Patching: 2× performance boost with optional jsonb_delta integration
- 🚀 UNLOGGED Tables: 2-3× write performance with automatic crash recovery
Production-Ready
- 🏊 Connection Pooling: Full PgBouncer/pgpool-II compatibility with DISCARD ALL handling
- 📈 Comprehensive Monitoring: Real-time metrics, health checks, performance views
- 🛡️ Enterprise-Grade Code: 100% clippy-strict compliance, panic-safe FFI, zero unwraps
Compliance & Security
- 📋 SBOM Generation: Automated Software Bill of Materials in SPDX 2.3 and CycloneDX 1.5 formats
- 🔐 Cryptographic Signing: Sigstore keyless + GPG maintainer signatures for all releases
- 🛡️ Dependency Security: Automated vulnerability scanning with cargo-audit + cargo-vet audits
- 🔄 Automated Updates: Dependabot integration for security patches and updates
- 🏗️ Reproducible Builds: Docker-based build environment with locked dependencies
- 🌍 International Compliance: EU Cyber Resilience Act, US EO 14028, PCI-DSS 4.0, ISO 27001
- 🔒 Supply Chain Security: SLSA Level 3 provenance with dependency transparency
- 📊 Vulnerability Management: Complete dependency inventory for CVE tracking
Developer-Friendly
- 📝 Simple API:
pg_tviews_create()function for easy TVIEW creation - 🔧 JSONB Optimized: Built for modern JSONB-heavy applications
- 📊 Array Support: Full INSERT/DELETE handling for array columns
- 🐛 Excellent Debugging: Rich error messages, debug functions, health checks
- ⏸️ Bulk Operations: Suspend/resume triggers for safe bulk data loading (Issue #44)
📊 Performance
Real-World Benchmarks
| Operation | Traditional MV | pg_tviews | Improvement |
|---|---|---|---|
| Single row update | 2,500ms | 1.2ms | 2,083× |
| Medium cascade (50 rows) | 7,550ms | 3.72ms | 2,028× |
| Bulk operation (1K rows) | 180,000ms | 100ms | 1,800× |
Scaling Characteristics
- Linear scaling with data size for incremental updates
- Sub-linear scaling for cascading updates (graph caching)
- Constant time for cache hits (90%+ hit rate in production)
- O(1) queue operations with HashSet-based deduplication
⚡ Direct-patch fast path (Issue #56)
For an eligible UPDATE, pg_tviews builds a JSONB patch at trigger time from
NEW and applies it straight to tv_<entity>.data with
jsonb_smart_patch_scalar/_nested — skipping the backing-view recompute
entirely (zero SELECT … FROM v_entity). The same patch is derived for
parent tviews that embed the entity as a nested object, so a single-field change
with a wide fan-out (e.g. an author edited on 60 posts) propagates as a handful of
grouped patch UPDATEs with no view queries.
Anything outside the eligibility boundary silently falls back to the normal
recompute path, which stays the single source of truth — the fast path is a pure
optimization and its data output is byte-identical to a recompute.
What qualifies
The fast path engages only when every condition holds (otherwise recompute):
- the change is a row-level
UPDATE(INSERT/DELETE change membership); jsonb_deltais installed andpg_tviews.direct_patch_enabled = on;- the entity is not
DISTINCT ONand not aUNION; - every changed column maps identity-style to a top-level
datakey (jsonb_build_object('bio', bio)— no expression, cast, or other relation); - no changed column is a foreign key, the primary key, or a column the tview also
projects outside
data; - the changed values' types are TEXT/VARCHAR, INT2/4/8, BOOL, UUID, JSONB, or NULL (anything else — float, numeric, timestamps, arrays — falls back);
- for a parent, its dependency on the child is
nested_objectwith a path (array/scalar/UUID-fk parents recompute).
Copied parent columns (Issue #120)
A tview that copies a column of a joined parent table into its data
(jsonb_build_object('author_name', u.name) over JOIN tb_user u ON u.pk_user = p.fk_user) is written by one statement when that column changes:
UPDATE tv_post … WHERE fk_user = <pk>, instead of recomputing every post of that
user. Same rules as above, plus: the tview reads the parent through one join on its
own base table, projects that fk_* column, reads the parent table only once, and
uses the column nowhere else in its definition. Anything else recomputes the
children. Measured with test/sql/real_benchmark/scalar_cascade_fanout.sh … scalar:
1.8–2.7× faster per parent update from 10 to 10 000 children.
Kill-switch and observability
SET pg_tviews.direct_patch_enabled = off; -- force the recompute path everywhere
pg_tviews_queue_stats() exposes the counters (session-cumulative):
SELECT pg_tviews_queue_stats;
-- { … "direct_patch_captured": N, "direct_patches_applied": N,
-- "direct_patch_fallbacks": N, "view_recomputes": N }
An eligible update leaves view_recomputes unchanged and bumps
direct_patches_applied — proof it skipped the view.
Upgrade note
The column→key map is extracted when a TVIEW is registered. TVIEWs registered by
an older release keep working on the recompute path until
SELECT * FROM tviews.pg_tviews_reregister_all() re-derives their metadata in place
(see Upgrading).
🚀 UNLOGGED Tables
pg_tviews automatically creates TVIEWs as UNLOGGED tables for maximum write performance.
Benefits
- ⚡ 2-3× Faster Writes: No WAL overhead for TVIEW updates
- 🔄 Automatic Recovery: Transparent crash recovery from base tables
- 💾 I/O Reduction: Less disk writes for high-frequency updates
- 🔧 Configurable: GUC parameter controls default behavior
⚠️ Hot standbys, promotion and crash restarts
An UNLOGGED table is not replicated. A hot standby cannot read an UNLOGGED
TVIEW at all (ERROR: cannot access temporary or unlogged relations during recovery), and promotion or a crash restart leaves it empty. If reads are
routed to replicas, make those TVIEWs LOGGED:
SET pg_tviews.unlogged_by_default = off; -- for new TVIEWs
SELECT pg_tviews_set_logged('post', true); -- existing TVIEW (rewrites it)
SELECT * FROM pg_tviews_replication_status; -- what a standby can serve
To repopulate emptied UNLOGGED TVIEWs as soon as a server leaves recovery, list
the databases in pg_tviews.auto_rebuild_databases (needs a restart), or call
SELECT * FROM pg_tviews_rebuild_all(); after a failover or restore. See
docs/operations/replication.md.
Crash Recovery
UNLOGGED tables are truncated on PostgreSQL crash. pg_tviews rebuilds a TVIEW on the first write that touches it, and the startup worker above rebuilds the configured databases without waiting for a write. To check one TVIEW by hand:
-- Check and recover after potential crash
SELECT pg_tviews_recover_after_crash('user_summary');
-- Returns true if recovery was performed, false if not needed
Configuration
All limits and toggles are runtime-tunable GUCs (SET per-session or set in
postgresql.conf); none require recompiling:
| GUC | Type | Default | Purpose |
|---|---|---|---|
pg_tviews.max_propagation_depth |
int | 100 | Max cascade iterations before aborting |
pg_tviews.max_dependency_depth |
int | 10 | Max pg_depend traversal depth |
pg_tviews.max_queue_size |
int | 10000 | Refresh-queue backpressure limit |
pg_tviews.batch_size |
int | 1000 | Max PKs per bulk-refresh statement (chunking) |
pg_tviews.cache_size |
int | 10000 | Max entries per in-memory metadata cache |
pg_tviews.graph_cache_enabled |
bool | on | Cache dependency graphs |
pg_tviews.table_cache_enabled |
bool | on | Cache table→entity mappings |
pg_tviews.metrics_enabled |
bool | off | Deprecated, has no effect (metrics are always collected); will be removed |
pg_tviews.audit_enabled |
bool | off | Audit logging (opt-in) |
pg_tviews.unlogged_by_default |
bool | on | Create TVIEW tables UNLOGGED (not readable on standbys) |
pg_tviews.auto_rebuild_databases |
string | "" | Databases whose emptied UNLOGGED TVIEWs are rebuilt when recovery ends (restart required) |
pg_tviews.data_gin_index |
bool | off | Create a GIN index on data for new TVIEWs |
pg_tviews.fillfactor |
int | 85 | Heap fillfactor for new TVIEW tables (keeps refreshes HOT) |
pg_tviews.direct_patch_enabled |
bool | on | Direct-patch fast path (see above) |
pg_tviews.suspend_triggers |
bool | off | Suspend trigger-based refresh (bulk loads) |
pg_tviews.union_duplicate_policy |
string | error | first or error on duplicate UNION-ALL keys |
pg_tviews.report_max_tracked |
int | 10000 | Changed rows journaled per transaction for pg_tviews_flush_and_report() (0 = off) |
pg_tviews.uncascaded_policy |
enum | warn | warn, error or full_refresh: what a new TVIEW does about base tables no cascade reaches. Read at create time and stored with the TVIEW; full_refresh recomputes the whole TVIEW on each write to such a table (details) |
pg_tviews.log_level |
string | info | Logging verbosity |
-- Examples
SET pg_tviews.unlogged_by_default = true; -- default UNLOGGED behavior
SET pg_tviews.batch_size = 5000; -- larger bulk-refresh chunks
SET pg_tviews.cache_size = 50000; -- bigger per-session caches
-- Alter existing TVIEWs (each rewrites the table under an exclusive lock)
SELECT pg_tviews_set_logged('my_view', false); -- UNLOGGED
SELECT pg_tviews_set_logged('my_view', true); -- LOGGED, readable on standbys
GUCs require
shared_preload_libraries = 'pg_tviews'(already needed for the extension) so they are registered at backend start.
TVIEW naming convention
A TVIEW's derived entity must be refreshable: either a tb_<entity> base table
exists (its primary key is pk_<entity>), or the definition joins the base tables
it derives from so cascade paths can route changes to it. pg_tviews_create
rejects a definition that satisfies neither, since it could never refresh.
Safety Guarantees
- ✅ Data Recovery: All TVIEW data reconstructible from base tables
- ✅ Transparent: Applications work unchanged
- ✅ Configurable: Can disable UNLOGGED for specific use cases
- ✅ Tested: Comprehensive crash simulation and recovery testing
🎬 Quick Start
Installation
# Prerequisites
# - PostgreSQL 16, 17 or 18 (the supported versions; CREATE EXTENSION refuses older ones)
# - Rust toolchain (pinned in rust-toolchain.toml)
# Install pgrx (must match project version)
# Initialize pgrx
# Clone and build
# --no-default-features --features pg16 (or pg17)
# Enable in your database
The tviews schema
Every object of the extension lives in the schema tviews, whatever the search_path
at CREATE EXTENSION (CREATE EXTENSION pg_tviews SCHEMA other is refused).
CREATE EXTENSION creates the schema when it is missing and refuses one owned by
another role. Call the functions qualified, or add tviews to the search_path:
SELECT tviews.pg_tviews_create('tv_post', $$ SELECT ... $$);
your_database SET search_path = "$user", public, tviews;
SELECT pg_tviews_create('tv_post', $$ SELECT ... $$);
TVIEWs themselves (tv_*, v_*) are created in the schema you name, or in
current_schema(). Nothing pg_tviews does at run time needs tviews on the
search_path. DROP EXTENSION pg_tviews leaves the tviews schema behind, empty.
Privileges
A TVIEW is maintained by its owner, like a materialized view: when a role writes to a
base table, the refresh reads and writes each affected TVIEW as the owner of its
tv_* table, with search_path set to pg_catalog, pg_temp. Writers need only
their privileges on the base tables; grant application roles SELECT on the
tv_* tables they read. The owner needs SELECT on everything its definition
reads.
Creating TVIEWs from migrations
tviews.pg_tviews_create_or_replace(name, query, options) creates a TVIEW, or brings an
existing one to query and options with the smallest change, so a migration can be
applied again:
SELECT tviews.pg_tviews_create_or_replace('app.tv_post', $$
SELECT p.pk_post, p.id, p.fk_user, jsonb_build_object('title', p.title) AS data
FROM app.tb_post p $$, options => '{"logged": true, "fillfactor": 85}');
-- created | unchanged | altered (storage only) | replaced (same columns, rows
-- reconciled in place) | rebuilt
It runs the DDL as the caller and requires owning an existing TVIEW; a role that owns
the schema and has TRIGGER on the base tables needs no superuser. CREATE TABLE tv_post AS SELECT … (optionally UNLOGGED, WITH (fillfactor = n)) runs the same
code with create-only semantics. See
docs/reference/read-contract.md.
Reading what is registered
Tools read tviews.registry (one row per TVIEW: schema, name, entity, normalized
query, base tables, options, needs_reregister, backing view) and check
tviews.contract_version(). Both follow the stability rules in
docs/reference/read-contract.md;
tviews.pg_tview_meta and the other pg_tview_* tables are internal.
Upgrading
Each release has its own extension version (SELECT extversion FROM pg_extension WHERE extname = 'pg_tviews') and ships upgrade scripts:
- Install the new package and restart PostgreSQL (the library is preloaded).
- In each database:
ALTER EXTENSION pg_tviews UPDATE; - When the release notes say so, or
tviews.pg_tviews_health_check()reports TVIEWs to re-register:SELECT * FROM tviews.pg_tviews_reregister_all();It re-derives each TVIEW's metadata and triggers from its definition, without touching its rows.
Between steps 1 and 2, writes to the TVIEWs' base tables fail with
pg_tviews library catalog revision … does not match the installed extension: they
are never served by a mismatched library.
Installs of 0.1.0 (every release up to 0.1.0-beta.19) cannot be updated in place.
After step 1, run scripts/migrate-from-0.1.0.sql in
each database instead: it moves the extension to tviews and re-registers every
TVIEW, keeping their rows (not the audit log).
Your First TVIEW
-- Create base tables (FraiseQL style)
(
pk_user BIGSERIAL PRIMARY KEY,
id UUID NOT NULL DEFAULT gen_random_uuid,
identifier TEXT UNIQUE,
name TEXT,
email TEXT
);
(
pk_post BIGSERIAL PRIMARY KEY,
id UUID NOT NULL DEFAULT gen_random_uuid,
identifier TEXT UNIQUE,
title TEXT,
content TEXT,
fk_user BIGINT REFERENCES tb_user(pk_user)
);
-- Create a TVIEW (note: tv_ prefix is required)
SELECT
p.pk_post as pk_post, -- Primary key column (required)
p.id, -- GraphQL ID
p.identifier, -- SEO slug
p.fk_user, -- Cascade FK
u.id as user_id, -- FraiseQL filtering FK
jsonb_build_object(
'id', p.id,
'identifier', p.identifier,
'title', p.title,
'content', p.content,
'author', jsonb_build_object(
'id', u.id,
'identifier', u.identifier,
'name', u.name,
'email', u.email
)
) as data -- JSONB data column (required)
FROM tb_post p
JOIN tb_user u ON p.fk_user = u.pk_user;
-- Use it like a table
SELECT data FROM tv_post WHERE data->>'title' ILIKE '%rust%';
-- It updates automatically!
INSERT INTO tb_user (identifier, name, email) VALUES ('alice', 'Alice', 'alice@example.com');
INSERT INTO tb_post (identifier, title, content, fk_user) VALUES
('learning-rust', 'Learning Rust', 'Rust is amazing!', 1);
-- tv_post is now automatically up-to-date!
SELECT data FROM tv_post;
Enable Advanced Features
-- Monitor system health
SELECT * FROM pg_tviews_health_check;
-- Size, rows and indexes of each TVIEW
SELECT * FROM pg_tviews_performance_stats;
⏸️ Bulk Operations (Issue #44)
For bulk INSERT/UPDATE/DELETE operations (e.g., seed data loading, ETL imports) on tables with TVIEWs, use the suspend/resume API to prevent trigger-based refresh during the operation:
Basic Pattern
Suspension lasts until the end of the transaction, so run the load in one:
BEGIN;
SELECT pg_tviews_suspend_triggers; -- no refresh from here on in this transaction
INSERT INTO customers SELECT * FROM staging_customers;
INSERT INTO orders SELECT * FROM staging_orders;
SELECT pg_tviews_resume_triggers; -- rebuilds the changed TVIEWs, dependencies first
COMMIT;
Why This Matters
When bulk inserting into multiple related tables, triggers refresh TVIEW rows after each statement, while the related tables may not be loaded yet. With suspension, all the data is loaded first, and each TVIEW that changed (and every TVIEW that embeds it) is rebuilt once, in dependency order.
Ending the Transaction Without Resuming
An explicit COMMIT catches up the same way when the transaction is still
suspended. An implicit commit (an autocommit statement, a DO block) cannot: it
logs a WARNING naming the stale TVIEWs, to be fixed with pg_tviews_refresh(entity)
or pg_tviews_refresh_all().
API Reference
pg_tviews_suspend_triggers()- Start suspension (supports nesting)pg_tviews_resume_triggers()- Resume; rebuilds the TVIEWs that changed and those that embed thempg_tviews_refresh(entity)- Rebuild one TVIEW and every TVIEW that embeds it, in dependency orderpg_tviews_refresh_all()- Rebuild every TVIEW in dependency orderpg_tviews_is_suspended()- Check current suspension statepg_tviews_suspended_entities()- List entities that changed during suspension
Nested Suspension
Calls can be nested; each must be matched:
BEGIN;
SELECT pg_tviews_suspend_triggers; -- depth 1
SELECT pg_tviews_suspend_triggers; -- depth 2
-- operations...
SELECT pg_tviews_resume_triggers; -- depth 1
SELECT pg_tviews_resume_triggers; -- depth 0 (now resumed)
COMMIT;
Use Cases
- Seed data loading: DB initialization with initial data set
- ETL imports: Loading data from external sources into staging tables
- Snapshot imports: Restoring from database dumps or migrations
- Bulk migrations: Large data transformations
🏗️ Architecture
High-Level Design
┌─────────────────────────────────────────────────────────────────┐
│ User Application │
└────────────────────┬────────────────────────────────────────────┘
│ INSERT/UPDATE/DELETE
▼
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL Core │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ tb_* Tables │────▶│ Triggers │────▶│ Refresh Queue│ │
│ │ (command) │ │ (per-row or │ │ (thread-local)│ │
│ └──────────────┘ │ statement) │ └──────┬────────┘ │
│ └──────────────┘ │ │
│ ┌──────────────┐ │ │
│ │ ProcessUtil │ │ │
│ │ Hook (DDL) │ │ │
│ └──────────────┘ │ │
│ │ │
│ ┌───────────────────────────▼──────────┐ │
│ │ Transaction Callback Handler │ │
│ │ (PRE_COMMIT, COMMIT, ABORT, 2PC) │ │
│ └──────────┬────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ pg_tviews Refresh Engine │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ Dependency Graph Resolution │ │ │
│ │ │ (Topological Sort, Cycle Detect) │ │ │
│ │ └───────────┬──────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ Bulk Refresh Processor │ │ │
│ │ │ (2 queries for N rows) │ │ │
│ │ └───────────┬──────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ Cache Layer (Graph, Table, Plan) │ │ │
│ │ └───────────┬──────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ Metrics & Monitoring │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ TVIEW Tables│◀────│ Backing │◀────│ Metadata │ │
│ │ (tv_*) │ │ Views (v_*) │ │ (pg_tview_*)│ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Key Components
- Trigger System: Captures changes at source tables, enqueues refresh operations
- Transaction Queue: Thread-local HashSet for deduplication and ACID guarantees
- Dependency Graph: Resolves refresh order, detects cycles, enables cascading
- Refresh Engine: Executes surgical updates with bulk optimization
- Cache Layer: Three-tier caching (graph, table OIDs, query plans)
- Monitoring: Real-time metrics, health checks, performance analytics
📚 Documentation
Getting Started
- Quick Start - Step-by-step setup guide
- Installation - Detailed installation instructions
- FraiseQL Integration - Framework integration guide
User Guides
- For Developers - Application integration patterns
- For Operators - Production deployment guide
- For Architects - CQRS design decisions
Reference
- API Reference - Complete function reference
- DDL Reference - CREATE/DROP TABLE syntax
- Syntax Comparison - TVIEW creation methods
- Error Reference - Error types and solutions
- Configuration - GUC settings
Operations
- Monitoring - Metrics and health checks
- Troubleshooting - Debugging procedures
- Performance - 📊 Performance tuning
- Performance Best Practices - Essential patterns
- Performance Analysis - Diagnostic tools
- Index Optimization - Index strategies
- Performance Tuning - Advanced tuning
- Security - Security best practices
- SBOM - Software Bill of Materials and supply chain security
- Disaster Recovery - Backup and recovery
- Runbooks - Operational procedures
- Upgrades - Version migration guides
Benchmarks
- Overview - Methodology and the three-arm comparison
- Running Benchmarks - How to run the harness on a local pgrx cluster
- Results - Measured performance figures
- Results Interpretation - Reading the numbers honestly
- jsonb_delta Integration - jsonb_delta's role and the parity finding
Development
- Development - Development setup
- Testing - Testing patterns and procedures
- Architecture - Technical architecture
🎯 Use Cases
Perfect For:
✅ FraiseQL Applications - Real-time GraphQL Cascade with UUID filtering ✅ E-commerce Dashboards - Real-time product aggregations with inventory ✅ Analytics Workloads - Pre-aggregated reporting tables that stay fresh ✅ API Response Caching - JSONB views for fast API responses ✅ Activity Feeds - User timelines with JOINed data ✅ Denormalization - Read-optimized tables without manual cache invalidation
Not Recommended For:
❌ Write-Heavy Tables - If you have >1000 writes/sec per table ❌ Simple Queries - If a regular index works fine ❌ Append-Only Logs - No need for incremental refresh
🤝 Contributing
Contributions welcome! This is a portfolio project, but I'm happy to collaborate:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Development Setup: See docs/development.md
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
⭐ If you find this project interesting, please consider starring it! ⭐
Built with ❤️ and Rust 🦀