Lint your SQL the same way you lint everything else.
eslint-plugin-postgresql ships 89 rules that catch real
PostgreSQL pitfalls — syntax errors, timezone-naive timestamps, NOT IN against subqueries, missing WHERE on DELETE. Backed by postgresql-eslint-parser so the AST is the same one Postgres itself uses.
npm install --save-dev eslint-plugin-postgresqlFeatures
Real PostgreSQL grammar
Built on libpg-query — the Postgres server parser compiled to WebAssembly via postgresql-eslint-parser. CTEs, partial indexes, JSON path expressions all parse the same way they would in your database.
22 curated rules
Each rule has one concern. require-where-in-delete, no-not-in-subquery, prefer-timestamptz, prefer-text-over-varchar — pitfalls every Postgres reviewer eventually flags, encoded as lint diagnostics.
One-line flat config
Spread configs.recommended into your eslint.config.js. The preset binds the parser, plugin, severities and file glob — no separate languageOptions wiring required.
Browser playground
Open /playground in any browser. Parser and rules run entirely in a Web Worker through libpg-query.wasm — your SQL never leaves the tab.
Stable messageIds
Every diagnostic has a stable messageId you can match in suppressions and severity overrides. Renames count as breaking changes.
Errors don't bring down the lint run
Syntax errors surface as a single no-syntax-error diagnostic instead of crashing the linter, so the rest of your SQL files still get analysed.
Drop it into your ESLint config
Spread postgresql.configs.recommended into a flat-config entry and point
ESLint at your .sql files. Your editor and CI will lint SQL the same way
they lint TypeScript.
See the rules for what fires by default, or try the playground to feel the diagnostics in your hands.
// eslint.config.js
import postgresql from "eslint-plugin-postgresql";
export default [
{
files: ["**/*.sql"],
...postgresql.configs.recommended,
},
];What it covers
Every rule has a single concern. Together they map onto the categories that come up most often in PostgreSQL code review.
Safety
require-where-in-deleterequire-where-in-updateno-drop-table-cascadeno-truncate-cascadeno-cross-joinno-natural-joinno-not-in-subqueryno-distinct-on-without-order-byno-add-column-not-null-without-defaultno-having-without-group-byno-identifier-too-longno-alter-column-typeno-rename-columnno-rename-tableno-drop-columnno-drop-not-nullconsistent-fk-not-validno-unlogged-tableno-temporary-tableno-set-not-nullno-set-search-pathno-vacuum-fullno-clusterno-drop-databaseno-drop-schema-cascadeno-equality-with-nullno-on-delete-cascadeno-ruleno-update-primary-keyno-update-without-from-bindingno-with-recursive-without-limitprefer-add-constraint-not-validconsistent-create-or-replaceconsistent-drop-index-concurrentlyrequire-if-existsrequire-on-delete-actionno-add-check-constraint-without-not-validno-add-unique-constraint-directlyno-volatile-default-on-add-column
Schema
consistent-jsonb-over-jsonconsistent-identity-over-serialrequire-primary-keyconsistent-text-over-varcharconsistent-timestamptzno-money-typeno-char-typerequire-named-constraintno-numeric-without-precisionno-time-typerequire-schema-qualified-tableno-composite-primary-keyprefer-bigint-idrequire-fk-include-columnsrequire-table-columns
Performance
Security
Style
no-select-starsnake-case-table-namesnake-case-column-nameno-implicit-joinno-order-by-ordinalno-group-by-ordinalno-select-intoprefer-coalesce-over-caseprefer-explicit-null-orderingalign-column-definitionsalign-valuesno-unnecessary-quoted-identifierplpgsql-keyword-caseconsistent-as-for-column-aliasconsistent-as-for-table-aliasconsistent-between-over-andprefer-cast-operatorprefer-current-timestamp-over-nowconsistent-explicit-inner-joinconsistent-explicit-outer-joinprefer-in-list-over-orprefer-keyword-caseprefer-not-equals-operatorrequire-trailing-semicolon
Syntax