Skip to content

Add sql-lint: style linter for PostgreSQL SQL files - #16

Open
jnasbyupgrade wants to merge 4 commits into
Postgres-Extensions:masterfrom
jnasbyupgrade:add-sql-lint
Open

Add sql-lint: style linter for PostgreSQL SQL files#16
jnasbyupgrade wants to merge 4 commits into
Postgres-Extensions:masterfrom
jnasbyupgrade:add-sql-lint

Conversation

@jnasbyupgrade

@jnasbyupgrade jnasbyupgrade commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports cat_tools PR #49 to this repo (extension_drop).

  • Vendors the shared Postgres-Extensions/linter as a git submodule at .vendor/linter, pinned to b8632c2a3d93de664a45f4622235871e8f19cf78 — the same commit cat_tools#49 pinned.
  • Adds a thin local lint.mk (the entire local footprint per the linter's own README) that self-initializes the submodule via git submodule update --init on first use, then hands off to .vendor/linter/lint.mk. This means make lint works right after a plain git clone, no --recurse-submodules needed.
  • Makefile sets LINT_TARGETS = sql/extension_drop.sql test/, scoping make lint to the actively-maintained source. sql/extension_drop--1.0.0.sql is excluded: it's a frozen, already-released version snapshot (per RELEASE.md's "Ongoing development" section, a released sql/<ext>--<version>.sql is never hand-edited again), so linting it would produce permanent, unfixable findings and make make lint unusable as a CI gate. In practice the linter's own generated-file skip (first line matching both "GENERATED" and "DO NOT EDIT", which sql/extension_drop--1.0.0.sql's /* DO NOT EDIT - AUTO-GENERATED FILE */ header satisfies) already excludes it independently — the explicit LINT_TARGETS scoping just documents the intent, mirroring cat_tools's own Makefile comment.
  • Adds a new lint job to .github/workflows/ci.yml: cheap, PG-version-independent, so it runs once (not matrixed). Its checkout deliberately omits submodules: true — same choice cat_tools made — so make lint (the same command a developer runs locally) is what actually proves lint.mk's self-init works, rather than papering over it with a pre-populated checkout.
  • The existing 12-leg PostgreSQL test matrix now gates behind lint (needs: [lint], if: success()): per ~/test-fixes.md item 3 (gate expensive CI matrices behind cheap ones), there's no point running a dozen expensive legs against a baseline that's already broken by a lint violation. Per item 6, the lint job itself does not get crossed into the PG matrix — it's a single job, not matrix.pg-scoped.
  • lint is added to all-checks-passed's needs: list.

What was fixed

Nothing (originally) — make lint already passes clean against this repo's real SQL (sql/extension_drop.sql, test/deps.sql, test/helpers/*.sql, test/sql/*.sql, test/extension/*.sql). No pre-existing style violations were found, unlike cat_tools (which needed several inline-comment and borrowed-view fixes). Verified locally: with the submodule de-initialized (simulating a fresh CI checkout without submodules), make lint self-initializes to the pinned commit and exits 0.

Update: fixed a real bug — unconditional include breaks every build from a source tarball

Makefile does include lint.mk unconditionally, and lint.mk in turn did include .vendor/linter/lint.mk unconditionally, alongside its self-init rule:

.vendor/linter/lint.mk:
	git submodule update --init -- .vendor/linter

