barddoo/pgzx

Create PostgreSQL extensions using Zig.

Zig

3

135 commits

updated Oct 14, 2026

See the code

See what people are saying

SourceMessageScoreDate

Pgzx: Create Postgres Extensions in Zig (Fork)

4

Oct 14, 2026

README

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:

ExtensionDescription
char_count_zigAdds 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_zigAdds a function that returns the database server's host name.
sqlfnsThe ways to export a Zig function to SQL: a raw C export, PG_FUNCTION_V1, and PG_EXPORT for a whole struct or file.
gucRegisters custom GUCs (bool, int, string, enum) in _PG_init, including a check hook that clamps a value, and reads them back into SQL.
bgworkerRegisters a static background worker (pgzx.bgworker.register) that the postmaster launches on the next server start.
pg_audit_zigInspired by the pgaudit C extension, this one registers callbacks to multiple hooks and uses more advanced error handling and memory allocation patterns.
srfPort of pgrx-examples/srf. Set-returning functions with pgzx.srf.SetOf (RETURNS SETOF) and pgzx.srf.Table (RETURNS TABLE), driven by a plain Zig iterator.
arraysPort of pgrx-examples/arrays. Postgres arrays as Zig slices ([]const i32, []const ?i32, text[]), plus parameter defaults, VARIADIC and pgzx.IntList.
spi_sqlUses SPI to run SQL statements; the exported functions double as the regression-test fixture.
spiPort 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.
rationalA 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.
file_ioBackend-safe file I/O through the virtual file descriptor layer (pgzx.fd), plus asynchronous VFD reads on PostgreSQL 18 (pgzx.aio).
compress_zigA Timescale-style columnar compression layer: SPI reads a heap table, encodes it into type-aware batches (plain, rle, delta, delta2, gorilla, dictionary, bit-packed bool) in a sidecar relation, and decodes them back.

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.

Set-returning functions

SETOF and TABLE functions return a pgzx.srf.SetOf(I) or pgzx.srf.Table(I) wrapping an iterator I: any struct with a next(self: *I) method that yields the next row, or null when the set is exhausted. next may also return !?Item to raise a Zig error as a Postgres error.

const Series = struct {
    cur: i64,
    finish: i64,
    step: i64,

    pub fn next(self: *Series) ?i64 {
        if (self.cur > self.finish) return null;
        defer self.cur += self.step;
        return self.cur;
    }
};

pub fn srf_series(start: i64, finish: i64, step: i64) pgzx.srf.SetOf(Series) {
    return .{ .iter = .{ .cur = start, .finish = finish, .step = step } };
}

For Table(I) the item is a struct with one field per column; the field names and the pgzx.datum converters (see SQL schema generation) produce the RETURNS TABLE (...) clause in generated DDL. Use SetOf for a single unnamed column.

Postgres uses the value-per-call protocol: it calls the function once per row. The function body runs only on the first call, in a memory context that lives for the whole set, and the iterator is moved there; later calls run only next. Allocate everything the iterator needs while building it with mem.PGCurrentContextAllocator — next runs in a short-lived per-call context, so memory it allocates is only good for the row it returns. An iterator type may also define fn deinit(self: *I) void, which runs when the set ends or the query stops early (a LIMIT, an error, a cancelled query); it must not allocate or raise errors.

See examples/srf for a complete set of fixtures, including nullable columns and iterators that fail.

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:

OptionMeaning
nameSQL function name (required).
funcThe Zig function (required).
volatilitypgzx.ddl.Volatility: .@"volatile" (default), .stable, .immutable.
strictEmit STRICT (default false).
parallelpgzx.ddl.Parallel: .unsafe, .restricted, .safe.
argsOverride argument SQL types, e.g. &.{"text", "integer"}.
returnsOverride the return SQL type.
commentEmit 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

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.

PostgreSQL 15, 16, 17 and 18 are supported: the examples and unit tests are compiled and run against all four in CI. PostgreSQL 14 is not supported yet.

ROADMAP.md tracks the feature status relative to pgrx:

  • Done — SQL/DDL generation, set-returning functions, composite and custom types, arrays and int/oid lists, operators and casts, SPI, GUCs, background workers, heap tuple access.
  • In progress — the remaining Datum conversions (json/jsonb, numeric, inet/cidr, ranges), a generic hook/callback registry, concurrency (latches, dynamic workers, DSM/DSA), and a full FFI error-safety audit.
  • Planned — triggers, aggregates, zig build package, and PostgreSQL 14 support, among other items.

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 🦋

postgres
postgresql
postgresql-extension
zig

