Lots of changes. Fully implement tkrzw

This commit is contained in:
Dendy 2022-10-31 08:55:35 +01:00
parent 895fc2b359
commit ce9a7ed56b
4 changed files with 219 additions and 142 deletions

View File

@ -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;
}

View File

@ -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:<tagname>"
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" => "<tag1> <tag2> <tag3> <tag4>"
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:<id>" => "<tag1> <tag2> <tag3>"
// NOTE: Only assign the whole tag. Ex.: "<tag>:<subtag>" but not "<tag>"
if (tag.len == subtag.len) db.append(item[0..], tag, separator);
// "tag:<tag>" => "<id1> <id2> <id3>"
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;
}

View File

@ -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);
}

View File

@ -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:<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: "<item1> <item2> <item3>"
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;
}