Skip to main content
This tutorial demonstrates how Vers transforms database development and testing workflows by enabling parallel execution from branched states. We’ll showcase how Vers eliminates repetitive setup through VM branching.

What You’ll Learn

  • The power of branching states at critical decision points
  • Parallel testing of different database schemas and features
  • Real-world time savings compared to traditional database testing

Prerequisites

  • Vers CLI installed and authenticated
  • Basic familiarity with databases and SQL

Project Overview

We’ll test different database features and schema changes. Instead of resetting the database and rebuilding test data for each scenario, we’ll:
  1. Set up once: Create a VM with SQLite and base schema
  2. Branch at decision points: Capture database state before major changes
  3. Test in parallel: Run different feature implementations simultaneously
  4. Compare results: Analyze different approaches without data loss
This approach saves significant time and enables safe experimentation with database changes.

Step 1: Project Setup

Initialize the Project

mkdir database-testing
cd database-testing
vers init

Configure the Environment

Edit the generated vers.toml:
[machine]
mem_size_mib = 1024
vcpu_count = 1
fs_size_vm_mib = 2048

[rootfs]
name = "default"

[kernel]
name = "default.bin"

Step 2: Launch and Set Up the Environment

Start the VM

vers run --vm-alias db-root

Connect and Set Up Database

vers connect
Inside the VM, set up SQLite and create the base schema:
# Configure DNS if needed
echo 'nameserver 8.8.8.8' > /etc/resolv.conf

# Install SQLite
apt-get update && apt-get install -y sqlite3

# Create working directory
mkdir -p /app/db
cd /app

Create the Base Database Schema

# Create the e-commerce database with base schema
sqlite3 /app/db/ecommerce.db << 'EOF'
-- Users table
CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT UNIQUE NOT NULL,
    name TEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Products table
CREATE TABLE products (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock INTEGER DEFAULT 0
);