barddoo/pgzx

Create PostgreSQL extensions using Zig.

Zig

3

135 commits

updated Oct 14, 2026

See the code

See what people are saying

SourceMessageScoreDate

Pgzx: Create Postgres Extensions in Zig (Fork)

4

Oct 14, 2026

README

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:

ExtensionDescription
char_count_zigAdds 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_zigAdds a function that returns the database server's host name.
sqlfnsThe ways to export a Zig function to SQL: a raw C export, PG_FUNCTION_V1, and PG_EXPORT for a whole struct or file.
gucRegisters custom GUCs (bool, int, string, enum) in _PG_init, including a check hook that clamps a value, and reads them back into SQL.
bgworkerRegisters a static background worker (pgzx.bgworker.register) that the postmaster launches on the next server start.
pg_audit_zigInspired by the pgaudit C extension, this one registers callbacks to multiple hooks and uses more advanced error handling and memory allocation patterns.
srfPort of pgrx-examples/srf. Set-returning functions with pgzx.srf.SetOf (RETURNS SETOF) and pgzx.srf.Table (RETURNS TABLE), driven by a plain Zig iterator.
arraysPort of pgrx-examples/arrays. Postgres arrays as Zig slices ([]const i32, []const ?i32, text[]), plus parameter defaults, VARIADIC and pgzx.IntList.
spi_sqlUses SPI to run SQL statements; the exported functions double as the regression-test fixture.
spiPort 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.
rationalA 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.
file_ioBackend-safe file I/O through the virtual file descriptor layer (pgzx.fd), plus asynchronous VFD reads on PostgreSQL 18 (pgzx.aio).
compress_zigA Timescale-style columnar compression layer: SPI reads a heap table, encodes it into type-aware batches (plain, rle, delta, delta2, gorilla, dictionary, bit-packed bool) in a sidecar relation, and decodes them back.

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.

Set-returning functions

SETOF and TABLE functions return a pgzx.srf.SetOf(I) or pgzx.srf.Table(I) wrapping an iterator I: any struct with a next(self: *I) method that yields the next row, or null when the set is exhausted. next may also return !?Item to raise a Zig error as a Postgres error.

const Series = struct {
    cur: i64,
    finish: i64,
    step: i64,

    pub fn next(self: *Series) ?i64 {
        if (self.cur > self.finish) return null;
        defer self.cur += self.step;
        return self.cur;
    }
};

pub fn srf_series(start: i64, finish: i64, step: i64) pgzx.srf.SetOf(Series) {
    return .{ .iter = .{ .cur = start, .finish = finish, .step = step } };
}

For Table(I) the item is a struct with one field per column; the field names and the pgzx.datum converters (see SQL schema generation) produce the RETURNS TABLE (...) clause in generated DDL. Use SetOf for a single unnamed column.

Postgres uses the value-per-call protocol: it calls the function once per row. The function body runs only on the first call, in a memory context that lives for the whole set, and the iterator is moved there; later calls run only next. Allocate everything the iterator needs while building it with mem.PGCurrentContextAllocator — next runs in a short-lived per-call context, so memory it allocates is only good for the row it returns. An iterator type may also define fn deinit(self: *I) void, which runs when the set ends or the query stops early (a LIMIT, an error, a cancelled query); it must not allocate or raise errors.

See examples/srf for a complete set of fixtures, including nullable columns and iterators that fail.

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:

OptionMeaning
nameSQL function name (required).
funcThe Zig function (required).
volatilitypgzx.ddl.Volatility: .@"volatile" (default), .stable, .immutable.
strictEmit STRICT (default false).
parallelpgzx.ddl.Parallel: .unsafe, .restricted, .safe.
argsOverride argument SQL types, e.g. &.{"text", "integer"}.
returnsOverride the return SQL type.
commentEmit 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

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.

PostgreSQL 15, 16, 17 and 18 are supported: the examples and unit tests are compiled and run against all four in CI. PostgreSQL 14 is not supported yet.

ROADMAP.md tracks the feature status relative to pgrx:

  • Done — SQL/DDL generation, set-returning functions, composite and custom types, arrays and int/oid lists, operators and casts, SPI, GUCs, background workers, heap tuple access.
  • In progress — the remaining Datum conversions (json/jsonb, numeric, inet/cidr, ranges), a generic hook/callback registry, concurrency (latches, dynamic workers, DSM/DSA), and a full FFI error-safety audit.
  • Planned — triggers, aggregates, zig build package, and PostgreSQL 14 support, among other items.

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 🦋

postgres
postgresql
postgresql-extension
zig