Skip to content

fraiseql/jsonb_ivm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jsonb_ivm - Incremental JSONB View Maintenance

PostgreSQL License Build Status

High-performance PostgreSQL extension for intelligent partial updates of JSONB materialized views in CQRS architectures.

100x faster than traditional full refresh approaches. Perfect for maintaining denormalized JSONB projections when relational data changes.

🎯 Quick Links

📊 The Problem

In CQRS systems with:

  • tb_ tables (OLTP): Normalized relational data optimized for writes
  • tv_ tables (OLAP): Denormalized JSONB projections optimized for reads

Traditional approach - Full Refresh:

-- Slow: Recalculate entire JSONB document on every change
UPDATE tv_orders
SET data = (
    SELECT jsonb_build_object(
        'order_id', o.id,
        'customer', row_to_json(c.*)::jsonb,
        'shipping', row_to_json(s.*)::jsonb,
        'items', (SELECT jsonb_agg(...) FROM order_items...)
        -- 20+ fields rebuilt from scratch
    )
    FROM tb_orders o
    JOIN tb_customers c ON c.id = o.customer_id
    -- ... many more joins ...
)
WHERE id = $1;

Problems:

  • ❌ Slow: Recomputes entire document even if only 1 field changed
  • ❌ Expensive: Multiple joins for every update
  • ❌ Resource intensive: High CPU and memory usage
  • ❌ Doesn't scale: Performance degrades with document complexity

💡 The Solution

jsonb_ivm provides intelligent partial updates:

-- Fast: Update only the changed portion
UPDATE tv_orders
SET data = jsonb_merge_at_path(
    data,
    (SELECT row_to_json(c.*)::jsonb FROM tb_customers c WHERE id = $1),
    ARRAY['customer']  -- Only update this nested object
)
WHERE customer_id = $1;

Benefits:

  • 100x faster: Partial updates vs full refresh
  • 2000x faster for no-op detection (no changes)
  • < 1ms change detection on 100-key JSONB objects
  • Incremental: Update only what changed
  • Automatic: Trigger-driven, zero manual intervention
  • Scalable: Performance independent of document size

🚀 Quick Start

Installation

# Build from source
cd jsonb_ivm
make clean && make
sudo make install

# Load extension in your database
psql -d your_database -c "CREATE EXTENSION jsonb_ivm;"

5-Minute Example

Setup: Create tb_ and tv_ tables with automatic incremental updates:

