unqueryvet

package module
v1.5.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 22, 2026 License: MIT Imports: 3 Imported by: 2

README

unqueryvet

Go Report Card Go Reference License

JetBrains Plugins JetBrains Plugins Downloads

VS Code Marketplace VS Code Installs

Get from Marketplace Get from VS Code Marketplace

unqueryvet is a comprehensive Go 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
Transaction Leak Detection Detects unclosed transactions and improper lifecycle management
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 (all rules enabled by default)
unqueryvet ./...

# Verbose output with explanations
unqueryvet -verbose ./...

# Quiet mode (errors only) for CI/CD
unqueryvet -quiet ./...

# Show statistics
unqueryvet -stats ./...

# Interactive fix mode
unqueryvet -fix ./...

# Show version
unqueryvet -version
Default Rules

All detection rules are enabled by default:

Rule Default Severity Description
select-star warning Detects SELECT * usage
n1-queries warning Detects N+1 query patterns (queries in loops)
sql-injection error Detects SQL injection vulnerabilities
tx-leak warning Detects unclosed SQL transactions

To disable a rule, set its severity to ignore in your .unqueryvet.yaml:

rules:
  n1-queries: ignore  # Disable N+1 detection
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 Force enable N+1 detection (overrides config)
-sqli Force enable SQL injection detection (overrides config)
-tx-leak Force enable transaction leak detection (overrides config)
-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

# Rules are enabled by default - configure severity if needed
rules:
  select-star: warning
  n1-queries: warning
  sql-injection: error

ignore:
  - "*_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})
4. Transaction Leak Detection

Detects unclosed SQL transactions using 19-phase AST analysis. Supports multiple transaction begin methods across different libraries.

Supported Begin Methods:

Library Methods
database/sql Begin, BeginTx
sqlx Beginx, BeginTxx, MustBegin, MustBeginTx
pgx BeginFunc, BeginTxFunc
bun RunInTx
ent Tx, NewTx

Detection Patterns:

Violation Type Severity Description
no_commit_rollback critical Transaction has neither Commit() nor Rollback()
no_rollback high Transaction has Commit() but no Rollback() for error paths
no_commit medium Transaction has Rollback() but no Commit()
early_return high Early return paths bypass Commit() without defer
defer_in_loop high Defer inside loop - defers pile up until function returns
shadowed_transaction high Transaction variable shadowed in inner scope
goroutine_capture high Transaction captured by goroutine without defer
variable_reassignment high Transaction variable reassigned - previous tx may leak
fatal_without_defer high Transaction may leak if os.Exit/log.Fatal called
panic_without_defer medium Transaction may leak if panic() called
conditional_commit medium Commit() inside conditional - may not execute
commit_in_switch medium Commit() in switch/case - may not execute in all cases
commit_in_select medium Commit() in select/case - may not execute
commit_in_loop medium Commit() in loop - may not execute if loop doesn't iterate
deferred_commit medium Using defer Commit() is an antipattern
commit_error_ignored low Commit() error ignored with blank identifier
rollback_error_ignored low Rollback() error ignored with blank identifier

Bad code (triggers error):

func createUser(db *sql.DB, name string) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    // Missing defer tx.Rollback() and missing tx.Commit()!
    
    _, err = tx.Exec("INSERT INTO users (name) VALUES (?)", name)
    if err != nil {
        return err  // Transaction leaked!
    }
    return nil
}

func deferInLoop(db *sql.DB, items []string) error {
    for _, item := range items {
        tx, _ := db.Begin()
        defer tx.Rollback()  // Defers pile up until function returns!
        tx.Exec("INSERT...", item)
        tx.Commit()
    }
    return nil
}

Good code:

func createUser(db *sql.DB, name string) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer tx.Rollback()  // Safe: called after Commit() is a no-op
    
    _, err = tx.Exec("INSERT INTO users (name) VALUES (?)", name)
    if err != nil {
        return err  // Rollback will be called via defer
    }
    
    return tx.Commit()
}

// Using callback pattern (automatically handled)
func createUserCallback(db *pgx.Conn, name string) error {
    return db.BeginFunc(ctx, func(tx pgx.Tx) error {
        _, err := tx.Exec(ctx, "INSERT INTO users (name) VALUES ($1)", name)
        return err  // Commit/Rollback handled automatically
    })
}

