Review SQL migrations before applying them. Validates naming conventions, soft-delete filtering, COMMENT ON coverage, SECURITY DEFINER hardening, RLS policies, and Postgres performance. Use when reviewing a migration, a CREATE TABLE/FUNCTION/POLICY, or any schema change before it hits production.
How this skill is triggered — by the user, by Claude, or both
Slash command
/Wachines-Plugin-Ironman:db-reviewerThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Review SQL migrations before they are applied to a database. The checklist below is
Review SQL migrations before they are applied to a database. The checklist below is generic — adapt the project-specific lists (domains, soft-delete columns, RLS helper functions) to your codebase. A worked example for a fictional project is shown in each section so you can see the shape of a real configuration.
Companion skill: for the performance checks in section 7, use the
supabase-postgres-best-practicesskill.
Perennia BackOffice: if you are working in the BackOffice repo, the concrete config (12 DDD domains, soft-delete table list,
private.*RLS helpers, test guardrails) is inreferences/perennia-backoffice.md. Load it and skip the inference below.
Before reviewing, establish the project's conventions. Look in this order:
CLAUDE.md / AGENTS.md for documented naming and schema rules.schema-remote.sql, schema.sql) — the source of truth for
which tables, columns, enums and functions actually exist. Local per-domain files can
drift; always cross-check against the real dump to avoid false positives.supabase/migrations/ (or equivalent) for established patterns.If a project config is missing, infer conventions from the existing migrations and state your assumptions in the review.
Migration files usually follow a timestamped format: YYYYMMDDHHMMSS_<area>_<description>.sql.
If the project groups migrations by domain/bounded-context, verify the file uses a valid domain prefix. Read the project's list of domains — do not assume.
Flag deprecated domain names if the project documents a rename map (old → new).
Example config (replace with your project's): valid domains accounts, billing,
catalog, shared; deprecated finance → use billing.
If the migration does SELECT/JOIN over tables that use soft delete, verify it filters
out deleted rows. Detect the soft-delete column per table — conventions vary:
is_deleted (boolean), isdeleted (no underscore), or deleted_at (timestamp, filter IS NULL).
Build the list of soft-deleted tables from the schema (grep for the column), don't hardcode blindly. Watch for inconsistent column naming across tables — that's a common trap.
El COMMENT es la fuente de verdad de la semántica: lo lee pg_catalog, PostgREST lo vuelve la description del OpenAPI, y el catálogo de RPCs/columnas se genera de ahí. Una columna sin COMMENT obliga a agentes (y humanos) a adivinar. Por eso es un gate, no un nice-to-have.
Forward-only (bloquea lo nuevo):
COMMENT ON TABLE.COMMENT ON COLUMN.COMMENT ON FUNCTION.tipo_educador).Regla boy-scout — "arreglá la casa mientras la construís" (idea de Emi):
CREATE OR REPLACE), exigí su COMMENT ON FUNCTION aunque ya existiera sin comentar.Contexto: el baseline de cobertura está en el benchmark RAG-readiness (tablas ~80-93%, columnas ~17-30%). El gate forward-only + esta regla boy-scout son cómo se cierra sin reescribir todo a mano.
SECURITY DEFINER must also SET search_path = public
(or the appropriate fixed schema). Without it the function is vulnerable to
search_path injection.CREATE TABLE on a public-facing table must include
ALTER TABLE ... ENABLE ROW LEVEL SECURITY.CREATE POLICY — a table with RLS enabled but no policies
blocks all access.FOR ALL policy plus role-specific policies.USING clause selects from a soft-deleted table (see section 2), it should
filter deleted rows (AND NOT is_deleted / AND deleted_at IS NULL), especially for
restrictive/least-privilege roles. Broad admin FOR ALL policies may omit it if the
application layer already filters.USING/WITH CHECK are type-safe. RLS helper functions return
specific types — compare only against matching columns (e.g. a function returning bigint
must not be compared against a text column without an explicit cast).Policies should reuse the project's RLS helper functions (commonly under a private
schema) rather than re-implementing auth logic inline. Read which helpers exist before
reviewing.
Example: private.is_admin(), private.is_staff(), private.get_user_role().
IF NOT EXISTS where appropriate (CREATE INDEX, CREATE TABLE).bigint for FKs, text over varchar, timestamptz over
timestamp).$f$ inside $function$) — use quote_literal() if needed.ILIKE/LIKE, =, or similarity()/pg_trgm) must wrap both the column and the
search term in unaccent() — otherwise it fails on diacritics (user types Espin, the
row is El Espín → 0 results). Apply it consistently on both sides and combine with
lower() for case-insensitivity: unaccent(lower(col)) ILIKE unaccent(lower('%'||term||'%')).
Notes: unaccent() is not IMMUTABLE by default — fine at query runtime, but a functional
index needs an IMMUTABLE wrapper. On Supabase the extension lives in schema extensions, so
schema-qualify it (extensions.unaccent(...)) when the function's search_path doesn't
include extensions. Flag any text-search predicate that touches only one side.Use the supabase-postgres-best-practices skill to validate:
STABLE/IMMUTABLE where correct; avoid needless VOLATILE).npx claudepluginhub perennia-regeneracion/wachines-plugin-ironman --plugin Wachines-Plugin-IronmanPrevents silent decimal mismatch bugs across EVM chains with runtime lookup, chain-aware caching, and safe normalization for bots, dashboards, and DeFi tools.