pg.zig

repository·master·Indexed 20 days ago

https://github.com/karlseguin/pg.zig

A native PostgreSQL driver for the Zig programming language. It provides high-performance database access featuring connection pooling via pg.Pool, prepared statements with Stmt, and type-safe row mapping. The library supports strict type mapping for PostgreSQL columns, array iteration using pg.Iterator(T), and an optimized Mapper for converting rows into Zig structs.

Tokens
6.3K
Snippets
20
Records
27
Agent score
20%

What's inside pg.zig

  1. Map rows to structs using Mapper

    master

    A Mapper is an optimized way to convert rows into a custom Zig struct T. It is significantly more efficient than using row.to with name-based mapping because it performs the name-to-index lookup only once.

    Requirements:

    • The query must be executed with {.column_names = true} or the column_names build option must be set.

    Mapping Rules:

    • Columns with no matching field in the struct are ignored.
    • Fields with no matching column are set to their default value. If no default is defined, mapper.next() returns error.FieldColumnMismatch.

    Configuration Options (ToOpts):

    • dupe: If true, string columns are duplicated using an internal arena. This allows non-scalar values to persist until the row/result is deinitialized.
    • allocator: An explicit allocator for duplicating non-scalar values. Setting this implies dupe = true.
    • map: Determines mapping strategy. .ordinal (default) matches by position; .name matches by column name.
    const User = struct {
      id: i32,
      name: []const u8,
    };
    
    var result = try conn.queryOpts("select id, name from users", .{}, .{.column_names = true});
    defer result.deinit();
    
    var mapper = result.mapper(User, .{});
    while (try mapper.next()) |user| {
      // use: user.id and user.name
    }
  2. Install pg.zig

    master

    To use pg.zig in your Zig project, follow these two steps:

    1. Add the dependency to your build.zig.zon using zig fetch:

    2. Register the pg module in your build.zig so it can be imported into your source code.

    zig fetch --save git+https://github.com/karlseguin/pg.zig#master
    const pg_module = b.dependency("pg", .{ .{} }).module("pg");
    
    const exe = b.addExecutable(.{
      .name = "example",
      // ...
      .imports = &.{ 
        .{ .name = "pg", .module = pg_module },
      },
    });
  3. Configure TLS support via OpenSSL

    master

    TLS is supported through OpenSSL. To enable it, you must configure the pg dependency in your build.zig to include OpenSSL settings.

    Setup Steps:

    1. In build.zig, provide .openssl_lib_name (e.g., "ssl" or "openssl"). You may also need to provide .openssl_lib_path and .openssl_include_path if OpenSSL is in a non-standard location.
    2. Set the connection's tls option to .required or provide a specific configuration like .{ .verify_full = null } or .{ .verify_full = "/path/to/root.crt" }.

    Debugging TLS:

    • Define pub const pg_stderr_tls = true; in your main file to print TLS errors to stderr.
    • Call pg.printSSLError(); in a catch block to attempt to print SSL-specific error details.
    // build.zig configuration
    const pg_module = b.dependency("pg", .{
      .target = target,
      .optimize = optimize,
      .openssl_lib_name = @as([]const u8, "ssl"),
      .openssl_lib_path = std.Build.LazyPath{.cwd_relative = "/path/to/openssl/lib"},
      .openssl_include_path = std.Build.LazyPath{.cwd_relative = "/path/to/openssl/include"},
    }).module("pg");
    
    // Connection configuration
    var pool = try pg.Pool.init(allocator, .{
      .connect = .{ .port = 5432, .host = "ip_or_hostname", .tls = .{.verify_full = null}},
      .auth = .{ ... },
      .size = 5,
    });
  4. Implement Listen/Notify with pg.Listener

    master

    You can use PostgreSQL's LISTEN and NOTIFY features using the pg.Listener type. You can create a listener directly (similar to a standard connection) or via a pg.Pool using pool.newListener().

    Key behaviors:

    • Blocking: listener.next() blocks until a notification is received or an error occurs.
    • Timeouts: You can specify a timeout in milliseconds when calling listen. If no message is received within that window, next() returns null and listener.err will contain error.WouldBlock.
    • No Auto-Reconnect: Listeners do not automatically reconnect. To handle disconnects, wrap your listener logic in a while (true) loop.
    • Stopping: Calling listener.stop() from any thread will cause next() to return null and set listener.stopped to true.
    // Creating a listener directly
    var listener = try pg.Listener.open(allocator, .{
      .host = "127.0.0.1",
      .port = 5432,
    });
    defer listener.deinit();
    
    try listener.auth(.{
      .username = "leto",
      .password = "ghanima",
      .database = "caladan",
    });
    
    try listener.listen("chan_1", .{});
    
    while (listener.next()) |notification| {
      std.debug.print("Channel: {s}\nPayload: {s}", .{notification.channel, notification.payload});
    }
    
    // Handling errors in the listener
    switch (listener.err.?) {
      .pg => |pg| std.debug.print("{s}\n", .{pg.message}),
      .err => |err| std.debug.print("{s}\n", .{@errorName(err)}),
    }
  5. Handle PostgreSQL errors via `conn.err`

    master

    Because Zig error sets do not support arbitrary payloads, pg.zig returns a generic error.PG when a database error occurs. To access the specific details of the error (like the message or error code), you must check the conn.err field on the connection object.

    Error Handling Pattern:

    1. Catch the error from your query execution.
    2. Check if the error is error.PG.
    3. If so, inspect conn.err for the error details.

    Note: conn.err is automatically reset when a connection is acquired from a pool, so it will not contain stale errors from previous users of that connection.

    _ = conn.exec("drop table x", .{}) catch |err| {
      if (err == error.PG) {
        if (conn.err) |pge| {
          std.log.err("PG {s}\n", .{pge.message});
        }
      }
      return err;
    };
  6. Enable column names by default in pg.zig

    master

    To avoid manually setting the column_names option on every query, you can enable it globally at the build level by passing column_names = true to the b.dependency("pg", ...) call in your build.zig.

    const pg_module = b.dependency("pg", .{
      .target = target,
      .optimize = optimize,
      .column_names = true,
    }).module("pg");
  7. Bind and read JSON/JSONB

    master

    The library supports both serialized strings and structured data for JSON/JSONB columns.

    Binding:

    • Serialized: Provide a []u8 containing the JSON string.
    • Structured: Provide a Zig struct, which the library will serialize using std.json.stringify.
    • Arrays: When binding to an array of JSON/JSONB, you must provide an array of already serialized []u8 values; automatic serialization of structs within arrays is not supported.

    Reading: Reading a JSON/JSONB column as []u8 returns the serialized JSON string.

  8. Iterate over Array columns using pg.Iterator(T)

    master

    To handle PostgreSQL array columns, use row.get(pg.Iterator(T), col). This returns an iterator that can be traversed using .next().

    Supported Array Types:

    • u8, ?u8 $\rightarrow$ char[]
    • i16, ?i16 $\rightarrow$ smallint[]
    • i32, ?i32 $\rightarrow$ int[]
    • i64, ?i64 $\rightarrow$ bigint[] or timestamp(tz)[]
    • f32, ?f32 $\rightarrow$ float4
    • f64, ?f64 $\rightarrow$ float8
    • bool, ?bool $\rightarrow$ bool[]
    • []const u8, []?const u8 $\rightarrow$ text[], char(n)[], bytea[], uuid[], json[], jsonb[]
    • pg.Numeric and pg.Cidr

    Iterator Methods:

    • next() ?T: Returns the next value or null if the end is reached.
    • alloc(it, allocator): Allocates a slice and populates it with all values. Note: if the type is a string, the caller is responsible for freeing both the slice and the string values.
    • fill(it, into): Fills an existing slice into with values from the iterator. This is faster than multiple next() calls.
    var names = try row.get(pg.Iterator([]const u8), 0);
    while (names.next()) |name| {
      // process name
    }
  9. Bind and read UUIDs

    master

    UUIDs can be handled using []u8 slices.

    Binding: When binding to a UUID column, provide either:

    • A 16-byte slice (raw binary).
    • A 36-byte hex-encoded string.

    Reading: When reading a UUID column as []u8, the library always returns the 16-byte raw binary representation. If you need the hex-encoded string, use the pg.uuidToHex() helper.

    // If you read a UUID as []u8, it is 16 bytes. 
    // Convert to hex if needed:
    const hex_uuid = pg.uuidToHex(binary_uuid_slice); // returns ![36]u8
  10. Iterate through query results with pg.Result

    master

    When using conn.query, you receive a pg.Result. To process the data, use the next() method to iterate through rows.

    Important Lifecycle Rules:

    • Always call defer result.deinit() to release resources.
    • If you do not iterate through the entire result set using next() until it returns null, you must call result.drain() before deinit(). This is because deinit() cannot handle the errors that might occur during a drain operation.

    Example Iteration:

    var result = try pool.query("select id, name from users where power > $1", .{9000});
    defer result.deinit();
    
    while (try result.next()) |row| {
      const id = try row.get(i32, 0);
      const name = try row.get([]u8, 1);
    }
  11. Use Pool convenience methods for single queries

    master

    The Pool provides wrapper methods that handle acquire and release automatically for single-query operations:

    • pool.exec: Acquires, executes, and releases the connection. Returns the number of rows affected.
    • pool.query / pool.queryOpts: Acquires and executes the query. The connection is automatically returned to the pool when result.deinit() is called. Note: This automatic release is a special behavior of the pool wrapper; if you call conn.query directly, you must release the connection manually.
    • pool.row / pool.rowOpts: Acquires and executes the query for a single row. The connection is automatically returned to the pool when row.deinit() is called.
  12. Use Prepared Statements with Stmt

    master

    For queries requiring parameters or high flexibility (like dynamic SQL), use the Stmt API.

    Workflow:

    1. Initialize a statement with Stmt.init(conn, opts).
    2. Prepare the SQL using stmt.prepare(sql, null) or conn.prepare(sql).
    3. Bind parameters using stmt.bind(value).
    4. Execute the statement using stmt.execute().

    Lifecycle Management:

    • Use errdefer stmt.deinit() immediately after initialization.
    • Crucial: Do NOT call stmt.deinit() if stmt.execute() returns a valid Result. Once a Result is returned, the statement is considered invalid. Only call deinit() if execute() returns an error or is never called.
    var stmt = try Stmt.init(conn, opts);
    errdefer stmt.deinit();
    
    try stmt.prepare("SELECT * FROM users WHERE id = $1", null);
    try stmt.bind(user_id);
    const result = try stmt.execute();
    // result is now owned by the caller; stmt is invalid.