This commit is contained in:
2025-01-31 15:39:44 +03:00
parent 27cb6cb0c6
commit 74ab68d05f
185 changed files with 2321 additions and 972 deletions

170
lib/data/graphdb/README.md Normal file
View File

@@ -0,0 +1,170 @@
# GraphDB
A lightweight, efficient graph database implementation in V that supports property graphs with nodes and edges. It provides both in-memory caching and persistent storage capabilities.
## Features
- Property Graph Model
- Nodes with key-value properties
- Typed edges with properties
- Bidirectional edge traversal
- Persistent Storage
- Automatic data persistence
- Efficient serialization
- Memory-Efficient Caching
- LRU caching for nodes and edges
- Configurable cache sizes
- Rich Query Capabilities
- Property-based node queries
- Edge-based node traversal
- Relationship type filtering
- CRUD Operations
- Create, read, update, and delete nodes
- Manage relationships between nodes
- Update properties dynamically
## Installation
GraphDB is part of the HeroLib library. Include it in your V project:
```v
import freeflowuniverse.herolib.data.graphdb
```
## Basic Usage
Here's a simple example demonstrating core functionality:
```v
import freeflowuniverse.herolib.data.graphdb
fn main() {
// Create a new graph database
mut gdb := graphdb.new(path: '/tmp/mydb', reset: true)!
// Create nodes
user_id := gdb.create_node({
'name': 'John',
'age': '30',
'city': 'London'
})!
company_id := gdb.create_node({
'name': 'TechCorp',
'industry': 'Technology'
})!
// Create relationship
gdb.create_edge(user_id, company_id, 'WORKS_AT', {
'role': 'Developer',
'since': '2022'
})!
// Query nodes by property
london_users := gdb.query_nodes_by_property('city', 'London')!
// Find connected nodes
workplaces := gdb.get_connected_nodes(user_id, 'WORKS_AT', 'out')!
}
```
## API Reference
### Creating a Database
```v
// Create new database instance
struct NewArgs {
path string // Storage path
reset bool // Clear existing data
cache_config CacheConfig // Optional cache configuration
}
db := graphdb.new(NewArgs{...})!
```
### Node Operations
```v
// Create node
node_id := db.create_node(properties: map[string]string)!
// Get node
node := db.get_node(id: u32)!
// Update node
db.update_node(id: u32, properties: map[string]string)!
// Delete node (and connected edges)
db.delete_node(id: u32)!
// Query nodes by property
nodes := db.query_nodes_by_property(key: string, value: string)!
```
### Edge Operations
```v
// Create edge
edge_id := db.create_edge(from_id: u32, to_id: u32, edge_type: string, properties: map[string]string)!
// Get edge
edge := db.get_edge(id: u32)!
// Update edge
db.update_edge(id: u32, properties: map[string]string)!
// Delete edge
db.delete_edge(id: u32)!
// Get edges between nodes
edges := db.get_edges_between(from_id: u32, to_id: u32)!
```
### Graph Traversal
```v
// Get connected nodes
// direction can be 'in', 'out', or 'both'
nodes := db.get_connected_nodes(id: u32, edge_type: string, direction: string)!
```
## Data Model
### Node Structure
```v
struct Node {
id u32 // Unique identifier
properties map[string]string // Key-value properties
node_type string // Type of node
edges_out []EdgeRef // Outgoing edge references
edges_in []EdgeRef // Incoming edge references
}
```
### Edge Structure
```v
struct Edge {
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
weight u16 // Edge weight
}
```
## Performance Considerations
- The database uses LRU caching for both nodes and edges to improve read performance
- Persistent storage is handled efficiently through the underlying OurDB implementation
- Edge references are stored in both source and target nodes for efficient traversal
- Property queries perform full scans - consider indexing needs for large datasets
## Example Use Cases
- Social Networks: Modeling user relationships and interactions
- Knowledge Graphs: Representing connected information and metadata
- Organization Charts: Modeling company structure and relationships
- Recommendation Systems: Building relationship-based recommendation engines

