README.md

# 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