Static analyzer for detecting true, data-dependent N+1 database query patterns in Go code. The tool traces query parameters backward from GORM-style sinks through interprocedural dataflow and reports cases where a dynamic value reaches a database call inside a loop.
Unlike pattern matchers that flag any query inside a loop, Query Controller uses IFDS-style tabulation to distinguish real N+1 issues (loop-indexed or phi-dependent values flowing into query arguments) from safe constant queries.
For architecture, algorithm details, and contributor onboarding, see onboarding.md.
Analysis runs in two phases:
-
Phase 1 — Dataflow trace
Seeds GORM query sinks (Where,Raw,Query,Exec, etc.) and walks backward through SSA, following assignments, stores, and cross-function parameter/return edges. -
Phase 2 — Vulnerability check
Flags sinks inside loops that also invoke a database execution method (Find,Scan,First,Exec, …) when the traced argument is not a compile-time constant and depends on loop iteration (phi nodes, indexed access, etc.).
for _, u := range users {
GetUser(db, u.name) // loop variable flows into query arg
}
func GetUser(db *GormDB, u string) {
db.Where("it is", u).Find(nil) // ← reported as N+1
}
| Path | Description |
|---|---|
analyzer.go, cross_packages.go, internal/ |
golangci-lint plugin with cross-package fact propagation |
onboarding.md |
Architecture overview and contributor onboarding |
code_examples/ |
Test fixtures for the plugin |
plugin/ |
golangci-lint module plugin registration |
The project is a golangci-lint plugin with cross-package summaries (SinkParamFact, ReturnToParamFact, ExecutorFact).
- Go 1.25+
- golangci-lint v2 with custom build support (for the plugin)
The plugin registers as linter name nplusone.
Create a .custom-gcl.yml (see .custom-gcl.yml in the repo root) and build the custom binary:
golangci-lint customThis produces a custom-gcl binary in the current directory.
./custom-gcl run ./...Configuration lives in .golangci.yml. Example diagnostic:
🚨 [TRUE N+1] Found dynamic database execution in loop (detected via dataflow)
Run plugin tests from the project root:
go test ./...Tests cover scenarios such as base N+1, nested functions, tuple returns, recursion, dynamic query building, and state checking. Fixtures live under code_examples/.
Query sinks (arguments are traced):
Where, Raw, Not, Or, Select, Having, Group, Order, Query, QueryRow, Exec
Execution methods (must appear in the loop body for a finding):
Scan, Find, First, Take, Last, Pluck, Count, Exec, QueryRow, Query
The standalone CLI also treats print as an execution method for debugging.
- Targets GORM-style method names; other ORMs are not modeled unless they use the same surface API.
- Cross-package analysis is supported via exported analysis facts.
- Findings require data dependence on loop iteration; queries in loops with constant arguments are not reported.
See repository history for license information.