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.
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.
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). |
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.
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.
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.
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,
);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.
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.
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_regressUnit 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_HOMEThis 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.
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
- Array based list (List)
- Postgres versions (compile and test)
- 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
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.
- pgrx - Similar project but for Rust, it served as an inspiration for this project.
- pg_tle - Trusted Language Extensions for PostgreSQL.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
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 🦋
