Reviews SQL and migration code for PostgreSQL-specific best practices, anti-patterns, JSONB and array usage, schema design, extensions, PL/pgSQL, and security (including RLS). Use when reviewing PostgreSQL migrations, .sql files, Prisma raw SQL, Supabase policies, or when the user asks for a PostgreSQL-focused code review.
Expert PostgreSQL code review for the active selection, open SQL-related files, or the whole project when no selection applies. Prioritize PostgreSQL-specific behavior over portable SQL.
schema.prisma raw SQL, Supabase SQL, seed scripts, and *.sql files.-- Inefficient: expression index on text extraction often still needs careful design
SELECT * FROM orders WHERE data->>'status' = 'shipped';
-- Prefer containment + GIN when querying JSON structure
CREATE INDEX idx_orders_data_status ON orders USING gin ((data jsonb_path_ops));
-- or expression GIN on (data->'status') depending on query shape
SELECT * FROM orders WHERE data @> '{"status": "shipped"}';
-- Prefer constraints to document allowed shapes where stable
ALTER TABLE orders ADD CONSTRAINT orders_status_valid
CHECK (data->>'status' IN ('pending', 'shipped', 'delivered'));
Review: @>, ?, ?&, ?| vs ad-hoc ->> equality; GIN vs btree; avoid huge JSONB blobs without partial indexing strategy.
-- Often poor: = ANY(array_col) without a supporting index strategy
SELECT * FROM products WHERE 'electronics' = ANY(categories);
-- Prefer @> / && with GIN on the array column when filtering by membership
CREATE INDEX idx_products_categories ON products USING gin (categories);
SELECT * FROM products WHERE categories @> ARRAY['electronics'];
Review: GIN default vs jsonb_path_ops; avoid repeated concatenation in hot loops; prefer set-based updates.
-- Generic types where PostgreSQL can model intent more tightly
CREATE TABLE users (
id INTEGER,
email VARCHAR(255),
created_at TIMESTAMP
);
-- Prefer BIGSERIAL/identity, TIMESTAMPTZ, TEXT or citext (if extension), CHECK/ENUM
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
metadata JSONB NOT NULL DEFAULT '{}',
CONSTRAINT users_email_format CHECK (email ~* '^[^@]+@[^@]+\.[^@]+$')
);
CREATE INDEX idx_users_metadata ON users USING gin (metadata);
Review: TIMESTAMPTZ vs TIMESTAMP; TEXT vs arbitrary VARCHAR(n); identities vs manual sequences; validation at DB boundary where appropriate.
CREATE TYPE currency_code AS ENUM ('USD', 'EUR', 'GBP', 'JPY');
CREATE TYPE transaction_status AS ENUM ('pending', 'completed', 'failed', 'cancelled');
CREATE DOMAIN positive_amount AS numeric(12, 2) CHECK (VALUE > 0);
CREATE TABLE transactions (
amount positive_amount NOT NULL,
currency currency_code NOT NULL,
status transaction_status NOT NULL DEFAULT 'pending'
);
Review: ENUM evolution (value adds are awkward); domains for repeated constraints.
Performance
Schema
VARCHAR for small fixed sets instead of ENUM or FK to lookup table.CHECK / NOT VALID workflows when migrating large tables.CREATE OR REPLACE FUNCTION update_modified_time()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := now(); -- column should be timestamptz
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_table_name_updated_at
BEFORE UPDATE ON table_name
FOR EACH ROW
WHEN (OLD IS DISTINCT FROM NEW)
EXECUTE FUNCTION update_modified_time();
Review: STABLE/IMMUTABLE correctness; avoid heavy work in row triggers; security definer risks; lock duration in triggers.
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
Review: extension is justified, version pinned in migrations where teams require it, privileges documented.
RLS
ALTER TABLE sensitive_data ENABLE ROW LEVEL SECURITY;
CREATE POLICY sensitive_data_isolation ON sensitive_data
FOR ALL
TO app_role
USING (user_id = current_setting('app.user_id', true)::bigint);
Privileges
-- Avoid
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_user;
-- Prefer least privilege per table/sequence/routine
GRANT SELECT, INSERT, UPDATE ON specific_table TO app_user;
GRANT USAGE ON SEQUENCE specific_table_id_seq TO app_user;
Review: RLS + FORCE ROW LEVEL SECURITY where appropriate; policies testable; no superuser-only assumptions in app paths.
Schema
JSONB, arrays, ENUM/FK, TIMESTAMPTZ, domains where repeated.CHECK, NOT NULL, FK ON DELETE behavior explicit).Performance
@>, &&, or indexed expressions consistent with queries.Features
SECURITY DEFINER.Security
Structure findings as: Issue → PostgreSQL-specific rationale → Concrete fix or pattern (SQL snippet or migration step). Treat PostgreSQL as PostgreSQL, not as generic SQL.