GNU Make tries to satisfy every included file before running any requested target, for any target — not just lint. That's fine in a normal git checkout: if .vendor/linter/lint.mk is missing, Make runs the remake rule, the submodule gets initialized, and everything proceeds. But git archive — which is exactly how PGXN distribution tarballs, and this repo's own make dist (pgxntool/base.mk's dist-only target), are built — never includes submodule content and strips .git entirely. In a tree built that way, .vendor/linter/lint.mk doesn't exist and never can (there's no .git for git submodule update to work with), so the remake rule fails immediately with fatal: not a git repository, and that failure aborted every make invocation — plain make, make install, everything — not just make lint. CI never caught this because CI always runs from a real git checkout with submodules present, never from an actual archived tarball a real downstream consumer would build from.

Fix: guard the include .vendor/linter/lint.mk (and its remake rule) behind ifneq ($(wildcard .git),), which matches both a real .git directory and a gitdir: file (worktree/submodule checkouts). No .git at all → the include and remake rule are skipped entirely, and make/make install/etc. proceed normally without ever touching .vendor/linter. make lint itself becomes unavailable in that case (fails with a plain "No rule to make target 'lint'" if explicitly invoked) — correct, since a frozen source tarball has no reason to lint. The top-level Makefile's own include lint.mk needed no change: lint.mk is a regular committed file (not vendored), so it's always present and includes cleanly regardless of .git.

Verification performed (tarball build, not just make lint from a normal checkout — that environment can never hit this bug):

  1. git archive --prefix=.../ -o ... HEAD from this branch, extracted into a clean scratch directory with no .git anywhere (confirmed via find.vendor/linter present in the archive but completely empty, submodule content stripped as expected).
  2. In that clean tree: make (default target) → succeeds (Nothing to be done for 'all').
  3. make install DESTDIR=<scratch> → succeeds, installs cleanly into the scratch DESTDIR, no reference to .vendor/linter, no submodule error.
  4. make lint in that same tree → fails gracefully with make: *** No rule to make target 'lint'. Stop. (exit 2) rather than the submodule crash — confirms lint is simply unavailable there, not silently broken.
  5. To confirm this was a real, pre-existing bug (not just an untested scenario): archived the prior commit (e96e675, this PR's tip before the fix) the same way and ran plain make in a clean .git-less tree — reproduced the exact failure: fatal: not a git repository (or any of the parent directories): .git / make: *** [lint.mk:9: .vendor/linter/lint.mk] Error 128.
  6. Back in the normal git checkout (real .git present): make lint still runs exactly as before, self-initializing the submodule and exiting 0.

Rebase note

This branch is independent, based on upstream/master. Other in-flight testing-infrastructure branches in this repo (fix-cat-tools-installtest-install-foundationpg-upgrade-ci/pg-tle-ci) also touch test/sql/*.sql and .github/workflows/ci.yml. A rebase against those will likely be needed once they land — not addressed here.

Test plan

  • make lint runs clean locally against sql/extension_drop.sql and test/
  • Simulated a fresh checkout (submodule de-initialized) and confirmed make lint self-inits to the pinned commit and still exits 0
  • .github/workflows/ci.yml validated as parseable YAML
  • Built a real git archive tarball, extracted to a clean directory with no .git, and confirmed plain make and make install DESTDIR=... succeed without touching .vendor/linter
  • Confirmed the same tarball build against the pre-fix commit reproduces the original crash, proving the fix addresses a real bug
  • CI green on this PR (watching)

🤖 Generated with Claude Code

Port cat_tools PR #49: vendor the shared Postgres-Extensions style linter
as a git submodule (.vendor/linter, pinned to b8632c2, same commit used by
cat_tools#49), add a `make lint` target via a thin lint.mk hand-off, and
gate it into CI as a new `lint` job.

LINT_TARGETS is scoped to sql/extension_drop.sql and test/, excluding the
frozen sql/extension_drop--1.0.0.sql release snapshot (RELEASE.md: once a
version is released its sql/<ext>--<version>.sql is never hand-edited
again, so linting it would produce permanent, unfixable findings) --
though the linter's own generated-file skip (first line matching both
"GENERATED" and "DO NOT EDIT") already excludes it independently.

The lint job runs once, ungated (no PG version dependency), and the
existing 12-leg PostgreSQL test matrix now gates behind it (needs: [lint])
per the cost-gating pattern of running cheap checks before expensive ones.
Its own checkout deliberately omits submodules: true, matching cat_tools's
choice, so `make lint` (lint.mk's self-init via `git submodule update
--init`) is exercised the same way a developer would run it locally.

No pre-existing SQL style violations were found in this repo's actual
source (sql/extension_drop.sql, test/*) -- make lint already passes clean.

This branch is independent, based on upstream/master. Other in-flight
testing-infrastructure branches in this repo also touch test/sql/*.sql and
.github/workflows/ci.yml, so a rebase will likely be needed once those
land.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f2faa458-7d70-4d2f-a5bb-1d23343970f6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jnasbyupgrade

Copy link
Copy Markdown
Contributor Author

Verification: confirmed the lint gate can actually fail (not just pass)

Per the lesson in a related incident (linter added to another repo with continue-on-error: true, silently staying green despite real findings), I didn't stop at the first green run here — verified the whole chain can go red:

  1. Confirmed no suppression anywhere in the wiring: no continue-on-error, no || true, no swallowed exit codes in .github/workflows/ci.yml, lint.mk, or .vendor/linter/lint.mk. The CI step is a bare run: make lint — the exact command a developer runs locally.
  2. Injected a real violation (a single-line /* */ comment, appended to sql/extension_drop.sql) and confirmed make lint failed locally with a non-zero exit (make: *** [.vendor/linter/lint.mk:28: lint] Error 11).
  3. Pushed that violation as commit 8198f76 and watched real CI: run 30952107621🧹 SQL Lint failed with the expected finding (comment-single-line), the 🐘 PostgreSQL matrix correctly skipped (proving the needs: [lint] gate works, not just that lint itself failed), and all-checks-passed failed too.
  4. Reverted the violation (e96e675), confirmed the file diff against the original commit is empty, and watched CI go fully green again: run 30952216009 — lint passed, all 12 PG matrix legs ran and passed, all-checks-passed succeeded.

Leaving the temp-violation + revert commits in the branch history rather than squashing, since they're the actual evidence this verification happened on real CI, not just locally.

lint.mk unconditionally included the .vendor/linter submodule's lint.mk,
with a remake rule that runs `git submodule update --init` if the file
is missing. GNU Make tries to satisfy every `include` before running any
requested target, so a tree with no .git at all (exactly what git
archive produces -- PGXN dist packages, make dist, never include
submodule content and strip .git entirely) hit the remake rule on every
single `make` invocation, not just `make lint`. `git submodule update`
fails immediately outside a git repo, aborting the whole build. CI never
caught this because CI always runs from a real checkout with submodules
present.

Guard the include on a real .git being present (directory for a normal
clone, or a gitdir: file for a worktree/submodule checkout -- both match
$(wildcard .git)). Lint is simply unavailable in a tarball build, which
is correct -- there's no reason to lint a frozen source tarball.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant