Skip to content

v2-io/archema

Repository files navigation

Archema

A Ruby port of Elixir's Ash Framework - declarative, resource-oriented domain modeling.

Installation

Add to your Gemfile:

gem "archema", path: "~/src/archema"  # Local development

Then bundle install. For CLI access: bundle binstubs archema

Status: Early Development

Archema provides a declarative approach to building Ruby applications where resources are authoritative and infrastructure (migrations, APIs, etc.) is derived from resource definitions.

Vision: Three-Worlds Unification

Archema is built on a key insight: we can unify the best of three paradigms into a single resource-oriented model.

Paradigm What Archema Takes
RDBMS Relationship semantics, JOIN efficiency, referential integrity, ACID transactions
Document stores Schema expressiveness (JSON Schema oneOf/anyOf/dependentRequired), versioning, readability
Event sourcing Temporal awareness, immutable audit trails, as_of queries, projection rebuilds

This isn't about replacing any paradigm—it's about a unified resource definition that projects appropriately to each storage layer. Document schemas (JSON/YAML) have richer constraint vocabulary than RDBMS; Archema uses that as the canonical constraint model and projects down to SQL where possible.

See ADR-003: Document-Schema-First for the architectural rationale.

The result: schema changes as safe as code changes.

Key ideas:

  • Resource definition is the single source of truth — storage layers are intelligent servants
  • Document-schema-first constraints — JSON Schema vocabulary, RDBMS gets best-effort projection + triggers
  • Automatic expand-contract transitions — rename a field, both names work during transition
  • Branch-safe schema changes — feature branches and production coexist safely
  • Temporal awarenessas_of(time) queries, full version history
  • Cross-store composition — same Resource writing to RDBMS + read replica + JSONL audit trail
  • Cross-store relationships — JOIN across databases, even across store types (e.g., YAML frontmatter files joined against PostgreSQL)
# The vision: declare intent, everything else is automatic
class AgentCard < Archema::Resource
  uuid_primary_key :id
  field :full_name, :string, was: :name  # Rename with automatic transition
end
# Old code using :name continues to work
# Storage layer syncs writes, monitors usage, contracts when safe

Quick Example

class User < Archema::Resource
  uuid_primary_key :id
  field :email, :string, constraints: { format: /@/ }
  field :name, :string
  field :bio, :string, :optional                    # Allow nil
  field :role, :atom, default: :reader, constraints: { one_of: [:admin, :author, :reader] }
  field :api_key, :string, :optional, :sensitive    # Redacted in logs
  timestamps

  relationships do
    has_many :posts, Post
    belongs_to :organization, Organization, :optional     # Optional relationship
  end

  identities do
    identity :unique_email, [:email]
  end

  actions do
    defaults [:create, :read, :update, :destroy]
  end

  policies do
    authorize_if :always
  end
end

DSL Flags

Fields and relationships use flag symbols for common options:

Flag Fields Relationships Effect
:optional belongs_to Allow nil values (FK nullable)
:required belongs_to Disallow nil (default for fields and belongs_to)
:private Exclude from serialization
:sensitive Redact in logs, exclude from tool export
:immutable Write-once: settable on create, then locked
:readonly No setter (usually prefer generated fields instead)
:mutable Explicitly allow updates (overrides default PK immutability)

Flags can be combined: field :secret, :string, :optional, :private, :sensitive

Cardinality Constraints: has_one/has_many/many_to_many support min_required/max_allowed. The :required flag on these types sets min_required: 1. Validate via record.validate_relationship_cardinality!.

What's Implemented

Core Framework

  • Resource DSL - Attributes, relationships, identities, actions, policies, calculations
  • Type System - Built on dry-types with Ash-style names (:atom, :uuid, etc.)
  • Changeset - For tracking changes and validations
  • Query Builder - Filtering (with OR/AND compound conditions), sorting, pagination, immutable API
  • Result Monad - Railway-oriented programming with pattern matching support
  • Preloading - Efficient batch-loading of relationships to avoid N+1
  • Calculations & Aggregates - Derived fields computed on-demand (count, sum, avg, min, max, etc.)

