| ... | @@ -1,51 +1,42 @@ | ... | @@ -1,51 +1,42 @@ |
| 1 | const std = @import("std"); | 1 | const std = @import("std"); |
| 2 | const string = []const u8; | 2 | const string = []const u8; |
| 3 | const sqlite = @import("sqlite"); | | |
| 4 | const tracer = @import("tracer"); | 3 | const tracer = @import("tracer"); |
| 5 | const extras = @import("extras"); | 4 | const extras = @import("extras"); |
| | 5 | const builtin = @import("builtin"); |
| 6 | | 6 | |
| 7 | const Self = @This(); | 7 | const Self = @This(); |
| 8 | | 8 | |
| 9 | db: sqlite.Db = undefined, | 9 | db: *c.sqlite3, |
| 10 | | 10 | |
| 11 | pub fn connect(allocator: std.mem.Allocator, path: [:0]const u8) !Self { | 11 | pub fn connect(allocator: std.mem.Allocator, path: [:0]const u8) !Self { |
| 12 | std.log.scoped(.zorm).info("connecting to {s} @ {s}", .{ "sqlite3", path }); | 12 | std.log.scoped(.zorm).info("connecting to {s} @ {s}", .{ "sqlite3", path }); |
| 13 | _ = allocator; | 13 | _ = allocator; |
| 14 | return Self{ | 14 | var db: ?*c.sqlite3 = null; |
| 15 | .db = try sqlite.Db.init(.{ | 15 | var flags: c_int = 0; |
| 16 | .mode = .{ .File = path }, | 16 | flags |= c.SQLITE_OPEN_READWRITE; |
| 17 | .open_flags = .{ | 17 | flags |= c.SQLITE_OPEN_CREATE; |
| 18 | .write = true, | 18 | flags |= c.SQLITE_OPEN_FULLMUTEX; |
| 19 | .create = true, | 19 | s.assert(c.sqlite3_open_v2(path, &db, flags, null)); |
| 20 | }, | 20 | std.debug.assert(c.sqlite3_threadsafe() > 0); |
| 21 | .threading_mode = .SingleThread, | 21 | return .{ .db = db.? }; |
| 22 | }), | | |
| 23 | }; | | |
| 24 | } | 22 | } |
| 25 | | 23 | |
| 26 | pub fn close(self: *Self) void { | 24 | pub fn close(self: *Self) void { |
| 27 | self.db.deinit(); | 25 | s.assert(c.sqlite3_close_v2(self.db)); |
| 28 | } | | |
| 29 | | | |
| 30 | fn prepare(self: *Self, comptime query: string) !sqlite.StatementType(.{}, query) { | | |
| 31 | return self.db.prepare(query) catch |err| switch (err) { | | |
| 32 | error.SQLiteError => std.debug.panic("`{s}`: {f}", .{ query, self.db.getDetailedError() }), | | |
| 33 | else => return err, | | |
| 34 | }; | | |
| 35 | } | 26 | } |
| 36 | | 27 | |
| 37 | pub fn collect(self: *Self, alloc: std.mem.Allocator, comptime T: type, comptime query: string, args: anytype) ![]T { | 28 | pub fn collect(self: *Self, alloc: std.mem.Allocator, comptime T: type, comptime query: string, args: anytype) ![]T { |
| 38 | const t = tracer.trace(@src(), " {s}", .{query}); | 29 | const t = tracer.trace(@src(), " {s}", .{query}); |
| 39 | defer t.end(); | 30 | defer t.end(); |
| 40 | | 31 | |
| 41 | var stmt = try self.prepare(query); | | |
| 42 | defer stmt.deinit(); | | |
| 43 | var iter = try stmt.iteratorAlloc(T, alloc, args); | | |
| 44 | var list = std.array_list.Managed(T).init(alloc); | 32 | var list = std.array_list.Managed(T).init(alloc); |
| 45 | errdefer list.deinit(); | 33 | errdefer list.deinit(); |
| 46 | while (try iter.nextAlloc(alloc, .{})) |row| { | 34 | var stmt: Statement = try .prepare(self, query); |
| 47 | try list.append(row); | 35 | defer stmt.finalize(); |
| 48 | } | 36 | try stmt.bindArgs(alloc, args); |
| | 37 | const iter = stmt.iterate(); |
| | 38 | errdefer iter.reset(); |
| | 39 | while (try iter.step(alloc, T)) |row| try list.append(row); |
| 49 | return list.toOwnedSlice(); | 40 | return list.toOwnedSlice(); |
| 50 | } | 41 | } |
| 51 | | 42 | |
| ... | @@ -53,29 +44,29 @@ pub fn exec(self: *Self, alloc: std.mem.Allocator, comptime query: string, args: | ... | @@ -53,29 +44,29 @@ pub fn exec(self: *Self, alloc: std.mem.Allocator, comptime query: string, args: |
| 53 | const t = tracer.trace(@src(), " {s}", .{query}); | 44 | const t = tracer.trace(@src(), " {s}", .{query}); |
| 54 | defer t.end(); | 45 | defer t.end(); |
| 55 | | 46 | |
| 56 | var stmt = try self.prepare(query); | 47 | var stmt: Statement = try .prepare(self, query); |
| 57 | defer stmt.deinit(); | 48 | defer stmt.finalize(); |
| 58 | try stmt.execAlloc(alloc, .{}, args); | 49 | try stmt.bindArgs(alloc, args); |
| | 50 | try stmt.exec(alloc); |
| 59 | } | 51 | } |
| 60 | | 52 | |
| 61 | pub fn first(self: *Self, alloc: std.mem.Allocator, comptime T: type, comptime query: string, args: anytype) !?T { | 53 | pub fn first(self: *Self, alloc: std.mem.Allocator, comptime T: type, comptime query: string, args: anytype) !?T { |
| 62 | const t = tracer.trace(@src(), " {s}", .{query}); | 54 | const t = tracer.trace(@src(), " {s}", .{query}); |
| 63 | defer t.end(); | 55 | defer t.end(); |
| 64 | | 56 | |
| 65 | var stmt = try self.prepare(query); | 57 | var stmt: Statement = try .prepare(self, query); |
| 66 | defer stmt.deinit(); | 58 | defer stmt.finalize(); |
| 67 | return try stmt.oneAlloc(T, alloc, .{}, args); | 59 | try stmt.bindArgs(alloc, args); |
| 68 | } | 60 | const iter = stmt.iterate(); |
| 69 | | 61 | errdefer iter.reset(); |
| 70 | pub fn prepareDynamic(self: *Self, query: string) !sqlite.DynamicStatement { | 62 | return iter.step(alloc, T); |
| 71 | return self.db.prepareDynamic(query); | | |
| 72 | } | 63 | } |
| 73 | | 64 | |
| 74 | pub fn doesTableExist(self: *Self, alloc: std.mem.Allocator, name: string) !bool { | 65 | pub fn doesTableExist(self: *Self, alloc: std.mem.Allocator, name: string) !bool { |
| 75 | const t = tracer.trace(@src(), " {s}", .{name}); | 66 | const t = tracer.trace(@src(), " {s}", .{name}); |
| 76 | defer t.end(); | 67 | defer t.end(); |
| 77 | | 68 | |
| 78 | for (try self.collect(alloc, string, "select name from sqlite_master where type=? AND name=?", .{ .type = "table", .name = name })) |item| { | 69 | for (try self.collect(alloc, string, "select name from sqlite_master where type = ? AND name = ?", .{ .type = "table", .name = name })) |item| { |
| 79 | if (std.mem.eql(u8, item, name)) { | 70 | if (std.mem.eql(u8, item, name)) { |
| 80 | return true; | 71 | return true; |
| 81 | } | 72 | } |
| ... | @@ -187,3 +178,255 @@ pub const pragma = struct { | ... | @@ -187,3 +178,255 @@ pub const pragma = struct { |
| 187 | return try self.collect(alloc, Pragma.TableInfo, "pragma table_info(" ++ name ++ ")", .{}); | 178 | return try self.collect(alloc, Pragma.TableInfo, "pragma table_info(" ++ name ++ ")", .{}); |
| 188 | } | 179 | } |
| 189 | }; | 180 | }; |
| | 181 | |
| | 182 | pub const c = @cImport({ |
| | 183 | @cInclude("sqlite3.h"); |
| | 184 | }); |
| | 185 | |
| | 186 | pub const s = struct { |
| | 187 | const Error = error{ |
| | 188 | SQLITE_ERROR, |
| | 189 | SQLITE_INTERNAL, |
| | 190 | SQLITE_PERM, |
| | 191 | SQLITE_ABORT, |
| | 192 | SQLITE_BUSY, |
| | 193 | SQLITE_LOCKED, |
| | 194 | OutOfMemory, |
| | 195 | SQLITE_READONLY, |
| | 196 | SQLITE_INTERRUPT, |
| | 197 | SQLITE_IOERR, |
| | 198 | SQLITE_CORRUPT, |
| | 199 | SQLITE_NOTFOUND, |
| | 200 | SQLITE_FULL, |
| | 201 | SQLITE_CANTOPEN, |
| | 202 | SQLITE_PROTOCOL, |
| | 203 | SQLITE_EMPTY, |
| | 204 | SQLITE_SCHEMA, |
| | 205 | SQLITE_TOOBIG, |
| | 206 | SQLITE_CONSTRAINT, |
| | 207 | SQLITE_MISMATCH, |
| | 208 | SQLITE_MISUSE, |
| | 209 | SQLITE_NOLFS, |
| | 210 | SQLITE_AUTH, |
| | 211 | SQLITE_FORMAT, |
| | 212 | SQLITE_RANGE, |
| | 213 | SQLITE_NOTADB, |
| | 214 | SQLITE_NOTICE, |
| | 215 | SQLITE_WARNING, |
| | 216 | }; |
| | 217 | pub fn rc2e(code: c_int) Error { |
| | 218 | if (code == c.SQLITE_ERROR) return error.SQLITE_ERROR; |
| | 219 | if (code == c.SQLITE_INTERNAL) return error.SQLITE_INTERNAL; |
| | 220 | if (code == c.SQLITE_PERM) return error.SQLITE_PERM; |
| | 221 | if (code == c.SQLITE_ABORT) return error.SQLITE_ABORT; |
| | 222 | if (code == c.SQLITE_BUSY) return error.SQLITE_BUSY; |
| | 223 | if (code == c.SQLITE_LOCKED) return error.SQLITE_LOCKED; |
| | 224 | if (code == c.SQLITE_NOMEM) return error.OutOfMemory; |
| | 225 | if (code == c.SQLITE_READONLY) return error.SQLITE_READONLY; |
| | 226 | if (code == c.SQLITE_INTERRUPT) return error.SQLITE_INTERRUPT; |
| | 227 | if (code == c.SQLITE_IOERR) return error.SQLITE_IOERR; |
| | 228 | if (code == c.SQLITE_CORRUPT) return error.SQLITE_CORRUPT; |
| | 229 | if (code == c.SQLITE_NOTFOUND) return error.SQLITE_NOTFOUND; |
| | 230 | if (code == c.SQLITE_FULL) return error.SQLITE_FULL; |
| | 231 | if (code == c.SQLITE_CANTOPEN) return error.SQLITE_CANTOPEN; |
| | 232 | if (code == c.SQLITE_PROTOCOL) return error.SQLITE_PROTOCOL; |
| | 233 | if (code == c.SQLITE_EMPTY) return error.SQLITE_EMPTY; |
| | 234 | if (code == c.SQLITE_SCHEMA) return error.SQLITE_SCHEMA; |
| | 235 | if (code == c.SQLITE_TOOBIG) return error.SQLITE_TOOBIG; |
| | 236 | if (code == c.SQLITE_CONSTRAINT) return error.SQLITE_CONSTRAINT; |
| | 237 | if (code == c.SQLITE_MISMATCH) return error.SQLITE_MISMATCH; |
| | 238 | if (code == c.SQLITE_MISUSE) return error.SQLITE_MISUSE; |
| | 239 | if (code == c.SQLITE_NOLFS) return error.SQLITE_NOLFS; |
| | 240 | if (code == c.SQLITE_AUTH) return error.SQLITE_AUTH; |
| | 241 | if (code == c.SQLITE_FORMAT) return error.SQLITE_FORMAT; |
| | 242 | if (code == c.SQLITE_RANGE) return error.SQLITE_RANGE; |
| | 243 | if (code == c.SQLITE_NOTADB) return error.SQLITE_NOTADB; |
| | 244 | if (code == c.SQLITE_NOTICE) return error.SQLITE_NOTICE; |
| | 245 | if (code == c.SQLITE_WARNING) return error.SQLITE_WARNING; |
| | 246 | unreachable; |
| | 247 | } |
| | 248 | pub fn rc2p(code: c_int) Error { |
| | 249 | if (builtin.mode == .Debug) @panic(std.mem.sliceTo(c.sqlite3_errstr(code), 0)); |
| | 250 | return rc2e(code); |
| | 251 | } |
| | 252 | pub fn assert(code: c_int) void { |
| | 253 | if (code == c.SQLITE_OK) return; |
| | 254 | @panic(std.mem.sliceTo(c.sqlite3_errstr(code), 0)); |
| | 255 | } |
| | 256 | pub fn please(code: c_int) !void { |
| | 257 | if (code == c.SQLITE_OK) return; |
| | 258 | return rc2p(code); |
| | 259 | } |
| | 260 | }; |
| | 261 | |
| | 262 | pub const Statement = struct { |
| | 263 | stmt: *c.sqlite3_stmt, |
| | 264 | |
| | 265 | pub fn prepare(driver: *Self, query: []const u8) !Statement { |
| | 266 | var stmt: ?*c.sqlite3_stmt = null; |
| | 267 | var flags: c_uint = 0; |
| | 268 | _ = &flags; |
| | 269 | try s.please(c.sqlite3_prepare_v3(driver.db, query.ptr, @intCast(query.len), flags, &stmt, null)); |
| | 270 | return .{ .stmt = stmt.? }; |
| | 271 | } |
| | 272 | |
| | 273 | pub fn finalize(stmt: Statement) void { |
| | 274 | s.assert(c.sqlite3_finalize(stmt.stmt)); |
| | 275 | } |
| | 276 | |
| | 277 | pub fn bindArgs(stmt: Statement, allocator: std.mem.Allocator, args: anytype) !void { |
| | 278 | if (comptime extras.isSlice(@TypeOf(args))) { |
| | 279 | for (args, 0..) |a, i| { |
| | 280 | const A = @TypeOf(a); |
| | 281 | try bindType(stmt, allocator, i + 1, A, a); |
| | 282 | } |
| | 283 | return; |
| | 284 | } |
| | 285 | inline for (@typeInfo(@TypeOf(args)).@"struct".fields, 0..) |f, i| { |
| | 286 | try bindType(stmt, allocator, i + 1, f.type, @field(args, f.name)); |
| | 287 | } |
| | 288 | } |
| | 289 | |
| | 290 | fn bindType(stmt: Statement, allocator: std.mem.Allocator, idx: usize, T: type, value: T) !void { |
| | 291 | if (comptime extras.isZigString(T)) { |
| | 292 | return s.please(c.sqlite3_bind_text64(stmt.stmt, @intCast(idx), value.ptr, value.len, c.SQLITE_STATIC, c.SQLITE_UTF8)); |
| | 293 | } |
| | 294 | if (comptime extras.isArrayOf(u8)(T)) { |
| | 295 | return s.please(c.sqlite3_bind_blob64(stmt.stmt, @intCast(idx), &value, value.len, c.SQLITE_TRANSIENT)); |
| | 296 | } |
| | 297 | switch (@typeInfo(T)) { |
| | 298 | .@"struct" => |info| { |
| | 299 | if (@hasDecl(T, "BaseType")) return bindBaseType(stmt, allocator, idx, T, value); |
| | 300 | if (info.layout == .@"packed") return bindType(stmt, allocator, idx, info.backing_integer.?, @bitCast(value)); |
| | 301 | return bindBaseType(stmt, allocator, idx, T, value); |
| | 302 | }, |
| | 303 | .int => |info| { |
| | 304 | comptime std.debug.assert(info.bits <= 64); |
| | 305 | if (value > std.math.maxInt(c.sqlite_int64)) return error.Overflow; |
| | 306 | if (value < std.math.minInt(c.sqlite_int64)) return error.Overflow; |
| | 307 | return s.please(c.sqlite3_bind_int64(stmt.stmt, @intCast(idx), @intCast(value))); |
| | 308 | }, |
| | 309 | .optional => |info| { |
| | 310 | if (value == null) return s.please(c.sqlite3_bind_null(stmt.stmt, @intCast(idx))); |
| | 311 | return bindType(stmt, allocator, idx, info.child, value.?); |
| | 312 | }, |
| | 313 | .bool => { |
| | 314 | return bindType(stmt, allocator, idx, u1, @intFromBool(value)); |
| | 315 | }, |
| | 316 | .@"enum" => { |
| | 317 | if (T.BaseType == []const u8 and !@hasDecl(T, "bindField")) { |
| | 318 | return bindType(stmt, allocator, idx, []const u8, @tagName(value)); |
| | 319 | } |
| | 320 | return bindBaseType(stmt, allocator, idx, T, value); |
| | 321 | }, |
| | 322 | .@"union" => { |
| | 323 | switch (value) { |
| | 324 | inline else => |val| return bindType(stmt, allocator, idx, @TypeOf(val), val), |
| | 325 | } |
| | 326 | }, |
| | 327 | else => @compileError(@typeName(T)), |
| | 328 | } |
| | 329 | } |
| | 330 | |
| | 331 | fn bindBaseType(stmt: Statement, allocator: std.mem.Allocator, idx: usize, T: type, value: T) !void { |
| | 332 | const bind_fn = T.bindField; |
| | 333 | const info = @typeInfo(@TypeOf(bind_fn)).@"fn"; |
| | 334 | switch (info.params.len) { |
| | 335 | 1 => { |
| | 336 | const base = try value.bindField(); |
| | 337 | return bindType(stmt, allocator, idx, @TypeOf(base), base); |
| | 338 | }, |
| | 339 | 2 => { |
| | 340 | const base = try value.bindField(allocator); |
| | 341 | return bindType(stmt, allocator, idx, @TypeOf(base), base); |
| | 342 | }, |
| | 343 | else => comptime unreachable, |
| | 344 | } |
| | 345 | } |
| | 346 | |
| | 347 | pub fn exec(stmt: Statement, allocator: std.mem.Allocator) !void { |
| | 348 | const iter = stmt.iterate(); |
| | 349 | errdefer iter.reset(); |
| | 350 | const row = try iter.step(allocator, void); |
| | 351 | if (builtin.mode == .Debug) std.debug.assert(row == null); |
| | 352 | } |
| | 353 | |
| | 354 | pub fn iterate(stmt: Statement) Iterator { |
| | 355 | return .{ .stmt = stmt.stmt }; |
| | 356 | } |
| | 357 | |
| | 358 | pub const Iterator = struct { |
| | 359 | stmt: *c.sqlite3_stmt, |
| | 360 | |
| | 361 | pub fn reset(iter: Iterator) void { |
| | 362 | return s.please(c.sqlite3_reset(iter.stmt)) catch {}; |
| | 363 | } |
| | 364 | |
| | 365 | pub fn step(iter: Iterator, allocator: std.mem.Allocator, T: type) !?T { |
| | 366 | const code = c.sqlite3_step(iter.stmt); |
| | 367 | if (code == c.SQLITE_DONE) return null; |
| | 368 | if (code != c.SQLITE_ROW) return s.rc2p(code); |
| | 369 | if (T == void) return; |
| | 370 | if (T == string) return try readType(iter, allocator, 0, string); |
| | 371 | if (@typeInfo(T) == .int) return try readType(iter, allocator, 0, T); |
| | 372 | var result: T = undefined; |
| | 373 | inline for (@typeInfo(T).@"struct".fields, 0..) |f, i| { |
| | 374 | @field(result, f.name) = try readType(iter, allocator, i, f.type); |
| | 375 | } |
| | 376 | return result; |
| | 377 | } |
| | 378 | |
| | 379 | fn readType(iter: Iterator, allocator: std.mem.Allocator, idx: usize, T: type) !T { |
| | 380 | if (comptime extras.isZigString(T)) { |
| | 381 | const res = c.sqlite3_column_text(iter.stmt, @intCast(idx)); |
| | 382 | if (res == null) return ""; |
| | 383 | const ptr: [*:0]const u8 = @ptrCast(res); |
| | 384 | const len = c.sqlite3_column_bytes(iter.stmt, @intCast(idx)); |
| | 385 | return try allocator.dupe(u8, ptr[0..@intCast(len) :0]); |
| | 386 | } |
| | 387 | if (comptime extras.isArrayOf(u8)(T)) { |
| | 388 | const info = @typeInfo(T).array; |
| | 389 | const ptr: [*:0]const u8 = @ptrCast(c.sqlite3_column_blob(iter.stmt, @intCast(idx))); |
| | 390 | const len = c.sqlite3_column_bytes(iter.stmt, @intCast(idx)); |
| | 391 | std.debug.assert(len == info.len); |
| | 392 | return ptr[0..info.len].*; |
| | 393 | } |
| | 394 | switch (@typeInfo(T)) { |
| | 395 | .int => |info| { |
| | 396 | comptime std.debug.assert(info.bits <= 64); |
| | 397 | const res = c.sqlite3_column_int64(iter.stmt, @intCast(idx)); |
| | 398 | if (res > std.math.maxInt(T)) return error.Overflow; |
| | 399 | if (res < std.math.minInt(T)) return error.Overflow; |
| | 400 | return @intCast(res); |
| | 401 | }, |
| | 402 | .bool => { |
| | 403 | return @bitCast(try readType(iter, allocator, idx, u1)); |
| | 404 | }, |
| | 405 | .@"struct" => |info| { |
| | 406 | if (@hasDecl(T, "BaseType")) return readBaseType(iter, allocator, idx, T); |
| | 407 | if (info.layout == .@"packed") return @bitCast(try readType(iter, allocator, idx, info.backing_integer.?)); |
| | 408 | return readBaseType(iter, allocator, idx, T); |
| | 409 | }, |
| | 410 | .optional => |info| { |
| | 411 | if (c.sqlite3_column_type(iter.stmt, @intCast(idx)) == c.SQLITE_NULL) return null; |
| | 412 | return try readType(iter, allocator, idx, info.child); |
| | 413 | }, |
| | 414 | .@"enum" => { |
| | 415 | if (T.BaseType == string) { |
| | 416 | const ptr: [*:0]const u8 = @ptrCast(c.sqlite3_column_text(iter.stmt, @intCast(idx))); |
| | 417 | const len = c.sqlite3_column_bytes(iter.stmt, @intCast(idx)); |
| | 418 | const str = ptr[0..@intCast(len) :0]; |
| | 419 | const enm = std.meta.stringToEnum(T, str); |
| | 420 | return enm orelse T.default; |
| | 421 | } |
| | 422 | }, |
| | 423 | else => {}, // else => @compileError(T), // https://codeberg.org/ziglang/zig/issues/32119 |
| | 424 | } |
| | 425 | } |
| | 426 | |
| | 427 | fn readBaseType(iter: Iterator, allocator: std.mem.Allocator, idx: usize, T: type) !T { |
| | 428 | const base = try readType(iter, allocator, idx, T.BaseType); |
| | 429 | return T.readField(allocator, base); |
| | 430 | } |
| | 431 | }; |
| | 432 | }; |