karlseguin / pg.zig

Native PostgreSQL driver / client for Zig
MIT License
158 stars 9 forks source link

Segfault trying to read query results `result.next()` #14

Closed richard-powers closed 3 months ago

richard-powers commented 3 months ago

Zig version: 0.11.0

build.zig.zon:

.{
    .name = "test",
    .version = "0.0.1",

    .dependencies = .{
    .pg = .{
            .url = "https://github.com/karlseguin/pg.zig/archive/zig-0.11.tar.gz",
            .hash = "1220e19021af49c72e97c814e317aa0e567c655fdb42fa725d0c7f8a3046506df151"
        }
    }
}

I've created a min-repro:

const std = @import("std");
const pg = @import("pg");
const Pool = pg.Pool;
const Result = pg.Result;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{
        .thread_safe = true,
    }){};
    var allocator = gpa.allocator();

    var pool = try Pool.init(allocator, .{
        .size = 10,
        .connect = .{
            .host = "myhost",
            .port = 6543,
        },
        .auth = .{
            .username = "user",
            .database = "postgres",
            .password = "pass",
            .timeout = 10_000,
        },
    });
    defer pool.deinit();

    var result = try query(
        pool,
        "SELECT * FROM public.\"Product\" p WHERE p.\"companyId\"=$1;",
        .{1},
    );
    defer result.deinit();

    while (try result.next()) |row| {
        std.debug.print("{}\n", .{row});
    }
}

pub fn query(pool: *Pool, sql: []const u8, values: anytype) !Result {
    const conn = try pool.acquire();
    defer conn.deinit();

    var result = try conn.query(sql, values);
    defer result.deinit();
    return result;
}

zig build run output:

Segmentation fault at address 0x7373de80f006
/home/user/zig/0.11.0/files/lib/std/mem.zig:1430:35: 0x3a3329 in readIntNative__anon_8428 (test)
    return @as(*align(1) const T, @ptrCast(bytes)).*;
                                  ^
/home/user/zig/0.11.0/files/lib/std/mem.zig:1438:35: 0x367420 in readIntForeign__anon_5964 (test)
    return @byteSwap(readIntNative(T, bytes));
                                  ^
/home/user/.cache/zig/p/1220e19021af49c72e97c814e317aa0e567c655fdb42fa725d0c7f8a3046506df151/src/reader.zig:236:34: 0x3677f1 in buffered (test)
   const len = std.mem.readIntBig(u32, buf[start+1..len_end][0..4]);
                                 ^
/home/user/.cache/zig/p/1220e19021af49c72e97c814e317aa0e567c655fdb42fa725d0c7f8a3046506df151/src/reader.zig:140:24: 0x367b6c in next (test)
   return self.buffered(self.pos) orelse self.read();
                       ^
/home/user/.cache/zig/p/1220e19021af49c72e97c814e317aa0e567c655fdb42fa725d0c7f8a3046506df151/src/conn.zig:476:27: 0x367cd2 in read (test)
   const msg = reader.next() catch |err| {
                          ^
/home/user/.cache/zig/p/1220e19021af49c72e97c814e317aa0e567c655fdb42fa725d0c7f8a3046506df151/src/result.zig:75:34: 0x37aac9 in next (test)
  const msg = try self._conn.read();
                                 ^
/home/user/programming/zig/test/src/main.zig:34:27: 0x37b232 in main (test)
    while (try result.next()) |row| {
                          ^
/home/user/zig/0.11.0/files/lib/std/start.zig:574:37: 0x2a1d8e in posixCallMainAndExit (test)
            const result = root.main() catch |err| {
                                    ^
/home/user/zig/0.11.0/files/lib/std/start.zig:243:5: 0x2a1871 in _start (test)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
run test: error: the following command terminated unexpectedly:
/home/user/programming/zig/test/zig-out/bin/test
Build Summary: 3/5 steps succeeded; 1 failed (disable with --summary none)
run transitive failure
└─ run test failure
error: the following build command failed with exit code 1:
/home/user/programming/zig/test/zig-cache/o/27bb83cfc76fda6c71d3b8afdfe5a95e/build /home/user/zig/0.11.0/files/zig /home/user/programming/zig/test /home/user/programming/zig/test/zig-cache /home/user/.cache/zig run

For more info that may be helpful, there is just 1 row in the table. The query is correct SELECT * FROM public."Product" p WHERE p."companyId"=1, and pg.zig correctly reports back the number of columns. It just segfaults on result.next()

richard-powers commented 3 months ago

It seems my main problem was misunderstanding that Result could not be used if the connection was closed, I got this working by keeping the connection open until I was finished with the Result. Is this intended?

karlseguin commented 3 months ago

Very much so, yes. The connection holds the socket that's used to communicate with PostgreSQL. There's more back and forth, but for simplicity, when you send a query and read a response, you essentially write the query to the socket, e.g. "select * from products", and then read the response from the socket. In order not to keep the connection alive for this would involve reading the entire response in memory. For a large result set this might take hundreds of thousands of megabytes.

You could, pass the socket to the result, make it own the socket, so that the query can be read from the socket without the connection being valid. But it's pretty uncommon, I think, that people just want to run 1 query. Typically, connections are re-used (as part of a pool, which pg.zig offers), because opening and closing connections is relatively expensive - better to keep them open and re-use them.

karlseguin commented 3 months ago

If you're using the pool, then you can call pool.query(...) for one-off queries. In that case, the returned result "owns" the connection, so that when you call result.deinit(), the connection will automatically be returned to the pool.

richard-powers commented 3 months ago

I believe pool.query doesn't exist on the version I'm using, but that's really useful information. Thanks!