Skip to main content
pgvet’s power comes from its extensible rule system that detects common SQL pitfalls before they reach production.

Rule Architecture

All pgvet rules implement the Rule interface defined in rule/rule.go:
type Rule interface {
    Name() string
    Description() string
    Check(stmt *pg_query.RawStmt, sql string) []Diagnostic
}
Each rule:
  • Has a unique kebab-case name (e.g., select-star, not-in-subquery)
  • Provides a human-readable description
  • Analyzes parsed PostgreSQL AST nodes via pg_query_go
  • Returns zero or more diagnostics with file location and severity

Diagnostic Structure

When a rule finds an issue, it returns a Diagnostic:
type Diagnostic struct {
    Rule     string   // Rule name
    Message  string   // Description of the issue
    File     string   // Source file path
    Line     int      // Line number (1-based)
    Col      int      // Column number (1-based)
    Severity string   // "warning" or "error"
}

Severity Levels

pgvet uses two severity levels:

Warning

Indicates a code smell or best practice violation that’s likely unintentional but not always wrong:
  • select-star - Using SELECT * makes code fragile
  • limit-without-order - Non-deterministic results (except LIMIT 1)
  • update-without-where - Accidentally updating all rows
  • delete-without-where - Accidentally deleting all rows
  • insert-without-columns - Fragile column order dependency
  • ban-char-type - char(n) pads with spaces
  • timestamp-without-timezone - Loses timezone context
  • order-by-ordinal - Fragile positional references
  • group-by-ordinal - Fragile positional references
  • like-starts-with-wildcard - Prevents index usage
  • offset-without-limit - Returns all remaining rows
  • for-update-no-skip - Can cause lock contention
  • distinct-on-order - Non-deterministic row selection

Error

Indicates code that is almost certainly incorrect:
  • not-in-subquery - NOT IN (SELECT ...) broken when subquery returns NULLs
  • null-comparison - = NULL or <> NULL always yields NULL
  • multi-statement - Multiple statements in one block lose CTE scope

Default vs Opt-In Rules

Default Rules

These rules run automatically when you run pgvet:
// From rule/registry.go
func All() []Rule {
    return []Rule{
        &SelectStar{},
        &LimitWithoutOrder{},
        &NotInSubquery{},
        &ForUpdateNoSkip{},
        &DistinctOnOrder{},
        &NullComparison{},
        &UpdateWithoutWhere{},
        &DeleteWithoutWhere{},
        &InsertWithoutColumns{},
        &BanCharType{},
        &TimestampWithoutTimezone{},
        &OrderByOrdinal{},
        &GroupByOrdinal{},
        &LikeStartsWithWildcard{},
        &OffsetWithoutLimit{},
    }
}
See Default Rules for detailed documentation.

Opt-In Rules

These rules must be explicitly enabled because they may not apply to all codebases:
// From rule/registry.go
func Extra() []Rule {
    return []Rule{
        &MultiStatement{},
    }
}
See Opt-In Rules for detailed documentation.

All Rules (Including Opt-In)

To get all rules programmatically (both default and opt-in):
// From rule/registry.go
func AllIncludingExtra() []Rule {
    return append(All(), Extra()...)
}
This function is used internally when --rules is specified to search the full rule pool.

Enabling and Disabling Rules

Exclude Specific Rules

Use --exclude to skip rules:
pgvet --exclude select-star migrations/
pgvet --exclude select-star,limit-without-order migrations/

Enable Opt-In Rules

Use --rules to explicitly enable extra rules:
pgvet --rules multi-statement migrations/

Run Only Specific Rules

You can run only the rules you specify:
# Run only the multi-statement rule
pgvet --rules multi-statement migrations/

# Run multiple specific rules
pgvet --rules select-star,delete-without-where migrations/

How Rules Work

pgvet uses pg_query_go, which wraps libpg_query (PostgreSQL’s own parser) to:
  1. Parse SQL into an Abstract Syntax Tree (AST)
  2. Walk the AST nodes to find problematic patterns
  3. Extract exact line and column positions for diagnostics
Example rule implementation:
func (r *NullComparison) Check(stmt *pg_query.RawStmt, sql string) []Diagnostic {
    var diags []Diagnostic
    
    walker.Walk(stmt.Stmt, func(node *pg_query.Node) bool {
        ae := node.GetAExpr()
        if ae == nil {
            return true
        }
        
        // Check if operator is = or <>
        if opName != "=" && opName != "<>" {
            return true
        }
        
        // Check if either operand is NULL
        if isNullConst(ae.Lexpr) || isNullConst(ae.Rexpr) {
            line, col := offsetToLineCol(sql, int(ae.Location))
            diags = append(diags, Diagnostic{
                Rule:     r.Name(),
                Message:  r.Description(),
                Line:     line,
                Col:      col,
                Severity: SeverityError,
            })
        }
        return true
    })
    
    return diags
}

Exit Codes

  • 0 - No issues found
  • 1 - Warnings or errors found
  • 2 - Fatal error (parse failure, file not found, etc.)

Next Steps

Default Rules

Explore all 15 rules that run by default

Opt-In Rules

Learn about rules that require explicit enablement

Build docs developers (and LLMs) love