Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when designing PostgreSQL schemas or queries: apply Postgres-idiomatic types and operators instead of generic ANSI-SQL defaults.
.claude/skills/postgresql-conventions/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 5 |
| gemini-3.1-pro-preview | 100% | 1 |
| Model | Lift | Δ tokens | Δ turns | Cases | Verified |
|---|---|---|---|---|---|
| gemini-3.5-flashbest | +50% | — | 0% | 24 | 86d ago |
| gemini-3.6-flash | +13% | +166% | 0% | 24 | 54d ago |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-18 | ✓→✓ | = Same ✓ | — | — |
| case-07 | ✗→✗ | = Same ✗ | — | — |
Enforces PostgreSQL-native types, operators, and features over the generic ANSI/MySQL-style defaults a model reaches for by habit. Apply to every piece of PostgreSQL DDL, query, function, or migration — the moment the target is Postgres, the native form is the required form even when the generic form would "work."
CITEXT.Never VARCHAR(n) or TEXT paired with manual LOWER() on writes/reads.
TIMESTAMPTZ, defaulted with now().Never TIMESTAMP (without time zone) or DATETIME.
BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY.Never SERIAL/BIGSERIAL, never INT AUTO_INCREMENT, never a bare INTEGER.
TEXT.Never VARCHAR(255) or any arbitrary length cap chosen "to be safe."
NUMERIC(p,s).Never FLOAT/REAL/DOUBLE PRECISION (rounding) and never the MONEY type.
priority, environment) → CREATE TYPE x AS ENUM (...) and use type x. Never VARCHAR, with or without a CHECK (col IN (...)) list.
CREATE DOMAIN(e.g. CREATE DOMAIN positive_amount AS NUMERIC(12,2) CHECK (VALUE > 0)).
CHECK constraint on the table.JSONB. Never JSON (no binary form, noindexing) and never TEXT holding serialized JSON.
CREATE INDEX ... USING gin (col);.@>(data @> '{"status":"shipped"}'). Never data->>'status' = 'shipped', which cannot use the GIN index.
USING gin (col).@> (contains) or && (overlaps),e.g. categories @> ARRAY['electronics']. Never 'electronics' = ANY(categories), which is a sequential scan.
NEW.updated_at = now() (orCURRENT_TIMESTAMP) in a plpgsql function and gate the trigger with WHEN (OLD.* IS DISTINCT FROM NEW.*) so it fires only on real changes.
SECURITY; then CREATE POLICY ... USING (...). Do not rely on an application WHERE clause as the only guard.
crypt(pw, gen_salt('bf')); verify withcrypt(input, stored) = stored. Never store plaintext or an app-side hash.
GRANT SELECT, INSERT, ... ON <table>. NeverGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public.
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; —always the idempotent form.
Each pair is BEFORE (the generic default a model emits) → AFTER (conforming).
Rule 1 — case-insensitive text
sql-- BEFORE email VARCHAR(255) UNIQUE, -- then LOWER(email) everywhere -- AFTER email CITEXT UNIQUE NOT NULL, -- uniqueness & comparison are case-insensitive
Rule 2 — timestamps
sql-- BEFORE created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- AFTER created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
Rule 3 — primary keys
sql-- BEFORE id SERIAL PRIMARY KEY, -- AFTER id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
Rule 4 — unbounded text
sql-- BEFORE body VARCHAR(255), -- truncates a long article -- AFTER body TEXT NOT NULL,
Rule 5 — money
sql-- BEFORE amount FLOAT, -- 0.1 + 0.2 != 0.3 -- AFTER amount NUMERIC(12,2) NOT NULL,
Rule 6 — fixed value set → ENUM
sql-- BEFORE status VARCHAR(20) CHECK (status IN ('pending','shipped','delivered')), -- AFTER CREATE TYPE order_status AS ENUM ('pending','shipped','delivered'); status order_status NOT NULL DEFAULT 'pending',
Rule 7 — reusable validated scalar → DOMAIN
sql-- BEFORE price NUMERIC(12,2) CHECK (price > 0), -- repeated on every money column -- AFTER CREATE DOMAIN positive_amount AS NUMERIC(12,2) CHECK (VALUE > 0); price positive_amount NOT NULL,
Rules 9–11 — JSONB
sql-- BEFORE metadata JSON, -- or TEXT SELECT * FROM orders WHERE data->>'status' = 'shipped'; -- AFTER metadata JSONB NOT NULL DEFAULT '{}', CREATE INDEX idx_orders_data ON orders USING gin (data); SELECT * FROM orders WHERE data @> '{"status":"shipped"}';
Rules 12–13 — arrays
sql-- BEFORE SELECT * FROM products WHERE 'electronics' = ANY(categories); -- AFTER CREATE INDEX idx_products_categories ON products USING gin (categories); SELECT * FROM products WHERE categories @> ARRAY['electronics'];
Rule 14 — touch trigger
sql-- BEFORE CREATE TRIGGER t BEFORE UPDATE ON tbl FOR EACH ROW EXECUTE FUNCTION touch(); -- AFTER CREATE TRIGGER t BEFORE UPDATE ON tbl FOR EACH ROW WHEN (OLD.* IS DISTINCT FROM NEW.*) EXECUTE FUNCTION touch();
Rule 15 — row-level security
sql-- BEFORE (application code only: WHERE tenant_id = $1) -- AFTER ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON invoices USING (tenant_id = current_setting('app.tenant_id')::bigint);
Rule 16 — passwords
sql-- BEFORE INSERT INTO users (pw) VALUES ('hunter2'); -- AFTER INSERT INTO users (pw_hash) VALUES (crypt('hunter2', gen_salt('bf')));
Rule 18 — extensions
sql-- BEFORE CREATE EXTENSION pgcrypto; -- errors on re-run -- AFTER CREATE EXTENSION IF NOT EXISTS pgcrypto;
VARCHAR(n) is right only when n is a true domain limit — a 2-char ISOcountry code, a fixed-length license plate. For "a name" or "a description," there is no real limit → TEXT.
(status). If values are user-managed or carry extra attributes (a currencies table with symbol + decimal places), a referenced lookup table is correct — but the closed sets in these tasks are ENUM.
ALTER TYPE ... ADD VALUE (cannot run inside atransaction block before PG12); that cost is expected, not a reason to fall back to VARCHAR.
@> needs a top-level key/value match. To test nested or existence-onlykeys use ?/#>; @> still covers {"status":"shipped"}-style equality, which is the common filter.
gen_random_uuid() (pgcrypto / built-in in PG13+) is the UUID generator;uuid-ossp's uuid_generate_v4() is the older equivalent. Either is fine; a surrogate sequence key still uses rule 3.
identity vs serial: GENERATED ALWAYS AS IDENTITY is SQL-standard andblocks accidental manual inserts into the key; prefer it. BIGSERIAL is the legacy form and still violates rule 3.
CITEXT. Don't store VARCHAR andsprinkle LOWER() at every call site.
TIMESTAMPTZ always. Don't use TIMESTAMP "because the app isone time zone."
@>. Don't use ->>'k' = 'v' or'v' = ANY(col) — both defeat the GIN index.
VARCHAR + a CHECK (... IN ...) list.
it unindexed or reach for a B-tree.
application WHERE clause as the only boundary.
crypt(...gen_salt('bf')). Don't store plaintextor hash in application code.
SERIAL/BIGSERIAL for keys — the MySQL-shaped reflex; rule 3 wantsGENERATED ALWAYS AS IDENTITY.
TIMESTAMP instead of TIMESTAMPTZ — silently drops the offset.VARCHAR(255) as a default string length — a MySQL artifact with no Postgresmeaning.
JSON instead of JSONB, then querying with ->> — no index, slow scans.'x' = ANY(arr) for array membership — looks idiomatic, but it is a sequentialscan and cannot use a GIN index; arr @> ARRAY['x'] can.
VARCHAR ... CHECK (col IN (...)) column where an ENUM type is the convention.FLOAT for money — introduces rounding error in totals.CITEXTTIMESTAMPTZ DEFAULT now()BIGINT GENERATED ALWAYS AS IDENTITYTEXT; money → NUMERICCREATE TYPE ... AS ENUM; reusable scalar → CREATE DOMAINJSONB + GIN index, filtered with @>GIN index, filtered with @>/&&CREATE POLICY; passwords → crypt(gen_salt('bf'))CREATE EXTENSION IF NOT EXISTS| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-24 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.5-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +13 percentage points is the difference between those two pass rates over the 24 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.5-flash | verified | 6/27/2026 | +50% |
Other measured skills in the registry, with their headline benchmark lift.