Skip to content
barddooPublic
forked from spa-28/pgzx

About

Create PostgreSQL extensions using Zig.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

pgzx logo

License - Apache 2.0  CI Build  

pgzx - Create Postgres Extensions with Zig!

pgzx is a library for developing PostgreSQL extensions written in Zig. It provides utilities (error handling, memory allocators, wrappers) and a development environment that simplify integrating with the Postgres code base.

Why Zig?

Zig is a small, simple language that aims to be a "modern C" with safe memory management, compile-time execution (comptime), and a rich standard library. It speaks the C ABI, works with C pointers and types directly, and can import and translate C headers — so a Zig extension can do anything a C extension can, with a modern language on top.

In practice you still need to understand a lot of Postgres internals, and Postgres leans on macros that cannot always be translated automatically. pgzx provides the Zig modules for those cases.

Examples

The following sample extensions (ordered from simple to complex) show how to use pgzx:

Extension Description
char_count_zig Adds a function that counts how many times a particular character shows up in a string. Shows how to register a function and how to interpret the parameters.
pghostname_zig Adds a function that returns the database server's host name.
pg_audit_zig Inspired by the pgaudit C extension, this one registers callbacks to multiple hooks and uses more advanced error handling and memory allocation patterns.
rational A rational base type with btree and hash operator classes. Shows the type system end to end: pg_type, operators, pg_opclass, casts and how they make indexes and GROUP BY work.
arrays Port of pgrx-examples/arrays. Postgres arrays as Zig slices ([]const i32, []const ?i32, text[]), plus parameter defaults, VARIADIC and pgzx.IntList.
spi Port of pgrx-examples/spi. Queries with arguments, a prepared plan kept for the session, cursors fetched in batches, subtransactions that skip failing rows, and SECURITY DEFINER.
file_io Backend-safe file I/O through the virtual file descriptor layer (pgzx.fd), plus asynchronous VFD reads on PostgreSQL 18 (pgzx.aio).

Docs

The reference documentation is available at here. The examples above are the best place to start; the sections below walk through the most important utilities.

Getting Started

This project uses Nix flakes to manage build dependencies and provide a development shell. A template bootstraps a new extension:

$ mkdir my_extension
$ cd my_extension
$ nix flake init -t github:xataio/pgzx

This creates a working extension named my_extension exporting a hello() function. Its README explains how to enter the shell, build, and test it. Rename the project by updating the files in extension/ and replacing my_extension in README.md, build.zig, build.zig.zon, and the extension SQL file.

The development shell sets the environment used by the project (see devshell.nix): PRJ_ROOT is the project folder, and PG_HOME is the Postgres install prefix (the shell relocates Postgres into ./out and points ./out/default at the active version). For a complete local setup guide see HACKING.md.

Logging and error handling

Postgres error reporting functions provide log levels, formatting, and errors that can be thrown and caught like exceptions. pgzx wraps them for Zig.

Simple logging uses Log, Info, Notice or Warning:

elog.Info(@src(), "input_text: {s}\n", .{input_text});

The @src() built-in records the file location in the error report.

To report errors, use Error (returns a Zig error) or ErrorThrow (throws a Postgres error report):

if (target_char.len > 1) {
    return elog.Error(@src(), "Target char is more than one byte", .{});
}

The module also exposes the C-style API (ereport, errcode, errmsg, ...).

Postgres handles errors with longjmp, which can skip Zig defer/errdefer cleanup. pgzx provides a Zig alternative to PG_TRY:

var errctx = pgzx.err.Context.init();
defer errctx.deinit();
if (errctx.pg_try()) {
    // Zig code that calls Postgres C functions.
} else {
    return errctx.errorValue();
}

This catches errors raised by Postgres functions and returns them as Zig errors, so all defer/errdefer in the callers run. The wrap helper packages this pattern:

try pgzx.err.wrap(myFunction, .{arg1, arg2});

See pgzx.err.Context for details.

Memory context allocators

Postgres uses a memory context system: allocations belong to a context, and freeing a context frees everything in it at once. Contexts are hierarchical, so a child context is freed with its parent.

pgzx wraps contexts as Zig allocators. createAllocSetContext returns a MemoryContextAllocator:

var memctx = try pgzx.mem.createAllocSetContext("zig_context", .{ .parent = pg.CurrentMemoryContext });
const allocator = memctx.allocator();

pg.CurrentMemoryContext is the context of the running query, so memory allocated with allocator is freed when the query finishes. You can also register a callback for when a context is reset or deleted, to release resources tied to it:

try memctx.registerAllocResetCallback(
    queryDesc.*.estate.*.es_query_cxt,
    pgaudit_zig_MemoryContextCallback,
);

Function manager

Register Zig functions so they can be called from SQL with PG_FUNCTION_V1:

comptime {
    pgzx.PG_FUNCTION_V1("my_function", myFunction);
}

Parameters are received from Postgres serialized, and pgzx deserializes them into Zig types automatically.

SQL schema generation

