| name | lokstra-schema-design |
| description | Generate PostgreSQL database schemas for Lokstra modules. Creates tables, indexes, constraints, triggers, and migration files. Use after module requirements and API specs are approved to design data layer. |
| phase | design |
| order | 5 |
| license | MIT |
| compatibility | {"lokstra_version":">=0.1.0","go_version":">=1.18","database":"postgresql"} |
Lokstra Schema Design
Purpose: Generate production-ready PostgreSQL database schemas for Lokstra modules with multi-tenant architecture, enforcing data integrity, security, and performance best practices.
Table of Contents
- When to Use This Skill
- Prerequisites
- Step-by-Step Process
- Multi-Tenant Requirements
- Table Design Standards
- Indexing Strategy
- Security & Access Control
- Migration Workflow
- Quality Checklist
- Examples
- Reference Files
When to Use This Skill
Trigger Conditions
Use this skill when:
- ✅ Module requirements are approved - Business logic is finalized
- ✅ API specifications are complete - Endpoints and data models defined
- ✅ Ready to design data layer - Need PostgreSQL table definitions
- ✅ Creating migrations - Need UP/DOWN SQL files
- ✅ Defining relationships - Need foreign keys and constraints
Prerequisites
Before starting schema design, ensure you have:
-
Approved Module Requirements (docs/modules/{module-name}/REQUIREMENTS.md)
- Functional requirements with acceptance criteria
- Use cases and business rules
- Data retention policies
-
Approved API Specifications (docs/modules/{module-name}/API_SPEC.md)
- Request/response DTOs
- Validation rules
- Endpoint definitions
-
Multi-Tenant Architecture Decision
- Shared schema (recommended for 100-10K tenants)
- Separate schemas (for 10-1K tenants)
- Separate databases (for 1-100 large tenants)
-
Database Environment
- PostgreSQL 14+ installed
- Migration tool configured (golang-migrate, Lokstra migration runner)
Step-by-Step Process
Step 1: Analyze Requirements (30 minutes)
Goal: Extract data entities and relationships from requirements
Actions:
- Read module requirements document
- Identify core entities (nouns in use cases)
- Map entity relationships (one-to-many, many-to-many)
- List required queries and access patterns
- Identify data volume estimates
Output: Entity-Relationship diagram (text or tool)
Example:
Module: Patient Management
Entities:
- Patients (10K-100K records per tenant)
- Appointments (100K-1M records per tenant)
- Medical Records (500K-5M records per tenant)
Relationships:
- Patient 1:N Appointments
- Patient 1:N Medical Records
- Appointment N:1 Doctor (from User module)
Step 2: Design Table Structures (60 minutes)
Goal: Define tables with all columns, types, and constraints
Actions:
- Create table for each entity
- Add
tenant_id TEXT NOT NULL to every table (multi-tenant)
- Choose ID strategy: ULID (recommended) or UUID v7
- Define business columns with appropriate types
- Add audit columns:
created_at, updated_at, deleted_at
- Add CHECK constraints for enum-like fields
- Add unique constraints (scoped to tenant)
Output: SQL CREATE TABLE statements
Template:
CREATE TABLE {module}.{entity_plural} (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
{column_name} {TYPE} {NOT NULL} {DEFAULT},
{reference_id} TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT chk_{table}_{column} CHECK ({column} IN ('value1', 'value2')),
CONSTRAINT uq_{table}_tenant_{column} UNIQUE(tenant_id, {column})
);
See: AUTH_SCHEMA_EXAMPLE.md for complete 10-table example
Step 3: Define Indexes (30 minutes)
Goal: Optimize query performance for common access patterns
Actions:
- Add index on
tenant_id for every table
- Create composite indexes with
tenant_id as first column
- Add partial indexes to exclude soft-deleted records
- Create GIN indexes for JSONB columns
- Add covering indexes for hot queries
Index Priority:
- Critical:
tenant_id (every table)
- High: Foreign keys, status columns, date ranges
- Medium: Full-text search, JSONB paths
- Low: Rarely queried columns
Template:
CREATE INDEX idx_{table}_tenant
ON {module}.{table}(tenant_id)
WHERE deleted_at IS NULL;
CREATE INDEX idx_{table}_tenant_{column}
ON {module}.{table}(tenant_id, {column} DESC)
WHERE deleted_at IS NULL;
CREATE INDEX idx_{table}_tenant_{fk}
ON {module}.{table}(tenant_id, {fk_column});
CREATE INDEX idx_{table}_metadata_gin
ON {module}.{table} USING GIN(metadata_jsonb);
CREATE INDEX idx_{table}_tenant_{col}_covering
ON {module}.{table}(tenant_id, {column})
INCLUDE (id, status, created_at);
See: MULTI_TENANT_SCHEMA_PATTERNS.md - Section on Indexing Strategies
Step 4: Add Security (30 minutes)
Goal: Enforce tenant isolation at database level
Actions:
- Enable Row-Level Security (RLS) on all tenant tables
- Create RLS policies for SELECT, INSERT, UPDATE, DELETE
- Define composite foreign keys with
tenant_id
- Add comments for sensitive columns (PII, passwords)
Template:
ALTER TABLE {module}.{table} ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_select ON {module}.{table}
FOR SELECT
USING (tenant_id = current_setting('app.tenant_id', true)::TEXT);
CREATE POLICY tenant_isolation_insert ON {module}.{table}
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::TEXT);
CREATE POLICY tenant_isolation_update ON {module}.{table}
FOR UPDATE
USING (tenant_id = current_setting('app.tenant_id', true)::TEXT);
CREATE POLICY tenant_isolation_delete ON {module}.{table}
FOR DELETE
USING (tenant_id = current_setting('app.tenant_id', true)::TEXT);
ALTER TABLE {module}.{child_table}
ADD CONSTRAINT fk_{child}_tenant_{parent}
FOREIGN KEY (tenant_id, {parent_id})
REFERENCES {module}.{parent_table}(tenant_id, id)
ON DELETE RESTRICT
ON UPDATE CASCADE;
See: MULTI_TENANT_SCHEMA_PATTERNS.md - Section on Row-Level Security
Step 5: Add Triggers & Functions (15 minutes)
Goal: Automate repetitive tasks (timestamps, audit logs)
Actions:
- Create
update_updated_at_column() function (once per database)
- Attach trigger to each table for auto-updating
updated_at
- Create audit trigger for sensitive tables (optional)
Template:
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_{table}_updated_at
BEFORE UPDATE ON {module}.{table}
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE OR REPLACE FUNCTION audit_{table}_changes()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'UPDATE' THEN
INSERT INTO {module}.audit_logs (
tenant_id, table_name, record_id, action,
old_values, new_values, changed_by
) VALUES (
OLD.tenant_id, TG_TABLE_NAME, OLD.id, 'UPDATE',
row_to_json(OLD), row_to_json(NEW),
current_setting('app.user_id', true)::TEXT
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_{table}_audit
AFTER UPDATE ON {module}.{table}
FOR EACH ROW
EXECUTE FUNCTION audit_{table}_changes();
See: AUTH_SCHEMA_EXAMPLE.md - Section on Triggers
Step 6: Create Migration Files (30 minutes)
Goal: Version-controlled schema changes with rollback capability
Actions:
- Create
migrations/{module}/ directory
- Generate sequential UP migration files
- Generate corresponding DOWN migration files
- Include all tables, indexes, RLS policies, triggers
- Test UP/DOWN on clean database
Naming Convention:
migrations/{module}/
├── 001_create_{table1}.up.sql
├── 001_create_{table1}.down.sql
├── 002_create_{table2}.up.sql
├── 002_create_{table2}.down.sql
└── ...
UP Migration Template:
BEGIN;
CREATE TABLE IF NOT EXISTS {module}.{table} (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
);
CREATE INDEX idx_{table}_tenant ON {module}.{table}(tenant_id);
ALTER TABLE {module}.{table} ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON {module}.{table}
USING (tenant_id = current_setting('app.tenant_id', true)::TEXT);
CREATE TRIGGER trg_{table}_updated_at
BEFORE UPDATE ON {module}.{table}
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON TABLE {module}.{table} IS '{Description}';
COMMENT ON COLUMN {module}.{table}.tenant_id IS 'Multi-tenant isolation';
COMMIT;
DOWN Migration Template:
BEGIN;
DROP TRIGGER IF EXISTS trg_{table}_updated_at ON {module}.{table};
DROP POLICY IF EXISTS tenant_isolation ON {module}.{table};
DROP TABLE IF EXISTS {module}.{table} CASCADE;
COMMIT;
See: MIGRATION_PATTERNS.md for comprehensive migration guide
Multi-Tenant Requirements
Mandatory Elements
Every schema MUST include:
1. tenant_id Column (CRITICAL)
CREATE TABLE patients (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
full_name VARCHAR(255) NOT NULL
);
CREATE TABLE countries (
code CHAR(2) PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
COMMENT ON TABLE countries IS 'Global lookup table (no tenant_id)';
CREATE TABLE patients (
id TEXT PRIMARY KEY,
full_name VARCHAR(255) NOT NULL
);
2. Composite Foreign Keys (CRITICAL)
ALTER TABLE appointments
ADD CONSTRAINT fk_appointments_patient
FOREIGN KEY (tenant_id, patient_id)
REFERENCES patients(tenant_id, id);
ALTER TABLE appointments
ADD CONSTRAINT fk_appointments_patient
FOREIGN KEY (patient_id)
REFERENCES patients(id);
3. Scoped Unique Constraints (CRITICAL)
CREATE UNIQUE INDEX uq_patients_tenant_email
ON patients(tenant_id, email)
WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX uq_patients_email
ON patients(email);
4. Row-Level Security (CRITICAL)
ALTER TABLE patients ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON patients
USING (tenant_id = current_setting('app.tenant_id', true)::TEXT);
See: MULTI_TENANT_SCHEMA_PATTERNS.md for complete patterns
Table Design Standards
Data Type Guidelines
| Use Case | Type | Example |
|---|
| IDs (Primary/Foreign) | TEXT | ULID: 01ARZ3NDEKTSV4RRFFQ69G5FAV |
| Tenant ID | TEXT NOT NULL | Same as primary ID |
| Timestamps | TIMESTAMPTZ | 2026-01-20 10:30:00+00 |
| Money/Decimal | NUMERIC(10,2) | 1234.56 |
| Boolean | BOOLEAN | true/false |
| Enum-like | VARCHAR(20) + CHECK | status VARCHAR(20) CHECK (status IN (...)) |
| JSON Data | JSONB | {"key": "value"} |
| Full Text | TEXT + tsvector | For search indexing |
| Short Text | VARCHAR(n) | Names, emails (n=50-255) |
| Long Text | TEXT | Descriptions, notes |
Column Naming
- Use
snake_case (lowercase with underscores)
- Avoid abbreviations:
full_name not fname
- Boolean prefix:
is_active, has_profile
- Timestamp suffix:
created_at, deleted_at
- Foreign key suffix:
user_id, patient_id
Required Columns
Every table MUST have:
CREATE TABLE {table} (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
Indexing Strategy
Index Types
| Type | Use Case | Syntax |
|---|
| B-tree (default) | Equality, range queries | CREATE INDEX idx_name ON table(col) |
| Partial | Filter subset | CREATE INDEX ... WHERE deleted_at IS NULL |
| Composite | Multi-column queries | CREATE INDEX ... ON table(col1, col2) |
| Covering | Avoid table lookup | CREATE INDEX ... INCLUDE (col3, col4) |
| GIN | JSONB, arrays, full-text | CREATE INDEX ... USING GIN(jsonb_col) |
| UNIQUE | Enforce uniqueness | CREATE UNIQUE INDEX ... ON table(col) |
Index Priorities
Always Create:
- Primary key (automatic)
tenant_id (every table)
- Foreign keys
- Unique constraints
High Priority:
- Status columns used in WHERE
- Date ranges (created_at, updated_at)
- Frequently joined columns
Medium Priority:
- JSONB columns (GIN)
- Full-text search (GIN + tsvector)
- Covering indexes for hot queries
Index Best Practices
CREATE INDEX idx_appointments_tenant_date
ON appointments(tenant_id, appointment_date DESC)
WHERE deleted_at IS NULL;
CREATE INDEX idx_patients_tenant_email_covering
ON patients(tenant_id, LOWER(email))
INCLUDE (id, full_name, status);
CREATE INDEX idx_patients_metadata_gin
ON patients USING GIN(metadata);
CREATE INDEX idx_appointments_date_tenant
ON appointments(appointment_date, tenant_id);
CREATE INDEX idx_patients_tenant
ON patients(tenant_id);
See: MULTI_TENANT_SCHEMA_PATTERNS.md - Indexing Strategies section
Security & Access Control
Row-Level Security (RLS)
Purpose: Database-level enforcement of tenant isolation (defense-in-depth)
When to use:
- All tables with
tenant_id column
- Prevents accidental cross-tenant data access
- Complements application-level filtering
Setup:
ALTER TABLE patients ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_select ON patients
FOR SELECT
USING (tenant_id = current_setting('app.tenant_id', true)::TEXT);
CREATE POLICY tenant_isolation_insert ON patients
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::TEXT);
CREATE POLICY tenant_isolation_update ON patients
FOR UPDATE
USING (tenant_id = current_setting('app.tenant_id', true)::TEXT);
CREATE POLICY tenant_isolation_delete ON patients
FOR DELETE
USING (tenant_id = current_setting('app.tenant_id', true)::TEXT);
Application Code (Go):
func (r *PatientRepository) List(ctx *request.Context) ([]*Patient, error) {
_, err := r.db.Exec("SET LOCAL app.tenant_id = $1", ctx.TenantID)
if err != nil {
return nil, err
}
rows, err := r.db.Query(`
SELECT id, tenant_id, full_name, email, created_at
FROM patients
WHERE deleted_at IS NULL
ORDER BY created_at DESC
`)
}
Sensitive Data Handling
CREATE TABLE users (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
email VARCHAR(255) NOT NULL,
password_hash TEXT NOT NULL,
phone VARCHAR(50),
ssn_encrypted TEXT,
);
COMMENT ON COLUMN users.password_hash IS 'Bcrypt hash with cost factor 12 (NEVER store plaintext)';
COMMENT ON COLUMN users.email IS 'PII: User email address (GDPR applicable)';
COMMENT ON COLUMN users.ssn_encrypted IS 'PII: Social Security Number (encrypted with AES-256, key in KMS)';
Migration Workflow
Development Workflow
lokstra migrate create add_emergency_contact
go run . migrate up
go run . migrate down
go run . migrate up
git add migrations/patient/003_*.sql
git commit -m "feat(patient): add emergency contact fields"
Production Deployment
pg_dump -U postgres -d prod_db -F c -f backup_$(date +%Y%m%d).dump
go run . migrate status
go run . migrate up
psql -U postgres -d prod_db -c "SELECT COUNT(*) FROM patients WHERE tenant_id IS NULL;"
go run . migrate down
Zero-Downtime Patterns
Adding Column:
ALTER TABLE patients ADD COLUMN priority VARCHAR(20);
ALTER TABLE patients ALTER COLUMN priority SET NOT NULL;
ALTER TABLE patients ALTER COLUMN priority SET DEFAULT 'normal';
Adding Index (Non-Blocking):
CREATE INDEX CONCURRENTLY idx_patients_priority
ON patients(tenant_id, priority)
WHERE deleted_at IS NULL;
See: MIGRATION_PATTERNS.md for complete migration patterns
Quality Checklist
Use this checklist before finalizing schema. Minimum passing score: 85/100 points.
Critical Requirements (Must have all)
Multi-Tenancy (30 pts total)
Data Types (15 pts)
Indexing (20 pts)
Security (15 pts)
Audit (10 pts)
Performance (10 pts)
Total Score: ___ / 100
Passing: 85+ points (all critical requirements + 50+ other)
See: SCHEMA_VALIDATION_CHECKLIST.md for detailed validation
Examples
Complete Auth Module Schema
See AUTH_SCHEMA_EXAMPLE.md for a production-ready example with:
- 10 tables (tenants, users, roles, permissions, role_permissions, user_roles, refresh_tokens, password_reset_tokens, login_attempts, audit_logs)
- 57 indexes (tenant, composite, partial, GIN)
- RLS policies on all tables
- Triggers for auto-update timestamps and audit logging
- Composite foreign keys
- Partitioning examples
- Sample queries
Key Patterns from Example
1. Tenant Scoped Queries:
SELECT id, full_name, email, status
FROM patients
WHERE tenant_id = $1
AND deleted_at IS NULL
ORDER BY created_at DESC;
2. Composite FK Enforcement:
ALTER TABLE patients
ADD CONSTRAINT uq_patients_tenant_id UNIQUE(tenant_id, id);
ALTER TABLE appointments
ADD CONSTRAINT fk_appointments_patient
FOREIGN KEY (tenant_id, patient_id)
REFERENCES patients(tenant_id, id);
3. Soft Delete Pattern:
UPDATE patients
SET deleted_at = NOW()
WHERE tenant_id = $1 AND id = $2;
SELECT * FROM patients
WHERE tenant_id = $1 AND deleted_at IS NULL;
Reference Files
Comprehensive Guides
-
AUTH_SCHEMA_EXAMPLE.md (28 KB)
- Complete 10-table auth module schema
- All multi-tenant patterns demonstrated
- RLS policies, triggers, indexes
- Sample queries and data volume estimates
-
MULTI_TENANT_SCHEMA_PATTERNS.md (22 KB)
- 3 multi-tenancy approaches (shared schema recommended)
- Tenant isolation patterns
- Composite foreign key patterns
- 8 indexing strategies
- RLS implementation with app code
- Partitioning strategies
- Anti-patterns to avoid
-
MIGRATION_PATTERNS.md (17 KB)
- UP/DOWN migration templates
- 6 common migration scenarios
- Zero-downtime migration techniques
- Batched data updates
- Rollback strategies
- Testing checklist
-
SCHEMA_VALIDATION_CHECKLIST.md (18 KB)
- 100-point quality scoring system
- 16 validation criteria with SQL queries
- Minimum 85/100 to pass
- Common issues and fixes
- 2.5-hour review process
Templates
- SCHEMA_TEMPLATE.md (748 lines)
- Generic schema documentation template
- Use as starting point for new modules
Output Format
Save schema design to: docs/modules/{module-name}/SCHEMA.md
Document Structure
# {Module Name} - Database Schema
**Module**: {module-name}
**Version**: 1.0.0
**Database**: PostgreSQL 14+
**Multi-Tenant**: Yes (Shared Schema)
**Date**: 2026-01-20
## Overview
{Brief description of module's data model}
## Tables
### 1. {table_name_plural}
**Purpose**: {What this table stores}
**Estimated Rows**: {volume estimate per tenant}
**Partitioning**: {Yes/No - strategy if applicable}
#### Table Definition
```sql
CREATE TABLE {module}.{table_plural} (
-- ... full SQL definition
);
Indexes
CREATE INDEX ...;
Row-Level Security
ALTER TABLE ... ENABLE ROW LEVEL SECURITY;
CREATE POLICY ...;
Triggers
CREATE TRIGGER ...;
Sample Queries
SELECT ...;
SELECT ...;
Relationships
{ER diagram or text description}
Data Volume Estimates
| Table | Year 1 | Year 3 | Year 5 |
|---|
| {table1} | 10K | 50K | 100K |
| {table2} | 100K | 500K | 1M |
Migration Files
migrations/{module}/001_create_{table1}.up.sql
migrations/{module}/001_create_{table1}.down.sql
- ...
Validation Checklist
Score: 92/100 (A - Very Good)
---
## Best Practices Summary
### DO
✅ Include `tenant_id` in ALL business tables
✅ Use composite foreign keys with `tenant_id`
✅ Scope unique constraints to tenant
✅ Enable RLS on all tenant tables
✅ Use `TIMESTAMPTZ` (not TIMESTAMP)
✅ Use `TEXT` for IDs (ULID/UUID v7)
✅ Add partial indexes: `WHERE deleted_at IS NULL`
✅ Put `tenant_id` first in composite indexes
✅ Attach `updated_at` trigger to all tables
✅ Soft delete with `deleted_at` column
✅ Add CHECK constraints for enum-like fields
✅ Test migrations on staging first
✅ Write DOWN migrations for rollback
✅ Comment sensitive columns (PII, passwords)
✅ Partition tables with >10M rows
### DON'T
❌ Skip `tenant_id` in any business table
❌ Create foreign keys without `tenant_id`
❌ Use global unique constraints
❌ Rely only on app-level tenant filtering
❌ Use `TIMESTAMP` without timezone
❌ Use auto-increment integers for IDs
❌ Create indexes without tenant_id first
❌ Hard delete business data
❌ Store passwords in plaintext
❌ Mix structured data with JSONB
❌ Create blocking indexes in production
❌ Update millions of rows in one transaction
❌ Deploy migrations without backup
❌ Forget to test rollback procedure
❌ Reuse migration sequence numbers
---
## Common Pitfalls
### Pitfall 1: Missing tenant_id in Queries
```sql
-- ❌ WRONG: Missing tenant filter (returns ALL tenants' data)
SELECT * FROM patients WHERE email = 'john@example.com';
-- ✅ CORRECT: Always filter by tenant
SELECT * FROM patients
WHERE tenant_id = $1 AND email = 'john@example.com';
Pitfall 2: Non-Composite Foreign Keys
CREATE TABLE appointments (
patient_id TEXT REFERENCES patients(id)
);
CREATE TABLE appointments (
tenant_id TEXT NOT NULL,
patient_id TEXT NOT NULL,
FOREIGN KEY (tenant_id, patient_id)
REFERENCES patients(tenant_id, id)
);
Pitfall 3: Long-Running Migrations
UPDATE patients SET status = 'active' WHERE status IS NULL;
DO $$
DECLARE batch_size INTEGER := 1000;
BEGIN
LOOP
UPDATE patients SET status = 'active'
WHERE status IS NULL
AND id IN (SELECT id FROM patients WHERE status IS NULL LIMIT batch_size);
EXIT WHEN NOT FOUND;
PERFORM pg_sleep(0.1);
END LOOP;
END $$;
PostgreSQL-Specific Features
Extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
CREATE EXTENSION IF NOT EXISTS "btree_gin";
Advanced Types
CREATE TYPE appointment_status AS ENUM ('scheduled', 'confirmed', 'completed');
CREATE TABLE bookings (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
time_range TSTZRANGE NOT NULL,
EXCLUDE USING GIST (tenant_id WITH =, time_range WITH &&)
);
Partitioning
CREATE TABLE audit_logs (
id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
action VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (created_at);
CREATE TABLE audit_logs_2026_01 PARTITION OF audit_logs
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE transactions (
id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
amount NUMERIC(10,2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY LIST (tenant_id);
CREATE TABLE transactions_tenant_abc123 PARTITION OF transactions
FOR VALUES IN ('abc123');
Troubleshooting
Issue: Migration Fails on Foreign Key
Error: ERROR: insert or update on table violates foreign key constraint
Cause: Child table has records referencing non-existent parent records
Fix:
SELECT DISTINCT a.patient_id
FROM appointments a
LEFT JOIN patients p ON a.tenant_id = p.tenant_id AND a.patient_id = p.id
WHERE p.id IS NULL;
DELETE FROM appointments WHERE patient_id IN (SELECT ...);
UPDATE appointments SET patient_id = NULL WHERE patient_id IN (SELECT ...);
ALTER TABLE appointments ADD CONSTRAINT fk_appointments_patient ...;
Issue: RLS Policy Not Working
Error: Queries return no rows even though data exists
Cause: app.tenant_id session variable not set
Fix:
_, err := db.Exec("SET LOCAL app.tenant_id = $1", ctx.TenantID)
if err != nil {
return nil, err
}
rows, err := db.Query("SELECT * FROM patients WHERE deleted_at IS NULL")
Issue: Slow Queries on Large Tables
Error: Queries take >1 second on tables with >1M rows
Cause: Missing indexes or wrong index column order
Fix:
EXPLAIN ANALYZE
SELECT * FROM appointments
WHERE tenant_id = 'abc123'
AND appointment_date >= '2026-01-01';
CREATE INDEX idx_appointments_tenant_date
ON appointments(tenant_id, appointment_date DESC)
WHERE deleted_at IS NULL;
EXPLAIN ANALYZE SELECT ...;
Total Length: ~650 lines
Estimated Reading Time: 30 minutes
Last Updated: 2024-01-20
Skill Version: 2.0.0