Skip to content

Change Tracking

How CinchDB tracks and manages schema changes across branches and tenants.

Overview

CinchDB maintains a complete history of all schema modifications. This enables: - Reproducible schema evolution - Safe merging between branches
- Consistent multi-tenant updates - Audit trail of changes

Change Types

Tracked Operations

CinchDB tracks these schema modifications:

  1. Table Operations

  2. CREATE_TABLE - New table creation

  3. DROP_TABLE - Table deletion
  4. RENAME_TABLE - Table renaming

  5. Column Operations

  6. ADD_COLUMN - New column addition

  7. DROP_COLUMN - Column removal
  8. RENAME_COLUMN - Column renaming

  9. View Operations

  10. CREATE_VIEW - View creation

  11. DROP_VIEW - View deletion
  12. UPDATE_VIEW - View modification

  13. Index Operations

  14. CREATE_INDEX - Index creation

  15. DROP_INDEX - Index removal

Change Storage

File Structure

Changes are stored in changes.json within each branch:

.cinchdb/databases/myapp/branches/feature/
├── metadata.json
├── changes.json    # All changes for this branch
└── tenants/

Change Format

Each change is recorded with:

{
  "id": "chg_1234567890",
  "timestamp": "2024-01-15T10:30:45.123Z",
  "type": "CREATE_TABLE",
  "details": {
    "table": "users",
    "columns": [
      {
        "name": "id",
        "type": "TEXT",
        "nullable": false
      },
      {
        "name": "email",
        "type": "TEXT",
        "nullable": false
      },
      {
        "name": "created_at",
        "type": "TEXT",
        "nullable": false
      }
    ]
  },
  "applied_to": ["main", "customer_a", "customer_b"]
}

Change Recording

Automatic Tracking

All schema operations through CinchDB are automatically tracked:

# This operation
db.create_table("products", [
    Column(name="name", type="TEXT"),
    Column(name="price", type="REAL")
])

# Generates this change record
{
  "id": "chg_1705316400123",
  "timestamp": "2024-01-15T10:40:00.123Z",
  "type": "CREATE_TABLE",
  "details": {
    "table": "products",
    "columns": [
      {"name": "id", "type": "TEXT", "nullable": false},
      {"name": "name", "type": "TEXT", "nullable": true},
      {"name": "price", "type": "REAL", "nullable": true},
      {"name": "created_at", "type": "TEXT", "nullable": false},
      {"name": "updated_at", "type": "TEXT", "nullable": false}
    ]
  }
}

Change IDs

Each change has a unique identifier: - Format: chg_<timestamp><random> - Globally unique across branches - Immutable once created - Used for ordering and deduplication

Change Application

Order Matters

Changes must be applied in chronological order:

# These changes must apply in sequence
changes = [
    {
        "type": "CREATE_TABLE",
        "details": {"table": "users", ...}
    },
    {
        "type": "ADD_COLUMN", 
        "details": {"table": "users", "column": "phone"}
    }
]

# Cannot add column before table exists!

Viewing Changes

CLI Commands

# View changes in current branch
cinch branch changes

# Output:
# Changes in branch 'feature.new-schema':
# 1. CREATE TABLE products (name TEXT, price REAL)
# 2. ADD COLUMN description TEXT TO products  
# 3. CREATE VIEW expensive_products AS SELECT * FROM products WHERE price > 100

Programmatic Access

def get_branch_changes(db, branch_name):
    """Get all changes for a branch."""
    if not db.is_local:
        return []

    changes_path = (
        db.project_dir / ".cinchdb" / "databases" / 
        db.database / "branches" / branch_name / "changes.json"
    )

    with open(changes_path) as f:
        return json.load(f)

# Analyze changes
changes = get_branch_changes(db, "feature.updates")
table_changes = [c for c in changes if "TABLE" in c["type"]]
column_changes = [c for c in changes if "COLUMN" in c["type"]]

Merge Behavior

Change Comparison

During merge, CinchDB compares change histories:

def can_merge(source_branch, target_branch):
    source_changes = get_changes(source_branch)
    target_changes = get_changes(target_branch)

    # Target must have all source's parent changes
    source_parent_ids = get_parent_change_ids(source_branch)
    target_ids = {c["id"] for c in target_changes}

    return source_parent_ids.issubset(target_ids)

Change Application

Merging applies new changes to target:

def merge_changes(source_branch, target_branch):
    source_changes = get_changes(source_branch)
    target_changes = get_changes(target_branch)

    # Find new changes
    target_ids = {c["id"] for c in target_changes}
    new_changes = [c for c in source_changes if c["id"] not in target_ids]

    # Apply in order
    for change in sorted(new_changes, key=lambda c: c["timestamp"]):
        apply_change_to_branch(change, target_branch)

Change Types in Detail

CREATE_TABLE

Records full table definition:

{
  "type": "CREATE_TABLE",
  "details": {
    "table": "orders",
    "columns": [
      {"name": "id", "type": "TEXT", "nullable": false},
      {"name": "user_id", "type": "TEXT", "nullable": true},
      {"name": "total", "type": "REAL", "nullable": true},
      {"name": "status", "type": "TEXT", "nullable": true}
    ],
    "indexes": [],
    "constraints": []
  }
}

ADD_COLUMN

Tracks column additions:

{
  "type": "ADD_COLUMN",
  "details": {
    "table": "users",
    "column": {
      "name": "phone",
      "type": "TEXT",
      "nullable": true,
      "default": null
    }
  }
}

CREATE_VIEW

Stores view definition:

{
  "type": "CREATE_VIEW",
  "details": {
    "view": "active_users",
    "sql": "SELECT * FROM users WHERE active = true",
    "columns": ["id", "name", "email", "active"]
  }
}

Best Practices

1. Atomic Changes

Make one logical change at a time:

# Good - single purpose
db.create_table("users", columns)

# Bad - multiple unrelated changes
db.create_table("users", columns)
db.create_table("products", columns)  # Separate change
db.add_column("orders", "notes")      # Separate change

2. Descriptive Changes

Changes should be self-documenting:

{
  "type": "ADD_COLUMN",
  "details": {
    "table": "users",
    "column": {
      "name": "email_verified",
      "type": "BOOLEAN",
      "nullable": false,
      "default": false
    }
  },
  "description": "Add email verification tracking"
}

Troubleshooting

Common Issues

  1. Change Order Violations
Error: Cannot add column to non-existent table
Solution: Ensure changes are applied in correct order
  1. Duplicate Changes
Error: Table already exists
Solution: Check if change was already applied
  1. Incompatible Changes
Error: Cannot merge - branches have diverged
Solution: Rebase or recreate branch from latest main

Debugging Changes

def debug_changes(branch_name):
    """Debug change tracking issues."""
    changes = get_branch_changes(db, branch_name)

    print(f"Total changes: {len(changes)}")
    print(f"Change types: {Counter(c['type'] for c in changes)}")

    # Check for issues
    tables_created = set()
    for change in changes:
        if change["type"] == "CREATE_TABLE":
            table = change["details"]["table"]
            if table in tables_created:
                print(f"WARNING: Duplicate CREATE_TABLE for {table}")
            tables_created.add(table)

        elif change["type"] == "ADD_COLUMN":
            table = change["details"]["table"]
            if table not in tables_created:
                print(f"WARNING: ADD_COLUMN to non-existent table {table}")

Next Steps