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/src/main.zig | 136 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 95 insertions(+), 41 deletions(-) (limited to 'comments/src') 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; -- cgit v1.2.3-ZIG