PostgreSQL code review
Review PostgreSQL-specific code for correctness, performance, type quality, security, and maintainability, then return a verdict, evidence-backed findings, and paste-ready corrected SQL.
When to invoke
- "Review this PostgreSQL migration for anti-patterns."
- "Audit our JSONB and array usage."
- "Is this schema using the right PostgreSQL types?"
- "Check this PL/pgSQL function and trigger before it merges."
- "Review this Row Level Security policy."
Criteria
JSONB and arrays
| Area |
Bad pattern |
Better pattern |
| JSONB filters |
data->>'status' = 'shipped' without index support. |
Use containment: data @> '{"status": "shipped"}' and CREATE INDEX idx_orders_status ON orders USING gin((data->'status'));. |
| JSONB structure |
Deep, unconstrained blobs such as `data |
|
| Array filters |
'electronics' = ANY(categories) without a supporting index. |
Use CREATE INDEX idx_products_categories ON products USING gin(categories); and categories @> ARRAY['electronics']. |
| Array mutation |
Array concatenation in row-by-row loops. |
Use bulk updates such as `categories = categories |
Schema design and data types
| Concern |
Review rule |
| Primary keys |
Prefer PostgreSQL-appropriate generated identifiers such as BIGSERIAL PRIMARY KEY or the project standard. |
| Email and text |
Use CITEXT for case-insensitive email, TEXT instead of arbitrary VARCHAR when there is no true length rule, and CHECK constraints for real validation. |
| Time |
Use TIMESTAMPTZ instead of TIMESTAMP for instants. |
| JSONB |
Default structured JSONB with metadata JSONB DEFAULT '{}' when optional document data is intentional. |
| Constrained values |
Use ENUM, custom domains, or lookup tables rather than free VARCHAR(20) values. |
| Money-like values |
Use a domain such as positive_amount AS DECIMAL(10,2) CHECK (VALUE > 0) when the constraint is reused. |
Example type objects to preserve: CREATE TYPE currency_code AS ENUM ('USD', 'EUR', 'GBP', 'JPY');, CREATE TYPE transaction_status AS ENUM ('pending', 'completed', 'failed', 'cancelled');, and CREATE DOMAIN positive_amount AS DECIMAL(10,2) CHECK (VALUE > 0);.
Functions, triggers, extensions, and security
| Area |
Review rule |
| Trigger timestamps |
Use CURRENT_TIMESTAMP and fire triggers only when needed with WHEN (OLD.* IS DISTINCT FROM NEW.*). |
| Trigger API |
Check CREATE OR REPLACE FUNCTION update_modified_time() RETURNS TRIGGER, NEW.updated_at, RETURN NEW, LANGUAGE plpgsql, CREATE TRIGGER update_modified_time_trigger, and EXECUTE FUNCTION update_modified_time(). |
| Extensions |
Use CREATE EXTENSION IF NOT EXISTS "uuid-ossp", "pgcrypto", and "pg_trgm" only when needed; know uuid_generate_v4(), crypt('password', gen_salt('bf')), and word_similarity('postgres', 'postgre'). |
| Row Level Security |
Require ALTER TABLE sensitive_data ENABLE ROW LEVEL SECURITY; and policies such as CREATE POLICY user_data_policy ... USING (user_id = current_setting('app.current_user_id')::INTEGER); when tenant or user isolation is needed. |
| Privileges |
Avoid GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_user; prefer GRANT SELECT, INSERT, UPDATE ON specific_table TO app_user and GRANT USAGE ON SEQUENCE specific_table_id_seq TO app_user. |
| Indexes |
Check use of GIN for JSONB and arrays, GiST for ranges/geospatial, partial indexes for selective predicates, and evidence from plans where available. |
| SQL injection |
No user input concatenated into SQL; require JPQL, derived queries, or bound native parameters when reviewing application access. |
PostgreSQL anti-patterns
- Avoiding PostgreSQL-specific indexes such as
GIN and GiST for appropriate data types.
- Treating
JSONB like a string field instead of using operators such as @> and ?.
- Ignoring array operators and indexes.
- Choosing poor partition keys or not leveraging partitioning where scale requires it.
- Using
VARCHAR for limited value sets instead of ENUM, domain, lookup table, or CHECK.
- Missing validation constraints on data that application code assumes.
- Using
TIMESTAMP for real-world instants where time zone correctness matters.
- Leaving unstructured
JSONB without validation for fields the application depends on.
SQL vocabulary to preserve
Use exact PostgreSQL examples when relevant: UNIQUE, NULL, created_at, idx_users_metadata, valid_email, table_name, application_role, UUID, BEGIN, BEFORE, EACH, GOOD, JSONB/arrays, GIN/GiST, function/procedure, and built-in security or extension capabilities.
Example schema facts include email CITEXT UNIQUE NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), metadata JSONB DEFAULT '{}', CONSTRAINT valid_email CHECK (...), and CREATE INDEX idx_users_metadata ON users USING gin(metadata);.
Output template
## PostgreSQL review — <file or selection>
**Verdict:** Pass | Fix required | Reject
| # | Severity | Finding | Evidence | Fix |
| --- | --- | --- | --- | --- |
| 1 | High | User input concatenated into SQL | `<file:line or snippet>` | Bind through JPQL, a derived query, or a parameterized native query. |
| 2 | Medium | JSONB containment query has no GIN index | `Seq Scan on orders` | `CREATE INDEX idx_orders_data ON orders USING gin(data);` |
| 3 | Low | VARCHAR used for case-insensitive email | `email VARCHAR(255)` | Use `CITEXT` plus an appropriate `CHECK` constraint. |
### Corrected SQL
```sql
CREATE INDEX idx_orders_data ON orders USING gin(data);
-- Repository query stays parameterized: WHERE data @> :filter
## Quality gate
- [ ] A verdict is stated: Pass, Fix required, or Reject.
- [ ] Every finding carries severity and concrete evidence such as file/line, snippet, or plan output.
- [ ] No user input is concatenated into SQL; every parameter is bound.
- [ ] PostgreSQL-specific types including `CITEXT`, `JSONB`, arrays, `ENUM`, and domains were considered.
- [ ] Index types including `GIN`, `GiST`, and partial indexes were evaluated where relevant.
- [ ] `CHECK`, `ENUM`, and domain constraints were validated for constrained values.
- [ ] `RLS`, privileges, extension usage, functions, and triggers were checked when present.
- [ ] Corrected SQL is paste-ready and rollback-safe for schema changes.
1---2name: postgresql-code-review-43description: Review existing PostgreSQL SQL, schema, migrations, functions, triggers, indexes, JSONB, arrays, custom types, domains, extensions, privileges, and Row Level Security for PostgreSQL-specific anti-patterns. Use when asked to audit or critique PostgreSQL code, database migrations, PL/pgSQL, RLS policies, or schema design.4---56<!-- Generated from harness/github-copilot/plugins/mainframe-cobol-db2/skills/postgresql-code-review/SKILL.md by harness/claude-code/scripts/convert_from_copilot.py. Edit the source, not this file. -->78# PostgreSQL code review910Review PostgreSQL-specific code for correctness, performance, type quality, security, and maintainability, then return a verdict, evidence-backed findings, and paste-ready corrected SQL.1112## When to invoke1314- "Review this PostgreSQL migration for anti-patterns."15- "Audit our JSONB and array usage."16- "Is this schema using the right PostgreSQL types?"17- "Check this PL/pgSQL function and trigger before it merges."18- "Review this Row Level Security policy."1920## Criteria2122### JSONB and arrays2324| Area | Bad pattern | Better pattern |25| --- | --- | --- |26| JSONB filters | `data->>'status' = 'shipped'` without index support. | Use containment: `data @> '{"status": "shipped"}'` and `CREATE INDEX idx_orders_status ON orders USING gin((data->'status'));`. |27| JSONB structure | Deep, unconstrained blobs such as `data || '{"shipping":{"tracking":{"number":"123"}}}'`. | Add validation such as `CONSTRAINT valid_status CHECK (data->>'status' IN ('pending', 'shipped', 'delivered'))`. |28| Array filters | `'electronics' = ANY(categories)` without a supporting index. | Use `CREATE INDEX idx_products_categories ON products USING gin(categories);` and `categories @> ARRAY['electronics']`. |29| Array mutation | Array concatenation in row-by-row loops. | Use bulk updates such as `categories = categories || ARRAY['new_category'] WHERE id IN (...)`. |3031### Schema design and data types3233| Concern | Review rule |34| --- | --- |35| Primary keys | Prefer PostgreSQL-appropriate generated identifiers such as `BIGSERIAL PRIMARY KEY` or the project standard. |36| Email and text | Use `CITEXT` for case-insensitive email, `TEXT` instead of arbitrary `VARCHAR` when there is no true length rule, and `CHECK` constraints for real validation. |37| Time | Use `TIMESTAMPTZ` instead of `TIMESTAMP` for instants. |38| JSONB | Default structured JSONB with `metadata JSONB DEFAULT '{}'` when optional document data is intentional. |39| Constrained values | Use `ENUM`, custom domains, or lookup tables rather than free `VARCHAR(20)` values. |40| Money-like values | Use a domain such as `positive_amount AS DECIMAL(10,2) CHECK (VALUE > 0)` when the constraint is reused. |4142Example type objects to preserve: `CREATE TYPE currency_code AS ENUM ('USD', 'EUR', 'GBP', 'JPY');`, `CREATE TYPE transaction_status AS ENUM ('pending', 'completed', 'failed', 'cancelled');`, and `CREATE DOMAIN positive_amount AS DECIMAL(10,2) CHECK (VALUE > 0);`.4344### Functions, triggers, extensions, and security4546| Area | Review rule |47| --- | --- |48| Trigger timestamps | Use `CURRENT_TIMESTAMP` and fire triggers only when needed with `WHEN (OLD.* IS DISTINCT FROM NEW.*)`. |49| Trigger API | Check `CREATE OR REPLACE FUNCTION update_modified_time() RETURNS TRIGGER`, `NEW.updated_at`, `RETURN NEW`, `LANGUAGE plpgsql`, `CREATE TRIGGER update_modified_time_trigger`, and `EXECUTE FUNCTION update_modified_time()`. |50| Extensions | Use `CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`, `"pgcrypto"`, and `"pg_trgm"` only when needed; know `uuid_generate_v4()`, `crypt('password', gen_salt('bf'))`, and `word_similarity('postgres', 'postgre')`. |51| Row Level Security | Require `ALTER TABLE sensitive_data ENABLE ROW LEVEL SECURITY;` and policies such as `CREATE POLICY user_data_policy ... USING (user_id = current_setting('app.current_user_id')::INTEGER);` when tenant or user isolation is needed. |52| Privileges | Avoid `GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_user`; prefer `GRANT SELECT, INSERT, UPDATE ON specific_table TO app_user` and `GRANT USAGE ON SEQUENCE specific_table_id_seq TO app_user`. |53| Indexes | Check use of `GIN` for JSONB and arrays, `GiST` for ranges/geospatial, partial indexes for selective predicates, and evidence from plans where available. |54| SQL injection | No user input concatenated into SQL; require JPQL, derived queries, or bound native parameters when reviewing application access. |5556## PostgreSQL anti-patterns5758- Avoiding PostgreSQL-specific indexes such as `GIN` and `GiST` for appropriate data types.59- Treating `JSONB` like a string field instead of using operators such as `@>` and `?`.60- Ignoring array operators and indexes.61- Choosing poor partition keys or not leveraging partitioning where scale requires it.62- Using `VARCHAR` for limited value sets instead of `ENUM`, domain, lookup table, or `CHECK`.63- Missing validation constraints on data that application code assumes.64- Using `TIMESTAMP` for real-world instants where time zone correctness matters.65- Leaving unstructured `JSONB` without validation for fields the application depends on.6667## SQL vocabulary to preserve6869Use exact PostgreSQL examples when relevant: `UNIQUE`, `NULL`, `created_at`, `idx_users_metadata`, `valid_email`, `table_name`, `application_role`, `UUID`, `BEGIN`, `BEFORE`, `EACH`, `GOOD`, `JSONB/arrays`, `GIN/GiST`, `function/procedure`, and built-in security or extension capabilities.7071Example schema facts include `email CITEXT UNIQUE NOT NULL`, `created_at TIMESTAMPTZ DEFAULT NOW()`, `metadata JSONB DEFAULT '{}'`, `CONSTRAINT valid_email CHECK (...)`, and `CREATE INDEX idx_users_metadata ON users USING gin(metadata);`.7273## Output template7475```markdown76## PostgreSQL review — <file or selection>7778**Verdict:** Pass | Fix required | Reject7980| # | Severity | Finding | Evidence | Fix |81| --- | --- | --- | --- | --- |82| 1 | High | User input concatenated into SQL | `<file:line or snippet>` | Bind through JPQL, a derived query, or a parameterized native query. |83| 2 | Medium | JSONB containment query has no GIN index | `Seq Scan on orders` | `CREATE INDEX idx_orders_data ON orders USING gin(data);` |84| 3 | Low | VARCHAR used for case-insensitive email | `email VARCHAR(255)` | Use `CITEXT` plus an appropriate `CHECK` constraint. |8586### Corrected SQL87```sql88CREATE INDEX idx_orders_data ON orders USING gin(data);89-- Repository query stays parameterized: WHERE data @> :filter90```91```9293## Quality gate9495- [ ] A verdict is stated: Pass, Fix required, or Reject.96- [ ] Every finding carries severity and concrete evidence such as file/line, snippet, or plan output.97- [ ] No user input is concatenated into SQL; every parameter is bound.98- [ ] PostgreSQL-specific types including `CITEXT`, `JSONB`, arrays, `ENUM`, and domains were considered.99- [ ] Index types including `GIN`, `GiST`, and partial indexes were evaluated where relevant.100- [ ] `CHECK`, `ENUM`, and domain constraints were validated for constrained values.101- [ ] `RLS`, privileges, extension usage, functions, and triggers were checked when present.102- [ ] Corrected SQL is paste-ready and rollback-safe for schema changes.