From 44e48a5276ef016665d7e49423607a080a26c514 Mon Sep 17 00:00:00 2001 From: MrBounty Date: Mon, 7 Oct 2024 00:40:24 +0200 Subject: [PATCH] Big rework - Now use global ziql parser - still buggy, need to debug the tests --- build.zig | 26 +- src/ADD.zig | 184 ---- src/GRAB.zig | 433 -------- src/cliParser.zig | 122 +++ src/{data-parsing.zig => dataParser.zig} | 0 src/dbconsole.zig | 211 ---- src/dbengine.zig | 55 - src/{dataEngine.zig => fileEngine.zig} | 122 +-- src/metadata.zig | 31 - src/schemaEngine.zig | 100 ++ src/{cliTokenizer.zig => tokenizers/cli.zig} | 0 .../schema.zig} | 0 .../ziql.zig} | 103 +- src/types/dataType.zig | 11 + src/{ => types}/uuid.zig | 0 src/ziqlParser.zig | 946 ++++++++++++++++++ test_runner.zig | 298 ++++++ 17 files changed, 1615 insertions(+), 1027 deletions(-) delete mode 100644 src/ADD.zig delete mode 100644 src/GRAB.zig create mode 100644 src/cliParser.zig rename src/{data-parsing.zig => dataParser.zig} (100%) delete mode 100644 src/dbconsole.zig delete mode 100644 src/dbengine.zig rename src/{dataEngine.zig => fileEngine.zig} (84%) delete mode 100644 src/metadata.zig create mode 100644 src/schemaEngine.zig rename src/{cliTokenizer.zig => tokenizers/cli.zig} (100%) rename src/{schemaTokenizer.zig => tokenizers/schema.zig} (100%) rename src/{ziqlTokenizer.zig => tokenizers/ziql.zig} (83%) create mode 100644 src/types/dataType.zig rename src/{ => types}/uuid.zig (100%) create mode 100644 src/ziqlParser.zig create mode 100644 test_runner.zig diff --git a/build.zig b/build.zig index 1acb250..91da368 100644 --- a/build.zig +++ b/build.zig @@ -6,7 +6,7 @@ pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{}); const exe = b.addExecutable(.{ .name = "zippon", - .root_source_file = b.path("src/dbconsole.zig"), + .root_source_file = b.path("src/cliParser.zig"), .target = target, .optimize = optimize, }); @@ -20,7 +20,7 @@ pub fn build(b: *std.Build) void { // Test step const tests1 = b.addTest(.{ - .root_source_file = b.path("src/data-parsing.zig"), + .root_source_file = b.path("src/dataParser.zig"), .target = target, .optimize = optimize, .name = "Data parsing", @@ -28,7 +28,7 @@ pub fn build(b: *std.Build) void { const run_tests1 = b.addRunArtifact(tests1); const tests2 = b.addTest(.{ - .root_source_file = b.path("src/cliTokenizer.zig"), + .root_source_file = b.path("src/tokenizers/cli.zig"), .target = target, .optimize = optimize, .name = "CLI tokenizer", @@ -36,7 +36,7 @@ pub fn build(b: *std.Build) void { const run_tests2 = b.addRunArtifact(tests2); const tests3 = b.addTest(.{ - .root_source_file = b.path("src/ziqlTokenizer.zig"), + .root_source_file = b.path("src/tokenizers/ziql.zig"), .target = target, .optimize = optimize, .name = "ZiQL tokenizer", @@ -44,7 +44,7 @@ pub fn build(b: *std.Build) void { const run_tests3 = b.addRunArtifact(tests3); const tests4 = b.addTest(.{ - .root_source_file = b.path("src/schemaTokenizer.zig"), + .root_source_file = b.path("src/tokenizers/schema.zig"), .target = target, .optimize = optimize, .name = "Schema tokenizer", @@ -52,7 +52,7 @@ pub fn build(b: *std.Build) void { const run_tests4 = b.addRunArtifact(tests4); const tests5 = b.addTest(.{ - .root_source_file = b.path("src/uuid.zig"), + .root_source_file = b.path("src/types/uuid.zig"), .target = target, .optimize = optimize, .name = "UUID", @@ -60,13 +60,22 @@ pub fn build(b: *std.Build) void { const run_tests5 = b.addRunArtifact(tests5); const tests6 = b.addTest(.{ - .root_source_file = b.path("src/GRAB.zig"), + .root_source_file = b.path("src/fileEngine.zig"), .target = target, .optimize = optimize, - .name = "GRAB", + .name = "File Engine", }); const run_tests6 = b.addRunArtifact(tests6); + const tests7 = b.addTest(.{ + .root_source_file = b.path("src/ziqlParser.zig"), + .target = target, + .optimize = optimize, + .name = "ZiQL parser", + //.test_runner = b.path("test_runner.zig"), + }); + const run_tests7 = b.addRunArtifact(tests7); + const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_tests1.step); test_step.dependOn(&run_tests2.step); @@ -74,4 +83,5 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&run_tests4.step); test_step.dependOn(&run_tests5.step); test_step.dependOn(&run_tests6.step); + test_step.dependOn(&run_tests7.step); } diff --git a/src/ADD.zig b/src/ADD.zig deleted file mode 100644 index 027e656..0000000 --- a/src/ADD.zig +++ /dev/null @@ -1,184 +0,0 @@ -const std = @import("std"); -const metadata = @import("metadata.zig"); -const UUID = @import("uuid.zig").UUID; -const Tokenizer = @import("ziqlTokenizer.zig").Tokenizer; -const Token = @import("ziqlTokenizer.zig").Token; -const DataEngine = @import("dataEngine.zig").DataEngine; -const Allocator = std.mem.Allocator; - -const stdout = std.io.getStdOut().writer(); - -// Query that need to work now -// ADD User (name='Adrien', email='adrien.bouvais@gmail.com') OK -// ADD User (name='Adrien', email='adrien.bouvais@gmail.com', age = 26) OK -// ADD User (name='Adrien', email='adrien.bouvais@gmail.com', books = ['book1', 'book2']) OK -// ADD User (name='Adrien', email=null) OK -// -// For later: links -// ADD User (name = 'Adrien', best_friend = {name='bob'}, friends = {name != 'bob'}) NOT OK -// ADD User (name = 'Adrien', friends = {(name = 'bob' AND age > 16) OR (id = '0000-0000')} ) NOT OK -// TODO: make real test - -/// Function for the ADD query command. -/// It will parse the reste of the query and create a map of member name / value. -/// Then add those value to the appropriete file. The proper file is the first one with a size < to the limit. -/// If no file is found, a new one is created. -/// Take the main.zippondata file, the index of the file where the data is saved and the string to add at the end of the line -pub const Parser = struct { - allocator: Allocator, - toker: *Tokenizer, - - pub fn init(allocator: Allocator, toker: *Tokenizer) Parser { - return Parser{ - .allocator = allocator, - .toker = toker, - }; - } - - pub fn parse(self: *Parser) !void { - var data_engine = DataEngine.init(self.allocator, null); - defer data_engine.deinit(); - - var struct_name_token = self.toker.next(); - const struct_name = self.toker.getTokenSlice(struct_name_token); - - if (!metadata.isStructNameExists(struct_name)) self.print_error("Struct not found in current schema", &struct_name_token); - - var token = self.toker.next(); - switch (token.tag) { - .l_paren => {}, - else => { - self.print_error("Error: Expected (", &token); - }, - } - - var data_map = self.parseData(struct_name); - defer data_map.deinit(); - - if (self.checkIfAllMemberInMap(struct_name, &data_map)) { - try data_engine.writeEntity(struct_name, data_map); - } else |_| {} - } - - /// Take the tokenizer and return a map of the query for the ADD command. - /// Keys are the member name and value are the string of the value in the query. E.g. 'Adrien' or '10' - /// TODO: Make it clean using a State like other parser - pub fn parseData(self: *Parser, struct_name: []const u8) std.StringHashMap([]const u8) { - var token = self.toker.next(); - - var member_map = std.StringHashMap([]const u8).init(self.allocator); - - while (token.tag != Token.Tag.eof) : (token = self.toker.next()) { - switch (token.tag) { - .r_paren => continue, - .identifier => { - const member_name_str = self.toker.getTokenSlice(token); - - if (!metadata.isMemberNameInStruct(struct_name, member_name_str)) self.print_error("Member not found in struct.", &token); - token = self.toker.next(); - switch (token.tag) { - .equal => { - token = self.toker.next(); - switch (token.tag) { - .string_literal, .number_literal => { - const value_str = self.toker.getTokenSlice(token); - member_map.put(member_name_str, value_str) catch self.print_error("Could not add member name and value to map in getMapOfMember", &token); - token = self.toker.next(); - switch (token.tag) { - .comma, .r_paren => continue, - else => self.print_error("Error: Expected , after string or number. E.g. ADD User (name='bob', age=10)", &token), - } - }, - .keyword_null => { - const value_str = "null"; - member_map.put(member_name_str, value_str) catch self.print_error("Error: 001", &token); - token = self.toker.next(); - switch (token.tag) { - .comma, .r_paren => continue, - else => self.print_error("Error: Expected , after string or number. E.g. ADD User (name='bob', age=10)", &token), - } - }, - // Create a tag to prevent creating an array then join them. Instead just read the buffer from [ to ] in the tekenizer itself - .l_bracket => { - var array_values = std.ArrayList([]const u8).init(self.allocator); - token = self.toker.next(); - while (token.tag != Token.Tag.r_bracket) : (token = self.toker.next()) { - switch (token.tag) { - .string_literal, .number_literal => { - const value_str = self.toker.getTokenSlice(token); - array_values.append(value_str) catch self.print_error("Could not add value to array in getMapOfMember", &token); - }, - else => self.print_error("Error: Expected string or number in array. E.g. ADD User (scores=[10 20 30])", &token), - } - } - // Maybe change that as it just recreate a string that is already in the buffer - const array_str = std.mem.join(self.allocator, " ", array_values.items) catch { - self.print_error("Couln't join the value of array", &token); - @panic("=)"); - }; - member_map.put(member_name_str, array_str) catch self.print_error("Could not add member name and value to map in getMapOfMember", &token); - - token = self.toker.next(); - switch (token.tag) { - .comma, .r_paren => continue, - else => self.print_error("Error: Expected , after string or number. E.g. ADD User (name='bob', age=10)", &token), - } - }, - else => self.print_error("Error: Expected string or number after =. E.g. ADD User (name='bob')", &token), - } - }, - else => self.print_error("Error: Expected = after a member declaration. E.g. ADD User (name='bob')", &token), - } - }, - else => self.print_error("Error: Unknow token. This should be the name of a member. E.g. name in ADD User (name='bob')", &token), - } - } - - return member_map; - } - - const AddError = error{NotAllMemberInMap}; - - fn checkIfAllMemberInMap(_: *Parser, struct_name: []const u8, map: *std.StringHashMap([]const u8)) !void { - const all_struct_member = metadata.structName2structMembers(struct_name); - var count: u16 = 0; - var started_printing = false; - - for (all_struct_member) |key| { - if (map.contains(key)) count += 1 else { - if (!started_printing) { - try stdout.print("Error: ADD query of struct: {s}; missing member: {s}", .{ struct_name, key }); - started_printing = true; - } else { - try stdout.print(" {s}", .{key}); - } - } - } - - if (started_printing) try stdout.print("\n", .{}); - - if (!((count == all_struct_member.len) and (count == map.count()))) return error.NotAllMemberInMap; - } - - fn print_error(self: *Parser, message: []const u8, token: *Token) void { - stdout.print("\n", .{}) catch {}; - stdout.print("{s}\n", .{self.toker.buffer}) catch {}; - - // Calculate the number of spaces needed to reach the start position. - var spaces: usize = 0; - while (spaces < token.loc.start) : (spaces += 1) { - stdout.print(" ", .{}) catch {}; - } - - // Print the '^' characters for the error span. - var i: usize = token.loc.start; - while (i < token.loc.end) : (i += 1) { - stdout.print("^", .{}) catch {}; - } - stdout.print(" \n", .{}) catch {}; // Align with the message - - stdout.print("{s}\n", .{message}) catch {}; - - @panic(""); - } -}; diff --git a/src/GRAB.zig b/src/GRAB.zig deleted file mode 100644 index b5a62ac..0000000 --- a/src/GRAB.zig +++ /dev/null @@ -1,433 +0,0 @@ -const std = @import("std"); -const metadata = @import("metadata.zig"); -const Allocator = std.mem.Allocator; -const Tokenizer = @import("ziqlTokenizer.zig").Tokenizer; -const Token = @import("ziqlTokenizer.zig").Token; -const DataEngine = @import("dataEngine.zig").DataEngine; -const UUID = @import("uuid.zig").UUID; - -// To work now -// GRAB User {} -// GRAB User {name = 'Adrien'} -// GRAB User {name='Adrien' AND age < 30} -// GRAB User [1] {} -// GRAB User [10; name] {age < 30} -// -// For later - -const stdout = std.io.getStdOut().writer(); - -pub const Parser = struct { - arena: std.heap.ArenaAllocator, - allocator: Allocator, - toker: *Tokenizer, - state: State, - - additional_data: AdditionalData, - - pub fn init(allocator: Allocator, toker: *Tokenizer) Parser { - var arena = std.heap.ArenaAllocator.init(allocator); - return Parser{ - .arena = arena, - .allocator = arena.allocator(), - .toker = toker, - .state = State.start, - .additional_data = AdditionalData.init(allocator), - }; - } - - pub fn deinit(self: *Parser) void { - self.additional_data.deinit(); - self.arena.deinit(); - } - - // This is the [] part - pub const AdditionalData = struct { - entity_count_to_find: usize = 0, - member_to_find: std.ArrayList(AdditionalDataMember), - - pub fn init(allocator: Allocator) AdditionalData { - return AdditionalData{ .member_to_find = std.ArrayList(AdditionalDataMember).init(allocator) }; - } - - pub fn deinit(self: *AdditionalData) void { - for (0..self.member_to_find.items.len) |i| { - std.debug.print("{d}\n", .{i}); - self.member_to_find.items[i].additional_data.deinit(); - } - - self.member_to_find.deinit(); - } - }; - - // This is name in: [name] - // There is an additional data because it can be [friend [1; name]] - const AdditionalDataMember = struct { - name: []const u8, - additional_data: AdditionalData, - - pub fn init(allocator: Allocator, name: []const u8) AdditionalDataMember { - const additional_data = AdditionalData.init(allocator); - return AdditionalDataMember{ .name = name, .additional_data = additional_data }; - } - }; - - const State = enum { - start, - invalid, - end, - - // For the main parse function - expect_filter, - - // For the additional data parser - expect_count_of_entity_to_find, - expect_semicolon_OR_right_bracket, - expect_member, - next_member_OR_end_OR_new_additional_data, - next_member_OR_end, - - // For the filter parser - expect_condition, - }; - - pub fn parse(self: *Parser) !void { - var data_engine = DataEngine.init(self.allocator, null); - defer data_engine.deinit(); - - var struct_name_token = self.toker.next(); - if (!self.isStructInSchema(self.toker.getTokenSlice(struct_name_token))) { - try self.printError("Error: Struct name not in current shema.", &struct_name_token); - return; - } - - var token = self.toker.next(); - var keep_next = false; - - while (self.state != State.end) : ({ - token = if (!keep_next) self.toker.next() else token; - keep_next = false; - }) { - switch (self.state) { - .start => { - switch (token.tag) { - .l_bracket => { - try self.parseAdditionalData(&self.additional_data); - self.state = State.expect_filter; - }, - .l_brace => { - self.state = State.expect_filter; - keep_next = true; - }, - else => { - try self.printError("Error: Expected filter starting with {} or what to return starting with []", &token); - return; - }, - } - }, - .expect_filter => { - var array = std.ArrayList(UUID).init(self.allocator); - try self.parseFilter(&array, struct_name_token); - self.state = State.end; - }, - else => return, - } - } - } - - fn parseFilter(self: *Parser, left_array: *std.ArrayList(UUID), struct_name_token: Token) !void { - const right_array = std.ArrayList(UUID).init(self.allocator); - var token = self.toker.next(); - var keep_next = false; - self.state = State.expect_member; - - _ = right_array; - _ = left_array; - - while (self.state != State.end) : ({ - token = if (!keep_next) self.toker.next() else token; - keep_next = false; - }) { - switch (self.state) { - .expect_member => { - if (!self.isMemberPartOfStruct(self.toker.getTokenSlice(struct_name_token), self.toker.getTokenSlice(token))) { - try self.printError("Error: Member not part of struct.", &token); - } - self.state = State.expect_condition; - }, - else => return, - } - } - } - - /// When this function is call, the tokenizer last token retrieved should be [. - /// Check if an int is here -> check if ; is here -> check if member is here -> check if [ is here -> loop - pub fn parseAdditionalData(self: *Parser, additional_data: *AdditionalData) !void { - var token = self.toker.next(); - var keep_next = false; - self.state = State.expect_count_of_entity_to_find; - - while (self.state != State.end) : ({ - token = if (!keep_next) self.toker.next() else token; - keep_next = false; - }) { - switch (self.state) { - .expect_count_of_entity_to_find => { - switch (token.tag) { - .number_literal => { - const count = std.fmt.parseInt(usize, self.toker.getTokenSlice(token), 10) catch { - try self.printError("Error while transforming this into a integer.", &token); - self.state = .invalid; - continue; - }; - additional_data.entity_count_to_find = count; - self.state = .expect_semicolon_OR_right_bracket; - }, - else => { - self.state = .expect_member; - keep_next = true; - }, - } - }, - .expect_semicolon_OR_right_bracket => { - switch (token.tag) { - .semicolon => { - self.state = .expect_member; - }, - .r_bracket => { - return; - }, - else => { - try self.printError( - "Error: Expect ';' or ']'.", - &token, - ); - self.state = .invalid; - }, - } - }, - .expect_member => { - switch (token.tag) { - .identifier => { - // TODO: Check if the member name exist - try additional_data.member_to_find.append( - AdditionalDataMember.init( - self.allocator, - self.toker.getTokenSlice(token), - ), - ); - - self.state = .next_member_OR_end_OR_new_additional_data; - }, - else => { - try self.printError( - "Error: A member name should be here.", - &token, - ); - }, - } - }, - .next_member_OR_end_OR_new_additional_data => { - switch (token.tag) { - .comma => { - self.state = .expect_member; - }, - .r_bracket => { - return; - }, - .l_bracket => { - try self.parseAdditionalData( - &additional_data.member_to_find.items[additional_data.member_to_find.items.len - 1].additional_data, - ); - self.state = .next_member_OR_end; - }, - else => { - try self.printError( - "Error: Expected a comma ',' or the end or a new list of member to return.", - &token, - ); - }, - } - }, - .next_member_OR_end => { - switch (token.tag) { - .comma => { - self.state = .expect_member; - }, - .r_bracket => { - return; - }, - else => { - try self.printError( - "Error: Expected a comma or the end of the list of member name to return.", - &token, - ); - }, - } - }, - .invalid => { - @panic("=)"); - }, - else => { - try self.printError( - "Error: Unknow state.", - &token, - ); - }, - } - } - } - - fn printError(self: *Parser, message: []const u8, token: *Token) !void { - try stdout.print("\n", .{}); - try stdout.print("{s}\n", .{self.toker.buffer}); - - // Calculate the number of spaces needed to reach the start position. - var spaces: usize = 0; - while (spaces < token.loc.start) : (spaces += 1) { - try stdout.print(" ", .{}); - } - - // Print the '^' characters for the error span. - var i: usize = token.loc.start; - while (i < token.loc.end) : (i += 1) { - try stdout.print("^", .{}); - } - try stdout.print(" \n", .{}); // Align with the message - - try stdout.print("{s}\n", .{message}); - - @panic(""); - } - - /// Take a struct name and a member name and return true if the member name is part of the struct - fn isMemberPartOfStruct(_: *Parser, struct_name: []const u8, member_name: []const u8) bool { - const all_struct_member = metadata.structName2structMembers(struct_name); - - for (all_struct_member) |key| { - if (std.mem.eql(u8, key, member_name)) return true; - } - - return false; - } - - /// Check if a string is a name of a struct in the currently use engine - fn isStructInSchema(_: *Parser, struct_name_to_check: []const u8) bool { - for (metadata.struct_name_list) |struct_name| { - if (std.mem.eql(u8, struct_name_to_check, struct_name)) { - return true; - } - } - return false; - } -}; - -// TODO: Optimize. Maybe just do a new list and return it instead -fn OR(arr1: *std.ArrayList(UUID), arr2: *std.ArrayList(UUID)) std.ArrayList(UUID) { - defer arr1.deinit(); - defer arr2.deinit(); - - var arr = try arr1.clone(); - - for (0..arr2.items.len) |i| { - if (!arr.contains(arr2[i])) { - arr.append(arr2[i]); - } - } - - return arr; -} - -fn AND(arr1: *std.ArrayList(UUID), arr2: *std.ArrayList(UUID)) std.ArrayList(UUID) { - defer arr1.deinit(); - defer arr2.deinit(); - - var arr = try arr1.clone(); - - for (0..arr1.items.len) |i| { - if (arr2.contains(arr1[i])) { - arr.append(arr1[i]); - } - } - - return arr; -} - -test "Test AdditionalData" { - const allocator = std.testing.allocator; - - var additional_data1 = Parser.AdditionalData.init(allocator); - additional_data1.entity_count_to_find = 1; - testAdditionalData("[1]", additional_data1); - - var additional_data2 = Parser.AdditionalData.init(allocator); - defer additional_data2.deinit(); - try additional_data2.member_to_find.append( - Parser.AdditionalDataMember.init( - allocator, - "name", - ), - ); - testAdditionalData("[name]", additional_data2); - - var additional_data3 = Parser.AdditionalData.init(allocator); - additional_data3.entity_count_to_find = 1; - defer additional_data3.deinit(); - try additional_data3.member_to_find.append( - Parser.AdditionalDataMember.init( - allocator, - "name", - ), - ); - testAdditionalData("[1; name]", additional_data3); - - var additional_data4 = Parser.AdditionalData.init(allocator); - additional_data4.entity_count_to_find = 100; - defer additional_data4.deinit(); - try additional_data4.member_to_find.append( - Parser.AdditionalDataMember.init( - allocator, - "friend", - ), - ); - testAdditionalData("[100; friend [name]]", additional_data4); -} - -fn testAdditionalData(source: [:0]const u8, expected_AdditionalData: Parser.AdditionalData) void { - const allocator = std.testing.allocator; - var tokenizer = Tokenizer.init(source); - var data_engine = DataEngine.init(allocator); - defer data_engine.deinit(); - - var parser = Parser.init(allocator, &tokenizer, &data_engine); - - defer parser.deinit(); - _ = tokenizer.next(); - parser.parse_additional_data(&parser.additional_data) catch |err| { - std.debug.print("Error parsing additional data: {any}\n", .{err}); - }; - - compareAdditionalData(expected_AdditionalData, parser.additional_data); -} - -// TODO: Check AdditionalData inside AdditionalData -fn compareAdditionalData(ad1: Parser.AdditionalData, ad2: Parser.AdditionalData) void { - std.testing.expectEqual(ad1.entity_count_to_find, ad2.entity_count_to_find) catch { - std.debug.print("Additional data entity_count_to_find are not equal.\n", .{}); - }; - - var founded = false; - - for (ad1.member_to_find.items) |elem1| { - founded = false; - for (ad2.member_to_find.items) |elem2| { - if (std.mem.eql(u8, elem1.name, elem2.name)) { - compareAdditionalData(elem1.additional_data, elem2.additional_data); - founded = true; - break; - } - } - - if (!founded) @panic("Member not found"); - } -} diff --git a/src/cliParser.zig b/src/cliParser.zig new file mode 100644 index 0000000..1015e8f --- /dev/null +++ b/src/cliParser.zig @@ -0,0 +1,122 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const cliTokenizer = @import("tokenizers/cli.zig").Tokenizer; +const cliToken = @import("tokenizers/cli.zig").Token; +const ziqlTokenizer = @import("tokenizers/ziql.zig").Tokenizer; +const ziqlToken = @import("tokenizers/ziql.zig").Token; +const ziqlParser = @import("ziqlParser.zig").Parser; + +const stdout = std.io.getStdOut().writer(); + +pub fn main() !void { + // TODO: Use an environment variable for the path of the DB + checkAndCreateDirectories(); + + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + defer { + switch (gpa.deinit()) { + .ok => std.debug.print("No memory leak baby !\n", .{}), + .leak => { + std.debug.print("We fucked it up bro...\n", .{}); + @panic("=("); + }, + } + } + + const line_buf = try allocator.alloc(u8, 1024 * 50); + defer allocator.free(line_buf); + + while (true) { + std.debug.print("> ", .{}); + const line = try std.io.getStdIn().reader().readUntilDelimiterOrEof(line_buf, '\n'); + + if (line) |line_str| { + const null_term_line_str = try allocator.dupeZ(u8, line_str[0..line_str.len]); + defer allocator.free(null_term_line_str); + + var cliToker = cliTokenizer.init(null_term_line_str); + const command_token = cliToker.next(); + switch (command_token.tag) { + .keyword_run => { + const query_token = cliToker.next(); + switch (query_token.tag) { + .string_literal => { + const null_term_query_str = try allocator.dupeZ(u8, line_str[query_token.loc.start + 1 .. query_token.loc.end - 1]); + defer allocator.free(null_term_query_str); + try runCommand(null_term_query_str); + }, + .keyword_help => std.debug.print("The run command will take a ZiQL query between \" and run it. eg: run \"GRAB User\"\n"), + else => std.debug.print("After command run, need a string of a query, eg: \"GRAB User\"\n", .{}), + } + }, + .keyword_schema => { + const second_token = cliToker.next(); + + switch (second_token.tag) { + .keyword_describe => try runCommand("__DESCRIBE__"), + .keyword_build => std.debug.print("Need to do the SchemaEngine tu update and migrate the schema"), + .keyword_help => { + std.debug.print("{s}", .{ + \\Here are all available options to use with the schema command: + \\ + \\describe Print the schema use by the current engine. + \\build Build a new engine using a schema file. Args => filename: str, path of schema file to use. Default 'schema.zipponschema'. + \\ + }); + }, + else => std.debug.print("schema available options: describe, build & help\n", .{}), + } + }, + .keyword_help => { + std.debug.print("{s}", .{ + \\Welcome to ZipponDB! + \\ + \\run To run a query. Args => query: str, the query to execute. + \\schema Build a new engine and print current schema. + \\kill To stop the process without saving + \\save Save the database to the normal files. + \\dump Create a new folder with all data as copy. Args => foldername: str, the name of the folder. + \\bump Replace current data with a previous dump. Args => foldername: str, the name of the folder. + \\ + }); + }, + .keyword_quit => break, + .eof => {}, + else => std.debug.print("Command need to start with a keyword, including: run, schema, help and quit\n", .{}), + } + } + } +} + +pub fn runCommand(null_term_query_str: [:0]const u8) !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + var toker = ziqlTokenizer.init(null_term_query_str); + + var parser = ziqlParser.init(allocator, &toker); + defer parser.deinit(); + + try parser.parse(); +} + +fn checkAndCreateDirectories() void { + const cwd = std.fs.cwd(); + + cwd.makeDir("ZipponDB") catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => @panic("Error other than path already exists when trying to create the ZipponDB directory.\n"), + }; + + cwd.makeDir("ZipponDB/DATA") catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => @panic("Error other than path already exists when trying to create the DATA directory.\n"), + }; + + cwd.makeDir("ZipponDB/ENGINE") catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => @panic("Error other than path already exists when trying to create the ENGINE directory.\n"), + }; +} diff --git a/src/data-parsing.zig b/src/dataParser.zig similarity index 100% rename from src/data-parsing.zig rename to src/dataParser.zig diff --git a/src/dbconsole.zig b/src/dbconsole.zig deleted file mode 100644 index b2cd8c4..0000000 --- a/src/dbconsole.zig +++ /dev/null @@ -1,211 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const cliTokenizer = @import("cliTokenizer.zig").Tokenizer; -const cliToken = @import("cliTokenizer.zig").Token; -const schemaTokenizer = @import("schemaTokenizer.zig").Tokenizer; -const schemaToken = @import("schemaTokenizer.zig").Token; -const schemaParser = @import("schemaParser.zig").Parser; - -pub fn main() !void { - checkAndCreateDirectories(); - - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); - - while (true) { - std.debug.print("> ", .{}); - var line_buf: [1024]u8 = undefined; - const line = try std.io.getStdIn().reader().readUntilDelimiterOrEof(&line_buf, '\n'); - - if (line) |line_str| { - const null_term_line_str = try allocator.dupeZ(u8, line_str[0..line_str.len]); - - var cliToker = cliTokenizer.init(null_term_line_str); - const command_token = cliToker.next(); - switch (command_token.tag) { - .keyword_run => { - const query_token = cliToker.next(); - switch (query_token.tag) { - .string_literal => { - const null_term_query_str = try allocator.dupeZ(u8, line_str[query_token.loc.start + 1 .. query_token.loc.end - 1]); - runCommand(null_term_query_str); - }, - else => { - std.debug.print("After command run, need a string of a query, eg: \"GRAB User\"\n", .{}); - continue; - }, - } - }, - .keyword_schema => { - const second_token = cliToker.next(); - - switch (second_token.tag) { - .keyword_describe => { - runCommand("__DESCRIBE__"); - }, - .keyword_build => { - const file_name_token = cliToker.next(); - var file_name = try allocator.alloc(u8, 1024); - var len: usize = 0; - - switch (file_name_token.tag) { - .eof => { - std.mem.copyForwards(u8, file_name, "schema.zipponschema"); - len = 19; - }, - else => file_name = line_str[file_name_token.loc.start..file_name_token.loc.end], - } - - std.debug.print("{s}", .{file_name[0..len]}); - - //blk: { - //createDtypeFile(file_name[0..len]) catch |err| switch (err) { - // error.FileNotFound => { - // std.debug.print("Error: Can't find file: {s}\n", .{file_name[0..len]}); - // break :blk; - // }, - // else => { - // std.debug.print("Error: Unknow error when creating Dtype file: {any}\n", .{err}); - // break :blk; - // }, - //}; - try buildEngine(); - //} - allocator.free(file_name); - }, - .keyword_help => { - std.debug.print("{s}", .{ - \\Here are all available options to use with the schema command: - \\ - \\describe Print the schema use by the current engine. - \\build Build a new engine using a schema file. Args => filename: str, path of schema file to use. Default 'schema.zipponschema'. - \\ - }); - }, - else => { - std.debug.print("schema available options: describe, build & help\n", .{}); - }, - } - }, - .keyword_help => { - std.debug.print("{s}", .{ - \\Welcome to ZipponDB! - \\ - \\run To run a query. Args => query: str, the query to execute. - \\schema Build a new engine and print current schema. - \\kill To stop the process without saving - \\save Save the database to the normal files. - \\dump Create a new folder with all data as copy. Args => foldername: str, the name of the folder. - \\bump Replace current data with a previous dump. Args => foldername: str, the name of the folder. - \\ - }); - }, - .keyword_quit => { - break; - }, - .eof => {}, - else => { - std.debug.print("Command need to start with a keyword, including: run, schema, help and quit\n", .{}); - }, - } - } - } -} - -fn createDtypeFile(file_path: []const u8) !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); - - const cwd = std.fs.cwd(); - var file = try cwd.openFile(file_path, .{ - .mode = .read_only, - }); - defer file.close(); - - const buffer = try file.readToEndAlloc(allocator, 1024); - const file_contents = try allocator.dupeZ(u8, buffer[0..]); // Duplicate to a null terminated string - - var schemaToker = schemaTokenizer.init(file_contents); - var parser = schemaParser.init(); - parser.parse(&schemaToker, buffer); - - // Free memory - allocator.free(buffer); - allocator.free(file_contents); - const check = gpa.deinit(); - switch (check) { - .ok => return, - .leak => std.debug.print("Error: Leak in createDtypeFile!\n", .{}), - } -} - -fn buildEngine() !void { - const argv = &[_][]const u8{ - "zig", - "build-exe", - "src/dbengine.zig", - "--name", - "engine", - }; - - var child = std.process.Child.init(argv, std.heap.page_allocator); - try child.spawn(); - _ = try child.wait(); - - runCommand("__INIT__"); -} - -fn runCommand(null_term_query_str: [:0]const u8) void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); - - // TODO: Use the folder ENGINE - const args = &[_][]const u8{ "./engine", null_term_query_str }; - - const result = std.process.Child.run(.{ .allocator = allocator, .argv = args }) catch |err| switch (err) { - error.FileNotFound => { - std.debug.print("No engine found, please use `schema build` to make one.\n", .{}); - return; - }, - else => { - std.debug.print("Error: Unknow error when trying to run the engine: {any}\n", .{err}); - return; - }, - }; - switch (result.term) { - .Exited => {}, - .Signal => std.debug.print("Error: term signal in runCommand\n", .{}), - .Stopped => std.debug.print("Error: term stopped in runCommand\n", .{}), - .Unknown => std.debug.print("Error: term unknow in runCommand\n", .{}), - } - - std.debug.print("{s}\n", .{result.stdout}); - - allocator.free(result.stdout); - allocator.free(result.stderr); - - const check = gpa.deinit(); - switch (check) { - .ok => return, - .leak => std.debug.print("Error: Leak in runCommand!\n", .{}), - } -} - -fn checkAndCreateDirectories() void { - const cwd = std.fs.cwd(); - - cwd.makeDir("ZipponDB") catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => @panic("Error other than path already exists when trying to create the ZipponDB directory.\n"), - }; - - cwd.makeDir("ZipponDB/DATA") catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => @panic("Error other than path already exists when trying to create the DATA directory.\n"), - }; - - cwd.makeDir("ZipponDB/ENGINE") catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => @panic("Error other than path already exists when trying to create the ENGINE directory.\n"), - }; -} diff --git a/src/dbengine.zig b/src/dbengine.zig deleted file mode 100644 index aa07c9b..0000000 --- a/src/dbengine.zig +++ /dev/null @@ -1,55 +0,0 @@ -const std = @import("std"); -const DataEngine = @import("dataEngine.zig").DataEngine; -const Tokenizer = @import("ziqlTokenizer.zig").Tokenizer; -const grabParser = @import("GRAB.zig").Parser; -const addParser = @import("ADD.zig").Parser; - -pub const Error = error{UUIDNotFound}; -const stdout = std.io.getStdOut().writer(); - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); - - const buffer = try allocator.alloc(u8, 1024); - defer allocator.free(buffer); - - var args = try std.process.argsWithAllocator(allocator); - defer args.deinit(); - - // Remove the first argument - _ = args.next(); - const null_term_query_str = args.next().?; - - var toker = Tokenizer.init(null_term_query_str); - const first_token = toker.next(); - - switch (first_token.tag) { - .keyword_grab => { - var parser = grabParser.init(allocator, &toker); - try parser.parse(); - }, - .keyword_add => { - var parser = addParser.init(allocator, &toker); - parser.parse() catch |err| { - try stdout.print("Error: {any} while parsin ADD.\n", .{err}); - }; - }, - .keyword_update => { - try stdout.print("Not yet implemented.\n", .{}); - }, - .keyword_delete => { - try stdout.print("Not yet implemented.\n", .{}); - }, - .keyword__describe__ => { - try stdout.print("{s}", .{@embedFile("schema.zipponschema")}); - }, - .keyword__init__ => { - var data_engine = DataEngine.init(allocator, null); - try data_engine.initDataFolder(); - }, - else => { - try stdout.print("Query need to start with a keyword, including: GRAB ADD UPDATE DELETE\n", .{}); - }, - } -} diff --git a/src/dataEngine.zig b/src/fileEngine.zig similarity index 84% rename from src/dataEngine.zig rename to src/fileEngine.zig index ae0b960..61f79ef 100644 --- a/src/dataEngine.zig +++ b/src/fileEngine.zig @@ -1,13 +1,16 @@ const std = @import("std"); -const dataParsing = @import("data-parsing.zig"); -const metadata = @import("metadata.zig"); +const dataParsing = @import("dataParser.zig"); +const schemaEngine = @import("schemaEngine.zig"); const Allocator = std.mem.Allocator; -const UUID = @import("uuid.zig").UUID; +const UUID = @import("types/uuid.zig").UUID; +const DataType = @import("types/dataType.zig").DataType; const stdout = std.io.getStdOut().writer(); +//TODO: Create a union class and chose between file and memory + /// Manage everything that is relate to read or write in files /// Or even get stats, whatever. If it touch files, it's here -pub const DataEngine = struct { +pub const FileEngine = struct { allocator: Allocator, dir: std.fs.Dir, // The path to the DATA folder max_file_size: usize = 1e+8, // 100mb @@ -31,18 +34,6 @@ pub const DataEngine = struct { inferior_or_equal, }; - /// Suported dataType for the DB - const DataType = enum { - int, - float, - str, - bool_, - int_array, - float_array, - str_array, - bool_array, - }; - const ComparisonValue = union { int: i64, float: f64, @@ -58,31 +49,41 @@ pub const DataEngine = struct { /// An Operation from equal, different, superior, superior_or_equal, ... /// The DataType from int, float and str /// TODO: Change the value to be the right type and not just a string all the time - const Condition = struct { + pub const Condition = struct { struct_name: []const u8, - member_name: []const u8, - value: []const u8, - operation: Operation, - data_type: DataType, + member_name: []const u8 = undefined, + value: []const u8 = undefined, + operation: Operation = undefined, + data_type: DataType = undefined, + + pub fn init(struct_name: []const u8) Condition { + return Condition{ .struct_name = struct_name }; + } }; - pub fn init(allocator: Allocator, DATA_path: ?[]const u8) DataEngine { + pub fn init(allocator: Allocator, DATA_path: ?[]const u8) FileEngine { const path = DATA_path orelse "ZipponDB/DATA"; const dir = std.fs.cwd().openDir(path, .{}) catch @panic("Error opening ZipponDB/DATA"); - return DataEngine{ + return FileEngine{ .allocator = allocator, .dir = dir, }; } - pub fn deinit(self: *DataEngine) void { + pub fn deinit(self: *FileEngine) void { self.dir.close(); } /// Take a condition and an array of UUID and fill the array with all UUID that match the condition - pub fn getUUIDListUsingCondition(self: *DataEngine, condition: Condition, uuid_array: *std.ArrayList(UUID)) !void { - const file_names = self.getFilesNames(condition.struct_name, condition.member_name) catch @panic("Can't get list of files"); - defer self.deinitFilesNames(&file_names); + pub fn getUUIDListUsingCondition(self: *FileEngine, condition: Condition, uuid_array: *std.ArrayList(UUID)) !void { + var file_names = std.ArrayList([]const u8).init(self.allocator); + self.getFilesNames(condition.struct_name, condition.member_name, &file_names) catch @panic("Can't get list of files"); + defer { + for (file_names.items) |elem| { + self.allocator.free(elem); + } + file_names.deinit(); + } const sub_path = std.fmt.allocPrint( self.allocator, @@ -92,7 +93,6 @@ pub const DataEngine = struct { defer self.allocator.free(sub_path); var file = self.dir.openFile(sub_path, .{}) catch @panic("Can't open first file to init a data iterator"); - // defer self.allocator.free(sub_path); var output: [1024 * 50]u8 = undefined; // Maybe need to increase that as it limit the size of a line in files var output_fbs = std.io.fixedBufferStream(&output); @@ -108,12 +108,21 @@ pub const DataEngine = struct { .int => compare_value = ComparisonValue{ .int = dataParsing.parseInt(condition.value) }, .str => compare_value = ComparisonValue{ .str = condition.value }, .float => compare_value = ComparisonValue{ .float = dataParsing.parseFloat(condition.value) }, - .bool_ => compare_value = ComparisonValue{ .bool_ = dataParsing.parseBool(condition.value) }, + .bool => compare_value = ComparisonValue{ .bool_ = dataParsing.parseBool(condition.value) }, .int_array => compare_value = ComparisonValue{ .int_array = dataParsing.parseArrayInt(self.allocator, condition.value) }, .str_array => compare_value = ComparisonValue{ .str_array = dataParsing.parseArrayStr(self.allocator, condition.value) }, .float_array => compare_value = ComparisonValue{ .float_array = dataParsing.parseArrayFloat(self.allocator, condition.value) }, .bool_array => compare_value = ComparisonValue{ .bool_array = dataParsing.parseArrayBool(self.allocator, condition.value) }, } + defer { + switch (condition.data_type) { + .int_array => compare_value.int_array.deinit(), + .str_array => compare_value.str_array.deinit(), + .float_array => compare_value.float_array.deinit(), + .bool_array => compare_value.bool_array.deinit(), + else => {}, + } + } while (true) { output_fbs.reset(); @@ -143,7 +152,7 @@ pub const DataEngine = struct { .int => if (compare_value.int == dataParsing.parseInt(output_fbs.getWritten()[37..])) try uuid_array.append(try UUID.parse(output_fbs.getWritten()[0..36])), .float => if (compare_value.float == dataParsing.parseFloat(output_fbs.getWritten()[37..])) try uuid_array.append(try UUID.parse(output_fbs.getWritten()[0..36])), .str => if (std.mem.eql(u8, compare_value.str, output_fbs.getWritten()[38 .. output_fbs.getWritten().len - 1])) try uuid_array.append(try UUID.parse(output_fbs.getWritten()[0..36])), - .bool_ => if (compare_value.bool_ == dataParsing.parseBool(output_fbs.getWritten()[37..])) try uuid_array.append(try UUID.parse(output_fbs.getWritten()[0..36])), + .bool => if (compare_value.bool_ == dataParsing.parseBool(output_fbs.getWritten()[37..])) try uuid_array.append(try UUID.parse(output_fbs.getWritten()[0..36])), // TODO: Implement for array too else => {}, } @@ -153,7 +162,7 @@ pub const DataEngine = struct { .int => if (compare_value.int != dataParsing.parseInt(output_fbs.getWritten()[37..])) try uuid_array.append(try UUID.parse(output_fbs.getWritten()[0..36])), .float => if (compare_value.float != dataParsing.parseFloat(output_fbs.getWritten()[37..])) try uuid_array.append(try UUID.parse(output_fbs.getWritten()[0..36])), .str => if (!std.mem.eql(u8, compare_value.str, output_fbs.getWritten()[38 .. output_fbs.getWritten().len - 1])) try uuid_array.append(try UUID.parse(output_fbs.getWritten()[0..36])), - .bool_ => if (compare_value.bool_ != dataParsing.parseBool(output_fbs.getWritten()[37..])) try uuid_array.append(try UUID.parse(output_fbs.getWritten()[0..36])), + .bool => if (compare_value.bool_ != dataParsing.parseBool(output_fbs.getWritten()[37..])) try uuid_array.append(try UUID.parse(output_fbs.getWritten()[0..36])), // TODO: Implement for array too else => {}, } @@ -195,14 +204,14 @@ pub const DataEngine = struct { } // TODO: Test leak on that - pub fn writeEntity(self: *DataEngine, struct_name: []const u8, data_map: std.StringHashMap([]const u8)) !void { + pub fn writeEntity(self: *FileEngine, struct_name: []const u8, data_map: std.StringHashMap([]const u8)) !void { const uuid_str = UUID.init().format_uuid(); defer stdout.print("Added new {s} successfully using UUID: {s}\n", .{ struct_name, uuid_str, }) catch {}; - const member_names = metadata.structName2structMembers(struct_name); + const member_names = schemaEngine.structName2structMembers(struct_name); for (member_names) |member_name| { const potential_file_name_to_use = try self.getFirstUsableFile(struct_name, member_name); @@ -252,10 +261,10 @@ pub const DataEngine = struct { member_name, max_index + 1, }); + defer self.allocator.free(new_file_path); try stdout.print("new file path: {s}\n", .{new_file_path}); - // TODO: Create new file and save the data inside const new_file = self.dir.createFile(new_file_path, .{}) catch @panic("Error creating new data file"); defer new_file.close(); @@ -284,7 +293,7 @@ pub const DataEngine = struct { } /// Use a filename in the format 1.zippondata and return the 1 - fn fileName2Index(_: *DataEngine, file_name: []const u8) usize { + fn fileName2Index(_: *FileEngine, file_name: []const u8) usize { var iter_file_name = std.mem.tokenize(u8, file_name, "."); const num_str = iter_file_name.next().?; const num: usize = std.fmt.parseInt(usize, num_str, 10) catch @panic("Couln't parse the int of a zippondata file."); @@ -293,7 +302,7 @@ pub const DataEngine = struct { /// Add an UUID at a specific index of a file /// Used when some data are deleted from previous zippondata files and are now bellow the file size limit - fn addUUIDToMainFile(_: *DataEngine, file: std.fs.File, index: usize, uuid_str: []const u8) !void { + fn addUUIDToMainFile(_: *FileEngine, file: std.fs.File, index: usize, uuid_str: []const u8) !void { var output: [1024 * 50]u8 = undefined; // Maybe need to increase that as it limit the size of a line in files var output_fbs = std.io.fixedBufferStream(&output); const writer = output_fbs.writer(); @@ -320,35 +329,26 @@ pub const DataEngine = struct { } } - fn getFilesNames(self: *DataEngine, struct_name: []const u8, member_name: []const u8) !std.ArrayList([]const u8) { + fn getFilesNames(self: *FileEngine, struct_name: []const u8, member_name: []const u8, file_names: *std.ArrayList([]const u8)) !void { const sub_path = try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ struct_name, member_name }); - - var file_names = std.ArrayList([]const u8).init(self.allocator); - - const member_dir = self.dir.openDir(sub_path, .{ .iterate = true }) catch @panic("Error opening member directory"); defer self.allocator.free(sub_path); + var member_dir = try self.dir.openDir(sub_path, .{ .iterate = true }); + defer member_dir.close(); + var iter = member_dir.iterate(); while (try iter.next()) |entry| { if ((entry.kind != std.fs.Dir.Entry.Kind.file) or (std.mem.eql(u8, "main.zippondata", entry.name))) continue; - try file_names.append(try self.allocator.dupe(u8, entry.name)); + try file_names.*.append(try self.allocator.dupe(u8, entry.name)); } - - return file_names; - } - - fn deinitFilesNames(self: *DataEngine, array: *const std.ArrayList([]const u8)) void { - for (array.items) |elem| { - self.allocator.free(elem); - } - array.deinit(); } /// Use the map of file stat to find the first file with under the bytes limit. /// return the name of the file. If none is found, return null. - fn getFirstUsableFile(self: *DataEngine, struct_name: []const u8, member_name: []const u8) !?[]const u8 { + fn getFirstUsableFile(self: *FileEngine, struct_name: []const u8, member_name: []const u8) !?[]const u8 { const sub_path = try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ struct_name, member_name }); defer self.allocator.free(sub_path); + var member_dir = try self.dir.openDir(sub_path, .{ .iterate = true }); defer member_dir.close(); @@ -364,7 +364,7 @@ pub const DataEngine = struct { /// Iter over all file and get the max name and return the value of it as usize /// So for example if there is 1.zippondata and 2.zippondata it return 2. - fn maxFileIndex(self: *DataEngine, struct_name: []const u8, member_name: []const u8) !usize { + fn maxFileIndex(self: *FileEngine, struct_name: []const u8, member_name: []const u8) !usize { const buffer = try self.allocator.alloc(u8, 1024); // Adjust the size as needed defer self.allocator.free(buffer); @@ -381,21 +381,23 @@ pub const DataEngine = struct { } // TODO: Give the option to keep , dump or erase the data - pub fn initDataFolder(self: *DataEngine) !void { - for (metadata.struct_name_list) |struct_name| { + pub fn initDataFolder(self: *FileEngine) !void { + for (schemaEngine.struct_name_list) |struct_name| { self.dir.makeDir(struct_name) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return DataEngineError.ErrorCreateStructFolder, }; const struct_dir = try self.dir.openDir(struct_name, .{}); + defer struct_dir.close(); - const member_names = metadata.structName2structMembers(struct_name); + const member_names = schemaEngine.structName2structMembers(struct_name); for (member_names) |member_name| { struct_dir.makeDir(member_name) catch |err| switch (err) { error.PathAlreadyExists => continue, else => return DataEngineError.ErrorCreateMemberFolder, }; const member_dir = try struct_dir.openDir(member_name, .{}); + defer member_dir.close(); blk: { const file = member_dir.createFile("main.zippondata", .{}) catch |err| switch (err) { @@ -413,15 +415,13 @@ pub const DataEngine = struct { } }; -test "File iterator" { +test "Get list of UUID using condition" { const allocator = std.testing.allocator; - var data_engine = DataEngine.init(allocator, null); + var data_engine = FileEngine.init(allocator, null); var uuid_array = std.ArrayList(UUID).init(allocator); defer uuid_array.deinit(); - const condition = DataEngine.Condition{ .struct_name = "User", .member_name = "email", .value = "adrien@mail.com", .operation = .equal, .data_type = .str }; + const condition = FileEngine.Condition{ .struct_name = "User", .member_name = "email", .value = "adrien@mail.com", .operation = .equal, .data_type = .str }; try data_engine.getUUIDListUsingCondition(condition, &uuid_array); - - std.debug.print("Found {d} uuid with first as {any}\n\n", .{ uuid_array.items.len, uuid_array.items[0] }); } diff --git a/src/metadata.zig b/src/metadata.zig deleted file mode 100644 index e9b01b4..0000000 --- a/src/metadata.zig +++ /dev/null @@ -1,31 +0,0 @@ -const std = @import("std"); - -// Maybe create a struct like StructMetadata for the string list of member and name, ect -pub const struct_name_list: [2][]const u8 = .{ - "User", - "Message", -}; - -pub const struct_member_list: [2][]const []const u8 = .{ - &[_][]const u8{ "name", "email", "age", "scores" }, - &[_][]const u8{"content"}, -}; - -/// Get the list of all member name for a struct name -pub fn structName2structMembers(struct_name: []const u8) []const []const u8 { - var i: u16 = 0; - - while (i < struct_name_list.len) : (i += 1) if (std.mem.eql(u8, struct_name_list[i], struct_name)) break; - - return struct_member_list[i]; -} - -pub fn isStructNameExists(struct_name: []const u8) bool { - for (struct_name_list) |sn| if (std.mem.eql(u8, sn, struct_name)) return true; - return false; -} - -pub fn isMemberNameInStruct(struct_name: []const u8, member_name: []const u8) bool { - for (structName2structMembers(struct_name)) |mn| if (std.mem.eql(u8, mn, member_name)) return true; - return false; -} diff --git a/src/schemaEngine.zig b/src/schemaEngine.zig new file mode 100644 index 0000000..dbfc67b --- /dev/null +++ b/src/schemaEngine.zig @@ -0,0 +1,100 @@ +// This file is named and use as a struct but is in fact just a series of utils functions to get and check the schema +// TODO: create a struct like SchemaEngine so I can do propre testing and it make update it easier +// Also can put the migration stuff in here + +const std = @import("std"); +const DataType = @import("types/dataType.zig").DataType; + +const struct_name_list: [2][]const u8 = .{ + "User", + "Message", +}; + +const struct_member_list: [2][]const []const u8 = .{ + &[_][]const u8{ "name", "email", "age", "scores", "friends" }, + &[_][]const u8{"content"}, +}; + +const struct_type_list: [2][]const DataType = .{ + &[_]DataType{ .str, .str, .int, .int_array, .bool_array }, + &[_]DataType{.str}, +}; + +/// Get the type of the member +pub fn memberName2DataType(struct_name: []const u8, member_name: []const u8) ?DataType { + var i: u16 = 0; + + for (structName2structMembers(struct_name)) |mn| { + if (std.mem.eql(u8, mn, member_name)) return structName2DataType(struct_name)[i]; + i += 1; + } + + return null; +} + +/// Get the list of all member name for a struct name +pub fn structName2structMembers(struct_name: []const u8) []const []const u8 { + var i: u16 = 0; + + while (i < struct_name_list.len) : (i += 1) if (std.mem.eql(u8, struct_name_list[i], struct_name)) break; + + if (i == struct_name_list.len) { + std.debug.print("{s} \n", .{struct_name}); + @panic("Struct name not found!"); + } + + return struct_member_list[i]; +} + +pub fn structName2DataType(struct_name: []const u8) []const DataType { + var i: u16 = 0; + + while (i < struct_name_list.len) : (i += 1) if (std.mem.eql(u8, struct_name_list[i], struct_name)) break; + + return struct_type_list[i]; +} + +/// Chech if the name of a struct is in the current schema +pub fn isStructNameExists(struct_name: []const u8) bool { + for (struct_name_list) |sn| if (std.mem.eql(u8, sn, struct_name)) return true; + return false; +} + +/// Check if a struct have the member name +pub fn isMemberNameInStruct(struct_name: []const u8, member_name: []const u8) bool { + for (structName2structMembers(struct_name)) |mn| if (std.mem.eql(u8, mn, member_name)) return true; + return false; +} + +/// Take a struct name and a member name and return true if the member name is part of the struct +pub fn isMemberPartOfStruct(struct_name: []const u8, member_name: []const u8) bool { + const all_struct_member = structName2structMembers(struct_name); + + for (all_struct_member) |key| { + if (std.mem.eql(u8, key, member_name)) return true; + } + + return false; +} + +/// Check if a string is a name of a struct in the currently use engine +pub fn isStructInSchema(struct_name_to_check: []const u8) bool { + for (struct_name_list) |struct_name| { + if (std.mem.eql(u8, struct_name_to_check, struct_name)) { + return true; + } + } + return false; +} + +// Return true if the map have all the member name as key and not more +pub fn checkIfAllMemberInMap(struct_name: []const u8, map: *std.StringHashMap([]const u8)) bool { + const all_struct_member = structName2structMembers(struct_name); + var count: u16 = 0; + + for (all_struct_member) |key| { + if (map.contains(key)) count += 1; + } + + return ((count == all_struct_member.len) and (count == map.count())); +} diff --git a/src/cliTokenizer.zig b/src/tokenizers/cli.zig similarity index 100% rename from src/cliTokenizer.zig rename to src/tokenizers/cli.zig diff --git a/src/schemaTokenizer.zig b/src/tokenizers/schema.zig similarity index 100% rename from src/schemaTokenizer.zig rename to src/tokenizers/schema.zig diff --git a/src/ziqlTokenizer.zig b/src/tokenizers/ziql.zig similarity index 83% rename from src/ziqlTokenizer.zig rename to src/tokenizers/ziql.zig index 9dccbb6..93ba2c1 100644 --- a/src/ziqlTokenizer.zig +++ b/src/tokenizers/ziql.zig @@ -19,6 +19,10 @@ pub const Token = struct { .{ "null", .keyword_null }, .{ "__DESCRIBE__", .keyword__describe__ }, .{ "__INIT__", .keyword__init__ }, + .{ "true", .bool_literal_true }, + .{ "false", .bool_literal_false }, + .{ "AND", .keyword_and }, + .{ "OR", .keyword_or }, }); pub fn getKeyword(bytes: []const u8) ?Tag { @@ -35,30 +39,35 @@ pub const Token = struct { keyword_add, keyword_in, keyword_null, + keyword_and, + keyword_or, keyword__describe__, keyword__init__, string_literal, - number_literal, + int_literal, + float_literal, + bool_literal_true, + bool_literal_false, identifier, equal, - bang, - pipe, - l_paren, - r_paren, - l_bracket, - r_bracket, - l_brace, - r_brace, - semicolon, - comma, - angle_bracket_left, - angle_bracket_right, - angle_bracket_left_equal, - angle_bracket_right_equal, - equal_angle_bracket_right, - period, - bang_equal, + bang, // ! + pipe, // | + l_paren, // ( + r_paren, // ) + l_bracket, // [ + r_bracket, // ] + l_brace, // { + r_brace, // } + semicolon, // ; + comma, // , + angle_bracket_left, // < + angle_bracket_right, // > + angle_bracket_left_equal, // <= + angle_bracket_right_equal, // >= + equal_angle_bracket_right, // => + period, // . + bang_equal, // != }; }; @@ -89,7 +98,6 @@ pub const Tokenizer = struct { angle_bracket_right, string_literal_backslash, int_exponent, - int_period, float, float_exponent, int, @@ -187,13 +195,12 @@ pub const Tokenizer = struct { break; }, '.' => { - result.tag = .period; - self.index += 1; - break; + state = .float; + result.tag = .float_literal; }, '0'...'9' => { state = .int; - result.tag = .number_literal; + result.tag = .int_literal; }, else => { state = .invalid; @@ -210,6 +217,8 @@ pub const Tokenizer = struct { else => { if (Token.getKeyword(self.buffer[result.loc.start..self.index])) |tag| { result.tag = tag; + } else { + result.tag = .identifier; } break; }, @@ -301,40 +310,44 @@ pub const Tokenizer = struct { }, .int => switch (c) { - '.' => state = .int_period, - '_', 'a'...'d', 'f'...'o', 'q'...'z', 'A'...'D', 'F'...'O', 'Q'...'Z', '0'...'9' => continue, - 'e', 'E', 'p', 'P' => state = .int_exponent, + '.' => { + state = .float; + result.tag = .float_literal; + }, + 'e', 'E' => { + state = .int_exponent; + result.tag = .float_literal; + }, + '_', '0'...'9' => continue, else => break, }, .int_exponent => switch (c) { - '-', '+' => { + '+', '-', '0'...'9' => { state = .float; }, - else => { - self.index -= 1; - state = .int; - }, - }, - .int_period => switch (c) { - '_', 'a'...'d', 'f'...'o', 'q'...'z', 'A'...'D', 'F'...'O', 'Q'...'Z', '0'...'9' => { - state = .float; - }, - 'e', 'E', 'p', 'P' => state = .float_exponent, else => { self.index -= 1; break; }, }, .float => switch (c) { - '_', 'a'...'d', 'f'...'o', 'q'...'z', 'A'...'D', 'F'...'O', 'Q'...'Z', '0'...'9' => continue, - 'e', 'E', 'p', 'P' => state = .float_exponent, - else => break, + 'e', 'E' => { + state = .float_exponent; + }, + '_', '0'...'9' => { + continue; + }, + else => { + break; + }, }, .float_exponent => switch (c) { - '-', '+' => state = .float, + '+', '-', '0'...'9' => { + continue; + }, else => { self.index -= 1; - state = .float; + break; }, }, } @@ -352,9 +365,11 @@ test "keywords" { test "basic query" { try testTokenize("GRAB User {}", &.{ .keyword_grab, .identifier, .l_brace, .r_brace }); try testTokenize("GRAB User { name = 'Adrien'}", &.{ .keyword_grab, .identifier, .l_brace, .identifier, .equal, .string_literal, .r_brace }); - try testTokenize("GRAB User [1; name] {}", &.{ .keyword_grab, .identifier, .l_bracket, .number_literal, .semicolon, .identifier, .r_bracket, .l_brace, .r_brace }); + try testTokenize("GRAB User { age = 1.5}", &.{ .keyword_grab, .identifier, .l_brace, .identifier, .equal, .float_literal, .r_brace }); + try testTokenize("GRAB User { admin = true}", &.{ .keyword_grab, .identifier, .l_brace, .identifier, .equal, .bool_literal_true, .r_brace }); + try testTokenize("GRAB User [1; name] {}", &.{ .keyword_grab, .identifier, .l_bracket, .int_literal, .semicolon, .identifier, .r_bracket, .l_brace, .r_brace }); try testTokenize("GRAB User{}|ASCENDING name|", &.{ .keyword_grab, .identifier, .l_brace, .r_brace, .pipe, .identifier, .identifier, .pipe }); - try testTokenize("DELETE User[1]{name='Adrien'}|ASCENDING name, age|", &.{ .keyword_delete, .identifier, .l_bracket, .number_literal, .r_bracket, .l_brace, .identifier, .equal, .string_literal, .r_brace, .pipe, .identifier, .identifier, .comma, .identifier, .pipe }); + try testTokenize("DELETE User[1]{name='Adrien'}|ASCENDING name, age|", &.{ .keyword_delete, .identifier, .l_bracket, .int_literal, .r_bracket, .l_brace, .identifier, .equal, .string_literal, .r_brace, .pipe, .identifier, .identifier, .comma, .identifier, .pipe }); } fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !void { diff --git a/src/types/dataType.zig b/src/types/dataType.zig new file mode 100644 index 0000000..22efaa4 --- /dev/null +++ b/src/types/dataType.zig @@ -0,0 +1,11 @@ +/// Suported dataType for the DB +pub const DataType = enum { + int, + float, + str, + bool, + int_array, + float_array, + str_array, + bool_array, +}; diff --git a/src/uuid.zig b/src/types/uuid.zig similarity index 100% rename from src/uuid.zig rename to src/types/uuid.zig diff --git a/src/ziqlParser.zig b/src/ziqlParser.zig new file mode 100644 index 0000000..c6d7cfc --- /dev/null +++ b/src/ziqlParser.zig @@ -0,0 +1,946 @@ +const std = @import("std"); +const schemaEngine = @import("schemaEngine.zig"); +const DataEngine = @import("fileEngine.zig").FileEngine; +const Condition = @import("fileEngine.zig").FileEngine.Condition; +const Tokenizer = @import("tokenizers/ziql.zig").Tokenizer; +const Token = @import("tokenizers/ziql.zig").Token; +const UUID = @import("types/uuid.zig").UUID; +const Allocator = std.mem.Allocator; + +pub const Parser = struct { + allocator: Allocator, + toker: *Tokenizer, + state: State, + data_engine: *DataEngine, + additional_data: AdditionalData, + struct_name: []const u8 = undefined, + + action: Action = undefined, + + pub fn init(allocator: Allocator, toker: *Tokenizer) Parser { + var data_engine = DataEngine.init(allocator, null); + return Parser{ + .allocator = allocator, + .toker = toker, + .state = State.start, + .data_engine = &data_engine, + .additional_data = AdditionalData.init(allocator), + }; + } + + pub fn deinit(self: *Parser) void { + self.additional_data.deinit(); + //self.allocator.free(self.struct_name); + //self.data_engine.deinit(); + } + + const Action = enum { + GRAB, + ADD, + UPDATE, + DELETE, + }; + + const State = enum { + start, + invalid, + end, + + // Endpoint + parse_new_data_and_add_data, + filter_and_send, + + // For the main parse function + expect_struct_name, + expect_filter, + expect_additional_data, + expect_filter_or_additional_data, + expect_new_data, + expect_right_arrow, + + // For the additional data parser + expect_count_of_entity_to_find, + expect_semicolon_OR_right_bracket, + expect_member, + expect_comma_OR_r_bracket_OR_l_bracket, + expect_comma_OR_r_bracket, + + // For the filter parser + expect_left_condition, // Condition is a struct in DataEngine, it's all info necessary to get a list of UUID usinf DataEngine.getUUIDListUsingCondition + expect_operation, // Operations are = != < <= > >= + expect_value, + expect_ANDOR_OR_end, + expect_right_uuid_array, + + // For the new data + expect_equal, + expect_new_value, + expect_comma_OR_end, + add_member_to_map, + add_array_to_map, + }; + + /// This is the [] part + /// IDK if saving it into the Parser struct is a good idea + pub const AdditionalData = struct { + entity_count_to_find: usize = 0, + member_to_find: std.ArrayList(AdditionalDataMember), + + pub fn init(allocator: Allocator) AdditionalData { + return AdditionalData{ .member_to_find = std.ArrayList(AdditionalDataMember).init(allocator) }; + } + + pub fn deinit(self: *AdditionalData) void { + for (0..self.member_to_find.items.len) |i| { + self.member_to_find.items[i].additional_data.deinit(); + } + + self.member_to_find.deinit(); + } + }; + + // This is name in: [name] + // There is an additional data because it can be [friend [1; name]] + const AdditionalDataMember = struct { + name: []const u8, + additional_data: AdditionalData, + + pub fn init(allocator: Allocator, name: []const u8) AdditionalDataMember { + const additional_data = AdditionalData.init(allocator); + return AdditionalDataMember{ .name = name, .additional_data = additional_data }; + } + }; + + pub fn parse(self: *Parser) !void { + var token = self.toker.next(); + var keep_next = false; // Use in the loop to prevent to get the next token when continue. Just need to make it true and it is reset at every loop + + while (self.state != State.end) : ({ + token = if (!keep_next) self.toker.next() else token; + keep_next = false; + }) { + switch (self.state) { + .start => { + switch (token.tag) { + .keyword_grab => { + self.action = Action.GRAB; + self.state = State.expect_struct_name; + }, + .keyword_add => { + self.action = Action.ADD; + self.state = State.expect_struct_name; + }, + .keyword_update => { + self.action = Action.UPDATE; + self.state = State.expect_struct_name; + }, + .keyword_delete => { + self.action = Action.DELETE; + self.state = State.expect_struct_name; + }, + .keyword__describe__ => { + std.debug.print("{s}", .{@embedFile("schema.zipponschema")}); + self.state = State.end; + }, + .keyword__init__ => { + try self.data_engine.initDataFolder(); + self.state = State.end; + }, + else => { + self.printError("Error: Expected action keyword. Available: GRAB ADD DELETE UPDATE", &token); + self.state = State.end; + }, + } + }, + .expect_struct_name => { + // Check if the struct name is in the schema + self.struct_name = try self.allocator.dupe(u8, self.toker.getTokenSlice(token)); + if (!schemaEngine.isStructNameExists(self.struct_name)) self.printError("Error: struct name not found in schema.", &token); + switch (self.action) { + .ADD => self.state = State.expect_new_data, + else => self.state = State.expect_filter_or_additional_data, + } + }, + .expect_filter_or_additional_data => { + keep_next = true; + switch (token.tag) { + .l_bracket => self.state = State.expect_additional_data, + .l_brace => self.state = State.filter_and_send, + else => self.printError("Error: Expect [ for additional data or { for a filter", &token), + } + }, + .expect_additional_data => { + try self.parseAdditionalData(&self.additional_data); + self.state = State.filter_and_send; + }, + .filter_and_send => { + var array = std.ArrayList(UUID).init(self.allocator); + defer array.deinit(); + try self.parseFilter(&array); + self.sendEntity(array.items); + self.state = State.end; + }, + .expect_new_data => { + switch (token.tag) { + .l_paren => { + keep_next = true; + self.state = State.parse_new_data_and_add_data; + }, + else => self.printError("Error: Expecting new data starting with (", &token), + } + }, + .parse_new_data_and_add_data => { + switch (self.action) { + .ADD => { + const data_map = std.StringHashMap([]const u8).init(self.allocator); + defer data_map.deinit(); + self.parseNewData(&data_map); + if (!schemaEngine.checkIfAllMemberInMap(self.struct_name, data_map)) {} + try self.data_engine.writeEntity(self.struct_name, data_map); + self.state = State.end; + }, + .UPDATE => {}, // TODO: + else => unreachable, + } + }, + else => unreachable, + } + } + } + + // TODO: Use that when I want to return data to the use, need to understand how it's work. + // I think for now put the ordering using additional data here + // Maybe to a struct Communicator to handle all communication between use and cli + fn sendEntity(self: *Parser, uuid_array: []UUID) void { + _ = self; + + std.debug.print("Number of uuid to send: {d}", .{uuid_array.len}); + } + + /// Take an array of UUID and populate it to be the array that represent filter between {} + /// Main is to know if between {} or (), main is true if between {} or the first to be call + /// TODO: Create a parseCondition + fn parseFilter(self: *Parser, left_array: *std.ArrayList(UUID), struct_name: []const u8, main: bool) !void { + var token = self.toker.next(); + var keep_next = false; + self.state = State.expect_left_condition; + + var left_condition = Condition.init(struct_name); + var curent_operation: enum { and_, or_ } = undefined; + + while (self.state != State.end) : ({ + token = if (!keep_next) self.toker.next() else token; + keep_next = false; + }) { + switch (self.state) { + .expect_left_condition => { + self.parseCondition(&left_condition, &token); + try self.data_engine.getUUIDListUsingCondition(left_condition, left_array); + self.state = State.expect_ANDOR_OR_end; + }, + .expect_ANDOR_OR_end => { + switch (token.tag) { + .r_brace => { + if (main) { + self.state = State.end; + } else { + self.printError("Error: Expected } to end main condition or AND/OR to continue it", &token); + } + }, + .r_paren => { + if (!main) { + self.state = State.end; + } else { + self.printError("Error: Expected ) to end inside condition or AND/OR to continue it", &token); + } + }, + .keyword_and => { + curent_operation = .and_; + self.state = State.expect_right_uuid_array; + }, + .keyword_or => { + curent_operation = .or_; + self.state = State.expect_right_uuid_array; + }, + else => self.printError("Error: Expected a condition including AND or OR or } or )", &token), + } + }, + .expect_right_uuid_array => { + var right_array = std.ArrayList(UUID).init(self.allocator); + defer right_array.deinit(); + + switch (token.tag) { + .l_paren => try self.parseFilter(&right_array, struct_name, false), // run parserFilter to get the right array + .identifier => { + var right_condition = Condition.init(struct_name); + + self.parseCondition(&right_condition, &token); + try self.data_engine.getUUIDListUsingCondition(right_condition, &right_array); + }, // Create a new condition and compare it + else => self.printError("Error: Expecting ( or member name.", &token), + } + + switch (curent_operation) { + .and_ => { + try AND(left_array, &right_array); + }, + .or_ => { + try OR(left_array, &right_array); + }, + } + self.state = .expect_ANDOR_OR_end; + }, + else => unreachable, + } + } + } + + fn parseCondition(self: *Parser, condition: *Condition, token_ptr: *Token) void { + var keep_next = false; + self.state = State.expect_member; + var token = token_ptr.*; + + while (self.state != State.end) : ({ + token = if (!keep_next) self.toker.next() else token; + keep_next = false; + }) { + switch (self.state) { + .expect_member => { + switch (token.tag) { + .identifier => { + if (!schemaEngine.isMemberPartOfStruct(condition.struct_name, self.toker.getTokenSlice(token))) { + self.printError("Error: Member not part of struct.", &token); + } + condition.data_type = schemaEngine.memberName2DataType(condition.struct_name, self.toker.getTokenSlice(token)) orelse @panic("Couldn't find the struct and member"); + condition.member_name = self.toker.getTokenSlice(token); + self.state = State.expect_operation; + }, + else => self.printError("Error: Expected member name.", &token), + } + }, + .expect_operation => { + switch (token.tag) { + .equal => condition.operation = .equal, // = + .angle_bracket_left => condition.operation = .inferior, // < + .angle_bracket_right => condition.operation = .superior, // > + .angle_bracket_left_equal => condition.operation = .inferior_or_equal, // <= + .angle_bracket_right_equal => condition.operation = .superior_or_equal, // >= + .bang_equal => condition.operation = .different, // != + else => self.printError("Error: Expected condition. Including < > <= >= = !=", &token), + } + self.state = State.expect_value; + }, + .expect_value => { + switch (condition.data_type) { + .int => { + switch (token.tag) { + .int_literal => condition.value = self.toker.getTokenSlice(token), + else => self.printError("Error: Expected int", &token), + } + }, + .float => { + switch (token.tag) { + .float_literal => condition.value = self.toker.getTokenSlice(token), + else => self.printError("Error: Expected float", &token), + } + }, + .str => { + switch (token.tag) { + .string_literal => condition.value = self.toker.getTokenSlice(token), + else => self.printError("Error: Expected string", &token), + } + }, + .bool => { + switch (token.tag) { + .bool_literal_true, .bool_literal_false => condition.value = self.toker.getTokenSlice(token), + else => self.printError("Error: Expected bool", &token), + } + }, + .int_array => { + const start_index = token.loc.start; + token = self.toker.next(); + while (token.tag != Token.Tag.r_bracket) : (token = self.toker.next()) { + switch (token.tag) { + .int_literal => continue, + else => self.printError("Error: Expected int or ].", &token), + } + } + condition.value = self.toker.buffer[start_index..token.loc.end]; + }, + .float_array => { + const start_index = token.loc.start; + token = self.toker.next(); + while (token.tag != Token.Tag.r_bracket) : (token = self.toker.next()) { + switch (token.tag) { + .float_literal => continue, + else => self.printError("Error: Expected float or ].", &token), + } + } + condition.value = self.toker.buffer[start_index..token.loc.end]; + }, + .str_array => { + const start_index = token.loc.start; + token = self.toker.next(); + while (token.tag != Token.Tag.r_bracket) : (token = self.toker.next()) { + switch (token.tag) { + .string_literal => continue, + else => self.printError("Error: Expected string or ].", &token), + } + } + condition.value = self.toker.buffer[start_index..token.loc.end]; + }, + .bool_array => { + const start_index = token.loc.start; + token = self.toker.next(); + while (token.tag != Token.Tag.r_bracket) : (token = self.toker.next()) { + switch (token.tag) { + .bool_literal_false, .bool_literal_true => continue, + else => self.printError("Error: Expected bool or ].", &token), + } + } + condition.value = self.toker.buffer[start_index..token.loc.end]; + }, + } + self.state = .end; + }, + else => unreachable, + } + } + } + + /// When this function is call, the tokenizer last token retrieved should be [. + /// Check if an int is here -> check if ; is here -> check if member is here -> check if [ is here -> loop + fn parseAdditionalData(self: *Parser, additional_data: *AdditionalData) !void { + var token = self.toker.next(); + var keep_next = false; + self.state = State.expect_count_of_entity_to_find; + + while (self.state != State.end) : ({ + token = if (!keep_next) self.toker.next() else token; + keep_next = false; + }) { + switch (self.state) { + .expect_count_of_entity_to_find => { + switch (token.tag) { + .int_literal => { + const count = std.fmt.parseInt(usize, self.toker.getTokenSlice(token), 10) catch { + self.printError("Error while transforming this into a integer.", &token); + self.state = .invalid; + continue; + }; + additional_data.entity_count_to_find = count; + self.state = .expect_semicolon_OR_right_bracket; + }, + else => { + self.state = .expect_member; + keep_next = true; + }, + } + }, + .expect_semicolon_OR_right_bracket => { + switch (token.tag) { + .semicolon => self.state = .expect_member, + .r_bracket => self.state = State.end, + else => self.printError("Error: Expect ';' or ']'.", &token), + } + }, + .expect_member => { + switch (token.tag) { + .identifier => { + if (!schemaEngine.isMemberNameInStruct(self.struct_name, self.toker.getTokenSlice(token))) self.printError("Member not found in struct.", &token); + try additional_data.member_to_find.append( + AdditionalDataMember.init( + self.allocator, + self.toker.getTokenSlice(token), + ), + ); + + self.state = .expect_comma_OR_r_bracket_OR_l_bracket; + }, + else => self.printError("Error: Expected a member name.", &token), + } + }, + .expect_comma_OR_r_bracket_OR_l_bracket => { + switch (token.tag) { + .comma => self.state = .expect_member, + .r_bracket => { + self.state = State.end; + keep_next = true; + }, + .l_bracket => { + try self.parseAdditionalData( + &additional_data.member_to_find.items[additional_data.member_to_find.items.len - 1].additional_data, + ); + self.state = .expect_comma_OR_r_bracket; + }, + else => self.printError("Error: Expected , or ] or [", &token), + } + }, + .expect_comma_OR_r_bracket => { + switch (token.tag) { + .comma => self.state = .expect_member, + .r_bracket => { + self.state = State.end; + keep_next = true; + }, + else => self.printError("Error: Expected , or ]", &token), + } + }, + else => unreachable, + } + } + } + + /// Take the tokenizer and return a map of the query for the ADD command. + /// Keys are the member name and value are the string of the value in the query. E.g. 'Adrien' or '10' + /// Entry token need to be ( + fn parseNewData(self: *Parser, member_map: *std.StringHashMap([]const u8)) void { + var token = self.toker.next(); + var keep_next = false; + var member_name: []const u8 = undefined; // Maybe use allocator.alloc + self.state = State.expect_member; + + while (self.state != State.end) : ({ + token = if (!keep_next) self.toker.next() else token; + keep_next = false; + }) { + switch (self.state) { + .expect_member => { + switch (token.tag) { + .identifier => { + member_name = self.toker.getTokenSlice(token); + if (!schemaEngine.isMemberNameInStruct(self.struct_name, member_name)) self.printError("Member not found in struct.", &token); + self.state = State.expect_equal; + }, + else => self.printError("Error: Expected member name.", &token), + } + }, + .expect_equal => { + switch (token.tag) { + // TODO: Add more comparison like IN or other stuff + .equal => self.state = State.expect_new_value, + else => self.printError("Error: Expected =", &token), + } + }, + .expect_new_value => { + const data_type = schemaEngine.memberName2DataType(self.struct_name, member_name); + switch (data_type.?) { + .int => { + switch (token.tag) { + .int_literal, .keyword_null => { + keep_next = true; + self.state = State.add_member_to_map; + }, + else => self.printError("Error: Expected int", &token), + } + }, + .float => { + switch (token.tag) { + .float_literal, .keyword_null => { + keep_next = true; + self.state = State.add_member_to_map; + }, + else => self.printError("Error: Expected float", &token), + } + }, + .bool => { + switch (token.tag) { + .bool_literal_true, .bool_literal_false, .keyword_null => { + keep_next = true; + self.state = State.add_member_to_map; + }, + else => self.printError("Error: Expected bool: true false", &token), + } + }, + .str => { + switch (token.tag) { + .string_literal, .keyword_null => { + keep_next = true; + self.state = State.add_member_to_map; + }, + else => self.printError("Error: Expected string between ''", &token), + } + }, + // TODO: Maybe upgrade that to use multiple state + .int_array => { + switch (token.tag) { + .l_bracket => { + const start_index = token.loc.start; + token = self.toker.next(); + while (token.tag != Token.Tag.r_bracket) : (token = self.toker.next()) { + switch (token.tag) { + .int_literal => continue, + else => self.printError("Error: Expected int or ].", &token), + } + } + // Maybe change that as it just recreate a string that is already in the buffer + member_map.put(member_name, self.toker.buffer[start_index..token.loc.end]) catch @panic("Couln't add string of array in data map"); + self.state = State.expect_comma_OR_end; + }, + else => self.printError("Error: Expected [ to start an array", &token), + } + }, + .float_array => { + switch (token.tag) { + .l_bracket => { + const start_index = token.loc.start; + token = self.toker.next(); + while (token.tag != Token.Tag.r_bracket) : (token = self.toker.next()) { + switch (token.tag) { + .float_literal => continue, + else => self.printError("Error: Expected float or ].", &token), + } + } + // Maybe change that as it just recreate a string that is already in the buffer + member_map.put(member_name, self.toker.buffer[start_index..token.loc.end]) catch @panic("Couln't add string of array in data map"); + self.state = State.expect_comma_OR_end; + }, + else => self.printError("Error: Expected [ to start an array", &token), + } + }, + .bool_array => { + switch (token.tag) { + .l_bracket => { + const start_index = token.loc.start; + token = self.toker.next(); + while (token.tag != Token.Tag.r_bracket) : (token = self.toker.next()) { + switch (token.tag) { + .bool_literal_false, .bool_literal_true => continue, + else => self.printError("Error: Expected bool or ].", &token), + } + } + // Maybe change that as it just recreate a string that is already in the buffer + member_map.put(member_name, self.toker.buffer[start_index..token.loc.end]) catch @panic("Couln't add string of array in data map"); + self.state = State.expect_comma_OR_end; + }, + else => self.printError("Error: Expected [ to start an array", &token), + } + }, + .str_array => { + switch (token.tag) { + .l_bracket => { + const start_index = token.loc.start; + token = self.toker.next(); + while (token.tag != Token.Tag.r_bracket) : (token = self.toker.next()) { + switch (token.tag) { + .string_literal => continue, + else => self.printError("Error: Expected str or ].", &token), + } + } + // Maybe change that as it just recreate a string that is already in the buffer + member_map.put(member_name, self.toker.buffer[start_index..token.loc.end]) catch @panic("Couln't add string of array in data map"); + self.state = State.expect_comma_OR_end; + }, + else => self.printError("Error: Expected [ to start an array", &token), + } + }, + } + }, + .add_member_to_map => { + member_map.put(member_name, self.toker.getTokenSlice(token)) catch @panic("Could not add member name and value to map in getMapOfMember"); + self.state = State.expect_comma_OR_end; + }, + .add_array_to_map => {}, + .expect_comma_OR_end => { + switch (token.tag) { + .r_paren => self.state = State.end, + .comma => self.state = State.expect_member, + else => self.printError("Error: Expect , or )", &token), + } + }, + else => unreachable, + } + } + } + + fn printError(self: *Parser, message: []const u8, token: *Token) void { + std.debug.print("\n", .{}); + std.debug.print("{s}\n", .{self.toker.buffer}); + + // Calculate the number of spaces needed to reach the start position. + var spaces: usize = 0; + while (spaces < token.loc.start) : (spaces += 1) { + std.debug.print(" ", .{}); + } + + // Print the '^' characters for the error span. + var i: usize = token.loc.start; + while (i < token.loc.end) : (i += 1) { + std.debug.print("^", .{}); + } + std.debug.print(" \n", .{}); // Align with the message + + std.debug.print("{s}\n", .{message}); + + std.debug.print("{any}\n{any}\n", .{ token.tag, token.loc }); + + @panic(""); + } +}; + +// TODO: Optimize both +fn OR(arr1: *std.ArrayList(UUID), arr2: *std.ArrayList(UUID)) !void { + for (0..arr2.items.len) |i| { + if (!containUUID(arr1.*, arr2.items[i])) { + try arr1.append(arr2.items[i]); + } + } +} + +fn AND(arr1: *std.ArrayList(UUID), arr2: *std.ArrayList(UUID)) !void { + var i: usize = 0; + for (0..arr1.items.len) |_| { + if (!containUUID(arr2.*, arr1.items[i])) { + _ = arr1.orderedRemove(i); + } else { + i += 1; + } + } +} + +test "OR & AND" { + const allocator = std.testing.allocator; + + var right_arr = std.ArrayList(UUID).init(allocator); + defer right_arr.deinit(); + try right_arr.append(try UUID.parse("00000000-0000-0000-0000-000000000000")); + try right_arr.append(try UUID.parse("00000000-0000-0000-0000-000000000001")); + try right_arr.append(try UUID.parse("00000000-0000-0000-0000-000000000005")); + try right_arr.append(try UUID.parse("00000000-0000-0000-0000-000000000006")); + try right_arr.append(try UUID.parse("00000000-0000-0000-0000-000000000007")); + + var left_arr1 = std.ArrayList(UUID).init(allocator); + defer left_arr1.deinit(); + try left_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000000")); + try left_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000001")); + try left_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000002")); + try left_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000003")); + try left_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000004")); + + var expected_arr1 = std.ArrayList(UUID).init(allocator); + defer expected_arr1.deinit(); + try expected_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000000")); + try expected_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000001")); + + try AND(&left_arr1, &right_arr); + try std.testing.expect(compareUUIDArray(left_arr1, expected_arr1)); + + var left_arr2 = std.ArrayList(UUID).init(allocator); + defer left_arr2.deinit(); + try left_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000000")); + try left_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000001")); + try left_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000002")); + try left_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000003")); + try left_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000004")); + + var expected_arr2 = std.ArrayList(UUID).init(allocator); + defer expected_arr2.deinit(); + try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000000")); + try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000001")); + try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000002")); + try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000003")); + try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000004")); + try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000005")); + try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000006")); + try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000007")); + + try OR(&left_arr2, &right_arr); + + try std.testing.expect(compareUUIDArray(left_arr2, expected_arr2)); +} + +fn containUUID(arr: std.ArrayList(UUID), value: UUID) bool { + return for (arr.items) |elem| { + if (value.compare(elem)) break true; + } else false; +} + +fn compareUUIDArray(arr1: std.ArrayList(UUID), arr2: std.ArrayList(UUID)) bool { + if (arr1.items.len != arr2.items.len) { + std.debug.print("Not same array len when comparing UUID. arr1: {d} arr2: {d}\n", .{ arr1.items.len, arr2.items.len }); + return false; + } + + for (0..arr1.items.len) |i| { + if (!containUUID(arr2, arr1.items[i])) return false; + } + + return true; +} + +test "Parse filter" { + const allocator = std.testing.allocator; + var tokenizer = Tokenizer.init("{name = 'Adrien'}"); + var parser = Parser.init(allocator, &tokenizer); + _ = tokenizer.next(); + + var uuid_array = std.ArrayList(UUID).init(allocator); + defer uuid_array.deinit(); + + try parser.parseFilter(&uuid_array, "User", true); +} + +test "Parse condition" { + const condition1 = Condition{ .data_type = .int, .member_name = "age", .operation = .superior_or_equal, .struct_name = "User", .value = "26" }; + try testConditionParsing("age >= 26", condition1); + + const condition2 = Condition{ .data_type = .int_array, .member_name = "scores", .operation = .equal, .struct_name = "User", .value = "[1 2 42]" }; + try testConditionParsing("scores = [1 2 42]", condition2); + + const condition3 = Condition{ .data_type = .str, .member_name = "email", .operation = .equal, .struct_name = "User", .value = "'adrien@email.com'" }; + try testConditionParsing("email = 'adrien@email.com'", condition3); +} + +fn testConditionParsing(source: [:0]const u8, expected_condition: Condition) !void { + const allocator = std.testing.allocator; + var tokenizer = Tokenizer.init(source); + var parser = Parser.init(allocator, &tokenizer); + var token = tokenizer.next(); + + var condition = Condition.init("User"); + parser.parseCondition(&condition, &token); + + try std.testing.expect(compareCondition(expected_condition, condition)); +} + +fn compareCondition(c1: Condition, c2: Condition) bool { + return ((std.mem.eql(u8, c1.value, c2.value)) and (std.mem.eql(u8, c1.struct_name, c2.struct_name)) and (std.mem.eql(u8, c1.member_name, c2.member_name)) and (c1.operation == c2.operation) and (c1.data_type == c2.data_type)); +} + +// TODO: Test Filter parser + +test "Parse new data" { + const allocator = std.testing.allocator; + + var map1 = std.StringHashMap([]const u8).init(allocator); + defer map1.deinit(); + try map1.put("name", "'Adrien'"); + testNewDataParsing("(name = 'Adrien')", map1); + + var map2 = std.StringHashMap([]const u8).init(allocator); + defer map2.deinit(); + try map2.put("name", "'Adrien'"); + try map2.put("email", "'adrien@email.com'"); + try map2.put("scores", "[1 4 19]"); + try map2.put("age", "26"); + testNewDataParsing("(name = 'Adrien', scores = [1 4 19], age = 26, email = 'adrien@email.com')", map2); +} + +fn testNewDataParsing(source: [:0]const u8, expected_member_map: std.StringHashMap([]const u8)) void { + const allocator = std.testing.allocator; + var tokenizer = Tokenizer.init(source); + + var parser = Parser.init(allocator, &tokenizer); + parser.struct_name = allocator.dupe(u8, "User") catch @panic("Cant alloc struct name"); + defer parser.deinit(); + defer allocator.free(parser.struct_name); + + var data_map = std.StringHashMap([]const u8).init(allocator); + defer data_map.deinit(); + + _ = tokenizer.next(); + parser.parseNewData(&data_map); + + var iterator = expected_member_map.iterator(); + + var expected_total_count: usize = 0; + var found_count: usize = 0; + var error_found = false; + while (iterator.next()) |entry| { + expected_total_count += 1; + if (!data_map.contains(entry.key_ptr.*)) { + std.debug.print("Error new data parsing: Missing {s} in parsed map.\n", .{entry.key_ptr.*}); + error_found = true; + continue; + } + if (!std.mem.eql(u8, entry.value_ptr.*, data_map.get(entry.key_ptr.*).?)) { + std.debug.print("Error new data parsing: Wrong data for {s} in parsed map.\n Expected: {s}\n Got: {s}", .{ entry.key_ptr.*, entry.value_ptr.*, data_map.get(entry.key_ptr.*).? }); + error_found = true; + continue; + } + found_count += 1; + } + + if ((error_found) or (expected_total_count != found_count)) @panic("=("); +} + +test "Parse additional data" { + const allocator = std.testing.allocator; + + var additional_data1 = Parser.AdditionalData.init(allocator); + additional_data1.entity_count_to_find = 1; + testAdditionalData("[1]", additional_data1); + + var additional_data2 = Parser.AdditionalData.init(allocator); + defer additional_data2.deinit(); + try additional_data2.member_to_find.append( + Parser.AdditionalDataMember.init( + allocator, + "name", + ), + ); + testAdditionalData("[name]", additional_data2); + + var additional_data3 = Parser.AdditionalData.init(allocator); + additional_data3.entity_count_to_find = 1; + defer additional_data3.deinit(); + try additional_data3.member_to_find.append( + Parser.AdditionalDataMember.init( + allocator, + "name", + ), + ); + testAdditionalData("[1; name]", additional_data3); + + var additional_data4 = Parser.AdditionalData.init(allocator); + additional_data4.entity_count_to_find = 100; + defer additional_data4.deinit(); + try additional_data4.member_to_find.append( + Parser.AdditionalDataMember.init( + allocator, + "friends", + ), + ); + testAdditionalData("[100; friends [name]]", additional_data4); +} + +fn testAdditionalData(source: [:0]const u8, expected_AdditionalData: Parser.AdditionalData) void { + const allocator = std.testing.allocator; + var tokenizer = Tokenizer.init(source); + + var parser = Parser.init(allocator, &tokenizer); + parser.struct_name = allocator.dupe(u8, "User") catch @panic("Cant alloc struct name"); + defer parser.deinit(); + defer allocator.free(parser.struct_name); + + _ = tokenizer.next(); + parser.parseAdditionalData(&parser.additional_data) catch |err| { + std.debug.print("Error parsing additional data: {any}\n", .{err}); + }; + + compareAdditionalData(expected_AdditionalData, parser.additional_data); +} + +fn compareAdditionalData(ad1: Parser.AdditionalData, ad2: Parser.AdditionalData) void { + std.testing.expectEqual(ad1.entity_count_to_find, ad2.entity_count_to_find) catch { + std.debug.print("Additional data entity_count_to_find are not equal.\n", .{}); + }; + + var founded = false; + + for (ad1.member_to_find.items) |elem1| { + founded = false; + for (ad2.member_to_find.items) |elem2| { + if (std.mem.eql(u8, elem1.name, elem2.name)) { + compareAdditionalData(elem1.additional_data, elem2.additional_data); + founded = true; + break; + } + } + + std.testing.expect(founded) catch { + std.debug.print("{s} not found\n", .{elem1.name}); + @panic("=("); + }; + } +} diff --git a/test_runner.zig b/test_runner.zig new file mode 100644 index 0000000..6824108 --- /dev/null +++ b/test_runner.zig @@ -0,0 +1,298 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const Allocator = std.mem.Allocator; + +const BORDER = "=" ** 80; + +// use in custom panic handler +var current_test: ?[]const u8 = null; + +pub fn main() !void { + var mem: [8192]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&mem); + + const allocator = fba.allocator(); + + const env = Env.init(allocator); + defer env.deinit(allocator); + + var slowest = SlowTracker.init(allocator, 5); + defer slowest.deinit(); + + var pass: usize = 0; + var fail: usize = 0; + var skip: usize = 0; + var leak: usize = 0; + + const printer = Printer.init(); + printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line + + for (builtin.test_functions) |t| { + if (isSetup(t)) { + t.func() catch |err| { + printer.status(.fail, "\nsetup \"{s}\" failed: {}\n", .{ t.name, err }); + return err; + }; + } + } + + for (builtin.test_functions) |t| { + if (isSetup(t) or isTeardown(t)) { + continue; + } + + var status = Status.pass; + slowest.startTiming(); + + const is_unnamed_test = isUnnamed(t); + if (env.filter) |f| { + if (!is_unnamed_test and std.mem.indexOf(u8, t.name, f) == null) { + continue; + } + } + + const friendly_name = blk: { + const name = t.name; + var it = std.mem.splitScalar(u8, name, '.'); + while (it.next()) |value| { + if (std.mem.eql(u8, value, "test")) { + const rest = it.rest(); + break :blk if (rest.len > 0) rest else name; + } + } + break :blk name; + }; + + current_test = friendly_name; + std.testing.allocator_instance = .{}; + const result = t.func(); + current_test = null; + + const ns_taken = slowest.endTiming(friendly_name); + + if (std.testing.allocator_instance.deinit() == .leak) { + leak += 1; + printer.status(.fail, "\n{s}\n\"{s}\" - Memory Leak\n{s}\n", .{ BORDER, friendly_name, BORDER }); + } + + if (result) |_| { + pass += 1; + } else |err| switch (err) { + error.SkipZigTest => { + skip += 1; + status = .skip; + }, + else => { + status = .fail; + fail += 1; + printer.status(.fail, "\n{s}\n\"{s}\" - {s}\n{s}\n", .{ BORDER, friendly_name, @errorName(err), BORDER }); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + if (env.fail_first) { + break; + } + }, + } + + if (env.verbose) { + const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0; + printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms }); + } else { + printer.status(status, ".", .{}); + } + } + + for (builtin.test_functions) |t| { + if (isTeardown(t)) { + t.func() catch |err| { + printer.status(.fail, "\nteardown \"{s}\" failed: {}\n", .{ t.name, err }); + return err; + }; + } + } + + const total_tests = pass + fail; + const status = if (fail == 0) Status.pass else Status.fail; + printer.status(status, "\n{d} of {d} test{s} passed\n", .{ pass, total_tests, if (total_tests != 1) "s" else "" }); + if (skip > 0) { + printer.status(.skip, "{d} test{s} skipped\n", .{ skip, if (skip != 1) "s" else "" }); + } + if (leak > 0) { + printer.status(.fail, "{d} test{s} leaked\n", .{ leak, if (leak != 1) "s" else "" }); + } + printer.fmt("\n", .{}); + try slowest.display(printer); + printer.fmt("\n", .{}); + std.posix.exit(if (fail == 0) 0 else 1); +} + +const Printer = struct { + out: std.fs.File.Writer, + + fn init() Printer { + return .{ + .out = std.io.getStdErr().writer(), + }; + } + + fn fmt(self: Printer, comptime format: []const u8, args: anytype) void { + std.fmt.format(self.out, format, args) catch unreachable; + } + + fn status(self: Printer, s: Status, comptime format: []const u8, args: anytype) void { + const color = switch (s) { + .pass => "\x1b[32m", + .fail => "\x1b[31m", + .skip => "\x1b[33m", + else => "", + }; + const out = self.out; + out.writeAll(color) catch @panic("writeAll failed?!"); + std.fmt.format(out, format, args) catch @panic("std.fmt.format failed?!"); + self.fmt("\x1b[0m", .{}); + } +}; + +const Status = enum { + pass, + fail, + skip, + text, +}; + +const SlowTracker = struct { + const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming); + max: usize, + slowest: SlowestQueue, + timer: std.time.Timer, + + fn init(allocator: Allocator, count: u32) SlowTracker { + const timer = std.time.Timer.start() catch @panic("failed to start timer"); + var slowest = SlowestQueue.init(allocator, {}); + slowest.ensureTotalCapacity(count) catch @panic("OOM"); + return .{ + .max = count, + .timer = timer, + .slowest = slowest, + }; + } + + const TestInfo = struct { + ns: u64, + name: []const u8, + }; + + fn deinit(self: SlowTracker) void { + self.slowest.deinit(); + } + + fn startTiming(self: *SlowTracker) void { + self.timer.reset(); + } + + fn endTiming(self: *SlowTracker, test_name: []const u8) u64 { + var timer = self.timer; + const ns = timer.lap(); + + var slowest = &self.slowest; + + if (slowest.count() < self.max) { + // Capacity is fixed to the # of slow tests we want to track + // If we've tracked fewer tests than this capacity, than always add + slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); + return ns; + } + + { + // Optimization to avoid shifting the dequeue for the common case + // where the test isn't one of our slowest. + const fastest_of_the_slow = slowest.peekMin() orelse unreachable; + if (fastest_of_the_slow.ns > ns) { + // the test was faster than our fastest slow test, don't add + return ns; + } + } + + // the previous fastest of our slow tests, has been pushed off. + _ = slowest.removeMin(); + slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); + return ns; + } + + fn display(self: *SlowTracker, printer: Printer) !void { + var slowest = self.slowest; + const count = slowest.count(); + printer.fmt("Slowest {d} test{s}: \n", .{ count, if (count != 1) "s" else "" }); + while (slowest.removeMinOrNull()) |info| { + const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0; + printer.fmt(" {d:.2}ms\t{s}\n", .{ ms, info.name }); + } + } + + fn compareTiming(context: void, a: TestInfo, b: TestInfo) std.math.Order { + _ = context; + return std.math.order(a.ns, b.ns); + } +}; + +const Env = struct { + verbose: bool, + fail_first: bool, + filter: ?[]const u8, + + fn init(allocator: Allocator) Env { + return .{ + .verbose = readEnvBool(allocator, "TEST_VERBOSE", true), + .fail_first = readEnvBool(allocator, "TEST_FAIL_FIRST", false), + .filter = readEnv(allocator, "TEST_FILTER"), + }; + } + + fn deinit(self: Env, allocator: Allocator) void { + if (self.filter) |f| { + allocator.free(f); + } + } + + fn readEnv(allocator: Allocator, key: []const u8) ?[]const u8 { + const v = std.process.getEnvVarOwned(allocator, key) catch |err| { + if (err == error.EnvironmentVariableNotFound) { + return null; + } + std.log.warn("failed to get env var {s} due to err {}", .{ key, err }); + return null; + }; + return v; + } + + fn readEnvBool(allocator: Allocator, key: []const u8, deflt: bool) bool { + const value = readEnv(allocator, key) orelse return deflt; + defer allocator.free(value); + return std.ascii.eqlIgnoreCase(value, "true"); + } +}; + +pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn { + if (current_test) |ct| { + std.debug.print("\x1b[31m{s}\npanic running \"{s}\"\n{s}\x1b[0m\n", .{ BORDER, ct, BORDER }); + } + std.builtin.panic(msg, error_return_trace, ret_addr); +} + +fn isUnnamed(t: std.builtin.TestFn) bool { + const marker = ".test_"; + const test_name = t.name; + const index = std.mem.indexOf(u8, test_name, marker) orelse return false; + _ = std.fmt.parseInt(u32, test_name[index + marker.len ..], 10) catch return false; + return true; +} + +fn isSetup(t: std.builtin.TestFn) bool { + return std.mem.endsWith(u8, t.name, "tests:beforeAll"); +} + +fn isTeardown(t: std.builtin.TestFn) bool { + return std.mem.endsWith(u8, t.name, "tests:afterAll"); +}