Installation Guide

Prerequisites

RequirementVersion
PostgreSQL18.x

Building from source additionally requires Rust 1.82+ and pgrx 0.17.x. Pre-built release artifacts only need a running PostgreSQL 18.x instance.


Installing from a Pre-built Release

1. Download the release archive

Download the archive for your platform from the GitHub Releases page:

PlatformArchive
Linux x86_64pg_stream-<ver>-pg18-linux-amd64.tar.gz
macOS Apple Siliconpg_stream-<ver>-pg18-macos-arm64.tar.gz
Windows x64pg_stream-<ver>-pg18-windows-amd64.zip

Optionally verify the checksum against SHA256SUMS.txt from the same release:

sha256sum -c SHA256SUMS.txt

2. Extract and install

Linux / macOS:

tar xzf pg_stream-0.1.1-pg18-linux-amd64.tar.gz
cd pg_stream-0.1.1-pg18-linux-amd64

sudo cp lib/*.so  "$(pg_config --pkglibdir)/"
sudo cp extension/*.control extension/*.sql "$(pg_config --sharedir)/extension/"

Windows (PowerShell):

Expand-Archive pg_stream-0.1.1-pg18-windows-amd64.zip -DestinationPath .
cd pg_stream-0.1.1-pg18-windows-amd64

Copy-Item lib\*.dll  "$(pg_config --pkglibdir)\"
Copy-Item extension\* "$(pg_config --sharedir)\extension\"

3. Using with CloudNativePG (Kubernetes)

pg_stream is distributed as an OCI extension image for use with CloudNativePG Image Volume Extensions.

Requirements: Kubernetes 1.33+, CNPG 1.28+, PostgreSQL 18.

# Pull the extension image
docker pull ghcr.io/grove/pg_stream-ext:0.1.1

See cnpg/cluster-example.yaml and cnpg/database-example.yaml for complete Cluster and Database deployment examples.

4. Local Docker development (without Kubernetes)

For local development without Kubernetes, install the extension files manually into a standard PostgreSQL container from a release archive:

# Extract extension files from the release archive
tar xzf pg_stream-0.1.1-pg18-linux-amd64.tar.gz
cd pg_stream-0.1.1-pg18-linux-amd64

# Run PostgreSQL with the extension mounted
docker run --rm \
  -v $PWD/lib/pg_stream.so:/usr/lib/postgresql/18/lib/pg_stream.so:ro \
  -v $PWD/extension/:/tmp/ext/:ro \
  -e POSTGRES_PASSWORD=postgres \
  postgres:18.1 \
  sh -c 'cp /tmp/ext/* /usr/share/postgresql/18/extension/ && \
         exec postgres -c shared_preload_libraries=pg_stream'

Building from Source

1. Install Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

2. Install pgrx

cargo install --locked cargo-pgrx --version 0.17.0
cargo pgrx init --pg18 $(pg_config --bindir)/pg_config

3. Build the Extension

# Development build (faster compilation)
cargo pgrx install --pg-config $(pg_config --bindir)/pg_config

# Release build (optimized, for production)
cargo pgrx install --release --pg-config $(pg_config --bindir)/pg_config

# Package for deployment (creates installable artifacts)
cargo pgrx package --pg-config $(pg_config --bindir)/pg_config

PostgreSQL Configuration

Add the following to postgresql.conf before starting PostgreSQL:

# Required — loads the extension shared library at server start
shared_preload_libraries = 'pg_stream'

# Recommended — must accommodate scheduler + refresh workers
max_worker_processes = 8

Note: wal_level = logical and max_replication_slots are not required. The extension uses lightweight row-level triggers for CDC, not logical replication.

Restart PostgreSQL after modifying these settings:

pg_ctl restart -D /path/to/data
# or
systemctl restart postgresql

Extension Installation

Connect to the target database and run:

CREATE EXTENSION pg_stream;

This creates:

  • The pgstream schema with catalog tables and SQL functions
  • The pgstream_changes schema for change buffer tables
  • Event triggers for DDL tracking
  • The pgstream.pg_stat_stream_tables monitoring view

Verification

After installation, verify everything is working:

-- Check the extension version
SELECT extname, extversion FROM pg_extension WHERE extname = 'pg_stream';

-- Or get a full status overview (includes version, scheduler state, stream table count)
SELECT * FROM pgstream.pgs_status();

Inspecting the installation

-- Check the installed version
SELECT extversion FROM pg_extension WHERE extname = 'pg_stream';

-- Check which schemas were created
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name IN ('pgstream', 'pgstream_changes');

-- Check all registered GUC variables
SHOW pg_stream.enabled;
SHOW pg_stream.scheduler_interval_ms;
SHOW pg_stream.max_concurrent_refreshes;

-- Check the scheduler background worker is running
SELECT * FROM pgstream.pgs_status();

-- List all stream tables
SELECT pgs_schema, pgs_name, status, refresh_mode, is_populated
FROM pgstream.pgs_stream_tables;

-- Check that the shared library loaded correctly
SELECT * FROM pg_extension WHERE extname = 'pg_stream';

-- Verify the catalog tables exist
SELECT tablename
FROM pg_tables
WHERE schemaname = 'pgstream'
ORDER BY tablename;

Quick functional test

CREATE TABLE test_source (id INT PRIMARY KEY, val TEXT);
INSERT INTO test_source VALUES (1, 'hello');

SELECT pgstream.create_stream_table(
    'test_st',
    'SELECT id, val FROM test_source',
    '1m',
    'FULL'
);

SELECT * FROM test_st;
-- Should return: 1 | hello

-- Clean up
SELECT pgstream.drop_stream_table('test_st');
DROP TABLE test_source;

Uninstallation

-- Drop all stream tables first
SELECT pgstream.drop_stream_table(pgs_schema || '.' || pgs_name)
FROM pgstream.pgs_stream_tables;

-- Drop the extension
DROP EXTENSION pg_stream CASCADE;

Remove pg_stream from shared_preload_libraries in postgresql.conf and restart PostgreSQL.

Troubleshooting

Unit tests crash on macOS 26+ (symbol not found in flat namespace)

macOS 26 (Tahoe) changed dyld to eagerly resolve all flat-namespace symbols at binary load time. pgrx extensions reference PostgreSQL server-internal symbols (e.g. CacheMemoryContext, SPI_connect) via the -Wl,-undefined,dynamic_lookup linker flag. These symbols are normally provided by the postgres executable when the extension is loaded as a shared library — but for cargo test --lib there is no postgres process, so the test binary aborts immediately:

dyld[66617]: symbol not found in flat namespace '_CacheMemoryContext'

This affects local development only — integration tests, E2E tests, and the extension itself running inside PostgreSQL are unaffected.

The fix is built into the just test-unit recipe. It automatically:

  1. Compiles a tiny C stub library (scripts/pg_stub.ctarget/libpg_stub.dylib) that provides NULL/no-op definitions for the ~28 PostgreSQL symbols.
  2. Compiles the test binary with --no-run.
  3. Runs the binary with DYLD_INSERT_LIBRARIES pointing to the stub.

The stub is only built on macOS 26+. On Linux or older macOS, just test-unit runs cargo test --lib directly with no changes.

Note: The stub symbols are never called — unit tests exercise pure Rust logic only. If a test accidentally calls a PostgreSQL function it will crash with a NULL dereference (the desired fail-fast behavior).

If you run unit tests without just (e.g. directly via cargo test --lib), you can use the wrapper script instead:

./scripts/run_unit_tests.sh pg18

# With test name filter:
./scripts/run_unit_tests.sh pg18 -- test_parse_basic

Extension fails to load

Ensure shared_preload_libraries = 'pg_stream' is set and PostgreSQL has been restarted (not just reloaded). The extension requires shared memory initialization at startup.

Background worker not starting

Check that max_worker_processes is high enough to accommodate the scheduler worker plus any refresh workers. The default of 8 is usually sufficient.

Check logs for details

The extension logs at various levels. Enable debug logging for more detail:

SET client_min_messages TO debug1;