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; 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) error{Unexpected} { 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("{?}", .{@errorReturnTrace()}); return error.Unexpected; } pub const Db = struct { db: *c.sqlite3, pub fn open(path: [:0]const u8) common.OpenError!Db { return openInternal(path, false); } pub fn openUri(path: [:0]const u8) common.OpenError!Db { return openInternal(path, true); } fn openInternal(path: [:0]const u8, is_uri: bool) common.OpenError!Db { const flags = c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_CREATE | c.SQLITE_OPEN_EXRESCODE | if (is_uri) c.SQLITE_OPEN_URI else 0; var db: ?*c.sqlite3 = null; switch (c.sqlite3_open_v2(@ptrCast([*c]const u8, path), &db, flags, null)) { c.SQLITE_OK => {}, else => |code| { if (db == null) { // this path should only be hit if out of memory, but log it anyways std.log.err( "Unable to open SQLite DB \"{s}\". Error: {?s} ({})", .{ path, c.sqlite3_errstr(code), code }, ); return error.BadConnection; } const ext_code = c.sqlite3_extended_errcode(db); std.log.err( \\Unable to open SQLite DB "{s}". Error: {?s} ({}) \\Details: {?s} , .{ path, c.sqlite3_errstr(ext_code), ext_code, c.sqlite3_errmsg(db) }, ); 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, opts: common.QueryOptions) common.ExecError!Results { var stmt: ?*c.sqlite3_stmt = undefined; switch (c.sqlite3_prepare_v2(self.db, sql.ptr, @intCast(c_int, sql.len), &stmt, null)) { c.SQLITE_OK => {}, else => |err| return handleUnexpectedError(self.db, err, sql), } errdefer switch (c.sqlite3_finalize(stmt)) { c.SQLITE_OK => {}, else => |err| { handleUnexpectedError(self.db, err, sql) catch {}; }, }; if (@TypeOf(args) != void) { // TODO: Fix for stage1 compiler //inline for (args) |arg, i| { inline for (std.meta.fields(@TypeOf(args))) |field, i| { const arg = @field(args, field.name); // 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) try self.bindArgument(stmt.?, @intCast(u15, db_idx), arg) else if (!opts.ignore_unused_arguments) return error.UnusedArgument; } } return Results{ .stmt = stmt.?, .db = self.db }; } fn bindArgument(self: Db, stmt: *c.sqlite3_stmt, idx: u15, val: anytype) !void { if (comptime std.meta.trait.isZigString(@TypeOf(val))) { return self.bindString(stmt, idx, val); } const T = @TypeOf(val); switch (@typeInfo(T)) { .Union => { const arr = if (@hasDecl(T, "toCharArray")) val.toCharArray() else if (@hasDecl(T, "toCharArrayZ")) val.toCharArrayZ() else { inline for (std.meta.fields(T)) |field| { const Tag = std.meta.Tag(T); const tag = @field(Tag, field.name); if (val == tag) return try self.bindArgument(stmt, idx, @field(val, field.name)); } unreachable; }; const len = std.mem.len(&arr); return self.bindString(stmt, idx, arr[0..len]); }, .Struct => { const arr = if (@hasDecl(T, "toCharArray")) val.toCharArray() else if (@hasDecl(T, "toCharArrayZ")) val.toCharArrayZ() else @compileError("SQLite: Could not serialize " ++ @typeName(T) ++ " into staticly sized string"); const len = std.mem.len(&arr); return self.bindString(stmt, idx, arr[0..len]); }, .Enum => |info| { const name = if (info.is_exhaustive) @tagName(val) else @compileError("SQLite: Could not serialize non-exhaustive enum " ++ @typeName(T) ++ " into string"); return self.bindString(stmt, idx, name); }, .Optional => { return if (val) |v| self.bindArgument(stmt, idx, v) else self.bindNull(stmt, idx); }, .Null => return self.bindNull(stmt, idx), .Int => return self.bindInt(stmt, idx, std.math.cast(i64, val) orelse unreachable), .Float => return self.bindFloat(stmt, idx, val), .Bool => return self.bindInt(stmt, idx, if (val) 1 else 0), else => @compileError("Unable to serialize type " ++ @typeName(T)), } } fn bindString(self: Db, stmt: *c.sqlite3_stmt, idx: u15, str: []const u8) !void { const len = std.math.cast(c_int, str.len) orelse { std.log.err("SQLite: string len {} too large", .{str.len}); return error.BindException; }; switch (c.sqlite3_bind_text(stmt, idx, str.ptr, len, c.SQLITE_TRANSIENT)) { c.SQLITE_OK => {}, else => |result| { std.log.err("SQLite: Unable to bind string to index {}", .{idx}); std.log.debug("SQLite: {s}", .{str}); return handleUnexpectedError(self.db, result, null); }, } } fn bindNull(self: Db, stmt: *c.sqlite3_stmt, idx: u15) !void { switch (c.sqlite3_bind_null(stmt, idx)) { c.SQLITE_OK => {}, else => |result| { std.log.err("SQLite: Unable to bind NULL to index {}", .{idx}); return handleUnexpectedError(self.db, result, null); }, } } fn bindInt(self: Db, stmt: *c.sqlite3_stmt, idx: u15, val: i64) !void { switch (c.sqlite3_bind_int64(stmt, idx, val)) { c.SQLITE_OK => {}, else => |result| { std.log.err("SQLite: Unable to bind int to index {}", .{idx}); std.log.debug("SQLite: {}", .{val}); return handleUnexpectedError(self.db, result, null); }, } } fn bindFloat(self: Db, stmt: *c.sqlite3_stmt, idx: u15, val: f64) !void { switch (c.sqlite3_bind_double(stmt, idx, val)) { c.SQLITE_OK => {}, else => |result| { std.log.err("SQLite: Unable to bind float to index {}", .{idx}); std.log.debug("SQLite: {}", .{val}); return handleUnexpectedError(self.db, result, null); }, } } }; pub const Results = struct { stmt: *c.sqlite3_stmt, db: *c.sqlite3, pub fn finish(self: Results) void { _ = c.sqlite3_finalize(self.stmt); } pub fn row(self: Results) common.RowError!?Row { return switch (c.sqlite3_step(self.stmt)) { c.SQLITE_ROW => Row{ .stmt = self.stmt, .db = self.db }, c.SQLITE_DONE => null, c.SQLITE_CONSTRAINT_UNIQUE => return error.UniqueViolation, c.SQLITE_CONSTRAINT_CHECK => return error.CheckViolation, c.SQLITE_CONSTRAINT_NOTNULL => return error.NotNullViolation, c.SQLITE_CONSTRAINT_FOREIGNKEY => return error.ForeignKeyViolation, c.SQLITE_CONSTRAINT => return error.ConstraintViolation, 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 fn columnCount(self: Results) common.ColumnCountError!u15 { return @intCast(u15, c.sqlite3_column_count(self.stmt)); } fn columnName(self: Results, idx: u15) ![]const u8 { return if (c.sqlite3_column_name(self.stmt, idx)) |ptr| ptr[0..std.mem.len(ptr)] else unreachable; } pub fn columnIndex(self: Results, name: []const u8) common.ColumnIndexError!u15 { var i: u15 = 0; const count = try self.columnCount(); while (i < count) : (i += 1) { const column = try self.columnName(i); if (std.mem.eql(u8, name, column)) return i; } return error.NotFound; } }; pub const Row = struct { stmt: *c.sqlite3_stmt, db: *c.sqlite3, pub fn get(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) common.GetError!T { return getColumn(self.stmt, T, idx, alloc); } }; fn getColumn(stmt: *c.sqlite3_stmt, comptime T: type, idx: u15, alloc: ?Allocator) common.GetError!T { const Eff = if (comptime std.meta.trait.is(.Optional)(T)) std.meta.Child(T) else T; return switch (c.sqlite3_column_type(stmt, idx)) { c.SQLITE_INTEGER => try getColumnInt(stmt, Eff, idx), c.SQLITE_FLOAT => try getColumnFloat(stmt, Eff, idx), c.SQLITE_TEXT => try getColumnText(stmt, Eff, idx, alloc), c.SQLITE_NULL => { if (T == DateTime) { std.log.warn("SQLite: Treating NULL as DateTime epoch", .{}); return std.mem.zeroes(DateTime); } if (@typeInfo(T) != .Optional) { std.log.err("SQLite column {}: Expected value of type {}, got (null)", .{ idx, T }); return error.ResultTypeMismatch; } return null; }, c.SQLITE_BLOB => { std.log.err("SQLite column {}: SQLite value had unsupported storage class BLOB", .{idx}); return error.ResultTypeMismatch; }, else => |class| { std.log.err("SQLite column {}: SQLite value had unknown storage class {}", .{ idx, class }); return error.ResultTypeMismatch; }, }; } fn getColumnInt(stmt: *c.sqlite3_stmt, comptime T: type, idx: u15) common.GetError!T { const val: i64 = c.sqlite3_column_int64(stmt, idx); switch (T) { DateTime => return DateTime{ .seconds_since_epoch = val }, else => switch (@typeInfo(T)) { .Int => if (std.math.cast(T, val)) |v| return v else { std.log.err("SQLite column {}: Expected value of type {}, got {} (outside of range)", .{ idx, T, val }); return error.ResultTypeMismatch; }, .Bool => if (val == 0) return false else return true, else => { std.log.err("SQLite column {}: Storage class INT cannot be parsed into type {}", .{ idx, T }); return error.ResultTypeMismatch; }, }, } } fn getColumnFloat(stmt: *c.sqlite3_stmt, comptime T: type, idx: u15) common.GetError!T { const val: f64 = c.sqlite3_column_double(stmt, idx); switch (T) { // Only support floats that fit in range for now f16, f32, f64 => return @floatCast(T, val), DateTime => return DateTime{ .seconds_since_epoch = std.math.lossyCast(i64, val * @intToFloat(f64, std.time.epoch.secs_per_day)), }, else => { std.log.err("SQLite column {}: Storage class FLOAT cannot be parsed into type {}", .{ idx, T }); return error.ResultTypeMismatch; }, } } fn getColumnText(stmt: *c.sqlite3_stmt, comptime T: type, idx: u15, alloc: ?Allocator) common.GetError!T { if (c.sqlite3_column_text(stmt, idx)) |ptr| { const size = @intCast(usize, c.sqlite3_column_bytes(stmt, idx)); const str = std.mem.sliceTo(ptr[0..size], 0); return common.parseValueNotNull(alloc, T, str); } else { std.log.err("SQLite column {}: TEXT value stored but engine returned null pointer (out of memory?)", .{idx}); return error.ResultTypeMismatch; } }