aboutsummaryrefslogtreecommitdiff
path: root/src/postgres.zig
diff options
context:
space:
mode:
authorMartin Ashby <martin@ashbysoft.com>2023-09-03 20:32:51 +0100
committerMartin Ashby <martin@ashbysoft.com>2023-09-03 20:32:51 +0100
commit2c4ac3819b8c42de1410fd524c2c9d08d937ec70 (patch)
tree570d63ee4c92d69c96c5d21bfb2be3adb35376a6 /src/postgres.zig
downloadsql-zig-2c4ac3819b8c42de1410fd524c2c9d08d937ec70.tar.gz
sql-zig-2c4ac3819b8c42de1410fd524c2c9d08d937ec70.tar.bz2
sql-zig-2c4ac3819b8c42de1410fd524c2c9d08d937ec70.tar.xz
sql-zig-2c4ac3819b8c42de1410fd524c2c9d08d937ec70.zip
InitialHEADmain
Diffstat (limited to 'src/postgres.zig')
-rw-r--r--src/postgres.zig126
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