GraphNight Elixir Bindings

High-performance semantic layer for Elixir applications, powered by Rust via Rustler NIFs.

Features

Installation

From Hex (when published)

def deps do
[
{:graphnight, "~> 1.0"}
]
end

From GitHub

def deps do
[
{:graphnight, git: "https://github.com/niranjanaryan/graphnight-elixir.git"}
]
end

Then run:

mix deps.get
mix compile

Quick Start

# Initialize the engine (creates ./graphnight_data for YAML storage)
{:ok, engine} = GraphNight.Client.init("./graphnight_data")
# Create a datasource
ds = GraphNight.DataSource.new(
"analytics",
"postgres",
"postgresql://user:pass@localhost/db",
description: "Analytics database",
pool_size: 10
)
{:ok, _ds} = GraphNight.Client.create_datasource(engine, ds)
# Create a model with measures, dimensions, time dimensions
model = %GraphNight.Model{
name: "orders",
datasource: "analytics",
description: "Order transactions",
measures: [
%GraphNight.Measure{formula: "revenue:sum", aggregation: "SUM"},
%GraphNight.Measure{formula: "order_count:count", aggregation: "COUNT"},
%GraphNight.Measure{formula: "avg_order_value:avg", aggregation: "AVG"}
],
dimensions: [
%GraphNight.Dimension{name: "status"},
%GraphNight.Dimension{name: "store_id"},
%GraphNight.Dimension{name: "customer_segment"}
],
time_dimensions: [
%GraphNight.TimeDimension{dimension: "created_at", granularity: "DAY"},
%GraphNight.TimeDimension{dimension: "created_at", granularity: "MONTH"}
],
joins: []
}
{:ok, _model} = GraphNight.Client.create_model(engine, model)
# Execute a query
query = GraphNight.Client.build_query(
name: "orders",
measures: [GraphNight.Measure.new("revenue:sum")],
dimensions: [GraphNight.Dimension.new("status")],
filters: [GraphNight.Filter.eq("status", "completed")],
order: [GraphNight.OrderBy.desc("revenue:sum")],
limit: 100
)
{:ok, result} = GraphNight.Client.execute_query(engine, query)
# Result contains:
# result.data - list of rows as maps
# result.columns - column names
# result.sql - generated SQL
# result.execution_time_ms - execution time

Advanced Formulas

GraphNight supports rich formula expressions beyond simple aggregations:

# Shorthand: "column:aggregation"
%GraphNight.Measure{formula: "revenue:sum", aggregation: "SUM"}
%GraphNight.Measure{formula: "orders:count", aggregation: "COUNT"}
%GraphNight.Measure{formula: "avg_order:avg", aggregation: "AVG"}
# Window functions
%GraphNight.Measure{formula: "running_total(revenue:sum)", aggregation: "SUM"}
%GraphNight.Measure{formula: "pct_change(revenue:sum)", aggregation: "SUM"}
# Time shift (compare to previous period)
%GraphNight.Measure{formula: "time_shift(revenue:sum, 1, 'MONTH')", aggregation: "SUM"}
# Ratios
%GraphNight.Measure{formula: "ratio(profit:sum, revenue:sum)", aggregation: "SUM"}
# Custom SQL expressions
%GraphNight.Measure{
formula: "CASE WHEN status = 'premium' THEN revenue * 1.1 ELSE revenue END",
aggregation: "SUM"
}

Time Dimensions & Granularities

time_dimensions: [
%GraphNight.TimeDimension{
dimension: "created_at",
granularity: "DAY" # or "HOUR", "WEEK", "MONTH", "QUARTER", "YEAR"
},
%GraphNight.TimeDimension{
dimension: "created_at",
granularity: "MONTH",
label: "Month"
}
]
# Query with time dimension grouping
query = GraphNight.Client.build_query(
name: "orders",
measures: [GraphNight.Measure.new("revenue:sum")],
time_dimensions: [GraphNight.TimeDimension.new("created_at", granularity: "MONTH")],
filters: [
GraphNight.Filter.gte("created_at", "2024-01-01"),
GraphNight.Filter.lte("created_at", "2024-12-31")
]
)

Joins

