# 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:
```elixir
def deps do
[
{:subaru, "~> 0.2.0"}
]
end
```
## Quick Start
```elixir
# 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:
```elixir
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
```elixir
# 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 edge
```
### Graph Traversals
Traversals build query plans (inert data). Call `run/1` to execute.
```elixir
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
```elixir
# 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
```elixir
# 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
```elixir
Graph.count(query) # Count results
Graph.exists?(query) # Any results?
Graph.unique(query) # Remove duplicates by ID
```
### Transactions
```elixir
Graph.transaction(fn ->
Graph.put(%{id: "alice", name: "Alice"})
Graph.put(%{id: "bob", name: "Bob"})
Graph.link("alice", :follows, "bob")
end)
```
### ID Generation
```elixir
id = Graph.gen_id() # ULID - sortable, unique
#=> "01HQMXK8M6B1234567890ABCDE"
```
## Query Plan Inspection
Queries are just data - inspect them before running:
```elixir
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:
```elixir
# config/config.exs
config :my_app, MyApp.Graph,
dir: ".mnesia/my_app",
storage_type: :disc_copies # or :ram_copies
```
## Design 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