Style linter for PostgreSQL SQL files: block comment formatting, leading commas, short type names, and a suppression syntax for the rest.
Consuming projects vendor this repo — see the top-level
README.md for how a project wires up make lint.
# From a project that vendors this repo (paths are set by that project's
# own LINT_TARGETS; see ../lint.mk)
make lint
# Run the linter directly on specific files or directories
.vendor/linter/sql/bin/sql-lint sql/myext.sql.in test/sql-lint [OPTIONS] FILE...
sql-lint [OPTIONS] DIRECTORY...
Files are checked against all enabled rules. Directories are searched
recursively for *.sql and *.sql.in files (excluding deps/,
docker/deps/, pgxntool/, and .claude/worktrees/).
| Flag | Description |
|---|---|
-h, --help |
Show help message |
-q, --quiet |
Suppress the summary line |
| Code | Meaning |
|---|---|
| 0 | No findings |
| 1-10 | Reserved for generic errors (2 = usage error) |
| 11+ | Lint findings: exit_code - 10 = number of findings |
Note: exit codes are capped at 255 by POSIX, so 245+ findings all exit 255.
All comment formatting rules share the comment- prefix so they sort
together in fixture listings.
Block comments must end with */ alone on its own line.
/*
* Bad: text before closing */
/*
* Good: closing on its own line.
*/Lines inside a block comment must start with *.
/*
Bad: missing the star prefix.
* Good: has the star prefix.
*/Block comments must start with /* alone on the opening line.
/* Bad: text on the opening line
* of a multi-line comment
*/
/*
* Good: opening line has only /*
*/Block comments must not have blank * lines immediately after /*
or immediately before */.
/*
*
* Bad: leading blank line above.
*/
/*
* Good: content starts immediately.
*/Single-line /* text */ comments are not allowed. Use -- for
single-line comments.
/* Bad: single-line block comment */
-- Good: use a line comment insteadA run of 3 or more consecutive -- lines must use /* */ syntax instead.
Up to 2 consecutive -- lines is fine — a short remark reads fine as --.
-- Bad: stacked dashes
-- spanning three
-- or more lines
-- Good: 2 lines is fine
-- as a short remark
/*
* Good: block comment
* for anything longer
*/Multi-line constructs must use leading commas (, item), not trailing
commas (item,).
-- Bad
SELECT
column1,
column2,
column3
-- Good
SELECT
column1
, column2
, column3Use short-form type names where PostgreSQL provides them. text and
varchar are not interchangeable and are not flagged against each other.
| Don't use | Use instead |
|---|---|
integer, int4 |
int |
bool |
boolean |
character varying |
varchar |
float8 |
double precision |
float4 |
real |
int2 |
smallint |
int8 |
bigint |
decimal |
numeric |
timestamp without time zone |
timestamp |
timestamp with time zone |
timestamptz |
time without time zone |
time |
time with time zone |
timetz |
Suppress a finding on a specific line:
SELECT something, -- sql-lint:disable trailing-commadisable and disable-line both apply to the line they appear on;
disable-next-line applies to the following line. Use all in place of a
rule id to suppress every rule.
-- sql-lint:disable-next-line trailing-comma
SELECT something,A disable-block <rule|all> directive on the line that opens a /* */ block
comment suppresses the rule for every line in that block. A reason may
follow after a colon, e.g. disable-block all: reason here.
For the common case — commenting out a chunk of code without the linter
demanding * prefixes (or other comment styling) on each line — use the
shorthand EXCLUDED CODE on the opening line. It is an alias for
sql-lint:disable-block all:
/* EXCLUDED CODE
SELECT not_ready_yet(
foo
, bar
);
*/Text may follow it (e.g. /* EXCLUDED CODE — disabled until issue #123).
To suppress a chunk of actual code — e.g. borrowed code deliberately left
unreformatted — wrap it in disable-block <rule|all> / enable-block
directives on their own -- lines. Unlike the /* */-anchored form above,
this pair works outside a comment and is closed explicitly rather than by a
comment's own */ (if never closed, it runs to end of file):
-- sql-lint:disable-block all
SELECT n1.nspname AS fk_schema_name,
c1.relname AS fk_table_name
FROM pg_catalog.pg_constraint k1
-- sql-lint:enable-blockA reason can follow all or a rule id after a colon — it's ignored by the
matcher, but says why for the next reader:
-- sql-lint:disable-block all: borrowed verbatim, do not reformatAdd a subroutine to bin/sql-lint and register it in the @rules array:
sub check_my_rule {
my ($file, $lines) = @_;
my @out;
for my $l (@$lines) {
next unless $l->{state} eq 'CODE';
my $cc = code_content($l->{text});
next unless $cc =~ /pattern/;
maybe_finding(\@out, $lines, $file, $l->{lineno}, 'my-rule',
'description of the violation');
}
return @out;
}Each line in $lines is a hashref with lineno, state (CODE or
COMMENT), and text. The code_content() helper strips strings,
comments, and /* from a line. maybe_finding() handles suppression
automatically.
Then add test fixtures and/or inline tests:
- Fixtures (
test/fixtures/): use.good.sqlfor clean files,.bad.sqlfor files with violations. Bad fixtures need a-- expect-findings: Nheader. No test code changes required. - Inline tests (
test/02-scanner.t): for specific edge cases, regression tests, and assertions about finding content.
Tests use Perl's Test::More + prove to test the linter itself (not a
consuming project's SQL — that's what make lint is for, from a project
that vendors this repo):
make test # all linters in this repo (currently just sql)
make -C sql test # sql linter onlySee DESIGN.md for architecture decisions and rationale.
This repo is designed to host linters for other languages too (e.g.
PL/pgSQL, bash). Each would be a sibling to sql/ with its own Makefile
and test suite, aggregated by the top-level Makefile.