231 lines
7.8 KiB
Zig
231 lines
7.8 KiB
Zig
|
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);
|
||
|
}
|
||
|
};
|