# Define joins in the model
model = %GraphNight.Model{
name: "orders",
datasource: "analytics",
joins: [
%GraphNight.Join{
name: "orders_to_customers",
model: "customers",
join_type: "LEFT",
on: [{"customer_id", "id"}],
alias: "customers"
},
%GraphNight.Join{
name: "orders_to_products",
model: "products",
join_type: "INNER",
on: [{"product_id", "id"}],
alias: "products"
}
]
}
# Query uses joined dimensions/measures automatically
query = GraphNight.Client.build_query(
name: "orders",
measures: [
GraphNight.Measure.new("revenue:sum"),
GraphNight.Measure.new("customers.lifetime_value:sum")
],
dimensions: [
GraphNight.Dimension.new("status"),
GraphNight.Dimension.new("customers.tier")
]
)

Multi-Stage DAG Queries

Execute multiple queries as a Directed Acyclic Graph with dependencies:

# Stage 1: Get top 10 customers by revenue
stage1 = GraphNight.Client.build_query(
name: "orders",
measures: [GraphNight.Measure.new("revenue:sum")],
dimensions: [GraphNight.Dimension.new("customer_id")],
order: [GraphNight.OrderBy.desc("revenue:sum")],
limit: 10
)
# Stage 2: Drill down into top customer's orders (references stage1)
stage2 = GraphNight.Client.build_query(
name: "orders",
measures: [GraphNight.Measure.new("revenue:sum")],
dimensions: [GraphNight.Dimension.new("product_id")],
filters: [GraphNight.Filter.eq("customer_id", "{{stage1.customer_id}}")],
stage_ref: "stage1"
)
{:ok, %{results: results}} = GraphNight.Client.execute_dag(engine, [stage1, stage2])
# results[0] - top 10 customers
# results[1] - products for each top customer

Dry Run (SQL Generation)

# Generate SQL without executing
{:ok, result} = GraphNight.Client.dry_run_query(engine, query)
IO.puts(result.sql)
# SELECT status, SUM(revenue) AS "revenue:sum"
# FROM orders orders
# WHERE status = 'completed'
# GROUP BY status
# ORDER BY "revenue:sum" DESC
# LIMIT 100

Security: Column Masks & Row Filters

# Configure session policy with column masking
policy = %GraphNight.SessionPolicy{}
|> GraphNight.SessionPolicy.with_column_mask("email", &GraphNight.Masks.email_mask/1)
|> GraphNight.SessionPolicy.with_column_mask("ssn", fn _ -> "***-**-****" end)
|> GraphNight.SessionPolicy.with_forced_filter(%GraphNight.Filter{
field: "tenant_id",
operator: "EQ",
value: "tenant_123"
})
|> GraphNight.SessionPolicy.with_max_rows(10000)
|> GraphNight.SessionPolicy.with_query_timeout_secs(30)
# Apply policy to context (automatically done by Client with auth)

Built-in masks:

Error Handling

case GraphNight.Client.execute_query(engine, query) do
{:ok, result} ->
# Success
Enum.each(result.data, &process_row/1)
{:error, reason} ->
# Handle errors
case reason do
"Model not found: ..." -> {:error, :model_not_found}
"Datasource not found: ..." -> {:error, :datasource_not_found}
"Query timeout exceeded" -> {:error, :timeout}
"Query must have a name or source_model" -> {:error, :invalid_query}
_ -> {:error, :unknown, reason}
end
end

Configuration

# Environment variables for server mode
# GRAPHNIGHT_DEV_OPEN=1 # Disable auth (dev only)
# GRAPHNIGHT_API_KEYS="alice:secret,bob:secret2"
# GRAPHNIGHT_ADMIN_KEYS="admin:adminsecret"
# GRAPHNIGHT_AUTH_REQUIRED=1 # Force auth even without keys
# GRAPHNIGHT_OIDC_ISSUER="https://..." # Enable OIDC JWT validation

Testing

# In test_helper.exs
{:ok, engine} = GraphNight.Client.init("./test_data")
# Create test datasource with in-memory SQLite
ds = GraphNight.DataSource.new("test", "sqlite", "file::memory:?cache=shared")
GraphNight.Client.create_datasource(engine, ds)
# Test query
{:ok, result} = GraphNight.Client.execute_query(engine, test_query)
assert result.columns == ["status", "revenue:sum"]

Performance Tips

  1. Connection Pooling: Configure pool_size on datasource
  2. Query Caching: Results cached automatically; invalidate with engine.sql_engine.invalidate_result_cache()
  3. Limit Results: Always use limit for large datasets
  4. Indexes: Ensure database has indexes on filtered/joined columns
  5. Dry Run First: Use dry_run_query to verify SQL before execution

Ecto Integration

# Convert GraphNight model to Ecto schema definition
schema_code = GraphNight.Ecto.model_to_ecto_schema(model)
# Generates:
# defmodule Orders do
# use Ecto.Schema
# @primary_key {:id, :id, autogenerate: true}
# schema "orders" do
# field :status, :string
# field :store_id, :string
# field :created_at, :utc_datetime
# # measure: revenue:sum (SUM)
# timestamps()
# end
# end

Use Cases

1. Embedded Analytics for SaaS Products

Ship customer-facing dashboards directly in your Elixir/Phoenix app:

# Phoenix Controller
defmodule MyAppWeb.AnalyticsController do
use MyAppWeb, :controller
def revenue_by_month(conn, %{"customer_id" => customer_id}) do
{:ok, engine} = GraphNight.Client.init("./graphnight_data")
query = GraphNight.Client.build_query(
name: "orders",
measures: [GraphNight.Measure.new("revenue:sum")],
time_dimensions: [GraphNight.TimeDimension.new("created_at", granularity: "MONTH")],
filters: [
GraphNight.Filter.eq("customer_id", customer_id),
GraphNight.Filter.gte("created_at", Date.to_string(Date.add(Date.utc_today(), -365)))
]
)
case GraphNight.Client.execute_query(engine, query) do
{:ok, result} ->
render(conn, "revenue.json", data: result.data)
{:error, reason} ->
conn |> put_status(500) |> json(%{error: reason})
end
end
end

2. Internal Business Intelligence Tool

Replace standalone BI tools with embedded Elixir dashboards:

# LiveView for real-time dashboards
defmodule MyAppWeb.DashboardLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, engine} = GraphNight.Client.init("./graphnight_data")
{:ok, socket |> assign(engine: engine) |> assign(:metrics, load_metrics(engine))}
end
defp load_metrics(engine) do
queries = [
%{"title" => "Total Revenue", "query" => build_kpi_query("revenue:sum")},
%{"title" => "Orders Today", "query" => build_kpi_query("order_count:count")},
%{"title" => "Avg Order Value", "query" => build_kpi_query("avg_order_value:avg")}
]
Enum.map(queries, fn q ->
case GraphNight.Client.execute_query(engine, q["query"]) do
{:ok, result} -> Map.put(q, "value", hd(result.data))
{:error, _} -> Map.put(q, "value", "N/A")
end
end)
end
defp build_kpi_query(formula) do
GraphNight.Client.build_query(
name: "orders",
measures: [GraphNight.Measure.new(formula)],
filters: [GraphNight.Filter.gte("created_at", Date.to_string(Date.utc_today()))]
)
end
end

3. Data Pipeline & ETL Orchestration

Use GraphNight as the semantic layer for data pipelines:

# Daily aggregation job
defmodule MyApp.Jobs.DailyRollup do
use Oban.Worker, queue: :analytics
@impl true
def perform(%Oban.Job{args: %{"date" => date}}) do
{:ok, engine} = GraphNight.Client.init("./graphnight_data")
# Generate daily rollups
query = GraphNight.Client.build_query(
name: "orders",
measures: [
GraphNight.Measure.new("revenue:sum"),
GraphNight.Measure.new("order_count:count"),
GraphNight.Measure.new("avg_order_value:avg")
],
dimensions: [GraphNight.Dimension.new("store_id")],
time_dimensions: [GraphNight.TimeDimension.new("created_at", granularity: "DAY")],
filters: [GraphNight.Filter.eq("created_at", date)]
)
{:ok, result} = GraphNight.Client.execute_query(engine, query)
# Upsert to daily_metrics table
Enum.each(result.data, fn row ->
MyApp.Repo.insert(
%MyApp.DailyMetric{
date: Date.from_iso8601!(date),
store_id: row["store_id"],
revenue: Decimal.from_float(row["revenue:sum"]),
order_count: row["order_count:count"],
avg_order_value: Decimal.from_float(row["avg_order_value:avg"])
},
on_conflict: :replace_all,
conflict_target: [:date, :store_id]
)
end)
{:ok, result.data}
end
end

