From cf358053f0b3a32cdbcff3d643e517db5bb62e06 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Fri, 29 May 2026 20:12:56 -0400 Subject: [PATCH] chore: add skills library --- claude-skills/design-postgres-tables.md | 79 +++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 claude-skills/design-postgres-tables.md diff --git a/claude-skills/design-postgres-tables.md b/claude-skills/design-postgres-tables.md new file mode 100644 index 0000000..1b50f17 --- /dev/null +++ b/claude-skills/design-postgres-tables.md @@ -0,0 +1,79 @@ +--- +name: design-postgres-tables +description: Design PostgreSQL table schemas. Use when modeling new data, adding tables, or reviewing/improving existing schema design. +--- + +# Design Postgres Tables Skill + +Use when: designing new tables, modeling relationships, reviewing schema, or planning migrations. + +## Column Conventions +- **Primary key**: `id UUID PRIMARY KEY DEFAULT gen_random_uuid()` (prefer UUID over serial for distributed systems; use BIGSERIAL if you need sortable IDs) +- **Timestamps**: always include `created_at TIMESTAMPTZ DEFAULT NOW()` and `updated_at TIMESTAMPTZ DEFAULT NOW()` +- **Soft delete**: `deleted_at TIMESTAMPTZ DEFAULT NULL` (NULL = active) +- **Foreign keys**: name as `_id`, e.g. `user_id`, `project_id` +- Use `TEXT` over `VARCHAR(n)` unless you need DB-enforced length limits +- Use `BOOLEAN` not `SMALLINT` for true/false +- Use `NUMERIC(precision, scale)` for money, never `FLOAT` + +## Naming Conventions +- Tables: `snake_case`, plural: `users`, `project_sessions`, `api_tokens` +- Columns: `snake_case`, singular +- Indexes: `idx__`: `idx_users_email` +- Foreign keys: `fk_
_`: `fk_orders_users` +- Unique constraints: `uq_
_`: `uq_users_email` + +## Standard Table Template +```sql +CREATE TABLE items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Business columns + name TEXT NOT NULL, + description TEXT, + status TEXT NOT NULL DEFAULT 'active' + CHECK (status IN ('active', 'inactive', 'archived')), + + -- Relationships + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Metadata + metadata JSONB DEFAULT '{}', + + -- Audit + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +-- Indexes +CREATE INDEX idx_items_user_id ON items(user_id); +CREATE INDEX idx_items_status ON items(status) WHERE deleted_at IS NULL; +CREATE INDEX idx_items_created_at ON items(created_at DESC); + +-- Auto-update updated_at +CREATE TRIGGER items_updated_at + BEFORE UPDATE ON items + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +``` + +## Relationship Patterns +- **One-to-many**: FK on the "many" side +- **Many-to-many**: junction table with `_id` + `_id` + its own PK + timestamps +- **Polymorphic**: `entity_type TEXT + entity_id UUID` (add check constraint for valid types) + +## Index Strategy +- Index all foreign keys +- Index columns used in WHERE clauses with high selectivity +- Partial indexes for filtered queries: `WHERE deleted_at IS NULL` +- Composite indexes: put equality columns first, range columns last +- JSONB: use GIN index for `@>` containment queries + +## Design Checklist +- [ ] Every table has UUID PK + created_at + updated_at +- [ ] Foreign keys have explicit ON DELETE behavior (CASCADE vs RESTRICT vs SET NULL) +- [ ] Enum-like columns use CHECK constraints or a lookup table +- [ ] Money stored as NUMERIC not FLOAT +- [ ] Indexes on all FK columns +- [ ] No storing arrays as comma-separated strings (use ARRAY[] or junction table) +- [ ] JSONB for truly dynamic/schemaless data only — not as a crutch