const sqlite = @import("sqlite"); const std = @import("std"); const models = @import("models.zig"); const utils = @import("../utils.zig"); const Allocator = std.mem.Allocator; const print = std.debug.print; pub const Db = struct { allocator: Allocator, _mode: ?sqlite.Db.Mode, _sql_db: sqlite.Db, pub fn init(allocator: Allocator, db_path: ?[]const u8, mode: ?sqlite.Db.Mode) !Db { const path: [:0]const u8 = if (db_path != null) try std.mem.Allocator.dupeZ(allocator, u8, db_path.?) else "./data.db"; defer allocator.free(path); var sqlDb = try sqlite.Db.init(.{ .mode = if (mode != null) mode.? else sqlite.Db.Mode{ .File = path }, .open_flags = .{ .write = true, .create = true, }, .threading_mode = .MultiThread, }); return Db{ .allocator = allocator, ._mode = mode, ._sql_db = sqlDb }; } pub fn deinit(self: *Db) void { self._sql_db.deinit(); } pub fn selectAllWhere( self: *Db, comptime Type: type, allocator: Allocator, comptime where_clause: []const u8, values: anytype, comptime order_by_field: ?[]const u8, comptime order: ?[]const u8, comptime limit: ?bool, limit_val: ?u32, ) !?[]Type { comptime { if (order == null and order_by_field != null or order != null and order_by_field == null) { @compileError("Must provide both order and order_by or neither, with select " ++ where_clause); } if (order != null) { if (!(std.mem.eql(u8, order.?, "DESC") or std.mem.eql(u8, order.?, "ASC"))) { @compileError("Must use ASC or DESC for order_by, used: " ++ order.?); } } if (!std.mem.containsAtLeast(u8, where_clause, 1, "?")) { @compileError("where_clause missing '?', no possible values to insert " ++ where_clause); } // Check that where_clause only contains fields in struct var query_objs_iter = std.mem.split(u8, where_clause, "?"); inline for (@typeInfo(@TypeOf(values)).Struct.fields) |struct_field| { const name = struct_field.name; const query_obj = query_objs_iter.next(); if (query_obj == null) { @compileError("Query does not have enough clauses for passed in data: " ++ where_clause); } if (std.mem.containsAtLeast(u8, query_obj.?, 1, name)) { continue; } else { @compileError("Missing field or messed up order in select:\n" ++ where_clause ++ "\n" ++ query_obj.?); } } const last = query_objs_iter.next(); if (last != null and !std.mem.eql(u8, last.?, "")) { @compileError("Values is lacking, query contains more ? than data provided: " ++ where_clause ++ "\nLeft with: " ++ last.?); } } if (limit == null and limit_val != null or limit != null and limit_val == null) { std.log.err("Must provide both limit and limit_val or neither, with select: {s}", .{where_clause}); return null; } var res_array: std.ArrayList(Type) = std.ArrayList(Type).init(allocator); if (order_by_field == null and limit == null) { const query = "SELECT * FROM " ++ models.getTypeTableName(Type) ++ " " ++ where_clause ++ ";"; var stmt = try self._sql_db.prepare(query); defer stmt.deinit(); var iter = try stmt.iteratorAlloc(Type, allocator, values); while (try iter.nextAlloc(allocator, .{})) |row| { try res_array.append(row); } } else if (order_by_field == null and limit != null) { const query = "SELECT * FROM " ++ models.getTypeTableName(Type) ++ " " ++ where_clause ++ " LIMIT ?;"; var stmt = try self._sql_db.prepare(query); defer stmt.deinit(); var iter = try stmt.iteratorAlloc(Type, allocator, utils.structConcatFields(values, .{ .limit = limit_val.? })); while (try iter.nextAlloc(allocator, .{})) |row| { try res_array.append(row); } } else if (order_by_field != null and limit == null) { const query = "SELECT * FROM " ++ models.getTypeTableName(Type) ++ " " ++ where_clause ++ " ORDER BY " ++ order_by_field.? ++ " " ++ order.? ++ ";"; var stmt = try self._sql_db.prepare(query); defer stmt.deinit(); var iter = try stmt.iteratorAlloc(Type, allocator, values); while (try iter.nextAlloc(allocator, .{})) |row| { try res_array.append(row); } } else { const query = "SELECT * FROM " ++ models.getTypeTableName(Type) ++ " " ++ where_clause ++ " ORDER BY " ++ order_by_field.? ++ " " ++ order.? ++ " LIMIT ?;"; var stmt = try self._sql_db.prepare(query); defer stmt.deinit(); var iter = try stmt.iteratorAlloc(Type, allocator, utils.structConcatFields(values, .{ .limit = limit_val.? })); while (try iter.nextAlloc(allocator, .{})) |row| { try res_array.append(row); } } return try res_array.toOwnedSlice(); } pub fn selectOneById(self: *Db, comptime Type: type, allocator: Allocator, id: u32) !?Type { const row = try self._sql_db.oneAlloc(Type, allocator, models.createSelectOnIdQuery(Type), .{}, .{ .id = id }); // std.debug.print("{any}", .{row}); return row; } pub fn selectOne(self: *Db, comptime Type: type, allocator: Allocator, comptime query: []const u8, values: anytype) !?Type { comptime { var query_objs_iter = std.mem.split(u8, query, "="); inline for (@typeInfo(@TypeOf(values)).Struct.fields) |struct_field| { const name = struct_field.name; const query_obj = query_objs_iter.next(); if (query_obj == null) { @compileError("Query does not have enough clauses for passed in data:\n" ++ "Type: " ++ @typeName(Type) ++ "\n" ++ query ++ "\n"); } if (std.mem.containsAtLeast(u8, query_obj.?, 1, name)) { continue; } else { @compileError("Missing field or messed up order in select:\n" ++ query ++ "\n" ++ query_obj.?); } } } const row = try self._sql_db.oneAlloc(Type, allocator, query, .{}, values); // std.debug.print("{any}", .{row}); return row; } pub fn updateById(self: *Db, comptime Type: type, values: anytype) !void { // TODO check there is an ID field const data = utils.structConcatFields(values, .{ .id2 = values.id }); self._sql_db.exec(models.createUpdateQuery(Type), .{}, data) catch |err| { std.debug.print("Encountered error while updating data:\n{any}\n\tQuery:{s}\n{any}\n", .{ values, models.createUpdateQuery(Type), err }); return err; }; } pub fn insert(self: *Db, comptime Type: type, values: anytype) !void { comptime { const query = models.createInsertQuery(Type); // const InsertType = utils.removeStructFields(Type, &[_]u8{0}); var query_objs_iter = std.mem.split(u8, query, ","); inline for (@typeInfo(@TypeOf(values)).Struct.fields) |struct_field| { const name = struct_field.name; const query_obj = query_objs_iter.next(); if (query_obj == null) { @compileError("Query does not have enough clauses for passed in data:\n" ++ "Type: " ++ @typeName(Type) ++ "\n" ++ query ++ "\n"); } if (std.mem.containsAtLeast(u8, query_obj.?, 1, name)) { continue; } else { @compileError("Missing field or messed up order in insert:\n" ++ query ++ "\n" ++ query_obj.? ++ "\n"); } } } self._sql_db.exec(models.createInsertQuery(Type), .{}, values) catch |err| { std.debug.print("Encountered error while inserting data:\n\t{any}\nQuery:\t{s}\n{any}\n", .{ values, models.createInsertQuery(Type), err }); return err; }; } pub fn deleteById(self: *Db, comptime Type: type, id: u32) !void { // TODO check there is an ID field self._sql_db.exec(models.createDeleteOnIdQuery(Type), .{}, .{ .id = id }) catch |err| { std.debug.print("Encountered error while deleting id {}\n\tQuery:{s}\n{any}\n", .{ id, models.createInsertQuery(Type), err }); return err; }; } pub fn migrateDb(self: *Db) !void { print("Making migration in DB...\n", .{}); _ = try self._sql_db.pragma(void, .{}, "foreign_keys", "1"); inline for (models.ModelTypes) |model_type| { self._sql_db.exec(models.createTableMigrationQuery(model_type), .{}, .{}) catch |err| { std.debug.print("Encountered error while executing migration\n\tQuery:{s}\n{any}\n", .{ models.createTableMigrationQuery(model_type), err }); return err; }; } } pub fn wipeAndMigrateDb(self: *Db) !void { print("Wiping DB data and schema...\n", .{}); inline for (models.ModelTypes) |model_type| { self._sql_db.exec(models.createTableDeleteQuery(model_type), .{}, .{}) catch |err| { std.debug.print("Encountered error while executing table deletion\n\tQuery:{s}\n{any}\n", .{ models.createTableDeleteQuery(model_type), err }); return err; }; } try self.migrateDb(); } fn formatQuery( query: []const u8, ) []const u8 { const max_chars = 260; return if (query.len > max_chars) query[0..max_chars] ++ "..." else query; } };