jpicklyk / database-implementation

Database schema design, migrations, query optimization with SQL, Exposed ORM, Flyway. Use for database, migration, schema, sql, flyway tags. Provides migration patterns, validation commands, rollback strategies.

1 views
0 installs

Skill Content

---
name: Database Implementation
description: Database schema design, migrations, query optimization with SQL, Exposed ORM, Flyway. Use for database, migration, schema, sql, flyway tags. Provides migration patterns, validation commands, rollback strategies.
allowed-tools: Read, Write, Edit, Bash, Grep, Glob
---

# Database Implementation Skill

Domain-specific guidance for database schema design, migrations, and data modeling.

## When To Use This Skill

Load this Skill when task has tags:
- `database`, `migration`, `schema`, `sql`, `flyway`
- `exposed`, `orm`, `query`, `index`, `constraint`

## Validation Commands

### Run Migrations
```bash
# Gradle + Flyway
./gradlew flywayMigrate

# Test migration on clean database
./gradlew flywayClean flywayMigrate

# Check migration status
./gradlew flywayInfo

# Validate migrations
./gradlew flywayValidate
```

### Run Tests
```bash
# Migration tests
./gradlew test --tests "*migration*"

# Database integration tests
./gradlew test --tests "*Repository*"

# All tests
./gradlew test
```

## Success Criteria (Before Completing Task)

✅ **Migration runs without errors** on clean database
✅ **Schema matches design specifications**
✅ **Indexes created correctly**
✅ **Constraints validate as expected**
✅ **Rollback works** (if applicable)
✅ **Tests pass** with new schema

## Common Database Tasks

### Creating Migrations
- Add tables with columns, constraints
- Create indexes for performance
- Add foreign keys for referential integrity
- Modify existing schema (ALTER TABLE)
- Seed data (reference data)

### ORM Models
- Map entities to tables (Exposed, JPA)
- Define relationships (one-to-many, many-to-many)
- Configure cascading behavior
- Define custom queries

### Query Optimization
- Add indexes for frequently queried columns
- Analyze query plans (EXPLAIN)
- Optimize N+1 query problems
- Use appropriate JOIN types

## Migration Patterns

### Create Table

```sql
-- V001__create_users_table.sql
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Indexes
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);
```

### Add Column

```sql
-- V002__add_users_phone.sql
ALTER TABLE users
ADD COLUMN phone VARCHAR(20);

-- Add with default value
ALTER TABLE users
ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT true;
```

### Create Foreign Key

```sql
-- V003__create_tasks_table.sql
CREATE TABLE tasks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    title VARCHAR(500) NOT NULL,
    user_id UUID NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

    -- Foreign key with cascade
    CONSTRAINT fk_tasks_user
        FOREIGN KEY (user_id)
        REFERENCES users(id)
        ON DELETE CASCADE
);

CREATE INDEX idx_tasks_user_id ON tasks(user_id);
```

### Create Junction Table (Many-to-Many)

```sql
-- V004__create_user_roles.sql
CREATE TABLE user_roles (
    user_id UUID NOT NULL,
    role_id UUID NOT NULL,
    assigned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY (user_id, role_id),

    CONSTRAINT fk_user_roles_user
        FOREIGN KEY (user_id)
        REFERENCES users(id)
        ON DELETE CASCADE,

    CONSTRAINT fk_user_roles_role
        FOREIGN KEY (role_id)
        REFERENCES roles(id)
        ON DELETE CASCADE
);

CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
CREATE INDEX idx_user_roles_role_id ON user_roles(role_id);
```

## Testing Migrations

### Test Migration Execution

```kotlin
@Test
fun `migration V004 creates user_roles table`() {
    // Arrange - Clean database
    flyway.clean()

    // Act - Run migrations
    flyway.migrate()

    // Assert - Check table exists
    val tableExists = database.useConnection { connection ->
        val meta = connection.metaData
        val rs = meta.getTables(null, null, "user_roles", null)
        rs.next()
    }

    assertTrue(tableExists, "user_roles table should exist after migration")
}
```

### Test Constraints

```kotlin
@Test
fun `user_roles enforces foreign key constraint`() {
    // Arrange
    val invalidUserId = UUID.randomUUID()
    val role = createTestRole()

    // Act & Assert
    assertThrows<SQLException> {
        database.transaction {
            UserRoles.insert {
                it[userId] = invalidUserId  // Invalid - user doesn't exist
                it[roleId] = role.id
            }
        }
    }
}
```

## Common Blocker Scenarios

### Blocker 1: Migration Fails on Existing Data

**Issue:** Adding NOT NULL column to table with existing rows
```
ERROR: column "status" contains null values
```

**What to try:**
- Add column as nullable first
- Update existing rows with default value
- Then alter column to NOT NULL

**Example fix:**
```sql
-- Step 1: Add nullable
ALTER TABLE tasks ADD COLUMN status VARCHAR(20);

-- Step 2: Update existing rows
UPDATE tasks SET status = 'pending' WHERE status IS NULL;

-- Step 3: Make NOT NULL
ALTER TABLE tasks ALTER COLUMN status SET NOT NULL;
```

### Blocker 2: Circular Foreign Key Dependencies

**Issue:** Table A references B, B references A - which to create first?

**What to try:**
- Create both tables without foreign keys first
- Add foreign keys in separate migration after both exist