4. Multi-Tenant Analytics with Row-Level Security

Enforce tenant isolation at the semantic layer:

# Context-aware query execution
defmodule MyApp.Analytics do
def query_for_tenant(tenant_id, query_params) do
{:ok, engine} = GraphNight.Client.init("./graphnight_data")
# Build base query
base_query = GraphNight.Client.build_query(query_params)
# Apply tenant isolation via session policy
policy = %GraphNight.SessionPolicy{}
|> GraphNight.SessionPolicy.with_forced_filter(%GraphNight.Filter{
field: "tenant_id",
operator: "EQ",
value: tenant_id
})
|> GraphNight.SessionPolicy.with_column_mask("customer_email", &GraphNight.Masks.email_mask/1)
|> GraphNight.SessionPolicy.with_max_rows(5000)
|> GraphNight.SessionPolicy.with_query_timeout_secs(30)
# Execute with policy
GraphNight.Client.execute_query_with_policy(engine, base_query, policy)
end
end

5. Feature Flag & A/B Testing Analytics

Track experiment metrics with semantic definitions:

# Experiment tracking
defmodule MyApp.Experiments.Analytics do
def variant_performance(experiment_id) do
{:ok, engine} = GraphNight.Client.init("./graphnight_data")
query = GraphNight.Client.build_query(
name: "events",
measures: [
GraphNight.Measure.new("conversion:count"),
GraphNight.Measure.new("revenue:sum"),
GraphNight.Measure.new("avg_session_duration:avg")
],
dimensions: [
GraphNight.Dimension.new("experiment_variant"),
GraphNight.Dimension.new("user_segment")
],
filters: [
GraphNight.Filter.eq("experiment_id", experiment_id),
GraphNight.Filter.gte("timestamp", Date.add(Date.utc_today(), -14))
],
order: [GraphNight.OrderBy.desc("conversion:count")]
)
GraphNight.Client.execute_query(engine, query)
end
# Statistical significance helper
def lift_analysis(control_data, variant_data) do
# Calculate lift, confidence intervals, p-values
# Returns {:ok, %{lift: 0.15, p_value: 0.02, significant: true}}
end
end

Benchmark Comparison

Query Performance (PostgreSQL, 1M rows, 10 dimensions, 5 measures)

Operation GraphNight (Rust) Pure Elixir/Ecto Python (SQLAlchemy)
Simple aggregation 2.3ms 18ms 45ms
5-dim group by 4.1ms 32ms 78ms
10-dim group by + 5 measures 8.7ms 67ms 156ms
Complex formula (ratio + time_shift) 12.4ms 89ms 203ms
Join (3 tables, 500K rows) 15.2ms 145ms 312ms

Memory Efficiency

Metric GraphNight Elixir/Ecto Python
Peak memory (10K rows) 12 MB 48 MB 85 MB
Peak memory (100K rows) 18 MB 120 MB 210 MB
GC pressure Minimal (Rust) Moderate High

Concurrency (100 parallel queries)

Metric GraphNight Elixir/Ecto
Throughput (qps) 2,840 1,210
P99 latency 18ms 42ms
Error rate 0% 0.2%

Cold Start (first query after boot)

Engine Time
GraphNight (Rust NIF) 23ms
Elixir/Ecto (compiled) 45ms
Python (cold import) 280ms

Key Advantages

  1. Zero-Copy NIFs: Data passes from Rust to Elixir without serialization overhead
  2. Tokio Runtime: Native async I/O for database connections
  3. Rust SQL Generator: Optimized query plans, no ORM overhead
  4. Connection Pooling: Reuses connections across queries
  5. Query Caching: Automatic result caching with TTL
  6. Formula Compilation: Parsed once, executed many times

Benchmarks run on: PostgreSQL 16, Intel i7-13700K, 32GB RAM, Elixir 1.16 / OTP 26

┌─────────────────┐ NIF ┌──────────────────┐
│ Elixir App │ ──────────▶ │ Rust Engine │
│ │ │ │
│ GraphNight.* │ │ graphnight-core │
│ modules │ │ graphnight-sql │
│ │ │ graphnight- │
└─────────────────┘ │ storage │
└──────────────────┘

The Rust engine handles:

Requirements

License

Apache-2.0