╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────╮ │1585 map: DB.HashMap(.read_only), │ │1586 ) !T { │ │1587 var event: T = undefined; │ │1588 - │ │1588 switch (@typeInfo(T)) { │ │1589 .@"struct" => |struct_info| { │ │1590 inline for (struct_info.fields) |field| {│ ╰───────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1592 - switch (@typeInfo(field.type)) { │ │1593 - .pointer => |pointer_info| { │ │1594 - if (pointer_info.size == .slice and pointer_info.child == u8 and pointer_info.is_const) { │ │1595 - @field(event, field.name) = try readBytes(DB, hash_kind, arena.allocator(), map, field.name); │ │1596 - } else { │ │1597 - @compileError("unsupported read field type: " ++ @typeName(field.type)); │ │1591 + const cursor = try map.getCursor(hash.hashInt(hash_kind, field.name)); │ │1592 + @field(event, field.name) = try readField(field.type, DB, hash_kind, arena, cursor); │ │1593 + } │ │1594 + }, │ │1595 + else => @compileError("read expects a struct"), │ │1596 + } │ │1597 + return event; │ │1598 +} │ │1599 + │ │1600 +fn readField( │ │1601 + comptime Field: type, │ │1602 + comptime DB: type, │ │1603 + comptime hash_kind: hash.HashKind, │ │1604 + arena: *std.heap.ArenaAllocator, │ │1605 + cursor_maybe: ?DB.Cursor(.read_only), │ │1606 +) !Field { │ │1607 + if (@typeInfo(Field) == .optional) { │ │1608 + // a missing key or a null union payload is null │ │1609 + const cursor = cursor_maybe orelse return null; │ │1610 + if (cursor.slot().tag == .none) return null; │ │1611 + return try readField(@typeInfo(Field).optional.child, DB, hash_kind, arena, cursor); │ │1612 + } │ │1613 + const cursor = cursor_maybe orelse return error.NotFound; │ │1614 + switch (@typeInfo(Field)) { │ │1615 + .pointer => |pointer_info| { │ │1616 + if (pointer_info.size != .slice or pointer_info.child != u8 or !pointer_info.is_const) { │ │1617 + @compileError("unsupported read field type: " ++ @typeName(Field)); │ │1618 + } │ │1619 + return try cursor.readBytesAlloc(arena.allocator(), null); │ │1620 + }, │ │1621 + .array => |array_info| { │ │1622 + if (array_info.child != u8) @compileError("unsupported read field type: " ++ @typeName(Field)); │ │1623 + var bytes: Field = undefined; │ │1624 + if ((try cursor.readBytes(&bytes)).len != bytes.len) return error.InvalidByteArrayLength; │ │1625 + return bytes; │ │1626 + }, │ │1627 + .bool => { │ │1628 + var buffer: [5]u8 = undefined; │ │1629 + const bytes = try cursor.readBytesObject(&buffer); │ │1630 + const format_tag = bytes.format_tag orelse return error.InvalidFormatTag; │ │1631 + if (!std.mem.eql(u8, &format_tag, "bl")) return error.InvalidFormatTag; │ │1632 + if (std.mem.eql(u8, bytes.value, "true")) return true; │ │1633 + if (std.mem.eql(u8, bytes.value, "false")) return false; │ │1634 + return error.InvalidBool; │ │1635 + }, │ │1636 + .int => |int_info| return switch (int_info.signedness) { │ │1637 + .unsigned => @intCast(try cursor.readUint()), │ │1638 + .signed => @intCast(try cursor.readInt()), │ │1639 + }, │ │1640 + .@"enum" => return std.meta.stringToEnum(Field, try cursor.readBytesAlloc(arena.allocator(), null)) orelse error.InvalidEnumTag, │ │1641 + .@"union" => |union_info| { │ │1642 + if (union_info.tag_type == null) @compileError("unsupported read field type: " ++ @typeName(Field)); │ │1643 + switch (cursor.slot().tag) { │ │1644 + .bytes, .short_bytes => { │ │1645 + const name = try cursor.readBytesAlloc(arena.allocator(), null); │ │1646 + inline for (union_info.fields) |variant| { │ │1647 + if (variant.type == void and std.mem.eql(u8, name, variant.name)) { │ │1648 + return @unionInit(Field, variant.name, {}); │ │1649 } │ │1599 - }, │ │1600 - .array => |array_info| { │ │1601 - if (array_info.child == u8) { │ │1602 - const bytes = try readBytes(DB, hash_kind, arena.allocator(), map, field.name); │ │1603 - if (bytes.len != array_info.len) return error.InvalidByteArrayLength; │ │1604 - @memcpy(@field(event, field.name)[0..], bytes); │ │1605 - } else { │ │1606 - @compileError("unsupported read field type: " ++ @typeName(field.type)); │ │1650 + } │ │1651 + return error.InvalidUnion; │ │1652 + }, │ │1653 + .hash_map => { │ │1654 + const variants = try DB.HashMap(.read_only).init(cursor); │ │1655 + var iter = try variants.iterator(); │ │1656 + const entry = (try iter.next()) orelse return error.InvalidUnion; │ │1657 + const pair = try entry.readKeyValuePair(); │ │1658 + if (try iter.next() != null) return error.InvalidUnion; │ │1659 + inline for (union_info.fields) |variant| { │ │1660 + if (variant.type != void and pair.hash == hash.hashInt(hash_kind, variant.name)) { │ │1661 + return @unionInit(Field, variant.name, try readField(variant.type, DB, hash_kind, arena, pair.value_cursor)); │ │1662 } │ │1608 - }, │ │1609 - .bool => @field(event, field.name) = try readBool(DB, hash_kind, map, field.name), │ │1610 - .int => |int_info| switch (int_info.signedness) { │ │1611 - .unsigned => @field(event, field.name) = @intCast(try readUint(DB, hash_kind, map, field.name)), │ │1612 - .signed => @field(event, field.name) = @intCast(try readInt(DB, hash_kind, map, field.name)), │ │1613 - }, │ │1614 - .@"enum" => { │ │1615 - const bytes = try readBytes(DB, hash_kind, arena.allocator(), map, field.name); │ │1616 - @field(event, field.name) = std.meta.stringToEnum(field.type, bytes) orelse return error.InvalidEnumTag; │ │1617 - }, │ │1618 - .@"union" => { │ │1619 - if (!@hasDecl(field.type, "decode")) @compileError("unsupported read field type: " ++ @typeName(field.type)); │ │1620 - @field(event, field.name) = try field.type.decode(try readBytes(DB, hash_kind, arena.allocator(), map, field.name));│ │1621 - }, │ │1622 - .optional => |optional_info| { │ │1623 - // a missing key is null │ │1624 - if (try map.getCursor(hash.hashInt(hash_kind, field.name))) |cursor| { │ │1625 - if (optional_info.child == []const u8) { │ │1626 - @field(event, field.name) = try cursor.readBytesAlloc(arena.allocator(), null); │ │1627 - } else switch (@typeInfo(optional_info.child)) { │ │1628 - .int => |int_info| switch (int_info.signedness) { │ │1629 - .unsigned => @field(event, field.name) = @intCast(try cursor.readUint()), │ │1630 - .signed => @field(event, field.name) = @intCast(try cursor.readInt()), │ │1631 - }, │ │1632 - .array => |array_info| { │ │1633 - if (array_info.child != u8) @compileError("unsupported read field type: " ++ @typeName(field.type)); │ │1634 - var bytes: optional_info.child = undefined; │ │1635 - const value = try cursor.readBytes(&bytes); │ │1636 - if (value.len != bytes.len) return error.InvalidByteArrayLength; │ │1637 - @field(event, field.name) = bytes; │ │1638 - }, │ │1639 - .@"struct" => @field(event, field.name) = try read( │ │1640 - optional_info.child, │ │1641 - DB, │ │1642 - hash_kind, │ │1643 - arena, │ │1644 - try DB.HashMap(.read_only).init(cursor), │ │1645 - ), │ │1646 - else => @compileError("unsupported read field type: " ++ @typeName(field.type)), │ │1647 - } │ │1648 - } else { │ │1649 - @field(event, field.name) = null; │ │1650 - } │ │1651 - }, │ │1652 - .@"struct" => { │ │1653 - const cursor = try map.getCursor(hash.hashInt(hash_kind, field.name)) orelse return error.NotFound; │ │1654 - @field(event, field.name) = try read( │ │1655 - field.type, │ │1656 - DB, │ │1657 - hash_kind, │ │1658 - arena, │ │1659 - try DB.HashMap(.read_only).init(cursor), │ │1660 - ); │ │1661 - }, │ │1662 - else => @compileError("unsupported read field type: " ++ @typeName(field.type)), │ │1663 - } │ │1663 + } │ │1664 + return error.InvalidUnion; │ │1665 + }, │ │1666 + else => return error.InvalidUnion, │ │1667 } │ │1668 }, │ │1666 - else => @compileError("read expects a struct"), │ │1669 + .@"struct" => return try read(Field, DB, hash_kind, arena, try DB.HashMap(.read_only).init(cursor)), │ │1670 + else => @compileError("unsupported read field type: " ++ @typeName(Field)), │ │1671 } │ │1668 - │ │1669 - return event; │ │1672 } │ │1673 │ │1674 // whether two events carry the same data │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1719 }, │ │1720 else => @compileError("unsupported field type: " ++ @typeName(Field)), │ │1721 } │ │1720 -} │ │1721 - │ │1722 -fn readBytes( │ │1723 - comptime DB: type, │ │1724 - comptime hash_kind: hash.HashKind, │ │1725 - allocator: std.mem.Allocator, │ │1726 - map: DB.HashMap(.read_only), │ │1727 - field_name: []const u8, │ │1728 -) ![]const u8 { │ │1729 - const cursor = try map.getCursor(hash.hashInt(hash_kind, field_name)) orelse return error.NotFound;│ │1730 - return try cursor.readBytesAlloc(allocator, null); │ │1731 -} │ │1732 - │ │1733 -fn readUint( │ │1734 - comptime DB: type, │ │1735 - comptime hash_kind: hash.HashKind, │ │1736 - map: DB.HashMap(.read_only), │ │1737 - field_name: []const u8, │ │1738 -) !u64 { │ │1739 - const cursor = try map.getCursor(hash.hashInt(hash_kind, field_name)) orelse return error.NotFound;│ │1740 - return try cursor.readUint(); │ │1741 -} │ │1742 - │ │1743 -fn readInt( │ │1744 - comptime DB: type, │ │1745 - comptime hash_kind: hash.HashKind, │ │1746 - map: DB.HashMap(.read_only), │ │1747 - field_name: []const u8, │ │1748 -) !i64 { │ │1749 - const cursor = try map.getCursor(hash.hashInt(hash_kind, field_name)) orelse return error.NotFound;│ │1750 - return try cursor.readInt(); │ │1722 } │ │1723 │ │1753 -fn readBool( │ │1754 - comptime DB: type, │ │1755 - comptime hash_kind: hash.HashKind, │ │1756 - map: DB.HashMap(.read_only), │ │1757 - field_name: []const u8, │ │1758 -) !bool { │ │1759 - const cursor = try map.getCursor(hash.hashInt(hash_kind, field_name)) orelse return error.NotFound;│ │1760 - var buffer: [5]u8 = undefined; │ │1761 - const bytes = try cursor.readBytesObject(&buffer); │ │1762 - const format_tag = bytes.format_tag orelse return error.InvalidFormatTag; │ │1763 - if (!std.mem.eql(u8, &format_tag, "bl")) return error.InvalidFormatTag; │ │1764 - if (std.mem.eql(u8, bytes.value, "true")) return true; │ │1765 - if (std.mem.eql(u8, bytes.value, "false")) return false; │ │1766 - return error.InvalidBool; │ │1767 -} │ │1768 - │ │1724 // │ │1725 // writing to xitdb │ │1726 // │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1802 try map.put(key, .{ .bytes_object = .{ .value = bytes, .format_tag = "bl".* } }); │ │1803 }, │ │1804 .@"enum" => try upsertBytes(DB, hash_kind, map, key, @tagName(value)), │ │1850 - .@"union" => { │ │1851 - if (!@hasDecl(Field, "encode") or !@hasDecl(Field, "max_encoded_len")) { │ │1852 - @compileError("unsupported upsert field type: " ++ @typeName(Field)); │ │1853 - } │ │1854 - var buffer: [Field.max_encoded_len]u8 = undefined; │ │1855 - try upsertBytes(DB, hash_kind, map, key, try value.encode(&buffer)); │ │1805 + .@"union" => switch (value) { │ │1806 + inline else => |payload, tag| { │ │1807 + if (@TypeOf(payload) == void) { │ │1808 + try upsertBytes(DB, hash_kind, map, key, @tagName(tag)); │ │1809 + } else { │ │1810 + const tag_key = hash.hashInt(hash_kind, @tagName(tag)); │ │1811 + if (try map.getCursor(key)) |cursor| { │ │1812 + switch (cursor.slot().tag) { │ │1813 + .hash_map => { │ │1814 + const variants = try DB.HashMap(.read_only).init(cursor); │ │1815 + if (try variants.getCursor(tag_key) == null) try map.put(key, .{ .slot = null });│ │1816 + }, │ │1817 + else => try map.put(key, .{ .slot = null }), │ │1818 + } │ │1819 + } │ │1820 + const variants = try DB.HashMap(.read_write).init(try map.putCursor(key)); │ │1821 + if (@typeInfo(@TypeOf(payload)) == .optional) { │ │1822 + // keep the variant even when its payload is null │ │1823 + if (payload == null) return try variants.put(tag_key, .{ .slot = null }); │ │1824 + } │ │1825 + if (try variants.getCursor(tag_key)) |cursor| { │ │1826 + if (cursor.slot().tag == .none) _ = try variants.remove(tag_key); │ │1827 + } │ │1828 + try upsertField(DB, hash_kind, variants, @tagName(tag), @TypeOf(payload), payload); │ │1829 + } │ │1830 + }, │ │1831 }, │ │1832 .optional => |optional_info| { │ │1833 // a missing key is null │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────╮ │1870 cursor: *DB.Cursor(.read_only), │ │1871 value: []const u8, │ │1872 ) !bool { │ │1873 + switch (cursor.slot().tag) { │ │1874 + .bytes, .short_bytes => {}, │ │1875 + else => return false, │ │1876 + } │ │1877 var read_buffer: [1024]u8 = undefined; │ │1878 var reader = try cursor.reader(&read_buffer);│ │1879 if (reader.size != value.len) { │ ╰───────────────────────────────────────────────────────╯