·
Last changedVersion:
This version passed the hardening gate. Hardening updated the skill content and independently checked the resulting changes. Classified findings before hardening: 1. Classified findings closed: 0. Classified findings remaining: 1. Unclassified scanner findings remaining: 0. Review findings closed: 14. Review findings remaining: 1.
1
1
Expand | File | Group | Subtype | Severity | Status |
|---|---|---|---|---|---|
SKILL.md | Improper Output Handling | — | Medium | Open |
1 finding
BIGINT GENERATED ALWAYS AS IDENTITY. Use UUID only when you need global uniqueness or opacity.NUMERIC for exact decimal arithmetic)snake_case for table and column names.UNIQUE (...) NULLS NOT DISTINCT (PG15+) to allow only one NULL.NUMERIC(2,0) fails with an error.CLUSTER is a one-off reorganization. It is not maintained on later inserts.CLUSTER takes an exclusive lock on the whole table while it runs, which blocks reads and writes. Schedule it in a maintenance window with user approval.BIGINT GENERATED ALWAYS AS IDENTITY (GENERATED BY DEFAULT is also fine).
UUID when merging, federating, running a distributed system, or needing opaque IDs.uuidv7() (preferred on PG18+) or gen_random_uuid() (on older PG versions).BIGINT unless storage space is critical. Use INTEGER for smaller ranges. Avoid SMALLINT unless the values are constrained.DOUBLE PRECISION over REAL unless storage space is critical. Use NUMERIC for exact decimal arithmetic.TEXT.
CHECK (LENGTH(col) <= n) instead of VARCHAR(n). Avoid CHAR(n).BYTEA for binary data.PLAIN (no TOAST)EXTENDED (compress + out-of-line)EXTERNAL (out-of-line, no compress)MAIN (compress, keep in-line if possible)EXTENDED is usually best.ALTER TABLE tbl ALTER COLUMN col SET STORAGE strategy, and the threshold with ALTER TABLE tbl SET (toast_tuple_target = 4096).LOWER(col) (preferred unless the column needs a case-insensitive PK/FK/UNIQUE) or CITEXT.NUMERIC(p,s) (never float).TIMESTAMPTZ for timestamps, DATE for date-only values, and INTERVAL for durations.
TIMESTAMP (without timezone).now() for transaction start time and clock_timestamp() for current wall-clock time.BOOLEAN with a NOT NULL constraint, unless you need tri-state values.CREATE TYPE ... AS ENUM for small, stable sets (e.g. US states, days of week). For values driven by business logic that change over time (e.g. order statuses), use TEXT (or INT) + CHECK, or a lookup table.TEXT[], INTEGER[], etc.
@>, <@) and overlap (&&) queries.arr[1] (1-indexed), arr[1:3] (slicing).'{val1,val2}' or ARRAY[val1,val2].daterange, numrange, tstzrange for intervals.
&&), containment (@>) and other operators. Index with GiST.[) (inclusive/exclusive) by default.INET for IP addresses, CIDR for network ranges, MACADDR for MAC addresses. They support network operators (<<, >>, &&).POINT, LINE, POLYGON, CIRCLE for 2D spatial data. Index with GiST. Consider PostGIS for advanced spatial features.TSVECTOR for full-text search documents, TSQUERY for search queries. Index tsvector with GIN.
to_tsvector('english', col) and to_tsquery('english', 'query').CREATE DOMAIN email AS TEXT CHECK (VALUE ~ '^[^@]+@[^@]+$') for reusable custom types with validation. A domain enforces its constraints across tables. This pattern is only an illustrative sanity check: it accepts whitespace and malformed addresses. Handle real email validation in the application.CREATE TYPE address AS (street TEXT, city TEXT, zip TEXT) for structured data within columns. Access fields with (col).field syntax.vector type from pgvector for vector similarity search over embeddings.timestamp (without time zone); DO use timestamptz instead.char(n) or varchar(n); DO use text instead.money type; DO use numeric instead.timetz type; DO use timestamptz instead.timestamptz(0) or any other precision specification; DO use timestamptz insteadserial type; DO use generated always as identity instead.Enable it with ALTER TABLE tbl ENABLE ROW LEVEL SECURITY. Then create policies: CREATE POLICY user_access ON orders FOR SELECT TO app_users USING (user_id = current_user_id()). This gives built-in user-based access control at the row level.
current_user_id() is not built in. It is a placeholder for a function the application defines.
ON DELETE/UPDATE action (CASCADE, RESTRICT, SET NULL, SET DEFAULT).
DEFERRABLE INITIALLY DEFERRED for circular FK dependencies, which are then checked at transaction end.NULLS NOT DISTINCT (PG15+).
(1, NULL) and (1, NULL) are both allowed.NULLS NOT DISTINCT: only one (1, NULL) is allowed.NULLS NOT DISTINCT unless you specifically need duplicate NULLs.CHECK (price > 0) allows NULL prices.NOT NULL to enforce the rule: price NUMERIC NOT NULL CHECK (price > 0).EXCLUDE USING gist (room_id WITH =, booking_period WITH &&) prevents double-booking rooms. It requires a suitable index type (often GiST).=, <, >, BETWEEN, ORDER BY).WHERE a = ? AND b > ? uses an index on (a,b), but WHERE b = ? does not.CREATE INDEX ON tbl (id) INCLUDE (name, email) adds non-key columns, enabling index-only scans that never visit the table.WHERE status = 'active' → CREATE INDEX ON tbl (user_id) WHERE status = 'active'). Any query with status = 'active' can use this index.CREATE INDEX ON tbl (LOWER(email))). The expression must match exactly in the WHERE clause: WHERE LOWER(email) = 'user@example.com'.@>, ?), full-text search (@@).CLUSTER).PARTITION BY RANGE (created_at)).
CREATE TABLE logs_2024_01 PARTITION OF logs FOR VALUES FROM ('2024-01-01') TO ('2024-02-01').PARTITION BY LIST (region)). Example: FOR VALUES IN ('us-east', 'us-west').PARTITION BY HASH (user_id)). It creates N partitions using a modulus.CHECK-based constraint exclusion applies only to legacy inheritance partitioning.fillfactor=90 to leave space for HOT updates, which avoid index maintenance.COPY or multi-row INSERT instead of single-row inserts.BIGINT GENERATED ALWAYS AS IDENTITY over UUID.ON CONFLICT (col1, col2) needs a matching unique index. A partial unique index also works if the conflict target repeats the index's WHERE predicate.EXCLUDED.column to reference the would-be-inserted values. Only update columns that actually changed, to reduce write overhead.DO NOTHING is faster than DO UPDATE when no actual update is needed.BEGIN; ALTER TABLE...; ROLLBACK;. This still takes heavy locks, so do such testing on a staging copy rather than in production.CREATE INDEX CONCURRENTLY avoids blocking writes but can't run in transactions.now(), gen_random_uuid()) rewrites the entire table under an exclusive lock, whether or not the column is NOT NULL. In modern releases, non-volatile defaults are a fast, metadata-only change.ALTER TABLE DROP CONSTRAINT then DROP COLUMN to avoid dependency issues. Treat both as destructive and get user confirmation first.CREATE OR REPLACE with different arguments creates overloads, not replacements. DROP the old version if you don't want an overload.... GENERATED ALWAYS AS (<expr>) STORED for computed, indexable fields. PG18+ adds VIRTUAL columns (computed on read, not stored).pgcrypto: prefer hashing passwords in the application with a slow, modern password hashing algorithm, and store only the hash.
crypt() is a fallback. If you use it, choose the strongest salt type with a high cost factor.uuid-ossp: gen_random_uuid() is built into modern PostgreSQL and uuidv7() is built into PG18+, so new projects need no extension for UUIDs. Use uuid-ossp or pgcrypto only on older versions.pg_trgm: fuzzy text search with the % operator and similarity() function. Index with GIN to accelerate LIKE '%pattern%'.citext: case-insensitive text type. Prefer expression indexes on LOWER(col) unless you need case-insensitive constraints.btree_gin/btree_gist: enable mixed-type indexes (e.g., a GIN index on both JSONB and text columns).hstore: key-value pairs. Mostly superseded by JSONB but useful for simple string mappings.timescaledb: consider for time-series. It provides partitioning, retention, compression and continuous aggregates. Review its licensing, since features are split across licenses.postgis: consider for location-based applications. It provides comprehensive geospatial support beyond the basic geometric types.pgvector: vector similarity search for embeddings.pgaudit: audit logging scoped to the needed classes, such as schema changes, role changes and writes on sensitive tables.
JSONB with a GIN index.CREATE INDEX ON tbl USING GIN (jsonb_col); accelerates:
jsonb_col @> '{"k":"v"}'jsonb_col ? 'k', any/all keys ?\|, ?&jsonb_col @> ANY(ARRAY['{"status":"active"}', '{"status":"pending"}'])@> workloads, consider the jsonb_path_ops opclass for smaller, faster containment-only indexes:
CREATE INDEX ON tbl USING GIN (jsonb_col jsonb_path_ops);?, ?|, ?&) queries and only supports containment (@>).ALTER TABLE tbl ADD COLUMN price INT GENERATED ALWAYS AS ((jsonb_col->>'price')::INT) STORED;CREATE INDEX ON tbl (price);WHERE price BETWEEN 100 AND 500 (uses B-tree) over WHERE (jsonb_col->>'price')::INT BETWEEN 100 AND 500 without an index.@> for containment (e.g., tags). Consider jsonb_path_ops if you only do containment.config JSONB NOT NULL CHECK(jsonb_typeof(config) = 'object').Get started with chainctlchainctl skill install public/wshobson/postgresql:latest
Bring Chainguard's hardening pipeline to your first-party skills — custom rulesets, automated remediation, full audit trail.
skills.cgr.dev/public/github.com/wshobson/plugins/database-design/skills/postgresql:latest
MIT
HARDENING.md
LICENSE
SKILL.md