Files
castle/packages/datastore/src/root.zig

739 lines
27 KiB
Zig

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