This commit is contained in:
2025-01-31 09:58:53 +03:00
parent 5670efc4cb
commit 27cb6cb0c6
13 changed files with 1174 additions and 96 deletions

228
lib/data/graphdb/graphdb.v Normal file
View File

@@ -0,0 +1,228 @@
module graphdb
import freeflowuniverse.herolib.data.ourdb
// Node represents a vertex in the graph with properties and edge references
pub struct Node {
pub mut:
id u32 // Unique identifier
properties map[string]string // Key-value properties
edges_out []EdgeRef // Outgoing edge references
edges_in []EdgeRef // Incoming edge references
}
// Edge represents a connection between nodes with properties
pub struct Edge {
pub mut:
id u32 // Unique identifier
from_node u32 // Source node ID
to_node u32 // Target node ID
edge_type string // Type of relationship
properties map[string]string // Key-value properties
}
// EdgeRef is a lightweight reference to an edge
pub struct EdgeRef {
pub mut:
edge_id u32 // Database ID of the edge
edge_type string // Type of the edge relationship
}
// GraphDB represents the graph database
pub struct GraphDB {
mut:
db &ourdb.OurDB // Database for persistent storage
}
pub struct NewArgs {
pub mut:
path string
reset bool
}
// Creates a new graph database instance
pub fn new(args NewArgs) !&GraphDB {
mut db := ourdb.new(
path: args.path
record_size_max: 1024 * 4 // 4KB max record size
incremental_mode: true
reset: args.reset
)!
return &GraphDB{
db: &db
}
}
// Creates a new node with the given properties
pub fn (mut gdb GraphDB) create_node(properties map[string]string) !u32 {
node := Node{
properties: properties
edges_out: []EdgeRef{}
edges_in: []EdgeRef{}
}
node_id := gdb.db.set(data: serialize_node(node))!
return node_id
}
// Creates an edge between two nodes
pub fn (mut gdb GraphDB) create_edge(from_id u32, to_id u32, edge_type string, properties map[string]string) !u32 {
// Create the edge
edge := Edge{
from_node: from_id
to_node: to_id
edge_type: edge_type
properties: properties
}
edge_id := gdb.db.set(data: serialize_edge(edge))!
// Update source node's outgoing edges
mut from_node := deserialize_node(gdb.db.get(from_id)!)!
from_node.edges_out << EdgeRef{
edge_id: edge_id
edge_type: edge_type
}
gdb.db.set(id: from_id, data: serialize_node(from_node))!
// Update target node's incoming edges
mut to_node := deserialize_node(gdb.db.get(to_id)!)!
to_node.edges_in << EdgeRef{
edge_id: edge_id
edge_type: edge_type
}
gdb.db.set(id: to_id, data: serialize_node(to_node))!
return edge_id
}
// Gets a node by its ID
pub fn (mut gdb GraphDB) get_node(id u32) !Node {
node_data := gdb.db.get(id)!
return deserialize_node(node_data)!
}
// Gets an edge by its ID
pub fn (mut gdb GraphDB) get_edge(id u32) !Edge {
edge_data := gdb.db.get(id)!
return deserialize_edge(edge_data)!
}
// Updates a node's properties
pub fn (mut gdb GraphDB) update_node(id u32, properties map[string]string) ! {
mut node := deserialize_node(gdb.db.get(id)!)!
node.properties = properties.clone()
gdb.db.set(id: id, data: serialize_node(node))!
}
// Updates an edge's properties
pub fn (mut gdb GraphDB) update_edge(id u32, properties map[string]string) ! {
mut edge := deserialize_edge(gdb.db.get(id)!)!
edge.properties = properties.clone()
gdb.db.set(id: id, data: serialize_edge(edge))!
}
// Deletes a node and all its edges
pub fn (mut gdb GraphDB) delete_node(id u32) ! {
node := deserialize_node(gdb.db.get(id)!)!
// Delete outgoing edges
for edge_ref in node.edges_out {
gdb.delete_edge(edge_ref.edge_id)!
}
// Delete incoming edges
for edge_ref in node.edges_in {
gdb.delete_edge(edge_ref.edge_id)!
}
// Delete the node itself
gdb.db.delete(id)!
}
// Deletes an edge and updates connected nodes
pub fn (mut gdb GraphDB) delete_edge(id u32) ! {
edge := deserialize_edge(gdb.db.get(id)!)!
// Update source node
mut from_node := deserialize_node(gdb.db.get(edge.from_node)!)!
for i, edge_ref in from_node.edges_out {
if edge_ref.edge_id == id {
from_node.edges_out.delete(i)
break
}
}
gdb.db.set(id: edge.from_node, data: serialize_node(from_node))!
// Update target node
mut to_node := deserialize_node(gdb.db.get(edge.to_node)!)!
for i, edge_ref in to_node.edges_in {
if edge_ref.edge_id == id {
to_node.edges_in.delete(i)
break
}
}
gdb.db.set(id: edge.to_node, data: serialize_node(to_node))!
// Delete the edge itself
gdb.db.delete(id)!
}
// Queries nodes by property value
pub fn (mut gdb GraphDB) query_nodes_by_property(key string, value string) ![]Node {
mut nodes := []Node{}
mut next_id := gdb.db.get_next_id()!
for id := u32(0); id < next_id; id++ {
if node_data := gdb.db.get(id) {
if node := deserialize_node(node_data) {
if node.properties[key] == value {
nodes << node
}
}
}
}
return nodes
}
// Gets all edges between two nodes
pub fn (mut gdb GraphDB) get_edges_between(from_id u32, to_id u32) ![]Edge {
from_node := deserialize_node(gdb.db.get(from_id)!)!
mut edges := []Edge{}
for edge_ref in from_node.edges_out {
edge := deserialize_edge(gdb.db.get(edge_ref.edge_id)!)!
if edge.to_node == to_id {
edges << edge
}
}
return edges
}
// Gets all nodes connected to a given node by edge type
pub fn (mut gdb GraphDB) get_connected_nodes(id u32, edge_type string, direction string) ![]Node {
node := deserialize_node(gdb.db.get(id)!)!
mut connected_nodes := []Node{}
if direction in ['out', 'both'] {
for edge_ref in node.edges_out {
if edge_ref.edge_type == edge_type {
edge := deserialize_edge(gdb.db.get(edge_ref.edge_id)!)!
connected_nodes << deserialize_node(gdb.db.get(edge.to_node)!)!
}
}
}
if direction in ['in', 'both'] {
for edge_ref in node.edges_in {
if edge_ref.edge_type == edge_type {
edge := deserialize_edge(gdb.db.get(edge_ref.edge_id)!)!
connected_nodes << deserialize_node(gdb.db.get(edge.from_node)!)!
}
}
}
return connected_nodes
}

