Add postgres support in db package
This commit is contained in:
parent
c8641247fe
commit
e94e4384bb
6 changed files with 469 additions and 223 deletions
|
@ -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");
|
||||
|
|
44
src/sql/common.zig
Normal file
44
src/sql/common.zig
Normal file
|
@ -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"),
|
||||
},
|
||||
};
|
||||
}
|
291
src/sql/lib.zig
291
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) },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
120
src/sql/postgres.zig
Normal file
120
src/sql/postgres.zig
Normal file
|
@ -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,
|
||||
};
|
||||
}
|
||||
};
|
230
src/sql/sqlite.zig
Normal file
230
src/sql/sqlite.zig
Normal file
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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"),
|
||||
|
|
Loading…
Reference in a new issue