aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--README.md3
-rw-r--r--build.zig32
-rw-r--r--build.zig.zon4
-rw-r--r--src/main.zig151
5 files changed, 192 insertions, 0 deletions
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;
+}