Claude Code Plugins

Community-maintained marketplace

Feedback

idempotent-migrations

@bd28/claude-automation
0
0

Make database migrations safe for redeployment with idempotent patterns

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name idempotent-migrations
version 1.0.0
description Make database migrations safe for redeployment with idempotent patterns
category database
tags migrations, database, drizzle, idempotency
applies_to schema-wizard, feature-builder
trigger when_creating_migrations
priority critical

Idempotent Migrations Skill

Purpose

Ensure all database migrations are safe to run multiple times without errors or unintended side effects. This prevents migration failures in CI/CD pipelines and makes deployments more reliable.

When to Apply This Skill

Apply this skill automatically when:

  • Creating new database migrations
  • Adding or modifying tables, columns, or constraints
  • Creating or updating database functions/triggers
  • Adding or modifying indexes
  • Creating or updating views

Do NOT skip this skill - it should be applied to ALL migrations.

How to Apply This Skill

1. Use Conditional Checks

Always wrap DDL statements in conditional checks to prevent errors on re-runs:

Creating Tables

CREATE TABLE IF NOT EXISTS users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL UNIQUE,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Adding Columns

DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_name = 'users' AND column_name = 'last_login'
  ) THEN
    ALTER TABLE users ADD COLUMN last_login TIMESTAMPTZ;
  END IF;
END $$;

Creating Indexes

CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

Adding Constraints

DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.table_constraints
    WHERE constraint_name = 'users_email_check'
  ) THEN
    ALTER TABLE users ADD CONSTRAINT users_email_check
    CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$');
  END IF;
END $$;

2. Handle Type Changes Safely

When modifying column types, use conditional logic:

DO $$
BEGIN
  IF EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_name = 'products'
    AND column_name = 'price'
    AND data_type = 'integer'
  ) THEN
    ALTER TABLE products ALTER COLUMN price TYPE DECIMAL(10,2);
  END IF;
END $$;

3. Drop Operations (Use with Caution)

Only drop objects if they exist:

DROP TABLE IF EXISTS old_table CASCADE;
DROP INDEX IF EXISTS old_index;
DROP FUNCTION IF EXISTS old_function(param_types) CASCADE;

Warning: Always verify dropping objects won't break production before deploying.

4. Use Transactions

Wrap migrations in transactions for atomicity:

BEGIN;

-- All migration statements here

COMMIT;

For Drizzle migrations, this is automatic.

5. Test Idempotency Locally

Before committing, run the migration twice locally:

# First run
npm run db:migrate

# Second run - should complete without errors
npm run db:migrate

6. Handle Default Values

When adding columns with defaults, use conditional logic to avoid resetting existing values:

DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_name = 'users' AND column_name = 'role'
  ) THEN
    ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user' NOT NULL;
  END IF;
END $$;

Examples

Good Examples

Table Creation

CREATE TABLE IF NOT EXISTS posts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  content TEXT,
  published BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_posts_user_id ON posts(user_id);
CREATE INDEX IF NOT EXISTS idx_posts_published ON posts(published) WHERE published = TRUE;

Column Addition

-- Add 'verified' column to users table
DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_name = 'users' AND column_name = 'verified'
  ) THEN
    ALTER TABLE users ADD COLUMN verified BOOLEAN DEFAULT FALSE NOT NULL;
  END IF;
END $$;

Function Creation

CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Create trigger only if it doesn't exist
DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM pg_trigger WHERE tgname = 'update_posts_updated_at'
  ) THEN
    CREATE TRIGGER update_posts_updated_at
    BEFORE UPDATE ON posts
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();
  END IF;
END $$;

Bad Examples

-- ❌ Not idempotent - will fail on second run
CREATE TABLE users (
  id UUID PRIMARY KEY,
  email TEXT NOT NULL
);

-- ❌ Not idempotent - will fail if column exists
ALTER TABLE users ADD COLUMN last_login TIMESTAMPTZ;

-- ❌ Not idempotent - will fail if index exists
CREATE INDEX idx_users_email ON users(email);

-- ❌ Not idempotent - will fail if constraint exists
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE(email);

Integration Points

This skill is automatically applied by:

  • schema-wizard agent - When generating migrations (step 2)
  • feature-builder agent - When database changes are needed (step 3)

Checklist

When applying this skill, verify:

  • All CREATE statements use IF NOT EXISTS
  • All DROP statements use IF EXISTS
  • Column additions check for existing columns
  • Constraint additions check for existing constraints
  • Index creations use IF NOT EXISTS
  • Type changes check current type before modifying
  • Migration tested locally by running twice
  • No hard-coded values that might override existing data
  • Transactions used where appropriate (automatic in Drizzle)

Common Patterns

Check if Table Exists

DO $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'my_table') THEN
    CREATE TABLE my_table (...);
  END IF;
END $$;

Check if Column Exists

DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_name = 'my_table' AND column_name = 'my_column'
  ) THEN
    ALTER TABLE my_table ADD COLUMN my_column TEXT;
  END IF;
END $$;

Check if Index Exists

CREATE INDEX IF NOT EXISTS idx_name ON table_name(column_name);

Check if Constraint Exists

DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.table_constraints
    WHERE constraint_name = 'my_constraint'
  ) THEN
    ALTER TABLE my_table ADD CONSTRAINT my_constraint CHECK (...);
  END IF;
END $$;

Related Skills

  • rls-security-patterns - Apply RLS policies idempotently
  • test-strategy-patterns - Test migrations thoroughly before deployment

References