Morpheus
A powerful DAG-based migration system for Neo4j graph databases with support for parallel execution, dependency management, and automatic conflict detection.
Table of Contents
- Features
- Installation
- Quick Start
- CLI Commands
- Advanced Features
- Testing
- Development
- Common Use Cases
- Best Practices
- Troubleshooting
- Contributing
- License
- Support
Features
- DAG-based migrations: Define complex dependencies between migrations
- Parallel execution: Run independent migrations concurrently for faster deployments
- Conflict detection: Automatically detect and prevent conflicting migrations
- Priority levels: Control migration execution order with priorities
- Tag system: Organize migrations with tags for selective execution
- Rollback support: Safe downgrade capabilities with transaction management
- Rich CLI: Interactive command-line interface with colored output
- Dry-run mode: Preview changes before applying them
Installation
Using uv (recommended)
uv add morpheus-neo4j
Using pip
pip install morpheus-neo4j
From source
git clone https://github.com/AZX-PBC/morpheus
cd morpheus
uv sync
Quick Start
1. Initialize Migration System
morpheus init
This creates:
migrations/directory (or custom directory you specify)migrations/versions/directory for versioned migrationsmigrations/morpheus-config.ymlconfiguration file with default settings
2. Configure Database Connection
Edit migrations/morpheus-config.yml:
database:
uri: bolt://localhost:7687
username: neo4j
password: your-password
database: neo4j # optional, for multi-database setups
migrations:
directory: ./migrations/versions
execution:
parallel: true
max_parallel: 4
Environment Variable Support
Morpheus supports environment variables in configuration files using OmegaConf's ${oc.env:VAR_NAME,default} syntax:
database:
uri: ${oc.env:NEO4J_URI,bolt://localhost:7687}
username: ${oc.env:NEO4J_USERNAME,neo4j}
password: ${oc.env:NEO4J_PASSWORD,password}
# database: ${oc.env:NEO4J_DATABASE} # Optional database name
migrations:
directory: ./migrations/versions
execution:
parallel: true
max_parallel: ${oc.env:MAX_PARALLEL,4}
How it works:
- Environment variables use
${oc.env:VAR_NAME,default}syntax (OmegaConf resolver) - Default values are specified after the comma
- If the environment variable isn't set, the default value will be used
- Variables without defaults will be empty if not set
Example usage:
# Set environment variables to override defaults
export NEO4J_URI=bolt://production:7687
export NEO4J_USERNAME=prod_user
export NEO4J_PASSWORD=secure_password
export MAX_PARALLEL=8
# Run morpheus commands - will use env vars if set, defaults otherwise
morpheus status
morpheus upgrade
3. Create Your First Migration
morpheus create initial_schema
This generates a migration file like 20250819_120000_initial_schema.py:
from morpheus.models.migration import MigrationBase
from morpheus.models.priority import Priority
class Migration(MigrationBase):
"""Initial schema setup."""
# Define dependencies (other migration IDs that must run first)
dependencies = []
# Define conflicting migrations (cannot run in parallel)
conflicts = []
# Add tags for organization
tags = ["schema", "initial"]
# Set priority (CRITICAL, HIGH, NORMAL, LOW)
priority = Priority.NORMAL
def upgrade(self, tx):
"""Apply migration."""
# Create constraints
tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (u:User) REQUIRE u.id IS UNIQUE")
tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (u:User) REQUIRE u.email IS UNIQUE")
# Create indexes
tx.run("CREATE INDEX IF NOT EXISTS FOR (u:User) ON (u.created_at)")
def downgrade(self, tx):
"""Rollback migration."""
tx.run("DROP CONSTRAINT IF EXISTS ON (u:User) ASSERT u.id IS UNIQUE")
tx.run("DROP CONSTRAINT IF EXISTS ON (u:User) ASSERT u.email IS UNIQUE")
tx.run("DROP INDEX IF EXISTS FOR (u:User) ON (u.created_at)")
4. Apply Migrations
# Preview execution plan
morpheus upgrade --dry-run
# Apply all pending migrations
morpheus upgrade
# Apply up to a specific migration
morpheus upgrade --target 20250819_120000_initial_schema
CLI Commands
morpheus init
Initialize the migration system in your project. Interactive by default.
morpheus init [OPTIONS]
Options:
--directory, -d TEXT Migrations directory (optional)
--non-interactive Skip interactive prompts and use defaults
The init command will:
- Prompt for the migrations directory (defaults to
./migrations) - Create the directory structure
- Generate a config file with environment variable support
- Display next steps
morpheus create
Create a new migration file.
morpheus create <name> [OPTIONS]
Options:
--depends-on TEXT Migration IDs this depends on (can be used multiple times)
--conflicts-with TEXT Migration IDs that conflict (can be used multiple times)
--tags TEXT Tags for this migration (can be used multiple times)
--priority TEXT Priority level (critical/high/normal/low)
Examples:
# Simple migration
morpheus create add_user_properties
# With dependencies
morpheus create add_relationships --depends-on 20250819_120000_initial_schema
# With multiple options
morpheus create complex_migration \
--depends-on migration1 \
--depends-on migration2 \
--conflicts-with migration3 \
--tags performance \
--tags optimization \
--priority high
# With multiple dependencies and tags
morpheus create my_migration --depends-on dep1 --depends-on dep2 --tags tag1
morpheus status
View migration status and pending migrations.
morpheus status [--format ascii|table]
Output shows:
- Applied migrations with execution timestamps
- Pending migrations ready to run
- Blocked migrations waiting for dependencies
morpheus upgrade
Apply pending migrations to the database.
morpheus upgrade [OPTIONS]
Options:
--target TEXT Target migration ID to upgrade to
--parallel/--no-parallel Enable/disable parallel execution
--dry-run Show execution plan without applying
--ci Enable CI mode with detailed exit status messages
--yes, -y Skip confirmation prompt
--failfast/--no-failfast Stop execution when any migration fails (default: False)
morpheus downgrade
Rollback applied migrations.
morpheus downgrade [OPTIONS]
Options:
--target TEXT Target migration ID to downgrade to
--branch Smart rollback affecting only specific branch
--dry-run Show rollback plan without applying
morpheus dag
Visualize and analyze the migration dependency graph.
morpheus dag [OPTIONS]
Options:
--format TEXT Output format (ascii/dot/json) (default: ascii)
--output, -o PATH Output file (default: stdout)
--show-branches Show independent branches
--filter-status TEXT Filter by migration status (pending/applied/failed/rolled_back)
Advanced Features
Dependency Management
Morpheus uses a DAG (Directed Acyclic Graph) to manage migration dependencies:
class Migration(MigrationBase):
# This migration requires user and product schemas to exist first
dependencies = [
"20250819_120000_create_users",
"20250819_120100_create_products"
]
def upgrade(self, tx):
# Can safely reference User and Product nodes
tx.run("""
CREATE CONSTRAINT IF NOT EXISTS
FOR (p:Purchase) REQUIRE p.id IS UNIQUE
""")
tx.run("""
CREATE INDEX IF NOT EXISTS
FOR ()-[r:PURCHASED]->() ON (r.timestamp)
""")
Conflict Detection
Conflicts prevent migrations from running in parallel without imposing an execution order. This is different from dependencies:
| Dependencies | Conflicts | |
|---|---|---|
| Meaning | "B must complete before A starts" | "A and B cannot run simultaneously" |
| Ordering | Strict order enforced | No order preference |
| Use case | Logical/data dependency | Resource contention, race conditions |
Why not just use dependencies?
Consider two independent migrations that both modify User indexes:
# Migration A: add_email_index.py
class Migration(MigrationBase):
def upgrade(self, tx):
tx.run("CREATE INDEX FOR (u:User) ON (u.email)")
# Migration B: add_name_index.py
class Migration(MigrationBase):
def upgrade(self, tx):
tx.run("CREATE INDEX FOR (u:User) ON (u.name)")
These have no logical dependency—either can run first. But running them in parallel might cause database contention.
❌ Using dependencies: Forces arbitrary ordering, reduces parallelism
dependencies = ["add_email_index"] # Why should name index wait for email?
✅ Using conflicts: Prevents parallel execution, preserves flexibility
conflicts = ["add_email_index"] # Just don't run at the same time
Additionally, bidirectional mutual exclusion is natural with conflicts but impossible with dependencies:
# Both migrations declare they conflict - valid and clear:
# Migration A
conflicts = ["B"]
# Migration B
conflicts = ["A"]
# With dependencies this would be a cycle (invalid DAG)
How conflicts affect batching:
# Migration A conflicts with B, but C has no conflicts
# Possible execution:
# Batch 1: [A, C] ← run in parallel
# Batch 2: [B]
# Or:
# Batch 1: [B, C] ← run in parallel
# Batch 2: [A]
Non-conflicting migrations (C) can still run in parallel with either A or B, maximizing throughput.
class Migration(MigrationBase):
# Cannot run in parallel with user schema modifications
conflicts = ["20250819_130000_modify_user_schema"]
def upgrade(self, tx):
tx.run("ALTER CONSTRAINT ON (u:User) ASSERT u.email IS UNIQUE")
Priority Levels
Control execution order when migrations have the same dependency level:
from morpheus.models.priority import Priority
class Migration(MigrationBase):
priority = Priority.CRITICAL # Runs first
def upgrade(self, tx):
# Critical system migration
pass
Priority levels (highest to lowest):
CRITICAL: System-critical migrationsHIGH: Important schema changesNORMAL: Standard migrations (default)LOW: Non-critical optimizations
Tag System
Organize and filter migrations with tags:
class Migration(MigrationBase):
tags = ["schema", "user", "auth"]
def upgrade(self, tx):
# User authentication related changes
pass
Future: Run migrations by tag:
morpheus upgrade --tag schema
morpheus upgrade --exclude-tag experimental
Parallel Execution
Morpheus automatically detects independent migrations and runs them in parallel:
Execution Plan:
Batch 1: 20250819_120000_users
Batch 2 (parallel):
• 20250819_130000_user_properties
• 20250819_130001_user_indexes
• 20250819_130002_user_constraints
Batch 3: 20250819_140000_user_relationships
Control parallel execution:
# Disable parallel execution
morpheus upgrade --no-parallel
# Configure max parallel migrations
# In morpheus-config.yml:
execution:
parallel: true
max_parallel: 4
Testing
Unit Tests
uv run pytest tests/unit -v
Integration Tests (requires Docker)
# Start Neo4j container
docker run -d \
--name neo4j-test \
-p 7474:7474 -p 7687:7687 \
-e NEO4J_AUTH=neo4j/test_password \
neo4j:latest
# Run integration tests
uv run pytest tests/integration -v
Coverage Report
uv run pytest --cov=morpheus --cov-report=html
open htmlcov/index.html
Development
Setup Development Environment
# Clone repository
git clone https://github.com/AZX-PBC/morpheus
cd morpheus
# Install dependencies with dev extras
uv sync --dev
# Install pre-commit hooks (optional)
pre-commit install
Code Quality
# Format code
uv run ruff format
# Lint code
uv run ruff check --fix
# Type checking
uv run pyright
Project Structure
morpheus/
├── morpheus/
│ ├── cli/ # CLI commands and interface
│ │ ├── commands/ # Individual command implementations
│ │ └── utils.py # CLI utilities
│ ├── config/ # Configuration management
│ ├── core/ # Core migration logic
│ │ ├── dag_resolver.py # DAG resolution and validation
│ │ └── executor.py # Migration execution engine
│ ├── models/ # Data models
│ │ ├── migration.py # Migration base class
│ │ └── priority.py # Priority enum
│ └── templates/ # Migration file templates
├── tests/
│ ├── unit/ # Unit tests
│ └── integration/ # Integration tests
├── migrations/ # Example migrations
└── pyproject.toml # Project configuration
Common Use Cases
1. Schema Evolution
# Migration 1: Create initial schema
class Migration(MigrationBase):
def upgrade(self, tx):
tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (u:User) REQUIRE u.id IS UNIQUE")
tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (p:Post) REQUIRE p.id IS UNIQUE")
# Migration 2: Add properties
class Migration(MigrationBase):
dependencies = ["20250819_120000_initial_schema"]
def upgrade(self, tx):
tx.run("MATCH (u:User) WHERE u.created_at IS NULL SET u.created_at = datetime()")
tx.run("CREATE INDEX IF NOT EXISTS FOR (u:User) ON (u.created_at)")
2. Data Migrations
class Migration(MigrationBase):
tags = ["data", "cleanup"]
def upgrade(self, tx):
# Migrate legacy data format
tx.run("""
MATCH (u:User)
WHERE u.fullName IS NOT NULL AND u.firstName IS NULL
SET u.firstName = split(u.fullName, ' ')[0],
u.lastName = split(u.fullName, ' ')[1]
""")
def downgrade(self, tx):
# Restore original format
tx.run("""
MATCH (u:User)
WHERE u.firstName IS NOT NULL
SET u.fullName = u.firstName + ' ' + u.lastName
REMOVE u.firstName, u.lastName
""")
3. Performance Optimizations
class Migration(MigrationBase):
priority = Priority.LOW
tags = ["performance", "index"]
def upgrade(self, tx):
# Add composite index for query optimization
tx.run("""
CREATE INDEX IF NOT EXISTS FOR (u:User)
ON (u.country, u.city, u.created_at)
""")
# Add relationship index
tx.run("""
CREATE INDEX IF NOT EXISTS FOR ()-[r:FOLLOWS]->()
ON (r.created_at)
""")
4. Complex Refactoring
class Migration(MigrationBase):
dependencies = ["20250819_120000_initial_schema"]
conflicts = ["20250819_130000_other_refactor"] # Prevent parallel execution
def upgrade(self, tx):
# Step 1: Create new structure
tx.run("""
MATCH (u:User)-[:POSTED]->(p:Post)
CREATE (u)-[:AUTHORED {created_at: p.created_at}]->(p)
""")
# Step 2: Migrate data
tx.run("""
MATCH ()-[r:POSTED]->()
DELETE r
""")
def downgrade(self, tx):
# Restore original structure
tx.run("""
MATCH (u:User)-[r:AUTHORED]->(p:Post)
CREATE (u)-[:POSTED]->(p)
DELETE r
""")
Best Practices
- Always provide rollback logic: Implement both
upgradeanddowngrademethods - Use transactions: All operations run in transactions automatically
- Make migrations idempotent: Use
IF NOT EXISTSandIF EXISTSclauses - Keep migrations focused: One logical change per migration
- Test migrations: Always test in development before production
- Document dependencies: Clearly specify migration dependencies
- Use meaningful names: Migration names should describe what they do
- Version control: Commit migration files to version control
Troubleshooting
Connection Issues
# Test connection
morpheus status
# Check Neo4j is running
docker ps | grep neo4j
# Verify credentials in morpheus-config.yml
Migration Conflicts
# Check for conflicts
morpheus dag --show-branches
# Conflicts are automatically detected during upgrade
morpheus upgrade --dry-run
Failed Migrations
# Check migration status
morpheus status
# Rollback last migration
morpheus downgrade --steps 1
# Fix and retry
morpheus upgrade
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
- Documentation: https://morpheus-docs.example.com
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Metadata
Release files for morpheus-neo4j 0.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| morpheus_neo4j-0.4.2.tar.gz | 117.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| morpheus_neo4j-0.4.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 169.4 kB
Release files / morpheus_neo4j-0.4.2.tar.gz
| Download URL | morpheus_neo4j-0.4.2.tar.gz |
|---|---|
| Size | 117.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cccf82145b59ce305dad9a91aeab4e76ea793fbc559e2542e1aa067629fe2515
|
|
BLAKE2b-256 checksum How to use checksums |
d0d145663932fd461aacd7beb90f86ed10699bb7df4c39ec8a9f71afdbf6213c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Release files / morpheus_neo4j-0.4.2-py3-none-any.whl
| Download URL | morpheus_neo4j-0.4.2-py3-none-any.whl |
|---|---|
| Size | 51.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ad2a80f1baba259db7caa3bb6263e54285cc8a466c314e6c145e552b56d68869
|
|
BLAKE2b-256 checksum How to use checksums |
68c8787cca86485bdc0de481ceedd339b567a00aec019fb35a5f6141c1d8f7ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Security for morpheus-neo4j 0.4.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.