We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
Graph Databases in Elixir: KuzuDB Integration
Modeling entity relationships with an embedded graph database
Prismatic Engineering
Prismatic Platform
Why Graph Databases?
Relational databases excel at storing structured records with well-defined schemas. But some data is inherently about relationships. In due diligence investigations, you care less about individual entities and more about how entities connect: who owns what company, which companies share directors, how money flows between accounts, where subsidiaries overlap.
Graph databases model this naturally. Entities are nodes, relationships are edges, and queries traverse the graph to discover patterns that would require complex multi-table JOINs in SQL.
KuzuDB: An Embedded Graph Database
KuzuDB is an embedded, columnar graph database engine written in C++. Unlike Neo4j or ArangoDB, KuzuDB runs in-process -- no separate server, no network overhead, no operational complexity. It is to graph databases what SQLite is to relational databases.
Key characteristics:
The prismatic_storage_kuzudb Adapter
The adapter wraps KuzuDB in the platform's storage trait system:
defmodule PrismaticStorageKuzudb do
@behaviour PrismaticStorageCore.StorageTrait
def init(config) do
db_path = Keyword.fetch!(config, :db_path)
{:ok, conn} = Kuzu.Database.open(db_path)
{:ok, %{conn: conn, db_path: db_path}}
end
def query(state, cypher_query, params) do
Kuzu.Connection.execute(state.conn, cypher_query, params)
end
end
The adapter implements the standard StorageTrait interface (init/1, get/2, put/3, delete/2, query/2) while exposing KuzuDB-specific capabilities through an extended API.
Entity Relationship Modeling
The DD (Due Diligence) system uses KuzuDB to model entity relationships. The schema defines node and edge types:
Node Types
|------|-----------|---------|
Edge Types
|------|------|----|-----------|
Graph Traversal Queries
The power of graph databases shines in traversal queries -- finding paths and patterns across relationships.
Finding Connected Entities
// Find all companies connected to a person within 3 hops
MATCH (p:Person {name: $name})-[*1..3]-(c:Company)
RETURN DISTINCT c.name, c.ico
Detecting Circular Ownership
// Find circular ownership chains
MATCH path = (c1:Company)-[:OWNS*2..5]->(c1)
RETURN path
Identifying Shared Directors
// Find people who direct multiple companies
MATCH (p:Person)-[:DIRECTS]->(c:Company)
WITH p, collect(c) AS companies
WHERE size(companies) > 1
RETURN p.name, [c IN companies | c.name] AS directed_companies
Money Flow Analysis
// Trace money flow from source company through intermediaries
MATCH path = (source:Company {name: $source})-[:TRANSACTS_WITH*1..4]->(dest:Company)
WHERE dest.name = $destination
RETURN path, [r IN relationships(path) | r.amount] AS amounts
Cypher Query Integration
The adapter provides a safe Cypher query interface that prevents injection through parameterized queries:
def find_connected_entities(person_name, max_depth) do
query = """
MATCH (p:Person {name: $name})-[*1..#{max_depth}]-(e)
RETURN labels(e) AS type, properties(e) AS props
LIMIT 100
"""
PrismaticStorageKuzudb.query(query, %{name: person_name})
end
Note the use of $name parameter binding instead of string interpolation. The max_depth is an integer used in the Cypher pattern syntax (not a string value), so it is safe to interpolate.
The DD Relationship Explorer
The relationship explorer is a LiveView component at /hub/dd/graph that visualizes the entity graph:
Features
Performance
KuzuDB's columnar storage provides excellent query performance for graph traversals:
Single-hop neighbors: < 5ms (up to 1000 results)
3-hop path finding: < 50ms (typical case graphs)
Circular ownership check: < 100ms (up to 10,000 nodes)
Full graph export: < 500ms (typical case, ~5000 nodes)
For comparison, equivalent SQL queries with multiple self-JOINs typically take 5-10x longer and become exponentially slower as hop depth increases.
Schema Migration
KuzuDB schema changes are managed through the adapter's migration system:
def migrate(state, :add_domain_nodes) do
Kuzu.Connection.execute(state.conn,
"CREATE NODE TABLE Domain(fqdn STRING, registrar STRING, PRIMARY KEY(fqdn))"
)
Kuzu.Connection.execute(state.conn,
"CREATE REL TABLE OPERATES(FROM Company TO Domain, since DATE)"
)
end
Migrations are idempotent and tracked in a metadata table within the KuzuDB database itself.
Relationships are data. Graph databases make that data queryable.