From 6257db275bd9bb5794effaa9f7c4f5d6995024ce Mon Sep 17 00:00:00 2001 From: Martin Ashby Date: Fri, 19 Jul 2024 21:59:42 +0100 Subject: Convert to using pg.zig instead of wrapping libpq for postgresql access Pure zig also means cross compilation works again! --- comments/build.zig | 6 +-- comments/build.zig.zon | 8 +-- comments/src/main.zig | 136 ++++++++++++++++++++++++++++++++++--------------- deploy.sh | 10 ++-- 4 files changed, 105 insertions(+), 55 deletions(-) diff --git a/comments/build.zig b/comments/build.zig index c978095..958fb6d 100644 --- a/comments/build.zig +++ b/comments/build.zig @@ -29,10 +29,8 @@ pub fn build(b: *std.Build) void { const zws = b.dependency("zigwebserver", opts); exe.root_module.addImport("zws", zws.module("zigwebserver")); - const pq = b.dependency("pq", opts); - exe.root_module.addImport("pq", pq.module("pq")); - exe.linkLibC(); - exe.linkSystemLibrary("pq"); + const pg = b.dependency("pg", opts); + exe.root_module.addImport("pg", pg.module("pg")); const mustache = b.dependency("mustache", opts); exe.root_module.addImport("mustache", mustache.module("mustache")); diff --git a/comments/build.zig.zon b/comments/build.zig.zon index 5740fa1..88bc50c 100644 --- a/comments/build.zig.zon +++ b/comments/build.zig.zon @@ -1,10 +1,10 @@ .{ .name = "comments", .version = "0.0.1", - .dependencies = .{ .pq = .{ - .url = "https://code.mfashby.net/pq-zig/snapshot/pq-zig-main.tar.xz", - .hash = "1220895045b9a9333a385a0329d9c11604036f28a2150d10c3ed711d4bab21d4a218", - //.path = "../../pq-zig" + .dependencies = .{ .pg = .{ + //.url = "https://github.com/karlseguin/pg.zig/archive/fe2965b7651462258bcdabbb9a5d3aa09a404746.tar.gz", + //.hash = "12208adc9d7072c98cdfe9dba33d72430e95be1763b6bc4b1091148b997a9281741b", + .path = "../../pg.zig" }, .smtp = .{ .url = "https://code.mfashby.net/smtp-zig/snapshot/smtp-zig-main.tar.xz", .hash = "1220b42673da5f313d4ce816e41de2f17e4b7ebbae1669904b9c17a385d9997e72ed", diff --git a/comments/src/main.zig b/comments/src/main.zig index 165dff0..161f250 100644 --- a/comments/src/main.zig +++ b/comments/src/main.zig @@ -1,6 +1,6 @@ const std = @import("std"); const zws = @import("zws"); -const pq = @import("pq"); +const pg = @import("pg"); const mustache = @import("mustache"); const smtp = @import("smtp"); @@ -8,38 +8,50 @@ const Params = zws.Params; const Err = error{ AccessDenied, + AlreadyConnected, BrokenPipe, - ColumnNotFound, + Closed, + ConnectionBusy, ConnectionResetByPeer, ConnectionTimedOut, DeviceBusy, DiskQuota, + EndOfStream, + FieldColumnMismatch, + FileDescriptorNotASocket, FileTooBig, InputOutput, InvalidArgument, InvalidCharacter, + InvalidDataRow, InvalidLength, - InvalidRequestMethod, + InvalidProtocolOption, + InvalidUUID, IsDir, LockViolation, Malformatted, - NetNameDeleted, + NetworkSubsystemFailed, + NoDevice, NoSpaceLeft, + NotAString, NotOpenForReading, NotOpenForWriting, OperationAborted, OutOfMemory, - PqError, + Overflow, + PermissionDenied, + PG, + SocketNotBound, SocketNotConnected, - StreamTooLong, SystemResources, + TimeoutTooBig, Unexpected, + UnexpectedDBMessage, WouldBlock, - Overflow, - EndOfStream, }; + const Ctx = struct { - db: pq.Db, + db: *pg.Conn, pub fn clone(self: @This()) Ctx { return Ctx{ .db = self.db, @@ -127,14 +139,58 @@ const router = Rtr{ .notfound = notfound, }; +const UriParams = struct { + connOpts: pg.Conn.Opts, + authOpts: pg.Conn.AuthOpts, + aa: std.heap.ArenaAllocator, + pub fn deinit(self: *UriParams) void { + self.aa.deinit(); + } +}; + +fn toParams(uri: std.Uri, parent_a: std.mem.Allocator) !UriParams { + var aa = std.heap.ArenaAllocator.init(parent_a); + errdefer aa.deinit(); + const a = aa.allocator(); + const co: pg.Conn.Opts = .{ + .host = if (uri.host) |host| try host.toRawMaybeAlloc(a) else "localhost", + .port = uri.port orelse 5432, + }; + const ao: pg.Conn.AuthOpts = .{ + .username = if (uri.user) |user| try user.toRawMaybeAlloc(a) else "postgres", + .password = if (uri.password) |password| try password.toRawMaybeAlloc(a) else "", + .database = if (uri.path.isEmpty()) "postgres" else std.mem.trimLeft(u8, try uri.path.toRawMaybeAlloc(a), "/"), + // TODO startup parameters + }; + return .{ + .connOpts = co, + .authOpts = ao, + .aa = aa, + }; +} + /// Run as a CGI program! pub fn main() !void { const allocator = gpa.allocator(); const db_url = std.posix.getenv("DATABASE_URL") orelse "postgresql://comments@localhost/comments"; - var db = try pq.Db.init(db_url); + const uri = try std.Uri.parse(db_url); + var uriParams = try toParams(uri, allocator); + defer uriParams.deinit(); + var db = try pg.Conn.open(allocator, uriParams.connOpts); + defer db.deinit(); + + db.auth(uriParams.authOpts) catch |e| switch (e) { + error.PG => { + std.log.err("PG error authenticating {s}", .{db.err.?.message}); + return; + }, + else => { + std.log.err("Error authenticating! {}", .{e}); + return; + }, + }; // try db.exec(@embedFile("migrations/0_init.sql")); // try db.exec(@embedFile("migrations/1_capcha.sql")); - defer db.deinit(); const req = Request{ .method = std.meta.stringToEnum(std.http.Method, std.posix.getenv("REQUEST_METHOD") orelse "GET") orelse { return error.InvalidRequestMethod; @@ -148,7 +204,7 @@ pub fn main() !void { .transfer_encoding = .none, .headers = Headers.init(allocator), }; - const ctx = Ctx{ .db = db }; + const ctx = Ctx{ .db = &db }; try router.handle(&res, ctx); } @@ -185,16 +241,14 @@ fn get_comment(res: *Response, ctx: Ctx, _: Params) Err!void { ts: []const u8, }; var comments = std.ArrayList(Comment).init(res.allocator); - var stmt = try ctx.db.prepare_statement(res.allocator, - \\ select author,comment,ts from comments where url = $1 order by ts - ); - defer stmt.deinit(); - try stmt.bind(0, url); - while (try stmt.step()) { - const cmt = try stmt.read_struct(Comment); - try comments.append(cmt); + var qr = try ctx.db.queryOpts( + \\ select author,comment,ts::text from comments where url = $1 order by ts + , .{url}, .{ .column_names = true }); + defer qr.deinit(); + var mapper = qr.mapper(Comment, .{ .allocator = res.allocator, .dupe = true }); + while (try mapper.next()) |nxt| { + try comments.append(nxt); } - const rr = @embedFile("templates/comments.html"); const tt = mustache.parseText(res.allocator, rr, .{}, .{ .copy_strings = false }) catch unreachable; res.transfer_encoding = .chunked; @@ -233,30 +287,29 @@ fn post_comment(res: *Response, ctx: Ctx, _: Params) Err!void { // Validate the capcha { - var stmt = try ctx.db.prepare_statement(res.allocator, "select answer from capchas where id = $1"); - defer stmt.deinit(); - try stmt.bind(0, form_val.capcha_id); - if (!try stmt.step()) { + var qr = try ctx.db.query("select answer from capchas where id = $1", .{form_val.capcha_id}); + defer qr.deinit(); + const row: pg.Row = try qr.next() orelse { std.log.err("missing capcha_id {s}", .{form_val.capcha_id}); try badrequest(res, ctx); return; - } - const ans = try stmt.read_column(0, []const u8); + }; + const ans = row.get([]const u8, 0); if (!std.mem.eql(u8, ans, form_val.capcha_answer)) { std.log.err("bad capcha answer {s} expected {s}", .{ form_val.capcha_answer, ans }); try constresponse(res, @embedFile("templates/capchainvalid.html"), std.http.Status.unauthorized); return; } + try qr.drain(); } // Add the comment... { - var stmt = try ctx.db.prepare_statement(res.allocator, "insert into comments(url,author,comment) values($1, $2, $3)"); - defer stmt.deinit(); - try stmt.bind(0, form_val.url); - try stmt.bind(1, form_val.author); - try stmt.bind(2, form_val.comment); - _ = try stmt.step(); + _ = try ctx.db.exec("insert into comments(url,author,comment) values($1, $2, $3)", .{ + form_val.url, + form_val.author, + form_val.comment, + }); } // Send me an email @@ -292,18 +345,19 @@ fn get_form(res: *Response, ctx: Ctx, _: Params) Err!void { return; }; - var stmt = try ctx.db.prepare_statement(res.allocator, "select id, question from capchas order by random() limit 1"); - defer stmt.deinit(); - if (!try stmt.step()) { - std.log.err("no capcha!", .{}); - try badrequest(res, ctx); - return; - } const Capcha = struct { id: []const u8, question: []const u8, }; - const capcha = try stmt.read_struct(Capcha); + + var qr = try ctx.db.queryOpts("select id::text, question from capchas order by random() limit 1", .{}, .{ .column_names = true }); + defer qr.deinit(); + var m = qr.mapper(Capcha, .{ .allocator = res.allocator, .dupe = true }); + const capcha = try m.next() orelse { + std.log.err("no capcha!", .{}); + try badrequest(res, ctx); + return; + }; const rr = @embedFile("templates/form.html"); const tt = mustache.parseText(res.allocator, rr, .{}, .{ .copy_strings = false }) catch unreachable; diff --git a/deploy.sh b/deploy.sh index 91e476f..48f8c79 100755 --- a/deploy.sh +++ b/deploy.sh @@ -7,12 +7,10 @@ zig build # Build comments app pushd comments -if [ $(uname -m) != "aarch64" ] -then - echo "must build on aarch64 for now" - exit -fi -zig build -Doptimize=ReleaseSafe -Dcpu=baseline +zig build \ + -Doptimize=ReleaseSafe \ + -Dtarget=aarch64-linux-gnu \ + -Dcpu=baseline popd # Copy static site -- cgit v1.2.3-ZIG