View File

@@ -1,131 +1,192 @@
module graphdb
import freeflowuniverse.herolib.data.ourdb
import freeflowuniverse.herolib.data.cache { Cache, CacheConfig, new_cache }
// Node represents a vertex in the graph with properties and edge references
@[heap]
pub struct Node {
pub mut:
id u32 // Unique identifier
properties map[string]string // Key-value properties
id u32 // Unique identifier
properties map[string]string // Key-value properties
node_type string // Type of node can e.g. refer to a object implementation e.g. a User, ...
edges_out []EdgeRef // Outgoing edge references
edges_in []EdgeRef // Incoming edge references
}
// Edge represents a connection between nodes with properties
@[heap]
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
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
weight u16 // weight of the connection between the objects
}
// EdgeRef is a lightweight reference to an edge
@[heap]
pub struct EdgeRef {
pub mut:
edge_id u32 // Database ID of the edge
edge_type string // Type of the edge relationship
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
db &ourdb.OurDB // Database for persistent storage
node_cache &Cache[Node] // Cache for nodes
edge_cache &Cache[Edge] // Cache for edges
}
pub struct NewArgs {
pub mut:
path string
reset bool
path string
reset bool
cache_config CacheConfig = CacheConfig{} // Default cache configuration
}
// 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
path: args.path
record_size_max: 1024 * 4 // 4KB max record size
incremental_mode: true
reset: args.reset
reset: args.reset
)!
// Create type-specific caches with provided config
node_cache := new_cache[Node](args.cache_config)
edge_cache := new_cache[Edge](args.cache_config)
return &GraphDB{
db: &db
db: &db
node_cache: node_cache
edge_cache: edge_cache
}
}
// Creates a new node with the given properties
pub fn (mut gdb GraphDB) create_node(properties map[string]string) !u32 {
node := Node{
mut node := Node{
properties: properties
edges_out: []EdgeRef{}
edges_in: []EdgeRef{}
edges_out: []EdgeRef{}
edges_in: []EdgeRef{}
}
// Let OurDB assign the ID in incremental mode
node_id := gdb.db.set(data: serialize_node(node))!
// Update node with assigned ID and cache it
node.id = node_id
gdb.node_cache.set(node_id, &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
mut edge := Edge{
from_node: from_id
to_node: to_id
edge_type: edge_type
properties: properties
}
// Let OurDB assign the ID in incremental mode
edge_id := gdb.db.set(data: serialize_edge(edge))!
// Update edge with assigned ID and cache it
edge.id = edge_id
gdb.edge_cache.set(edge_id, &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_id: edge_id
edge_type: edge_type
}
gdb.db.set(id: from_id, data: serialize_node(from_node))!
gdb.node_cache.set(from_id, &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_id: edge_id
edge_type: edge_type
}
gdb.db.set(id: to_id, data: serialize_node(to_node))!
gdb.node_cache.set(to_id, &to_node)
return edge_id
}
// Gets a node by its ID
pub fn (mut gdb GraphDB) get_node(id u32) !Node {
// Try cache first
if cached_node := gdb.node_cache.get(id) {
return *cached_node
}
// Load from database
node_data := gdb.db.get(id)!
return deserialize_node(node_data)!
node := deserialize_node(node_data)!
// Cache the node
gdb.node_cache.set(id, &node)
return node
}
// Gets an edge by its ID
pub fn (mut gdb GraphDB) get_edge(id u32) !Edge {
// Try cache first
if cached_edge := gdb.edge_cache.get(id) {
return *cached_edge
}
// Load from database
edge_data := gdb.db.get(id)!
return deserialize_edge(edge_data)!
edge := deserialize_edge(edge_data)!
// Cache the edge
gdb.edge_cache.set(id, &edge)
return edge
}
// 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()
// Update database
gdb.db.set(id: id, data: serialize_node(node))!
// Update cache
gdb.node_cache.set(id, &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()
// Update database
gdb.db.set(id: id, data: serialize_edge(edge))!
// Update cache
gdb.edge_cache.set(id, &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)!
@@ -136,8 +197,11 @@ pub fn (mut gdb GraphDB) delete_node(id u32) ! {
gdb.delete_edge(edge_ref.edge_id)!
}
// Delete the node itself
// Delete from database
gdb.db.delete(id)!
// Remove from cache
gdb.node_cache.remove(id)
}
// Deletes an edge and updates connected nodes
@@ -153,6 +217,7 @@ pub fn (mut gdb GraphDB) delete_edge(id u32) ! {
}
}
gdb.db.set(id: edge.from_node, data: serialize_node(from_node))!
gdb.node_cache.set(edge.from_node, &from_node)
// Update target node
mut to_node := deserialize_node(gdb.db.get(edge.to_node)!)!
@@ -163,9 +228,11 @@ pub fn (mut gdb GraphDB) delete_edge(id u32) ! {
}
}
gdb.db.set(id: edge.to_node, data: serialize_node(to_node))!
gdb.node_cache.set(edge.to_node, &to_node)
// Delete the edge itself
// Delete from database and cache
gdb.db.delete(id)!
gdb.edge_cache.remove(id)
}
// Queries nodes by property value
@@ -173,13 +240,30 @@ pub fn (mut gdb GraphDB) query_nodes_by_property(key string, value string) ![]No
mut nodes := []Node{}
mut next_id := gdb.db.get_next_id()!
// Process each ID up to 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
// Try to get from cache first
if cached := gdb.node_cache.get(id) {
if prop_value := cached.properties[key] {
if prop_value == value {
nodes << *cached
}
}
continue
}
// Not in cache, try to get from database
raw_data := gdb.db.get(id) or { continue }
mut node := deserialize_node(raw_data) or { continue }
// Cache the node for future use
gdb.node_cache.set(id, &node)
// Check if this node matches the query
if prop_value := node.properties[key] {
if prop_value == value {
nodes << node
}
}
}
@@ -188,13 +272,26 @@ pub fn (mut gdb GraphDB) query_nodes_by_property(key string, value string) ![]No
// 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{}
mut from_node := if cached := gdb.node_cache.get(from_id) {
*cached
} else {
node := deserialize_node(gdb.db.get(from_id)!)!
gdb.node_cache.set(from_id, &node)
node
}
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
edge_data := if cached := gdb.edge_cache.get(edge_ref.edge_id) {
*cached
} else {
mut edge := deserialize_edge(gdb.db.get(edge_ref.edge_id)!)!
gdb.edge_cache.set(edge_ref.edge_id, &edge)
edge
}
if edge_data.to_node == to_id {
edges << edge_data
}
}
@@ -203,23 +300,58 @@ pub fn (mut gdb GraphDB) get_edges_between(from_id u32, to_id u32) ![]Edge {
// 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 start_node := if cached := gdb.node_cache.get(id) {
*cached
} else {
node := deserialize_node(gdb.db.get(id)!)!
gdb.node_cache.set(id, &node)
node
}
mut connected_nodes := []Node{}
if direction in ['out', 'both'] {
for edge_ref in node.edges_out {
for edge_ref in start_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)!)!
edge_data := if cached := gdb.edge_cache.get(edge_ref.edge_id) {
*cached
} else {
mut edge := deserialize_edge(gdb.db.get(edge_ref.edge_id)!)!
gdb.edge_cache.set(edge_ref.edge_id, &edge)
edge
}
mut target_node := if cached := gdb.node_cache.get(edge_data.to_node) {
*cached
} else {
node := deserialize_node(gdb.db.get(edge_data.to_node)!)!
gdb.node_cache.set(edge_data.to_node, &node)
node
}
connected_nodes << target_node
}
}
}
if direction in ['in', 'both'] {
for edge_ref in node.edges_in {
for edge_ref in start_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)!)!
edge_data := if cached := gdb.edge_cache.get(edge_ref.edge_id) {
*cached
} else {
mut edge := deserialize_edge(gdb.db.get(edge_ref.edge_id)!)!
gdb.edge_cache.set(edge_ref.edge_id, &edge)
edge
}
mut source_node := if cached := gdb.node_cache.get(edge_data.from_node) {
*cached
} else {
node := deserialize_node(gdb.db.get(edge_data.from_node)!)!
gdb.node_cache.set(edge_data.from_node, &node)
node
}
connected_nodes << source_node
}
}
}

View File

@@ -3,10 +3,10 @@ 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 {
@@ -81,13 +81,13 @@ 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'] {
@@ -125,10 +125,10 @@ pub fn (mut gdb GraphDB) debug_edge(id u32) !string {
// 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('------')
@@ -153,13 +153,13 @@ pub fn (mut gdb GraphDB) debug_db() ! {
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})'
@@ -195,7 +195,7 @@ pub fn (mut gdb GraphDB) print_graph_from(start_id u32, visited map[u32]bool) !
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})'
@@ -206,7 +206,7 @@ pub fn (mut gdb GraphDB) print_graph_from(start_id u32, visited map[u32]bool) !
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
@@ -219,7 +219,7 @@ pub fn (mut gdb GraphDB) print_graph_from(start_id u32, visited map[u32]bool) !
}
edge_info += '}'
}
println(edge_info)
gdb.print_graph_from(edge.to_node, my_visited)!
}
@@ -229,7 +229,7 @@ pub fn (mut gdb GraphDB) print_graph_from(start_id u32, visited map[u32]bool) !
pub fn (mut gdb GraphDB) print_graph() ! {
println('\nGraph Structure')
println('===============')
mut visited := map[u32]bool{}
mut next_id := gdb.db.get_next_id()!

View File

@@ -5,13 +5,13 @@ fn test_basic_operations() ! {
// Test creating nodes with properties
mut person1_id := gdb.create_node({
'name': 'Alice',
'age': '30'
'name': 'Alice'
'age': '30'
})!
mut person2_id := gdb.create_node({
'name': 'Bob',
'age': '25'
'name': 'Bob'
'age': '25'
})!
// Test retrieving nodes
@@ -51,8 +51,8 @@ fn test_basic_operations() ! {
// Test updating node properties
gdb.update_node(person1_id, {
'name': 'Alice',
'age': '31'
'name': 'Alice'
'age': '31'
})!
updated_alice := gdb.get_node(person1_id)!
assert updated_alice.properties['age'] == '31'
@@ -86,42 +86,54 @@ fn test_complex_graph() ! {
// Create nodes representing people
mut alice_id := gdb.create_node({
'name': 'Alice',
'age': '30',
'name': 'Alice'
'age': '30'
'city': 'New York'
})!
mut bob_id := gdb.create_node({
'name': 'Bob',
'age': '25',
'name': 'Bob'
'age': '25'
'city': 'Boston'
})!
mut charlie_id := gdb.create_node({
'name': 'Charlie',
'age': '35',
'name': 'Charlie'
'age': '35'
'city': 'New York'
})!
// Create nodes representing companies
mut company1_id := gdb.create_node({
'name': 'TechCorp',
'name': 'TechCorp'
'industry': 'Technology'
})!
mut company2_id := gdb.create_node({
'name': 'FinCo',
'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, 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'})!
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')!
@@ -159,7 +171,7 @@ fn test_edge_cases() ! {
// Test node with many properties
mut large_props := map[string]string{}
for i in 0..100 {
for i in 0 .. 100 {
large_props['key${i}'] = 'value${i}'
}
large_node_id := gdb.create_node(large_props)!

99
lib/data/graphdb/search.v Normal file
View File

@@ -0,0 +1,99 @@
module graphdb
// SearchConfig represents the configuration for graph traversal search
pub struct SearchConfig {
pub mut:
types []string // List of node types to search for
max_distance f32 // Maximum distance to traverse using edge weights
}
// SearchResult represents a node found during search with its distance from start
pub struct SearchResult {
pub:
node &Node
distance f32
}
// search performs a breadth-first traversal from a start node
// Returns nodes of specified types within max_distance
pub fn (mut gdb GraphDB) search(start_id u32, config SearchConfig) ![]SearchResult {
mut results := []SearchResult{}
mut visited := map[u32]f32{} // Maps node ID to shortest distance found
mut queue := []u32{cap: 100} // Queue of node IDs to visit
// Start from the given node
queue << start_id
visited[start_id] = 0
// Process nodes in queue
for queue.len > 0 {
current_id := queue[0]
queue.delete(0)
current_distance := visited[current_id]
if current_distance > config.max_distance {
continue
}
// Get current node
current_node := gdb.get_node(current_id)!
// Add to results if node type matches search criteria
if config.types.len == 0 || current_node.node_type in config.types {
results << SearchResult{
node: &current_node
distance: current_distance
}
}
// Process outgoing edges
for edge_ref in current_node.edges_out {
edge := gdb.get_edge(edge_ref.edge_id)!
next_id := edge.to_node
// Calculate new distance using edge weight
weight := if edge.weight == 0 { f32(1) } else { f32(edge.weight) }
new_distance := current_distance + weight
// Skip if we've found a shorter path or would exceed max distance
if new_distance > config.max_distance {
continue
}
if next_distance := visited[next_id] {
if new_distance >= next_distance {
continue
}
}
// Add to queue and update distance
queue << next_id
visited[next_id] = new_distance
}
// Process incoming edges
for edge_ref in current_node.edges_in {
edge := gdb.get_edge(edge_ref.edge_id)!
next_id := edge.from_node
// Calculate new distance using edge weight
weight := if edge.weight == 0 { f32(1) } else { f32(edge.weight) }
new_distance := current_distance + weight
// Skip if we've found a shorter path or would exceed max distance
if new_distance > config.max_distance {
continue
}
if next_distance := visited[next_id] {
if new_distance >= next_distance {
continue
}
}
// Add to queue and update distance
queue << next_id
visited[next_id] = new_distance
}
}
return results
}

View File

@@ -0,0 +1,156 @@
module graphdb
fn test_search() ! {
mut gdb := new(NewArgs{
path: 'test_search.db'
reset: true
})!
// Create test nodes of different types
mut user1 := Node{
properties: {
'name': 'User 1'
}
node_type: 'user'
}
user1_id := gdb.db.set(data: serialize_node(user1))!
user1.id = user1_id
gdb.node_cache.set(user1_id, &user1)
mut user2 := Node{
properties: {
'name': 'User 2'
}
node_type: 'user'
}
user2_id := gdb.db.set(data: serialize_node(user2))!
user2.id = user2_id
gdb.node_cache.set(user2_id, &user2)
mut post1 := Node{
properties: {
'title': 'Post 1'
}
node_type: 'post'
}
post1_id := gdb.db.set(data: serialize_node(post1))!
post1.id = post1_id
gdb.node_cache.set(post1_id, &post1)
mut post2 := Node{
properties: {
'title': 'Post 2'
}
node_type: 'post'
}
post2_id := gdb.db.set(data: serialize_node(post2))!
post2.id = post2_id
gdb.node_cache.set(post2_id, &post2)
// Create edges with different weights
mut edge1 := Edge{
from_node: user1_id
to_node: post1_id
edge_type: 'created'
weight: 1
}
edge1_id := gdb.db.set(data: serialize_edge(edge1))!
edge1.id = edge1_id
gdb.edge_cache.set(edge1_id, &edge1)
mut edge2 := Edge{
from_node: post1_id
to_node: post2_id
edge_type: 'related'
weight: 2
}
edge2_id := gdb.db.set(data: serialize_edge(edge2))!
edge2.id = edge2_id
gdb.edge_cache.set(edge2_id, &edge2)
mut edge3 := Edge{
from_node: user2_id
to_node: post2_id
edge_type: 'created'
weight: 1
}
edge3_id := gdb.db.set(data: serialize_edge(edge3))!
edge3.id = edge3_id
gdb.edge_cache.set(edge3_id, &edge3)
// Update node edge references
user1.edges_out << EdgeRef{
edge_id: edge1_id
edge_type: 'created'
}
gdb.db.set(id: user1_id, data: serialize_node(user1))!
gdb.node_cache.set(user1_id, &user1)
post1.edges_in << EdgeRef{
edge_id: edge1_id
edge_type: 'created'
}
post1.edges_out << EdgeRef{
edge_id: edge2_id
edge_type: 'related'
}
gdb.db.set(id: post1_id, data: serialize_node(post1))!
gdb.node_cache.set(post1_id, &post1)
post2.edges_in << EdgeRef{
edge_id: edge2_id
edge_type: 'related'
}
post2.edges_in << EdgeRef{
edge_id: edge3_id
edge_type: 'created'
}
gdb.db.set(id: post2_id, data: serialize_node(post2))!
gdb.node_cache.set(post2_id, &post2)
user2.edges_out << EdgeRef{
edge_id: edge3_id
edge_type: 'created'
}
gdb.db.set(id: user2_id, data: serialize_node(user2))!
gdb.node_cache.set(user2_id, &user2)
// Test 1: Search for posts within distance 2
results1 := gdb.search(user1_id, SearchConfig{
types: ['post']
max_distance: 2
})!
assert results1.len == 1 // Should only find post1 within distance 2
assert results1[0].node.properties['title'] == 'Post 1'
assert results1[0].distance == 1
// Test 2: Search for posts within distance 4
results2 := gdb.search(user1_id, SearchConfig{
types: ['post']
max_distance: 4
})!
assert results2.len == 2 // Should find both posts
assert results2[0].node.properties['title'] == 'Post 1'
assert results2[1].node.properties['title'] == 'Post 2'
assert results2[1].distance == 3
// Test 3: Search for users within distance 3
results3 := gdb.search(post2_id, SearchConfig{
types: ['user']
max_distance: 3
})!
assert results3.len == 2 // Should find both users
assert results3[0].node.properties['name'] in ['User 1', 'User 2']
assert results3[1].node.properties['name'] in ['User 1', 'User 2']
// Test 4: Search without type filter
results4 := gdb.search(user1_id, SearchConfig{
types: []
max_distance: 4
})!
assert results4.len == 4 // Should find all nodes
}

View File

@@ -1,165 +1,98 @@
module graphdb
import encoding.binary
import math
import freeflowuniverse.herolib.data.encoder
const version_v1 = u8(1)
// Serializes a Node struct to bytes
fn serialize_node(node Node) []u8 {
mut data := []u8{}
pub fn serialize_node(node Node) []u8 {
mut e := encoder.new()
// Add version byte
e.add_u8(version_v1)
// Serialize node ID
e.add_u32(node.id)
// Serialize node type
e.add_string(node.node_type)
// Serialize properties
data << u32_to_bytes(u32(node.properties.len)) // Number of properties
e.add_u16(u16(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()
e.add_string(key)
e.add_string(value)
}
// Serialize outgoing edges
data << u32_to_bytes(u32(node.edges_out.len)) // Number of outgoing edges
e.add_u16(u16(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()
e.add_u32(edge.edge_id)
e.add_string(edge.edge_type)
}
// Serialize incoming edges
data << u32_to_bytes(u32(node.edges_in.len)) // Number of incoming edges
e.add_u16(u16(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()
e.add_u32(edge.edge_id)
e.add_string(edge.edge_type)
}
return data
return e.data
}
// Deserializes bytes to a Node struct
fn deserialize_node(data []u8) !Node {
if data.len < 4 {
pub fn deserialize_node(data []u8) !Node {
if data.len < 1 {
return error('Invalid node data: too short')
}
mut offset := 0
mut d := encoder.decoder_new(data)
// Check version
version := d.get_u8()!
if version != version_v1 {
return error('Unsupported version: ${version}')
}
mut node := Node{
properties: map[string]string{}
edges_out: []EdgeRef{}
edges_in: []EdgeRef{}
edges_out: []EdgeRef{}
edges_in: []EdgeRef{}
}
// Deserialize node ID
node.id = d.get_u32()!
// Deserialize node type
node.node_type = d.get_string()!
// 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
num_properties := d.get_u16()!
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
key := d.get_string()!
value := d.get_string()!
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
num_edges_out := d.get_u16()!
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
edge_id := d.get_u32()!
edge_type := d.get_string()!
node.edges_out << EdgeRef{
edge_id: edge_id
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
num_edges_in := d.get_u16()!
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
edge_id := d.get_u32()!
edge_type := d.get_string()!
node.edges_in << EdgeRef{
edge_id: edge_id
edge_id: edge_id
edge_type: edge_type
}
}
@@ -168,120 +101,65 @@ fn deserialize_node(data []u8) !Node {
}
// Serializes an Edge struct to bytes
fn serialize_edge(edge Edge) []u8 {
mut data := []u8{}
pub fn serialize_edge(edge Edge) []u8 {
mut e := encoder.new()
// Add version byte
e.add_u8(version_v1)
// Serialize edge ID
e.add_u32(edge.id)
// 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()
e.add_u32(edge.from_node)
e.add_u32(edge.to_node)
e.add_string(edge.edge_type)
e.add_u16(edge.weight)
// Serialize properties
data << u32_to_bytes(u32(edge.properties.len))
e.add_u16(u16(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()
e.add_string(key)
e.add_string(value)
}
return data
return e.data
}
// Deserializes bytes to an Edge struct
fn deserialize_edge(data []u8) !Edge {
if data.len < 12 {
pub fn deserialize_edge(data []u8) !Edge {
if data.len < 1 {
return error('Invalid edge data: too short')
}
mut offset := 0
mut d := encoder.decoder_new(data)
// Check version
version := d.get_u8()!
if version != version_v1 {
return error('Unsupported version: ${version}')
}
mut edge := Edge{
properties: map[string]string{}
}
// Deserialize edge ID
edge.id = d.get_u32()!
// 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
edge.from_node = d.get_u32()!
edge.to_node = d.get_u32()!
edge.edge_type = d.get_string()!
edge.weight = d.get_u16()!
// 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
num_properties := d.get_u16()!
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
key := d.get_string()!
value := d.get_string()!
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)
}

View File

@@ -0,0 +1,202 @@
module graphdb
fn test_node_serialization() {
// Create a test node with all fields populated
node := Node{
node_type: 'user'
properties: {
'name': 'John Doe'
'age': '30'
'email': 'john@example.com'
}
edges_out: [
EdgeRef{
edge_id: 1
edge_type: 'follows'
},
EdgeRef{
edge_id: 2
edge_type: 'likes'
},
]
edges_in: [
EdgeRef{
edge_id: 3
edge_type: 'followed_by'
},
]
}
// Serialize the node
serialized := serialize_node(node)
// Deserialize back to node
deserialized := deserialize_node(serialized) or {
assert false, 'Failed to deserialize node: ${err}'
Node{}
}
// Verify all fields match
assert deserialized.node_type == node.node_type, 'node_type mismatch'
assert deserialized.properties.len == node.properties.len, 'properties length mismatch'
for key, value in node.properties {
assert deserialized.properties[key] == value, 'property ${key} mismatch'
}
assert deserialized.edges_out.len == node.edges_out.len, 'edges_out length mismatch'
for i, edge in node.edges_out {
assert deserialized.edges_out[i].edge_id == edge.edge_id, 'edge_out ${i} id mismatch'
assert deserialized.edges_out[i].edge_type == edge.edge_type, 'edge_out ${i} type mismatch'
}
assert deserialized.edges_in.len == node.edges_in.len, 'edges_in length mismatch'
for i, edge in node.edges_in {
assert deserialized.edges_in[i].edge_id == edge.edge_id, 'edge_in ${i} id mismatch'
assert deserialized.edges_in[i].edge_type == edge.edge_type, 'edge_in ${i} type mismatch'
}
}
fn test_edge_serialization() {
// Create a test edge with all fields populated
edge := Edge{
from_node: 1
to_node: 2
edge_type: 'follows'
weight: 5
properties: {
'created_at': '2024-01-31'
'active': 'true'
}
}
// Serialize the edge
serialized := serialize_edge(edge)
// Deserialize back to edge
deserialized := deserialize_edge(serialized) or {
assert false, 'Failed to deserialize edge: ${err}'
Edge{}
}
// Verify all fields match
assert deserialized.from_node == edge.from_node, 'from_node mismatch'
assert deserialized.to_node == edge.to_node, 'to_node mismatch'
assert deserialized.edge_type == edge.edge_type, 'edge_type mismatch'
assert deserialized.weight == edge.weight, 'weight mismatch'
assert deserialized.properties.len == edge.properties.len, 'properties length mismatch'
for key, value in edge.properties {
assert deserialized.properties[key] == value, 'property ${key} mismatch'
}
}
fn test_node_serialization_empty() {
// Test with empty node
node := Node{
node_type: ''
properties: map[string]string{}
edges_out: []EdgeRef{}
edges_in: []EdgeRef{}
}
serialized := serialize_node(node)
deserialized := deserialize_node(serialized) or {
assert false, 'Failed to deserialize empty node: ${err}'
Node{}
}
assert deserialized.node_type == '', 'empty node_type mismatch'
assert deserialized.properties.len == 0, 'empty properties mismatch'
assert deserialized.edges_out.len == 0, 'empty edges_out mismatch'
assert deserialized.edges_in.len == 0, 'empty edges_in mismatch'
}
fn test_edge_serialization_empty() {
// Test with empty edge
edge := Edge{
from_node: 0
to_node: 0
edge_type: ''
weight: 0
properties: map[string]string{}
}
serialized := serialize_edge(edge)
deserialized := deserialize_edge(serialized) or {
assert false, 'Failed to deserialize empty edge: ${err}'
Edge{}
}
assert deserialized.from_node == 0, 'empty from_node mismatch'
assert deserialized.to_node == 0, 'empty to_node mismatch'
assert deserialized.edge_type == '', 'empty edge_type mismatch'
assert deserialized.weight == 0, 'empty weight mismatch'
assert deserialized.properties.len == 0, 'empty properties mismatch'
}
fn test_version_compatibility() {
// Test version checking
node := Node{
node_type: 'test'
}
mut serialized := serialize_node(node)
// Modify version byte to invalid version
serialized[0] = 99
// Should fail with version error
deserialize_node(serialized) or {
assert err.msg().contains('Unsupported version'), 'Expected version error'
return
}
assert false, 'Expected error for invalid version'
}
fn test_large_property_values() {
// Create a large string that's bigger than the slice bounds we're seeing in the error (20043)
mut large_value := ''
for _ in 0 .. 25000 {
large_value += 'x'
}
// Create a node with the large property value
node := Node{
node_type: 'test'
properties: {
'large_prop': large_value
}
}
// Serialize and deserialize
serialized := serialize_node(node)
deserialized := deserialize_node(serialized) or {
assert false, 'Failed to deserialize node with large property: ${err}'
Node{}
}
// Verify the large property was preserved
assert deserialized.properties['large_prop'] == large_value, 'large property value mismatch'
}
fn test_data_validation() {
// Test with invalid data
invalid_data := []u8{}
deserialize_node(invalid_data) or {
assert err.msg().contains('too short'), 'Expected data length error'
return
}
assert false, 'Expected error for empty data'
// Test with truncated data
node := Node{
node_type: 'test'
properties: {
'key': 'value'
}
}
serialized := serialize_node(node)
truncated := serialized[..serialized.len / 2]
deserialize_node(truncated) or {
assert err.msg().contains('Invalid'), 'Expected truncation error'
return
}
assert false, 'Expected error for truncated data'
}