Current section
Files
Jump to
Current section
Files
lib/subaru.ex
defmodule Subaru do
@moduledoc """
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.
## Quick 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"})
## 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
"""
@doc false
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@otp_app Keyword.fetch!(opts, :otp_app)
@adapter Keyword.get(opts, :adapter, Subaru.Adapters.Mnesia)
@opts Keyword.drop(opts, [:otp_app, :adapter])
def child_spec(opts) do
Subaru.Repo.child_spec({__MODULE__, @otp_app, @adapter, Keyword.merge(@opts, opts)})
end
def start_link(opts \\ []) do
Subaru.Repo.start_link({__MODULE__, @otp_app, @adapter, Keyword.merge(@opts, opts)})
end
# Vertex CRUD
@doc """
Insert or replace a vertex.
The vertex must have an `:id` field. If a vertex with the same ID exists,
it will be replaced.
## Options
* `:on_conflict` - `:replace` (default) or `:skip`
## Examples
Graph.put(%{id: "alice", name: "Alice"})
Graph.put(%MyApp.User{id: "alice", name: "Alice"})
Graph.put(%{id: "alice", name: "Alice"}, on_conflict: :skip)
"""
@spec put(map(), keyword()) :: :ok | {:error, term()}
def put(vertex, opts \\ [])
def put(%{id: _} = vertex, opts) do
on_conflict = Keyword.get(opts, :on_conflict, :replace)
case on_conflict do
:skip ->
case get(vertex.id) do
{:ok, _} -> :ok
:error -> @adapter.put_vertex(__MODULE__, vertex)
end
:replace ->
@adapter.put_vertex(__MODULE__, vertex)
end
end
@doc """
Get a vertex by ID.
Returns `{:ok, vertex}` if found, `:error` otherwise.
## Examples
{:ok, user} = Graph.get("alice")
:error = Graph.get("nonexistent")
"""
@spec get(String.t()) :: {:ok, map()} | :error
def get(id) when is_binary(id) do
@adapter.get_vertex(__MODULE__, id)
end
@doc """
Get a vertex by ID, raising if not found.
## Examples
user = Graph.get!("alice")
"""
@spec get!(String.t()) :: map()
def get!(id) when is_binary(id) do
case get(id) do
{:ok, vertex} -> vertex
:error -> raise KeyError, key: id, term: __MODULE__
end
end
@doc """
Get a vertex by ID, returning nil if not found.
## Examples
user = Graph.fetch("alice") #=> %{id: "alice", ...} or nil
"""
@spec fetch(String.t()) :: map() | nil
def fetch(id) when is_binary(id) do
case get(id) do
{:ok, vertex} -> vertex
:error -> nil
end
end
@doc """
Delete a vertex by ID.
Also removes all edges connected to this vertex.
## Examples
:ok = Graph.delete("alice")
"""
@spec delete(String.t()) :: :ok | {:error, term()}
def delete(id) when is_binary(id) do
@adapter.delete_vertex(__MODULE__, id)
end
# Edge operations
@doc """
Create an edge between two vertices.
## Examples
Graph.link("alice", :follows, "bob")
Graph.link("alice", :follows, "bob", %{since: ~D[2020-01-15]})
"""
@spec link(String.t(), atom(), String.t(), map()) :: :ok | {:error, term()}
def link(from_id, edge_type, to_id, props \\ %{})
when is_binary(from_id) and is_atom(edge_type) and is_binary(to_id) do
@adapter.put_edge(__MODULE__, from_id, edge_type, to_id, props)
end
@doc """
Remove an edge between two vertices.
## Examples
Graph.unlink("alice", :follows, "bob")
"""
@spec unlink(String.t(), atom(), String.t()) :: :ok | {:error, term()}
def unlink(from_id, edge_type, to_id)
when is_binary(from_id) and is_atom(edge_type) and is_binary(to_id) do
@adapter.delete_edge(__MODULE__, from_id, edge_type, to_id)
end
# Query execution
@doc """
Execute a query and return results as a list.
## Examples
import Subaru.Query
Graph.run(v("alice") |> out(:follows))
#=> [%{id: "bob", ...}]
"""
@spec run(Subaru.Query.t()) :: [map()]
def run(%Subaru.Query{} = query) do
Subaru.Executor.run(query, __MODULE__, @adapter)
end
@doc """
Count the results of a query.
More efficient than `run(query) |> length()` for large result sets
when you only need the count.
## Examples
import Subaru.Query
Graph.count(v("alice") |> out(:follows))
#=> 42
"""
@spec count(Subaru.Query.t()) :: non_neg_integer()
def count(%Subaru.Query{} = query) do
query
|> Subaru.Executor.stream(__MODULE__, @adapter)
|> Enum.count()
end
@doc """
Check if a query returns any results.
## Examples
import Subaru.Query
Graph.exists?(v("alice") |> out(:follows))
#=> true
"""
@spec exists?(Subaru.Query.t()) :: boolean()
def exists?(%Subaru.Query{} = query) do
query
|> Subaru.Executor.stream(__MODULE__, @adapter)
|> Enum.any?()
end
@doc """
Get unique results from a query.
Removes duplicate vertices based on their ID.
## Examples
import Subaru.Query
# Friends of friends (may include duplicates)
Graph.unique(v("alice") |> out(:follows) |> out(:follows))
"""
@spec unique(Subaru.Query.t()) :: [map()]
def unique(%Subaru.Query{} = query) do
query
|> Subaru.Executor.stream(__MODULE__, @adapter)
|> Enum.uniq_by(fn
%{id: id} -> id
other -> other
end)
end
# Transactions
@doc """
Execute multiple operations in a transaction.
All operations inside the function are atomic - they either all succeed
or all fail together.
## Examples
Graph.transaction(fn ->
Graph.put(%{id: "alice", name: "Alice"})
Graph.put(%{id: "bob", name: "Bob"})
Graph.link("alice", :follows, "bob")
end)
"""
@spec transaction((-> result)) :: {:ok, result} | {:error, term()} when result: term()
def transaction(fun) when is_function(fun, 0) do
@adapter.transaction(__MODULE__, fun)
end
# ID Generation
@doc """
Generate a unique, sortable ID (ULID).
## Examples
id = Graph.gen_id()
#=> "01HQMXK8M6B1234567890ABCDE"
"""
@spec gen_id() :: String.t()
def gen_id do
ExULID.ULID.generate()
end
end
end
end