View File

@@ -0,0 +1,246 @@
module graphdb
// Gets detailed information about a node
pub fn (mut gdb GraphDB) debug_node(id u32) !string {
node := gdb.get_node(id)!
mut info := '\nNode Details (ID: ${id})\n'
info += '===================\n'
// Properties
info += '\nProperties:\n'
if node.properties.len == 0 {
info += ' (none)\n'
} else {
for key, value in node.properties {
info += ' ${key}: ${value}\n'
}
}
// Outgoing edges
info += '\nOutgoing Edges:\n'
if node.edges_out.len == 0 {
info += ' (none)\n'
} else {
for edge_ref in node.edges_out {
edge := gdb.get_edge(edge_ref.edge_id)!
target := gdb.get_node(edge.to_node)!
info += ' -[${edge_ref.edge_type}]-> Node(${edge.to_node})'
if name := target.properties['name'] {
info += ' (${name})'
}
if edge.properties.len > 0 {
info += ' {'
mut first := true
for key, value in edge.properties {
if !first {
info += ', '
}
info += '${key}: ${value}'
first = false
}
info += '}'
}
info += '\n'
}
}
// Incoming edges
info += '\nIncoming Edges:\n'
if node.edges_in.len == 0 {
info += ' (none)\n'
} else {
for edge_ref in node.edges_in {
edge := gdb.get_edge(edge_ref.edge_id)!
source := gdb.get_node(edge.from_node)!
info += ' <-[${edge_ref.edge_type}]- Node(${edge.from_node})'
if name := source.properties['name'] {
info += ' (${name})'
}
if edge.properties.len > 0 {
info += ' {'
mut first := true
for key, value in edge.properties {
if !first {
info += ', '
}
info += '${key}: ${value}'
first = false
}
info += '}'
}
info += '\n'
}
}
return info
}
// Gets detailed information about an edge
pub fn (mut gdb GraphDB) debug_edge(id u32) !string {
edge := gdb.get_edge(id)!
from_node := gdb.get_node(edge.from_node)!
to_node := gdb.get_node(edge.to_node)!
mut info := '\nEdge Details (ID: ${id})\n'
info += '===================\n'
// Basic info
info += '\nType: ${edge.edge_type}\n'
// Connected nodes
info += '\nFrom Node (ID: ${edge.from_node}):\n'
if name := from_node.properties['name'] {
info += ' name: ${name}\n'
}
for key, value in from_node.properties {
if key != 'name' {
info += ' ${key}: ${value}\n'
}
}
info += '\nTo Node (ID: ${edge.to_node}):\n'
if name := to_node.properties['name'] {
info += ' name: ${name}\n'
}
for key, value in to_node.properties {
if key != 'name' {
info += ' ${key}: ${value}\n'
}
}
// Edge properties
info += '\nProperties:\n'
if edge.properties.len == 0 {
info += ' (none)\n'
} else {
for key, value in edge.properties {
info += ' ${key}: ${value}\n'
}
}
return info
}
// Prints the current state of the database
pub fn (mut gdb GraphDB) debug_db() ! {
mut next_id := gdb.db.get_next_id()!
println('\nGraph Database State')
println('===================')
// Print all nodes
println('\nNodes:')
println('------')
for id := u32(0); id < next_id; id++ {
if node_data := gdb.db.get(id) {
if node := deserialize_node(node_data) {
mut node_info := 'Node(${id})'
if name := node.properties['name'] {
node_info += ' (${name})'
}
node_info += ' - Properties: ${node.properties.len}, Out Edges: ${node.edges_out.len}, In Edges: ${node.edges_in.len}'
println(node_info)
}
}
}
// Print all edges
println('\nEdges:')
println('------')
for id := u32(0); id < next_id; id++ {
if edge_data := gdb.db.get(id) {
if edge := deserialize_edge(edge_data) {
mut from_name := ''
mut to_name := ''
if from_node := gdb.get_node(edge.from_node) {
if name := from_node.properties['name'] {
from_name = ' (${name})'
}
}
if to_node := gdb.get_node(edge.to_node) {
if name := to_node.properties['name'] {
to_name = ' (${name})'
}
}
mut edge_info := 'Edge(${id}): Node(${edge.from_node})${from_name} -[${edge.edge_type}]-> Node(${edge.to_node})${to_name}'
if edge.properties.len > 0 {
edge_info += ' {'
mut first := true
for key, value in edge.properties {
if !first {
edge_info += ', '
}
edge_info += '${key}: ${value}'
first = false
}
edge_info += '}'
}
println(edge_info)
}
}
}
}
// Prints a visual representation of the graph starting from a given node
pub fn (mut gdb GraphDB) print_graph_from(start_id u32, visited map[u32]bool) ! {
if start_id in visited {
return
}
mut my_visited := visited.clone()
my_visited[start_id] = true
node := gdb.get_node(start_id)!
mut node_info := 'Node(${start_id})'
if name := node.properties['name'] {
node_info += ' (${name})'
}
println(node_info)
// Print outgoing edges and recurse
for edge_ref in node.edges_out {
edge := gdb.get_edge(edge_ref.edge_id)!
mut edge_info := ' -[${edge.edge_type}]->'
if edge.properties.len > 0 {
edge_info += ' {'
mut first := true
for key, value in edge.properties {
if !first {
edge_info += ', '
}
edge_info += '${key}: ${value}'
first = false
}
edge_info += '}'
}
println(edge_info)
gdb.print_graph_from(edge.to_node, my_visited)!
}
}
// Prints a visual representation of the entire graph
pub fn (mut gdb GraphDB) print_graph() ! {
println('\nGraph Structure')
println('===============')
mut visited := map[u32]bool{}
mut next_id := gdb.db.get_next_id()!
// Start from each unvisited node to handle disconnected components
for id := u32(0); id < next_id; id++ {
if id !in visited {
if node_data := gdb.db.get(id) {
if _ := deserialize_node(node_data) {
gdb.print_graph_from(id, visited)!
}
}
}
}
}