Instead of hand-writing the versioned extension script (<name>--<version>.sql), you can describe your SQL objects in a comptime declaration and let zig build render it. By convention the declaration lives in src/schema.zig and is named pgzx_sql:

const functions = @import("functions.zig");

pub const pgzx_sql = .{
    .functions = .{
        .{ .name = "char_count_zig", .func = functions.char_count_zig },
    },
};

Then enable the schema step in build.zig:

_ = proj.addSteps(.{
    .schema = .{},
    // ...
});

zig build (or zig build sql) compiles a small generator, renders the SQL, and installs it next to the .control file so CREATE EXTENSION picks it up. Argument types are derived from the Zig signatures through the pgzx.datum converters: i32 becomes integer, []const u8 becomes text, and optional arguments do not change the SQL type. A pg.FunctionCallInfo parameter is treated as the fmgr context and is not emitted as an SQL argument.

Per-function options:

Option Meaning
name SQL function name (required).
func The Zig function (required).
volatility pgzx.ddl.Volatility: .@"volatile" (default), .stable, .immutable.
strict Emit STRICT (default false).
parallel pgzx.ddl.Parallel: .unsafe, .restricted, .safe.
args Override argument SQL types, e.g. &.{"text", "integer"}.
returns Override the return SQL type.
comment Emit a COMMENT ON FUNCTION.

Use args/returns for signatures with no direct SQL mapping, such as raw pg.Datum arguments. Keep PG_FUNCTION_V1/PG_EXPORT in main.zig, not in the schema module: the generator is linked as a standalone executable and cannot resolve Postgres server symbols. Objects that are not CREATE FUNCTION (types, operators, operator classes, casts) go in a source-level catalog.sql, which the generator appends after the functions. See examples/rational and examples/sqlfns for the complete pattern.

Testing your extension

pgzx provides pg_regress tests and in-server unit tests, set up through the PGBuild.Project build helper:

const std = @import("std");
const PGBuild = @import("pgzx").Build;

pub fn build(b: *std.Build) void {
    const proj = PGBuild.Project.init(b, .{
        .name = "my_extension",
        .version = .{ .major = 0, .minor = 1 },
        .root_dir = "src/",
        .root_source_file = "src/main.zig",
    });

    _ = proj.addSteps(.{
        .pg_regress = .{
            .db_user = "postgres",
            .db_port = 5432,
            .scripts = &[_][]const u8{"my_extension_test"},
        },
        .unit = .{
            .db_user = "postgres",
            .db_port = 5432,
        },
    });
}

addSteps creates the check, install, pg_regress (when .pg_regress is set) and unit (when .unit is set) build steps, and defines the build_options module that gates testfn.

pg_regress tests work like they do for C extensions: inputs go in sql/, expected outputs in expected/, and run with:

zig build pg_regress

Unit tests run inside Postgres, so they compile in the same environment as the tested code and can call Postgres APIs. Each function whose name starts with test in a registered test suite is a unit test:

comptime {
    pgzx.testing.registerTests(@import("build_options").testfn, .{Tests});
}

registerTests may only be called once per extension; pass multiple suites in the array. Run the tests with:

zig build unit -p $PG_HOME

This builds a dedicated {name}_unit library with testfn = true, deploys it, and calls SELECT run_tests();. The separate library name means the test build never collides with the production extension.

Status/Roadmap

pgzx is under heavy development by the Xata team. Expect breaking changes and potential instability. If you need help, join us on the Xata discord.

  • Utilities
    • Postgres versions (compile and test)
      • Postgres 15
      • Postgres 16
      • Postgres 17
      • Postgres 18
      • Postgres 14
    • Logging
    • Error handling
    • Memory context allocators
    • Function manager
    • SQL/DDL generation
    • Background worker process
    • LWLocks
    • Signals and interrupts
    • String formatting
    • Shared memory
    • SPI
    • GUCs (custom variables)
    • Postgres data structure wrappers:
      • Array based list (List)
        • Pointer list
        • int list
        • oid list
        • ...
      • Single list
      • Double list
      • Hash tables
  • Development environment
    • Download and vendor Postgres source code
    • Compile example extensions against the Postgres source code
    • Build target to run Postgres regression tests
    • Run unit tests in the Postgres environment
    • Provide a standard way to test extensions from separate repos
  • Packaging
    • Add support for Zig packaging

Contributing

For a complete local development guide — creating a local PostgreSQL install, building, testing, debugging, editor setup, and switching Postgres versions — see HACKING.md.

$ nix develop          # enter the development shell
$ ./dev/docker/run.sh  # or use the docker development shell

The examples build and test through each example's ci/run.sh; ./ci/run.sh at the repo root runs all of them.

See also

  • pgrx - Similar project but for Rust, it served as an inspiration for this project.
  • pg_tle - Trusted Language Extensions for PostgreSQL.

License

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

Support

If you have any questions, encounter issues, or need assistance, open an issue in this repository or join our Discord, and our community will be happy to help.


Made with ❤️ by Xata 🦋

About

Create PostgreSQL extensions using Zig.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages