From b9fc4a67c309082dcd7f34d075c9ff25b5156d5b Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Mon, 17 Aug 2026 21:15:50 -0700 Subject: [PATCH] move off of vrischmann/zig-sqlite --- src/lib.zig | 12 +- src/sqlite3.zig | 317 ++++++++++++++++++++++++++++++++++++++++++------ zig.mod | 10 +- 3 files changed, 295 insertions(+), 44 deletions(-) diff --git a/src/lib.zig b/src/lib.zig index fca69f388bd400263e4546fadbc56dc204d25545..83a4af394fd37276f550030a789abe5025c7649d 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -49,14 +49,14 @@ pub const Engine = union(DriverType) { pub fn collectDyn(engine: *Engine, alloc: std.mem.Allocator, comptime T: type, query: []const u8, args: anytype) ![]T { return switch (engine.*) { .sqlite3 => |*e| { - var stmt = try e.prepareDynamic(query); - defer stmt.deinit(); - var iter = try stmt.iteratorAlloc(T, alloc, args); var list = std.array_list.Managed(T).init(alloc); errdefer list.deinit(); - while (try iter.nextAlloc(alloc, .{})) |row| { - try list.append(row); - } + var stmt: Driver(.sqlite3).Statement = try .prepare(e, query); + defer stmt.finalize(); + try stmt.bindArgs(alloc, args); + const iter = stmt.iterate(); + errdefer iter.reset(); + while (try iter.step(alloc, T)) |row| try list.append(row); return list.toOwnedSlice(); }, .postgresql => { diff --git a/src/sqlite3.zig b/src/sqlite3.zig index 4fa30adee11df9289f6ad95ddc55713207482711..e49a0f26c72ecb5d7ff77a69542f58230f22e2cb 100644 --- a/src/sqlite3.zig +++ b/src/sqlite3.zig @@ -1,51 +1,42 @@ const std = @import("std"); const string = []const u8; -const sqlite = @import("sqlite"); const tracer = @import("tracer"); const extras = @import("extras"); +const builtin = @import("builtin"); const Self = @This(); -db: sqlite.Db = undefined, +db: *c.sqlite3, pub fn connect(allocator: std.mem.Allocator, path: [:0]const u8) !Self { std.log.scoped(.zorm).info("connecting to {s} @ {s}", .{ "sqlite3", path }); _ = allocator; - return Self{ - .db = try sqlite.Db.init(.{ - .mode = .{ .File = path }, - .open_flags = .{ - .write = true, - .create = true, - }, - .threading_mode = .SingleThread, - }), - }; + var db: ?*c.sqlite3 = null; + var flags: c_int = 0; + flags |= c.SQLITE_OPEN_READWRITE; + flags |= c.SQLITE_OPEN_CREATE; + flags |= c.SQLITE_OPEN_FULLMUTEX; + s.assert(c.sqlite3_open_v2(path, &db, flags, null)); + std.debug.assert(c.sqlite3_threadsafe() > 0); + return .{ .db = db.? }; } pub fn close(self: *Self) void { - self.db.deinit(); -} - -fn prepare(self: *Self, comptime query: string) !sqlite.StatementType(.{}, query) { - return self.db.prepare(query) catch |err| switch (err) { - error.SQLiteError => std.debug.panic("`{s}`: {f}", .{ query, self.db.getDetailedError() }), - else => return err, - }; + s.assert(c.sqlite3_close_v2(self.db)); } pub fn collect(self: *Self, alloc: std.mem.Allocator, comptime T: type, comptime query: string, args: anytype) ![]T { const t = tracer.trace(@src(), " {s}", .{query}); defer t.end(); - var stmt = try self.prepare(query); - defer stmt.deinit(); - var iter = try stmt.iteratorAlloc(T, alloc, args); var list = std.array_list.Managed(T).init(alloc); errdefer list.deinit(); - while (try iter.nextAlloc(alloc, .{})) |row| { - try list.append(row); - } + var stmt: Statement = try .prepare(self, query); + defer stmt.finalize(); + try stmt.bindArgs(alloc, args); + const iter = stmt.iterate(); + errdefer iter.reset(); + while (try iter.step(alloc, T)) |row| try list.append(row); return list.toOwnedSlice(); } @@ -53,29 +44,29 @@ pub fn exec(self: *Self, alloc: std.mem.Allocator, comptime query: string, args: const t = tracer.trace(@src(), " {s}", .{query}); defer t.end(); - var stmt = try self.prepare(query); - defer stmt.deinit(); - try stmt.execAlloc(alloc, .{}, args); + var stmt: Statement = try .prepare(self, query); + defer stmt.finalize(); + try stmt.bindArgs(alloc, args); + try stmt.exec(alloc); } pub fn first(self: *Self, alloc: std.mem.Allocator, comptime T: type, comptime query: string, args: anytype) !?T { const t = tracer.trace(@src(), " {s}", .{query}); defer t.end(); - var stmt = try self.prepare(query); - defer stmt.deinit(); - return try stmt.oneAlloc(T, alloc, .{}, args); -} - -pub fn prepareDynamic(self: *Self, query: string) !sqlite.DynamicStatement { - return self.db.prepareDynamic(query); + var stmt: Statement = try .prepare(self, query); + defer stmt.finalize(); + try stmt.bindArgs(alloc, args); + const iter = stmt.iterate(); + errdefer iter.reset(); + return iter.step(alloc, T); } pub fn doesTableExist(self: *Self, alloc: std.mem.Allocator, name: string) !bool { const t = tracer.trace(@src(), " {s}", .{name}); defer t.end(); - for (try self.collect(alloc, string, "select name from sqlite_master where type=? AND name=?", .{ .type = "table", .name = name })) |item| { + for (try self.collect(alloc, string, "select name from sqlite_master where type = ? AND name = ?", .{ .type = "table", .name = name })) |item| { if (std.mem.eql(u8, item, name)) { return true; } @@ -187,3 +178,255 @@ pub const pragma = struct { return try self.collect(alloc, Pragma.TableInfo, "pragma table_info(" ++ name ++ ")", .{}); } }; + +pub const c = @cImport({ + @cInclude("sqlite3.h"); +}); + +pub const s = struct { + const Error = error{ + SQLITE_ERROR, + SQLITE_INTERNAL, + SQLITE_PERM, + SQLITE_ABORT, + SQLITE_BUSY, + SQLITE_LOCKED, + OutOfMemory, + SQLITE_READONLY, + SQLITE_INTERRUPT, + SQLITE_IOERR, + SQLITE_CORRUPT, + SQLITE_NOTFOUND, + SQLITE_FULL, + SQLITE_CANTOPEN, + SQLITE_PROTOCOL, + SQLITE_EMPTY, + SQLITE_SCHEMA, + SQLITE_TOOBIG, + SQLITE_CONSTRAINT, + SQLITE_MISMATCH, + SQLITE_MISUSE, + SQLITE_NOLFS, + SQLITE_AUTH, + SQLITE_FORMAT, + SQLITE_RANGE, + SQLITE_NOTADB, + SQLITE_NOTICE, + SQLITE_WARNING, + }; + pub fn rc2e(code: c_int) Error { + if (code == c.SQLITE_ERROR) return error.SQLITE_ERROR; + if (code == c.SQLITE_INTERNAL) return error.SQLITE_INTERNAL; + if (code == c.SQLITE_PERM) return error.SQLITE_PERM; + if (code == c.SQLITE_ABORT) return error.SQLITE_ABORT; + if (code == c.SQLITE_BUSY) return error.SQLITE_BUSY; + if (code == c.SQLITE_LOCKED) return error.SQLITE_LOCKED; + if (code == c.SQLITE_NOMEM) return error.OutOfMemory; + if (code == c.SQLITE_READONLY) return error.SQLITE_READONLY; + if (code == c.SQLITE_INTERRUPT) return error.SQLITE_INTERRUPT; + if (code == c.SQLITE_IOERR) return error.SQLITE_IOERR; + if (code == c.SQLITE_CORRUPT) return error.SQLITE_CORRUPT; + if (code == c.SQLITE_NOTFOUND) return error.SQLITE_NOTFOUND; + if (code == c.SQLITE_FULL) return error.SQLITE_FULL; + if (code == c.SQLITE_CANTOPEN) return error.SQLITE_CANTOPEN; + if (code == c.SQLITE_PROTOCOL) return error.SQLITE_PROTOCOL; + if (code == c.SQLITE_EMPTY) return error.SQLITE_EMPTY; + if (code == c.SQLITE_SCHEMA) return error.SQLITE_SCHEMA; + if (code == c.SQLITE_TOOBIG) return error.SQLITE_TOOBIG; + if (code == c.SQLITE_CONSTRAINT) return error.SQLITE_CONSTRAINT; + if (code == c.SQLITE_MISMATCH) return error.SQLITE_MISMATCH; + if (code == c.SQLITE_MISUSE) return error.SQLITE_MISUSE; + if (code == c.SQLITE_NOLFS) return error.SQLITE_NOLFS; + if (code == c.SQLITE_AUTH) return error.SQLITE_AUTH; + if (code == c.SQLITE_FORMAT) return error.SQLITE_FORMAT; + if (code == c.SQLITE_RANGE) return error.SQLITE_RANGE; + if (code == c.SQLITE_NOTADB) return error.SQLITE_NOTADB; + if (code == c.SQLITE_NOTICE) return error.SQLITE_NOTICE; + if (code == c.SQLITE_WARNING) return error.SQLITE_WARNING; + unreachable; + } + pub fn rc2p(code: c_int) Error { + if (builtin.mode == .Debug) @panic(std.mem.sliceTo(c.sqlite3_errstr(code), 0)); + return rc2e(code); + } + pub fn assert(code: c_int) void { + if (code == c.SQLITE_OK) return; + @panic(std.mem.sliceTo(c.sqlite3_errstr(code), 0)); + } + pub fn please(code: c_int) !void { + if (code == c.SQLITE_OK) return; + return rc2p(code); + } +}; + +pub const Statement = struct { + stmt: *c.sqlite3_stmt, + + pub fn prepare(driver: *Self, query: []const u8) !Statement { + var stmt: ?*c.sqlite3_stmt = null; + var flags: c_uint = 0; + _ = &flags; + try s.please(c.sqlite3_prepare_v3(driver.db, query.ptr, @intCast(query.len), flags, &stmt, null)); + return .{ .stmt = stmt.? }; + } + + pub fn finalize(stmt: Statement) void { + s.assert(c.sqlite3_finalize(stmt.stmt)); + } + + pub fn bindArgs(stmt: Statement, allocator: std.mem.Allocator, args: anytype) !void { + if (comptime extras.isSlice(@TypeOf(args))) { + for (args, 0..) |a, i| { + const A = @TypeOf(a); + try bindType(stmt, allocator, i + 1, A, a); + } + return; + } + inline for (@typeInfo(@TypeOf(args)).@"struct".fields, 0..) |f, i| { + try bindType(stmt, allocator, i + 1, f.type, @field(args, f.name)); + } + } + + fn bindType(stmt: Statement, allocator: std.mem.Allocator, idx: usize, T: type, value: T) !void { + if (comptime extras.isZigString(T)) { + return s.please(c.sqlite3_bind_text64(stmt.stmt, @intCast(idx), value.ptr, value.len, c.SQLITE_STATIC, c.SQLITE_UTF8)); + } + if (comptime extras.isArrayOf(u8)(T)) { + return s.please(c.sqlite3_bind_blob64(stmt.stmt, @intCast(idx), &value, value.len, c.SQLITE_TRANSIENT)); + } + switch (@typeInfo(T)) { + .@"struct" => |info| { + if (@hasDecl(T, "BaseType")) return bindBaseType(stmt, allocator, idx, T, value); + if (info.layout == .@"packed") return bindType(stmt, allocator, idx, info.backing_integer.?, @bitCast(value)); + return bindBaseType(stmt, allocator, idx, T, value); + }, + .int => |info| { + comptime std.debug.assert(info.bits <= 64); + if (value > std.math.maxInt(c.sqlite_int64)) return error.Overflow; + if (value < std.math.minInt(c.sqlite_int64)) return error.Overflow; + return s.please(c.sqlite3_bind_int64(stmt.stmt, @intCast(idx), @intCast(value))); + }, + .optional => |info| { + if (value == null) return s.please(c.sqlite3_bind_null(stmt.stmt, @intCast(idx))); + return bindType(stmt, allocator, idx, info.child, value.?); + }, + .bool => { + return bindType(stmt, allocator, idx, u1, @intFromBool(value)); + }, + .@"enum" => { + if (T.BaseType == []const u8 and !@hasDecl(T, "bindField")) { + return bindType(stmt, allocator, idx, []const u8, @tagName(value)); + } + return bindBaseType(stmt, allocator, idx, T, value); + }, + .@"union" => { + switch (value) { + inline else => |val| return bindType(stmt, allocator, idx, @TypeOf(val), val), + } + }, + else => @compileError(@typeName(T)), + } + } + + fn bindBaseType(stmt: Statement, allocator: std.mem.Allocator, idx: usize, T: type, value: T) !void { + const bind_fn = T.bindField; + const info = @typeInfo(@TypeOf(bind_fn)).@"fn"; + switch (info.params.len) { + 1 => { + const base = try value.bindField(); + return bindType(stmt, allocator, idx, @TypeOf(base), base); + }, + 2 => { + const base = try value.bindField(allocator); + return bindType(stmt, allocator, idx, @TypeOf(base), base); + }, + else => comptime unreachable, + } + } + + pub fn exec(stmt: Statement, allocator: std.mem.Allocator) !void { + const iter = stmt.iterate(); + errdefer iter.reset(); + const row = try iter.step(allocator, void); + if (builtin.mode == .Debug) std.debug.assert(row == null); + } + + pub fn iterate(stmt: Statement) Iterator { + return .{ .stmt = stmt.stmt }; + } + + pub const Iterator = struct { + stmt: *c.sqlite3_stmt, + + pub fn reset(iter: Iterator) void { + return s.please(c.sqlite3_reset(iter.stmt)) catch {}; + } + + pub fn step(iter: Iterator, allocator: std.mem.Allocator, T: type) !?T { + const code = c.sqlite3_step(iter.stmt); + if (code == c.SQLITE_DONE) return null; + if (code != c.SQLITE_ROW) return s.rc2p(code); + if (T == void) return; + if (T == string) return try readType(iter, allocator, 0, string); + if (@typeInfo(T) == .int) return try readType(iter, allocator, 0, T); + var result: T = undefined; + inline for (@typeInfo(T).@"struct".fields, 0..) |f, i| { + @field(result, f.name) = try readType(iter, allocator, i, f.type); + } + return result; + } + + fn readType(iter: Iterator, allocator: std.mem.Allocator, idx: usize, T: type) !T { + if (comptime extras.isZigString(T)) { + const res = c.sqlite3_column_text(iter.stmt, @intCast(idx)); + if (res == null) return ""; + const ptr: [*:0]const u8 = @ptrCast(res); + const len = c.sqlite3_column_bytes(iter.stmt, @intCast(idx)); + return try allocator.dupe(u8, ptr[0..@intCast(len) :0]); + } + if (comptime extras.isArrayOf(u8)(T)) { + const info = @typeInfo(T).array; + const ptr: [*:0]const u8 = @ptrCast(c.sqlite3_column_blob(iter.stmt, @intCast(idx))); + const len = c.sqlite3_column_bytes(iter.stmt, @intCast(idx)); + std.debug.assert(len == info.len); + return ptr[0..info.len].*; + } + switch (@typeInfo(T)) { + .int => |info| { + comptime std.debug.assert(info.bits <= 64); + const res = c.sqlite3_column_int64(iter.stmt, @intCast(idx)); + if (res > std.math.maxInt(T)) return error.Overflow; + if (res < std.math.minInt(T)) return error.Overflow; + return @intCast(res); + }, + .bool => { + return @bitCast(try readType(iter, allocator, idx, u1)); + }, + .@"struct" => |info| { + if (@hasDecl(T, "BaseType")) return readBaseType(iter, allocator, idx, T); + if (info.layout == .@"packed") return @bitCast(try readType(iter, allocator, idx, info.backing_integer.?)); + return readBaseType(iter, allocator, idx, T); + }, + .optional => |info| { + if (c.sqlite3_column_type(iter.stmt, @intCast(idx)) == c.SQLITE_NULL) return null; + return try readType(iter, allocator, idx, info.child); + }, + .@"enum" => { + if (T.BaseType == string) { + const ptr: [*:0]const u8 = @ptrCast(c.sqlite3_column_text(iter.stmt, @intCast(idx))); + const len = c.sqlite3_column_bytes(iter.stmt, @intCast(idx)); + const str = ptr[0..@intCast(len) :0]; + const enm = std.meta.stringToEnum(T, str); + return enm orelse T.default; + } + }, + else => {}, // else => @compileError(T), // https://codeberg.org/ziglang/zig/issues/32119 + } + } + + fn readBaseType(iter: Iterator, allocator: std.mem.Allocator, idx: usize, T: type) !T { + const base = try readType(iter, allocator, idx, T.BaseType); + return T.readField(allocator, base); + } + }; +}; diff --git a/zig.mod b/zig.mod index 1a75733c5d065e0208c4fe718519f97d7b9b9e6a..f256703828bca260871267cbb1bdd749610b17c5 100644 --- a/zig.mod +++ b/zig.mod @@ -4,7 +4,15 @@ main: src/lib.zig license: MPL-2.0 description: The ORM library for Zig. dependencies: - - src: git https://github.com/vrischmann/zig-sqlite + - src: http https://sqlite.org/2025/sqlite-amalgamation-3480000.zip sha256-d9a15a42db7c78f88fe3d3c5945acce2f4bfe9e4da9f685cd19f6ea1d40aa884 + id: 5wea8xz8pv9w3gv4ve959e6wnxp372g6t4dpzf8j + license: blessing + description: SQLite is a C-language library that implements a small, fast, self-contained, high-reliability, full-featured, SQL database engine. + c_include_dirs: + - sqlite-amalgamation-3480000 + c_source_files: + - sqlite-amalgamation-3480000/sqlite3.c + - src: git https://github.com/nektro/zig-tracer - src: git https://github.com/nektro/zig-whatwg-url - src: git https://github.com/nektro/zig-extras -- 2.54.0