Configuration

Full Configuration File (.unqueryvet.yaml)
# Built-in rules severity (all enabled by default)
# Available values: error, warning, info, ignore
rules:
  select-star: warning    # SELECT * detection
  n1-queries: warning     # N+1 query detection
  sql-injection: error    # SQL injection scanning
  tx-leak: warning        # Transaction leak detection

# Diagnostic severity for legacy options: "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

# 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_.*"

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

  - id: require-tx-timeout
    pattern: db.BeginTx($CTX, $OPTS)
    when: "!contains(opts, 'Timeout')"
    message: "Transaction should have timeout set"
    severity: warning
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",
  // All rules enabled by default, args optional
  "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
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: actions/setup-go@v6
        with:
          go-version: '1.24'

      - name: Install unqueryvet
        run: go install github.com/MirrexOne/unqueryvet/cmd/unqueryvet@latest

      - name: Run unqueryvet
        run: unqueryvet -n1 -sqli -tx-leak ./...
GitLab CI
sql-lint:
  image: ghcr.io/mirrexone/unqueryvet:latest
  script:
    - unqueryvet -quiet -n1 -sqli -tx-leak ./...
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

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

Documentation

Overview

Package unqueryvet provides a Go static analysis tool that detects SELECT * usage

Index

Constants

This section is empty.

Variables

View Source
var Analyzer = analyzer.NewAnalyzer()

Analyzer is the main unqueryvet analyzer instance This is the primary export that golangci-lint will use

Functions

func New

func New() *analysis.Analyzer

New creates a new instance of the unqueryvet analyzer

func NewWithConfig

func NewWithConfig(cfg *config.UnqueryvetSettings) *analysis.Analyzer

NewWithConfig creates a new analyzer instance with custom configuration This is the recommended way to use unqueryvet with custom settings

Types

type Settings

type Settings = config.UnqueryvetSettings

Settings is a type alias for UnqueryvetSettings from the config package.

func DefaultSettings

func DefaultSettings() Settings

DefaultSettings returns the default configuration for Unqueryvet.

Directories

Path Synopsis
cmd
unqueryvet command
unqueryvet-lsp command
Command unqueryvet-lsp runs the Language Server Protocol server for unqueryvet.
Command unqueryvet-lsp runs the Language Server Protocol server for unqueryvet.
internal
analyzer
Package analyzer provides the SQL static analysis implementation for detecting SELECT * usage.
Package analyzer provides the SQL static analysis implementation for detecting SELECT * usage.
analyzer/sqlbuilders
Package sqlbuilders provides SQL builder library-specific checkers for SELECT * detection.
Package sqlbuilders provides SQL builder library-specific checkers for SELECT * detection.
cli
Package cli provides command-line interface utilities.
Package cli provides command-line interface utilities.
configloader
Package configloader provides functionality to load configuration from .unqueryvet.yaml file.
Package configloader provides functionality to load configuration from .unqueryvet.yaml file.
dsl
Package dsl provides a domain-specific language for defining custom SQL analysis rules.
Package dsl provides a domain-specific language for defining custom SQL analysis rules.
lsp
Package lsp implements a Language Server Protocol server for unqueryvet.
Package lsp implements a Language Server Protocol server for unqueryvet.
lsp/protocol
Package protocol defines the types and constants for the Language Server Protocol.
Package protocol defines the types and constants for the Language Server Protocol.
messages
Package messages provides enhanced diagnostic messages with examples and documentation links.
Package messages provides enhanced diagnostic messages with examples and documentation links.
runner
Package runner provides a custom analyzer runner with statistics and exit codes.
Package runner provides a custom analyzer runner with statistics and exit codes.
tui
Package tui provides interactive terminal UI for fixing SELECT * issues.
Package tui provides interactive terminal UI for fixing SELECT * issues.
version
Package version provides version information for unqueryvet.
Package version provides version information for unqueryvet.
pkg
config
Package config provides configuration structures for Unqueryvet analyzer.
Package config provides configuration structures for Unqueryvet analyzer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL