> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vers.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Database state testing

> Seed a database once, branch the live state, apply three competing schema migrations in parallel, compare outcomes. No more 'reset the DB and reseed' tax.

Schema migrations are the canonical example of a change you want to try three ways before committing to one. The tax is always the same: reset the database, reseed test data, apply migration, test. For every scenario.

Vers removes the tax. Seed once, branch the live database, apply competing migrations against isolated copies, compare in parallel.

## What you'll build

* A SQLite e-commerce database with a seeded base schema
* Three **parallel migration branches**: premium-user features, inventory tracking, analytics
* A workflow for **comparing** the three outcomes side by side
* **Time:** \~25 minutes

## Prerequisites

* Vers CLI installed and authenticated
* Basic familiarity with SQL

## The idea

The expensive part of testing a migration isn't the migration — it's the base state the migration modifies. You want to test three different premium-features implementations against *the same seeded database*, and traditionally that means reseeding three times.

With Vers you seed once, branch the live VM (which includes the SQLite file's exact page layout, WAL state, and any in-memory buffers), then apply a different migration to each branch. Every branch sees the same starting data; each diverges only in the migration you run.

## Step 1: Initialize the project

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
mkdir database-state-testing && cd database-state-testing
vers init
```

Default `vers.toml` resources are fine for SQLite:

```toml theme={"theme":{"light":"min-light","dark":"min-dark"}}
[machine]
mem_size_mib = 1024
vcpu_count = 1
fs_size_mib = 2048

[rootfs]
name = "default"

[kernel]
name = "default.bin"
```

## Step 2: Boot the VM and seed the base schema

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers run --vm-alias db-root
vers connect
```

Inside the VM:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
apt-get update -qq && apt-get install -y -qq sqlite3
mkdir -p /app/db && cd /app
```

Seed the base schema — users, products, orders, plus sample rows:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
sqlite3 /app/db/ecommerce.db << 'EOF'
CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

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

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 INTO users (email, name) VALUES
  ('alice@example.com', 'Alice Smith'),
  ('bob@example.com',   'Bob Jones'),
  ('carol@example.com', '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');
EOF
```

Write a helper to dump schema + row counts so you can quickly diff branches later:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
cat > /app/inspect.sh << 'EOF'
#!/bin/bash
echo "=== Tables ==="
sqlite3 /app/db/ecommerce.db ".tables"
echo "=== Row counts ==="
for t in $(sqlite3 /app/db/ecommerce.db ".tables"); do
  c=$(sqlite3 /app/db/ecommerce.db "SELECT COUNT(*) FROM $t")
  echo "  $t: $c"
done
EOF
chmod +x /app/inspect.sh
/app/inspect.sh
```

Exit back to your host shell:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
exit
```

## Step 3: Branch at the pre-migration state

This is the decision point. Three migration candidates, one base state:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers branch --alias migration-premium
vers branch --alias migration-inventory
vers branch --alias migration-analytics
```

Four VMs running now — `db-root` as the baseline, three children each with an identical copy of the seeded database. No reseeding, no reset scripts.

## Step 4: Apply three migrations in parallel

Each migration goes on its own branch. For genuine parallelism, open three terminals; for a linear read, just run them one after the other — the `vers execute` form is fine.

### Migration A — premium user features

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers execute migration-premium "sqlite3 /app/db/ecommerce.db \"\
ALTER TABLE users ADD COLUMN is_premium BOOLEAN DEFAULT 0;\
ALTER TABLE users ADD COLUMN premium_since DATETIME;\
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);\
UPDATE users SET is_premium = 1, premium_since = CURRENT_TIMESTAMP WHERE id = 1;\
\""
```

### Migration B — inventory management

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers execute migration-inventory "sqlite3 /app/db/ecommerce.db \"\
ALTER TABLE products ADD COLUMN reorder_level INTEGER DEFAULT 10;\
ALTER TABLE products ADD COLUMN supplier_id INTEGER;\
CREATE TABLE suppliers (\
  id INTEGER PRIMARY KEY AUTOINCREMENT,\
  name TEXT NOT NULL,\
  contact_email TEXT\
);\
CREATE TABLE inventory_movements (\
  id INTEGER PRIMARY KEY AUTOINCREMENT,\
  product_id INTEGER REFERENCES products(id),\
  quantity_change INTEGER NOT NULL,\
  reason TEXT\
);\
INSERT INTO suppliers (name, contact_email) VALUES\
  ('Tech Supplies Inc', 'orders@techsupplies.com');\
UPDATE products SET supplier_id = 1 WHERE id IN (1, 2, 3);\
\""
```

### Migration C — analytics tables

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers execute migration-analytics "sqlite3 /app/db/ecommerce.db \"\
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 daily_stats (\
  date DATE PRIMARY KEY,\
  total_orders INTEGER DEFAULT 0,\
  total_revenue DECIMAL(10,2) DEFAULT 0\
);\
INSERT INTO page_views (user_id, page_path) VALUES\
  (1, '/products'),\
  (2, '/cart');\
\""
```

## Step 5: Compare outcomes

The `inspect.sh` helper you wrote in Step 2 is present in every branch — they inherit the parent's filesystem at branch time. Run it on each:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers execute migration-premium   "/app/inspect.sh"
vers execute migration-inventory "/app/inspect.sh"
vers execute migration-analytics "/app/inspect.sh"
```

Each branch shows a different schema. Every branch still has the baseline users/products/orders data unchanged — only the migration's additions diverge.

### Confirm the baseline is untouched

The root VM is exactly where you left it at Step 2:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers execute db-root "/app/inspect.sh"
```

No `premium_benefits`, no `suppliers`, no `page_views`. The migrations live only on their branches.

## Step 6: Branch a branch — test a rollback scenario

A branch is itself a VM you can branch. Say you want to apply a risky secondary migration on top of `migration-premium` without disturbing it:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers branch migration-premium --alias premium-risky
vers execute premium-risky "sqlite3 /app/db/ecommerce.db 'DROP TABLE premium_benefits'"
vers execute premium-risky "/app/inspect.sh"
vers execute migration-premium "/app/inspect.sh"   # unchanged
```

The risky change lives on its own branch. The parent migration branch is unaffected. This is the rollback story for free: every commit you take is a restoration point, every branch is an isolation boundary.

## Step 7: Clean up

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers checkout db-root
vers kill db-root -r
```

The `-r` tears down the root and all descendants.

## The pattern

**Seed expensive state once, branch at the decision point, diverge per scenario.** Databases happen to be the cleanest example because the setup cost (schema + seed data) is visibly expensive, but the pattern is the same one as [parallel scenario testing](/tutorials/parallel-web-testing) and [agent swarms](/tutorials/agent-swarms):

* The setup prefix is paid once, at commit time
* Every fork inherits the exact state — byte-identical, memory and filesystem both
* Divergence only costs what it actually writes

Combine that with `vers commit` and you have a rollback primitive strong enough that "test three migrations and keep the best one" becomes one afternoon of work, not one sprint.

## What's next

<CardGroup cols={2}>
  <Card title="Parallel scenario testing" icon="code-branch" href="/tutorials/parallel-web-testing">
    Same pattern applied to end-to-end HTTP testing against a live stateful service.
  </Card>

  <Card title="Agent swarms" icon="robot" href="/tutorials/agent-swarms">
    Branch a golden state into parallel coding agents.
  </Card>

  <Card title="vers branch" icon="code-branch" href="/cli-reference/branch">
    Every flag on the branch primitive.
  </Card>

  <Card title="Architecture" icon="sitemap" href="/architecture">
    How commits, overlays, and content addressing make migration testing cheap.
  </Card>
</CardGroup>
