Database Schema Validator Skill
database-schema-validator
Validates database schemas, Kysely types, and migrations. Use when checking schema correctness or migration safety.
SKILL.md
Full skill instructions
Database Schema Validator Skill
This skill provides database schema validation for Kysely-based projects. It ensures type safety, migration correctness, and schema best practices.
Capabilities
Schema Type Validation
- Verify Kysely schema type definitions
- Check column type consistency
- Validate foreign key relationships
- Ensure proper nullable/required field definitions
- Detect missing or incorrect type mappings
Migration Analysis
- Validate migration file structure
- Check migration naming conventions
- Verify migration ordering
- Detect unsafe migrations (data loss risks)
- Ensure rollback compatibility
Best Practices Enforcement
- Check for proper indexing
- Verify primary key definitions
- Validate unique constraints
- Ensure proper use of timestamps
- Check for naming convention compliance
Consistency Checks
- Compare schema types with actual migrations
- Verify consistency between schema.ts and migration files
- Check for orphaned foreign keys
- Detect schema drift
Usage Examples
Validate Schema Type Definitions
// Good: Properly typed schema
export interface Database {
users: {
id: Generated<string>;
email: string;
name: string | null;
created_at: Generated<string>;
updated_at: string;
};
posts: {
id: Generated<string>;
user_id: string; // Foreign key
title: string;
content: string;
published: Generated<boolean>;
created_at: Generated<string>;
};
}
// Bad: Inconsistent or missing types
export interface Database {
users: {
id: string; // ❌ Should use Generated<string>
email: any; // ❌ Should have specific type
// ❌ Missing created_at
};
}
Migration File Structure
// Good: Proper migration structure
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('users')
.addColumn('id', 'text', (col) => col.primaryKey())
.addColumn('email', 'text', (col) => col.notNull().unique())
.addColumn('name', 'text')
.addColumn('created_at', 'text', (col) =>
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
)
.execute();
// Create index for frequently queried columns
await db.schema
.createIndex('users_email_idx')
.on('users')
.column('email')
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('users').execute();
}
// Bad: Missing down migration
export async function up(db: Kysely<any>): Promise<void> {
// ... migration code
}
// ❌ No down function - can't rollback!
Foreign Key Validation
// Good: Proper foreign key with constraints
await db.schema
.createTable('posts')
.addColumn('id', 'text', (col) => col.primaryKey())
.addColumn('user_id', 'text', (col) =>
col.notNull().references('users.id').onDelete('cascade')
)
.execute();
// Bad: No foreign key constraint
await db.schema
.createTable('posts')
.addColumn('user_id', 'text') // ❌ No foreign key reference
.execute();
Validation Checks
Schema Type Checks
1. Primary Keys
// ✅ Correct
id: Generated<string>;
// ❌ Incorrect (missing Generated)
id: string;
2. Timestamps
// ✅ Correct (auto-generated)
created_at: Generated<string>;
// ✅ Correct (manually set)
updated_at: string;
// ❌ Incorrect (should be Generated or string)
created_at: Date;
3. Nullable Fields
// ✅ Correct
name: string | null;
// ❌ Incorrect (TypeScript null vs SQL NULL)
name?: string; // Optional property, not nullable column
4. Foreign Keys
// ✅ Correct (matches referenced table's PK type)
users: {
id: Generated<string>;
};
posts: {
user_id: string; // Matches users.id type
};
// ❌ Incorrect (type mismatch)
users: {
id: Generated<number>;
};
posts: {
user_id: string; // ❌ Should be number
};
Migration Safety Checks
Safe Migrations:
- Adding new tables
- Adding new columns (with defaults or nullable)
- Adding indexes
- Creating new foreign keys (if data integrity allows)
Unsafe Migrations (require caution):
- Dropping tables (data loss)
- Dropping columns (data loss)
- Changing column types (potential data loss)
- Adding NOT NULL constraints (fails if existing NULLs)
- Modifying foreign keys (may fail on existing data)
Migration Naming Convention
Format: <sequence>_<description>.ts
Good examples:
- 001_initial_schema.ts
- 002_add_user_roles.ts
- 003_add_post_categories.ts
Bad examples:
- migration.ts // ❌ No sequence number
- 1_users.ts // ❌ Inconsistent padding
- add-posts.ts // ❌ No sequence number
- 002_AddPosts.ts // ❌ PascalCase instead of snake_case
Validation Workflow
1. Schema Type Validation
# Check TypeScript compilation
npx tsc --noEmit
# Verify schema types match database
# (Manual review or custom validation script)
2. Migration File Validation
Check each migration for:
- Proper naming convention
- Presence of both
upanddownfunctions - Correct Kysely API usage
- Proper error handling
- Transaction safety
3. Schema Consistency Check
// Example validation script
import { Database } from './schema';
import { Kysely } from 'kysely';
async function validateSchema(db: Kysely<Database>) {
// 1. Check if all tables exist
const tables = await db.introspection.getTables();
// 2. Verify foreign key references
for (const table of tables) {
const foreignKeys = await db.introspection.getTableMetadata(table.name);
// Validate each FK points to existing table/column
}
// 3. Check for orphaned rows
// Custom queries to detect FK violations
}
Common Issues and Solutions
Issue: Schema Type Mismatch
Problem:
// schema.ts
posts: {
user_id: string;
};
// Migration has:
.addColumn('user_id', 'integer') // ❌ Type mismatch
Solution:
// Fix migration to match schema
.addColumn('user_id', 'text') // ✅ Now matches
Issue: Missing Down Migration
Problem:
export async function up(db: Kysely<any>): Promise<void> {
await db.schema.createTable('users').execute();
}
// ❌ No down function
Solution:
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('users').execute();
}
Issue: Unsafe Column Deletion
Problem:
// Migration that drops column
await db.schema
.alterTable('users')
.dropColumn('old_field')
.execute();
// ❌ Data loss!
Solution:
// 1. First, migrate data to new column
await db
.updateTable('users')
.set({ new_field: sql`old_field` })
.execute();
// 2. Then drop old column in a later migration
// Give users time to rollback if needed
Issue: Missing Foreign Key Constraint
Problem:
// Schema defines relationship
posts: {
user_id: string; // Implies FK to users.id
};
// But migration doesn't enforce it
.addColumn('user_id', 'text') // ❌ No constraint
Solution:
.addColumn('user_id', 'text', (col) =>
col.notNull().references('users.id').onDelete('cascade')
)
Best Practices
Schema Design
1. Use Consistent Naming
- Tables: plural, snake_case (e.g.,
user_profiles) - Columns: singular, snake_case (e.g.,
created_at) - Foreign keys:
{table}_id(e.g.,user_id)
2. Always Include Timestamps
created_at: Generated<string>;
updated_at: string;
3. Use Appropriate Types
- IDs:
text(for UUIDs) orinteger(for auto-increment) - Booleans:
booleanwith defaults - Timestamps:
text(ISO 8601) orinteger(Unix timestamp) - Money:
integer(cents) to avoid floating point issues
4. Index Frequently Queried Columns
// Create index for email lookups
await db.schema
.createIndex('users_email_idx')
.on('users')
.column('email')
.execute();
Migration Best Practices
1. Make Migrations Atomic
Use transactions when possible:
await db.transaction().execute(async (trx) => {
await trx.schema.createTable('users').execute();
await trx.schema.createTable('posts').execute();
});
2. Test Migrations Both Ways
- Test
upmigration applies successfully - Test
downmigration rolls back completely - Verify data integrity after each
3. Version Control
- Always commit schema.ts and migrations together
- Never modify existing migrations that have been applied
- Create new migrations for schema changes
4. Document Complex Migrations
/**
* Migration: Add user_roles table
*
* This migration creates a many-to-many relationship between
* users and roles through the user_roles join table.
*
* Rollback: Drops user_roles table and all role assignments.
*/
export async function up(db: Kysely<any>): Promise<void> {
// ...
}
Output Format
When reporting validation results, use this format:
# Database Schema Validation Report
## Summary
- Total Tables: X
- Total Migrations: Y
- Issues Found: Z
## Critical Issues
[Issues that must be fixed immediately]
### Schema Type Mismatch
**Table:** `posts`
**Column:** `user_id`
**Issue:** Schema type `string` doesn't match migration type `integer`
**Fix:**
\`\`\`typescript
// Update migration or schema to match
.addColumn('user_id', 'text') // Match schema type
\`\`\`
## Warnings
[Issues that should be addressed]
## Recommendations
[Suggestions for improvement]
## Schema Consistency
✅ All foreign keys have proper constraints
✅ All tables have timestamps
✅ All migrations have down functions
⚠️ Missing indexes on frequently queried columns
Validation Checklist
- All tables defined in schema.ts have corresponding migrations
- All foreign keys in schema match migration definitions
- All migrations have both
upanddownfunctions - Migration files follow naming convention
- No
anytypes in schema definitions - All required fields use
notNull()in migrations - Timestamps use
Generated<string>for auto-generated fields - Foreign keys have proper
onDeletebehavior - Frequently queried columns are indexed
- No unsafe migrations without data migration plan
