aboutsummaryrefslogtreecommitdiff
path: root/zig/sqlite.zig
diff options
context:
space:
mode:
Diffstat (limited to 'zig/sqlite.zig')
-rw-r--r--zig/sqlite.zig277
1 files changed, 277 insertions, 0 deletions
diff --git a/zig/sqlite.zig b/zig/sqlite.zig
new file mode 100644
index 0000000..084aec4
--- /dev/null
+++ b/zig/sqlite.zig
@@ -0,0 +1,277 @@
+//! A SQLite database.
+//!
+//! Put the SQLite amalgamation into the `lib/sqlite` directory and add them to
+//! your compile step:
+//!
+//! var step: *std.Build.Step.Compile;
+//! step.addIncludePath(b.path("lib"));
+//! step.addCSourceFile(.{
+//! .file = b.path("lib/sqlite/sqlite3.c"),
+//! .flags = &[_][]const u8{ "-DSQLITE_OMIT_SHARED_CACHE", "-DSQLITE_THREADSAFE=0" },
+//! });
+//!
+const std = @import("std");
+const assert = std.debug.assert;
+const c = @cImport({
+ @cInclude("sqlite/sqlite3.h");
+});
+
+const Self = @This();
+
+handle: *c.sqlite3,
+err: Error!void,
+errmsg: ?[]const u8,
+in_tx: bool,
+
+pub fn open(path: [*c]const u8) Error!Self {
+ var handle: ?*c.sqlite3 = undefined;
+ var res = c.sqlite3_open(path, &handle);
+ if (res & 0xff == c.SQLITE_CANTOPEN) {
+ // We assume that the directory of the given path does not exist.
+ // Create the directory and retry the operation.
+ const dir = std.fs.path.dirname(path).?;
+ std.fs.cwd().makePath(dir) catch unreachable; // TODO: better error reporting?
+ res = c.sqlite3_open(path.name(), &handle);
+ }
+ try error_from_code(res);
+ assert(handle != null);
+ _ = c.sqlite3_extended_result_codes(handle, 1);
+
+ return Self{
+ .handle = handle.?,
+ .err = {},
+ .errmsg = null,
+ .in_tx = false,
+ };
+}
+
+pub fn close(self: *Self) void {
+ assert(!self.in_tx);
+ _ = c.sqlite3_close(self.handle);
+}
+
+// Execute an SQL statement and returns the number of affected rows.
+pub fn exec(self: *Self, sql: [:0]const u8, args: anytype) Error!usize {
+ self.err catch unreachable;
+
+ const stmt = try Stmt.init(self, sql, args);
+ defer stmt.deinit();
+ const res = stmt.eval();
+ if (res != c.SQLITE_DONE) {
+ assert(res != c.SQLITE_OK and res != c.SQLITE_ROW);
+ try self.check_errc(res);
+ }
+ return @intCast(c.sqlite3_changes(self.handle));
+}
+
+pub fn query(self: *Self, sql: [:0]const u8, args: anytype) Error!Iter {
+ self.err catch unreachable;
+
+ const stmt = try Stmt.init(self, sql, args);
+ return Iter.init(self, stmt);
+}
+
+pub fn begin_tx(self: *Self) Error!void {
+ assert(!self.in_tx);
+ _ = try self.exec("begin transaction", .{});
+ self.in_tx = true;
+}
+
+pub fn commit_tx(self: *Self) void {
+ if (self.in_tx) {
+ // Make sure we can commit a transaction in a `defer` statement
+ // and only execute a "commit" if no error occured before.
+ self.err catch return;
+ _ = self.exec("commit", .{}) catch unreachable;
+ self.in_tx = false;
+ }
+}
+
+pub fn rollback_tx(self: *Self) void {
+ if (self.in_tx) {
+ const stmt = Stmt.init(self, "rollback", .{}) catch unreachable;
+ defer stmt.deinit();
+ const res = stmt.eval();
+ error_from_code(res) catch unreachable;
+ self.in_tx = false;
+ }
+}
+
+pub fn last_error(self: *Self) ?[]const u8 {
+ if (self.errmsg) |errmsg| return errmsg;
+ self.err catch |err| return @errorName(err);
+ return null;
+}
+
+fn check_errc(self: *Self, errc: c_int) Error!void {
+ self.err catch return self.err;
+ error_from_code(errc) catch |err| {
+ const msg = c.sqlite3_errmsg(self.handle);
+ self.errmsg = if (msg == null) null else std.mem.sliceTo(msg, 0);
+ self.err = err;
+ };
+ return self.err;
+}
+
+pub const Iter = struct {
+ db: *Self,
+ stmt: Stmt,
+ errc: c_int,
+
+ inline fn init(db: *Self, stmt: Stmt) Iter {
+ return Iter{ .db = db, .stmt = stmt, .errc = c.SQLITE_ROW };
+ }
+
+ pub inline fn deinit(self: *Iter) void {
+ self.stmt.deinit();
+ }
+
+ pub fn next(self: *Iter) bool {
+ assert(self.errc == c.SQLITE_ROW);
+ self.errc = c.sqlite3_step(self.stmt.handle);
+ return self.errc != c.SQLITE_DONE;
+ }
+
+ pub fn read_col(self: *Iter, column: usize, comptime T: type) Error!T {
+ assert(self.errc != c.SQLITE_DONE and self.errc != c.SQLITE_OK);
+ if (self.errc != c.SQLITE_ROW) {
+ try self.db.check_errc(self.errc);
+ }
+ return self.stmt.read_col(column, T);
+ }
+};
+
+const Stmt = struct {
+ handle: *c.sqlite3_stmt,
+
+ fn init(db: *Self, sql: [:0]const u8, args: anytype) Error!Stmt {
+ var handle: ?*c.sqlite3_stmt = undefined;
+ var rest: [*c]const u8 = undefined;
+ const res = c.sqlite3_prepare_v2(db.handle, sql.ptr, @intCast(sql.len), &handle, &rest);
+ if (res != c.SQLITE_OK) {
+ assert(res != c.SQLITE_DONE and res != c.SQLITE_ROW);
+ try db.check_errc(res);
+ }
+ assert(rest == null or rest.* == 0);
+ assert(handle != null);
+
+ inline for (@typeInfo(@TypeOf(args)).@"struct".fields, 1..) |arg, i| {
+ const bind_res = switch (@typeInfo(arg.type)) {
+ .int => |int| blk: {
+ if (int.bits <= 64) {
+ break :blk c.sqlite3_bind_int64(handle, @intCast(i), @intCast(@field(args, arg.name)));
+ }
+ comptime assert(int.bits <= 128);
+ const value = std.mem.nativeToBig(u128, @field(args, arg.name));
+ const buf = std.mem.asBytes(&value);
+ comptime assert(buf.len == 16);
+ break :blk c.sqlite3_bind_blob(handle, @intCast(i), buf.ptr, @intCast(buf.len), c.SQLITE_STATIC);
+ },
+ .float => c.sqlite3_bind_double(handle, @intCast(i), @floatCast(@field(args, arg.name))),
+ .pointer => |ptr| blk: {
+ if (ptr.child != u8 or ptr.size != .slice) {
+ @compileError("unsupported type for sql statement: " ++ @typeName(arg.type));
+ }
+ const text = @field(args, arg.name);
+ break :blk c.sqlite3_bind_blob(handle, @intCast(i), text.ptr, @intCast(text.len), c.SQLITE_STATIC);
+ },
+ else => @compileError("unsupported type for sql statement: " ++ @typeName(arg.type)),
+ };
+ if (bind_res != c.SQLITE_OK) {
+ assert(bind_res != c.SQLITE_DONE and res != c.SQLITE_ROW);
+ try db.check_errc(bind_res);
+ }
+ }
+ return Stmt{ .handle = handle.? };
+ }
+
+ inline fn deinit(self: Stmt) void {
+ _ = c.sqlite3_finalize(self.handle);
+ }
+
+ inline fn eval(self: Stmt) c_int {
+ return c.sqlite3_step(self.handle);
+ }
+
+ fn read_col(self: Stmt, column: usize, comptime T: type) T {
+ switch (@typeInfo(T)) {
+ .int => |int| {
+ if (int.bits <= 64) {
+ return c.sqlite3_column_int64(self.stmt.handle, @intCast(column));
+ }
+ comptime assert(int.bits <= 128);
+ const bytes: [*c]const u8 = @ptrCast(c.sqlite3_column_blob(self.handle, @intCast(column)));
+ const len = c.sqlite3_column_bytes(self.handle, @intCast(column));
+ assert(len == 16);
+ const value = std.mem.bytesAsValue(u128, bytes[0..16]);
+ return std.mem.bigToNative(u128, value.*);
+ },
+ .float => {
+ return c.sqlite3_column_double(self.handle, @intCast(column));
+ },
+ .pointer => |ptr| {
+ if (ptr.child != u8 or ptr.size != .slice) {
+ @compileError("unsupported field type: " ++ @typeName(T));
+ }
+ const text: [*c]const u8 = @ptrCast(c.sqlite3_column_blob(self.handle, @intCast(column)));
+ const len = c.sqlite3_column_bytes(self.handle, @intCast(column));
+ return if (len == 0) "" else text[0..@intCast(len)];
+ },
+ else => @compileError("unsupported field type: " ++ @typeName(T)),
+ }
+ }
+};
+
+const Error = error{
+ Aborted,
+ Busy,
+ CantOpen,
+ Constraint,
+ Corrupted,
+ DiskQuota,
+ Fsync,
+ IO,
+ Lock,
+ PermissionDenied,
+ SystemResources,
+ Unexpected,
+};
+
+fn error_from_code(errc: c_int) Error!void {
+ return switch (errc & 0xff) {
+ c.SQLITE_OK, c.SQLITE_ROW, c.SQLITE_DONE => {},
+ c.SQLITE_ABORT => error.Aborted,
+ c.SQLITE_AUTH => unreachable,
+ c.SQLITE_BUSY => error.Busy,
+ c.SQLITE_CANTOPEN => error.CantOpen,
+ c.SQLITE_CONSTRAINT => error.Constraint,
+ c.SQLITE_CORRUPT => error.Corrupted,
+ c.SQLITE_EMPTY => unreachable,
+ c.SQLITE_ERROR => error.Unexpected,
+ c.SQLITE_FORMAT => unreachable,
+ c.SQLITE_FULL => error.DiskQuota,
+ c.SQLITE_INTERNAL => unreachable,
+ c.SQLITE_INTERRUPT => unreachable,
+ c.SQLITE_IOERR => switch (errc) {
+ c.SQLITE_IOERR_CORRUPTFS => error.Corrupted,
+ c.SQLITE_IOERR_DATA => error.Corrupted,
+ c.SQLITE_IOERR_FSYNC, c.SQLITE_IOERR_DIR_FSYNC => error.Fsync,
+ c.SQLITE_IOERR_LOCK, c.SQLITE_IOERR_RDLOCK => error.Lock,
+ c.SQLITE_IOERR_NOMEM => error.SystemResources,
+ else => error.IO,
+ },
+ c.SQLITE_LOCKED => unreachable,
+ c.SQLITE_MISMATCH => unreachable,
+ c.SQLITE_MISUSE => unreachable,
+ c.SQLITE_NOLFS => error.DiskQuota,
+ c.SQLITE_NOMEM => error.SystemResources,
+ c.SQLITE_NOTADB => error.Corrupted,
+ c.SQLITE_NOTFOUND => unreachable,
+ c.SQLITE_NOTICE => unreachable,
+ c.SQLITE_PERM => error.PermissionDenied,
+ c.SQLITE_PROTOCOL => unreachable,
+ c.SQLITE_RANGE => unreachable,
+ c.SQLITE_READONLY => unreachable,
+ else => unreachable,
+ };
+}