diff --git a/src/Db.zig b/src/Db.zig index c3e6175..7b1b77b 100644 --- a/src/Db.zig +++ b/src/Db.zig @@ -64,6 +64,7 @@ pub fn open(path: [:0]const u8, writable: bool, params: [:0]const u8) Self { }; } +// TODO: Returrn error pub fn set(self: *Self, key: []const u8, value: []const u8) void { const key_len = @intCast(i32, key.len); const value_len = @intCast(i32, value.len); @@ -71,10 +72,68 @@ pub fn set(self: *Self, key: []const u8, value: []const u8) void { _ = c.tkrzw_dbm_set(self.dbm, key.ptr, key_len, value.ptr, value_len, true); } -pub fn get(self: *Self, key: []const u8) []u8 { +// TODO: Returrn error +pub fn append(self: *Self, key: []const u8, value: []const u8, delim: []const u8) void { + const key_len = @intCast(i32, key.len); + const value_len = @intCast(i32, value.len); + const delim_len = @intCast(i32, delim.len); + + _ = c.tkrzw_dbm_append(self.dbm, key.ptr, key_len, value.ptr, value_len, delim.ptr, delim_len); +} + +pub fn check(self: *Self, key: []const u8) bool { + const key_len = @intCast(i32, key.len); + + return c.tkrzw_dbm_check(self.dbm, key.ptr, key_len); +} + +// You gotta call free on return +pub fn get(self: *Self, key: []const u8) ?[]u8 { const key_len = @intCast(i32, key.len); var len: i32 = 0; var ret = c.tkrzw_dbm_get(self.dbm, key.ptr, key_len, &len); - return ret[0..@intCast(usize, len)]; + + return if (ret) |nnret| nnret[0..@intCast(usize, len)] else return null; +} + +pub fn getList(self: *Self, key: []const u8, allocator: std.mem.Allocator) !?[][:0]u8 { + const list_str = self.get(key) orelse return null; + defer Self.free(list_str.ptr); + + const count = std.mem.count(u8, list_str, " ") + 1; + var buffer = try allocator.alloc([:0]u8, count); + + var iter = std.mem.split(u8, list_str, " "); + var i: usize = 0; + while (iter.next()) |unit| { + defer i += 1; + + buffer[i] = try allocator.dupeZ(u8, unit); + } + + return buffer; +} + +/// Helper function to make it easier to find occurrences in lists +pub fn isInList(self: *Self, key: []const u8, needle: []const u8) bool { + if (self.get(key)) |haystack| { + defer Self.free(haystack.ptr); + + if (isInStringDelim(haystack, needle, ' ')) return true; + } + + return false; +} + +/// Generic function to find occurences of a string inside another one with delimiters +fn isInStringDelim(haystack: []const u8, needle: []const u8, delim: u8) bool { + var spliter = std.mem.split(u8, haystack, &[_]u8{delim}); + while (spliter.next()) |blade| { + if (std.mem.eql(u8, blade, needle)) { + return true; + } + } + + return false; } diff --git a/src/Item.zig b/src/Item.zig index 7c62e3e..043c9af 100644 --- a/src/Item.zig +++ b/src/Item.zig @@ -2,17 +2,11 @@ const std = @import("std"); const json = @import("json.zig"); const Db = @import("Db.zig"); -const sqlite = @import("sqlite"); const Self = @This(); +const Tag = [:0]u8; -pub const Tag = struct { - // :0 for C compatility - name: [:0]const u8, - value: ?[:0]const u8, -}; - -id: ?i64, // If null, the object hasn't been persisted +id: ?[4:0]u8, // If null, the object hasn't been persisted tags: ?[]Tag, allocator: ?std.mem.Allocator = null, @@ -22,57 +16,113 @@ pub fn deinit(self: *Self) void { const tags = self.tags orelse return; // Free each tag - for (tags) |*tag| { + for (tags) |tag| { // Free name - allocator.free(tag.name); - - // Free value - if (tag.value) |value| { - allocator.free(value); - } + allocator.free(tag); } // Free the tags buffer allocator.free(tags); } -pub fn persist(self: *Self, db: *sqlite.Db) !void { +const SubtagIterator = struct { + index: ?usize, + orig_tag: []const u8, + + pub fn init(tag: []const u8) @This() { + return @This(){ + .index = 0, + .orig_tag = tag, + }; + } + + pub fn next(self: *@This()) ?[]const u8 { + if (self.index) |index| { + const opt_i = std.mem.indexOfScalarPos(u8, self.orig_tag, index, ':'); + + if (opt_i) |i| { + defer self.index = i+1; + return self.orig_tag[0..i]; + } + + // Use the nullability as a marker to not enter here again + defer self.index = null; + return self.orig_tag[0..]; + } + + return null; + } +}; + +pub fn persist(self: *Self, db: *Db, allocator: std.mem.Allocator) !void { // Insert only if there's no ID. if (self.id == null) { - const query = "INSERT INTO item VALUES(NULL);"; - try db.exec(query, .{}, .{}); + var opt_last_id = db.get("item:last"); + defer if (opt_last_id) |i| Db.free(i.ptr); + + // Get ID. Should be last+1 or "0000" + const id = if (opt_last_id) |last_id| + try Db.numEncode(Db.numDecode(last_id) + 1) + else + try Db.numEncode(0); + + // Insert + db.append("item", &id, " "); + db.set("item:last", &id); // Update the ID field - self.id = try Db.getLastId(db); + self.id = [_:0]u8{0} ** 4; + std.mem.copy(u8, self.id.?[0..], id[0..]); } // If the tags haven't been initialized, don't touch anything if (self.tags) |tags| { - // Create new tags if they don't exist - // TODO: Do the "if they don't exist" part. - for (tags) |*tags_i| { - const name_query = "INSERT OR IGNORE INTO tag (name) VALUES (?)"; - // TODO: This does work but doesn't prevent SQLite3 from crying on stdout - try db.exec(name_query, .{}, .{ .name = tags_i.name }); - const rel_query = "INSERT INTO item_tag (item, tag, value) VALUES (?, ?, ?);"; - try db.exec(rel_query, .{}, .{ .item = self.id, .tag = tags_i.name, .value = tags_i.value }); + const separator = " "; + + var item = "item:----".*; + std.mem.copy(u8, item[5..], self.id.?[0..]); + + for (tags) |tag| { + // Go through every subtag (tag -> tag:subtag -> tag:subtag:subsubtag -> ...) + var iter = SubtagIterator.init(tag); + while (iter.next()) |subtag| { + // allocate "tag:" + var tagkey = try std.mem.concat(allocator, u8, &[_][]const u8{"tag:", subtag}); + defer allocator.free(tagkey); + + // Append the tag to the total of tags if it doesn't exist + // "tag" => " " + if (!db.check(tagkey)) db.append("tag", subtag, separator); + + // Skip if the item is already tagged with it + // TODO: Test if this works, lol + if (db.isInList(&item, subtag)) continue; + + // Insert the tag into the item. + // "item:" => " " + // NOTE: Only assign the whole tag. Ex.: ":" but not "" + if (tag.len == subtag.len) db.append(item[0..], tag, separator); + + // "tag:" => " " + db.append(tagkey, &self.id.?, separator); + } } } } -pub fn getTagsById(id: i64, db: *sqlite.Db, allocator: std.mem.Allocator) !?[]Tag { - var stmt = try db.prepare("SELECT tag, value FROM item_tag WHERE item = ?"); - defer stmt.deinit(); +pub fn getById(id: []const u8, db: *Db, allocator: std.mem.Allocator) !?Self { + var item_sel = "item:----".*; + std.mem.copy(u8, item_sel[5..], id[0..]); - const res = try stmt.all(Tag, allocator, .{}, .{ .id = id }); + const tags = (try db.getList(&item_sel, allocator)) orelse return null; - if (res.len < 1) { - // Detect wether it returns 0 because it has no tags or because it doesn't exist - const exists = (try db.one(i64, "SELECT COUNT(id) FROM item WHERE id = ?", .{}, .{id})).?; - return if (exists == 0) null else res; - } + var aid: [4:0]u8 = "0000".*; + std.mem.copy(u8, &aid, id); - return res; + return Self{ + .id = aid, + .tags = tags, + }; } /// Convert into a JSON object. A call to deinit() is necessary from the caller's side @@ -81,20 +131,15 @@ pub fn toJson(self: Self) json.Obj { var jobj = json.Obj.newObject(); // Add id (i64|null) - jobj.objectAdd("id", if (self.id) |id| &json.Obj.newInt64(id) else null); + jobj.objectAdd("id", if (self.id) |id| &json.Obj.newString(&id) else null); // Add tags only if they're initialized if (self.tags) |tags| { - var jtags = json.Obj.newObject(); + var jtags = json.Obj.newArray(); jobj.objectAdd("tags", &jtags); for (tags) |tag_i| { - const jtag = if (tag_i.value) |value| - &json.Obj.newString(value) - else - null; - - jtags.objectAdd(tag_i.name, jtag); + jtags.arrayAdd(&json.Obj.newString(tag_i)); } } else { // If nothing is found, set it to null @@ -116,12 +161,12 @@ pub fn fromJson(jobj: json.Obj, allocator: std.mem.Allocator) !Self { tags = try tagsFromJson(jobj, allocator); } - var id: ?i64 = null; + var id: ?[4:0]u8 = null; if (jobj.objectGet("id") catch null) |*jid| { defer jid.deinit(); - id = jid.getInt64(); + std.mem.copy(u8, id, jid.getString()); } // TODO: What should be done when both things are null? @@ -136,16 +181,15 @@ pub fn fromJson(jobj: json.Obj, allocator: std.mem.Allocator) !Self { pub fn tagsFromJson(jobj: *json.Obj, allocator: std.mem.Allocator) ![]Tag { // Reserve space for slice of tags - const len = @intCast(usize, jobj.objectLen()); + const len = @intCast(usize, jobj.arrayLen()); var tags = try allocator.alloc(Tag, len); - var iter = jobj.objectGetIterator(); + var iter = jobj.arrayGetIterator(); var i: usize = 0; while (iter.next()) |*tag| { // Whe know it's not null at this point - tags[i].name = try allocator.dupeZ(u8, tag.key); - tags[i].value = if (tag.value) |*value| try allocator.dupeZ(u8, value.getString()) else null; + tags[i] = try allocator.dupeZ(u8, tag.getString()); i += 1; } diff --git a/src/main.zig b/src/main.zig index 9ad5a25..a2aec25 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,60 +9,47 @@ const request = @import("request.zig"); pub fn main() !void { var db = Db.open("test.tkh", true, ""); - db.set("test", "lol"); - db.set("kaka", "jeje"); - db.set("kaka", "poop"); - var str = db.get("test"); - defer Db.free(str.ptr); - std.debug.print("{s}\n", .{str}); + const jsonText: [:0]const u8 = + \\{ "add" : [ + \\ [ + \\ "task:uned:led", + \\ "fur:dusk", + \\ "made_with:krita", + \\ "date:2022:10:01", + \\ "alunya" + \\ ],[ + \\ "fur:lara", + \\ "made_with:krita", + \\ "date:2022:04:08", + \\ "cell-shading" + \\ ],[ + \\ "made_with:ballpoint_pen", + \\ "date:2022:11:04", + \\ "practice", + \\ "pose_practice" + \\ ],[ + \\ "fur:lidiarock1", + \\ "made_with:krita", + \\ "date:2022:10:15", + \\ "niko_(oneshot)" + \\ ] + \\] } + ; - const num = 69420; - const numch = try Db.numEncode(num); - std.debug.print("{}\n", .{Db.numDecode(numch[0..])}); - std.debug.print("{}\n", .{num}); + var jobj = json.Obj.newFromString(jsonText); + defer jobj.deinit(); - //var db = try Db.init(); -// - //const jsonText: [:0]const u8 = - //\\{ "add" : [ - //\\ { - //\\ "fur": "dusk", - //\\ "made_with": "krita", - //\\ "date": "2022-10-01", - //\\ "alunya": null - //\\ },{ - //\\ "fur": "lara", - //\\ "made_with": "krita", - //\\ "date": "2022-04-08", - //\\ "cell-shading": null - //\\ },{ - //\\ "made_with": "ballpoint_pen", - //\\ "date": "2022-11-04", - //\\ "practice": "", - //\\ "pose_practice": null - //\\ },{ - //\\ "fur": "lidiarock1", - //\\ "made_with": "krita", - //\\ "date": "2022-02-15", - //\\ "niko_(oneshot)": null - //\\ } - //\\] } - //; -// - //var jobj = json.Obj.newFromString(jsonText); - //defer jobj.deinit(); -// - //try request.process(&jobj, &db); -// - //std.debug.print("\n\n", .{}); -// - //const jsonQuery: [:0]const u8 = - //\\{ "query" : "niko_(oneshot) made_with:krita -fur:dusk" } - //; -// - //var jquery = json.Obj.newFromString(jsonQuery); - //defer jquery.deinit(); -// - //try request.process(&jquery, &db); + try request.process(&jobj, &db); + + std.debug.print("\n\n", .{}); + + const jsonQuery: [:0]const u8 = + \\{ "query" : "fur made_with:krita -fur:dusk", "limit" : 20 } + ; + + var jquery = json.Obj.newFromString(jsonQuery); + defer jquery.deinit(); + + try request.process(&jquery, &db); } diff --git a/src/request.zig b/src/request.zig index 08137d3..c9a5c69 100644 --- a/src/request.zig +++ b/src/request.zig @@ -1,12 +1,11 @@ const std = @import("std"); -const sqlite = @import("sqlite"); const Db = @import("Db.zig"); const Item = @import("Item.zig"); const Tag = @import("Tag.zig"); const json = @import("json.zig"); -pub fn process(jobj: *json.Obj, db: *sqlite.Db) !void { +pub fn process(jobj: *json.Obj, db: *Db) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -25,25 +24,25 @@ pub fn process(jobj: *json.Obj, db: *sqlite.Db) !void { jret.objectAdd("queried", &ret); } - std.debug.print("{s}", .{ jret.toString() }); + std.debug.print("{s}", .{jret.toString()}); } -pub fn add(jobj: *json.Obj, db: *sqlite.Db, allocator: std.mem.Allocator) !json.Obj { +pub fn add(jobj: *json.Obj, db: *Db, allocator: std.mem.Allocator) !json.Obj { // TODO: Maybe return error when no items in the array? // Freed by the caller var jret = json.Obj.newArray(); var iter = jobj.arrayGetIterator(); - while(iter.next()) |*jtags| { - var item = Item { + while (iter.next()) |*jtags| { + var item = Item{ .id = null, .tags = try Item.tagsFromJson(jtags, allocator), }; item.deinit(); // Insert new items into the DB - try item.persist(db); + try item.persist(db, allocator); // Add item to new json array (Makes a deep copy, freed with jret.deinit()) jret.arrayAdd(&item.toJson()); @@ -52,43 +51,31 @@ pub fn add(jobj: *json.Obj, db: *sqlite.Db, allocator: std.mem.Allocator) !json. return jret; } -pub fn query(jobj: *json.Obj, db: *sqlite.Db, allocator: std.mem.Allocator) !json.Obj { - _ = db; +pub fn query(jobj: *json.Obj, db: *Db, allocator: std.mem.Allocator) !json.Obj { + const query_str = jobj.getString(); var jret = json.Obj.newArray(); - const query_string = jobj.getString(); + // Go through each tag + var iter = std.mem.split(u8, query_str, " "); + const opt_tag = iter.next(); - var keywords = std.ArrayList([]const u8).init(allocator); - var sql = std.ArrayList(u8).init(allocator); - try sql.appendSlice("SELECT * FROM item_tag WHERE"); + if (opt_tag) |tag| { + // Get the tag selector: "tag:" + const tag_sel = try std.mem.concat(allocator, u8, &[_][]const u8{"tag:", tag}); + defer allocator.free(tag_sel); - var iter = std.mem.split(u8, query_string, " "); + // Get the items that have that tag: " " + const tag_str = db.get(tag_sel) orelse return jret; + defer Db.free(tag_str.ptr); - var i: usize = 0; - while(iter.next()) |tag| { - defer i += 1; + // Iterate through the items id + var item_iter = std.mem.split(u8, tag_str, " "); + while (item_iter.next()) |item_id| { + const item = (try Item.getById(item_id, db, allocator)) orelse continue; - try keywords.append(tag); - - // Just put AND between keywords - if (i > 0) { - try sql.appendSlice(" AND"); + jret.arrayAdd(&item.toJson()); } - // Exclude tags that start with ! - - try sql.appendSlice(" tag "); - - if (tag[0] == '-') { - try sql.append('!'); - } - try sql.appendSlice("= ?"); - } - std.debug.print("{s}\n", .{sql.items}); - - //var stmt = try db.prepareDynamic(); - //defer stmt.deinit(); -// return jret; }