Store Adapters

  • Memory - In-memory storage for testing (thread-safe)
  • Sequel - PostgreSQL and SQLite support (fully wired to resources)
  • YAML Frontmatter - File-based persistence with YAML frontmatter + Markdown body
  • JSONL - JSON Lines append-only log with optional hash-chaining (ideal for event stores)

See ADR-001: Store Composition for unified multi-store architecture.

Schema Versioning

  • Schema ID & Version - schema_id "my-resource", schema_version "2.0.0"
  • Attribute Lifecycle - since:, deprecated:, removed: options
  • Upcasting - Transform old data to current schema on read
  • Compatibility Declarations - backward_compatible_with, forward_compatible_with
  • UUID7 & UUID8 - Time-sortable UUIDs (RFC 9562) for natural ordering

AI Agent Integration

  • Tool Definition Export - Resource.to_tool_definitions generates Anthropic/OpenAI tool schemas
  • Actions become typed function calls with proper JSON Schema parameters
  • Constraints, descriptions, and required fields all preserved

Migration System (Ash-style)

  • Snapshots - Captures resource definitions as JSON
  • Differ - Compares snapshots to detect changes
  • Codegen - Generates Sequel migrations from diffs
  • CLI - archema codegen, archema migrate, etc.

Key features:

  • Resources are authoritative, migrations are derived
  • UUIDv7 for PostgreSQL (time-ordered, better indexing)
  • Dialect-aware (PostgreSQL vs SQLite differences handled)
  • Interactive conflict resolution for renames/type changes

Behavioral Contracts

Understanding how Archema operations flow helps debug issues and design correct systems.

Action Pipeline

All CRUD operations follow a consistent pipeline:

Write Actions (create/update/destroy):

  1. Action lookup — Verify action exists; fail with InvalidActionError if not
  2. Atomicity check — If require_atomic: true, verify adapter supports transactions
  3. Authorization — Check policies; fail with AuthorizationError if denied
  4. Build changeset — Separate params into attributes vs arguments, enforce write protections (:readonly, :immutable)
  5. Validate changeset — Type coercion, required fields, constraints, composition rules
  6. Apply changes — Run action's transformations (set_timestamp, etc.)
  7. Execute via adapter — Wrapped in transaction if require_atomic?

Read Actions:

  1. Action lookup — Verify action exists
  2. Authorization — Check policies
  3. Apply preparations — Tenant scoping, policy filters, custom preps
  4. Execute query — Send to adapter with filters, sorts, pagination
  5. Preload relationships — Batch-load via load: option
  6. Load calculations — Compute aggregates via load_calculations:

Context Propagation: Actor and tenant flow through the entire pipeline, including preloaded relationships:

User.read(actor: admin, tenant: org, load: :posts)
# → Posts are also read with actor: admin, tenant: org

Query Operators

Operator SQL Equivalent Example
:eq = where(:status, :eq, :active)
:neq != where(:status, :neq, :deleted)
:gt / :gte > / >= where(:age, :gte, 21)
:lt / :lte < / <= where(:price, :lt, 100)
:in / :not_in IN / NOT IN where(:role, :in, [:admin, :mod])
:like / :ilike LIKE / ILIKE where(:email, :ilike, "%@gmail%")
:contains LIKE %v% where(:bio, :contains, "ruby")
:starts_with LIKE v% where(:name, :starts_with, "Dr.")
:ends_with LIKE %v where(:email, :ends_with, ".edu")
:is_nil / :is_not_nil IS NULL where(:deleted_at, :is_nil, true)
:matches ~ (regex) PostgreSQL only

Query Composition: Conditions combine with AND at top level; use where_any for OR:

query.where(active: true)                         # active AND
     .where_any([{role: :admin}, {role: :mod}])   # (admin OR mod)

Note: filter and filter_any are aliases for Ash Framework users.

Authorization Model

Policies are declarative and composable:

