├── .gitignore ├── NOTES.md ├── README.md ├── build.zig ├── execute.zig ├── lex.zig ├── main.zig ├── old ├── execute_disk.zig ├── execute_memory.zig └── test.zig ├── parse.zig ├── rocksdb.zig ├── storage.zig ├── tests ├── create.sql ├── insert.sql ├── run.sh ├── select.sql └── where.sql └── types.zig /.gitignore: -------------------------------------------------------------------------------- 1 | rocksdb 2 | zig-cache 3 | zig-out 4 | *~ 5 | *.o 6 | *.dylib 7 | main 8 | kv 9 | data -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eatonphil/zigrocks/50ef1a8b5b75c1409e85d3e3c107299db7384ce8/NOTES.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zigrocks: a basic SQL database in Zig, with storage via RocksDB 2 | 3 | See [Writing a SQL database, take two: Zig and 4 | RocksDB](https://notes.eatonphil.com/zigrocks-sql.html) for a walkthrough! 5 | 6 | Build: 7 | 8 | ```bash 9 | $ git clone https://github.com/facebook/rocksdb 10 | $ ( cd rocksdb && make shared_lib -j8 ) 11 | $ zig build 12 | ``` 13 | 14 | And run! 15 | 16 | ```bash 17 | $ ./main --database data --script <(echo "CREATE TABLE y (year int, age int, name text)") 18 | echo "CREATE TABLE y (year int, age int, name text)" 19 | ok 20 | $ ./main --database data --script <(echo "INSERT INTO y VALUES (2010, 38, 'Gary')") 21 | echo "INSERT INTO y VALUES (2010, 38, 'Gary')" 22 | ok 23 | $ ./main --database data --script <(echo "INSERT INTO y VALUES (2021, 92, 'Teej')") 24 | echo "INSERT INTO y VALUES (2021, 92, 'Teej')" 25 | ok 26 | $ ./main --database data --script <(echo "INSERT INTO y VALUES (1994, 18, 'Mel')") 27 | echo "INSERT INTO y VALUES (1994, 18, 'Mel')" 28 | ok 29 | 30 | # Basic query 31 | $ ./main --database data --script <(echo "SELECT name, age, year FROM y") 32 | echo "SELECT name, age, year FROM y" 33 | | name |age |year | 34 | + ==== +=== +==== + 35 | | Mel |18 |1994 | 36 | | Gary |38 |2010 | 37 | | Teej |92 |2021 | 38 | 39 | # With WHERE 40 | $ ./main --database data --script <(echo "SELECT name, year, age FROM y WHERE age < 40") 41 | echo "SELECT name, year, age FROM y WHERE age < 40" 42 | | name |year |age | 43 | + ==== +==== +=== + 44 | | Mel |1994 |18 | 45 | | Gary |2010 |38 | 46 | 47 | # With operations 48 | $ ./main --database data --script <(echo "SELECT 'Name: ' || name, year + 30, age FROM y WHERE age < 40") 49 | echo "SELECT 'Name: ' || name, year + 30, age FROM y WHERE age < 40" 50 | | unknown |unknown |age | 51 | + ======= +======= +=== + 52 | | Name: Mel |2024 |18 | 53 | | Name: Gary |2040 |38 | 54 | ``` 55 | 56 | References: 57 | * [RocksDB C header](https://github.com/facebook/rocksdb/blob/main/include/rocksdb/c.h) 58 | * [RocksDB C wrapper implementation](https://github.com/facebook/rocksdb/blob/main/db/c.cc) 59 | * [RocksDB C tests](https://github.com/facebook/rocksdb/blob/main/db/c_test.c) 60 | * [Minimal RocksDB/C example](https://gist.github.com/nitingupta910/4640638be7e7ad39c41e) 61 | * [Zig build explained](https://zig.news/xq/zig-build-explained-part-3-1ima) 62 | * [Zig Programming Language Discord's #zig-help channel](https://discord.gg/gxsFFjE) 63 | * [gosql](https://github.com/eatonphil/gosql) 64 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const version = @import("builtin").zig_version; 2 | const std = @import("std"); 3 | 4 | pub fn build(b: *std.build.Builder) void { 5 | const exe = b.addExecutable("main", "main.zig"); 6 | exe.linkLibC(); 7 | exe.linkSystemLibraryName("rocksdb"); 8 | 9 | if (@hasDecl(@TypeOf(exe.*), "addLibraryPath")) { 10 | exe.addLibraryPath("./rocksdb"); 11 | exe.addIncludePath("./rocksdb/include"); 12 | } else { 13 | exe.addLibPath("./rocksdb"); 14 | exe.addIncludeDir("./rocksdb/include"); 15 | } 16 | 17 | exe.setOutputDir("."); 18 | 19 | if (exe.target.isDarwin()) { 20 | b.installFile("./rocksdb/librocksdb.7.8.dylib", "../librocksdb.7.8.dylib"); 21 | exe.addRPath("."); 22 | } 23 | 24 | exe.install(); 25 | 26 | // And also the key-value store 27 | const kvExe = b.addExecutable("kv", "rocksdb.zig"); 28 | kvExe.linkLibC(); 29 | kvExe.linkSystemLibraryName("rocksdb"); 30 | 31 | if (@hasDecl(@TypeOf(kvExe.*), "addLibraryPath")) { 32 | kvExe.addLibraryPath("./rocksdb"); 33 | kvExe.addIncludePath("./rocksdb/include"); 34 | } else { 35 | kvExe.addLibPath("./rocksdb"); 36 | kvExe.addIncludeDir("./rocksdb/include"); 37 | } 38 | 39 | kvExe.setOutputDir("."); 40 | 41 | if (kvExe.target.isDarwin()) { 42 | b.installFile("./rocksdb/librocksdb.7.8.dylib", "../librocksdb.7.8.dylib"); 43 | kvExe.addRPath("."); 44 | } 45 | 46 | kvExe.install(); 47 | } 48 | -------------------------------------------------------------------------------- /execute.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Parser = @import("parse.zig").Parser; 4 | const RocksDB = @import("rocksdb.zig").RocksDB; 5 | const Storage = @import("storage.zig").Storage; 6 | const Result = @import("types.zig").Result; 7 | const String = @import("types.zig").String; 8 | 9 | pub const Executor = struct { 10 | allocator: std.mem.Allocator, 11 | storage: Storage, 12 | 13 | pub fn init(allocator: std.mem.Allocator, storage: Storage) Executor { 14 | return Executor{ .allocator = allocator, .storage = storage }; 15 | } 16 | 17 | const QueryResponse = struct { 18 | fields: []String, 19 | // Array of cells (which is an array of serde (which is an array of u8)) 20 | rows: [][]String, 21 | empty: bool, 22 | }; 23 | const QueryResponseResult = Result(QueryResponse); 24 | 25 | fn executeExpression(self: Executor, e: Parser.ExpressionAST, row: Storage.Row) Storage.Value { 26 | return switch (e) { 27 | .literal => |lit| switch (lit.kind) { 28 | .string => Storage.Value{ .string_value = lit.string() }, 29 | .integer => Storage.Value.fromIntegerString(lit.string()), 30 | .identifier => row.get(lit.string()), 31 | else => unreachable, 32 | }, 33 | .binary_operation => |bin_op| { 34 | var left = self.executeExpression(bin_op.left.*, row); 35 | var right = self.executeExpression(bin_op.right.*, row); 36 | 37 | if (bin_op.operator.kind == .equal_operator) { 38 | // Cast dissimilar types to serde 39 | if (@enumToInt(left) != @enumToInt(right)) { 40 | var leftBuf = std.ArrayList(u8).init(self.allocator); 41 | left.asString(&leftBuf) catch unreachable; 42 | left = Storage.Value{ .string_value = leftBuf.items }; 43 | 44 | var rightBuf = std.ArrayList(u8).init(self.allocator); 45 | right.asString(&rightBuf) catch unreachable; 46 | right = Storage.Value{ .string_value = rightBuf.items }; 47 | } 48 | 49 | return Storage.Value{ 50 | .bool_value = switch (left) { 51 | .null_value => true, 52 | .bool_value => |v| v == right.asBool(), 53 | .string_value => blk: { 54 | var leftBuf = std.ArrayList(u8).init(self.allocator); 55 | left.asString(&leftBuf) catch unreachable; 56 | 57 | var rightBuf = std.ArrayList(u8).init(self.allocator); 58 | right.asString(&rightBuf) catch unreachable; 59 | 60 | break :blk std.mem.eql(u8, leftBuf.items, rightBuf.items); 61 | }, 62 | .integer_value => left.asInteger() == right.asInteger(), 63 | }, 64 | }; 65 | } 66 | 67 | if (bin_op.operator.kind == .concat_operator) { 68 | var copy = std.ArrayList(u8).init(self.allocator); 69 | left.asString(©) catch unreachable; 70 | right.asString(©) catch unreachable; 71 | return Storage.Value{ .string_value = copy.items }; 72 | } 73 | 74 | return switch (bin_op.operator.kind) { 75 | .lt_operator => if (left.asInteger() < right.asInteger()) Storage.Value.TRUE else Storage.Value.FALSE, 76 | .plus_operator => Storage.Value{ .integer_value = left.asInteger() + right.asInteger() }, 77 | else => Storage.Value.NULL, 78 | }; 79 | }, 80 | }; 81 | } 82 | 83 | fn executeSelect(self: Executor, s: Parser.SelectAST) QueryResponseResult { 84 | switch (self.storage.getTable(s.from.string())) { 85 | .err => |err| return .{ .err = err }, 86 | else => _ = 1, 87 | } 88 | 89 | // Now validate and store requested fields 90 | var requestedFields = std.ArrayList(String).init(self.allocator); 91 | for (s.columns) |requestedColumn| { 92 | var fieldName = switch (requestedColumn) { 93 | .literal => |lit| switch (lit.kind) { 94 | .identifier => lit.string(), 95 | // TODO: give reasonable names 96 | else => "unknown", 97 | }, 98 | // TODO: give reasonable names 99 | else => "unknown", 100 | }; 101 | requestedFields.append(fieldName) catch return .{ 102 | .err = "Could not allocate for requested field.", 103 | }; 104 | } 105 | 106 | // Prepare response 107 | var rows = std.ArrayList([]String).init(self.allocator); 108 | var response = QueryResponse{ 109 | .fields = requestedFields.items, 110 | .rows = undefined, 111 | .empty = false, 112 | }; 113 | 114 | var iter = switch (self.storage.getRowIter(s.from.string())) { 115 | .err => |err| return .{ .err = err }, 116 | .val => |it| it, 117 | }; 118 | defer iter.close(); 119 | 120 | while (iter.next()) |row| { 121 | var add = false; 122 | if (s.where) |where| { 123 | if (self.executeExpression(where, row).asBool()) { 124 | add = true; 125 | } 126 | } else { 127 | add = true; 128 | } 129 | 130 | if (add) { 131 | var requested = std.ArrayList(String).init(self.allocator); 132 | for (s.columns) |exp| { 133 | var val = self.executeExpression(exp, row); 134 | var valBuf = std.ArrayList(u8).init(self.allocator); 135 | val.asString(&valBuf) catch unreachable; 136 | requested.append(valBuf.items) catch return .{ 137 | .err = "Could not allocate for requested cell", 138 | }; 139 | } 140 | rows.append(requested.items) catch return .{ 141 | .err = "Could not allocate for row", 142 | }; 143 | } 144 | } 145 | 146 | response.rows = rows.items; 147 | return .{ .val = response }; 148 | } 149 | 150 | fn executeInsert(self: Executor, i: Parser.InsertAST) QueryResponseResult { 151 | var emptyRow = Storage.Row.init(self.allocator, undefined); 152 | var row = Storage.Row.init(self.allocator, undefined); 153 | for (i.values) |v| { 154 | var exp = self.executeExpression(v, emptyRow); 155 | row.append(exp) catch return .{ .err = "Could not allocate for cell" }; 156 | } 157 | 158 | if (self.storage.writeRow(i.table.string(), row)) |err| { 159 | return .{ .err = err }; 160 | } 161 | 162 | return .{ 163 | .val = .{ .fields = undefined, .rows = undefined, .empty = true }, 164 | }; 165 | } 166 | 167 | fn executeCreateTable(self: Executor, c: Parser.CreateTableAST) QueryResponseResult { 168 | var columns = std.ArrayList(String).init(self.allocator); 169 | var types = std.ArrayList(String).init(self.allocator); 170 | 171 | for (c.columns) |column| { 172 | columns.append(column.name.string()) catch return .{ 173 | .err = "Could not allocate for column name", 174 | }; 175 | types.append(column.kind.string()) catch return .{ 176 | .err = "Could not allocate for column kind", 177 | }; 178 | } 179 | 180 | var table = Storage.Table{ 181 | .name = c.table.string(), 182 | .columns = columns.items, 183 | .types = types.items, 184 | }; 185 | 186 | if (self.storage.writeTable(table)) |err| { 187 | return .{ .err = err }; 188 | } 189 | return .{ 190 | .val = .{ .fields = undefined, .rows = undefined, .empty = true }, 191 | }; 192 | } 193 | 194 | pub fn execute(self: Executor, ast: Parser.AST) QueryResponseResult { 195 | return switch (ast) { 196 | .select => |select| switch (self.executeSelect(select)) { 197 | .val => |val| .{ .val = val }, 198 | .err => |err| .{ .err = err }, 199 | }, 200 | .insert => |insert| switch (self.executeInsert(insert)) { 201 | .val => |val| .{ .val = val }, 202 | .err => |err| .{ .err = err }, 203 | }, 204 | .create_table => |createTable| switch (self.executeCreateTable(createTable)) { 205 | .val => |val| .{ .val = val }, 206 | .err => |err| .{ .err = err }, 207 | }, 208 | }; 209 | } 210 | }; 211 | -------------------------------------------------------------------------------- /lex.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Error = @import("types.zig").Error; 4 | const String = @import("types.zig").String; 5 | 6 | pub const Token = struct { 7 | start: u64, 8 | end: u64, 9 | kind: Kind, 10 | source: String, 11 | 12 | pub const Kind = enum { 13 | select_keyword, 14 | create_table_keyword, 15 | insert_keyword, 16 | values_keyword, 17 | from_keyword, 18 | where_keyword, 19 | 20 | plus_operator, 21 | equal_operator, 22 | lt_operator, 23 | concat_operator, 24 | 25 | left_paren_syntax, 26 | right_paren_syntax, 27 | comma_syntax, 28 | 29 | identifier, 30 | integer, 31 | string, 32 | }; 33 | 34 | pub fn string(self: Token) String { 35 | return self.source[self.start..self.end]; 36 | } 37 | 38 | fn debug(self: Token, msg: String) void { 39 | var line: usize = 0; 40 | var column: usize = 0; 41 | var lineStartIndex: usize = 0; 42 | var lineEndIndex: usize = 0; 43 | var i: usize = 0; 44 | var source = self.source; 45 | while (i < source.len) { 46 | if (source[i] == '\n') { 47 | line = line + 1; 48 | column = 0; 49 | lineStartIndex = i; 50 | } else { 51 | column = column + 1; 52 | } 53 | 54 | if (i == self.start) { 55 | // Find the end of the line 56 | lineEndIndex = i; 57 | while (source[lineEndIndex] != '\n') { 58 | lineEndIndex = lineEndIndex + 1; 59 | } 60 | break; 61 | } 62 | 63 | i = i + 1; 64 | } 65 | 66 | std.debug.print( 67 | "{s}\nNear line {}, column {}.\n{s}\n", 68 | .{ msg, line + 1, column, source[lineStartIndex..lineEndIndex] }, 69 | ); 70 | while (column - 1 > 0) { 71 | std.debug.print(" ", .{}); 72 | column = column - 1; 73 | } 74 | std.debug.print("^ Near here\n\n", .{}); 75 | } 76 | }; 77 | 78 | pub fn debug(tokens: []Token, preferredIndex: usize, msg: String) void { 79 | var i = preferredIndex; 80 | while (i >= tokens.len) { 81 | i = i - 1; 82 | } 83 | 84 | tokens[i].debug(msg); 85 | } 86 | 87 | const Builtin = struct { 88 | name: String, 89 | kind: Token.Kind, 90 | }; 91 | 92 | // These must be sorted by length of the name text, descending 93 | var BUILTINS = [_]Builtin{ 94 | .{ .name = "CREATE TABLE", .kind = Token.Kind.create_table_keyword }, 95 | .{ .name = "INSERT INTO", .kind = Token.Kind.insert_keyword }, 96 | .{ .name = "SELECT", .kind = Token.Kind.select_keyword }, 97 | .{ .name = "VALUES", .kind = Token.Kind.values_keyword }, 98 | .{ .name = "WHERE", .kind = Token.Kind.where_keyword }, 99 | .{ .name = "FROM", .kind = Token.Kind.from_keyword }, 100 | .{ .name = "||", .kind = Token.Kind.concat_operator }, 101 | .{ .name = "=", .kind = Token.Kind.equal_operator }, 102 | .{ .name = "+", .kind = Token.Kind.plus_operator }, 103 | .{ .name = "<", .kind = Token.Kind.lt_operator }, 104 | .{ .name = "(", .kind = Token.Kind.left_paren_syntax }, 105 | .{ .name = ")", .kind = Token.Kind.right_paren_syntax }, 106 | .{ .name = ",", .kind = Token.Kind.comma_syntax }, 107 | }; 108 | 109 | fn eatWhitespace(source: String, index: usize) usize { 110 | var res = index; 111 | while (source[res] == ' ' or 112 | source[res] == '\n' or 113 | source[res] == '\t' or 114 | source[res] == '\r') 115 | { 116 | res = res + 1; 117 | if (res == source.len) { 118 | break; 119 | } 120 | } 121 | 122 | return res; 123 | } 124 | 125 | fn asciiCaseInsensitiveEqual(left: String, right: String) bool { 126 | var min = left; 127 | if (right.len < left.len) { 128 | min = right; 129 | } 130 | 131 | for (min) |_, i| { 132 | var l = left[i]; 133 | if (l >= 97 and l <= 122) { 134 | l = l - 32; 135 | } 136 | 137 | var r = right[i]; 138 | if (r >= 97 and r <= 122) { 139 | r = r - 32; 140 | } 141 | 142 | if (l != r) { 143 | return false; 144 | } 145 | } 146 | 147 | return true; 148 | } 149 | 150 | fn lexKeyword(source: String, index: usize) struct { nextPosition: usize, token: ?Token } { 151 | var longestLen: usize = 0; 152 | var kind = Token.Kind.select_keyword; 153 | for (BUILTINS) |builtin| { 154 | if (index + builtin.name.len >= source.len) { 155 | continue; 156 | } 157 | 158 | if (asciiCaseInsensitiveEqual(source[index .. index + builtin.name.len], builtin.name)) { 159 | longestLen = builtin.name.len; 160 | kind = builtin.kind; 161 | // First match is the longest match 162 | break; 163 | } 164 | } 165 | 166 | if (longestLen == 0) { 167 | return .{ .nextPosition = 0, .token = null }; 168 | } 169 | 170 | return .{ 171 | .nextPosition = index + longestLen, 172 | .token = Token{ 173 | .source = source, 174 | .start = index, 175 | .end = index + longestLen, 176 | .kind = kind, 177 | }, 178 | }; 179 | } 180 | 181 | fn lexInteger(source: String, index: usize) struct { nextPosition: usize, token: ?Token } { 182 | var start = index; 183 | var end = index; 184 | var i = index; 185 | while (source[i] >= '0' and source[i] <= '9') { 186 | end = end + 1; 187 | i = i + 1; 188 | } 189 | 190 | if (start == end) { 191 | return .{ .nextPosition = 0, .token = null }; 192 | } 193 | 194 | return .{ 195 | .nextPosition = end, 196 | .token = Token{ 197 | .source = source, 198 | .start = start, 199 | .end = end, 200 | .kind = Token.Kind.integer, 201 | }, 202 | }; 203 | } 204 | 205 | fn lexString(source: String, index: usize) struct { nextPosition: usize, token: ?Token } { 206 | var i = index; 207 | if (source[i] != '\'') { 208 | return .{ .nextPosition = 0, .token = null }; 209 | } 210 | i = i + 1; 211 | 212 | var start = i; 213 | var end = i; 214 | while (source[i] != '\'') { 215 | end = end + 1; 216 | i = i + 1; 217 | } 218 | 219 | if (source[i] == '\'') { 220 | i = i + 1; 221 | } 222 | 223 | if (start == end) { 224 | return .{ .nextPosition = 0, .token = null }; 225 | } 226 | 227 | return .{ 228 | .nextPosition = i, 229 | .token = Token{ 230 | .source = source, 231 | .start = start, 232 | .end = end, 233 | .kind = Token.Kind.string, 234 | }, 235 | }; 236 | } 237 | 238 | fn lexIdentifier(source: String, index: usize) struct { nextPosition: usize, token: ?Token } { 239 | var start = index; 240 | var end = index; 241 | var i = index; 242 | while ((source[i] >= 'a' and source[i] <= 'z') or 243 | (source[i] >= 'A' and source[i] <= 'Z') or 244 | (source[i] == '*')) 245 | { 246 | end = end + 1; 247 | i = i + 1; 248 | } 249 | 250 | if (start == end) { 251 | return .{ .nextPosition = 0, .token = null }; 252 | } 253 | 254 | return .{ 255 | .nextPosition = end, 256 | .token = Token{ 257 | .source = source, 258 | .start = start, 259 | .end = end, 260 | .kind = Token.Kind.identifier, 261 | }, 262 | }; 263 | } 264 | 265 | pub fn lex(source: String, tokens: *std.ArrayList(Token)) ?Error { 266 | var i: usize = 0; 267 | while (true) { 268 | i = eatWhitespace(source, i); 269 | if (i >= source.len) { 270 | break; 271 | } 272 | 273 | const keywordRes = lexKeyword(source, i); 274 | if (keywordRes.token) |token| { 275 | tokens.append(token) catch return "Failed to allocate space for keyword token"; 276 | i = keywordRes.nextPosition; 277 | continue; 278 | } 279 | 280 | const integerRes = lexInteger(source, i); 281 | if (integerRes.token) |token| { 282 | tokens.append(token) catch return "Failed to allocate space for integer token"; 283 | i = integerRes.nextPosition; 284 | continue; 285 | } 286 | 287 | const stringRes = lexString(source, i); 288 | if (stringRes.token) |token| { 289 | tokens.append(token) catch return "Failed to allocate space for string token"; 290 | i = stringRes.nextPosition; 291 | continue; 292 | } 293 | 294 | const identifierRes = lexIdentifier(source, i); 295 | if (identifierRes.token) |token| { 296 | tokens.append(token) catch return "Failed to allocate space for identifier token"; 297 | i = identifierRes.nextPosition; 298 | continue; 299 | } 300 | 301 | if (tokens.items.len > 0) { 302 | debug(tokens.items, tokens.items.len - 1, "Last good token.\n"); 303 | } 304 | return "Bad token"; 305 | } 306 | 307 | return null; 308 | } 309 | -------------------------------------------------------------------------------- /main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const RocksDB = @import("rocksdb.zig").RocksDB; 4 | const lex = @import("lex.zig"); 5 | const parse = @import("parse.zig"); 6 | const execute = @import("execute.zig"); 7 | const Storage = @import("storage.zig").Storage; 8 | 9 | pub fn main() !void { 10 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 11 | defer arena.deinit(); 12 | const allocator = arena.allocator(); 13 | 14 | var debugTokens = false; 15 | var debugAST = false; 16 | var args = std.process.args(); 17 | var scriptArg: usize = 0; 18 | var databaseArg: usize = 0; 19 | var i: usize = 0; 20 | while (args.next()) |arg| { 21 | if (std.mem.eql(u8, arg, "--debug-tokens")) { 22 | debugTokens = true; 23 | } 24 | 25 | if (std.mem.eql(u8, arg, "--debug-ast")) { 26 | debugAST = true; 27 | } 28 | 29 | if (std.mem.eql(u8, arg, "--database")) { 30 | databaseArg = i + 1; 31 | i += 1; 32 | _ = args.next(); 33 | } 34 | 35 | if (std.mem.eql(u8, arg, "--script")) { 36 | scriptArg = i + 1; 37 | i += 1; 38 | _ = args.next(); 39 | } 40 | 41 | i += 1; 42 | } 43 | 44 | if (databaseArg == 0) { 45 | std.debug.print("--database is a required flag. Should be a directory for data.\n", .{}); 46 | return; 47 | } 48 | 49 | if (scriptArg == 0) { 50 | std.debug.print("--script is a required flag. Should be a file containing SQL.\n", .{}); 51 | return; 52 | } 53 | 54 | const file = try std.fs.cwd().openFileZ(std.os.argv[scriptArg], .{}); 55 | defer file.close(); 56 | 57 | const file_size = try file.getEndPos(); 58 | var prog = try allocator.alloc(u8, file_size); 59 | 60 | _ = try file.read(prog); 61 | 62 | var tokens = std.ArrayList(lex.Token).init(allocator); 63 | const lexErr = lex.lex(prog, &tokens); 64 | if (lexErr) |err| { 65 | std.debug.print("Failed to lex: {s}", .{err}); 66 | return; 67 | } 68 | 69 | if (debugTokens) { 70 | for (tokens.items) |token| { 71 | std.debug.print("Token: {s}\n", .{token.string()}); 72 | } 73 | } 74 | 75 | if (tokens.items.len == 0) { 76 | std.debug.print("Program is empty", .{}); 77 | return; 78 | } 79 | 80 | const parser = parse.Parser.init(allocator); 81 | var ast: parse.Parser.AST = undefined; 82 | switch (parser.parse(tokens.items)) { 83 | .err => |err| { 84 | std.debug.print("Failed to parse: {s}", .{err}); 85 | return; 86 | }, 87 | .val => |val| ast = val, 88 | } 89 | 90 | if (debugAST) { 91 | ast.print(); 92 | } 93 | 94 | var db: RocksDB = undefined; 95 | var dataDirectory = std.mem.span(std.os.argv[databaseArg]); 96 | switch (RocksDB.open(allocator, dataDirectory)) { 97 | .err => |err| { 98 | std.debug.print("Failed to open database: {s}", .{err}); 99 | return; 100 | }, 101 | .val => |val| db = val, 102 | } 103 | defer db.close(); 104 | 105 | const storage = Storage.init(allocator, db); 106 | 107 | const executor = execute.Executor.init(allocator, storage); 108 | switch (executor.execute(ast)) { 109 | .err => |err| { 110 | std.debug.print("Failed to execute: {s}", .{err}); 111 | return; 112 | }, 113 | .val => |val| { 114 | if (val.rows.len == 0) { 115 | std.debug.print("ok\n", .{}); 116 | return; 117 | } 118 | 119 | std.debug.print("| ", .{}); 120 | for (val.fields) |field| { 121 | std.debug.print("{s}\t\t|", .{field}); 122 | } 123 | std.debug.print("\n", .{}); 124 | std.debug.print("+ ", .{}); 125 | for (val.fields) |field| { 126 | var fieldLen = field.len; 127 | while (fieldLen > 0) { 128 | std.debug.print("=", .{}); 129 | fieldLen -= 1; 130 | } 131 | std.debug.print("\t\t+", .{}); 132 | } 133 | std.debug.print("\n", .{}); 134 | 135 | for (val.rows) |row| { 136 | std.debug.print("| ", .{}); 137 | for (row) |cell| { 138 | std.debug.print("{s}\t\t|", .{cell}); 139 | } 140 | std.debug.print("\n", .{}); 141 | } 142 | }, 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /old/execute_disk.zig: -------------------------------------------------------------------------------- 1 | // const std = @import("std"); 2 | // 3 | // const RocksDB = @import("./rocksdb.zig"); 4 | // const parse = @import("parse.zig"); 5 | // 6 | // const Error = []const u8; 7 | // 8 | // pub const Executor = struct { 9 | // allocator: std.mem.Allocator, 10 | // 11 | // const QueryResponse = struct { 12 | // fields: std.ArrayList([]u8), 13 | // rows: std.ArrayList([]u8), 14 | // }; 15 | // 16 | // pub fn init(allocator: std.mem.Allocator) Executor { 17 | // return Executor{ .allocator = allocator }; 18 | // } 19 | // 20 | // const TableMetadata = struct {}; 21 | // 22 | // fn serializeString(writer: std.io.Writer, string: []u8) ?Error { 23 | // var length: [8]u8 = @as(u64, string.len); 24 | // writer.write(length) catch return "Could not write string length"; 25 | // writer.write(string) catch return "Could not write string"; 26 | // return null; 27 | // } 28 | // 29 | // fn deserializeString(serialized: []u8) []u8 { 30 | // var lengthBytes = serialized[0..8]; 31 | // var length = @as(u64, lengthBytes); 32 | // return serialized[8..length]; 33 | // } 34 | // 35 | // fn executeSelect(self: Executor, db: RocksDB, s: parse.SelectAST) struct { 36 | // val: ?QueryResponse, 37 | // err: ?Error, 38 | // } { 39 | // // First grab table info 40 | // var tableInfo = db.get("tbl" ++ s.table.string()); 41 | // var tableColumns = std.ArrayList([]u8).init(self.allocator); 42 | // while (tableInfo.length > 0) { 43 | // var column = deserializeString(tableInfo); 44 | // tableColumns.append(column) catch return .{ 45 | // .val = null, 46 | // .err = "Could not allocate for column.", 47 | // }; 48 | // tableInfo = tableInfo[8 + column.length ..]; 49 | // } 50 | // 51 | // var realFields = std.ArrayList([]u8).init(self.allocator); 52 | // for (s.columns) |requestedColumn| { 53 | // var found = false; 54 | // for (tableColumns) |column| { 55 | // if (std.mem.eql(u8, column, requestedColumn)) { 56 | // found = true; 57 | // } 58 | // } 59 | // 60 | // if (!found) { 61 | // return .{ .val = null, .err = "No such column exists: " ++ requestedColumn }; 62 | // } 63 | // 64 | // realFields.append(column) catch return .{ .val = null, .err = "Could not allocate for real field." }; 65 | // } 66 | // 67 | // var response = QueryResponse{ 68 | // .fields = realFields, 69 | // .values = std.ArrayList([]u8).init(self.allocator), 70 | // }; 71 | // } 72 | // 73 | // fn executeInsert(self: Executor, db: RocksDB, c: parse.InsertAST) struct { val: ?QueryResponse, err: ?Error } { 74 | // var key = std.ArrayList(u8).init(self.allocator); 75 | // var keyWriter = key.writer(); 76 | // _ = keyWriter.write("row" ++ c.table.string()) catch return .{ .val = null, .err = "Could not write row's table name" }; 77 | // var uuid = self.generateUUID() catch return .{ .val = null, .err = "Could not generate UUID" }; 78 | // _ = keyWriter.write(uuid) catch return .{ .val = null, .err = "Could not write UUID" }; 79 | // 80 | // var value = std.ArrayList(u8).init(self.allocator); 81 | // var valueWriter = value.writer(); 82 | // for (c.values.items) |v| { 83 | // var exp = self.executeExpression(v); 84 | // var err = serializeString(valueWriter, exp); 85 | // if (err != null) { 86 | // return .{ .val = null, .err = err }; 87 | // } 88 | // } 89 | // 90 | // // TODO: can we get rid of all ptrCasts? 91 | // 92 | // var keySlice: [:0]const u8 = std.mem.span(@ptrCast(*[:0]const u8, &key.items[0..key.items.len]).*); 93 | // var valueSlice: [:0]const u8 = std.mem.span(@ptrCast(*[:0]const u8, &value.items[0..value.items.len]).*); 94 | // _ = db.set(keySlice, valueSlice); 95 | // return .{ .val = null, .err = null }; 96 | // } 97 | // 98 | // fn executeCreateTable(self: Executor, db: RocksDB, c: parse.CreateTableAST) struct { val: ?QueryResponse, err: ?Error } { 99 | // var key = std.ArrayList(u8).init(self.allocator); 100 | // var keyWriter = key.writer(); 101 | // _ = keyWriter.write("tbl" ++ c.table.string()) catch return .{ .val = null, .err = "Could not write table name" }; 102 | // 103 | // var value = std.ArrayList(u8).init(self.allocator); 104 | // var valueWriter = value.writer(); 105 | // for (c.columns.items) |column| { 106 | // serializeString( 107 | // valueWriter, 108 | // column.string(), 109 | // ); 110 | // } 111 | // 112 | // // TODO: can we get rid of all ptrCasts? 113 | // var keySlice: [:0]const u8 = std.mem.span(@ptrCast(*[:0]const u8, &key.items[0..key.items.len]).*); 114 | // var valueSlice: [:0]const u8 = std.mem.span(@ptrCast(*[:0]const u8, &value.items[0..value.items.len]).*); 115 | // _ = db.set(keySlice, valueSlice); 116 | // return .{ .val = null, .err = null }; 117 | // } 118 | // 119 | // fn execute(self: Executor, db: RocksDB, ast: parse.AST) struct { val: ?QueryResponse, err: ?Error } { 120 | // if (ast.kind == .select_keyword) { 121 | // var res = self.executeSelect(db, ast.select.*); 122 | // return .{ .val = res.val, .err = res.err }; 123 | // } else if (ast.kind == .insert_keyword) { 124 | // var res = self.executeInsert(db, ast.insert.*); 125 | // return .{ .val = res.val, .err = res.err }; 126 | // } else if (ast.kind == .create_table_keyword) { 127 | // var res = self.executeCreateTable(db, ast.create_table.*); 128 | // return .{ .val = res.val, .err = res.err }; 129 | // } 130 | // 131 | // return .{ .val = null, .err = "Cannot execute unknown statement" }; 132 | // } 133 | // }; 134 | -------------------------------------------------------------------------------- /old/execute_memory.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const parse = @import("parse.zig"); 4 | 5 | const Error = []const u8; 6 | 7 | pub const Executor = struct { 8 | const ColumnInfo = struct { 9 | name: []u8, 10 | kind: []u8, 11 | }; 12 | const RowInfo = struct { 13 | cells: std.ArrayList([]u8), 14 | }; 15 | const TableInfo = struct { 16 | name: []u8, 17 | columns: std.ArrayList([]u8), 18 | rows: std.ArrayList(RowInfo), 19 | }; 20 | 21 | allocator: std.mem.Allocator, 22 | tables: std.ArrayList(TableInfo), 23 | 24 | pub fn init(allocator: std.mem.Allocator) Executor { 25 | return Executor{ .allocator = allocator }; 26 | } 27 | 28 | const QueryResponse = struct { 29 | fields: std.ArrayList([]u8), 30 | rows: std.ArrayList(std.ArrayList([]u8)), 31 | }; 32 | 33 | fn executeSelect(self: Executor, s: parse.SelectAST) struct { 34 | val: ?QueryResponse, 35 | err: ?Error, 36 | } { 37 | var table: ?TableInfo = null; 38 | for (self.tables.items) |t| { 39 | if (!std.mem.eql(u8, t.name, s.table.string())) { 40 | continue; 41 | } 42 | 43 | table = t; 44 | } 45 | 46 | if (table == null) { 47 | return .{ .val = null, .err = "No such table exists: " ++ s.table.string() }; 48 | } 49 | 50 | var realFields = std.ArrayList([]u8).init(self.allocator); 51 | var realFieldIndexes = std.ArrayList(usize).init(self.allocator); 52 | for (s.columns) |requestedColumn, i| { 53 | var found = false; 54 | for (tableColumns) |column| { 55 | if (std.mem.eql(u8, column, requestedColumn)) { 56 | found = true; 57 | } 58 | } 59 | 60 | if (!found) { 61 | return .{ .val = null, .err = "No such column exists: " ++ requestedColumn }; 62 | } 63 | 64 | realFields.append(column) catch return .{ 65 | .val = null, 66 | .err = "Could not allocate for real field.", 67 | }; 68 | realFieldIndexes.append(i) catch return .{ 69 | .val = null, 70 | .err = "Could not allocate for real field index.", 71 | }; 72 | } 73 | 74 | var response = QueryResponse{ 75 | .fields = realFields, 76 | .values = std.ArrayList(RowInfo).init(self.allocator), 77 | }; 78 | 79 | for (table.rows.items) |row| { 80 | if (s.where) |where| { 81 | var filtered = self.evaluateExpression(where, row); 82 | if (filtered) { 83 | continue; 84 | } 85 | } 86 | 87 | var ri = RowInfo{ .cells = std.ArrayList([]u8).init(self.allocator) }; 88 | for (realFieldIndexes.items) |index| {} 89 | } 90 | 91 | // First grab table info 92 | var tableInfo = db.get("tbl" ++ s.table.string()); 93 | var tableColumns = std.ArrayList([]u8).init(self.allocator); 94 | while (tableInfo.length > 0) { 95 | var column = deserializeString(tableInfo); 96 | tableColumns.append(column) catch return .{ 97 | .val = null, 98 | .err = "Could not allocate for column.", 99 | }; 100 | tableInfo = tableInfo[8 + column.length ..]; 101 | } 102 | 103 | return .{ .val = response, .err = null }; 104 | } 105 | 106 | fn executeInsert(self: Executor, c: parse.InsertAST) struct { val: ?QueryResponse, err: ?Error } { 107 | var key = std.ArrayList(u8).init(self.allocator); 108 | var keyWriter = key.writer(); 109 | _ = keyWriter.write("row" ++ c.table.string()) catch return .{ .val = null, .err = "Could not write row's table name" }; 110 | var uuid = self.generateUUID() catch return .{ .val = null, .err = "Could not generate UUID" }; 111 | _ = keyWriter.write(uuid) catch return .{ .val = null, .err = "Could not write UUID" }; 112 | 113 | var value = std.ArrayList(u8).init(self.allocator); 114 | var valueWriter = value.writer(); 115 | for (c.values.items) |v| { 116 | var exp = self.executeExpression(v); 117 | var err = serializeString(valueWriter, exp); 118 | if (err != null) { 119 | return .{ .val = null, .err = err }; 120 | } 121 | } 122 | 123 | return .{ .val = null, .err = null }; 124 | } 125 | 126 | fn executeCreateTable(self: Executor, c: parse.CreateTableAST) struct { 127 | val: ?QueryResponse, 128 | err: ?Error, 129 | } { 130 | var table = TableInfo{ 131 | .name = c.table.string(), 132 | .columns = std.ArrayList(ColumnInfo).init(self.allocator), 133 | .rows = std.ArrayList(RowInfo).init(self.allocator), 134 | }; 135 | 136 | for (c.columns.items) |column| { 137 | var info = ColumnInfo{ 138 | .name = column.name.string(), 139 | .kind = column.kind.string(), 140 | }; 141 | table.columns.append(info) catch return .{ 142 | .val = null, 143 | .err = "Could not allocate for column info.", 144 | }; 145 | } 146 | 147 | return .{ .val = QueryResponse{}, .err = null }; 148 | } 149 | 150 | fn execute(self: Executor, ast: parse.AST) struct { val: ?QueryResponse, err: ?Error } { 151 | if (ast.kind == .select_keyword) { 152 | var res = self.executeSelect(ast.select.*); 153 | return .{ .val = res.val, .err = res.err }; 154 | } else if (ast.kind == .insert_keyword) { 155 | var res = self.executeInsert(ast.insert.*); 156 | return .{ .val = res.val, .err = res.err }; 157 | } else if (ast.kind == .create_table_keyword) { 158 | var res = self.executeCreateTable(ast.create_table.*); 159 | return .{ .val = res.val, .err = res.err }; 160 | } 161 | 162 | return .{ .val = null, .err = "Cannot execute unknown statement" }; 163 | } 164 | }; 165 | -------------------------------------------------------------------------------- /old/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | fn ownZeroString(zstr: [*:0]u8) []u8 { 4 | var spanned = std.mem.span(zstr); 5 | const result = std.heap.c_allocator.alloc(u8, spanned.len) catch unreachable; 6 | std.mem.copy(u8, result, spanned); 7 | std.heap.c_allocator.free(zstr); 8 | return result; 9 | } 10 | 11 | pub fn main() void { 12 | var err: ?[*:0]u8 = null; 13 | if (err) |errStr| { 14 | var x = ownZeroString(errStr); 15 | _ = x; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /parse.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const lex = @import("lex.zig"); 4 | const Result = @import("types.zig").Result; 5 | 6 | const Token = lex.Token; 7 | 8 | pub const Parser = struct { 9 | allocator: std.mem.Allocator, 10 | 11 | pub fn init(allocator: std.mem.Allocator) Parser { 12 | return Parser{ .allocator = allocator }; 13 | } 14 | 15 | fn expectTokenKind(tokens: []Token, index: usize, kind: Token.Kind) bool { 16 | if (index >= tokens.len) { 17 | return false; 18 | } 19 | 20 | return tokens[index].kind == kind; 21 | } 22 | 23 | pub const BinaryOperationAST = struct { 24 | operator: Token, 25 | left: *ExpressionAST, 26 | right: *ExpressionAST, 27 | 28 | fn print(self: BinaryOperationAST) void { 29 | self.left.print(); 30 | std.debug.print(" {s} ", .{self.operator.string()}); 31 | self.right.print(); 32 | } 33 | }; 34 | 35 | pub const ExpressionAST = union(enum) { 36 | literal: Token, 37 | binary_operation: BinaryOperationAST, 38 | 39 | fn print(self: ExpressionAST) void { 40 | switch (self) { 41 | .literal => |literal| switch (literal.kind) { 42 | .string => std.debug.print("'{s}'", .{literal.string()}), 43 | else => std.debug.print("{s}", .{literal.string()}), 44 | }, 45 | .binary_operation => self.binary_operation.print(), 46 | } 47 | } 48 | }; 49 | 50 | fn parseExpression(self: Parser, tokens: []Token, index: usize) Result(struct { 51 | ast: ExpressionAST, 52 | nextPosition: usize, 53 | }) { 54 | var i = index; 55 | 56 | var e: ExpressionAST = undefined; 57 | 58 | if (expectTokenKind(tokens, i, Token.Kind.integer) or 59 | expectTokenKind(tokens, i, Token.Kind.identifier) or 60 | expectTokenKind(tokens, i, Token.Kind.string)) 61 | { 62 | e = ExpressionAST{ .literal = tokens[i] }; 63 | i = i + 1; 64 | } else { 65 | return .{ .err = "No expression" }; 66 | } 67 | 68 | if (expectTokenKind(tokens, i, Token.Kind.equal_operator) or 69 | expectTokenKind(tokens, i, Token.Kind.lt_operator) or 70 | expectTokenKind(tokens, i, Token.Kind.plus_operator) or 71 | expectTokenKind(tokens, i, Token.Kind.concat_operator)) 72 | { 73 | var newE = ExpressionAST{ 74 | .binary_operation = BinaryOperationAST{ 75 | .operator = tokens[i], 76 | .left = self.allocator.create(ExpressionAST) catch return .{ 77 | .err = "Could not allocate for left expression.", 78 | }, 79 | .right = self.allocator.create(ExpressionAST) catch return .{ 80 | .err = "Could not allocate for right expression.", 81 | }, 82 | }, 83 | }; 84 | newE.binary_operation.left.* = e; 85 | e = newE; 86 | 87 | switch (self.parseExpression(tokens, i + 1)) { 88 | .err => |err| return .{ .err = err }, 89 | .val => |val| { 90 | e.binary_operation.right.* = val.ast; 91 | i = val.nextPosition; 92 | }, 93 | } 94 | } 95 | 96 | return .{ .val = .{ .ast = e, .nextPosition = i } }; 97 | } 98 | 99 | pub const SelectAST = struct { 100 | columns: []ExpressionAST, 101 | from: Token, 102 | where: ?ExpressionAST, 103 | 104 | fn print(self: SelectAST) void { 105 | std.debug.print("SELECT\n", .{}); 106 | for (self.columns) |column, i| { 107 | std.debug.print(" ", .{}); 108 | column.print(); 109 | if (i < self.columns.len - 1) { 110 | std.debug.print(",", .{}); 111 | } 112 | std.debug.print("\n", .{}); 113 | } 114 | std.debug.print("FROM\n {s}", .{self.from.string()}); 115 | 116 | if (self.where) |where| { 117 | std.debug.print("\nWHERE\n ", .{}); 118 | where.print(); 119 | } 120 | 121 | std.debug.print("\n", .{}); 122 | } 123 | }; 124 | 125 | fn parseSelect(self: Parser, tokens: []Token) Result(AST) { 126 | var i: usize = 0; 127 | if (!expectTokenKind(tokens, i, Token.Kind.select_keyword)) { 128 | return .{ .err = "Expected SELECT keyword" }; 129 | } 130 | i = i + 1; 131 | 132 | var columns = std.ArrayList(ExpressionAST).init(self.allocator); 133 | var select = SelectAST{ 134 | .columns = undefined, 135 | .from = undefined, 136 | .where = null, 137 | }; 138 | 139 | // Parse columns 140 | while (!expectTokenKind(tokens, i, Token.Kind.from_keyword)) { 141 | if (columns.items.len > 0) { 142 | if (!expectTokenKind(tokens, i, Token.Kind.comma_syntax)) { 143 | lex.debug(tokens, i, "Expected comma.\n"); 144 | return .{ .err = "Expected comma." }; 145 | } 146 | 147 | i = i + 1; 148 | } 149 | 150 | switch (self.parseExpression(tokens, i)) { 151 | .err => |err| return .{ .err = err }, 152 | .val => |val| { 153 | i = val.nextPosition; 154 | 155 | columns.append(val.ast) catch return .{ 156 | .err = "Could not allocate for token.", 157 | }; 158 | }, 159 | } 160 | } 161 | 162 | if (!expectTokenKind(tokens, i, Token.Kind.from_keyword)) { 163 | lex.debug(tokens, i, "Expected FROM keyword after this.\n"); 164 | return .{ .err = "Expected FROM keyword" }; 165 | } 166 | i = i + 1; 167 | 168 | if (!expectTokenKind(tokens, i, Token.Kind.identifier)) { 169 | lex.debug(tokens, i, "Expected FROM table name after this.\n"); 170 | return .{ .err = "Expected FROM keyword" }; 171 | } 172 | select.from = tokens[i]; 173 | i = i + 1; 174 | 175 | if (expectTokenKind(tokens, i, Token.Kind.where_keyword)) { 176 | // i + 1, skip past the where 177 | switch (self.parseExpression(tokens, i + 1)) { 178 | .err => |err| return .{ .err = err }, 179 | .val => |val| { 180 | select.where = val.ast; 181 | i = val.nextPosition; 182 | }, 183 | } 184 | } 185 | 186 | if (i < tokens.len) { 187 | lex.debug(tokens, i, "Unexpected token."); 188 | return .{ .err = "Did not complete parsing SELECT" }; 189 | } 190 | 191 | select.columns = columns.items; 192 | return .{ .val = AST{ .select = select } }; 193 | } 194 | 195 | const CreateTableColumnAST = struct { 196 | name: Token, 197 | kind: Token, 198 | }; 199 | 200 | pub const CreateTableAST = struct { 201 | table: Token, 202 | columns: []CreateTableColumnAST, 203 | 204 | fn print(self: CreateTableAST) void { 205 | std.debug.print("CREATE TABLE {s} (\n", .{self.table.string()}); 206 | for (self.columns) |column, i| { 207 | std.debug.print( 208 | " {s} {s}", 209 | .{ column.name.string(), column.kind.string() }, 210 | ); 211 | if (i < self.columns.len - 1) { 212 | std.debug.print(",", .{}); 213 | } 214 | std.debug.print("\n", .{}); 215 | } 216 | std.debug.print(")\n", .{}); 217 | } 218 | }; 219 | 220 | fn parseCreateTable(self: Parser, tokens: []Token) Result(AST) { 221 | var i: usize = 0; 222 | if (!expectTokenKind(tokens, i, Token.Kind.create_table_keyword)) { 223 | return .{ .err = "Expected CREATE TABLE keyword" }; 224 | } 225 | i = i + 1; 226 | 227 | if (!expectTokenKind(tokens, i, Token.Kind.identifier)) { 228 | lex.debug(tokens, i, "Expected table name after CREATE TABLE keyword.\n"); 229 | return .{ .err = "Expected CREATE TABLE name" }; 230 | } 231 | 232 | var columns = std.ArrayList(CreateTableColumnAST).init(self.allocator); 233 | var create_table = CreateTableAST{ 234 | .columns = undefined, 235 | .table = tokens[i], 236 | }; 237 | i = i + 1; 238 | 239 | if (!expectTokenKind(tokens, i, Token.Kind.left_paren_syntax)) { 240 | lex.debug(tokens, i, "Expected opening paren after CREATE TABLE name.\n"); 241 | return .{ .err = "Expected opening paren" }; 242 | } 243 | i = i + 1; 244 | 245 | while (!expectTokenKind(tokens, i, Token.Kind.right_paren_syntax)) { 246 | if (columns.items.len > 0) { 247 | if (!expectTokenKind(tokens, i, Token.Kind.comma_syntax)) { 248 | lex.debug(tokens, i, "Expected comma.\n"); 249 | return .{ .err = "Expected comma." }; 250 | } 251 | 252 | i = i + 1; 253 | } 254 | 255 | var column = CreateTableColumnAST{ .name = undefined, .kind = undefined }; 256 | if (!expectTokenKind(tokens, i, Token.Kind.identifier)) { 257 | lex.debug(tokens, i, "Expected column name after comma.\n"); 258 | return .{ .err = "Expected identifier." }; 259 | } 260 | 261 | column.name = tokens[i]; 262 | i = i + 1; 263 | 264 | if (!expectTokenKind(tokens, i, Token.Kind.identifier)) { 265 | lex.debug(tokens, i, "Expected column type after column name.\n"); 266 | return .{ .err = "Expected identifier." }; 267 | } 268 | 269 | column.kind = tokens[i]; 270 | i = i + 1; 271 | 272 | columns.append(column) catch return .{ 273 | .err = "Could not allocate for column.", 274 | }; 275 | } 276 | 277 | // Skip past final paren. 278 | i = i + 1; 279 | 280 | if (i < tokens.len) { 281 | lex.debug(tokens, i, "Unexpected token."); 282 | return .{ .err = "Did not complete parsing CREATE TABLE" }; 283 | } 284 | 285 | create_table.columns = columns.items; 286 | return .{ .val = AST{ .create_table = create_table } }; 287 | } 288 | 289 | pub const InsertAST = struct { 290 | table: Token, 291 | values: []ExpressionAST, 292 | 293 | fn print(self: InsertAST) void { 294 | std.debug.print("INSERT INTO {s} VALUES (", .{self.table.string()}); 295 | for (self.values) |value, i| { 296 | value.print(); 297 | if (i < self.values.len - 1) { 298 | std.debug.print(", ", .{}); 299 | } 300 | } 301 | std.debug.print(")\n", .{}); 302 | } 303 | }; 304 | 305 | fn parseInsert(self: Parser, tokens: []Token) Result(AST) { 306 | var i: usize = 0; 307 | if (!expectTokenKind(tokens, i, Token.Kind.insert_keyword)) { 308 | return .{ .err = "Expected INSERT INTO keyword" }; 309 | } 310 | i = i + 1; 311 | 312 | if (!expectTokenKind(tokens, i, Token.Kind.identifier)) { 313 | lex.debug(tokens, i, "Expected table name after INSERT INTO keyword.\n"); 314 | return .{ .err = "Expected INSERT INTO table name" }; 315 | } 316 | 317 | var values = std.ArrayList(ExpressionAST).init(self.allocator); 318 | var insert = InsertAST{ 319 | .values = undefined, 320 | .table = tokens[i], 321 | }; 322 | i = i + 1; 323 | 324 | if (!expectTokenKind(tokens, i, Token.Kind.values_keyword)) { 325 | lex.debug(tokens, i, "Expected VALUES keyword.\n"); 326 | return .{ .err = "Expected VALUES keyword" }; 327 | } 328 | i = i + 1; 329 | 330 | if (!expectTokenKind(tokens, i, Token.Kind.left_paren_syntax)) { 331 | lex.debug(tokens, i, "Expected opening paren after CREATE TABLE name.\n"); 332 | return .{ .err = "Expected opening paren" }; 333 | } 334 | i = i + 1; 335 | 336 | while (!expectTokenKind(tokens, i, Token.Kind.right_paren_syntax)) { 337 | if (values.items.len > 0) { 338 | if (!expectTokenKind(tokens, i, Token.Kind.comma_syntax)) { 339 | lex.debug(tokens, i, "Expected comma.\n"); 340 | return .{ .err = "Expected comma." }; 341 | } 342 | 343 | i = i + 1; 344 | } 345 | 346 | switch (self.parseExpression(tokens, i)) { 347 | .err => |err| return .{ .err = err }, 348 | .val => |val| { 349 | values.append(val.ast) catch return .{ 350 | .err = "Could not allocate for expression.", 351 | }; 352 | i = val.nextPosition; 353 | }, 354 | } 355 | } 356 | 357 | // Skip past final paren. 358 | i = i + 1; 359 | 360 | if (i < tokens.len) { 361 | lex.debug(tokens, i, "Unexpected token."); 362 | return .{ .err = "Did not complete parsing INSERT INTO" }; 363 | } 364 | 365 | insert.values = values.items; 366 | return .{ .val = AST{ .insert = insert } }; 367 | } 368 | 369 | pub const AST = union(enum) { 370 | select: SelectAST, 371 | insert: InsertAST, 372 | create_table: CreateTableAST, 373 | 374 | pub fn print(self: AST) void { 375 | switch (self) { 376 | .select => |select| select.print(), 377 | .insert => |insert| insert.print(), 378 | .create_table => |create_table| create_table.print(), 379 | } 380 | } 381 | }; 382 | 383 | pub fn parse(self: Parser, tokens: []Token) Result(AST) { 384 | if (expectTokenKind(tokens, 0, Token.Kind.select_keyword)) { 385 | return switch (self.parseSelect(tokens)) { 386 | .err => |err| .{ .err = err }, 387 | .val => |val| .{ .val = val }, 388 | }; 389 | } 390 | 391 | if (expectTokenKind(tokens, 0, Token.Kind.create_table_keyword)) { 392 | return switch (self.parseCreateTable(tokens)) { 393 | .err => |err| .{ .err = err }, 394 | .val => |val| .{ .val = val }, 395 | }; 396 | } 397 | 398 | if (expectTokenKind(tokens, 0, Token.Kind.insert_keyword)) { 399 | return switch (self.parseInsert(tokens)) { 400 | .err => |err| .{ .err = err }, 401 | .val => |val| .{ .val = val }, 402 | }; 403 | } 404 | 405 | return .{ .err = "Unknown statement" }; 406 | } 407 | }; 408 | -------------------------------------------------------------------------------- /rocksdb.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const rdb = @cImport(@cInclude("rocksdb/c.h")); 4 | 5 | pub const RocksDB = struct { 6 | db: *rdb.rocksdb_t, 7 | allocator: std.mem.Allocator, 8 | 9 | // Strings in RocksDB are malloc-ed. So make a copy that our 10 | // allocator owns and then free the string. 11 | fn ownString(self: RocksDB, string: []u8) []u8 { 12 | const result = self.allocator.alloc(u8, string.len) catch unreachable; 13 | std.mem.copy(u8, result, string); 14 | std.heap.c_allocator.free(string); 15 | return result; 16 | } 17 | 18 | // Similar to ownString but for strings that are zero delimited, 19 | // drops the zero. 20 | fn ownZeroString(self: RocksDB, zstr: [*:0]u8) []u8 { 21 | var spanned = std.mem.span(zstr); 22 | const result = self.allocator.alloc(u8, spanned.len) catch unreachable; 23 | std.mem.copy(u8, result, spanned); 24 | std.heap.c_allocator.free(zstr); 25 | return result; 26 | } 27 | 28 | // TODO: replace std.mem.span(errStr) with ownZeroString() 29 | 30 | pub fn open(allocator: std.mem.Allocator, dir: []const u8) union(enum) { val: RocksDB, err: []u8 } { 31 | var options: ?*rdb.rocksdb_options_t = rdb.rocksdb_options_create(); 32 | rdb.rocksdb_options_set_create_if_missing(options, 1); 33 | var err: ?[*:0]u8 = null; 34 | var db = rdb.rocksdb_open(options, dir.ptr, &err); 35 | var r = RocksDB{ .db = db.?, .allocator = allocator }; 36 | if (err) |errStr| { 37 | return .{ .err = std.mem.span(errStr) }; 38 | } 39 | return .{ .val = r }; 40 | } 41 | 42 | pub fn close(self: RocksDB) void { 43 | rdb.rocksdb_close(self.db); 44 | } 45 | 46 | pub fn set(self: RocksDB, key: []const u8, value: []const u8) ?[]u8 { 47 | var writeOptions = rdb.rocksdb_writeoptions_create(); 48 | var err: ?[*:0]u8 = null; 49 | rdb.rocksdb_put( 50 | self.db, 51 | writeOptions, 52 | key.ptr, 53 | key.len, 54 | value.ptr, 55 | value.len, 56 | &err, 57 | ); 58 | if (err) |errStr| { 59 | return std.mem.span(errStr); 60 | } 61 | 62 | return null; 63 | } 64 | 65 | pub fn get(self: RocksDB, key: []const u8) union(enum) { val: []u8, err: []u8, not_found: bool } { 66 | var readOptions = rdb.rocksdb_readoptions_create(); 67 | var valueLength: usize = 0; 68 | var err: ?[*:0]u8 = null; 69 | var v = rdb.rocksdb_get( 70 | self.db, 71 | readOptions, 72 | key.ptr, 73 | key.len, 74 | &valueLength, 75 | &err, 76 | ); 77 | if (err) |errStr| { 78 | return .{ .err = std.mem.span(errStr) }; 79 | } 80 | if (v == 0) { 81 | return .{ .not_found = true }; 82 | } 83 | 84 | return .{ .val = self.ownString(v[0..valueLength]) }; 85 | } 86 | 87 | pub const IterEntry = struct { 88 | key: []const u8, 89 | value: []const u8, 90 | }; 91 | 92 | pub const Iter = struct { 93 | iter: *rdb.rocksdb_iterator_t, 94 | first: bool, 95 | prefix: []const u8, 96 | 97 | pub fn next(self: *Iter) ?IterEntry { 98 | if (!self.first) { 99 | rdb.rocksdb_iter_next(self.iter); 100 | } 101 | 102 | self.first = false; 103 | if (rdb.rocksdb_iter_valid(self.iter) != 1) { 104 | return null; 105 | } 106 | 107 | var keySize: usize = 0; 108 | var key = rdb.rocksdb_iter_key(self.iter, &keySize); 109 | 110 | // Make sure key is still within the prefix 111 | if (self.prefix.len > 0) { 112 | if (self.prefix.len > keySize or 113 | !std.mem.eql(u8, key[0..self.prefix.len], self.prefix)) 114 | { 115 | return null; 116 | } 117 | } 118 | 119 | var valueSize: usize = 0; 120 | var value = rdb.rocksdb_iter_value(self.iter, &valueSize); 121 | 122 | return IterEntry{ 123 | .key = key[0..keySize], 124 | .value = value[0..valueSize], 125 | }; 126 | } 127 | 128 | pub fn close(self: Iter) void { 129 | rdb.rocksdb_iter_destroy(self.iter); 130 | } 131 | }; 132 | 133 | pub fn iter(self: RocksDB, prefix: []const u8) union(enum) { val: Iter, err: []u8 } { 134 | var readOptions = rdb.rocksdb_readoptions_create(); 135 | var it = Iter{ 136 | .iter = undefined, 137 | .first = true, 138 | .prefix = prefix, 139 | }; 140 | it.iter = rdb.rocksdb_create_iterator(self.db, readOptions).?; 141 | 142 | var err: ?[*:0]u8 = null; 143 | rdb.rocksdb_iter_get_error(it.iter, &err); 144 | if (err) |errStr| { 145 | return .{ .err = std.mem.span(errStr) }; 146 | } 147 | 148 | if (prefix.len > 0) { 149 | rdb.rocksdb_iter_seek( 150 | it.iter, 151 | prefix.ptr, 152 | prefix.len, 153 | ); 154 | } else { 155 | rdb.rocksdb_iter_seek_to_first(it.iter); 156 | } 157 | 158 | return .{ .val = it }; 159 | } 160 | }; 161 | 162 | pub fn main() !void { 163 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 164 | defer arena.deinit(); 165 | 166 | const allocator = arena.allocator(); 167 | 168 | var db: RocksDB = undefined; 169 | switch (RocksDB.open(allocator, "/tmp/db")) { 170 | .val => |_db| { 171 | db = _db; 172 | }, 173 | .err => |err| { 174 | std.debug.print("Failed to open: {s}.\n", .{err}); 175 | return; 176 | }, 177 | } 178 | defer db.close(); 179 | 180 | var args = std.process.args(); 181 | _ = args.next(); 182 | var key: []const u8 = ""; 183 | var value: []const u8 = ""; 184 | var command = "get"; 185 | while (args.next()) |arg| { 186 | if (std.mem.eql(u8, arg, "set")) { 187 | command = "set"; 188 | key = std.mem.span(args.next().?); 189 | value = std.mem.span(args.next().?); 190 | } else if (std.mem.eql(u8, arg, "get")) { 191 | command = "get"; 192 | key = std.mem.span(args.next().?); 193 | } else if (std.mem.eql(u8, arg, "list")) { 194 | command = "lst"; 195 | if (args.next()) |argNext| { 196 | key = std.mem.span(argNext); 197 | } 198 | } else { 199 | std.debug.print("Must specify command (get, set, or list). Got: '{s}'.\n", .{arg}); 200 | return; 201 | } 202 | } 203 | 204 | if (std.mem.eql(u8, command, "set")) { 205 | var setErr = db.set(key, value); 206 | if (setErr) |err| { 207 | std.debug.print("Error setting key: {s}.\n", .{err}); 208 | return; 209 | } 210 | } else if (std.mem.eql(u8, command, "get")) { 211 | switch (db.get(key)) { 212 | .err => |err| { 213 | std.debug.print("Error getting key: {s}.\n", .{err}); 214 | return; 215 | }, 216 | .val => |val| std.debug.print("{s}\n", .{val}), 217 | .not_found => std.debug.print("Key not found.\n", .{}), 218 | } 219 | } else { 220 | var prefix = key; 221 | switch (db.iter(prefix)) { 222 | .err => |err| std.debug.print("Error getting iterator: {s}.\n", .{err}), 223 | .val => |iter| { 224 | // Create a local variable so that it.next() can 225 | // mutate it as a reference. 226 | var it = iter; 227 | defer it.close(); 228 | while (it.next()) |entry| { 229 | std.debug.print("{s} = {s}\n", .{ entry.key, entry.value }); 230 | } 231 | }, 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /storage.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const RocksDB = @import("rocksdb.zig").RocksDB; 4 | const Error = @import("types.zig").Error; 5 | const Result = @import("types.zig").Result; 6 | const String = @import("types.zig").String; 7 | 8 | pub fn serializeInteger(comptime T: type, buf: *std.ArrayList(u8), i: T) !void { 9 | var length: [@sizeOf(T)]u8 = undefined; 10 | std.mem.writeIntBig(T, &length, i); 11 | try buf.appendSlice(length[0..8]); 12 | } 13 | 14 | pub fn deserializeInteger(comptime T: type, buf: String) T { 15 | return std.mem.readIntBig(T, buf[0..@sizeOf(T)]); 16 | } 17 | 18 | pub fn serializeBytes(buf: *std.ArrayList(u8), bytes: String) !void { 19 | try serializeInteger(u64, buf, bytes.len); 20 | try buf.appendSlice(bytes); 21 | } 22 | 23 | pub fn deserializeBytes(bytes: String) struct { 24 | offset: usize, 25 | bytes: String, 26 | } { 27 | var length = deserializeInteger(u64, bytes); 28 | var offset = length + 8; 29 | return .{ .offset = offset, .bytes = bytes[8..offset] }; 30 | } 31 | 32 | pub const Storage = struct { 33 | db: RocksDB, 34 | allocator: std.mem.Allocator, 35 | 36 | pub fn init(allocator: std.mem.Allocator, db: RocksDB) Storage { 37 | return Storage{ 38 | .db = db, 39 | .allocator = allocator, 40 | }; 41 | } 42 | 43 | pub const Value = union(enum) { 44 | bool_value: bool, 45 | null_value: bool, 46 | string_value: String, 47 | integer_value: i64, 48 | 49 | pub const TRUE = Value{ .bool_value = true }; 50 | pub const FALSE = Value{ .bool_value = false }; 51 | pub const NULL = Value{ .null_value = true }; 52 | 53 | pub fn fromIntegerString(iBytes: String) Value { 54 | const i = std.fmt.parseInt(i64, iBytes, 10) catch return Value{ 55 | .integer_value = 0, 56 | }; 57 | return Value{ .integer_value = i }; 58 | } 59 | 60 | pub fn asBool(self: Value) bool { 61 | return switch (self) { 62 | .null_value => false, 63 | .bool_value => |value| value, 64 | .string_value => |value| value.len > 0, 65 | .integer_value => |value| value != 0, 66 | }; 67 | } 68 | 69 | pub fn asString(self: Value, buf: *std.ArrayList(u8)) !void { 70 | try switch (self) { 71 | .null_value => _ = 1, // Do nothing 72 | .bool_value => |value| buf.appendSlice(if (value) "true" else "false"), 73 | .string_value => |value| buf.appendSlice(value), 74 | .integer_value => |value| buf.writer().print("{d}", .{value}), 75 | }; 76 | } 77 | 78 | pub fn asInteger(self: Value) i64 { 79 | return switch (self) { 80 | .null_value => 0, 81 | .bool_value => |value| if (value) 1 else 0, 82 | .string_value => |value| fromIntegerString(value).integer_value, 83 | .integer_value => |value| value, 84 | }; 85 | } 86 | 87 | pub fn serialize(self: Value, buf: *std.ArrayList(u8)) String { 88 | switch (self) { 89 | .null_value => buf.append('0') catch return "", 90 | 91 | .bool_value => |value| { 92 | buf.append('1') catch return ""; 93 | buf.append(if (value) '1' else '0') catch return ""; 94 | }, 95 | 96 | .string_value => |value| { 97 | buf.append('2') catch return ""; 98 | buf.appendSlice(value) catch return ""; 99 | }, 100 | 101 | .integer_value => |value| { 102 | buf.append('3') catch return ""; 103 | serializeInteger(i64, buf, value) catch return ""; 104 | }, 105 | } 106 | 107 | return buf.items; 108 | } 109 | 110 | pub fn deserialize(data: String) Value { 111 | return switch (data[0]) { 112 | '0' => Value.NULL, 113 | '1' => Value{ .bool_value = data[1] == '1' }, 114 | '2' => Value{ .string_value = data[1..] }, 115 | '3' => Value{ .integer_value = deserializeInteger(i64, data[1..]) }, 116 | else => unreachable, 117 | }; 118 | } 119 | }; 120 | 121 | pub const Row = struct { 122 | allocator: std.mem.Allocator, 123 | cells: std.ArrayList(String), 124 | fields: []String, 125 | 126 | pub fn init(allocator: std.mem.Allocator, fields: []String) Row { 127 | return Row{ 128 | .allocator = allocator, 129 | .cells = std.ArrayList(String).init(allocator), 130 | .fields = fields, 131 | }; 132 | } 133 | 134 | pub fn append(self: *Row, cell: Value) !void { 135 | var cellBuffer = std.ArrayList(u8).init(self.allocator); 136 | try self.cells.append(cell.serialize(&cellBuffer)); 137 | } 138 | 139 | pub fn appendBytes(self: *Row, cell: String) !void { 140 | try self.cells.append(cell); 141 | } 142 | 143 | pub fn get(self: Row, field: String) Value { 144 | for (self.fields) |f, i| { 145 | if (std.mem.eql(u8, field, f)) { 146 | // Results are internal buffer views. So make a copy. 147 | var copy = std.ArrayList(u8).init(self.allocator); 148 | copy.appendSlice(self.cells.items[i]) catch return Storage.Value.NULL; 149 | return Storage.Value.deserialize(copy.items); 150 | } 151 | } 152 | 153 | return Value.NULL; 154 | } 155 | 156 | pub fn items(self: Row) []String { 157 | return self.cells.items; 158 | } 159 | 160 | fn reset(self: *Row) void { 161 | self.cells.clearRetainingCapacity(); 162 | } 163 | }; 164 | 165 | fn generateId() ![]u8 { 166 | const file = try std.fs.cwd().openFileZ("/dev/random", .{}); 167 | defer file.close(); 168 | 169 | var buf: [16]u8 = .{}; 170 | _ = try file.read(&buf); 171 | return buf[0..]; 172 | } 173 | 174 | pub fn writeRow(self: Storage, table: String, row: Row) ?Error { 175 | // Table name prefix 176 | var key = std.ArrayList(u8).init(self.allocator); 177 | key.writer().print("row_{s}_", .{table}) catch return "Could not allocate row key"; 178 | 179 | // Unique row id 180 | var id = generateId() catch return "Could not generate id"; 181 | key.appendSlice(id) catch return "Could not allocate for id"; 182 | 183 | var value = std.ArrayList(u8).init(self.allocator); 184 | for (row.cells.items) |cell| { 185 | serializeBytes(&value, cell) catch return "Could not allocate for cell"; 186 | } 187 | 188 | return self.db.set(key.items, value.items); 189 | } 190 | 191 | pub const RowIter = struct { 192 | row: Row, 193 | iter: RocksDB.Iter, 194 | 195 | fn init(allocator: std.mem.Allocator, iter: RocksDB.Iter, fields: []String) RowIter { 196 | return RowIter{ 197 | .iter = iter, 198 | .row = Row.init(allocator, fields), 199 | }; 200 | } 201 | 202 | pub fn next(self: *RowIter) ?Row { 203 | var rowBytes: String = undefined; 204 | if (self.iter.next()) |b| { 205 | rowBytes = b.value; 206 | } else { 207 | return null; 208 | } 209 | 210 | self.row.reset(); 211 | var offset: usize = 0; 212 | while (offset < rowBytes.len) { 213 | var d = deserializeBytes(rowBytes[offset..]); 214 | offset += d.offset; 215 | self.row.appendBytes(d.bytes) catch return null; 216 | } 217 | 218 | return self.row; 219 | } 220 | 221 | pub fn close(self: RowIter) void { 222 | self.iter.close(); 223 | } 224 | }; 225 | 226 | pub fn getRowIter(self: Storage, table: String) Result(RowIter) { 227 | var rowPrefix = std.ArrayList(u8).init(self.allocator); 228 | rowPrefix.writer().print("row_{s}_", .{table}) catch return .{ 229 | .err = "Could not allocate for row prefix", 230 | }; 231 | 232 | var iter = switch (self.db.iter(rowPrefix.items)) { 233 | .err => |err| return .{ .err = err }, 234 | .val => |it| it, 235 | }; 236 | 237 | var tableInfo = switch (self.getTable(table)) { 238 | .err => |err| return .{ .err = err }, 239 | .val => |t| t, 240 | }; 241 | 242 | return .{ 243 | .val = RowIter.init(self.allocator, iter, tableInfo.columns), 244 | }; 245 | } 246 | 247 | pub const Table = struct { 248 | name: String, 249 | columns: []String, 250 | types: []String, 251 | }; 252 | 253 | pub fn writeTable(self: Storage, table: Table) ?Error { 254 | // Table name prefix 255 | var key = std.ArrayList(u8).init(self.allocator); 256 | key.writer().print("tbl_{s}_", .{table.name}) catch return "Could not allocate key for table"; 257 | 258 | var value = std.ArrayList(u8).init(self.allocator); 259 | for (table.columns) |column, i| { 260 | serializeBytes(&value, column) catch return "Could not allocate for column"; 261 | serializeBytes(&value, table.types[i]) catch return "Could not allocate for column type"; 262 | } 263 | 264 | return self.db.set(key.items, value.items); 265 | } 266 | 267 | pub fn getTable(self: Storage, name: String) Result(Table) { 268 | var tableKey = std.ArrayList(u8).init(self.allocator); 269 | tableKey.writer().print("tbl_{s}_", .{name}) catch return .{ 270 | .err = "Could not allocate for table prefix", 271 | }; 272 | 273 | var columns = std.ArrayList(String).init(self.allocator); 274 | var types = std.ArrayList(String).init(self.allocator); 275 | var table = Table{ 276 | .name = name, 277 | .columns = undefined, 278 | .types = undefined, 279 | }; 280 | // First grab table info 281 | var columnInfo = switch (self.db.get(tableKey.items)) { 282 | .err => |err| return .{ .err = err }, 283 | .val => |val| val, 284 | .not_found => return .{ .err = "No such table" }, 285 | }; 286 | 287 | var columnOffset: usize = 0; 288 | while (columnOffset < columnInfo.len) { 289 | var column = deserializeBytes(columnInfo[columnOffset..]); 290 | columnOffset += column.offset; 291 | columns.append(column.bytes) catch return .{ 292 | .err = "Could not allocate for column name.", 293 | }; 294 | 295 | var kind = deserializeBytes(columnInfo[columnOffset..]); 296 | columnOffset += kind.offset; 297 | types.append(kind.bytes) catch return .{ 298 | .err = "Could not allocate for column kind.", 299 | }; 300 | } 301 | 302 | table.columns = columns.items; 303 | table.types = types.items; 304 | 305 | return .{ .val = table }; 306 | } 307 | }; 308 | 309 | test "serialize/deserialize Value strings" { 310 | const expectEqualStrings = std.testing.expectEqualStrings; 311 | const Value = Storage.Value; 312 | 313 | var stringTests = [_]struct { 314 | value: Value, 315 | string: String, 316 | }{ 317 | .{ .value = Value.fromIntegerString("1"), .string = "1" }, 318 | .{ .value = Value{ .integer_value = 1 }, .string = "1" }, 319 | .{ .value = Value{ .integer_value = 1003 }, .string = "1003" }, 320 | .{ .value = Value{ .bool_value = false }, .string = "false" }, 321 | .{ .value = Value{ .bool_value = true }, .string = "true" }, 322 | }; 323 | 324 | var buf = std.ArrayList(u8).init(std.testing.allocator); 325 | defer buf.deinit(); 326 | 327 | var buf2 = std.ArrayList(u8).init(std.testing.allocator); 328 | defer buf2.deinit(); 329 | 330 | for (stringTests) |testCase| { 331 | buf.clearRetainingCapacity(); 332 | var serialized = testCase.value.serialize(&buf); 333 | 334 | buf2.clearRetainingCapacity(); 335 | try Value.deserialize(serialized).asString(&buf2); 336 | try expectEqualStrings(testCase.string, buf2.items); 337 | } 338 | } 339 | 340 | test "serialize/deserialize Value integers" { 341 | const expectEqual = std.testing.expectEqual; 342 | const Value = Storage.Value; 343 | 344 | var integerTests = [_]struct { 345 | value: Value, 346 | integer: i64, 347 | }{ 348 | .{ .value = Value.fromIntegerString("1"), .integer = 1 }, 349 | .{ .value = Value{ .integer_value = 1 }, .integer = 1 }, 350 | .{ .value = Value.FALSE, .integer = 0 }, 351 | .{ .value = Value.TRUE, .integer = 1 }, 352 | .{ .value = Value{ .string_value = "1" }, .integer = 1 }, 353 | .{ .value = Value{ .string_value = "1002" }, .integer = 1002 }, 354 | }; 355 | 356 | var buf = std.ArrayList(u8).init(std.testing.allocator); 357 | defer buf.deinit(); 358 | for (integerTests) |testCase| { 359 | buf.clearRetainingCapacity(); 360 | var serialized = testCase.value.serialize(&buf); 361 | try expectEqual(testCase.integer, Value.deserialize(serialized).asInteger()); 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /tests/create.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE x( 2 | name TEXT, 3 | age INT 4 | ) 5 | -------------------------------------------------------------------------------- /tests/insert.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO x VALUES (6, 19) 2 | -------------------------------------------------------------------------------- /tests/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ev 4 | 5 | ./main --database data --script <(echo "CREATE TABLE y (year int, age int, name text)") 6 | ./main --database data --script <(echo "INSERT INTO y VALUES (2010, 38, 'Gary')") 7 | ./main --database data --script <(echo "INSERT INTO y VALUES (2021, 92, 'Teej')") 8 | ./main --database data --script <(echo "INSERT INTO y VALUES (1994, 18, 'Mel')") 9 | 10 | # Basic query 11 | ./main --database data --script <(echo "SELECT name, age, year FROM y") 12 | 13 | # With WHERE 14 | ./main --database data --script <(echo "SELECT name, year, age FROM y WHERE age < 40") 15 | 16 | # With operations 17 | ./main --database data --script <(echo "SELECT 'Name: ' || name, year + 30, age FROM y WHERE age < 40") 18 | -------------------------------------------------------------------------------- /tests/select.sql: -------------------------------------------------------------------------------- 1 | select name, age from x 2 | -------------------------------------------------------------------------------- /tests/where.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | a,b 3 | FROM 4 | main 5 | WHERE x = 1 6 | -------------------------------------------------------------------------------- /types.zig: -------------------------------------------------------------------------------- 1 | pub const String = []const u8; 2 | 3 | pub const Error = String; 4 | 5 | pub fn Result(comptime T: type) type { 6 | return union(enum) { 7 | val: T, 8 | err: Error, 9 | }; 10 | } 11 | --------------------------------------------------------------------------------