datastore: refactors, clustered (simd/aosoa) value support
This commit is contained in:
156
packages/datastore/src/ColumnType.zig
Normal file
156
packages/datastore/src/ColumnType.zig
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const vm = @import("vecmath");
|
||||||
|
|
||||||
|
pub const ColumnType = enum {
|
||||||
|
/// 1 byte, 0x00 or 0x01
|
||||||
|
bool,
|
||||||
|
/// 1 bit, bit-packed
|
||||||
|
bit,
|
||||||
|
u8,
|
||||||
|
u16,
|
||||||
|
u32,
|
||||||
|
u64,
|
||||||
|
i8,
|
||||||
|
i16,
|
||||||
|
i32,
|
||||||
|
i64,
|
||||||
|
f32,
|
||||||
|
f64,
|
||||||
|
/// RGBA u8
|
||||||
|
color,
|
||||||
|
/// RGBA f16
|
||||||
|
color_hdr,
|
||||||
|
/// Stored as ×8 AOSOA
|
||||||
|
mat3x2,
|
||||||
|
/// Stored as ×8 AOSOA
|
||||||
|
mat4x4,
|
||||||
|
/// Stored as ×8 AOSOA
|
||||||
|
complex,
|
||||||
|
/// Stored as ×8 AOSOA
|
||||||
|
quaternion,
|
||||||
|
/// Stored as ×8 AOSOA
|
||||||
|
vec2,
|
||||||
|
/// Stored as ×8 AOSOA
|
||||||
|
vec2i,
|
||||||
|
/// Stored as ×8 AOSOA
|
||||||
|
vec3,
|
||||||
|
/// Stored as ×8 AOSOA
|
||||||
|
vec3i,
|
||||||
|
/// Stored as ×8 AOSOA
|
||||||
|
vec4,
|
||||||
|
/// Stored as ×8 AOSOA
|
||||||
|
vec4i,
|
||||||
|
/// 8 B (1 B len ++ <= 7 B str)
|
||||||
|
str8,
|
||||||
|
/// 16 B (1 B len ++ <= 15 B str)
|
||||||
|
str16,
|
||||||
|
/// 32 B (1 B len ++ <= 31 B str)
|
||||||
|
str32,
|
||||||
|
/// 64 B (1 B len ++ <= 63 B str)
|
||||||
|
str64,
|
||||||
|
/// 128 B (1B len ++ <= 127 B str)
|
||||||
|
str128,
|
||||||
|
/// 256 B (1 B len ++ <= 255 B str)
|
||||||
|
str256,
|
||||||
|
/// 512 B (2 B len ++ <= 510 B str)
|
||||||
|
str512,
|
||||||
|
/// 1 kiB (2 B len ++ <= 1022 B str)
|
||||||
|
str1k,
|
||||||
|
/// 2 kiB (2 B len ++ <= 2046 B str)
|
||||||
|
str2k,
|
||||||
|
/// 4 kiB (2 B len ++ <= 4094 B str)
|
||||||
|
str4k,
|
||||||
|
|
||||||
|
pub fn valuesPerCluster(self: ColumnType) usize {
|
||||||
|
return switch (self) {
|
||||||
|
.bool => 1,
|
||||||
|
.bit => 8,
|
||||||
|
.u8 => 1,
|
||||||
|
.u16 => 1,
|
||||||
|
.u32 => 1,
|
||||||
|
.u64 => 1,
|
||||||
|
.i8 => 1,
|
||||||
|
.i16 => 1,
|
||||||
|
.i32 => 1,
|
||||||
|
.i64 => 1,
|
||||||
|
.f32 => 1,
|
||||||
|
.f64 => 1,
|
||||||
|
.color => 1,
|
||||||
|
.color_hdr => 1,
|
||||||
|
.mat3x2 => 8,
|
||||||
|
.mat4x4 => 8,
|
||||||
|
.complex => 8,
|
||||||
|
.quaternion => 8,
|
||||||
|
.vec2 => 8,
|
||||||
|
.vec2i => 8,
|
||||||
|
.vec3 => 8,
|
||||||
|
.vec3i => 8,
|
||||||
|
.vec4 => 8,
|
||||||
|
.vec4i => 8,
|
||||||
|
.str8 => 1,
|
||||||
|
.str16 => 1,
|
||||||
|
.str32 => 1,
|
||||||
|
.str64 => 1,
|
||||||
|
.str128 => 1,
|
||||||
|
.str256 => 1,
|
||||||
|
.str512 => 1,
|
||||||
|
.str1k => 1,
|
||||||
|
.str2k => 1,
|
||||||
|
.str4k => 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clusterByteSize(self: ColumnType) usize {
|
||||||
|
return switch (self) {
|
||||||
|
.bool => 1,
|
||||||
|
.bit => 1,
|
||||||
|
.u8 => 1,
|
||||||
|
.u16 => 2,
|
||||||
|
.u32 => 4,
|
||||||
|
.u64 => 8,
|
||||||
|
.i8 => 1,
|
||||||
|
.i16 => 2,
|
||||||
|
.i32 => 4,
|
||||||
|
.i64 => 8,
|
||||||
|
.f32 => 4,
|
||||||
|
.f64 => 8,
|
||||||
|
.color => 4,
|
||||||
|
.color_hdr => 8,
|
||||||
|
.mat3x2 => 24 * 8,
|
||||||
|
.mat4x4 => 64 * 8,
|
||||||
|
.complex => 8 * 8,
|
||||||
|
.quaternion => 16 * 8,
|
||||||
|
.vec2 => 8 * 8,
|
||||||
|
.vec2i => 8 * 8,
|
||||||
|
.vec3 => 12 * 8,
|
||||||
|
.vec3i => 12 * 8,
|
||||||
|
.vec4 => 16 * 8,
|
||||||
|
.vec4i => 16 * 8,
|
||||||
|
.str8 => 8,
|
||||||
|
.str16 => 16,
|
||||||
|
.str32 => 32,
|
||||||
|
.str64 => 64,
|
||||||
|
.str128 => 128,
|
||||||
|
.str256 => 256,
|
||||||
|
.str512 => 512,
|
||||||
|
.str1k => 1024,
|
||||||
|
.str2k => 2048,
|
||||||
|
.str4k => 4096,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn valuesPerPage(self: ColumnType, page_size: usize) usize {
|
||||||
|
const clusters_per_page = page_size / self.clusterByteSize();
|
||||||
|
return self.valuesPerCluster() * clusters_per_page;
|
||||||
|
}
|
||||||
|
|
||||||
|
test valuesPerPage {
|
||||||
|
const page_size = 4096;
|
||||||
|
|
||||||
|
try std.testing.expectEqual(page_size, valuesPerPage(.bool, page_size));
|
||||||
|
try std.testing.expectEqual(8 * page_size, valuesPerPage(.bit, page_size));
|
||||||
|
try std.testing.expectEqual(page_size / 4, valuesPerPage(.u32, page_size));
|
||||||
|
try std.testing.expectEqual(page_size / 8, valuesPerPage(.u64, page_size));
|
||||||
|
try std.testing.expectEqual(page_size / @sizeOf(vm.Vector3x8) * 8, valuesPerPage(.vec3, page_size));
|
||||||
|
}
|
||||||
|
};
|
||||||
32
packages/datastore/src/TaggedValue.zig
Normal file
32
packages/datastore/src/TaggedValue.zig
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const vm = @import("vecmath");
|
||||||
|
|
||||||
|
pub const TaggedValue = union(enum) {
|
||||||
|
/// Corresponds to `ColumnType.boolean` and `ColumnType.bit`.
|
||||||
|
bool: bool,
|
||||||
|
u8: u8,
|
||||||
|
u16: u16,
|
||||||
|
u32: u32,
|
||||||
|
u64: u64,
|
||||||
|
i8: i8,
|
||||||
|
i16: i16,
|
||||||
|
i32: i32,
|
||||||
|
i64: i64,
|
||||||
|
f32: f32,
|
||||||
|
f64: f64,
|
||||||
|
color: vm.Color,
|
||||||
|
color_hdr: vm.ColorHdr,
|
||||||
|
mat3x2: vm.Matrix3x2,
|
||||||
|
mat4x4: vm.Matrix4x4,
|
||||||
|
complex: vm.Complex,
|
||||||
|
quaternion: vm.Quaternion,
|
||||||
|
vec2: vm.Vector2,
|
||||||
|
vec2i: vm.Vector2Int,
|
||||||
|
vec3: vm.Vector3,
|
||||||
|
vec3i: vm.Vector3Int,
|
||||||
|
vec4: vm.Vector4,
|
||||||
|
vec4i: vm.Vector4Int,
|
||||||
|
/// Corresponds to all string types in `ColumnType`, from `ColumnType.str8`
|
||||||
|
/// to `ColumnType.str4k`.
|
||||||
|
str: []const u8,
|
||||||
|
};
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const vm = @import("vecmath");
|
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_size = 4096;
|
||||||
pub const page_align = std.mem.Alignment.fromByteUnits(page_size);
|
pub const page_align = std.mem.Alignment.fromByteUnits(page_size);
|
||||||
|
|
||||||
@@ -27,6 +30,35 @@ fn IdMixin(comptime Enum: type) type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 ColumnId = enum(u16) {
|
||||||
_,
|
_,
|
||||||
|
|
||||||
@@ -51,205 +83,6 @@ pub const RowId = enum(u64) {
|
|||||||
pub const next = IdMixin(RowId).next;
|
pub const next = IdMixin(RowId).next;
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ColumnType = enum {
|
|
||||||
/// 1 byte, 0x00 or 0x01
|
|
||||||
boolean,
|
|
||||||
/// 1 bit, bit-packed
|
|
||||||
bit,
|
|
||||||
u8,
|
|
||||||
u16,
|
|
||||||
u32,
|
|
||||||
u64,
|
|
||||||
i8,
|
|
||||||
i16,
|
|
||||||
i32,
|
|
||||||
i64,
|
|
||||||
f32,
|
|
||||||
f64,
|
|
||||||
/// RGBA u8
|
|
||||||
color,
|
|
||||||
/// RGBA f16
|
|
||||||
color_hdr,
|
|
||||||
mat3x2,
|
|
||||||
mat4x4,
|
|
||||||
complex,
|
|
||||||
quaternion,
|
|
||||||
vec2,
|
|
||||||
vec2i,
|
|
||||||
vec3,
|
|
||||||
vec3i,
|
|
||||||
vec4,
|
|
||||||
vec4i,
|
|
||||||
/// 16 B (1 B len ++ 15 B str)
|
|
||||||
str16,
|
|
||||||
/// 64 B (1 B len ++ 63 B str)
|
|
||||||
str64,
|
|
||||||
/// 256 B (1 B len ++ 255 B str)
|
|
||||||
str256,
|
|
||||||
/// 1 kiB (2 B len ++ <= 1022 B str)
|
|
||||||
str1k,
|
|
||||||
/// 4 kiB (2 B len ++ <= 4094 B str)
|
|
||||||
str4k,
|
|
||||||
|
|
||||||
pub fn countPerPage(self: ColumnType) usize {
|
|
||||||
return switch (self) {
|
|
||||||
.str16 => page_size / 16,
|
|
||||||
.str64 => page_size / 64,
|
|
||||||
.str256 => page_size / 256,
|
|
||||||
.str1k => page_size / 1024,
|
|
||||||
.str4k => page_size / 4096,
|
|
||||||
inline else => |x| page_size / @sizeOf(ZigTypeSimd(x)) * x.simdWidth(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test countPerPage {
|
|
||||||
try std.testing.expectEqual(page_size, countPerPage(.boolean));
|
|
||||||
try std.testing.expectEqual(8 * page_size, countPerPage(.bit));
|
|
||||||
try std.testing.expectEqual(page_size / 4, countPerPage(.u32));
|
|
||||||
try std.testing.expectEqual(page_size / 8, countPerPage(.u64));
|
|
||||||
try std.testing.expectEqual(page_size / @sizeOf(vm.Vector3x8) * 8, countPerPage(.vec3));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simdWidth(self: ColumnType) usize {
|
|
||||||
return switch (self) {
|
|
||||||
.boolean => 1,
|
|
||||||
.bit => 8,
|
|
||||||
.u8 => 1,
|
|
||||||
.u16 => 1,
|
|
||||||
.u32 => 1,
|
|
||||||
.u64 => 1,
|
|
||||||
.i8 => 1,
|
|
||||||
.i16 => 1,
|
|
||||||
.i32 => 1,
|
|
||||||
.i64 => 1,
|
|
||||||
.f32 => 1,
|
|
||||||
.f64 => 1,
|
|
||||||
.color => 1,
|
|
||||||
.color_hdr => 1,
|
|
||||||
.mat3x2 => 8,
|
|
||||||
.mat4x4 => 8,
|
|
||||||
.complex => 8,
|
|
||||||
.quaternion => 8,
|
|
||||||
.vec2 => 8,
|
|
||||||
.vec2i => 8,
|
|
||||||
.vec3 => 8,
|
|
||||||
.vec3i => 8,
|
|
||||||
.vec4 => 8,
|
|
||||||
.vec4i => 8,
|
|
||||||
.str16 => 1,
|
|
||||||
.str64 => 1,
|
|
||||||
.str256 => 1,
|
|
||||||
.str1k => 1,
|
|
||||||
.str4k => 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ZigType(comptime self: ColumnType) type {
|
|
||||||
return switch (self) {
|
|
||||||
.boolean => bool,
|
|
||||||
.bit => u1,
|
|
||||||
.u8 => u8,
|
|
||||||
.u16 => u16,
|
|
||||||
.u32 => u32,
|
|
||||||
.u64 => u64,
|
|
||||||
.i8 => i8,
|
|
||||||
.i16 => i16,
|
|
||||||
.i32 => i32,
|
|
||||||
.i64 => i64,
|
|
||||||
.f32 => f32,
|
|
||||||
.f64 => f64,
|
|
||||||
.color => vm.Color,
|
|
||||||
.color_hdr => vm.ColorHdr,
|
|
||||||
.mat3x2 => vm.Matrix3x2,
|
|
||||||
.mat4x4 => vm.Matrix4x4,
|
|
||||||
.complex => vm.Complex,
|
|
||||||
.quaternion => vm.Quaternion,
|
|
||||||
.vec2 => vm.Vector2,
|
|
||||||
.vec2i => vm.Vector2Int,
|
|
||||||
.vec3 => vm.Vector3,
|
|
||||||
.vec3i => vm.Vector3Int,
|
|
||||||
.vec4 => vm.Vector4,
|
|
||||||
.vec4i => vm.Vector4Int,
|
|
||||||
.str16 => []const u8,
|
|
||||||
.str64 => []const u8,
|
|
||||||
.str256 => []const u8,
|
|
||||||
.str1k => []const u8,
|
|
||||||
.str4k => []const u8,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ZigTypeSimd(comptime self: ColumnType) type {
|
|
||||||
return switch (self) {
|
|
||||||
.boolean => bool,
|
|
||||||
.bit => u8,
|
|
||||||
.u8 => u8,
|
|
||||||
.u16 => u16,
|
|
||||||
.u32 => u32,
|
|
||||||
.u64 => u64,
|
|
||||||
.i8 => i8,
|
|
||||||
.i16 => i16,
|
|
||||||
.i32 => i32,
|
|
||||||
.i64 => i64,
|
|
||||||
.f32 => f32,
|
|
||||||
.f64 => f64,
|
|
||||||
.color => vm.Color,
|
|
||||||
.color_hdr => vm.ColorHdr,
|
|
||||||
.mat3x2 => vm.Matrix3x2x8,
|
|
||||||
.mat4x4 => vm.Matrix4x4x8,
|
|
||||||
.complex => vm.Complex_x8,
|
|
||||||
.quaternion => vm.Quaternion_x8,
|
|
||||||
.vec2 => vm.Vector2x8,
|
|
||||||
.vec2i => vm.Vector2Int_x8,
|
|
||||||
.vec3 => vm.Vector3x8,
|
|
||||||
.vec3i => vm.Vector3Int_x8,
|
|
||||||
.vec4 => vm.Vector4x8,
|
|
||||||
.vec4i => vm.Vector4Int_x8,
|
|
||||||
.str16 => []const u8,
|
|
||||||
.str64 => []const u8,
|
|
||||||
.str256 => []const u8,
|
|
||||||
.str1k => []const u8,
|
|
||||||
.str4k => []const u8,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const TypedValue = union(enum) {
|
|
||||||
boolean: bool,
|
|
||||||
uint: u64,
|
|
||||||
int: i64,
|
|
||||||
f32: f32,
|
|
||||||
f64: f64,
|
|
||||||
color: vm.Color,
|
|
||||||
color_hdr: vm.ColorHdr,
|
|
||||||
mat3x2: vm.Matrix3x2,
|
|
||||||
mat4x4: vm.Matrix4x4,
|
|
||||||
complex: vm.Complex,
|
|
||||||
quaternion: vm.Quaternion,
|
|
||||||
vec2: vm.Vector2,
|
|
||||||
vec2i: vm.Vector2Int,
|
|
||||||
vec3: vm.Vector3,
|
|
||||||
vec3i: vm.Vector3Int,
|
|
||||||
vec4: vm.Vector4,
|
|
||||||
vec4i: vm.Vector4Int,
|
|
||||||
str: []const u8,
|
|
||||||
|
|
||||||
pub fn initBool(x: bool) TypedValue {
|
|
||||||
return .{ .boolean = x };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn initUint(x: u64) TypedValue {
|
|
||||||
return .{ .uint = x };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn initColor(x: vm.Color) TypedValue {
|
|
||||||
return .{ .color = x };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn initStr(x: []const u8) TypedValue {
|
|
||||||
return .{ .str = x };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const TableDefinition = struct {
|
pub const TableDefinition = struct {
|
||||||
id: TableId,
|
id: TableId,
|
||||||
column_definitions: []const ColumnDefinition,
|
column_definitions: []const ColumnDefinition,
|
||||||
@@ -262,7 +95,7 @@ pub const ColumnDefinition = struct {
|
|||||||
|
|
||||||
pub const CellValue = struct {
|
pub const CellValue = struct {
|
||||||
id: ColumnId,
|
id: ColumnId,
|
||||||
value: TypedValue,
|
value: TaggedValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Database = struct {
|
pub const Database = struct {
|
||||||
@@ -331,6 +164,14 @@ pub const Database = struct {
|
|||||||
table.next_row_id = try row_id.next();
|
table.next_row_id = try row_id.next();
|
||||||
return row_id;
|
return row_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getValue(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.get(row_id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Table = struct {
|
pub const Table = struct {
|
||||||
@@ -407,144 +248,188 @@ pub const Column = struct {
|
|||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(self: *Column, row_id: RowId, value: TypedValue, allocator: std.mem.Allocator) !void {
|
pub fn get(self: *Column, row_id: RowId) TaggedValue {
|
||||||
const count_per_page = self.type.countPerPage();
|
const values_per_page = self.type.valuesPerPage(page_size);
|
||||||
|
|
||||||
const page_i = @as(usize, row_id.toInt() / count_per_page);
|
const page_index = @as(usize, row_id.toInt() / values_per_page);
|
||||||
const page_j = @as(usize, row_id.toInt() % count_per_page);
|
const value_index = @as(usize, row_id.toInt() % values_per_page);
|
||||||
|
|
||||||
try self.pages.ensureTotalCapacity(allocator, page_i + 1);
|
const page = &self.pages.items[page_index];
|
||||||
while (page_i >= self.pages.items.len) {
|
|
||||||
|
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 getBool(self: *Column, row_id: RowId) !bool {
|
||||||
|
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 => page.get(bool, value_index),
|
||||||
|
.bit => page.getBit(value_index),
|
||||||
|
else => error.TypeError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
self.pages.appendAssumeCapacity(try .init());
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = &self.pages.items[page_i];
|
const page = &self.pages.items[page_index];
|
||||||
|
|
||||||
switch (value) {
|
return switch (value) {
|
||||||
.boolean => |x| switch (self.type) {
|
.bool => |x| switch (self.type) {
|
||||||
.boolean => page.assign(bool, page_j, x),
|
.bool => page.set(bool, value_index, x),
|
||||||
.bit => page.setBit(page_j, x),
|
.bit => page.setBit(value_index, x),
|
||||||
else => return error.TypeError,
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.uint => |x| switch (self.type) {
|
.u8 => |x| switch (self.type) {
|
||||||
.u8 => page.assign(u8, page_j, std.math.cast(u8, x) orelse return error.RangeError),
|
.u8 => page.set(u8, value_index, x),
|
||||||
.u16 => page.assign(u16, page_j, std.math.cast(u16, x) orelse return error.RangeError),
|
else => error.TypeError,
|
||||||
.u32 => page.assign(u32, page_j, std.math.cast(u32, x) orelse return error.RangeError),
|
|
||||||
.u64 => page.assign(u64, page_j, x),
|
|
||||||
else => return error.TypeError,
|
|
||||||
},
|
},
|
||||||
.int => |x| switch (self.type) {
|
.u16 => |x| switch (self.type) {
|
||||||
.i8 => page.assign(i8, page_j, std.math.cast(i8, x) orelse return error.RangeError),
|
.u16 => page.set(u16, value_index, x),
|
||||||
.i16 => page.assign(i16, page_j, std.math.cast(i16, x) orelse return error.RangeError),
|
else => error.TypeError,
|
||||||
.i32 => page.assign(i32, page_j, std.math.cast(i32, x) orelse return error.RangeError),
|
|
||||||
.i64 => page.assign(i64, page_j, x),
|
|
||||||
else => return error.TypeError,
|
|
||||||
},
|
},
|
||||||
.f32 => |x| {
|
.u32 => |x| switch (self.type) {
|
||||||
if (self.type != .f32) return error.TypeError;
|
.u32 => page.set(u32, value_index, x),
|
||||||
page.assign(f32, page_j, x);
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.f64 => |x| {
|
.u64 => |x| switch (self.type) {
|
||||||
if (self.type != .f64) return error.TypeError;
|
.u64 => page.set(u64, value_index, x),
|
||||||
page.assign(f64, page_j, x);
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.color => |x| {
|
.i8 => |x| switch (self.type) {
|
||||||
if (self.type != .color) return error.TypeError;
|
.i8 => page.set(i8, value_index, x),
|
||||||
page.assign(vm.Color, page_j, x);
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.color_hdr => |x| {
|
.i16 => |x| switch (self.type) {
|
||||||
if (self.type != .color_hdr) return error.TypeError;
|
.i16 => page.set(i16, value_index, x),
|
||||||
page.assign(vm.ColorHdr, page_j, x);
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.mat3x2 => |x| {
|
.i32 => |x| switch (self.type) {
|
||||||
_ = x;
|
.i32 => page.set(i32, value_index, x),
|
||||||
@panic("TODO");
|
else => error.TypeError,
|
||||||
//if (self.type != .mat3x2) return error.TypeError;
|
|
||||||
//const simd_i = page_j / 8;
|
|
||||||
//const simd_j = page_j % 8;
|
|
||||||
//const ptr = page.getPtr(vm.Matrix3x2x8, simd_i);
|
|
||||||
//ptr.ix[simd_j] = x.ix;
|
|
||||||
//ptr.iy[simd_j] = x.iy;
|
|
||||||
//ptr.jx[simd_j] = x.jx;
|
|
||||||
//ptr.jy[simd_j] = x.jy;
|
|
||||||
//ptr.tx[simd_j] = x.tx;
|
|
||||||
//ptr.ty[simd_j] = x.ty;
|
|
||||||
},
|
},
|
||||||
.mat4x4 => |x| {
|
.i64 => |x| switch (self.type) {
|
||||||
_ = x;
|
.i64 => page.set(i64, value_index, x),
|
||||||
@panic("TODO");
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.complex => |x| {
|
.f32 => |x| switch (self.type) {
|
||||||
_ = x;
|
.f32 => page.set(f32, value_index, x),
|
||||||
@panic("TODO");
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.quaternion => |x| {
|
.f64 => |x| switch (self.type) {
|
||||||
_ = x;
|
.f64 => page.set(f64, value_index, x),
|
||||||
@panic("TODO");
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.vec2 => |x| {
|
.color => |x| switch (self.type) {
|
||||||
_ = x;
|
.color => page.set(vm.Color, value_index, x),
|
||||||
@panic("TODO");
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.vec2i => |x| {
|
.color_hdr => |x| switch (self.type) {
|
||||||
_ = x;
|
.color_hdr => page.set(vm.ColorHdr, value_index, x),
|
||||||
@panic("TODO");
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.vec3 => |x| {
|
.mat3x2 => |x| switch (self.type) {
|
||||||
_ = x;
|
.mat3x2 => page.setClustered(vm.Matrix3x2, 8, value_index, x),
|
||||||
@panic("TODO");
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.vec3i => |x| {
|
.mat4x4 => |x| switch (self.type) {
|
||||||
_ = x;
|
.mat4x4 => page.setClustered(vm.Matrix4x4, 8, value_index, x),
|
||||||
@panic("TODO");
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.vec4 => |x| {
|
.complex => |x| switch (self.type) {
|
||||||
_ = x;
|
.complex => page.set(vm.Complex, value_index, x),
|
||||||
@panic("TODO");
|
else => error.TypeError,
|
||||||
},
|
},
|
||||||
.vec4i => |x| {
|
.quaternion => |x| switch (self.type) {
|
||||||
_ = x;
|
.quaternion => page.set(vm.Quaternion, value_index, x),
|
||||||
@panic("TODO");
|
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) {
|
.str => |x| switch (self.type) {
|
||||||
.str16 => {
|
.str8 => page.getPtr(FixedString(u8, 8), value_index).tryCopy(x),
|
||||||
if (x.len > 15) return error.RangeError;
|
.str16 => page.getPtr(FixedString(u8, 16), value_index).tryCopy(x),
|
||||||
const slice = page.ptr[page_j * 16 .. (page_j + 1) * 16];
|
.str32 => page.getPtr(FixedString(u8, 32), value_index).tryCopy(x),
|
||||||
@as(*u8, @ptrCast(slice[0..1])).* = @intCast(x.len);
|
.str64 => page.getPtr(FixedString(u8, 64), value_index).tryCopy(x),
|
||||||
@memcpy(slice[1 .. 1 + x.len], x);
|
.str128 => page.getPtr(FixedString(u8, 128), value_index).tryCopy(x),
|
||||||
@memset(slice[1 + x.len ..], 0);
|
.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,
|
||||||
},
|
},
|
||||||
.str64 => {
|
};
|
||||||
if (x.len > 63) return error.RangeError;
|
|
||||||
const slice = page.ptr[page_j * 64 .. (page_j + 1) * 64];
|
|
||||||
@as(*u8, @ptrCast(slice[0..1])).* = @intCast(x.len);
|
|
||||||
@memcpy(slice[1 .. 1 + x.len], x);
|
|
||||||
@memset(slice[1 + x.len ..], 0);
|
|
||||||
},
|
|
||||||
.str256 => {
|
|
||||||
if (x.len > 255) return error.RangeError;
|
|
||||||
const slice = page.ptr[page_j * 256 .. (page_j + 1) * 256];
|
|
||||||
@as(*u8, @ptrCast(slice[0..1])).* = @intCast(x.len);
|
|
||||||
@memcpy(slice[1 .. 1 + x.len], x);
|
|
||||||
@memset(slice[1 + x.len ..], 0);
|
|
||||||
},
|
|
||||||
.str1k => {
|
|
||||||
if (x.len > 1022) return error.RangeError;
|
|
||||||
const slice = page.ptr[page_j * 1024 .. (page_j + 1) * 1024];
|
|
||||||
@as(*u16, @ptrCast(@alignCast(slice[0..2]))).* = @intCast(x.len);
|
|
||||||
@memcpy(slice[2 .. 2 + x.len], x);
|
|
||||||
@memset(slice[2 + x.len ..], 0);
|
|
||||||
},
|
|
||||||
.str4k => {
|
|
||||||
if (x.len > 4094) return error.RangeError;
|
|
||||||
const slice = page.ptr[page_j * 4096 .. (page_j + 1) * 4096];
|
|
||||||
@as(*u16, @ptrCast(@alignCast(slice[0..2]))).* = @intCast(x.len);
|
|
||||||
@memcpy(slice[2 .. 2 + x.len], x);
|
|
||||||
@memset(slice[2 + x.len ..], 0);
|
|
||||||
},
|
|
||||||
else => return error.TypeError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -565,18 +450,63 @@ pub const Page = struct {
|
|||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assign(self: Page, comptime T: type, index: usize, value: T) void {
|
pub fn get(self: Page, comptime T: type, index: usize) T {
|
||||||
const byte_offset = index * @sizeOf(T);
|
const byte_offset = index * @sizeOf(T);
|
||||||
std.debug.assert(byte_offset + @sizeOf(T) <= page_size);
|
std.debug.assert(byte_offset + @sizeOf(T) <= page_size);
|
||||||
|
|
||||||
@as([*]T, @ptrCast(self.ptr))[index] = value;
|
return @as([*]T, @ptrCast(self.ptr))[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getPtr(self: Page, comptime T: type, index: usize) *T {
|
pub fn getPtr(self: Page, comptime T: type, index: usize) *T {
|
||||||
const byte_offset = index * @sizeOf(T);
|
const byte_offset = index * @sizeOf(T);
|
||||||
std.debug.assert(byte_offset + @sizeOf(T) <= page_size);
|
std.debug.assert(byte_offset + @sizeOf(T) <= page_size);
|
||||||
|
|
||||||
return @ptrCast(@alignCast(self.ptr[byte_offset..].ptr));
|
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 {
|
pub fn setBit(self: Page, bit_index: usize, value: bool) void {
|
||||||
@@ -591,6 +521,33 @@ pub const Page = struct {
|
|||||||
self.ptr[byte_index] &= mask;
|
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 {
|
test {
|
||||||
@@ -616,7 +573,7 @@ test {
|
|||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.id = column_ids.password_hash,
|
.id = column_ids.password_hash,
|
||||||
.type = .str256,
|
.type = .str128,
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.id = column_ids.color,
|
.id = column_ids.color,
|
||||||
@@ -625,33 +582,45 @@ test {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
_ = try db.insertRow(table_ids.users, &.{
|
const admin_id = try db.insertRow(table_ids.users, &.{
|
||||||
.{
|
.{
|
||||||
.id = column_ids.username,
|
.id = column_ids.username,
|
||||||
.value = .initStr("admin"),
|
.value = .{ .str = "admin" },
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.id = column_ids.password_hash,
|
.id = column_ids.password_hash,
|
||||||
.value = .initStr("$argon2id$v=19$m=16,t=2,p=1$R0l1WjlpM1BUSXZ3dU0xbQ$muWG51NiQjaIe6RTh6LJgk/7VJMvtbtJiN5Z11fEFbI"),
|
.value = .{ .str = "$argon2id$v=19$m=16,t=2,p=1$R0l1WjlpM1BUSXZ3dU0xbQ$muWG51NiQjaIe6RTh6LJgk/7VJMvtbtJiN5Z11fEFbI" },
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.id = column_ids.color,
|
.id = column_ids.color,
|
||||||
.value = .initColor(.red),
|
.value = .{ .color = .red },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
_ = try db.insertRow(table_ids.users, &.{
|
const user_id = try db.insertRow(table_ids.users, &.{
|
||||||
.{
|
.{
|
||||||
.id = column_ids.username,
|
.id = column_ids.username,
|
||||||
.value = .initStr("user"),
|
.value = .{ .str = "user" },
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.id = column_ids.password_hash,
|
.id = column_ids.password_hash,
|
||||||
.value = .initStr("$argon2id$v=19$m=16,t=2,p=1$RlNyZTR3dklRU1hVb3hHSA$8EDVcnkQouADrpvb5bONOqQWrUil1Jc/YaZGe3t34q0"),
|
.value = .{ .str = "$argon2id$v=19$m=16,t=2,p=1$RlNyZTR3dklRU1hVb3hHSA$8EDVcnkQouADrpvb5bONOqQWrUil1Jc/YaZGe3t34q0" },
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.id = column_ids.color,
|
.id = column_ids.color,
|
||||||
.value = .initColor(.blue),
|
.value = .{ .color = .blue },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const admin_username = try db.getValue(table_ids.users, column_ids.username, admin_id);
|
||||||
|
const admin_color = try db.getValue(table_ids.users, column_ids.color, admin_id);
|
||||||
|
|
||||||
|
const user_username = try db.getValue(table_ids.users, column_ids.username, user_id);
|
||||||
|
const user_color = try db.getValue(table_ids.users, column_ids.color, user_id);
|
||||||
|
|
||||||
|
try std.testing.expectEqualStrings("admin", admin_username.str);
|
||||||
|
try std.testing.expectEqual(vm.Color.red, admin_color.color);
|
||||||
|
|
||||||
|
try std.testing.expectEqualStrings("user", user_username.str);
|
||||||
|
try std.testing.expectEqual(vm.Color.blue, user_color.color);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user