const std = @import("std"); const pq = @cImport( @cInclude("libpq-fe.h"), ); const Db = @import("main.zig"); const OpenError = Db.OpenError; const PrepareError = Db.PrepareError; const StepError = Db.StepError; const ColumnError = Db.ColumnError; const log = @import("log.zig").scoped_log_t(.postgres); /// Postgres implementation /// Single persistent connection implementation of postgres via libpq pub const Postgres = @This(); allocator: std.mem.Allocator, conn: *pq.PGconn, /// Connect to a postgres database. URL is format accepted by libpq pub fn open(allocator: std.mem.Allocator, url: [:0]const u8) OpenError!Db { if (pq.PQisthreadsafe() == 0) { log.err("Postgres#open: PQisthreadsafe returned 0, can't use libpq in this program", .{}); return OpenError.NotThreadSafe; } var maybe_conn: ?*pq.PGconn = pq.PQconnectdb(url); if (pq.PQstatus(maybe_conn) != pq.CONNECTION_OK) { log.err("Postgres#open: PQstatus returned error {}: {s}", .{ pq.PQstatus(maybe_conn), pq.PQerrorMessage(maybe_conn) }); return OpenError.Failed; } var pg = try allocator.create(Postgres); pg.allocator = allocator; pg.conn = maybe_conn.?; return Db{ .ptr = pg, .vtable = .{ .prepare = prepare, .step = step, .column_i64 = column_i64, .column_slice_const_u8 = column_slice_const_u8, .close_stmt = close_stmt, .close_db = close_db, } }; } fn prepare(db: *anyopaque, query: [:0]const u8) PrepareError!*anyopaque { var self: *Postgres = @alignCast(@ptrCast(db)); var pg_stmt = try self.allocator.create(PgStmt); pg_stmt.* = .{ .query = query, .params = std.ArrayList([*c]const u8).init(self.allocator), }; return pg_stmt; } const PgStmt = struct { query: [:0]const u8, params: std.ArrayList([*c]const u8), c_res: ?*pq.PGresult = null, did_exec: bool = false, n_tuples: ?c_int = null, n_fields: ?c_int = null, res_index: c_int = -1, }; // TODO // fn bind fn step(db: *anyopaque, stmt: *anyopaque) StepError!bool { var self: *Postgres = @alignCast(@ptrCast(db)); var pgstmt: *PgStmt = @alignCast(@ptrCast(stmt)); if (!pgstmt.did_exec) { const params = try pgstmt.params.toOwnedSlice(); pgstmt.c_res = pq.PQexecParams(self.conn, pgstmt.query, @intCast(params.len), null, params.ptr, null, null, 0); const rs = pq.PQresultStatus(pgstmt.c_res); if (rs != pq.PGRES_TUPLES_OK and rs != pq.PGRES_SINGLE_TUPLE and rs != pq.PGRES_COMMAND_OK) { log.err("PQresultStatus {} error: {s}", .{ rs, pq.PQerrorMessage(self.conn) }); return StepError.Failed; } pgstmt.n_tuples = pq.PQntuples(pgstmt.c_res); pgstmt.n_fields = pq.PQnfields(pgstmt.c_res); pgstmt.did_exec = true; } pgstmt.res_index = pgstmt.res_index + 1; return pgstmt.res_index < pgstmt.n_tuples.?; } fn column_i64(db: *anyopaque, stmt: *anyopaque, idx: u31) ColumnError!?i64 { _ = db; var pgstmt: *PgStmt = @alignCast(@ptrCast(stmt)); if (!pgstmt.did_exec) @panic("did_exec == false you must call exec then step before trying column"); if (pgstmt.res_index == -1) @panic("res_index == -1 you must call exec then step before trying column"); if (pq.PQgetisnull(pgstmt.c_res, pgstmt.res_index, idx) == 1) { return null; } const value_c: [*c]const u8 = pq.PQgetvalue(pgstmt.c_res, pgstmt.res_index, idx); const slice = std.mem.sliceTo(value_c, 0); return try std.fmt.parseInt(i64, slice, 10); } fn column_slice_const_u8(db: *anyopaque, stmt: *anyopaque, idx: u31) ColumnError!?[:0]const u8 { _ = db; var pgstmt: *PgStmt = @alignCast(@ptrCast(stmt)); if (!pgstmt.did_exec) @panic("did_exec == false you must call exec then step before trying column"); if (pgstmt.res_index == -1) @panic("res_index == -1 you must call exec then step before trying column"); if (pq.PQgetisnull(pgstmt.c_res, pgstmt.res_index, idx) == 1) { return null; } const value_c: ?[*:0]u8 = pq.PQgetvalue(pgstmt.c_res, pgstmt.res_index, idx); const value_c_nonnull = value_c orelse return null; return std.mem.sliceTo(value_c_nonnull, 0); } fn close_stmt(db: *anyopaque, stmt: *anyopaque) void { var self: *Postgres = @alignCast(@ptrCast(db)); var pgstmt: *PgStmt = @alignCast(@ptrCast(stmt)); if (pgstmt.c_res != null) { pq.PQclear(pgstmt.c_res.?); } pgstmt.params.deinit(); self.allocator.destroy(pgstmt); } fn close_db(db: *anyopaque) void { var self: *Postgres = @alignCast(@ptrCast(db)); pq.PQfinish(self.conn); self.allocator.destroy(self); }