Skip to content

Unqueryvet is a Go SQL Linter that detects inconsistencies in SQL queries by applying rules that encourage explicit column selection for better performance, maintainability, and API stability.

License

Notifications You must be signed in to change notification settings

MirrexOne/unqueryvet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

unqueryvet

Go Report Card GoDoc License

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.

Key Features

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

Installation

Standalone Tool

go install github.com/MirrexOne/unqueryvet/cmd/unqueryvet@latest

LSP Server (for IDE integration)

go install github.com/MirrexOne/unqueryvet/cmd/unqueryvet-lsp@latest

Docker

docker pull ghcr.io/mirrexone/unqueryvet:latest
docker run --rm -v $(pwd):/app ghcr.io/mirrexone/unqueryvet /app/...

With golangci-lint

Add to your .golangci.yml:

version: "2"

linters:
  enable:
    - unqueryvet

  settings:
    unqueryvet:
      check-sql-builders: true

Quick Start

Basic Usage

# 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

CLI Flags

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

With Configuration File

# 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 ./...

Detection Examples

1. SELECT * Detection

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")

2. N+1 Query Detection

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)

3. SQL Injection Detection

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})

Configuration

Full Configuration File (.unqueryvet.yaml)

# 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

Environment Variables

# Disable colors
export NO_COLOR=1

# Set config path
export UNQUERYVET_CONFIG=/path/to/.unqueryvet.yaml

Custom Rules DSL

Define your own analysis rules using a powerful DSL with three levels of complexity.

Level 1: Simple Configuration

# .unqueryvet.yaml
rules:
  select-star: error      # Built-in rule severity
  n1-queries: warning
  sql-injection: error

ignore:
  - "*_test.go"
  - "testdata/**"

allow:
  - "COUNT(*)"
  - "information_schema.*"

Level 2: Pattern Matching

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: error

Level 3: Advanced Conditions

custom-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"

DSL Reference

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


LSP Server (IDE Integration)

The LSP server provides real-time analysis in your IDE.

Starting the Server

unqueryvet-lsp

VS Code Setup

Install 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"
}

Features

  • 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

GoLand/IntelliJ Setup

  1. Build the plugin: cd extensions/goland && ./gradlew buildPlugin
  2. Install from disk: Settings → Plugins → Install from disk
  3. Configure: Settings → Tools → unqueryvet

Interactive TUI Mode

Fix issues interactively with a terminal UI.

unqueryvet -fix ./...

Controls

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

Example Session

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]: _

Supported SQL Builders

Full Support (12 builders)

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()

Examples by Builder

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;

Docker & CI/CD

Dockerfile

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"]

GitHub Actions

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: true

GitLab CI

sql-lint:
  image: ghcr.io/mirrexone/unqueryvet:latest
  script:
    - unqueryvet -quiet -n1 -sqli ./...
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Output Formats

Text (default)

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

JSON

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
  }
}

SARIF (for GitHub Code Scanning)

unqueryvet -format=sarif ./... > results.sarif

Exit Codes

Code Meaning
0 No issues found
1 Warnings found
2 Errors found
3 Analysis failed

Documentation


Development

Build

go build ./cmd/unqueryvet
go build ./cmd/unqueryvet-lsp

Test

go test ./...

Install locally

go install ./cmd/unqueryvet
go install ./cmd/unqueryvet-lsp

Contributing

git clone https://github.com/MirrexOne/unqueryvet.git
cd unqueryvet
go mod download
go test ./...
go build ./...

See CONTRIBUTING.md for guidelines.


License

MIT License - see LICENSE file for details.


Acknowledgments


Support

About

Unqueryvet is a Go SQL Linter that detects inconsistencies in SQL queries by applying rules that encourage explicit column selection for better performance, maintainability, and API stability.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •