From 2c4ac3819b8c42de1410fd524c2c9d08d937ec70 Mon Sep 17 00:00:00 2001 From: Martin Ashby Date: Sun, 3 Sep 2023 20:32:51 +0100 Subject: Initial --- src/postgres.zig | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/postgres.zig (limited to 'src/postgres.zig') diff --git a/src/postgres.zig b/src/postgres.zig new file mode 100644 index 0000000..5a17bd7 --- /dev/null +++ b/src/postgres.zig @@ -0,0 +1,126 @@ +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); +} \ No newline at end of file -- cgit v1.2.3-ZIG