unqueryvet is a comprehensive Go static analysis tool (linter) for SQL queries. It detects SELECT * usage, N+1 query problems, SQL injection vulnerabilities, and provides suggestions for query optimization.
| Feature | Description |
|---|---|
| SELECT * Detection | Finds SELECT * in raw SQL, SQL builders, and templates |
| N+1 Query Detection | Identifies queries inside loops |
| SQL Injection Scanner | Detects fmt.Sprintf and string concatenation vulnerabilities |
| 12 SQL Builder Support | Squirrel, GORM, SQLx, Ent, PGX, Bun, SQLBoiler, Jet, sqlc, goqu, rel, reform |
| Custom Rules DSL | Define your own analysis rules |
| LSP Server | Real-time IDE integration |
| Interactive TUI | Fix issues interactively |
go install github.com/MirrexOne/unqueryvet/cmd/unqueryvet@latestgo install github.com/MirrexOne/unqueryvet/cmd/unqueryvet-lsp@latestdocker pull ghcr.io/mirrexone/unqueryvet:latest
docker run --rm -v $(pwd):/app ghcr.io/mirrexone/unqueryvet /app/...Add to your .golangci.yml:
version: "2"
linters:
enable:
- unqueryvet
settings:
unqueryvet:
check-sql-builders: true# Analyze all packages
unqueryvet ./...
# Verbose output with explanations
unqueryvet -verbose ./...
# Quiet mode (errors only) for CI/CD
unqueryvet -quiet ./...
# Enable N+1 detection
unqueryvet -n1 ./...
# Enable SQL injection scanning
unqueryvet -sqli ./...
# Show statistics
unqueryvet -stats ./...
# Interactive fix mode
unqueryvet -fix ./...
# Show version
unqueryvet -version| Flag | Description |
|---|---|
-version |
Print version information |
-verbose |
Enable verbose output with detailed explanations |
-quiet |
Quiet mode (only errors) |
-stats |
Show analysis statistics |
-no-color |
Disable colored output |
-n1 |
Detect potential N+1 query problems |
-sqli |
Detect potential SQL injection vulnerabilities |
-fix |
Interactive fix mode - step through issues and apply fixes |
# Create config file
cat > .unqueryvet.yaml << 'EOF'
severity: warning
check-sql-builders: true
check-n1-queries: true
check-sql-injection: true
ignored-files:
- "*_test.go"
- "vendor/**"
EOF
# Run (auto-loads config)
unqueryvet ./...Bad code:
// Direct SELECT *
query := "SELECT * FROM users"
// Aliased wildcard
query := "SELECT t.* FROM users t"
// In subquery
query := "SELECT id FROM (SELECT * FROM users)"
// String concatenation
query := "SELECT * " + "FROM users"
// Format string
query := fmt.Sprintf("SELECT * FROM %s", table)
// SQL builders
squirrel.Select("*").From("users")
db.Model(&User{}).Select("*")
goqu.From("users").Select(goqu.Star())Good code:
// Explicit columns
query := "SELECT id, name, email FROM users"
// SQL builders
squirrel.Select("id", "name", "email").From("users")
db.Model(&User{}).Select("id", "name", "email")
goqu.From("users").Select("id", "name", "email")Bad code (triggers warning):
users, _ := db.Query("SELECT id, name FROM users")
for users.Next() {
var user User
users.Scan(&user.ID, &user.Name)
// N+1 problem: query inside loop
orders, _ := db.Query("SELECT * FROM orders WHERE user_id = ?", user.ID)
}Good code:
// Use JOIN
query := `
SELECT u.id, u.name, o.id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
`
// Or use IN clause
userIDs := []int{1, 2, 3, 4, 5}
query := "SELECT * FROM orders WHERE user_id IN (?)"
db.Query(query, userIDs)Bad code (triggers warning):
// String concatenation with user input
query := "SELECT * FROM users WHERE name = '" + userName + "'"
// fmt.Sprintf with user input
query := fmt.Sprintf("SELECT * FROM users WHERE id = %s", userID)Good code:
// Parameterized query
query := "SELECT id, name FROM users WHERE name = ?"
db.Query(query, userName)
// Named parameters
query := "SELECT id, name FROM users WHERE id = :id"
db.NamedQuery(query, map[string]interface{}{"id": userID})# Diagnostic severity: "error" or "warning"
severity: warning
# Core analysis options
check-sql-builders: true
check-aliased-wildcard: true
check-string-concat: true
check-format-strings: true
check-string-builder: true
check-subqueries: true
# Advanced analysis
check-n1-queries: true # N+1 query detection
check-sql-injection: true # SQL injection scanning
# SQL builder libraries to check
sql-builders:
squirrel: true
gorm: true
sqlx: true
ent: true
pgx: true
bun: true
sqlboiler: true
jet: true
sqlc: true
goqu: true
rel: true
reform: true
# File patterns to ignore (glob)
ignored-files:
- "*_test.go"
- "testdata/**"
- "vendor/**"
- "mock_*.go"
# Function patterns to ignore (regex)
ignored-functions:
- "debug\\..*"
- "test.*"
# Allowed SELECT * patterns (regex)
allowed-patterns:
- "SELECT \\* FROM information_schema\\..*"
- "SELECT \\* FROM pg_catalog\\..*"
- "SELECT \\* FROM temp_.*"
# Output options
output:
format: text # text, json, sarif
color: auto # auto, always, never
verbose: false
quiet: false# Disable colors
export NO_COLOR=1
# Set config path
export UNQUERYVET_CONFIG=/path/to/.unqueryvet.yamlDefine your own analysis rules using a powerful DSL with three levels of complexity.
# .unqueryvet.yaml
rules:
select-star: error # Built-in rule severity
n1-queries: warning
sql-injection: error
ignore:
- "*_test.go"
- "testdata/**"
allow:
- "COUNT(*)"
- "information_schema.*"custom-rules:
- id: allow-temp-tables
pattern: SELECT * FROM $TABLE
when: isTempTable(table)
action: allow
- id: dangerous-delete
pattern: DELETE FROM $TABLE
when: "!has_where"
message: "DELETE without WHERE clause"
severity: errorcustom-rules:
- id: n1-detection
pattern: $DB.Query($QUERY)
when: |
in_loop &&
!contains(function, "batch") &&
!matches(file, "_test.go$")
message: "N+1 query in loop"
severity: warning
fix: "Use batch query or preloading"| Metavariables | Description |
|---|---|
$TABLE |
Table name (with optional schema) |
$VAR |
Identifier/variable |
$QUERY |
String literal |
$COLS |
Column list |
$DB |
Database object |
| Variables | Description |
|---|---|
file, package, function |
Code context |
query, query_type, table |
SQL context |
has_where, has_join |
Query structure |
in_loop, loop_depth |
Loop context |
builder |
SQL builder type |
| Functions | Description |
|---|---|
contains(s, sub) |
String contains |
matches(s, regex) |
Regex match |
isSystemTable(t) |
System table check |
isTempTable(t) |
Temp table check |
isAggregate(q) |
Aggregate function check |
| Operators | Description |
|---|---|
=~, !~ |
Regex match/not match |
&&, ||, ! |
Logical operators |
Full documentation: docs/DSL.md
The LSP server provides real-time analysis in your IDE.
unqueryvet-lspInstall the extension from extensions/vscode/ or configure manually:
// .vscode/settings.json
{
"unqueryvet.enable": true,
"unqueryvet.path": "unqueryvet-lsp",
"unqueryvet.args": ["-n1", "-sqli"],
"unqueryvet.trace.server": "verbose"
}- Real-time diagnostics - See issues as you type
- Hover information - Explanations on hover
- Quick fixes - One-click fixes for SELECT *
- Code completion - Column name suggestions
- Go to definition - Navigate to table definitions
- Build the plugin:
cd extensions/goland && ./gradlew buildPlugin - Install from disk: Settings → Plugins → Install from disk
- Configure: Settings → Tools → unqueryvet
Fix issues interactively with a terminal UI.
unqueryvet -fix ./...| Category | Key | Action |
|---|---|---|
| Navigation | ↑/k |
Previous issue |
↓/j |
Next issue | |
g |
Go to first issue | |
G |
Go to last issue | |
| Actions | Enter/a |
Apply fix |
s |
Skip issue | |
u |
Undo last action | |
p |
Toggle preview | |
| Batch | A |
Apply all remaining |
S |
Skip all remaining | |
R |
Reset all actions | |
| Other | e |
Export results to JSON |
? |
Toggle help | |
q/Esc |
Quit |
Found 15 issues. Review each one:
[1/15] internal/api/users.go:42:15
─────────────────────────────────────
41 | func getUsers(db *sql.DB) {
42 | query := "SELECT * FROM users"
| ^^^^^^^^^^^^^^^^^^^^^ avoid SELECT *
43 | rows, _ := db.Query(query)
Suggestions:
1. SELECT id, username, email, created_at (from struct User)
2. SELECT id, username, email
3. Skip this issue
4. Edit manually
Your choice [1-4]: _
| Builder | Package | Patterns Detected |
|---|---|---|
| Squirrel | github.com/Masterminds/squirrel |
Select("*"), Columns("*") |
| GORM | gorm.io/gorm |
Select("*"), Find(&users) without Select |
| SQLx | github.com/jmoiron/sqlx |
Select(), raw queries |
| Ent | entgo.io/ent |
Query builder patterns |
| PGX | github.com/jackc/pgx |
Query(), QueryRow() |
| Bun | github.com/uptrace/bun |
NewSelect(), raw queries |
| SQLBoiler | github.com/volatiletech/sqlboiler |
Generated query methods |
| Jet | github.com/go-jet/jet |
SELECT(), STAR |
| sqlc | Generated code | SELECT * in .sql files |
| goqu | github.com/doug-martin/goqu |
Select(goqu.Star()), SelectAll() |
| rel | github.com/go-rel/rel |
Find(), FindAll() without Select |
| reform | gopkg.in/reform.v1 |
FindByPrimaryKeyFrom(), SelectAllFrom() |
Squirrel
// Bad
sq.Select("*").From("users")
sq.Select().Columns("*").From("users")
// Good
sq.Select("id", "name", "email").From("users")GORM
// Bad
db.Select("*").Find(&users)
db.Table("users").Find(&users) // implicit SELECT *
// Good
db.Select("id", "name", "email").Find(&users)goqu
// Bad
goqu.From("users").Select(goqu.Star())
goqu.From("users").SelectAll()
// Good
goqu.From("users").Select("id", "name", "email")rel
// Bad
repo.Find(ctx, &user) // loads all columns
repo.FindAll(ctx, &users)
// Good
repo.Find(ctx, &user, rel.Select("id", "name", "email"))reform
// Bad
db.FindByPrimaryKeyFrom(UserTable, id, &user)
db.FindAllFrom(UserTable, "status", "active")
// Good
db.SelectOneFrom(UserTable, "id, name, email WHERE id = ?", id)SQLx
// Bad
db.Select(&users, "SELECT * FROM users")
db.Get(&user, "SELECT * FROM users WHERE id = ?", id)
// Good
db.Select(&users, "SELECT id, name, email FROM users")
db.Get(&user, "SELECT id, name, email FROM users WHERE id = ?", id)Ent
// Bad - implicit SELECT *
users, err := client.User.Query().All(ctx)
// Good - explicit column selection
users, err := client.User.Query().
Select(user.FieldID, user.FieldName, user.FieldEmail).
All(ctx)PGX
// Bad
rows, err := conn.Query(ctx, "SELECT * FROM users")
// Good
rows, err := conn.Query(ctx, "SELECT id, name, email FROM users")Bun
// Bad
db.NewSelect().Model(&users).Scan(ctx)
db.NewSelect().TableExpr("users").Scan(ctx, &users)
// Good
db.NewSelect().Model(&users).Column("id", "name", "email").Scan(ctx)SQLBoiler
// Bad - loads all columns
users, err := models.Users().All(ctx, db)
user, err := models.FindUser(ctx, db, userID)
// Good - explicit column selection
users, err := models.Users(
qm.Select("id", "name", "email"),
).All(ctx, db)Jet
// Bad
stmt := SELECT(User.AllColumns).FROM(User)
// Good
stmt := SELECT(User.ID, User.Name, User.Email).FROM(User)sqlc
-- Bad (in .sql file)
-- name: GetUsers :many
SELECT * FROM users;
-- Good
-- name: GetUsers :many
SELECT id, name, email FROM users;FROM golang:1.24-alpine AS builder
RUN go install github.com/MirrexOne/unqueryvet/cmd/unqueryvet@latest
FROM alpine:latest
COPY --from=builder /go/bin/unqueryvet /usr/local/bin/
ENTRYPOINT ["unqueryvet"]name: SQL Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: MirrexOne/unqueryvet-action@v1
with:
version: latest
args: "-n1 -sqli ./..."
fail-on-issues: truesql-lint:
image: ghcr.io/mirrexone/unqueryvet:latest
script:
- unqueryvet -quiet -n1 -sqli ./...
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"internal/api/users.go:42:15: avoid SELECT * - explicitly specify needed columns
internal/api/orders.go:78:10: potential N+1 query detected - query inside loop
unqueryvet -format=json ./...{
"issues": [
{
"file": "internal/api/users.go",
"line": 42,
"column": 15,
"severity": "warning",
"message": "avoid SELECT * - explicitly specify needed columns",
"rule": "select-star",
"suggestion": "SELECT id, name, email FROM users"
}
],
"summary": {
"total": 5,
"errors": 0,
"warnings": 5,
"files_analyzed": 127,
"duration_ms": 2340
}
}unqueryvet -format=sarif ./... > results.sarif| Code | Meaning |
|---|---|
| 0 | No issues found |
| 1 | Warnings found |
| 2 | Errors found |
| 3 | Analysis failed |
go build ./cmd/unqueryvet
go build ./cmd/unqueryvet-lspgo test ./...go install ./cmd/unqueryvet
go install ./cmd/unqueryvet-lspgit clone https://github.com/MirrexOne/unqueryvet.git
cd unqueryvet
go mod download
go test ./...
go build ./...See CONTRIBUTING.md for guidelines.
MIT License - see LICENSE file for details.
- Built on golang.org/x/tools/go/analysis
- TUI powered by Bubbletea
- Bug Reports: GitHub Issues
- Discussions: GitHub Discussions