const std = @import("std"); const vm = @import("vecmath"); pub const ColumnType = @import("ColumnType.zig").ColumnType; pub const TaggedValue = @import("TaggedValue.zig").TaggedValue; pub const page_size = 4096; pub const page_align = std.mem.Alignment.fromByteUnits(page_size); fn IdMixin(comptime Enum: type) type { const Tag = switch (@typeInfo(Enum)) { .@"enum" => |e| e.tag_type, else => @compileError("Expected " ++ @typeName(Enum) ++ " to be an enum."), }; return struct { pub inline fn fromInt(tag: Tag) Enum { return @enumFromInt(tag); } pub inline fn toInt(self: Enum) Tag { return @intFromEnum(self); } pub inline fn next(self: Enum) error{OutOfIds}!Enum { const tag = @intFromEnum(self); const next_tag = std.math.add(Tag, tag, 1) catch return error.OutOfIds; return @enumFromInt(next_tag); } }; } fn FixedString(comptime Len: type, comptime total_size: usize) type { return extern struct { len: Len, str: [max_str_len]u8, const max_str_len = total_size - @sizeOf(Len); pub fn slice(self: *@This()) []const u8 { return self.str[0..self.len]; } pub fn copy(self: *@This(), src: []const u8) void { std.debug.assert(src.len <= max_str_len); self.len = @intCast(src.len); @memcpy(self.str[0..src.len], src); @memset(self.str[src.len..], 0); } pub fn tryCopy(self: *@This(), src: []const u8) error{RangeError}!void { if (src.len > max_str_len) { return error.RangeError; } self.copy(src); } }; } pub const ColumnId = enum(u16) { _, pub const fromInt = IdMixin(ColumnId).fromInt; pub const toInt = IdMixin(ColumnId).toInt; pub const next = IdMixin(ColumnId).next; }; pub const TableId = enum(u16) { _, pub const fromInt = IdMixin(TableId).fromInt; pub const toInt = IdMixin(TableId).toInt; pub const next = IdMixin(TableId).next; }; pub const RowId = enum(u64) { _, pub const fromInt = IdMixin(RowId).fromInt; pub const toInt = IdMixin(RowId).toInt; pub const next = IdMixin(RowId).next; }; pub const TableDefinition = struct { id: TableId, column_definitions: []const ColumnDefinition, }; pub const ColumnDefinition = struct { id: ColumnId, type: ColumnType, }; pub const CellValue = struct { id: ColumnId, value: TaggedValue, }; pub const Database = struct { allocator: std.mem.Allocator, io: std.Io, tables: std.AutoHashMapUnmanaged(TableId, Table), pub fn init(allocator: std.mem.Allocator, io: std.Io) !Database { return .{ .allocator = allocator, .io = io, .tables = .empty, }; } pub fn deinit(self: *Database) void { var it = self.tables.valueIterator(); while (it.next()) |table| { table.deinit(self.allocator); } self.tables.deinit(self.allocator); self.* = undefined; } pub fn createTable(self: *Database, table_definition: TableDefinition) !void { const get_or_put_result = try self.tables.getOrPut(self.allocator, table_definition.id); if (get_or_put_result.found_existing) { return error.DuplicateTableId; } errdefer _ = self.tables.remove(table_definition.id); get_or_put_result.value_ptr.* = try .init(table_definition, self.allocator); } pub fn insertRow(self: *Database, table_id: TableId, row: []const CellValue) !RowId { const table = self.tables.getPtr(table_id) orelse return error.TableNotFound; const row_id = table.next_row_id; var it = table.columns.valueIterator(); while (it.next()) |column| { var corresponding_cell_value: ?*const CellValue = null; for (row) |*cell_value| { if (column.id == cell_value.id) { if (corresponding_cell_value != null) { return error.DuplicateColumnId; } corresponding_cell_value = cell_value; } } if (corresponding_cell_value) |cell_value| { try column.set(row_id, cell_value.value, self.allocator); } else { return error.MissingColumnId; } } table.next_row_id = try row_id.next(); return row_id; } pub fn getTaggedValue(self: *Database, table_id: TableId, column_id: ColumnId, row_id: RowId) !TaggedValue { const table = self.tables.getPtr(table_id) orelse return error.TableNotFound; const column = table.columns.getPtr(column_id) orelse return error.ColumnNotFound; if (row_id.toInt() >= table.next_row_id.toInt()) return error.RowIdNotFound; return column.getTagged(row_id); } pub fn getValue(self: *Database, comptime T: type, table_id: TableId, column_id: ColumnId, row_id: RowId) !TaggedValue { const table = self.tables.getPtr(table_id) orelse return error.TableNotFound; const column = table.columns.getPtr(column_id) orelse return error.ColumnNotFound; if (row_id.toInt() >= table.next_row_id.toInt()) return error.RowIdNotFound; return column.get(T, row_id); } }; pub const Table = struct { id: TableId, columns: std.AutoHashMapUnmanaged(ColumnId, Column), next_row_id: RowId, pub fn init(table_definition: TableDefinition, allocator: std.mem.Allocator) !Table { const column_count = std.math.cast(u32, table_definition.column_definitions.len) orelse return error.OutOfMemory; var columns: std.AutoHashMapUnmanaged(ColumnId, Column) = .empty; try columns.ensureTotalCapacity(allocator, column_count); errdefer { var it = columns.valueIterator(); while (it.next()) |column| { column.deinit(allocator); } columns.deinit(allocator); } for (table_definition.column_definitions) |column_definition| { const get_or_put_result = columns.getOrPutAssumeCapacity(column_definition.id); if (get_or_put_result.found_existing) { return error.DuplicateColumnId; } errdefer _ = columns.remove(column_definition.id); get_or_put_result.value_ptr.* = .init(column_definition); } return .{ .id = table_definition.id, .columns = columns, .next_row_id = .fromInt(0), }; } pub fn deinit(self: *Table, allocator: std.mem.Allocator) void { var it = self.columns.valueIterator(); while (it.next()) |column| { column.deinit(allocator); } self.columns.deinit(allocator); self.* = undefined; } }; pub const Column = struct { id: ColumnId, type: ColumnType, pages: std.ArrayList(Page), pub fn init(column_definition: ColumnDefinition) Column { return .{ .id = column_definition.id, .type = column_definition.type, .pages = .empty, }; } pub fn deinit(self: *Column, allocator: std.mem.Allocator) void { for (self.pages.items) |*page| { page.deinit(); } self.pages.deinit(allocator); self.* = undefined; } pub fn getTagged(self: *Column, row_id: RowId) TaggedValue { const values_per_page = self.type.valuesPerPage(page_size); const page_index = @as(usize, row_id.toInt() / values_per_page); const value_index = @as(usize, row_id.toInt() % values_per_page); const page = &self.pages.items[page_index]; return switch (self.type) { .bool => .{ .bool = page.get(bool, value_index) }, .bit => .{ .bool = page.getBit(value_index) }, .u8 => .{ .u8 = page.get(u8, value_index) }, .u16 => .{ .u16 = page.get(u16, value_index) }, .u32 => .{ .u32 = page.get(u32, value_index) }, .u64 => .{ .u64 = page.get(u64, value_index) }, .i8 => .{ .i8 = page.get(i8, value_index) }, .i16 => .{ .i16 = page.get(i16, value_index) }, .i32 => .{ .i32 = page.get(i32, value_index) }, .i64 => .{ .i64 = page.get(i64, value_index) }, .f32 => .{ .f32 = page.get(f32, value_index) }, .f64 => .{ .f64 = page.get(f64, value_index) }, .color => .{ .color = page.get(vm.Color, value_index) }, .color_hdr => .{ .color_hdr = page.get(vm.ColorHdr, value_index) }, .mat3x2 => .{ .mat3x2 = page.getClustered(vm.Matrix3x2, 8, value_index) }, .mat4x4 => .{ .mat4x4 = page.getClustered(vm.Matrix4x4, 8, value_index) }, .complex => .{ .complex = page.getClustered(vm.Complex, 8, value_index) }, .quaternion => .{ .quaternion = page.getClustered(vm.Quaternion, 8, value_index) }, .vec2 => .{ .vec2 = page.getClustered(vm.Vector2, 8, value_index) }, .vec2i => .{ .vec2i = page.getClustered(vm.Vector2Int, 8, value_index) }, .vec3 => .{ .vec3 = page.getClustered(vm.Vector3, 8, value_index) }, .vec3i => .{ .vec3i = page.getClustered(vm.Vector3Int, 8, value_index) }, .vec4 => .{ .vec4 = page.getClustered(vm.Vector4, 8, value_index) }, .vec4i => .{ .vec4i = page.getClustered(vm.Vector4Int, 8, value_index) }, .str8 => .{ .str = page.getPtr(FixedString(u8, 8), value_index).slice() }, .str16 => .{ .str = page.getPtr(FixedString(u8, 16), value_index).slice() }, .str32 => .{ .str = page.getPtr(FixedString(u8, 32), value_index).slice() }, .str64 => .{ .str = page.getPtr(FixedString(u8, 64), value_index).slice() }, .str128 => .{ .str = page.getPtr(FixedString(u8, 128), value_index).slice() }, .str256 => .{ .str = page.getPtr(FixedString(u8, 256), value_index).slice() }, .str512 => .{ .str = page.getPtr(FixedString(u16, 512), value_index).slice() }, .str1k => .{ .str = page.getPtr(FixedString(u16, 1024), value_index).slice() }, .str2k => .{ .str = page.getPtr(FixedString(u16, 2048), value_index).slice() }, .str4k => .{ .str = page.getPtr(FixedString(u16, 4096), value_index).slice() }, }; } pub fn get(self: *Column, comptime T: type, row_id: RowId) !T { const values_per_page = self.type.valuesPerPage(page_size); const page_index = @as(usize, row_id.toInt() / values_per_page); const value_index = @as(usize, row_id.toInt() % values_per_page); const page = &self.pages.items[page_index]; return switch (T) { bool => switch (self.type) { .bool => page.get(bool, value_index), .bit => page.getBit(value_index), else => error.TypeError, }, u8 => switch (self.type) { .u8 => page.get(u8, value_index), else => error.TypeError, }, u16 => switch (self.type) { .u16 => page.get(u16, value_index), else => error.TypeError, }, u32 => switch (self.type) { .u32 => page.get(u32, value_index), else => error.TypeError, }, u64 => switch (self.type) { .u64 => page.get(u64, value_index), else => error.TypeError, }, i8 => switch (self.type) { .i8 => page.get(i8, value_index), else => error.TypeError, }, i16 => switch (self.type) { .i16 => page.get(i16, value_index), else => error.TypeError, }, i32 => switch (self.type) { .i32 => page.get(i32, value_index), else => error.TypeError, }, i64 => switch (self.type) { .i64 => page.get(i64, value_index), else => error.TypeError, }, f32 => switch (self.type) { .f32 => page.get(f32, value_index), else => error.TypeError, }, f64 => switch (self.type) { .f64 => page.get(f64, value_index), else => error.TypeError, }, vm.Color => switch (self.type) { .color => page.get(vm.Color, value_index), else => error.TypeError, }, vm.ColorHdr => switch (self.type) { .color_hdr => page.get(vm.ColorHdr, value_index), else => error.TypeError, }, vm.Matrix3x2 => switch (self.type) { .mat3x2 => page.getClustered(vm.Matrix3x2, 8, value_index), else => error.TypeError, }, vm.Matrix4x4 => switch (self.type) { .mat4x4 => page.getClustered(vm.Matrix4x4, 8, value_index), else => error.TypeError, }, vm.Complex => switch (self.type) { .complex => page.getClustered(vm.Complex, 8, value_index), else => error.TypeError, }, vm.Quaternion => switch (self.type) { .quaternion => page.getClustered(vm.Quaternion, 8, value_index), else => error.TypeError, }, vm.Vector2 => switch (self.type) { .vec2 => page.getClustered(vm.Vector2, 8, value_index), else => error.TypeError, }, vm.Vector2Int => switch (self.type) { .vec2i => page.getClustered(vm.Vector2Int, 8, value_index), else => error.TypeError, }, vm.Vector3 => switch (self.type) { .vec3 => page.getClustered(vm.Vector3, 8, value_index), else => error.TypeError, }, vm.Vector3Int => switch (self.type) { .vec3i => page.getClustered(vm.Vector3Int, 8, value_index), else => error.TypeError, }, vm.Vector4 => switch (self.type) { .vec4 => page.getClustered(vm.Vector4, 8, value_index), else => error.TypeError, }, vm.Vector4Int => switch (self.type) { .vec4i => page.getClustered(vm.Vector4Int, 8, value_index), else => error.TypeError, }, []const u8 => switch (self.type) { .str8 => page.getPtr(FixedString(u8, 8), value_index).slice(), .str16 => page.getPtr(FixedString(u8, 16), value_index).slice(), .str32 => page.getPtr(FixedString(u8, 32), value_index).slice(), .str64 => page.getPtr(FixedString(u8, 64), value_index).slice(), .str128 => page.getPtr(FixedString(u8, 128), value_index).slice(), .str256 => page.getPtr(FixedString(u8, 256), value_index).slice(), .str512 => page.getPtr(FixedString(u16, 512), value_index).slice(), .str1k => page.getPtr(FixedString(u16, 1024), value_index).slice(), .str2k => page.getPtr(FixedString(u16, 2048), value_index).slice(), .str4k => page.getPtr(FixedString(u16, 4096), value_index).slice(), else => error.TypeError, }, else => @compileError("Type " ++ @typeName(T) ++ " not supported."), }; } pub fn set(self: *Column, row_id: RowId, value: TaggedValue, allocator: std.mem.Allocator) !void { const values_per_page = self.type.valuesPerPage(page_size); const page_index = @as(usize, row_id.toInt() / values_per_page); const value_index = @as(usize, row_id.toInt() % values_per_page); try self.pages.ensureTotalCapacity(allocator, page_index + 1); while (page_index >= self.pages.items.len) { self.pages.appendAssumeCapacity(try .init()); } const page = &self.pages.items[page_index]; return switch (value) { .bool => |x| switch (self.type) { .bool => page.set(bool, value_index, x), .bit => page.setBit(value_index, x), else => error.TypeError, }, .u8 => |x| switch (self.type) { .u8 => page.set(u8, value_index, x), else => error.TypeError, }, .u16 => |x| switch (self.type) { .u16 => page.set(u16, value_index, x), else => error.TypeError, }, .u32 => |x| switch (self.type) { .u32 => page.set(u32, value_index, x), else => error.TypeError, }, .u64 => |x| switch (self.type) { .u64 => page.set(u64, value_index, x), else => error.TypeError, }, .i8 => |x| switch (self.type) { .i8 => page.set(i8, value_index, x), else => error.TypeError, }, .i16 => |x| switch (self.type) { .i16 => page.set(i16, value_index, x), else => error.TypeError, }, .i32 => |x| switch (self.type) { .i32 => page.set(i32, value_index, x), else => error.TypeError, }, .i64 => |x| switch (self.type) { .i64 => page.set(i64, value_index, x), else => error.TypeError, }, .f32 => |x| switch (self.type) { .f32 => page.set(f32, value_index, x), else => error.TypeError, }, .f64 => |x| switch (self.type) { .f64 => page.set(f64, value_index, x), else => error.TypeError, }, .color => |x| switch (self.type) { .color => page.set(vm.Color, value_index, x), else => error.TypeError, }, .color_hdr => |x| switch (self.type) { .color_hdr => page.set(vm.ColorHdr, value_index, x), else => error.TypeError, }, .mat3x2 => |x| switch (self.type) { .mat3x2 => page.setClustered(vm.Matrix3x2, 8, value_index, x), else => error.TypeError, }, .mat4x4 => |x| switch (self.type) { .mat4x4 => page.setClustered(vm.Matrix4x4, 8, value_index, x), else => error.TypeError, }, .complex => |x| switch (self.type) { .complex => page.set(vm.Complex, value_index, x), else => error.TypeError, }, .quaternion => |x| switch (self.type) { .quaternion => page.set(vm.Quaternion, value_index, x), else => error.TypeError, }, .vec2 => |x| switch (self.type) { .vec2 => page.set(vm.Vector2, value_index, x), else => error.TypeError, }, .vec2i => |x| switch (self.type) { .vec2i => page.set(vm.Vector2Int, value_index, x), else => error.TypeError, }, .vec3 => |x| switch (self.type) { .vec3 => page.set(vm.Vector3, value_index, x), else => error.TypeError, }, .vec3i => |x| switch (self.type) { .vec3i => page.set(vm.Vector3Int, value_index, x), else => error.TypeError, }, .vec4 => |x| switch (self.type) { .vec4 => page.set(vm.Vector4, value_index, x), else => error.TypeError, }, .vec4i => |x| switch (self.type) { .vec4i => page.set(vm.Vector4Int, value_index, x), else => error.TypeError, }, .str => |x| switch (self.type) { .str8 => page.getPtr(FixedString(u8, 8), value_index).tryCopy(x), .str16 => page.getPtr(FixedString(u8, 16), value_index).tryCopy(x), .str32 => page.getPtr(FixedString(u8, 32), value_index).tryCopy(x), .str64 => page.getPtr(FixedString(u8, 64), value_index).tryCopy(x), .str128 => page.getPtr(FixedString(u8, 128), value_index).tryCopy(x), .str256 => page.getPtr(FixedString(u8, 256), value_index).tryCopy(x), .str512 => page.getPtr(FixedString(u16, 512), value_index).tryCopy(x), .str1k => page.getPtr(FixedString(u16, 1024), value_index).tryCopy(x), .str2k => page.getPtr(FixedString(u16, 2048), value_index).tryCopy(x), .str4k => page.getPtr(FixedString(u16, 4096), value_index).tryCopy(x), else => error.TypeError, }, }; } }; pub const Page = struct { ptr: *align(page_align.toByteUnits()) [page_size]u8, pub fn init() !Page { const bytes = try std.heap.page_allocator.alignedAlloc(u8, page_align, page_size); return .{ .ptr = bytes[0..page_size], }; } pub fn deinit(self: *Page) void { const bytes = @as([]u8, std.mem.asBytes(self.ptr)); std.heap.page_allocator.free(bytes); self.* = undefined; } pub fn get(self: Page, comptime T: type, index: usize) T { const byte_offset = index * @sizeOf(T); std.debug.assert(byte_offset + @sizeOf(T) <= page_size); return @as([*]T, @ptrCast(self.ptr))[index]; } pub fn getPtr(self: Page, comptime T: type, index: usize) *T { const byte_offset = index * @sizeOf(T); std.debug.assert(byte_offset + @sizeOf(T) <= page_size); return &@as([*]T, @ptrCast(self.ptr))[index]; } pub fn getBit(self: Page, bit_index: usize) bool { const byte_index = bit_index / 8; const bit_shift = @as(u3, @intCast(bit_index % 8)); const mask = @as(u8, 1) << bit_shift; return self.ptr[byte_index] & mask != 0; } pub fn getClustered(self: Page, comptime T: type, comptime values_per_cluster: usize, index: usize) T { const struct_info = switch (@typeInfo(T)) { .@"struct" => |s| s, else => @compileError("Expected " ++ @typeName(T) ++ " to be an extern struct."), }; if (struct_info.layout != .@"extern") { @compileError("Expected " ++ @typeName(T) ++ " to be an extern struct."); } const cluster_index = index / values_per_cluster; const value_index = index % values_per_cluster; const cluster_base_ptr = @as([*]u8, self.ptr) + cluster_index * @sizeOf(T) * values_per_cluster; var ret: T = undefined; inline for (struct_info.fields) |field| { if (field.is_comptime) { @compileError("Unexpected comptime field " ++ @typeName(T) ++ "." ++ field.name ++ "."); } const cluster_field_offset = @offsetOf(T, field.name) * values_per_cluster; const cluster_field_ptr = @as(*[values_per_cluster]field.type, @ptrCast(@alignCast(cluster_base_ptr + cluster_field_offset))); @field(ret, field.name) = cluster_field_ptr[value_index]; } return ret; } pub fn set(self: Page, comptime T: type, index: usize, value: T) void { const byte_offset = index * @sizeOf(T); std.debug.assert(byte_offset + @sizeOf(T) <= page_size); @as([*]T, @ptrCast(self.ptr))[index] = value; } pub fn setBit(self: Page, bit_index: usize, value: bool) void { const byte_index = bit_index / 8; const bit_shift = @as(u3, @intCast(bit_index % 8)); if (value) { const mask = @as(u8, 1) << bit_shift; self.ptr[byte_index] |= mask; } else { const mask = ~(@as(u8, 1) << bit_shift); self.ptr[byte_index] &= mask; } } pub fn setClustered(self: Page, comptime T: type, comptime values_per_cluster: usize, index: usize, value: T) void { const struct_info = switch (@typeInfo(T)) { .@"struct" => |s| s, else => @compileError("Expected " ++ @typeName(T) ++ " to be an extern struct."), }; if (struct_info.layout != .@"extern") { @compileError("Expected " ++ @typeName(T) ++ " to be an extern struct."); } const cluster_index = index / values_per_cluster; const value_index = index % values_per_cluster; const cluster_base_ptr = @as([*]u8, self.ptr) + cluster_index * @sizeOf(T) * values_per_cluster; inline for (struct_info.fields) |field| { if (field.is_comptime) { @compileError("Unexpected comptime field " ++ @typeName(T) ++ "." ++ field.name ++ "."); } const cluster_field_offset = @offsetOf(T, field.name) * values_per_cluster; const cluster_field_ptr = @as(*[values_per_cluster]field.type, @ptrCast(@alignCast(cluster_base_ptr + cluster_field_offset))); cluster_field_ptr[value_index] = @field(value, field.name); } } }; test { var db: Database = try .init(std.testing.allocator, std.testing.io); defer db.deinit(); const table_ids = struct { const users = TableId.fromInt(0); }; const column_ids = struct { const username = ColumnId.fromInt(0); const password_hash = ColumnId.fromInt(1); const color = ColumnId.fromInt(2); }; try db.createTable(.{ .id = table_ids.users, .column_definitions = &.{ .{ .id = column_ids.username, .type = .str64, }, .{ .id = column_ids.password_hash, .type = .str128, }, .{ .id = column_ids.color, .type = .color, }, }, }); const admin_id = try db.insertRow(table_ids.users, &.{ .{ .id = column_ids.username, .value = .{ .str = "admin" }, }, .{ .id = column_ids.password_hash, .value = .{ .str = "$argon2id$v=19$m=16,t=2,p=1$R0l1WjlpM1BUSXZ3dU0xbQ$muWG51NiQjaIe6RTh6LJgk/7VJMvtbtJiN5Z11fEFbI" }, }, .{ .id = column_ids.color, .value = .{ .color = .red }, }, }); const user_id = try db.insertRow(table_ids.users, &.{ .{ .id = column_ids.username, .value = .{ .str = "user" }, }, .{ .id = column_ids.password_hash, .value = .{ .str = "$argon2id$v=19$m=16,t=2,p=1$RlNyZTR3dklRU1hVb3hHSA$8EDVcnkQouADrpvb5bONOqQWrUil1Jc/YaZGe3t34q0" }, }, .{ .id = column_ids.color, .value = .{ .color = .blue }, }, }); const admin_username = try db.getValue([]const u8, table_ids.users, column_ids.username, admin_id); const admin_color = try db.getValue(vm.Color, table_ids.users, column_ids.color, admin_id); const user_username = try db.getValue([]const u8, table_ids.users, column_ids.username, user_id); const user_color = try db.getValue(vm.Color, table_ids.users, column_ids.color, user_id); try std.testing.expectEqualStrings("admin", admin_username); try std.testing.expectEqual(vm.Color.red, admin_color); try std.testing.expectEqualStrings("user", user_username); try std.testing.expectEqual(vm.Color.blue, user_color); }