Initial commit
This commit is contained in:
336
skills/schema-reviewer/SKILL.md
Normal file
336
skills/schema-reviewer/SKILL.md
Normal file
@@ -0,0 +1,336 @@
|
||||
---
|
||||
name: schema-reviewer
|
||||
description: |
|
||||
WHEN: Database schema review, table design, normalization, constraints, index planning
|
||||
WHAT: Normalization analysis + Constraint validation + Index strategy + Data types + Relationship design
|
||||
WHEN NOT: Query optimization → sql-optimizer, ORM code → orm-reviewer
|
||||
---
|
||||
|
||||
# Schema Reviewer Skill
|
||||
|
||||
## Purpose
|
||||
Reviews database schema design for normalization, constraints, indexes, and best practices.
|
||||
|
||||
## When to Use
|
||||
- Database schema review
|
||||
- Table design review
|
||||
- Normalization check
|
||||
- Index planning
|
||||
- Constraint validation
|
||||
|
||||
## Project Detection
|
||||
- Schema files (`.sql`, `schema.prisma`)
|
||||
- Migration files
|
||||
- Entity definitions
|
||||
- Database documentation
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Analyze Schema
|
||||
```
|
||||
**Database**: PostgreSQL/MySQL
|
||||
**Tables**: 15
|
||||
**Relationships**: 1:N, N:M
|
||||
**Normalization**: 3NF
|
||||
```
|
||||
|
||||
### Step 2: Select Review Areas
|
||||
**AskUserQuestion:**
|
||||
```
|
||||
"Which areas to review?"
|
||||
Options:
|
||||
- Full schema review (recommended)
|
||||
- Normalization and design
|
||||
- Constraints and integrity
|
||||
- Index strategy
|
||||
- Data type optimization
|
||||
multiSelect: true
|
||||
```
|
||||
|
||||
## Detection Rules
|
||||
|
||||
### Normalization
|
||||
| Check | Recommendation | Severity |
|
||||
|-------|----------------|----------|
|
||||
| Repeating groups | Move to separate table | HIGH |
|
||||
| Partial dependency | Apply 2NF | MEDIUM |
|
||||
| Transitive dependency | Apply 3NF | MEDIUM |
|
||||
| Over-normalization | Consider denormalization for reads | LOW |
|
||||
|
||||
```sql
|
||||
-- BAD: 1NF violation (repeating groups)
|
||||
CREATE TABLE orders (
|
||||
id INT PRIMARY KEY,
|
||||
product1_id INT,
|
||||
product1_qty INT,
|
||||
product2_id INT,
|
||||
product2_qty INT,
|
||||
product3_id INT,
|
||||
product3_qty INT
|
||||
);
|
||||
|
||||
-- GOOD: Normalized with separate table
|
||||
CREATE TABLE orders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE order_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
order_id INT NOT NULL REFERENCES orders(id),
|
||||
product_id INT NOT NULL REFERENCES products(id),
|
||||
quantity INT NOT NULL CHECK (quantity > 0),
|
||||
price DECIMAL(10,2) NOT NULL
|
||||
);
|
||||
|
||||
-- BAD: 2NF violation (partial dependency)
|
||||
CREATE TABLE order_items (
|
||||
order_id INT,
|
||||
product_id INT,
|
||||
product_name VARCHAR(100), -- Depends only on product_id!
|
||||
quantity INT,
|
||||
PRIMARY KEY (order_id, product_id)
|
||||
);
|
||||
|
||||
-- GOOD: Product name in products table only
|
||||
CREATE TABLE order_items (
|
||||
order_id INT REFERENCES orders(id),
|
||||
product_id INT REFERENCES products(id),
|
||||
quantity INT NOT NULL,
|
||||
PRIMARY KEY (order_id, product_id)
|
||||
);
|
||||
```
|
||||
|
||||
### Constraints
|
||||
| Check | Recommendation | Severity |
|
||||
|-------|----------------|----------|
|
||||
| Missing PRIMARY KEY | Add primary key | CRITICAL |
|
||||
| Missing FOREIGN KEY | Add for relationships | HIGH |
|
||||
| No NOT NULL | Add where appropriate | MEDIUM |
|
||||
| No CHECK constraints | Validate data at DB level | MEDIUM |
|
||||
| Missing UNIQUE | Add for natural keys | HIGH |
|
||||
|
||||
```sql
|
||||
-- BAD: No constraints
|
||||
CREATE TABLE users (
|
||||
id INT,
|
||||
email VARCHAR(255),
|
||||
status VARCHAR(20)
|
||||
);
|
||||
|
||||
-- GOOD: Proper constraints
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'inactive', 'banned')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- With proper foreign keys
|
||||
CREATE TABLE orders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT NOT NULL REFERENCES users(id)
|
||||
ON DELETE RESTRICT -- Prevent user deletion with orders
|
||||
ON UPDATE CASCADE,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled')),
|
||||
total DECIMAL(12,2) NOT NULL CHECK (total >= 0)
|
||||
);
|
||||
```
|
||||
|
||||
### Data Types
|
||||
| Check | Recommendation | Severity |
|
||||
|-------|----------------|----------|
|
||||
| VARCHAR for all strings | Use appropriate types | MEDIUM |
|
||||
| INT for monetary values | Use DECIMAL | HIGH |
|
||||
| FLOAT for money | Use DECIMAL | CRITICAL |
|
||||
| TEXT without limit | Consider VARCHAR with limit | LOW |
|
||||
| Missing timezone | Use TIMESTAMPTZ | HIGH |
|
||||
|
||||
```sql
|
||||
-- BAD: Poor data type choices
|
||||
CREATE TABLE products (
|
||||
id INT,
|
||||
price FLOAT, -- Precision issues!
|
||||
quantity VARCHAR(10), -- Should be INT
|
||||
created_at TIMESTAMP, -- No timezone!
|
||||
description TEXT -- Unlimited
|
||||
);
|
||||
|
||||
-- GOOD: Appropriate data types
|
||||
CREATE TABLE products (
|
||||
id SERIAL PRIMARY KEY,
|
||||
sku VARCHAR(50) NOT NULL UNIQUE,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
|
||||
quantity INT NOT NULL DEFAULT 0 CHECK (quantity >= 0),
|
||||
description TEXT, -- OK for long text
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Use ENUMs for fixed values (PostgreSQL)
|
||||
CREATE TYPE order_status AS ENUM (
|
||||
'pending', 'processing', 'shipped', 'delivered', 'cancelled'
|
||||
);
|
||||
|
||||
CREATE TABLE orders (
|
||||
status order_status NOT NULL DEFAULT 'pending'
|
||||
);
|
||||
```
|
||||
|
||||
### Index Strategy
|
||||
| Check | Recommendation | Severity |
|
||||
|-------|----------------|----------|
|
||||
| No index on FK | Add index | HIGH |
|
||||
| No index on filter columns | Add index | HIGH |
|
||||
| Too many single-column indexes | Use composite | MEDIUM |
|
||||
| Missing unique index | Add for unique constraints | HIGH |
|
||||
|
||||
```sql
|
||||
-- Index planning
|
||||
CREATE TABLE orders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT NOT NULL REFERENCES users(id),
|
||||
status VARCHAR(20) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Foreign key index (not automatic in PostgreSQL!)
|
||||
CREATE INDEX idx_orders_user_id ON orders(user_id);
|
||||
|
||||
-- Composite index for common queries
|
||||
-- WHERE status = ? ORDER BY created_at DESC
|
||||
CREATE INDEX idx_orders_status_created ON orders(status, created_at DESC);
|
||||
|
||||
-- Partial index for specific cases
|
||||
CREATE INDEX idx_orders_pending ON orders(created_at)
|
||||
WHERE status = 'pending';
|
||||
|
||||
-- Cover index (includes all needed columns)
|
||||
CREATE INDEX idx_orders_user_summary ON orders(user_id, status, total);
|
||||
```
|
||||
|
||||
### Relationship Design
|
||||
| Check | Recommendation | Severity |
|
||||
|-------|----------------|----------|
|
||||
| N:M without junction table | Create junction table | CRITICAL |
|
||||
| Self-reference without depth | Add level/path column | MEDIUM |
|
||||
| Circular references | Redesign relationships | HIGH |
|
||||
|
||||
```sql
|
||||
-- N:M relationship with junction table
|
||||
CREATE TABLE products (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE categories (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE product_categories (
|
||||
product_id INT NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
category_id INT NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (product_id, category_id)
|
||||
);
|
||||
|
||||
-- Hierarchical data (adjacency list)
|
||||
CREATE TABLE categories (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
parent_id INT REFERENCES categories(id),
|
||||
level INT NOT NULL DEFAULT 0,
|
||||
path LTREE -- PostgreSQL ltree extension
|
||||
);
|
||||
|
||||
-- Create index for hierarchical queries
|
||||
CREATE INDEX idx_categories_path ON categories USING GIST (path);
|
||||
```
|
||||
|
||||
### Audit Columns
|
||||
```sql
|
||||
-- Standard audit columns
|
||||
CREATE TABLE entities (
|
||||
id SERIAL PRIMARY KEY,
|
||||
-- ... business columns ...
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by INT REFERENCES users(id),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_by INT REFERENCES users(id),
|
||||
deleted_at TIMESTAMPTZ, -- Soft delete
|
||||
deleted_by INT REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Auto-update trigger
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_updated_at
|
||||
BEFORE UPDATE ON entities
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at();
|
||||
```
|
||||
|
||||
## Response Template
|
||||
```
|
||||
## Database Schema Review Results
|
||||
|
||||
**Database**: PostgreSQL 15
|
||||
**Tables**: 12 | **Relationships**: 8
|
||||
|
||||
### Normalization
|
||||
| Status | Table | Issue |
|
||||
|--------|-------|-------|
|
||||
| HIGH | orders | Repeating product columns |
|
||||
|
||||
### Constraints
|
||||
| Status | Table | Issue |
|
||||
|--------|-------|-------|
|
||||
| CRITICAL | users | Missing PRIMARY KEY |
|
||||
| HIGH | orders | Missing FOREIGN KEY to users |
|
||||
|
||||
### Data Types
|
||||
| Status | Table.Column | Issue |
|
||||
|--------|--------------|-------|
|
||||
| CRITICAL | products.price | Using FLOAT instead of DECIMAL |
|
||||
|
||||
### Indexes
|
||||
| Status | Table | Issue |
|
||||
|--------|-------|-------|
|
||||
| HIGH | orders.user_id | Missing index on foreign key |
|
||||
|
||||
### Recommended Changes
|
||||
```sql
|
||||
-- Add missing primary key
|
||||
ALTER TABLE users ADD PRIMARY KEY (id);
|
||||
|
||||
-- Fix price data type
|
||||
ALTER TABLE products
|
||||
ALTER COLUMN price TYPE DECIMAL(10,2);
|
||||
|
||||
-- Add foreign key index
|
||||
CREATE INDEX idx_orders_user_id ON orders(user_id);
|
||||
```
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
1. **Normalization**: 3NF default, denormalize for performance
|
||||
2. **Constraints**: PK, FK, NOT NULL, CHECK, UNIQUE
|
||||
3. **Data Types**: DECIMAL for money, TIMESTAMPTZ for time
|
||||
4. **Indexes**: FK columns, filter columns, composites
|
||||
5. **Audit**: created_at, updated_at, soft delete
|
||||
|
||||
## Integration
|
||||
- `sql-optimizer`: Query performance
|
||||
- `migration-checker`: Migration safety
|
||||
- `orm-reviewer`: ORM mapping
|
||||
Reference in New Issue
Block a user