View File

@@ -0,0 +1,196 @@
module graphdb
fn test_basic_operations() ! {
mut gdb := new(path: '/tmp/graphdb_test', reset: true)!
// Test creating nodes with properties
mut person1_id := gdb.create_node({
'name': 'Alice',
'age': '30'
})!
mut person2_id := gdb.create_node({
'name': 'Bob',
'age': '25'
})!
// Test retrieving nodes
person1 := gdb.get_node(person1_id)!
assert person1.properties['name'] == 'Alice'
assert person1.properties['age'] == '30'
person2 := gdb.get_node(person2_id)!
assert person2.properties['name'] == 'Bob'
assert person2.properties['age'] == '25'
// Test creating edge between nodes
edge_id := gdb.create_edge(person1_id, person2_id, 'KNOWS', {
'since': '2020'
})!
// Test retrieving edge
edge := gdb.get_edge(edge_id)!
assert edge.edge_type == 'KNOWS'
assert edge.properties['since'] == '2020'
assert edge.from_node == person1_id
assert edge.to_node == person2_id
// Test querying nodes by property
alice_nodes := gdb.query_nodes_by_property('name', 'Alice')!
assert alice_nodes.len == 1
assert alice_nodes[0].properties['age'] == '30'
// Test getting connected nodes
bob_knows := gdb.get_connected_nodes(person1_id, 'KNOWS', 'out')!
assert bob_knows.len == 1
assert bob_knows[0].properties['name'] == 'Bob'
alice_known_by := gdb.get_connected_nodes(person2_id, 'KNOWS', 'in')!
assert alice_known_by.len == 1
assert alice_known_by[0].properties['name'] == 'Alice'
// Test updating node properties
gdb.update_node(person1_id, {
'name': 'Alice',
'age': '31'
})!
updated_alice := gdb.get_node(person1_id)!
assert updated_alice.properties['age'] == '31'
// Test updating edge properties
gdb.update_edge(edge_id, {
'since': '2021'
})!
updated_edge := gdb.get_edge(edge_id)!
assert updated_edge.properties['since'] == '2021'
// Test getting edges between nodes
edges := gdb.get_edges_between(person1_id, person2_id)!
assert edges.len == 1
assert edges[0].edge_type == 'KNOWS'
// Test deleting edge
gdb.delete_edge(edge_id)!
remaining_edges := gdb.get_edges_between(person1_id, person2_id)!
assert remaining_edges.len == 0
// Test deleting node
gdb.delete_node(person1_id)!
if _ := gdb.get_node(person1_id) {
assert false, 'Expected error for deleted node'
}
}
fn test_complex_graph() ! {
mut gdb := new(path: '/tmp/graphdb_test_complex', reset: true)!
// Create nodes representing people
mut alice_id := gdb.create_node({
'name': 'Alice',
'age': '30',
'city': 'New York'
})!
mut bob_id := gdb.create_node({
'name': 'Bob',
'age': '25',
'city': 'Boston'
})!
mut charlie_id := gdb.create_node({
'name': 'Charlie',
'age': '35',
'city': 'New York'
})!
// Create nodes representing companies
mut company1_id := gdb.create_node({
'name': 'TechCorp',
'industry': 'Technology'
})!
mut company2_id := gdb.create_node({
'name': 'FinCo',
'industry': 'Finance'
})!
// Create relationships
gdb.create_edge(alice_id, bob_id, 'KNOWS', {'since': '2020'})!
gdb.create_edge(bob_id, charlie_id, 'KNOWS', {'since': '2019'})!
gdb.create_edge(charlie_id, alice_id, 'KNOWS', {'since': '2018'})!
gdb.create_edge(alice_id, company1_id, 'WORKS_AT', {'role': 'Engineer'})!
gdb.create_edge(bob_id, company2_id, 'WORKS_AT', {'role': 'Analyst'})!
gdb.create_edge(charlie_id, company1_id, 'WORKS_AT', {'role': 'Manager'})!
// Test querying by property
ny_people := gdb.query_nodes_by_property('city', 'New York')!
assert ny_people.len == 2
// Test getting connected nodes with different edge types
alice_knows := gdb.get_connected_nodes(alice_id, 'KNOWS', 'out')!
assert alice_knows.len == 1
assert alice_knows[0].properties['name'] == 'Bob'
alice_works_at := gdb.get_connected_nodes(alice_id, 'WORKS_AT', 'out')!
assert alice_works_at.len == 1
assert alice_works_at[0].properties['name'] == 'TechCorp'
// Test getting nodes connected in both directions
charlie_connections := gdb.get_connected_nodes(charlie_id, 'KNOWS', 'both')!
assert charlie_connections.len == 2
// Test company employees
techcorp_employees := gdb.get_connected_nodes(company1_id, 'WORKS_AT', 'in')!
assert techcorp_employees.len == 2
finco_employees := gdb.get_connected_nodes(company2_id, 'WORKS_AT', 'in')!
assert finco_employees.len == 1
assert finco_employees[0].properties['name'] == 'Bob'
}
fn test_edge_cases() ! {
mut gdb := new(path: '/tmp/graphdb_test_edge', reset: true)!
// Test empty properties
node_id := gdb.create_node(map[string]string{})!
node := gdb.get_node(node_id)!
assert node.properties.len == 0
// Test node with many properties
mut large_props := map[string]string{}
for i in 0..100 {
large_props['key${i}'] = 'value${i}'
}
large_node_id := gdb.create_node(large_props)!
large_node := gdb.get_node(large_node_id)!
assert large_node.properties.len == 100
// Test edge with empty properties
other_node_id := gdb.create_node({})!
edge_id := gdb.create_edge(node_id, other_node_id, 'TEST', map[string]string{})!
edge := gdb.get_edge(edge_id)!
assert edge.properties.len == 0
// Test querying non-existent property
empty_results := gdb.query_nodes_by_property('nonexistent', 'value')!
assert empty_results.len == 0
// Test getting edges between unconnected nodes
no_edges := gdb.get_edges_between(node_id, large_node_id)!
assert no_edges.len == 0
// Test getting connected nodes with non-existent edge type
no_connections := gdb.get_connected_nodes(node_id, 'NONEXISTENT', 'both')!
assert no_connections.len == 0
// Test deleting non-existent edge
if _ := gdb.delete_edge(u32(99999)) {
assert false, 'Expected error for non-existent edge'
}
// Test deleting non-existent node
if _ := gdb.delete_node(u32(99999)) {
assert false, 'Expected error for non-existent node'
}
}

