Repository navigation
feat: add rtk sqlfluff lint with JSON filter (~75% token reduction) - #253
Conversation
|
Hi @TheGlitching! PR #241 just landed. Could you rebase this on latest master? The sqlfluff support looks clean, we'd like to review it properly once it's current. |
793ebcd to
aebbaff
Compare
@FlorianBruniaux Rebase done! ready for your review |
|
Hi! Two things needed before we can review:
Thanks! |
|
Hey We are cleaning up the codebase and improving the project structure for better onboarding. As part of this effort, PR #826 reorganizes No logic changes — only file moves and import path updates. What you need to doRebase your branch on git fetch origin && git rebase origin/developGit detects renames automatically. If you get import conflicts, update the paths: use crate::git; // now: use crate::cmds::git::git;
use crate::tracking; // now: use crate::core::tracking;
use crate::config; // now: use crate::core::config;
use crate::init; // now: use crate::hooks::init;
use crate::gain; // now: use crate::analytics::gain;Need help rebasing? Tag @aeppling |
abf72d8 to
dea2704
Compare
|
Hi @pszymkowiak and @aeppling ! Thanks for the guidance.
Ready for review! |
|
@TheGlitching — picked this back up to bench it. Token-savings claim
Output is clean: group-by-rule + by-file, dbt path compaction Three blockers before merge — all from
With those three, Thanks for the patience — solid work, just got buried. Ping when ready, |
62b4c1a to
52a3ac8
Compare
|
Thank you for the detailed review, @pszymkowiak. All three items have been addressed:
Ready for re-review. |
|
Conflict resolved, kept mvn_cmd from develop and sqlfluff_cmd from this branch. Ready for review @pszymkowiak |
KuSh
left a comment
There was a problem hiding this comment.
Reviewed via /code-review high. Overall the filter logic and the 13-test suite (including the real-schema regression for start_line_no) are solid, but there are a few functional bugs and one integration gap that undercut the PR's own token-savings claim, plus some registrations in other files that this PR doesn't touch and so can't be inline-commented on directly — flagging them here instead:
src/discover/rules.rs(no matching entry yet — see theruffRtkRulearound line 458 for the pattern to follow): noRtkRulewas added forsqlfluff, so the Claude Code hook never auto-rewrites rawsqlfluff lint ...tortk sqlfluff lint .... Without this, the new filter and its ~75% savings are never applied transparently — unlikeruff/pytest/mypy/pip, which all have matchingRULESentries. This also is step 3 of the checklist insrc/cmds/README.md#adding-a-new-command-filter. This one seems important to fix since it's the mechanism that makes the filter actually get used.src/core/tracking.rs:1223:categorize_command's match arm ("pytest" | "ruff" | "mypy" | "pip" => "python") doesn't include"sqlfluff", so it falls into the_ => "other"bucket andrtk gain's ecosystem breakdown undercounts Python tooling for anyone using this command.src/cmds/system/pipe_cmd.rs(around line 27, next toruff-check/ruff-format): nosqlfluff-lintentry inresolve_filter, so... | rtk pipe sqlfluff-lint(e.g. piping saved CI logs through the filter) isn't supported, unlike the equivalent ruff/pytest/mypy workflows.src/core/toml_filter.rs(RUST_HANDLED_COMMANDS, around line 293 next to"pip"):"sqlfluff"isn't in this list, so a user's custom.rtk/filters/*.tomlfilter targetingsqlfluffwould silently never fire, with no shadow-warning printed (unlike every other Rust-handled command).
Requesting changes for the rules.rs hook registration (defeats the point of the filter if it never fires transparently) and the functional bugs called out inline: dialect/dialects typo, --format json disabling the filter, uncapped passthrough output, the production .expect(), the missing line/column data that forces a raw re-run to actually act on any violation, and the bareword-path heuristic — see the line-56 comment for a contained fix that doesn't require the shared tokenizer work this would otherwise need.
|
Hey @TheGlitching, circling back on this one. The filter itself has proven its worth (the benchmarks earlier in this thread still hold), so it would be a shame to let it stall now. Two things are standing between this PR and a merge:
You've been responsive throughout this PR's life, so I'm confident this can land once those are sorted. Happy to re-bench after the update. |
29ab9fb to
0f3b1d1
Compare
|
Hi @KuSh and @pszymkowiak, Thank you both for the detailed reviews and guidance! All items have been addressed: 1. Hook and Integration Registrations
2. Bug Fixes in
|
0f3b1d1 to
0455d7c
Compare
KuSh
left a comment
There was a problem hiding this comment.
Re-reviewed the latest push (0455d7c) via /code-review high. The previous round's 7 findings are all fixed (verified independently and resolved on the earlier review) — thanks for the quick turn.
This pass surfaced 7 more findings, all inline. One more that can't be inline-commented since this PR doesn't touch that file:
src/cmds/js/lint_cmd.rs(around lines 56, 118, 198, next to the ruff/mypy/pylint/flake8 entries): the genericrtk lint <linter>dispatcher's Python-linter allowlist and its format-flag/filter-dispatch match arms weren't extended forsqlfluff.rtk lint sqlfluff models/silently falls through to the generic text-scraping filter instead of using the new structuredfilter_sqlfluff_lint_json— so the compact grouped report this PR adds is unreachable through that entry point, unlike the analogous ruff/mypy paths. Requesting this be wired up alongside the directrtk sqlfluffentry point, for parity with the other Python linters.
Requesting changes for the 3 correctness bugs (JSON-parse-failure fallback pattern violation, the "first at" location bug, and non-deterministic tie ordering) plus the lint_cmd.rs gap above. The remaining inline comments are non-blocking — documentation/deferral for the joined-short-flag gap (tied to #3681), and quality/duplication notes.
…wiring Address the second-round review on PR rtk-ai#253: - JSON parse failure now passes raw sqlfluff output through unchanged, with an stderr warning, honoring the non-negotiable fallback rule (rust-patterns.md) instead of emitting a truncated hybrid summary. - "first at" rule locations are derived from the original sqlfluff JSON order, not the sorted worst-first file list, so the sample points at the earliest occurrence rather than the first file in the report. - All report sorts are now deterministic: tied rule counts break alphabetically by rule code, tied files by path. - rtk lint sqlfluff now routes through the shared structured filter, with the same format-flag handling as sqlfluff_cmd::run; a user-supplied non-JSON format keeps raw (truncated) output. - Document the joined short-flag gap (-fhuman) as a known limitation to be fixed with the arg-tokenizer migration (rtk-ai#3681). - Add regression tests for fallback passthrough (incl. large, uncapped), original-order first-location, and deterministic tie ordering. Pre-existing, out of scope, tracked separately: ruff_cmd's identical fallback deviation, a shared compact_path/grouping helper across the six duplicated modules, and the bare-sqlfluff discover rule gap (same as ruff).
0455d7c to
70e778e
Compare
|
Thanks for the detailed re-review, @KuSh - and @pszymkowiak for the nudge. All findings from this round are addressed in Blocking
Integration gap
Non-blocking
Verification - Ready for a re-bench whenever you have time. |
KuSh
left a comment
There was a problem hiding this comment.
Re-reviewed at 70e778e3 via /code-review high, in a dedicated worktree.
Round 2 is fully cleared — verified independently and all 7 threads resolved:
- Parse-failure path now
eprintln!s and returns raw output unchanged, per the non-negotiable fallback rule. by_rulenow iterates the pre-sortparsedvec, so "first at" is a genuine first occurrence.- All three sorts carry deterministic tiebreakers.
lint_cmd.rswires sqlfluff throughis_python_linter, format detection, JSON injection and dispatch.- The joined-short-flag gap is documented in place with a #3681 pointer, as agreed.
Quality gate in the worktree is clean: cargo fmt --check, cargo clippy --all-targets (zero warnings), cargo test --all all pass, and the new regression tests land directly on the round-2 findings. Merged against current upstream/develop it's conflict-free and still fully green. Nice turnaround.
This pass found 7 more, all inline. The two I'd most like addressed are the pair where a failed sqlfluff run is reported as success: through rtk lint sqlfluff it prints SQLFluff: No issues found and exits 2, and through rtk sqlfluff it prints a single blank line and exits 2. Both were reproduced with a stub sqlfluff on PATH. For a proxy the caller doesn't know is in the loop, silently converting a failure into a clean-lint message is the most costly thing a filter can do.
The third blocking one is about the report keeping enough detail to act on — see that comment for the note that this filter is budgeting against a stale 60% savings floor when CONTRIBUTING.md's authoritative floor is 20%, which leaves plenty of room for a ruff-style per-violation section.
The remaining four are non-blocking: two missing truncation markers, one latent serde alias issue, and a documentation note about bare-path handling.
Also checked and clean, for the record: module registration and every sibling registration point, no OOB on the effective_args[1] accesses, arg construction across 11 invocation shapes (no double lint, no double --format, and -f json / --format=json both detected), and exit-code propagation.
…ction) Adds `rtk sqlfluff lint` for SQL linting workflows, particularly dbt projects. Discovered as a savings opportunity via `rtk discover`. - Injects `--format json` automatically; passes through if user sets it - Groups violations by rule (top N) and by file with per-file breakdown - Surfaces first violation location per rule and line numbers per file - Shows fixable count and suggests `sqlfluff fix` when relevant - Passes fix/format/version/parse subcommands through unchanged (capped) - Registers hook rule in `discover/rules.rs` to auto-rewrite `sqlfluff lint` - Preserves exit codes for CI/CD compatibility - dbt-aware path compaction: keeps models/, macros/, seeds/ prefixes Real-world tested on a production dbt project. Tolerates both v3+ `start_line_no` and legacy `line_no` schemas. 17 tests: happy path, edge cases, JSON parse fallback, real schema regression, hook classification/rewrite, and token savings assertion (≥60% verified).
…wiring Address the second-round review on PR rtk-ai#253: - JSON parse failure now passes raw sqlfluff output through unchanged, with an stderr warning, honoring the non-negotiable fallback rule (rust-patterns.md) instead of emitting a truncated hybrid summary. - "first at" rule locations are derived from the original sqlfluff JSON order, not the sorted worst-first file list, so the sample points at the earliest occurrence rather than the first file in the report. - All report sorts are now deterministic: tied rule counts break alphabetically by rule code, tied files by path. - rtk lint sqlfluff now routes through the shared structured filter, with the same format-flag handling as sqlfluff_cmd::run; a user-supplied non-JSON format keeps raw (truncated) output. - Document the joined short-flag gap (-fhuman) as a known limitation to be fixed with the arg-tokenizer migration (rtk-ai#3681). - Add regression tests for fallback passthrough (incl. large, uncapped), original-order first-location, and deterministic tie ordering. Pre-existing, out of scope, tracked separately: ruff_cmd's identical fallback deviation, a shared compact_path/grouping helper across the six duplicated modules, and the bare-sqlfluff discover rule gap (same as ruff).
…, field aliases, README) - F1: gate 'No issues found' on success; surface stderr on failure (lint_cmd) - F2: early_exit_on_failure() so non-zero exit surfaces stderr (sqlfluff_cmd) - F3: add description field + per-violation section (50-line cap, ruff-style) - F4: '+N more rules' overflow after top-rules list - F5: '+N more rules' overflow in per-file breakdown - F6: dual-field line/col deserialization (start_line_no/line_no aliases) - F7: README example with bare-path usage - F8: pointer-only (lockstep comment already in lint_cmd) Test fixture updated to include realistic sqlfluff fields (end_line_no, end_line_pos, fixable) that real output always emits.
70e778e to
0db62bd
Compare
|
All 8 round-3 review items are addressed in 0db62bd. Summary: Blocking:
Non-blocking:
Verification: cargo fmt / clippy / test all pass. Test fixture updated to include realistic sqlfluff fields ( |
KuSh
left a comment
There was a problem hiding this comment.
Round 4, reviewed at 0db62bd5 in a dedicated worktree, verified against real sqlfluff 4.3.0 in Docker (a second pass independently confirmed on 3.2.5 and 2.3.5). cargo fmt --check and cargo clippy --all-targets are both clean — none of what follows is visible to CI, which is part of the problem.
Round 3 is fully addressed in intent: the Violations: section, both ... +N more markers, the split line_no/start_line_no fields, the README note and the savings bound are all in. Piped directly (rtk pipe -f sqlfluff-lint) the filter now produces 83.2% savings with full path:line:col CODE description detail on real output. The filter itself is in good shape.
But this push introduced two regressions that invert the tool's purpose. Both come from treating a non-zero exit as failure, when sqlfluff — like every linter — exits 1 to mean violations found:
rtk sqlfluff lintnow emits 13,621 bytes where the previous head emitted 966, and where plainsqlfluff lintemits 1,724. That is 7.9x more output than not using rtk at all, andrtk gainscores it 0.0%.rtk lint sqlfluffprintsSQLFluff: failed (exit 1)and discards every violation.
A correction I owe you: both regressions are downstream of a mistake in my round-3 review. I claimed sqlfluff writes fatal errors to stderr with an empty stdout; I had reproduced that against a stub rather than the real tool. Real sqlfluff writes Error: Unknown dialect 'NOPE' to stdout with stderr empty. The previous head already surfaced that correctly through the round-2 raw passthrough, so the two findings I filed were largely artifacts — and the fix they prompted is what broke the normal path. My apologies for the detour.
The third blocking item is a truncation-ordering bug: the new Violations: section is built from sqlfluff's emission order rather than the ranked order used everywhere above it, so it drops exactly the worst file. On a 61-file run the file ranked #1 under Top files: contributed zero of the 50 lines shown.
The pattern worth naming: that ordering bug is the third instance of one confusion — two vecs holding the same data in different orders, with the wrong one picked. Round 2 took "first at" from the sorted vec when it needed original order; this is the mirror image. And the exit-code logic now exists in two copies that this push edited differently, which is why the same command behaves in two incompatible ways depending on entry point. The comment on lint_cmd.rs:117 proposes extracting sqlfluff_cmd::render(stdout, exit_code) so both fixes land once and become unit-testable without a process.
One test would close the class fleet-wide. The suite has 18 tests, all calling filter_sqlfluff_lint_json directly and none exercising run(), which is why a wiring regression sails through. The integration harness already exists (env!("CARGO_BIN_EXE_rtk"), used by 10 tests). One invariant covers every filter: for a producer exiting 1 with parseable output, rtk's stdout must be smaller than the producer's.
Requesting changes on the three blocking items; the rest are notes, including two pre-existing ones (path collapsing, 2.x fixes) that this push makes more visible rather than causes.
…ry points sqlfluff exits 1 whenever it finds violations, which is the normal case this filter exists for. Two changes in the last push read that exit code as failure and so disabled the filter exactly when it had something to compress: - `sqlfluff_cmd` used `early_exit_on_failure()`, which dumps the injected JSON raw. On a 51-file corpus that is 9.8 KB where plain `sqlfluff lint` is 1.1 KB. - `lint_cmd` substituted `result.stderr`, which is always empty: sqlfluff writes even fatal errors to stdout (verified against 2.3.5 and 4.3.0 - `Error: Unknown dialect 'NOPE'` on stdout, stderr empty, exit 2). Every violation was discarded in favour of a 25-byte `SQLFluff: failed (exit 1)`. Both are one bug reached two ways, because both entry points restated the same routing and format-flag logic and were then edited differently. Extract it into `sqlfluff_cmd::plan`, which states once how rtk invokes sqlfluff and how it reads the result back; `lint_cmd` now calls it instead of keeping a copy. The exit code only disambiguates output the filter could not parse, so a fatal error reaches the user verbatim while violations are still summarized. Also in the report: - Rank the `Violations:` section worst-file-first like every section above it. Iterating sqlfluff's emission order and taking the first 50 dropped exactly the file the summary ranked first. - Build only the lines that get printed, instead of every line to keep fifty. - Omit positions sqlfluff did not report rather than fabricating `:0:0`. - Keep two path segments when no dbt root matches, so `migrations/orders.sql` and `reports/orders.sql` stay distinct in the section meant to be opened. - Treat a missing `fixes` key as unknown rather than zero. sqlfluff 2.x omits it entirely, which silently suppressed the `sqlfluff fix` hint. - Rename the savings test to the bound it actually asserts. Tests cover both regressions against real captured output: 4.3.0 for the current field names and fix data, 2.3.5 for the legacy names and the absent `fixes` key.
KuSh
left a comment
There was a problem hiding this comment.
Round 5 (landing) — APPROVED
Claim: add rtk sqlfluff lint with a JSON filter that condenses sqlfluff's output.
Scope: accept.
Ran: real sqlfluff 3.2.5 in Docker — wrapper fidelity checked first (exit 1 with violations; exit 2 with the fatal on stdout, stderr empty) — across a 4-file and a 51-file corpus, both entry points, 0db62bd5 against ebf4cbf5.
All three round-4 blockers are fixed
| blocker | 0db62bd5 |
ebf4cbf5 |
|---|---|---|
sqlfluff_cmd.rs:112 — filter disabled on the normal path |
18,561 B of raw JSON | 2,637 B (plain sqlfluff lint: 2,626) |
lint_cmd.rs:254 — violations discarded |
83 B, SQLFluff: failed (exit 1) |
2,633 B, full detail |
sqlfluff_cmd.rs:264 — worst file hidden |
0 of zzz_worst.sql's violations in the section |
leads the section, all 18 present |
rtk gain went from 0.0% to 96.2% on the 51-file corpus. Savings against the baseline rtk tracks: 85.8% on 4 files, 96.2% on 51, against the 20% floor in CONTRIBUTING.md.
Sharing one decision between the two entry points was the right shape, and it is what made the exit-code semantics unit-testable instead of dependent on the runner. Err(_) if exit_code != 0 rather than an exit-code taxonomy is the part that will still be correct after sqlfluff renumbers something.
Checked and correct — no need to re-verify
Overflow markers (+41 more files, … +68 more); description present in the detail lines; zero fabricated :0:0 coordinates; compact_path distinguishes migrations/orders.sql from reports/orders.sql; fixes: Option<Vec<_>> gated by fixes_reported; four independent line_no / start_line_no / line_pos / start_line_pos fields. cargo fmt --check and cargo clippy --all-targets clean, 3,246 tests on the branch; merged onto the current develop tip (22 commits ahead of this branch's base) it is a clean merge, clean gates, 3,278 tests.
One thing I checked and dropped
I flagged that the two entry points were not byte-identical as you claimed. They differ by 4 bytes, and it is only the tee log filename — …_sqlfluff.log against …_lint.log — which has to name the invoking command. Your claim was right; ignore that.
Your question
The second reads better to me, since
Top rulescurrently gives a code with no indication of what it means. But it reshapes something you settled inruff_cmd, so I have left it alone here. Say the word and I will do either in the next push.
Leaving it alone was the right call, and I do not want either change in this PR. Two reasons. The Top rules reshape changes something shared with ruff_cmd, and a cross-filter design decision should not be settled inside a sqlfluff PR — it should be decided for both filters at once. And this PR is five rounds in on a three-round budget; a reshape buys another round for something that is not blocking merge, when against the baseline rtk actually measures you are at 85.8–96.2%.
I have filed it as #3920 with both of your measurements and the ruff_cmd coupling written down, so the decision gets made once rather than drifting between filters. Your numbers are the substance of that issue — flagging it rather than settling it yourself was the right instinct.
Next
Nothing. Approving. All 17 open threads are resolved in this round: 14 fixed and verified by mechanism, 2 superseded by the plan / render refactor, and 1 no longer relevant now that early_exit_on_failure is gone.
Thanks for the patience across five rounds — and for the round-4 measurements, which found the regression's shape faster than I would have.
What & Why
Found
sqlfluff lintas a missed opportunity usingrtk discoveron aproduction dbt project — commands unhandled. This adds
rtk sqlfluff lintfollowing the same pattern as
rtk ruff.How it works
--format jsonautomatically so output is structuredsqlfluff fixwhen relevantfix,format,versionsubcommands pass through unchangedmodels/,macros/,seeds/prefixesReal-world discovery
While testing, I found that sqlfluff v3+ uses
start_line_no/start_line_posin its JSON output (not
line_no/line_posas older docs suggest). Fixed andcovered with a regression test using actual command output.
Tests
13 tests: happy path, edge cases, JSON parse fallback, real schema
regression fixture, and token savings assertion (≥60% verified).
Example output
Before (
sqlfluff lint models/):~3,000 tokens of raw text with per-line violations
After (
rtk sqlfluff lint models/):SQLFluff: 10 violations in 3 files (2 fixable)
═══════════════════════════════════════
Top rules:
RF01 (4x)
LT09 (3x)
LT12 (2x)
Top files:
models/staging/stg_customers.sql (5 violations)
RF01 (2)
LT09 (2)
LT12 (1)
...
💡 Run
sqlfluff fixto auto-fix 2 violations