MnesiaEx

A modern, functional wrapper for Erlang/OTP's Mnesia database

Hex.pm VersionHex.pm DownloadsDocumentationLicense: MITStatus: Beta

โš ๏ธ 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:

Use Cases

Features

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 patches

Examples

Check out the examples/ directory for complete, runnable examples:

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
end

3. 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):

user = MyApp.Users.write!(%{id: 1, name: "Alice"})  
# Auto-transaction + returns value directly

Functions without ! (smart, auto-detecting):

# 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):

{:ok, user} = MyApp.Users.dirty_write(%{id: 1, name: "Alice"})

MnesiaEx.sync_transaction/1 (distributed consistency):

{: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:

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/2

Requirements

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 disk

Configuration Reference

Option Type Default Description
backup_dirString.t()"priv/backups" Directory for backup files
export_dirString.t()"priv/exports" Directory for exported files
counter_tableatom():mnesia_counters System table for auto-increment
ttl_tableatom():mnesia_ttl System table for TTL tracking
cleanup_intervaltuple() | integer(){5, :minutes} TTL cleanup frequency
auto_cleanupboolean()true Enable/disable automatic TTL cleanup
ttl_persistenceboolean()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 mix tasks, 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
end

Available Functions:

All standard CRUD operations are automatically injected:

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:

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.exs

Time 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 automatically

Event 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
end

Backup & 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 data

API Reference

๐Ÿ’ก Tip: Use h ModuleName.function/arity in IEx to see detailed documentation for any function. Example: h MnesiaEx.Query.select/3

Generated Functions (via use MnesiaEx)

CRUD Operations:

Queries:

Fast Operations (Dirty - No Transaction):

Tip: Use select/0 without conditions to get all records. Dirty operations are faster but skip transaction overhead.

Batch:

Table Management:

TTL:

Counters:

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:

  1. Fork the repo
  2. Create a feature branch (git checkout -b feature/amazing)
  3. Make your changes following functional programming principles (pure functions, monads, no side effects)
  4. Add tests (mix test)
  5. Submit a PR

Development Principles:

License

MIT License - see LICENSE for details.

Documentation

Online Documentation

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