View File

@@ -0,0 +1,287 @@
module graphdb
import encoding.binary
import math
// Serializes a Node struct to bytes
fn serialize_node(node Node) []u8 {
mut data := []u8{}
// Serialize properties
data << u32_to_bytes(u32(node.properties.len)) // Number of properties
for key, value in node.properties {
// Key length and bytes
data << u32_to_bytes(u32(key.len))
data << key.bytes()
// Value length and bytes
data << u32_to_bytes(u32(value.len))
data << value.bytes()
}
// Serialize outgoing edges
data << u32_to_bytes(u32(node.edges_out.len)) // Number of outgoing edges
for edge in node.edges_out {
data << u32_to_bytes(edge.edge_id)
data << u32_to_bytes(u32(edge.edge_type.len))
data << edge.edge_type.bytes()
}
// Serialize incoming edges
data << u32_to_bytes(u32(node.edges_in.len)) // Number of incoming edges
for edge in node.edges_in {
data << u32_to_bytes(edge.edge_id)
data << u32_to_bytes(u32(edge.edge_type.len))
data << edge.edge_type.bytes()
}
return data
}
// Deserializes bytes to a Node struct
fn deserialize_node(data []u8) !Node {
if data.len < 4 {
return error('Invalid node data: too short')
}
mut offset := 0
mut node := Node{
properties: map[string]string{}
edges_out: []EdgeRef{}
edges_in: []EdgeRef{}
}
// Deserialize properties
mut end_pos := int(offset) + 4
if end_pos > data.len {
return error('Invalid node data: truncated properties count')
}
num_properties := bytes_to_u32(data[offset..end_pos])
offset = end_pos
for _ in 0 .. num_properties {
// Read key
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid node data: truncated property key length')
}
key_len := bytes_to_u32(data[offset..end_pos])
offset = end_pos
end_pos = int(offset) + int(key_len)
if end_pos > data.len {
return error('Invalid node data: truncated property key')
}
key := data[offset..end_pos].bytestr()
offset = end_pos
// Read value
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid node data: truncated property value length')
}
value_len := bytes_to_u32(data[offset..end_pos])
offset = end_pos
end_pos = int(offset) + int(value_len)
if end_pos > data.len {
return error('Invalid node data: truncated property value')
}
value := data[offset..end_pos].bytestr()
offset = end_pos
node.properties[key] = value
}
// Deserialize outgoing edges
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid node data: truncated outgoing edges count')
}
num_edges_out := bytes_to_u32(data[offset..end_pos])
offset = end_pos
for _ in 0 .. num_edges_out {
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid node data: truncated edge ID')
}
edge_id := bytes_to_u32(data[offset..end_pos])
offset = end_pos
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid node data: truncated edge type length')
}
type_len := bytes_to_u32(data[offset..end_pos])
offset = end_pos
end_pos = int(offset) + int(type_len)
if end_pos > data.len {
return error('Invalid node data: truncated edge type')
}
edge_type := data[offset..end_pos].bytestr()
offset = end_pos
node.edges_out << EdgeRef{
edge_id: edge_id
edge_type: edge_type
}
}
// Deserialize incoming edges
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid node data: truncated incoming edges count')
}
num_edges_in := bytes_to_u32(data[offset..end_pos])
offset = end_pos
for _ in 0 .. num_edges_in {
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid node data: truncated edge ID')
}
edge_id := bytes_to_u32(data[offset..end_pos])
offset = end_pos
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid node data: truncated edge type length')
}
type_len := bytes_to_u32(data[offset..end_pos])
offset = end_pos
end_pos = int(offset) + int(type_len)
if end_pos > data.len {
return error('Invalid node data: truncated edge type')
}
edge_type := data[offset..end_pos].bytestr()
offset = end_pos
node.edges_in << EdgeRef{
edge_id: edge_id
edge_type: edge_type
}
}
return node
}
// Serializes an Edge struct to bytes
fn serialize_edge(edge Edge) []u8 {
mut data := []u8{}
// Serialize edge metadata
data << u32_to_bytes(edge.from_node)
data << u32_to_bytes(edge.to_node)
data << u32_to_bytes(u32(edge.edge_type.len))
data << edge.edge_type.bytes()
// Serialize properties
data << u32_to_bytes(u32(edge.properties.len))
for key, value in edge.properties {
data << u32_to_bytes(u32(key.len))
data << key.bytes()
data << u32_to_bytes(u32(value.len))
data << value.bytes()
}
return data
}
// Deserializes bytes to an Edge struct
fn deserialize_edge(data []u8) !Edge {
if data.len < 12 {
return error('Invalid edge data: too short')
}
mut offset := 0
mut edge := Edge{
properties: map[string]string{}
}
// Deserialize edge metadata
mut end_pos := int(offset) + 4
if end_pos > data.len {
return error('Invalid edge data: truncated from_node')
}
edge.from_node = bytes_to_u32(data[offset..end_pos])
offset = end_pos
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid edge data: truncated to_node')
}
edge.to_node = bytes_to_u32(data[offset..end_pos])
offset = end_pos
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid edge data: truncated type length')
}
type_len := bytes_to_u32(data[offset..end_pos])
offset = end_pos
end_pos = int(offset) + int(type_len)
if end_pos > data.len {
return error('Invalid edge data: truncated edge type')
}
edge.edge_type = data[offset..end_pos].bytestr()
offset = end_pos
// Deserialize properties
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid edge data: truncated properties count')
}
num_properties := bytes_to_u32(data[offset..end_pos])
offset = end_pos
for _ in 0 .. num_properties {
// Read key
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid edge data: truncated property key length')
}
key_len := bytes_to_u32(data[offset..end_pos])
offset = end_pos
end_pos = int(offset) + int(key_len)
if end_pos > data.len {
return error('Invalid edge data: truncated property key')
}
key := data[offset..end_pos].bytestr()
offset = end_pos
// Read value
end_pos = int(offset) + 4
if end_pos > data.len {
return error('Invalid edge data: truncated property value length')
}
value_len := bytes_to_u32(data[offset..end_pos])
offset = end_pos
end_pos = int(offset) + int(value_len)
if end_pos > data.len {
return error('Invalid edge data: truncated property value')
}
value := data[offset..end_pos].bytestr()
offset = end_pos
edge.properties[key] = value
}
return edge
}
// Helper function to convert u32 to bytes
fn u32_to_bytes(n u32) []u8 {
mut bytes := []u8{len: 4}
binary.little_endian_put_u32(mut bytes, n)
return bytes
}
// Helper function to convert bytes to u32
fn bytes_to_u32(data []u8) u32 {
return binary.little_endian_u32(data)
}