From 6625fd0ab6c544d0402df2c7ffce2719697ca3d8 Mon Sep 17 00:00:00 2001 From: Martin Ashby Date: Wed, 30 Aug 2023 21:01:51 +0100 Subject: Initial --- .gitignore | 2 + README.md | 3 ++ build.zig | 32 +++++++++++++ build.zig.zon | 4 ++ src/main.zig | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 192 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/main.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee7098f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-out/ +zig-cache/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..100d956 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# pq-zig + +Zig wrapper around [libpq](https://www.postgresql.org/docs/current/libpq.html) \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..37ceb1d --- /dev/null +++ b/build.zig @@ -0,0 +1,32 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "pq", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + lib.linkLibC(); + lib.linkSystemLibrary("libpq"); + lib.addIncludePath(.{ .path = "/usr/include" }); + + // for zig package manager + _ = b.addModule("pq", .{ .source_file = .{ .path = "src/main.zig" } }); + + b.installArtifact(lib); + + const main_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_main_tests = b.addRunArtifact(main_tests); + + const test_step = b.step("test", "Run library tests"); + test_step.dependOn(&run_main_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..c1e91ab --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,4 @@ +.{ + .name = "pq", + .version = "0.0.1", +} \ No newline at end of file diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..a4fc382 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,151 @@ +const std = @import("std"); +const pq = @cImport( + @cInclude("libpq-fe.h"), +); + +pub const PqError = error{PqError}; + +// libpq wrapper +// later, this could be a pure-zig client implementation +pub const Db = struct { + c_conn: *pq.PGconn, + pub fn init(connect_url: [:0]const u8) !Db { + if (pq.PQisthreadsafe() == 0) { + std.log.err("PQisthreadsafe returned 0, can't use libpq in this program", .{}); + return PqError.PqError; + } + var maybe_conn: ?*pq.PGconn = pq.PQconnectdb(connect_url); + if (maybe_conn == null) { + std.log.err("PQconnectdb returned null", .{}); + return PqError.PqError; + } + if (pq.PQstatus(maybe_conn) == pq.CONNECTION_BAD) { + std.log.err("PQstatus returned CONNECTION_BAD: {s}", .{pq.PQerrorMessage(maybe_conn)}); + + return PqError.PqError; + } else if (pq.PQstatus(maybe_conn) != pq.CONNECTION_OK) { + std.log.err("PQstatus returned unknown status {}: {s}", .{ pq.PQstatus(maybe_conn), pq.PQerrorMessage(maybe_conn) }); + return PqError.PqError; + } + return Db{ + .c_conn = maybe_conn.?, + }; + } + + pub fn deinit(self: Db) void { + pq.PQfinish(self.c_conn); + } + + pub fn exec(self: Db, query: [:0]const u8) !void { + var res: ?*pq.PGresult = pq.PQexec(self.c_conn, query); + defer pq.PQclear(res); + var est: pq.ExecStatusType = pq.PQresultStatus(res); + if (est != pq.PGRES_COMMAND_OK) { + std.log.err("PQexec error code {} message {s}", .{ est, pq.PQerrorMessage(self.c_conn) }); + return PqError.PqError; + } + } + + pub fn prepare_statement(self: Db, allocator: std.mem.Allocator, query: [:0]const u8) PqError!Stmt { + return Stmt{ + .db = self, + .aa = std.heap.ArenaAllocator.init(allocator), + .query = query, + }; + } +}; + +pub const Stmt = struct { + const MAX_PARAMS = 128; + db: Db, + query: [:0]const u8, + aa: std.heap.ArenaAllocator, + + n_params: usize = 0, + param_values: [MAX_PARAMS][*c]const u8 = undefined, + did_exec: bool = false, + c_res: ?*pq.PGresult = null, + res_index: c_int = -1, + n_tuples: c_int = -1, + n_fields: c_int = -1, + + pub fn deinit(self: *Stmt) void { + self.aa.deinit(); + if (self.c_res != null) { + pq.PQclear(self.c_res); + } + } + + pub fn step(self: *Stmt) !bool { + if (!self.did_exec) { + self.did_exec = true; + self.c_res = pq.PQexecParams(self.db.c_conn, self.query, @as(c_int, @intCast(self.n_params)), null, &self.param_values, null, null, 0); + const rs = pq.PQresultStatus(self.c_res); + if (rs != pq.PGRES_TUPLES_OK and rs != pq.PGRES_SINGLE_TUPLE and rs != pq.PGRES_COMMAND_OK) { + std.log.err("PQresultStatus {} error: {s}", .{ rs, pq.PQerrorMessage(self.db.c_conn) }); + return PqError.PqError; + } + self.n_tuples = pq.PQntuples(self.c_res); + self.n_fields = pq.PQnfields(self.c_res); + } + self.res_index = self.res_index + 1; + return self.res_index < self.n_tuples; + } + + pub fn read_column(self: Stmt, idx: c_int, comptime T: type) !T { + const value_c: [*c]u8 = pq.PQgetvalue(self.c_res, self.res_index, idx); + const value: []const u8 = std.mem.sliceTo(value_c, 0); + return switch (@typeInfo(T)) { + .Int => std.fmt.parseInt(T, value, 10), + .Pointer => |ptrinfo| blk: { + if (ptrinfo.child != u8) { + @compileError("pointer type []const u8 only is supported in read_column"); + } + if (ptrinfo.size != .Slice) { + @compileError("pointer type []const u8 only is supported in read_column"); + } + break :blk value; + }, + else => @compileError("unhandled type " ++ @tagName(@typeInfo(T)) ++ " in read_column"), + }; + } + + pub fn read_columnN(self: Stmt, name: [:0]const u8, comptime T: type) !T { + const idx = pq.PQfnumber(self.c_res, name.ptr); + if (idx == -1) { + std.log.err("read_columnN ColumnNotFound [{s}]", .{name}); + return error.ColumnNotFound; + } + return read_column(self, idx, T); + } + + pub fn bind(self: *Stmt, idx: usize, t: anytype) !void { + const T = @TypeOf(t); + const value: [:0]const u8 = switch (@typeInfo(T)) { + .Pointer => try std.fmt.allocPrintZ(self.aa.allocator(), "{s}", .{t}), + .Int => try std.fmt.allocPrintZ(self.aa.allocator(), "{d}", .{t}), + else => @compileError("unhandled type " ++ @tagName(@typeInfo(T) ++ " in bind")), + }; + self.param_values[idx] = value.ptr; + self.n_params = @max(self.n_params, idx + 1); + } + + pub fn read_struct(self: Stmt, comptime T: type) !T { + const ti = @typeInfo(T); + var t: T = undefined; + inline for (ti.Struct.fields) |field| { + const name: [:0]const u8 = &addZ(field.name.len, field.name[0..].*); + const val = try self.read_columnN(name, field.type); + @field(t, field.name) = val; + } + return t; + } +}; + +// https://github.com/ziglang/zig/issues/16116 +pub fn addZ(comptime length: usize, value: [length]u8) [length:0]u8 { + var terminated_value: [length:0]u8 = undefined; + terminated_value[length] = 0; + @memcpy(&terminated_value, &value); + return terminated_value; +} -- cgit v1.2.3-ZIG