-- Orders table
CREATE TABLE orders (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER REFERENCES users(id),
    total DECIMAL(10,2),
    status TEXT DEFAULT 'pending',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Insert sample data
INSERT INTO users (email, name) VALUES
    ('[email protected]', 'Alice Smith'),
    ('[email protected]', 'Bob Jones'),
    ('[email protected]', 'Carol White');

INSERT INTO products (name, price, stock) VALUES
    ('Laptop', 999.99, 50),
    ('Mouse', 29.99, 200),
    ('Keyboard', 79.99, 150),
    ('Monitor', 299.99, 75);

INSERT INTO orders (user_id, total, status) VALUES
    (1, 1029.98, 'completed'),
    (2, 29.99, 'pending'),
    (1, 379.98, 'shipped');

SELECT 'Database created with ' || COUNT(*) || ' users' FROM users;
SELECT 'And ' || COUNT(*) || ' products' FROM products;
SELECT 'And ' || COUNT(*) || ' orders' FROM orders;
EOF

Create Helper Scripts

# Script to show database state
cat > /app/show-state.sh << 'EOF'
#!/bin/bash
echo "=== Database State ==="
echo ""
echo "Users:"
sqlite3 /app/db/ecommerce.db "SELECT * FROM users;"
echo ""
echo "Products:"
sqlite3 /app/db/ecommerce.db "SELECT * FROM products;"
echo ""
echo "Orders:"
sqlite3 /app/db/ecommerce.db "SELECT * FROM orders;"
echo ""
echo "Tables in database:"
sqlite3 /app/db/ecommerce.db ".tables"
EOF
chmod +x /app/show-state.sh

# Verify setup
/app/show-state.sh
You should see the base data displayed.

Step 3: The Critical Branch Point - Pre-Migration State

This is the key moment. We’ve set up our database with base schema and data. Now we’ll branch before making schema changes so we can test different migration approaches.
# Exit the VM
exit

Branch for Different Migration Scenarios

# Create branch for premium features migration
vers branch --alias migration-premium

# Create branch for inventory management migration
vers branch --alias migration-inventory

# Create branch for analytics migration
vers branch --alias migration-analytics
You now have four VMs, all with identical database state:
  • db-root: Original state (preserve as baseline)
  • migration-premium: For testing premium user features
  • migration-inventory: For testing inventory tracking
  • migration-analytics: For testing analytics tables

Step 4: Apply Different Migrations in Parallel

Migration A: Premium User Features

vers checkout migration-premium
vers connect
Apply the premium features migration:
sqlite3 /app/db/ecommerce.db << 'EOF'
-- Add premium tier to users
ALTER TABLE users ADD COLUMN is_premium BOOLEAN DEFAULT 0;
ALTER TABLE users ADD COLUMN premium_since DATETIME;

-- Create premium benefits table
CREATE TABLE premium_benefits (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    discount_percent INTEGER DEFAULT 0
);

INSERT INTO premium_benefits (name, discount_percent) VALUES
    ('Free Shipping', 0),
    ('10% Discount', 10),
    ('Priority Support', 0);

-- Upgrade Alice to premium
UPDATE users SET is_premium = 1, premium_since = CURRENT_TIMESTAMP WHERE id = 1;

SELECT 'Premium migration complete. Premium users: ' || COUNT(*) FROM users WHERE is_premium = 1;
EOF

/app/show-state.sh
exit

Migration B: Inventory Management

Open a new terminal (or switch branches):
vers checkout migration-inventory
vers connect
Apply the inventory migration:
sqlite3 /app/db/ecommerce.db << 'EOF'
-- Add inventory tracking
ALTER TABLE products ADD COLUMN reorder_level INTEGER DEFAULT 10;
ALTER TABLE products ADD COLUMN supplier_id INTEGER;

-- Create suppliers table
CREATE TABLE suppliers (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    contact_email TEXT
);

-- Create inventory movements table
CREATE TABLE inventory_movements (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    product_id INTEGER REFERENCES products(id),
    quantity_change INTEGER NOT NULL,
    reason TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO suppliers (name, contact_email) VALUES
    ('Tech Supplies Inc', '[email protected]'),
    ('Office Depot', '[email protected]');

UPDATE products SET supplier_id = 1 WHERE id IN (1, 2, 3);
UPDATE products SET supplier_id = 2 WHERE id = 4;

SELECT 'Inventory migration complete. Suppliers: ' || COUNT(*) FROM suppliers;
EOF

/app/show-state.sh
exit

Migration C: Analytics Tables

vers checkout migration-analytics
vers connect
Apply the analytics migration:
sqlite3 /app/db/ecommerce.db << 'EOF'
-- Create analytics schema
CREATE TABLE page_views (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER REFERENCES users(id),
    page_path TEXT NOT NULL,
    viewed_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE user_sessions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER REFERENCES users(id),
    session_start DATETIME NOT NULL,
    session_end DATETIME,
    pages_viewed INTEGER DEFAULT 0
);

CREATE TABLE daily_stats (
    date DATE PRIMARY KEY,
    total_orders INTEGER DEFAULT 0,
    total_revenue DECIMAL(10,2) DEFAULT 0,
    new_users INTEGER DEFAULT 0
);

-- Insert sample analytics data
INSERT INTO page_views (user_id, page_path) VALUES
    (1, '/products'),
    (1, '/products/1'),
    (2, '/cart'),
    (3, '/checkout');

INSERT INTO daily_stats (date, total_orders, total_revenue, new_users) VALUES
    ('2024-01-15', 45, 12500.00, 12),
    ('2024-01-16', 52, 15200.00, 8);

SELECT 'Analytics migration complete. Page views: ' || COUNT(*) FROM page_views;
EOF

/app/show-state.sh
exit

Step 5: Compare Results

View All VM States

vers status
You’ll see output showing:
  • Root VM: Running parent with the base database state
  • Premium Features VM: Running child with premium user features
  • Inventory Management VM: Running child with inventory tracking
  • Analytics VM: Running child with analytics tables
All VMs are running simultaneously - when you branch, the parent is briefly paused to create a consistent snapshot, then automatically resumes.

Compare Schemas

Check what tables exist in each migration:
# Check premium migration
vers checkout migration-premium
vers execute "sqlite3 /app/db/ecommerce.db '.tables'"

# Check inventory migration
vers checkout migration-inventory
vers execute "sqlite3 /app/db/ecommerce.db '.tables'"

# Check analytics migration
vers checkout migration-analytics
vers execute "sqlite3 /app/db/ecommerce.db '.tables'"
Each branch has different tables based on its migration!

Step 6: The Time Savings

Traditional Database Testing Approach

Test Premium Migration:
├── Spin up database: 2 minutes
├── Apply base schema: 1 minute
├── Seed test data: 2 minutes
├── Apply premium migration: 1 minute
├── Run tests: 3 minutes
├── Teardown: 1 minute
└── Total: 10 minutes

Test Inventory Migration:
├── Spin up database: 2 minutes (repeated!)
├── Apply base schema: 1 minute (repeated!)
├── Seed test data: 2 minutes (repeated!)
├── Apply inventory migration: 1 minute
├── Run tests: 3 minutes
├── Teardown: 1 minute
└── Total: 10 minutes

Test Analytics Migration:
├── Same setup overhead... (repeated!)
└── Total: 10 minutes

Grand Total: 30 minutes (sequential)

Vers Approach

Initial Setup (once):
├── Create VM and install SQLite: 3 minutes
├── Apply base schema: 1 minute
├── Seed test data: 2 minutes
├── Create 3 branches: 1 minute
└── Setup Total: 7 minutes

Parallel Testing:
├── Apply + test premium: 4 min ─┐
├── Apply + test inventory: 4 min ─┼── Simultaneous
├── Apply + test analytics: 4 min ─┘

Grand Total: ~11 minutes
Time Saved: ~19 minutes (63% reduction)

Step 7: Advanced Workflows

Testing Rollback Scenarios

Branch before risky operations to test rollback:
vers checkout migration-premium
vers branch --alias premium-risky-change

vers checkout premium-risky-change
vers connect
# Apply risky migration...
# If it fails, the premium-risky-change branch is isolated
# Your migration-premium branch is unchanged

A/B Testing Migrations

Create two approaches to the same feature:
vers branch --alias feature-approach-a
vers branch --alias feature-approach-b

# Apply different implementations to each
# Compare performance and behavior

Cleanup

# Delete test branches
vers kill migration-premium
vers kill migration-inventory
vers kill migration-analytics

# Or delete everything recursively
vers kill db-root -r

Key Takeaways

  1. Branch Before Migrations: Always branch before applying schema changes
  2. Parallel Testing: Test multiple migrations simultaneously
  3. Safe Experimentation: Each branch is isolated - failures don’t affect other branches
  4. Preserve Baselines: Keep your root VM as a clean baseline for future branches

Next Steps