Subaru
A simple property graph for Elixir.
Subaru provides a simple, fun-to-use graph database for Elixir. Define a repo, add it to your supervision tree, and start storing graphs.
Installation
Add Subaru to your mix.exs dependencies:
def deps do
[
{:subaru, "~> 0.2.0"}
]
endQuick Start
# 1. Define a graph repo
defmodule MyApp.Graph do
use Subaru,
otp_app: :my_app,
adapter: Subaru.Adapters.Mnesia
end
# 2. Add to supervision tree (application.ex)
children = [
MyApp.Graph
]
# 3. Use it!
alias MyApp.Graph
# Store vertices (maps or structs)
Graph.put(%{id: "alice", name: "Alice", type: :user})
Graph.put(%{id: "bob", name: "Bob", type: :user})
# Create edges
Graph.link("alice", :follows, "bob")
# Query the graph
import Subaru.Query
Graph.run(v("alice") |> out(:follows))
#=> [%{id: "bob", name: "Bob", type: :user}]Schemas (Optional)
Plain maps work great. For compiler-checked fields, use structs:
defmodule MyApp.User do
defstruct [:id, :name, :email]
end
Graph.put(%MyApp.User{id: "alice", name: "Alice", email: "alice@example.com"})API Reference
Basic CRUD
# Write
Graph.put(vertex) # Insert or replace vertex
Graph.put(vertex, on_conflict: :skip) # Insert only if not exists
Graph.delete(id) # Delete vertex by ID
# Read
Graph.get(id) # {:ok, vertex} | :error
Graph.get!(id) # vertex or raise
Graph.fetch(id) # vertex | nil
# Edges
Graph.link(from_id, :follows, to_id) # Create edge
Graph.link(from_id, :follows, to_id, %{since: ~D[2020-01-15]}) # With properties
Graph.unlink(from_id, :follows, to_id) # Remove edgeGraph Traversals
Traversals build query plans (inert data). Call run/1 to execute.
import Subaru.Query
# Start from a vertex
v("alice")
# Traverse outgoing edges
v("alice") |> out(:follows)
# Traverse incoming edges
v("bob") |> in_(:follows) # Who follows Bob?
# Chain traversals (friends of friends)
v("alice") |> out(:follows) |> out(:follows)
# Filter results
v("alice") |> out(:purchased) |> filter(& &1.year >= 2020)
# Limit results
v("alice") |> out(:follows) |> take(10)
# Execute
Graph.run(query)Query by Type
# Find all users
v(:user)
# Find users with properties
v(:user, name: "Alice", active: true)
# With struct pattern (type-checked)
v(%MyApp.User{name: "Alice"})Edge Access
# Get edges (returns edge maps with :from, :to, :type, and properties)
v("alice") |> edges(:follows)
#=> [%{from: "alice", to: "bob", type: :follows, since: ~D[2020-01-15]}, ...]
# Filter by edge properties, then get destinations
v("alice")
|> edges(:follows)
|> filter(& &1.since < ~D[2020-01-01])
|> to()Aggregations
Graph.count(query) # Count results
Graph.exists?(query) # Any results?
Graph.unique(query) # Remove duplicates by IDTransactions
Graph.transaction(fn ->
Graph.put(%{id: "alice", name: "Alice"})
Graph.put(%{id: "bob", name: "Bob"})
Graph.link("alice", :follows, "bob")
end)ID Generation
id = Graph.gen_id() # ULID - sortable, unique
#=> "01HQMXK8M6B1234567890ABCDE"Query Plan Inspection
Queries are just data - inspect them before running:
query = v("alice") |> out(:follows)
IO.inspect(query)
#=> %Subaru.Query{start: {:id, "alice"}, steps: [{:out, :follows}]}Configuration
Configuration is optional - sensible defaults work out of the box:
# config/config.exs
config :my_app, MyApp.Graph,
dir: ".mnesia/my_app",
storage_type: :disc_copies # or :ram_copiesDesign Principles
- Simple things should be simple
- Traversals feel like Enum/Stream
- Everything is data - queries are inspectable before execution
- Pattern matching works naturally
- Edges are first-class citizens
License
MIT