axiom-database-migration
Use when adding/modifying database columns, encountering "FOREIGN KEY constraint failed", "no such column", "cannot add NOT NULL column" errors, or creating schema migrations for SQLite/GRDB/SQLiteData - prevents data loss with safe migration patterns and testing workflows for iOS/macOS apps
Install
mkdir -p .claude/skills/axiom-database-migration && curl -L -o skill.zip "https://mcp.directory/api/skills/download/7227" && unzip -o skill.zip -d .claude/skills/axiom-database-migration && rm skill.zipInstalls to .claude/skills/axiom-database-migration
About this skill
Database Migration
Overview
Safe database schema evolution for production apps with user data. Core principle Migrations are immutable after shipping. Make them additive, idempotent, and thoroughly tested.
Example Prompts
These are real questions developers ask that this skill is designed to answer:
1. "I need to add a new column to store user preferences, but the app is already live with user data. How do I do this safely?"
→ The skill covers safe additive patterns for adding columns without losing existing data, including idempotency checks
2. "I'm getting 'cannot add NOT NULL column' errors when I try to migrate. What does this mean and how do I fix it?"
→ The skill explains why NOT NULL columns fail with existing rows, and shows the safe pattern (nullable first, backfill later)
3. "I need to change a column from text to integer. Can I just ALTER the column type?"
→ The skill demonstrates the safe pattern: add new column → migrate data → deprecate old (NEVER delete)
4. "I'm adding a foreign key relationship between tables. How do I add the relationship without breaking existing data?"
→ The skill covers safe foreign key patterns: add column → populate data → add index (SQLite limitations explained)
5. "Users are reporting crashes after the last update. I changed a migration but the app is already in production. What do I do?"
→ The skill explains migrations are immutable after shipping; shows how to create a new migration to fix the issue rather than modifying the old one
⛔ NEVER Do These (Data Loss Risk)
These actions DESTROY user data in production
❌ NEVER use DROP TABLE with user data ❌ NEVER modify shipped migrations (create new one instead) ❌ NEVER recreate tables to change schema (loses data) ❌ NEVER add NOT NULL column without DEFAULT value ❌ NEVER delete columns (SQLite doesn't support DROP COLUMN safely)
If you're tempted to do any of these, STOP and use the safe patterns below.
Mandatory Rules
ALWAYS follow these
- Additive only Add new columns/tables, never delete
- Idempotent Check existence before creating (safe to run twice)
- Transactional Wrap entire migration in single transaction
- Test both paths Fresh install AND migration from previous version
- Nullable first Add columns as NULL, backfill later if needed
- Immutable Once shipped to users, migrations cannot be changed
Safe Patterns
Adding Column (Most Common)
// ✅ Safe pattern
func migration00X_AddNewColumn() throws {
try database.write { db in
// 1. Check if column exists (idempotency)
let hasColumn = try db.columns(in: "tableName")
.contains { $0.name == "newColumn" }
if !hasColumn {
// 2. Add as nullable (works with existing rows)
try db.execute(sql: """
ALTER TABLE tableName
ADD COLUMN newColumn TEXT
""")
}
}
}
Why this works
- Nullable columns don't require DEFAULT
- Existing rows get NULL automatically
- No data transformation needed
- Safe for users upgrading from old versions
Adding Column with Default Value
// ✅ Safe pattern with default
func migration00X_AddColumnWithDefault() throws {
try database.write { db in
let hasColumn = try db.columns(in: "tracks")
.contains { $0.name == "playCount" }
if !hasColumn {
try db.execute(sql: """
ALTER TABLE tracks
ADD COLUMN playCount INTEGER DEFAULT 0
""")
}
}
}
Changing Column Type (Advanced)
Pattern: Add new column → migrate data → deprecate old (NEVER delete)
// ✅ Safe pattern for type change
func migration00X_ChangeColumnType() throws {
try database.write { db in
// Step 1: Add new column with new type
try db.execute(sql: """
ALTER TABLE users
ADD COLUMN age_new INTEGER
""")
// Step 2: Migrate existing data
try db.execute(sql: """
UPDATE users
SET age_new = CAST(age_old AS INTEGER)
WHERE age_old IS NOT NULL
""")
// Step 3: Application code uses age_new going forward
// (Never delete age_old column - just stop using it)
}
}
Adding Foreign Key Constraint
// ✅ Safe pattern for foreign keys
func migration00X_AddForeignKey() throws {
try database.write { db in
// Step 1: Add new column (nullable initially)
try db.execute(sql: """
ALTER TABLE tracks
ADD COLUMN album_id TEXT
""")
// Step 2: Populate the data
try db.execute(sql: """
UPDATE tracks
SET album_id = (
SELECT id FROM albums
WHERE albums.title = tracks.album_name
)
""")
// Step 3: Add index (helps query performance)
try db.execute(sql: """
CREATE INDEX IF NOT EXISTS idx_tracks_album_id
ON tracks(album_id)
""")
// Note: SQLite doesn't allow adding FK constraints to existing tables
// The foreign key relationship is enforced at the application level
}
}
Complex Schema Refactoring
Pattern: Break into multiple migrations
// Migration 1: Add new structure
func migration010_AddNewTable() throws {
try database.write { db in
try db.execute(sql: """
CREATE TABLE IF NOT EXISTS new_structure (
id TEXT PRIMARY KEY,
data TEXT
)
""")
}
}
// Migration 2: Copy data
func migration011_MigrateData() throws {
try database.write { db in
try db.execute(sql: """
INSERT INTO new_structure (id, data)
SELECT id, data FROM old_structure
""")
}
}
// Migration 3: Add indexes
func migration012_AddIndexes() throws {
try database.write { db in
try db.execute(sql: """
CREATE INDEX IF NOT EXISTS idx_new_structure_data
ON new_structure(data)
""")
}
}
// Old structure stays around (deprecated in code)
Testing Checklist
BEFORE deploying any migration
// Test 1: Migration path (CRITICAL - tests data preservation)
@Test func migrationFromV1ToV2Succeeds() async throws {
let db = try Database(inMemory: true)
// Simulate v1 schema
try db.write { db in
try db.execute(sql: "CREATE TABLE tableName (id TEXT PRIMARY KEY)")
try db.execute(sql: "INSERT INTO tableName (id) VALUES ('test1')")
}
// Run v2 migration
try db.runMigrations()
// Verify data survived + new column exists
try db.read { db in
let count = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM tableName")
#expect(count == 1) // Data preserved
let columns = try db.columns(in: "tableName").map { $0.name }
#expect(columns.contains("newColumn")) // New column exists
}
}
Test 2 Fresh install (run all migrations, verify final schema)
@Test func freshInstallCreatesCorrectSchema() async throws {
let db = try Database(inMemory: true)
// Run all migrations
try db.runMigrations()
// Verify final schema
try db.read { db in
let tables = try db.tables()
#expect(tables.contains("tableName"))
let columns = try db.columns(in: "tableName").map { $0.name }
#expect(columns.contains("id"))
#expect(columns.contains("newColumn"))
}
}
Test 3 Idempotency (run migrations twice, should not throw)
@Test func migrationsAreIdempotent() async throws {
let db = try Database(inMemory: true)
// Run migrations twice
try db.runMigrations()
try db.runMigrations() // Should not throw
// Verify still correct
try db.read { db in
let count = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM tableName")
#expect(count == 0) // No duplicate data
}
}
Manual testing (before TestFlight)
- Install v(n-1) build on device → add real user data
- Install v(n) build (with new migration)
- Verify: App launches, data visible, no crashes
Decision Tree
What are you trying to do?
├─ Add new column?
│ └─ ALTER TABLE ADD COLUMN (nullable) → Done
├─ Add column with default?
│ └─ ALTER TABLE ADD COLUMN ... DEFAULT value → Done
├─ Change column type?
│ └─ Add new column → Migrate data → Deprecate old → Done
├─ Delete column?
│ └─ Mark as deprecated in code → Never delete from schema → Done
├─ Rename column?
│ └─ Add new column → Migrate data → Deprecate old → Done
├─ Add foreign key?
│ └─ Add column → Populate data → Add index → Done
└─ Complex refactor?
└─ Break into multiple migrations → Test each step → Done
Common Errors
| Error | Fix |
|---|---|
FOREIGN KEY constraint failed | Check parent row exists, or disable FK temporarily |
no such column: columnName | Add migration to create column |
cannot add NOT NULL column | Use nullable column first, backfill in separate migration |
table tableName already exists | Add IF NOT EXISTS clause |
duplicate column name | Check if column exists before adding (idempotency) |
Common Mistakes
❌ Adding NOT NULL without DEFAULT
// ❌ Fails on existing data
ALTER TABLE albums ADD COLUMN rating INTEGER NOT NULL
✅ Correct: Add as nullable first
ALTER TABLE albums ADD COLUMN rating INTEGER // NULL allowed
// Backfill in separate migration if needed
UPDATE albums SET rating = 0 WHERE rating IS NULL
❌ Forgetting to check for existence — Always add IF NOT EXISTS or manual check
❌ Modifying shipped migrations — Create new migration instead
❌ Not testing migration path — Always test upgrade from previous version
GRDB-Specific Patterns
DatabaseMigrator Setup
var migrator = DatabaseM
---
*Content truncated.*
More by CharlesWiltgen
View all skills by CharlesWiltgen →You might also like
flutter-development
aj-geddes
Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.
drawio-diagrams-enhanced
jgtolentino
Create professional draw.io (diagrams.net) diagrams in XML format (.drawio files) with integrated PMP/PMBOK methodologies, extensive visual asset libraries, and industry-standard professional templates. Use this skill when users ask to create flowcharts, swimlane diagrams, cross-functional flowcharts, org charts, network diagrams, UML diagrams, BPMN, project management diagrams (WBS, Gantt, PERT, RACI), risk matrices, stakeholder maps, or any other visual diagram in draw.io format. This skill includes access to custom shape libraries for icons, clipart, and professional symbols.
ui-ux-pro-max
nextlevelbuilder
"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."
godot
bfollington
This skill should be used when working on Godot Engine projects. It provides specialized knowledge of Godot's file formats (.gd, .tscn, .tres), architecture patterns (component-based, signal-driven, resource-based), common pitfalls, validation tools, code templates, and CLI workflows. The `godot` command is available for running the game, validating scripts, importing resources, and exporting builds. Use this skill for tasks involving Godot game development, debugging scene/resource files, implementing game systems, or creating new Godot components.
nano-banana-pro
garg-aayush
Generate and edit images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Use when the user asks to generate, create, edit, modify, change, alter, or update images. Also use when user references an existing image file and asks to modify it in any way (e.g., "modify this image", "change the background", "replace X with Y"). Supports both text-to-image generation and image-to-image editing with configurable resolution (1K default, 2K, or 4K for high resolution). DO NOT read the image file first - use this skill directly with the --input-image parameter.
fastapi-templates
wshobson
Create production-ready FastAPI projects with async patterns, dependency injection, and comprehensive error handling. Use when building new FastAPI applications or setting up backend API projects.
Related MCP Servers
Browse all serversMCP Toolbox for Databases by Google. An open-source server that lets AI agents query Cloud SQL, Spanner, AlloyDB, and ot
Claude Context offers semantic code search and indexing with vector embeddings and AST-based code splitting. Natural lan
Cipher empowers agents with persistent memory using vector databases and embeddings for seamless context retention and t
Connect Supabase projects to AI with Supabase MCP Server. Standardize LLM communication for secure, efficient developmen
Boost Postgres performance with Postgres MCP Pro—AI-driven index tuning, health checks, and safe, intelligent SQL optimi
DBHub: Universal database gateway to view SQLite database, run sequel queries & browse tables. Secure, safe, and easy-to
Stay ahead of the MCP ecosystem
Get weekly updates on new skills and servers.