From e94e4384bb05f2be479df15fc07374d3ea73f333 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sun, 11 Sep 2022 01:55:20 -0700 Subject: [PATCH] Add postgres support in db package --- build.zig | 1 + src/sql/common.zig | 44 +++++++ src/sql/lib.zig | 291 +++++++++++-------------------------------- src/sql/postgres.zig | 120 ++++++++++++++++++ src/sql/sqlite.zig | 230 ++++++++++++++++++++++++++++++++++ src/util/lib.zig | 6 +- 6 files changed, 469 insertions(+), 223 deletions(-) create mode 100644 src/sql/common.zig create mode 100644 src/sql/postgres.zig create mode 100644 src/sql/sqlite.zig diff --git a/build.zig b/build.zig index d0487bf..143dec6 100644 --- a/build.zig +++ b/build.zig @@ -43,6 +43,7 @@ pub fn build(b: *std.build.Builder) void { exe.addPackage(http_pkg); exe.linkSystemLibrary("sqlite3"); + exe.linkSystemLibrary("pq"); exe.linkLibC(); const util_tests = b.addTest("src/util/lib.zig"); diff --git a/src/sql/common.zig b/src/sql/common.zig new file mode 100644 index 0000000..42a776f --- /dev/null +++ b/src/sql/common.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const util = @import("util"); + +const Uuid = util.Uuid; +const DateTime = util.DateTime; +const Allocator = std.mem.Allocator; + +// Turns a value into its appropriate textual value (or null) +// as appropriate using the given arena allocator +pub fn prepareParamText(arena: std.heap.ArenaAllocator, val: anytype) !?[:0]const u8 { + if (comptime std.meta.trait.isZigString(@TypeOf(val))) return val; + + return switch (@TypeOf(val)) { + [:0]u8, [:0]const u8 => val, + []const u8, []u8 => try std.cstr.addNullByte(val), + DateTime, Uuid => try std.fmt.allocPrintZ(arena.allocator(), "{}", .{val}), + + else => |T| switch (@typeInfo(T)) { + .Enum => return @tagName(val), + .Optional => if (val) |v| try prepareParamText(arena, v) else null, + .Int => try std.fmt.allocPrintZ(arena.allocator(), "{}", .{val}), + else => @compileError("Unsupported Type " ++ @typeName(T)), + }, + }; +} + +fn parseEnum(comptime T: type, _: []const u8) !T { + @panic("not implemented"); +} + +// Parse a (not-null) value from a string +pub fn parseValueNotNull(alloc: ?Allocator, comptime T: type, str: []const u8) !T { + return switch (T) { + Uuid => Uuid.parse(str), + DateTime => DateTime.parse(str), + []u8, []const u8 => if (alloc) |a| util.deepClone(a, str) else return error.AllocatorRequired, + + else => switch (@typeInfo(T)) { + .Enum => parseEnum(T, str), + + else => @compileError("Type " ++ @typeName(T) ++ " not supported"), + }, + }; +} diff --git a/src/sql/lib.zig b/src/sql/lib.zig index 0021b48..88a20d1 100644 --- a/src/sql/lib.zig +++ b/src/sql/lib.zig @@ -1,247 +1,98 @@ const std = @import("std"); -const util = @import("util"); -const c = @cImport({ - @cInclude("sqlite3.h"); -}); -const Uuid = util.Uuid; -const DateTime = util.DateTime; +const postgres = @import("./postgres.zig"); +const sqlite = @import("./sqlite.zig"); +const Allocator = std.mem.Allocator; -const UnexpectedError = error{Unexpected}; +pub const Type = enum { + postgres, + sqlite, +}; -pub const OpenError = error{ - DbCorrupt, - BadPathName, - IsDir, - InputOutput, - NoSpaceLeft, -} || UnexpectedError; -pub const PrepareError = UnexpectedError; -pub const RowGetError = error{ InvalidData, StreamTooLong, OutOfMemory } || UnexpectedError; -pub const BindError = UnexpectedError; +pub const Config = union(Type) { + postgres: struct { + conn_str: [:0]const u8, + }, + sqlite: struct { + file_path: [:0]const u8, + }, +}; -fn getCharPos(text: []const u8, offset: c_int) struct { row: usize, col: usize } { - var row: usize = 0; - var col: usize = 0; - var i: usize = 0; +//pub const OpenError = sqlite.OpenError | postgres.OpenError; - if (offset > text.len) return .{ .row = 0, .col = 0 }; +// Represents a set of results. +// row() must be called until it returns null, or the query may not complete +// Must be deallocated by a call to finish() +pub const Results = union(Type) { + postgres: postgres.Results, + sqlite: sqlite.Results, - while (i != offset) : (i += 1) { - if (text[i] == '\n') { - row += 1; - col = 0; - } else { - col += 1; + pub fn finish(self: Results) void { + switch (self) { + .postgres => |pg| pg.finish(), + .sqlite => |lite| lite.finish(), } } - return .{ .row = row, .col = col }; -} - -fn handleUnexpectedError(db: *c.sqlite3, code: c_int, sql_text: ?[]const u8) UnexpectedError { - std.log.err("Unexpected error in SQLite engine: {s} ({})", .{ c.sqlite3_errstr(code), code }); - - std.log.debug("Additional details:", .{}); - std.log.debug("{?s}", .{c.sqlite3_errmsg(db)}); - if (sql_text) |sql| { - const byte_offset = c.sqlite3_error_offset(db); - if (byte_offset >= 0) { - const pos = getCharPos(sql, byte_offset); - std.log.debug("Failed at char ({}:{}) of SQL:\n{s}", .{ pos.row, pos.col, sql }); - } - } - std.log.debug("{?s}", .{@errorReturnTrace()}); - - return error.Unexpected; -} - -pub const Sqlite = struct { - db: *c.sqlite3, - - pub fn open(path: [:0]const u8) OpenError!Sqlite { - const flags = c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_CREATE | c.SQLITE_OPEN_EXRESCODE; - - var db: ?*c.sqlite3 = undefined; - switch (c.sqlite3_open_v2(@ptrCast([*c]const u8, path), &db, flags, null)) { - c.SQLITE_OK => {}, - c.SQLITE_NOTADB, c.SQLITE_CORRUPT => return error.DbCorrupt, - c.SQLITE_IOERR_WRITE, c.SQLITE_IOERR_READ => return error.InputOutput, - c.SQLITE_CANTOPEN_ISDIR => return error.IsDir, - c.SQLITE_CANTOPEN_FULLPATH => return error.BadPathName, - c.SQLITE_FULL => return error.NoSpaceLeft, - - else => |err| { - std.log.err( - \\Unable to open SQLite DB "{s}" - \\Error: {s} ({}) - , .{ path, c.sqlite3_errstr(err), err }); - - return error.Unexpected; - }, - } - - return Sqlite{ - .db = db.?, + // can be used as an optimization to reduce memory reallocation + // only works on postgres + pub fn rowCount(self: Results) ?usize { + return switch (self) { + .postgres => |pg| pg.rowCount(), + .sqlite => null, // not possible without repeating the query }; } - pub fn close(self: Sqlite) void { - switch (c.sqlite3_close(self.db)) { - c.SQLITE_OK => {}, - - c.SQLITE_BUSY => { - std.log.err("SQLite DB could not be closed as it is busy.\n{s}", .{c.sqlite3_errmsg(self.db)}); - }, - - else => |err| { - std.log.err("Could not close SQLite DB", .{}); - handleUnexpectedError(self.db, err, null) catch {}; - }, - } - } - - pub fn prepare(self: *Sqlite, sql: []const u8) PrepareError!PreparedStmt { - var stmt: ?*c.sqlite3_stmt = undefined; - const err = c.sqlite3_prepare_v2(self.db, sql.ptr, @intCast(c_int, sql.len), &stmt, null); - if (err != c.SQLITE_OK) return handleUnexpectedError(self.db, err, sql); - - return PreparedStmt{ .stmt = stmt.?, .db = self.db }; + pub fn row(self: *Results) !?Row { + return switch (self.*) { + .postgres => |*pg| if (try pg.row()) |r| Row{ .postgres = r } else null, + .sqlite => |*lite| if (try lite.row()) |r| Row{ .sqlite = r } else null, + }; } }; -pub const Row = struct { - stmt: *c.sqlite3_stmt, - db: *c.sqlite3, +// Row is invalidated by the next call to result.row() +pub const Row = union(Type) { + postgres: postgres.Row, + sqlite: sqlite.Row, - pub fn isNull(self: Row, idx: u15) RowGetError!bool { - return c.sqlite3_column_type(self.stmt, idx) == c.SQLITE_NULL; - } - - pub fn getI64(self: Row, idx: u15) RowGetError!i64 { - return @intCast(i64, c.sqlite3_column_int64(self.stmt, idx)); - } - - pub fn getText(self: Row, idx: u15, buf: []u8) RowGetError![]u8 { - const ptr = c.sqlite3_column_text(self.stmt, idx); - - const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, idx)); - if (size > buf.len) return error.StreamTooLong; - for (ptr[0..size]) |ch, i| buf[i] = ch; - - return buf[0..size]; - } - - pub fn getTextAlloc(self: Row, idx: u15, alloc: std.mem.Allocator) RowGetError![]u8 { - const size = c.sqlite3_column_bytes(self.stmt, idx); - var buf = try alloc.alloc(u8, @intCast(usize, size)); - errdefer alloc.free(buf); - - return self.getText(idx, buf); - } - - pub fn getBlob(self: Row, idx: u15, buf: []u8) RowGetError![]u8 { - const ptr = @ptrCast([*]const u8, c.sqlite3_column_blob(self.stmt, idx)); - - const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, idx)); - if (size > buf.len) return error.StreamTooLong; - for (ptr[0..size]) |ch, i| buf[i] = ch; - - return buf[0..size]; - } - - pub fn getBlobAlloc(self: Row, idx: u15, alloc: std.mem.Allocator) RowGetError![]u8 { - const size = c.sqlite3_column_bytes(self.stmt, idx); - var buf = try alloc.alloc(u8, @intCast(usize, size)); - errdefer alloc.free(buf); - - return self.getBlob(idx, buf); - } - - pub fn getUuid(self: Row, idx: u15) RowGetError!Uuid { - var buf: [Uuid.string_len + 1]u8 = undefined; - _ = try self.getText(idx, &buf); - return Uuid.parse(buf[0..Uuid.string_len]) catch return error.InvalidData; - } - - pub fn getDateTime(self: Row, idx: u15) RowGetError!DateTime { - return DateTime{ .seconds_since_epoch = try self.getI64(idx) }; + // Returns a value of type T from the zero-indexed column given by idx. + // Not all types require an allocator to be present. If an allocator is needed but + // not required, it will return error.AllocatorRequired. + // The caller is responsible for deallocating T, if relevant. + pub fn get(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) !T { + return switch (self) { + .postgres => |pg| pg.get(T, idx, alloc), + .sqlite => |lite| lite.get(T, idx, alloc), + }; } }; -pub const PreparedStmt = struct { - stmt: *c.sqlite3_stmt, - db: *c.sqlite3, +pub const Db = union(Type) { + postgres: postgres.Db, + sqlite: sqlite.Db, - pub fn bindNull(self: PreparedStmt, idx: u15) BindError!void { - return switch (c.sqlite3_bind_null(self.stmt, idx)) { - c.SQLITE_OK => {}, - else => |err| handleUnexpectedError(self.db, err, null), - }; - } - - pub fn bindUuid(self: PreparedStmt, idx: u15, id: Uuid) BindError!void { - const str = id.toCharArray(); - return self.bindText(idx, &str); - } - - pub fn bindText(self: PreparedStmt, idx: u15, str: []const u8) BindError!void { - // Work around potential null pointer in empty string - const eff_str = if (str.len == 0) (" ")[0..0] else str; - return switch (c.sqlite3_bind_text(self.stmt, idx, eff_str.ptr, @intCast(c_int, eff_str.len), c.SQLITE_TRANSIENT)) { - c.SQLITE_OK => {}, - else => |err| handleUnexpectedError(self.db, err, null), - }; - } - - pub fn bindBlob(self: PreparedStmt, idx: u15, blob: []const u8) BindError!void { - return switch (c.sqlite3_bind_blob64(self.stmt, idx, blob.ptr, blob.len, c.SQLITE_TRANSIENT)) { - c.SQLITE_OK => {}, - else => |err| handleUnexpectedError(self.db, err, null), - }; - } - - pub fn bindI64(self: PreparedStmt, idx: u15, val: i64) BindError!void { - return switch (c.sqlite3_bind_int64(self.stmt, idx, val)) { - c.SQLITE_OK => {}, - else => |err| handleUnexpectedError(self.db, err, null), - }; - } - - pub fn bindDateTime(self: PreparedStmt, idx: u15, val: DateTime) BindError!void { - return self.bindI64(idx, val.seconds_since_epoch); - } - - pub const StepError = UnexpectedError; - pub fn step(self: PreparedStmt) !?Row { - return switch (c.sqlite3_step(self.stmt)) { - c.SQLITE_ROW => Row{ .stmt = self.stmt, .db = self.db }, - c.SQLITE_DONE => null, - - else => |err| handleUnexpectedError(self.db, err, self.getGeneratingSql()), - }; - } - - pub fn finalize(self: PreparedStmt) void { - switch (c.sqlite3_finalize(self.stmt)) { - c.SQLITE_OK => {}, - else => |err| { - handleUnexpectedError(self.db, err, self.getGeneratingSql()) catch {}; + pub fn open(cfg: Config) !Db { + return switch (cfg) { + .postgres => |postgres_cfg| Db{ + .postgres = try postgres.Db.open(postgres_cfg.conn_str), }, + .sqlite => |lite_cfg| Db{ + .sqlite = try sqlite.Db.open(lite_cfg.file_path), + }, + }; + } + pub fn close(self: Db) void { + switch (self) { + .postgres => |pg| pg.close(), + .sqlite => |lite| lite.close(), } } - pub fn reset(self: PreparedStmt) void { - switch (c.sqlite3_reset(self.stmt)) { - c.SQLITE_OK => {}, - else => |err| { - handleUnexpectedError(self.db, err, self.getGeneratingSql()) catch {}; - }, - } - } - - fn getGeneratingSql(self: PreparedStmt) ?[]const u8 { - const ptr = c.sqlite3_sql(self.stmt) orelse return null; - return ptr[0..std.mem.len(ptr)]; + pub fn exec(self: Db, sql: [:0]const u8, args: anytype, alloc: Allocator) !Results { + return switch (self) { + .postgres => |pg| Results{ .postgres = try pg.exec(sql, args, alloc) }, + .sqlite => |lite| Results{ .sqlite = try lite.exec(sql, args) }, + }; } }; diff --git a/src/sql/postgres.zig b/src/sql/postgres.zig new file mode 100644 index 0000000..4b90d43 --- /dev/null +++ b/src/sql/postgres.zig @@ -0,0 +1,120 @@ +const std = @import("std"); +const util = @import("util"); +const common = @import("./common.zig"); +const c = @cImport({ + @cInclude("libpq-fe.h"); +}); + +const Allocator = std.mem.Allocator; + +pub const Results = struct { + result: *c.PGresult, + next_row_index: c_int = 0, + + pub fn rowCount(self: Results) usize { + return @intCast(usize, c.PQntuples(self.result)); + } + + pub fn row(self: *Results) !?Row { + if (self.next_row_index >= self.rowCount()) return null; + const idx = self.next_row_index; + self.next_row_index += 1; + + return Row{ + .result = self.result, + .row_index = idx, + }; + } + + pub fn finish(self: Results) void { + c.PQclear(self.result); + } +}; + +pub const Row = struct { + result: *c.PGresult, + row_index: c_int, + + pub fn get(self: Row, comptime T: type, idx: u16, alloc: ?Allocator) !T { + const val = c.PQgetvalue(self.result, self.row_index, idx); + const is_null = (c.PQgetisnull(self.result, self.row_index, idx) != 0); + if (is_null) { + return if (@typeInfo(T) == .Optional) null else error.NullValue; + } + + if (val == null) return error.Unexpected; + + const len = @intCast(usize, c.PQgetlength(self.result, self.row_index, idx)); + + return try common.parseValueNotNull(alloc, T, val[0..len]); + } +}; + +pub const Db = struct { + conn: *c.PGconn, + + pub fn open(conn_str: [:0]const u8) !Db { + const conn = c.PQconnectdb(conn_str.ptr) orelse { + std.log.err("Unable to connect to database", .{}); + return error.UnknownError; + }; + errdefer c.PQfinish(conn); + + std.log.info("Connecting to database using provided connection string...", .{}); + loop: while (true) { + switch (c.PQstatus(conn)) { + c.CONNECTION_OK => break :loop, + + c.CONNECTION_BAD => { + std.log.err("Error connecting to database", .{}); + return error.BadConnection; + }, + + else => std.os.nanosleep(0, 100 * 1000 * 1000), // 100 ms + } + } + std.log.info("DB connection established", .{}); + + return Db{ .conn = conn }; + } + + pub fn close(self: Db) void { + c.PQfinish(self.conn); + } + + const format_text = 0; + const format_binary = 1; + pub fn exec(self: Db, sql: [:0]const u8, args: anytype, alloc: Allocator) !Results { + const result = blk: { + var arena = std.heap.ArenaAllocator.init(alloc); + defer arena.deinit(); + const params = try arena.allocator().alloc(?[*]const u8, args.len); + inline for (args) |a, i| params[i] = if (try common.prepareParamText(arena, a)) |slice| slice.ptr else null; + + break :blk c.PQexecParams(self.conn, sql.ptr, @intCast(c_int, params.len), null, params.ptr, null, null, format_text); + } orelse { + std.log.err("Error occurred in sql query: {?s}", .{c.PQerrorMessage(self.conn)}); + return error.UnknownError; + }; + errdefer c.PQclear(result); + + const status = c.PQresultStatus(result); + return switch (status) { + c.PGRES_EMPTY_QUERY => error.EmptyQuery, + c.PGRES_FATAL_ERROR => blk: { + std.log.err("Error occurred in sql query: {?s}", .{c.PQresultErrorMessage(result)}); + break :blk error.UnknownError; + }, + c.PGRES_BAD_RESPONSE => blk: { + std.log.err("Error occurred in sql query: {?s}", .{c.PQresultErrorMessage(result)}); + break :blk error.BadResponse; + }, + + c.PGRES_COMMAND_OK, c.PGRES_TUPLES_OK, c.PGRES_COPY_OUT, c.PGRES_COPY_IN, c.PGRES_COPY_BOTH => return Results{ .result = result }, //TODO + + c.PGRES_SINGLE_TUPLE => unreachable, // Not yet supported + + else => unreachable, + }; + } +}; diff --git a/src/sql/sqlite.zig b/src/sql/sqlite.zig new file mode 100644 index 0000000..d50c882 --- /dev/null +++ b/src/sql/sqlite.zig @@ -0,0 +1,230 @@ +const std = @import("std"); +const util = @import("util"); +const common = @import("./common.zig"); +const c = @cImport({ + @cInclude("sqlite3.h"); +}); + +const Uuid = util.Uuid; +const DateTime = util.DateTime; +const Allocator = std.mem.Allocator; + +const UnexpectedError = error{Unexpected}; + +pub const OpenError = error{ + DbCorrupt, + BadPathName, + IsDir, + InputOutput, + NoSpaceLeft, +} || UnexpectedError; +pub const PrepareError = UnexpectedError; +pub const RowGetError = error{ InvalidData, StreamTooLong, OutOfMemory } || UnexpectedError; +pub const BindError = UnexpectedError; + +fn getCharPos(text: []const u8, offset: c_int) struct { row: usize, col: usize } { + var row: usize = 0; + var col: usize = 0; + var i: usize = 0; + + if (offset > text.len) return .{ .row = 0, .col = 0 }; + + while (i != offset) : (i += 1) { + if (text[i] == '\n') { + row += 1; + col = 0; + } else { + col += 1; + } + } + + return .{ .row = row, .col = col }; +} + +fn handleUnexpectedError(db: *c.sqlite3, code: c_int, sql_text: ?[]const u8) UnexpectedError { + std.log.err("Unexpected error in SQLite engine: {s} ({})", .{ c.sqlite3_errstr(code), code }); + + std.log.debug("Additional details:", .{}); + std.log.debug("{?s}", .{c.sqlite3_errmsg(db)}); + if (sql_text) |sql| { + const byte_offset = c.sqlite3_error_offset(db); + if (byte_offset >= 0) { + const pos = getCharPos(sql, byte_offset); + std.log.debug("Failed at char ({}:{}) of SQL:\n{s}", .{ pos.row, pos.col, sql }); + } + } + std.log.debug("{?s}", .{@errorReturnTrace()}); + + return error.Unexpected; +} + +pub const Db = struct { + db: *c.sqlite3, + + pub fn open(path: [:0]const u8) OpenError!Db { + const flags = c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_CREATE | c.SQLITE_OPEN_EXRESCODE; + + var db: ?*c.sqlite3 = undefined; + switch (c.sqlite3_open_v2(@ptrCast([*c]const u8, path), &db, flags, null)) { + c.SQLITE_OK => {}, + c.SQLITE_NOTADB, c.SQLITE_CORRUPT => return error.DbCorrupt, + c.SQLITE_IOERR_WRITE, c.SQLITE_IOERR_READ => return error.InputOutput, + c.SQLITE_CANTOPEN_ISDIR => return error.IsDir, + c.SQLITE_CANTOPEN_FULLPATH => return error.BadPathName, + c.SQLITE_FULL => return error.NoSpaceLeft, + + else => |err| { + std.log.err( + \\Unable to open SQLite DB "{s}" + \\Error: {s} ({}) + , .{ path, c.sqlite3_errstr(err), err }); + + return error.Unexpected; + }, + } + + return Db{ + .db = db.?, + }; + } + + pub fn close(self: Db) void { + switch (c.sqlite3_close(self.db)) { + c.SQLITE_OK => {}, + + c.SQLITE_BUSY => { + std.log.err("SQLite DB could not be closed as it is busy.\n{s}", .{c.sqlite3_errmsg(self.db)}); + }, + + else => |err| { + std.log.err("Could not close SQLite DB", .{}); + handleUnexpectedError(self.db, err, null) catch {}; + }, + } + } + + pub fn exec(self: Db, sql: []const u8, args: anytype) !Results { + var stmt: ?*c.sqlite3_stmt = undefined; + const err = c.sqlite3_prepare_v2(self.db, sql.ptr, @intCast(c_int, sql.len), &stmt, null); + if (err != c.SQLITE_OK) return handleUnexpectedError(self.db, err, sql); + errdefer switch (c.sqlite3_finalize(stmt)) { + c.SQLITE_OK => {}, + else => |err| { + handleUnexpectedError(self.db, err, sql) catch {}; + }, + }; + + inline for (args) |arg, i| { + // SQLite treats $NNN args as having the name NNN, not index NNN. + // As such, if you reference $2 and not $1 in your query (such as + // when dynamically constructing queries), it could assign $2 the + // index 1. So we can't assume the index according to the 1-indexed + // arg array is equivalent to the param to bind it to. + // We can, however, look up the exact index to bind to. + // If the argument is not used in the query, then it will have an "index" + // of 0, and we must not bind the argument. + const name = std.fmt.comptimePrint("${}", .{i + 1}); + const db_idx = c.sqlite3_bind_parameter_index(stmt.?, name); + if (db_idx != 0) { + switch (bindArg(stmt.?, @intCast(u15, db_idx), arg)) { + c.SQLITE_OK => {}, + else => |err| { + return handleUnexpectedError(self.db, err, sql); + }, + } + } else unreachable; + } + + return Results{ .stmt = stmt.?, .db = self.db }; + } +}; + +fn bindArg(stmt: *c.sqlite3_stmt, idx: u15, val: anytype) c_int { + if (comptime std.meta.trait.isZigString(@TypeOf(val))) { + const slice = @as([]const u8, val); + return c.sqlite3_bind_text(stmt, idx, slice.ptr, @intCast(c_int, slice.len), c.SQLITE_TRANSIENT); + } + + return switch (@TypeOf(val)) { + Uuid => blk: { + const arr = val.toCharArrayZ(); + break :blk bindArg(stmt, idx, &arr); + }, + DateTime => blk: { + const arr = val.toCharArrayZ(); + break :blk bindArg(stmt, idx, &arr); + }, + @TypeOf(null) => c.sqlite3_bind_null(stmt, idx), + else => |T| switch (@typeInfo(T)) { + .Optional => if (val) |v| bindArg(stmt, idx, v) else bindArg(stmt, idx, null), + .Enum => bindArg(stmt, idx, @tagName(val)), + .Int => c.sqlite3_bind_int64(stmt, idx, @intCast(i64, val)), + else => @compileError("unsupported type " ++ @typeName(T)), + }, + }; +} + +pub const Results = struct { + stmt: *c.sqlite3_stmt, + db: *c.sqlite3, + + pub fn finish(self: Results) void { + switch (c.sqlite3_finalize(self.stmt)) { + c.SQLITE_OK => {}, + else => |err| { + handleUnexpectedError(self.db, err, self.getGeneratingSql()) catch {}; + }, + } + } + + pub const StepError = UnexpectedError; + pub fn row(self: Results) !?Row { + return switch (c.sqlite3_step(self.stmt)) { + c.SQLITE_ROW => Row{ .stmt = self.stmt, .db = self.db }, + c.SQLITE_DONE => null, + + else => |err| handleUnexpectedError(self.db, err, self.getGeneratingSql()), + }; + } + + fn getGeneratingSql(self: Results) ?[]const u8 { + const ptr = c.sqlite3_sql(self.stmt) orelse return null; + return ptr[0..std.mem.len(ptr)]; + } +}; + +pub const Row = struct { + stmt: *c.sqlite3_stmt, + db: *c.sqlite3, + + pub fn get(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) !T { + if (c.sqlite3_column_type(self.stmt, idx) == c.SQLITE_NULL) { + return if (@typeInfo(T) == .Optional) null else error.NullValue; + } + + return self.getNotNull(T, idx, alloc); + } + + fn getNotNull(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) !T { + return switch (T) { + f32, f64 => @floatCast(T, c.sqlite3_column_double(self.stmt, idx)), + + else => switch (@typeInfo(T)) { + .Int => |int| if (T == i63 or int.bits < 63) + @intCast(T, c.sqlite3_column_int64(self.stmt, idx)) + else + self.getFromString(T, idx, alloc), + .Optional => self.getNotNull(std.meta.Child(T), idx, alloc), + else => self.getFromString(T, idx, alloc), + }, + }; + } + + fn getFromString(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) !T { + const ptr = c.sqlite3_column_text(self.stmt, idx); + const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, idx)); + const str = ptr[0..size]; + + return common.parseValueNotNull(alloc, T, str); + } +}; diff --git a/src/util/lib.zig b/src/util/lib.zig index 45b856c..db465fe 100644 --- a/src/util/lib.zig +++ b/src/util/lib.zig @@ -28,14 +28,14 @@ pub fn deepFree(alloc: std.mem.Allocator, val: anytype) void { .Union, .ErrorUnion => @compileError("TODO: Unions not yet supported by deepFree"), .Array => for (val) |v| deepFree(alloc, v), - .Error, .Int, .Float, .Bool, .Void, .Type => {}, + .Int, .Float, .Bool, .Void, .Type => {}, else => @compileError("Type " ++ @typeName(T) ++ " not supported by deepFree"), } } // deepClone assumes the value owns any pointers inside it -pub fn deepClone(alloc: std.mem.Allocator, val: anytype) !void { +pub fn deepClone(alloc: std.mem.Allocator, val: anytype) !@TypeOf(val) { const T = @TypeOf(val); var result: T = undefined; switch (@typeInfo(T)) { @@ -87,7 +87,7 @@ pub fn deepClone(alloc: std.mem.Allocator, val: anytype) !void { count += 1; } }, - .Error, .Int, .Float, .Bool, .Void, .Type => { + .Int, .Float, .Bool, .Void, .Type => { result = val; }, else => @compileError("Type " ++ @typeName(T) ++ " not supported"),