-- 1. Create CQRS tables
-- Command side (write model) - normalized tables with tb_ prefix
CREATE TABLE tb_customers (
    id BIGINT PRIMARY KEY,
    name TEXT,
    email TEXT,
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Query side (read model) - denormalized JSONB with tv_ prefix
CREATE TABLE tv_orders (
    id BIGINT PRIMARY KEY,
    data JSONB  -- Denormalized order + customer data
);

-- 2. Create trigger for incremental sync from command → query side
CREATE OR REPLACE FUNCTION sync_customer_to_tv_orders()
RETURNS TRIGGER AS $$
BEGIN
    -- Incrementally update only the 'customer' portion of affected orders
    -- Updates query side (tv_orders) when command side (tb_customers) changes
    UPDATE tv_orders
    SET data = jsonb_merge_at_path(
        data,
        jsonb_build_object(
            'name', NEW.name,
            'email', NEW.email,
            'updated_at', NEW.updated_at
        ),
        ARRAY['customer']  -- Scope: only update nested customer object
    )
    WHERE (data->>'customer_id')::BIGINT = NEW.id;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER tb_customer_to_tv_orders
AFTER UPDATE ON tb_customers
FOR EACH ROW
EXECUTE FUNCTION sync_customer_to_tv_orders();

-- 3. Test it!
-- Insert test data
INSERT INTO tb_customers VALUES (1, 'John Doe', 'john@example.com');
INSERT INTO tv_orders VALUES (
    100,
    '{"order_id": 100, "customer_id": 1, "customer": {"name": "John Doe", "email": "john@example.com"}, "items": [...]}'::jsonb
);

-- Update command side - watch the trigger sync to query side automatically!
UPDATE tb_customers SET email = 'newemail@example.com' WHERE id = 1;

-- Verify: Only 'customer.email' was updated in tv_orders, rest unchanged
SELECT data->'customer'->>'email' FROM tv_orders WHERE id = 100;
-- → 'newemail@example.com'

Result: Customer email change propagated to all orders instantly, updating only the nested customer object without recalculating the entire order document!

🎁 Core Features

1. Change Detection (jsonb_detect_changes)

Detects which keys changed between two JSONB objects:

SELECT jsonb_detect_changes(
    '{"name": "John", "email": "old@example.com", "phone": "555-1234"}'::jsonb,
    '{"name": "John", "email": "new@example.com", "phone": "555-1234"}'::jsonb
);
-- → ["email"]  (only email changed)

Use case: Intelligent triggers that only update affected downstream views.

2. Shallow Merge (jsonb_merge_shallow)

Merges top-level keys from source into target:

SELECT jsonb_merge_shallow(
    '{"a": 1, "b": 2, "c": 3}'::jsonb,
    '{"b": 99, "d": 4}'::jsonb
);
-- → {"a": 1, "b": 99, "c": 3, "d": 4}

Use case: Simple top-level field updates.

3. Nested Merge (jsonb_merge_at_path)

Merges into nested objects at specified paths:

SELECT jsonb_merge_at_path(
    '{"order_id": 123, "customer": {"name": "John", "email": "old@example.com"}}'::jsonb,
    '{"email": "new@example.com", "phone": "555-1234"}'::jsonb,
    ARRAY['customer']
);
-- → {"order_id": 123, "customer": {"name": "John", "email": "new@example.com", "phone": "555-1234"}}

Use case: Scoped partial updates of nested dimension data.

4. Scope Building (core.build_scope_from_fields)

Configuration-driven scope resolution:

-- Configure field → scope mappings
INSERT INTO core.field_scope_mapping VALUES
    ('order', 'customer_id', ARRAY['customer'], 'Customer details'),
    ('order', 'shipping_address_id', ARRAY['shipping'], 'Shipping address');

-- Resolve scopes from changed fields
SELECT * FROM core.build_scope_from_fields(
    'order',
    ARRAY['customer_id', 'shipping_address_id']
);
-- → [['customer'], ['shipping']]

Use case: Automatic scope determination for complex entities.

📈 Performance

Benchmarks on PostgreSQL 17.5, typical e-commerce order JSONB (100 keys, nested customer/shipping/items):

Operation Traditional Full Refresh jsonb_ivm Partial Update Speedup
Change Detection N/A < 0.5ms -
Single Field Update ~50ms (20+ joins) ~0.5ms 100x faster
Nested Object Update ~50ms ~1ms 50x faster
No-Op Detection ~50ms (full recalc) ~0.025ms (early exit) 2000x faster

Key insight: Performance is independent of JSONB document size - only depends on the size of the changed portion.

📚 Documentation

🎯 Use Cases

E-commerce Order Tracking

Maintain denormalized order documents with customer, shipping, payment, and item details. When customer updates their email, update only the customer portion of all their orders.

Multi-Tenant SaaS

Track tenant configurations in JSONB. When a feature flag changes, update only that portion of the tenant config without rebuilding the entire settings object.

IoT Event Processing

Store device telemetry in JSONB with nested device metadata, location, and sensor readings. When device moves, update only location portion.

Social Media Feeds

Denormalized post documents with author, likes, comments. When author changes profile picture, update only author portion of all their posts.

See examples/ for complete working examples.

🔧 Requirements

  • PostgreSQL: 13 or later (tested on 13-17)
  • OS: Linux, macOS (Windows untested)
  • Compiler: GCC 4.9+ or Clang 3.4+
  • PostgreSQL Dev Headers: postgresql-server-dev-* package

🛠️ Advanced Usage

Batch Updates with Scope

-- Update multiple orders efficiently when customer changes
UPDATE tv_orders
SET data = jsonb_merge_at_path(
    data,
    (SELECT data FROM tv_customers WHERE id = fk_customer_id),
    ARRAY['customer']
)
WHERE id = ANY(
    SELECT order_id FROM orders_to_update
);

Conditional Updates Based on Changes

CREATE OR REPLACE FUNCTION smart_update_trigger()
RETURNS TRIGGER AS $$
DECLARE
    changed_keys text[];
BEGIN
    -- Detect what changed
    changed_keys := jsonb_detect_changes(
        row_to_json(OLD)::jsonb,
        row_to_json(NEW)::jsonb,
        ARRAY['name', 'email', 'phone']  -- Only check these fields
    );

    -- Only update if something relevant changed
    IF cardinality(changed_keys) > 0 THEN
        -- Perform incremental update
        UPDATE tv_customers
        SET data = jsonb_merge_shallow(data, row_to_json(NEW)::jsonb)
        WHERE id = NEW.id;
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Multi-Level Nested Paths

-- Update deeply nested configuration
SELECT jsonb_merge_at_path(
    data,
    '{"enabled": true, "threshold": 100}'::jsonb,
    ARRAY['organization', 'settings', 'notifications']
);

🧪 Testing

# Run comprehensive test suite
psql -d postgres -f test_change_detection.sql  # 14 tests
psql -d postgres -f test_scope_building.sql    # 10 tests
psql -d postgres -f test_scope_utilities.sql   # 7 tests

# Run performance benchmarks
psql -d postgres -f benchmark.sql
psql -d postgres -f benchmark_nested.sql

# All tests passing: 31/31 ✅

🤝 Contributing

Contributions welcome! Please see CONTRIBUTING.md for guidelines.

Reporting Issues:

Development:

# Build and test
make clean && make
sudo make install
make test

📜 License

This project is licensed under the PostgreSQL License - see LICENSE file for details.

🙏 Acknowledgments

  • Author: Lionel Hamayon
  • Organization: fraiseql
  • Built with disciplined TDD methodology
  • Inspired by real-world CQRS architecture challenges
  • PostgreSQL community for excellent extension APIs

🗺️ Roadmap

  • v1.0: Shallow merge (jsonb_merge_shallow, jsonb_merge_at_path)
  • v1.1: Change detection and scope building system
  • v1.2: Batch utility functions and helper procedures
  • v1.3: Deep recursive merge (if needed)
  • v2.0: Dependency tracking and automatic trigger generation

📞 Support


Built with PostgreSQL ❤️ | Production-ready | Battle-tested with 31 comprehensive tests

Make your CQRS views fly! 🚀

About

High-performance PostgreSQL extension for incremental JSONB view maintenance in CQRS architectures

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Sponsor this project

Packages

No packages published