diff options
author | Martin Ashby <martin@ashbysoft.com> | 2023-09-03 20:32:51 +0100 |
---|---|---|
committer | Martin Ashby <martin@ashbysoft.com> | 2023-09-03 20:32:51 +0100 |
commit | 2c4ac3819b8c42de1410fd524c2c9d08d937ec70 (patch) | |
tree | 570d63ee4c92d69c96c5d21bfb2be3adb35376a6 /src/postgres.zig | |
download | sql-zig-2c4ac3819b8c42de1410fd524c2c9d08d937ec70.tar.gz sql-zig-2c4ac3819b8c42de1410fd524c2c9d08d937ec70.tar.bz2 sql-zig-2c4ac3819b8c42de1410fd524c2c9d08d937ec70.tar.xz sql-zig-2c4ac3819b8c42de1410fd524c2c9d08d937ec70.zip |
Diffstat (limited to 'src/postgres.zig')
-rw-r--r-- | src/postgres.zig | 126 |
1 files changed, 126 insertions, 0 deletions
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 |