diff --git a/build.zig b/build.zig index 0a9cfd1..672979e 100644 --- a/build.zig +++ b/build.zig @@ -16,6 +16,7 @@ pub fn build(b: *std.build.Builder) void { exe.linkLibC(); exe.linkSystemLibrary("curl"); + exe.linkSystemLibrary("sqlite3"); exe.setTarget(target); exe.setBuildMode(mode); @@ -34,6 +35,13 @@ pub fn build(b: *std.build.Builder) void { exe_tests.setTarget(target); exe_tests.setBuildMode(mode); + const sqlite_tests = b.addTest("src/sqlite.zig"); + sqlite_tests.setTarget(target); + sqlite_tests.setBuildMode(mode); + sqlite_tests.linkSystemLibrary("sqlite3"); + sqlite_tests.linkLibC(); + const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&exe_tests.step); + test_step.dependOn(&sqlite_tests.step); } diff --git a/src/main.zig b/src/main.zig index 35da78e..4076e8e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,6 +4,7 @@ const json = std.json; const twitch = @import("twitch.zig"); const Client = @import("client.zig").Client; const webhook = @import("webhook.zig"); +const sqlite = @import("sqlite.zig"); const Config = struct { token: []const u8, @@ -33,6 +34,13 @@ const User = struct { }; pub fn main() anyerror!void { + var db = try sqlite.Database.open("a.db"); + defer { + _ = db.close() catch |e| { + std.log.err("Failed to close db : {}", .{e}); + }; + } + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); var allocator = arena.allocator(); @@ -51,11 +59,10 @@ pub fn main() anyerror!void { client.deinit(); try Client.cleanup(); - arena.deinit(); } -pub fn updateAlert(allocator: std.mem.Allocator, client: *Client, config: *Config, headers: *std.StringHashMap([]const u8)) anyerror!void{ +pub fn updateAlert(allocator: std.mem.Allocator, client: *Client, config: *Config, headers: *std.StringHashMap([]const u8)) anyerror!void { var request = std.ArrayList(u8).init(allocator); try request.appendSlice("https://api.twitch.tv/helix/streams?"); diff --git a/src/sqlite.zig b/src/sqlite.zig new file mode 100644 index 0000000..efba58f --- /dev/null +++ b/src/sqlite.zig @@ -0,0 +1,281 @@ +const std = @import("std"); + +const sqlite3 = @cImport({ + @cInclude("sqlite3.h"); +}); + +pub const Database = struct { + db: ?*sqlite3.sqlite3 = null, + + pub fn open(path: [*:0]const u8) anyerror!@This() { + var ptr: ?*sqlite3.sqlite3 = null; + + var rc = sqlite3.sqlite3_open(path, &ptr); + if (rc > 0) { + std.log.err("Can't open database: {s}\n", .{sqlite3.sqlite3_errmsg(ptr)}); + return error.FailedToOpenDatabase; + } + + return @This(){ + .db = ptr, + }; + } + + pub fn close(self: *@This()) anyerror!void { + var rc = sqlite3.sqlite3_close(self.db); + if (rc > 0) { + std.log.err("Can't close database: {s}\n", .{sqlite3.sqlite3_errmsg(self.db)}); + } + + self.db = null; + } + + pub fn prepare(self: *@This(), query: []const u8) anyerror!Statement { + var res: ?*sqlite3.sqlite3_stmt = null; + var rc = sqlite3.sqlite3_prepare_v2(self.db, query.ptr, @intCast(c_int, query.len), &res, 0); + + if (rc != sqlite3.SQLITE_OK) { + std.log.err("failed to fetch data: {s}\n", .{sqlite3.sqlite3_errmsg(self.db)}); + return error.FailedToFetchData; + } + + if (res) |ptr| { + return Statement{ + .db = self, + .statement = ptr, + }; + } + return error.NullPtr; + } + + pub fn exec(self: *@This(), queries: [:0]const u8) anyerror!void { // TODO ADD 0 IF NEEDED AT COMPILE TIME + var errorMsg: ?[*:0]u8 = null; + + var rc = sqlite3.sqlite3_exec(self.db, queries, null, null, &errorMsg); + if (rc != sqlite3.SQLITE_OK) { + std.log.err("failed to execute queries: {s}\n", .{errorMsg}); // TODO + sqlite3.sqlite3_free(errorMsg); + return error.FailedToExecuteQueries; + } + } +}; + +pub const Statement = struct { + db: *Database, + statement: *sqlite3.sqlite3_stmt, + + pub fn bind(self: *@This(), index: isize, value: anytype) anyerror!void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .Float, .ComptimeFloat => { + var rc = sqlite3.sqlite3_bind_double(self.statement, @intCast(c_int, index), value); + if (rc != sqlite3.SQLITE_OK) { + std.log.err("failed to bind parameter: {s}\n", .{sqlite3.sqlite3_errmsg(self.db.db)}); + return error.FailedToBindParameter; + } + }, + .Int, .ComptimeInt => { + var rc = sqlite3.sqlite3_bind_int(self.statement, @intCast(c_int, index), value); + if (rc != sqlite3.SQLITE_OK) { + std.log.err("failed to bind parameter: {s}\n", .{sqlite3.sqlite3_errmsg(self.db.db)}); + return error.FailedToBindParameter; + } + }, + .Union => { + if (T == U8Array) { + switch (value) { + U8ArrayTypeTag.text => { + var rc = sqlite3.sqlite3_bind_text(self.statement, @intCast(c_int, index), value.text.ptr, @intCast(c_int, value.text.len), sqlite3.SQLITE_TRANSIENT); + if (rc != sqlite3.SQLITE_OK) { + std.log.err("failed to bind parameter: {s}\n", .{sqlite3.sqlite3_errmsg(self.db.db)}); + return error.FailedToBindParameter; + } + }, + U8ArrayTypeTag.bytes => { + var rc = sqlite3.sqlite3_bind_blob(self.statement, @intCast(c_int, index), value.bytes.ptr, @intCast(c_int, value.bytes.len), sqlite3.SQLITE_TRANSIENT); + if (rc != sqlite3.SQLITE_OK) { + std.log.err("failed to bind parameter: {s}\n", .{sqlite3.sqlite3_errmsg(self.db.db)}); + return error.FailedToBindParameter; + } + }, + } + } else { + @compileError("Unable to bind type '" ++ @typeName(T) ++ "'"); + } + }, + else => @compileError("Unable to bind type '" ++ @typeName(T) ++ "'"), + } + } + + pub fn next(self: *@This()) bool { + var rc = sqlite3.sqlite3_step(self.statement); + + // TODO ERROR ? + return rc == sqlite3.SQLITE_ROW; + } + + pub fn exec(self: *@This()) anyerror!void { + var rc = sqlite3.sqlite3_step(self.statement); + + if (rc == sqlite3.SQLITE_ROW) { + return anyerror.ExecFailed; + } + } + + pub fn finalize(self: *@This()) void { + _ = sqlite3.sqlite3_finalize(self.statement); + } + + pub fn fetch(self: *@This(), args: anytype) anyerror!void { + const ArgsType = @TypeOf(args); + + if (@typeInfo(ArgsType) != .Struct) { + @compileError("Expected tuple or struct argument, found " ++ @typeName(ArgsType)); + } + + comptime var index = 0; + inline while (index < args.len) : (index += 1) { + const arg = args[index]; + + comptime var T = @TypeOf(arg); + + switch (@typeInfo(T)) { + .Pointer => |ptr_info| switch (ptr_info.size) { + .One => switch (@typeInfo(ptr_info.child)) { + .Float, .ComptimeFloat => { + arg.* = sqlite3.sqlite3_column_double(self.statement, @intCast(c_int, index)); + }, + .Int, .ComptimeInt => { + arg.* = sqlite3.sqlite3_column_int(self.statement, @intCast(c_int, index)); + }, + .Union => { + T = @TypeOf(arg.*); + if (T == U8Array) { + switch (arg.*) { + U8ArrayTypeTag.text => { + var text: [*c]const u8 = sqlite3.sqlite3_column_text(self.statement, @intCast(c_int, index)); + + if (text != null) { + var size = sqlite3.sqlite3_column_bytes(self.statement, @intCast(c_int, index)); + + arg.text = text[0..@intCast(usize, size)]; + } + }, + U8ArrayTypeTag.bytes => { + var blob_ptr: ?*const anyopaque = sqlite3.sqlite3_column_blob(self.statement, @intCast(c_int, index)); + + if (blob_ptr != null) { + var blob = @ptrCast([*]const u8, blob_ptr); + var size = sqlite3.sqlite3_column_bytes(self.statement, @intCast(c_int, index)); + + arg.bytes = blob[0..@intCast(usize, size)]; // TODO TEST + } + }, + } + } else { + @compileError("Unable to fetch type '" ++ @typeName(T) ++ "'"); + } + }, + else => @compileError("Unable to fetch type '" ++ @typeName(T) ++ "'"), + }, + + else => @compileError("Unable to fetch type '" ++ @typeName(T) ++ "'"), + }, + else => @compileError("Unable to fetch type '" ++ @typeName(T) ++ "'"), + } + } + } +}; + +const U8ArrayTypeTag = enum { + text, + bytes, +}; + +pub const U8Array = union(U8ArrayTypeTag) { + text: []const u8, + bytes: []const u8, + + pub fn text(value: []const u8) @This() { + return @This(){ .text = value }; + } + + pub fn bytes(value: []const u8) @This() { + return @This(){ .bytes = value }; + } +}; + +// For whatever reason this test make the compiler segfault. +fn testCreationBd() anyerror!void { + const assert = std.debug.assert; + + var db = try Database.open(":memory:"); + + defer { + _ = db.close() catch |e| { + std.log.err("Failed to close db : {}", .{e}); + }; + } + + try db.exec("CREATE TABLE A(A INTEGER, B TEXT, C REAL, D BLOB);"); + + const first_blob_test = [_]u8{ 0, 1, 2 }; + const second_blob_test = [_]u8{0} ** 1000; + + { + var st = try db.prepare("INSERT INTO A VALUES(?, ?, ?, ?)"); + try st.bind(1, 150); + try st.bind(2, U8Array.text("This is the first test")); + try st.bind(3, 3.45); + try st.bind(4, U8Array.bytes(&first_blob_test)); + + try st.exec(); + + st.finalize(); + } + + { + var st = try db.prepare("INSERT INTO A VALUES(?, ?, ?, ?)"); + try st.bind(1, 175); + try st.bind(2, U8Array.text("This is another test")); + try st.bind(3, 156.4); + try st.bind(4, U8Array.bytes(&second_blob_test)); + + try st.exec(); + + st.finalize(); + } + + var query = try db.prepare("SELECT * FROM A"); + + var a: isize = undefined; + var b: U8Array = U8Array.text(undefined); + var c: f64 = undefined; + var d: U8Array = U8Array.bytes(undefined); + + { + assert(query.next()); + try query.fetch(.{ &a, &b, &c, &d }); + + assert(a == 150); + assert(std.mem.eql(u8, "This is the first test", b.text)); + assert(c == 3.45); + assert(std.mem.eql(u8, &first_blob_test, d.bytes)); + } + + { + assert(query.next()); + try query.fetch(.{ &a, &b, &c, &d }); + + assert(a == 175); + assert(std.mem.eql(u8, "This is another test", b.text)); + assert(c == 156.4); + assert(std.mem.eql(u8, &second_blob_test, d.bytes)); + } + + query.finalize(); +} + +test "test-creation-bd" { + try testCreationBd(); +}