MnesiaEx
A modern, functional wrapper for Erlang/OTP's Mnesia database
โ ๏ธ Beta Release (v0.1.0): API is functional and tested but may evolve based on community feedback. Use in production with thorough testing.
MnesiaEx brings the power of Mnesia to Elixir with a clean, functional API. Built with category theory principles and monadic composition, it provides an idiomatic Elixir experience for distributed databases with features like automatic TTL, auto-increment counters, schema migrations, dirty operations, and JSON/CSV export.
Why MnesiaEx?
Mnesia is a distributed database built into Erlang/OTP. MnesiaEx provides an Elixir-friendly API:
- ๐ฏ Elixir-First API - Idiomatic Elixir patterns and conventions
- ๐ Functional & Pure - Built with monads and composable functions
- โก Zero External Dependencies - Uses only Mnesia (no Postgres, MySQL, etc.)
- ๐ Distributed by Default - Multi-node clustering out of the box
Use Cases
- ๐ Session storage and caching
- ๐ฎ Real-time multiplayer game state
- ๐ Distributed counters and metrics
- ๐ Feature flags and configuration
- ๐ Small to medium datasets (< 2GB per node)
- ๐ Edge computing and embedded systems
Features
- ๐ฏ Smart Auto-Transaction - Functions auto-detect if inside transaction (UNIQUE!)
- โจ Table-Scoped Modules - Clean API without repeating table names
- ๐ Auto-Increment - Built-in counter support for IDs
- โฐ TTL Support - Automatic record expiration and cleanup
- ๐พ Backup & Restore - Export/import to JSON, CSV, or Erlang terms
- ๐ก Event Subscriptions - Real-time notifications on data changes
- ๐ Query Builder - Composable conditions for data filtering
- โก Dirty Operations - 10x faster operations when ACID isn't critical
- ๐ง Schema Migrations - Transform table structure with data migration
- ๐ Distribution - Multi-node replication and fault tolerance
- ๐ System Info - Cluster monitoring and diagnostics
- ๐ฏ Type Safety - Full typespecs and dialyzer support
- ๐งช Pure Functional - No side effects, full monad support
Installation
Add to your mix.exs:
def deps do
[
{:mnesia_ex, "~> 0.1"}
]
endโ ๏ธ Beta Release: This is a beta release (v0.1.x). The API may change before v1.0.0.
For production use, consider pinning to a specific minor version:
{:mnesia_ex, "~> 0.1.0"} # Only allows 0.1.x patchesExamples
Check out the examples/ directory for complete, runnable examples:
- 01_basic_crud.exs - Write, read, update, delete, queries
- 02_ttl.exs - Automatic record expiration
- 03_counters.exs - Auto-increment IDs and distributed counters
- 04_events.exs - Real-time event subscriptions
- 05_backup_export.exs - Backup to JSON/CSV/Erlang
- 06_complete_app.exs - Full blog system demo
- 07_transactions.exs - Composable transactions
Run any example with:
elixir examples/01_basic_crud.exs
All examples use Mix.install/1 - no setup required!
๐ Note: Examples are included in the package and available in HexDocs. View them in your editor or browse on GitHub.
Quick Start
1. Initialize Schema (first time only)
# Create Mnesia schema on disk
MnesiaEx.Schema.create([node()])
MnesiaEx.start()2. Define Your Table Module
defmodule MyApp.Users do
use MnesiaEx, table: :users
def setup do
create([
attributes: [:id, :name, :email, :age],
index: [:email],
disc_copies: [node()]
])
end
end3. Create and Use Tables
# Create table (first time)
MyApp.Users.setup()
# Functions with ! return value directly (in transactions)
user = MyApp.Users.write!(%{id: 1, name: "Alice", email: "alice@example.com", age: 30})
user = MyApp.Users.read!(1)
user = MyApp.Users.update!(1, %{age: 31})
deleted = MyApp.Users.delete!(1)
# Functions without ! return tuples (composable)
{:ok, user} = MyApp.Users.write(%{id: 2, name: "Bob", email: "bob@example.com"})
{:error, :not_found} = MyApp.Users.read(999)
# Select and all_keys return lists directly (no ! needed, lists are always valid)
users = MyApp.Users.select([{:age, :>=, 18}])
all_users = MyApp.Users.select([])
keys = MyApp.Users.all_keys()
# Compose your own transactions
{:ok, {user, post}} = MnesiaEx.transaction(fn ->
{:ok, user} = MyApp.Users.write(%{id: 3, name: "Carol"})
{:ok, post} = MyApp.Posts.write(%{user_id: user.id, title: "Hello"})
{user, post}
end)API Convention - Smart Auto-Transaction Detection
MnesiaEx has a unique feature: functions automatically detect if they're inside a transaction and adapt their behavior. This makes the API extremely comfortable to use while maintaining full control when needed.
Functions with ! (convenience, always transactional):
- โ Auto-transaction + return value directly
- โ Raise exception on error
- ๐ Best for single operations
user = MyApp.Users.write!(%{id: 1, name: "Alice"})
# Auto-transaction + returns value directlyFunctions without ! (smart, auto-detecting):
- โ Standalone: Creates transaction automatically
-
โ
Inside
MnesiaEx.transaction: Uses existing transaction (no double-wrap) -
โ
Return
{:ok, value} | {:error, reason} - ๐ Works everywhere, adapts automatically
# Standalone - auto-transaction
{:ok, user} = MyApp.Users.write(%{id: 1, name: "Alice"})
# Internally creates a transaction automatically
# Inside manual transaction - no double-transaction
{:ok, {user, post}} = MnesiaEx.transaction(fn ->
{:ok, user} = MyApp.Users.write(%{id: 1, name: "Alice"}) # Detects transaction โ doesn't create another
{:ok, post} = MyApp.Posts.write(%{user_id: user.id}) # Detects transaction โ doesn't create another
{user, post} # Both succeed or both fail atomically
end)dirty_* functions (fast, no ACID guarantees):
- โ No transaction overhead (10x faster)
- โ Return tuples
- โ No atomic guarantees
- ๐ Use when speed > consistency
{:ok, user} = MyApp.Users.dirty_write(%{id: 1, name: "Alice"})MnesiaEx.sync_transaction/1 (distributed consistency):
- โ Waits for commit on ALL nodes
- โ Stronger consistency guarantees
- โ Higher latency
- ๐ Use for critical distributed operations
{:ok, transfer} = MnesiaEx.sync_transaction(fn ->
{:ok, _} = Accounts.update(from_id, %{balance: balance1 - amount})
{:ok, _} = Accounts.update(to_id, %{balance: balance2 + amount})
:transfer_complete
end)Why This Is Unique
Unlike other Mnesia wrappers, MnesiaEx's auto-transaction detection means:
- โ Functions work standalone (no manual transaction needed)
- โ Functions compose inside transactions (no double-wrapping)
- โ Best of both worlds: convenience + control
- โ You don't have to think about transactions unless composing
Exploring the Documentation
All modules and functions include inline documentation. Use h in IEx to explore:
# Start IEx
iex -S mix
# Get help on modules
iex> h MnesiaEx
iex> h MnesiaEx.Query
iex> h MnesiaEx.Table
# Get help on specific functions
iex> h MnesiaEx.Backup.backup/1
iex> h MnesiaEx.Counter.get_next_id/2
iex> h MnesiaEx.TTL.write/3
iex> h MnesiaEx.Query.select/3
iex> h MnesiaEx.Schema.create/1
# Get help on generated functions (after using MnesiaEx)
iex> h MyApp.Users.write/2
iex> h MyApp.Users.select/2
iex> h MyApp.Users.dirty_write/2Requirements
-
Elixir
~> 1.15 -
Erlang/OTP
~> 25
Table of Contents
Configuration
Configure MnesiaEx in your config/config.exs:
config :mnesia_ex,
# Directory paths
backup_dir: "priv/backups",
export_dir: "priv/exports",
# System table names
counter_table: :mnesia_counters,
ttl_table: :mnesia_ttl,
# TTL settings
cleanup_interval: {5, :minutes}, # How often to clean expired records
auto_cleanup: true, # Enable automatic cleanup
ttl_persistence: true # Persist TTL table to diskConfiguration Reference
| Option | Type | Default | Description |
|---|---|---|---|
backup_dir | String.t() | "priv/backups" | Directory for backup files |
export_dir | String.t() | "priv/exports" | Directory for exported files |
counter_table | atom() | :mnesia_counters | System table for auto-increment |
ttl_table | atom() | :mnesia_ttl | System table for TTL tracking |
cleanup_interval | tuple() | integer() | {5, :minutes} | TTL cleanup frequency |
auto_cleanup | boolean() | true | Enable/disable automatic TTL cleanup |
ttl_persistence | boolean() | true | Persist TTL data to disk |
Usage Guide
Schema Management
Before using Mnesia, you need to create a schema on disk. This is a one-time setup:
# In production, do this during deployment setup
MnesiaEx.Schema.create([node()])
# For distributed setup
nodes = [:"node1@host", :"node2@host"]
MnesiaEx.Schema.create(nodes)
# Check if schema exists
MnesiaEx.Schema.exists?([node()])
# => true
# Get schema information
{:ok, info} = MnesiaEx.Schema.info()
# => %{
# directory: "/var/mnesia/data",
# nodes: [:"myapp@host"],
# tables: [:users, :sessions],
# version: "4.21.0",
# running: true
# }
# Delete schema (careful!)
MnesiaEx.Schema.delete([node()])Note: Schema creation is typically done once during initial deployment or in
mixtasks, not in your application startup code.
Table-Scoped Modules
The recommended way to use MnesiaEx is through table-scoped modules. This gives you a clean, focused API:
defmodule MyApp.Users do
use MnesiaEx, table: :users
def setup do
create([
attributes: [:id, :name, :email, :role],
index: [:email],
disc_copies: [node()]
])
end
# Add custom queries
def admins do
select([{:role, :==, :admin}])
end
def active_users do
select([{:status, :==, :active}])
end
endAvailable Functions:
All standard CRUD operations are automatically injected:
write/2,write!/2- Insert or updateread/1,read!/1- Read by keydelete/1,delete!/1- Delete by keyselect/2- Query with conditions (returns list directly)update/3,update!/3- Update attributeslist/0,list!/0- Get all recordsget_by/2,get_by!/2- Find by field value- And many more... (see full API)
Queries and Conditions
Build queries with conditions:
# Simple condition
{:ok, adults} = MyApp.Users.select([{:age, :>, 18}])
# Multiple conditions (AND - implicit)
{:ok, active_adults} = MyApp.Users.select([
{:age, :>=, 18},
{:status, :==, :active}
])
# Find by specific field
{:ok, user} = MyApp.Users.get_by(:email, "alice@example.com")
# Get all records (returns list directly)
all_users = MyApp.Users.select([])
# Get all keys (lightweight, returns list directly)
keys = MyApp.Users.all_keys()
# => [1, 2, 3, 4, 5]Supported operators:
:==- Equal:"/="- Not equal:>- Greater than:<- Less than:>=- Greater than or equal:<=- Less than or equal
Note: Multiple conditions in a list use AND logic implicitly.
Basic Operations
# Create
{:ok, user} = MyApp.Users.write(%{
id: 1,
name: "Alice",
email: "alice@example.com"
})
# Read
{:ok, user} = MyApp.Users.read(1)
# Update
{:ok, updated_user} = MyApp.Users.update(1, %{name: "Alice Updated"})
# Delete
{:ok, deleted_user} = MyApp.Users.delete(1)
# Batch operations (return lists directly)
users = MyApp.Users.batch_write([
%{id: 1, name: "Alice"},
%{id: 2, name: "Bob"},
%{id: 3, name: "Charlie"}
])
deleted_users = MyApp.Users.batch_delete([1, 2, 3])TTL (Time To Live)
Automatic record expiration for sessions, caches, and temporary data:
defmodule MyApp.Sessions do
use MnesiaEx, table: :sessions
end
# Write with automatic expiration
MyApp.Sessions.write_with_ttl(%{id: "abc123", data: "..."}, {1, :hour})
# Check remaining time
{:ok, ttl_ms} = MyApp.Sessions.get_remaining("abc123")
# TTL cleanup runs automatically in background
# Configure interval in config.exsTime units::milliseconds, :seconds, :minutes, :hours, :days, :weeks
Auto-Increment Counters
Automatic ID generation for your records:
defmodule MyApp.Orders do
use MnesiaEx, table: :orders
end
# Get next ID
{:ok, next_id} = MyApp.Orders.get_next_id(:order_number)
{:ok, order} = MyApp.Orders.write(%{
order_number: next_id,
user_id: 123,
total: 99.99
})
# Or let it auto-increment on write (if configured in table creation)
# Just omit the ID field and it will be assigned automaticallyEvent Subscriptions
Monitor table changes in real-time:
defmodule MyApp.UserMonitor do
use GenServer
alias MnesiaEx.Events
def init(_) do
{:ok, :subscribed} = MyApp.Users.subscribe() # or subscribe(:detailed) for more info
{:ok, %{}}
end
def handle_info(event, state) do
case MyApp.Users.parse_event(event) do
{:write, :users, record} ->
IO.puts("User created/updated: #{inspect(record)}")
{:delete, :users, id} ->
IO.puts("User deleted: #{id}")
_ ->
:ok
end
{:noreply, state}
end
endBackup & Restore
Easy database backup and migration:
# Backup entire database
{:ok, file} = MnesiaEx.Backup.backup("backup.mnesia")
# Restore from backup
{:ok, _} = MnesiaEx.Backup.restore("backup.mnesia")
# Export table to JSON/CSV
{:ok, _} = MnesiaEx.Backup.export_table(:users, "users.json", :json)
{:ok, _} = MnesiaEx.Backup.export_table(:users, "users.csv", :csv)
# Import from file (format auto-detected by extension)
{:ok, :imported} = MnesiaEx.Backup.import_table(:users, "users.json")Distributed Tables
Run across multiple nodes with automatic replication:
defmodule MyApp.Sessions do
use MnesiaEx, table: :sessions
def setup_distributed do
create([
attributes: [:id, :user_id, :data],
disc_copies: [node() | Node.list()] # All connected nodes
])
end
end
# Add/remove nodes dynamically
{:ok, result} = MyApp.Sessions.add_table_copy(:"node2@host", :disc_copies)
{:ok, result} = MyApp.Sessions.remove_table_copy(:"node2@host")
# Check cluster status
MnesiaEx.system_info(:running_db_nodes)
# => [:"node1@host", :"node2@host"]
MnesiaEx.system_info(:tables)
# => [:users, :sessions, :schema]Schema Migrations
Transform table structure with data migration:
# Add new field to existing table
transform_fn = fn {_table, id, name, email} ->
# Old: {table, id, name, email}
# New: {table, id, name, email, inserted_at}
{_table, id, name, email, DateTime.utc_now()}
end
MyApp.Users.transform(
[:id, :name, :email, :inserted_at],
transform_fn
)Performance: Dirty Operations
When you need speed and can relax ACID guarantees:
# Regular operations (transactional, slower)
MyApp.Users.write(%{id: 1, name: "Alice"}) # ~100ฮผs
MyApp.Users.read(1) # ~80ฮผs
# Dirty operations (no transaction, faster)
MyApp.Users.dirty_write(%{id: 1, name: "Alice"}) # ~10ฮผs
MyApp.Users.dirty_read(1) # ~5ฮผs
# Perfect for:
# - High-frequency counters
# - Real-time analytics
# - Cache writes
# - Non-critical dataAPI Reference
๐ก Tip: Use
h ModuleName.function/arityin IEx to see detailed documentation for any function. Example:h MnesiaEx.Query.select/3
Generated Functions (via use MnesiaEx)
CRUD Operations:
write/2,write!/2- Create or update recordread/1,read!/1- Read by primary keyupdate/3,update!/3- Update specific fieldsdelete/1,delete!/1- Delete recordupsert/1,upsert!/1- Insert or update
Queries:
select/2- Query with conditions (returns list)get_by/2,get_by!/2- Find by field valueall_keys/0- Get all primary keys (returns list)
Fast Operations (Dirty - No Transaction):
dirty_write/2- Fast writedirty_read/1- Fast readdirty_update/3- Fast updatedirty_delete/1- Fast delete
Tip: Use
select/0without conditions to get all records. Dirty operations are faster but skip transaction overhead.
Batch:
batch_write/1- Write multiple (returns list)batch_delete/1- Delete multiple (returns list)
Table Management:
create/1- Create tableexists?/0- Check if existsdrop/0- Delete tableclear/0- Remove all recordstable_info/0- Get metadataadd_index/1,remove_index/1- Manage indexestransform/2- Migrate table structureget_storage_type/0- Get storage type
TTL:
write_with_ttl/2,write_with_ttl!/2- Write with expirationget_ttl/1,get_ttl!/1- Get remaining time
Counters:
get_next_id/1,get_next_id!/1- Get auto-increment IDreset_counter/1,reset_counter/2- Reset counterhas_counter?/1- Check if counter exists
Events:
subscribe/0,subscribe/1- Subscribe to changesunsubscribe/0- Unsubscribeparse_event/1- Parse Mnesia events
Note: Functions with
!run in transactions and raise on error. Functions without!return{:ok, result}or{:error, reason}.
Performance Tips
| Tip | Description |
|---|---|
| ๐ Batch Operations |
Use batch_write and batch_delete for bulk operations |
| โก Dirty Operations |
Use dirty_* functions when ACID isn't critical (10x faster) |
| ๐ Indexes | Only index fields you actually query on |
| ๐พ Storage Types |
Use :ram_copies for cache, :disc_copies for persistence |
| ๐ TTL | Auto-cleanup temporary data instead of manual deletion |
| ๐ Monitor |
Check memory usage with table_info/0 |
| ๐ Keys Only |
Use all_keys/0 instead of select/0 when you only need IDs |
| ๐ Distribution |
Monitor cluster with system_info(:running_db_nodes) |
Contributing
We welcome contributions! Here's how to help:
- Fork the repo
-
Create a feature branch (
git checkout -b feature/amazing) - Make your changes following functional programming principles (pure functions, monads, no side effects)
-
Add tests (
mix test) - Submit a PR
Development Principles:
- Pure functions only
-
Monadic composition (use
Monad.Error,Monad.Maybe) - Pattern matching over conditionals
- Full typespecs
- Documented code
License
MIT License - see LICENSE for details.
Documentation
Online Documentation
- ๐ HexDocs:hexdocs.pm/mnesia_ex
- ๐ฆ Hex Package:hex.pm/packages/mnesia_ex
Interactive Documentation (IEx)
Access documentation directly in your console:
iex> h MnesiaEx.Query.select/3
def select(table, conditions \\ [], return_fields \\ [:"$_"])
@spec select(atom(), [condition()], [atom() | :"$_"]) :: list(map())
Searches for records matching the specified conditions.
## Examples
iex> MnesiaEx.Query.select(:users, [{:age, :>, 18}])
[%{id: 1, name: "Alice", age: 25}, %{id: 2, name: "Bob", age: 30}]Support
- ๐ Issues:github.com/AR3ON/mnesia_ex/issues