**Example:**
```sql
-- V001: Create tables without FKs
CREATE TABLE users (...);
CREATE TABLE profiles (...);

-- V002: Add foreign keys
ALTER TABLE users ADD CONSTRAINT fk_users_profile ...;
ALTER TABLE profiles ADD CONSTRAINT fk_profiles_user ...;
```

### Blocker 3: Index Creation Takes Too Long

**Issue:** Creating index on large table times out

**What to try:**
- Use CREATE INDEX CONCURRENTLY (PostgreSQL)
- Create index during low-traffic period
- Check if similar index already exists

### Blocker 4: Data Type Mismatch

**Issue:** ORM expects UUID but database has VARCHAR

**What to try:**
- Check migration SQL - correct type used?
- Check ORM mapping - correct type specified?
- Migrate data type if needed:
  ```sql
  ALTER TABLE tasks ALTER COLUMN id TYPE UUID USING id::uuid;
  ```

### Blocker 5: Missing Prerequisite Table

**Issue:** Foreign key references table that doesn't exist yet

**What to try:**
- Check migration order - migrations run in version order (V001, V002, etc.)
- Ensure referenced table created in earlier migration
- Check for typos in table names

**If blocked:** Report to orchestrator - migration order issue or missing prerequisite

## Blocker Report Format

```
⚠️ BLOCKED - Requires Senior Engineer

Issue: [Specific problem - migration fails, constraint violation, etc.]

Attempted Fixes:
- [What you tried #1]
- [What you tried #2]
- [Why attempts didn't work]

Root Cause (if known): [Your analysis]

Partial Progress: [What work you DID complete]

Context for Senior Engineer:
- Migration SQL: [Paste migration]
- Error output: [Database error]
- Related migrations: [Dependencies]

Requires: [What needs to happen]
```

## Exposed ORM Patterns

### Table Definition

```kotlin
object Users : UUIDTable("users") {
    val email = varchar("email", 255).uniqueIndex()
    val passwordHash = varchar("password_hash", 255)
    val name = varchar("name", 255)
    val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp())
    val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp())
}
```

### Foreign Key Relationship

```kotlin
object Tasks : UUIDTable("tasks") {
    val title = varchar("title", 500)
    val userId = reference("user_id", Users)
    val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp())
}
```

### Query with Join

```kotlin
fun findTasksWithUser(userId: UUID): List<TaskWithUser> {
    return (Tasks innerJoin Users)
        .select { Tasks.userId eq userId }
        .map { row ->
            TaskWithUser(
                task = rowToTask(row),
                user = rowToUser(row)
            )
        }
}
```

## Rollback Strategies

### Reversible Migrations

**Good (can rollback):**
- Adding nullable columns
- Adding indexes
- Creating new tables (if no data)

**Difficult to rollback:**
- Dropping columns (data loss)
- Changing data types (data transformation)
- Deleting tables (data loss)

### Include Rollback SQL

For complex migrations, document rollback steps:
```sql
-- Migration: V005__add_user_status.sql
ALTER TABLE users ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'active';

-- Rollback (document in comments):
-- ALTER TABLE users DROP COLUMN status;
```

## Performance Tips

### Indexing Strategy

✅ **DO create indexes on:**
- Foreign key columns
- Frequently queried columns (WHERE, JOIN)
- Columns used in ORDER BY
- Unique constraints

❌ **DON'T create indexes on:**
- Small tables (< 1000 rows)
- Columns that change frequently
- Low cardinality columns (gender, boolean)

### Query Optimization

```sql
-- ❌ BAD - Missing index, full table scan
SELECT * FROM users WHERE email = 'user@example.com';

-- ✅ GOOD - Index on email column
CREATE INDEX idx_users_email ON users(email);

-- ❌ BAD - N+1 query problem
SELECT * FROM users;  -- 1 query
SELECT * FROM tasks WHERE user_id = ?;  -- N queries (one per user)

-- ✅ GOOD - Single query with JOIN
SELECT u.*, t.*
FROM users u
LEFT JOIN tasks t ON t.user_id = u.id;
```

## Common Patterns to Follow

1. **Sequential migration versioning** (V001, V002, V003...)
2. **Descriptive migration names** (V004__add_user_status.sql)
3. **Idempotent migrations** (can run multiple times safely)
4. **Test on clean database** before committing
5. **Foreign keys with indexes** for performance
6. **NOT NULL with defaults** for required fields
7. **Timestamps for audit trail** (created_at, updated_at)

## What NOT to Do

❌ Don't modify existing migrations (create new one)
❌ Don't drop columns without data backup
❌ Don't forget indexes on foreign keys
❌ Don't use SELECT * in production queries
❌ Don't skip testing migrations on clean database
❌ Don't forget CASCADE behavior on foreign keys
❌ Don't create migrations that depend on data state

## Focus Areas

When reading task sections, prioritize:
- `requirements` - What schema changes needed
- `technical-approach` - Migration strategy
- `data-model` - Entity relationships
- `migration` - Specific SQL requirements

## Remember

- **Test on clean database** - always validate migration from scratch
- **Indexes on foreign keys** - critical for performance
- **Sequential versioning** - V001, V002, V003...
- **Descriptive names** - migration filename explains what it does
- **Report blockers promptly** - constraint issues, circular dependencies
- **Document rollback** - comment how to reverse if needed
- **Validation is mandatory** - migration must succeed before completion

## Additional Resources

For deeper patterns and examples, see:
- **PATTERNS.md** - Complex schema patterns, performance optimization (load if needed)
- **BLOCKERS.md** - Detailed database-specific blockers (load if stuck)
- **examples.md** - Complete migration examples (load if uncertain)