Check Result Meaning
:authorized Policy permits; continue checking others
:bypass_authorized Policy permits AND short-circuits (skip remaining)
:forbidden Policy denies; authorization fails immediately
:continue No decision; treated as permitted

Evaluation rules:

  • If no policies apply, authorization succeeds (open by default)
  • All applicable policies must authorize (AND logic)
  • Bypass policies can short-circuit evaluation
policies do
  # Admin bypass - short-circuits remaining checks
  bypass actor_attribute_equals(:admin, true) do
    authorize_if always
  end

  # Normal users: must own the record
  policy action_type(:update) do
    authorize_if relates_to_actor_via(:author)
  end
end

Multi-Store Routing

When a Resource has multiple stores (primary, cache, projection, event):

Operation Routing Order Behavior
read cache → projection → primary First non-empty result wins
get cache → projection → primary First found record wins
count projection → primary Projection preferred (SQL COUNT)
create primary → event → projections Must succeed on primary
update primary → event → projections Must succeed on primary
destroy primary → event → projections Cache invalidated

Atomicity Warning: Multi-store writes are NOT atomic across heterogeneous stores. If primary succeeds but event/projection fails, the system may be inconsistent. Use event store as source of truth and rebuild_projection! for recovery.

Transaction Semantics

Adapter Transactions Isolation Nested
Sequel Full ACID DB default (READ COMMITTED / SERIALIZABLE) Savepoints
Memory None N/A N/A
JSONL None (append-only) N/A N/A
YAML None (file-based) N/A N/A

For atomic multi-record operations, use Archema.batch:

Archema.batch do |b|
  user = b.create(User, name: "Alice")
  b.create(Post, author_id: user.id, title: "Hello")
end  # Wrapped in single transaction if adapter supports it

Schema Evolution Heuristics

The differ detects changes between snapshots:

Scenario Detection Resolution
was: hint in DSL Explicit rename Auto-generates RenameColumn
1 removed + 1 added, same type Possible rename User chooses :rename or :separate
Type changed Conflict User must choose :alter or :abort

Expand/Contract Pattern for safe renames:

  1. Expand: Add new column, deploy code writing both
  2. Contract: Remove old column after all code uses new

CLI Commands

archema codegen [NAME]     # Generate migrations from resource changes
archema migrate            # Run pending migrations
archema rollback           # Rollback last migration
archema snapshot           # Show current resource snapshots
archema help [COMMAND]     # Show help for any command

Local Gem Development

Archema is currently distributed via local path reference for internal use.

Using in Other Projects

Add to your project's Gemfile:

gem "archema", path: "~/src/archema"

Then:

bundle install
bundle binstubs archema  # Optional: creates bin/archema for CLI access

Building the Gem

bin/dx gem           # Build the gem
bin/dx gem install   # Build and install locally
bin/dx gem clean     # Remove built gem files

Running Tests

All development tasks use bin/dx (a toys-core based CLI):

bin/dx test                    # Unit tests
bin/dx test integration        # SQLite/PostgreSQL integration tests
bin/dx test all                # Everything
bin/dx test -f path/to/test.rb # Run specific file
bin/dx test -n test_name       # Run test by name pattern

# Property-based testing
bin/dx prop                    # All property tests
bin/dx prop run query          # Single module
bin/dx prop list               # List available modules

# Mutation testing
bin/dx mutant                  # Run on all covered code
bin/dx mutant Archema::Query   # Run on specific subject

# Simulation-driven testing (LLM-generated scenarios)
bin/dx sim generate            # Generate scenario using Sonnet/Haiku
bin/dx sim run                 # Run pending scenarios
bin/dx sim                     # Library summary

# Type checking
bin/dx types                   # Run Steep type checker
bin/dx types extract           # Extract RBS from inline annotations

# Linting and pre-commit
bin/dx lint                    # Run RuboCop
bin/dx lint fix                # Auto-correct offenses
bin/dx pre-commit              # Run all checks (lint, types, tests, etc.)

Documentation

See KEY_FILES.md for complete documentation and source file reference.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages