...
This commit is contained in:
170
lib/data/graphdb/README.md
Normal file
170
lib/data/graphdb/README.md
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()!
|
||||
|
||||
|
||||
@@ -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
99
lib/data/graphdb/search.v
Normal 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: ¤t_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
|
||||
}
|
||||
156
lib/data/graphdb/search_test.v
Normal file
156
lib/data/graphdb/search_test.v
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
202
lib/data/graphdb/serialization_test.v
Normal file
202
lib/data/graphdb/serialization_test.v
Normal 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'
|
||||
}
|
||||
Reference in New Issue
Block a user