polls on atproto pollz.waow.tech
atproto zig
at main 131 lines 4.2 kB view raw
1const std = @import("std"); 2const json = std.json; 3const mem = std.mem; 4const Thread = std.Thread; 5const Allocator = mem.Allocator; 6const zqlite = @import("zqlite"); 7 8pub var conn: zqlite.Conn = undefined; 9pub var mutex: Thread.Mutex = .{}; 10 11pub fn init(path: [*:0]const u8) !void { 12 std.debug.print("opening database at: {s}\n", .{path}); 13 conn = zqlite.open(path, zqlite.OpenFlags.Create | zqlite.OpenFlags.ReadWrite) catch |err| { 14 std.debug.print("failed to open database: {}\n", .{err}); 15 return err; 16 }; 17 try initSchema(); 18} 19 20pub fn close() void { 21 conn.close(); 22} 23 24fn initSchema() !void { 25 mutex.lock(); 26 defer mutex.unlock(); 27 28 conn.execNoArgs( 29 \\CREATE TABLE IF NOT EXISTS polls ( 30 \\ uri TEXT PRIMARY KEY, 31 \\ repo TEXT NOT NULL, 32 \\ rkey TEXT NOT NULL, 33 \\ text TEXT NOT NULL, 34 \\ options TEXT NOT NULL, 35 \\ created_at TEXT NOT NULL 36 \\) 37 ) catch |err| { 38 std.debug.print("failed to create polls table: {}\n", .{err}); 39 return err; 40 }; 41 42 conn.execNoArgs( 43 \\CREATE TABLE IF NOT EXISTS votes ( 44 \\ uri TEXT PRIMARY KEY, 45 \\ subject TEXT NOT NULL, 46 \\ option INTEGER NOT NULL, 47 \\ voter TEXT NOT NULL, 48 \\ created_at TEXT, 49 \\ UNIQUE(subject, voter) 50 \\) 51 ) catch |err| { 52 std.debug.print("failed to create votes table: {}\n", .{err}); 53 return err; 54 }; 55 56 // add created_at column if it doesn't exist (migration for existing DBs) 57 conn.execNoArgs("ALTER TABLE votes ADD COLUMN created_at TEXT") catch {}; 58 59 conn.execNoArgs( 60 \\CREATE INDEX IF NOT EXISTS idx_votes_subject ON votes(subject) 61 ) catch |err| { 62 std.debug.print("failed to create index: {}\n", .{err}); 63 return err; 64 }; 65 66 conn.execNoArgs( 67 \\CREATE INDEX IF NOT EXISTS idx_votes_voter ON votes(subject, voter) 68 ) catch |err| { 69 std.debug.print("failed to create voter index: {}\n", .{err}); 70 return err; 71 }; 72 73 std.debug.print("database schema initialized\n", .{}); 74} 75 76pub fn insertPoll(uri: []const u8, did: []const u8, rkey: []const u8, text_json: []const u8, options_json: []const u8, created_at: []const u8) !void { 77 mutex.lock(); 78 defer mutex.unlock(); 79 80 conn.exec( 81 "INSERT OR IGNORE INTO polls (uri, repo, rkey, text, options, created_at) VALUES (?, ?, ?, ?, ?, ?)", 82 .{ uri, did, rkey, text_json, options_json, created_at }, 83 ) catch |err| { 84 std.debug.print("db insert poll error: {}\n", .{err}); 85 return err; 86 }; 87} 88 89pub fn insertVote(uri: []const u8, subject: []const u8, option: i32, voter: []const u8, created_at: ?[]const u8) !void { 90 mutex.lock(); 91 defer mutex.unlock(); 92 93 // upsert: update if exists and new vote is newer, otherwise insert 94 // this handles out-of-order events from tap 95 conn.exec( 96 \\INSERT INTO votes (uri, subject, option, voter, created_at) 97 \\VALUES (?, ?, ?, ?, ?) 98 \\ON CONFLICT(subject, voter) DO UPDATE SET 99 \\ uri = excluded.uri, 100 \\ option = excluded.option, 101 \\ created_at = excluded.created_at 102 \\WHERE excluded.created_at > votes.created_at OR votes.created_at IS NULL 103 , .{ uri, subject, option, voter, created_at }) catch |err| { 104 std.debug.print("db insert vote error: {}\n", .{err}); 105 return err; 106 }; 107} 108 109pub fn deletePoll(uri: []const u8) void { 110 mutex.lock(); 111 defer mutex.unlock(); 112 113 conn.exec("DELETE FROM polls WHERE uri = ?", .{uri}) catch |err| { 114 std.debug.print("db delete poll error: {}\n", .{err}); 115 }; 116 // also delete associated votes 117 conn.exec("DELETE FROM votes WHERE subject = ?", .{uri}) catch |err| { 118 std.debug.print("db delete votes error: {}\n", .{err}); 119 }; 120} 121 122pub fn deleteVote(uri: []const u8) void { 123 mutex.lock(); 124 defer mutex.unlock(); 125 126 // only delete if the URI matches - if a newer vote replaced this one, 127 // the URI won't match and we should not delete 128 conn.exec("DELETE FROM votes WHERE uri = ?", .{uri}) catch |err| { 129 std.debug.print("db delete vote error: {}\n", .{err}); 130 }; 131}