GitHub - ValkDB/valk-guard: Valk Guard scans raw SQL plus application code and catches risky queries before they merge. It parses real source code, synthesizes SQL from supported ORM/query-builder patterns, runs PostgreSQL-aware checks, and can post findings directly into pull requests.

9 min read Original article ↗

The SQL linter that catches production disasters at PR time.

DELETE FROM orders without a WHERE? SELECT * on a 50M-row table?
Valk Guard finds them in your code — before your pager does at 3am.

CI Go Version License Go Report Card Go Reference

valk-guard demo


Why Valk Guard?

Most SQL linters use regex and only see raw .sql files. Valk Guard parses real source structure instead.

It reads Goqu builder chains, SQLAlchemy ORM calls, Go db.Query invocations, and C# EF Core raw/query-builder calls with source-aware scanners — Go via go/ast, Python via ast, and C# via Roslyn. It synthesizes SQL from your ORM code, feeds it through a real PostgreSQL grammar, and runs every rule against it.

That means: if your ORM builds a DELETE without a WHERE, Valk Guard catches it — even though no raw SQL exists anywhere in your source.

What it prevents Example
Accidental mass updates UPDATE users SET active = false (no WHERE)
Unbounded queries SELECT id, email FROM users (no LIMIT)
Index-killing patterns WHERE email LIKE '%@gmail.com'
Dangerous migrations DROP TABLE, CREATE INDEX without CONCURRENTLY
Schema drift ORM model says email exists, but migration dropped it
ORM footguns session.query(User).delete() — no raw SQL, still caught

Zero config. No database connection. Runs in CI in seconds.

PostgreSQL only. Valk Guard uses a PostgreSQL parser. MySQL, SQLite, and other dialects are not supported.


Quick Start

# Install
go install github.com/valkdb/valk-guard/cmd/valk-guard@latest

# If this is your first Go-installed CLI, add the Go bin dir to PATH.
# Default location: $(go env GOPATH)/bin
export PATH="$(go env GOPATH)/bin:$PATH"

# Scan your project
valk-guard scan .

# Scan only a specific folder
valk-guard scan ./migrations

# Scan only a specific file
valk-guard scan ./queries/report.sql

# JSON for CI pipelines
valk-guard scan . --format json

# Reviewdog PR comments
valk-guard scan . --format rdjsonl

# GitHub Code Scanning (SARIF)
valk-guard scan . --format sarif --output results.sarif

That's it. All 19 rules are enabled by default.

Pass one or more paths to scan only part of a repo:

valk-guard scan ./migrations
valk-guard scan ./queries/report.sql
valk-guard scan ./migrations ./internal

What It Catches

Valk Guard ships with 19 rules across three categories. Here are the highlights:

Rule What it catches Severity
VG002 UPDATE without WHERE — may wipe entire tables error
VG003 DELETE without WHERE — same, but worse error
VG007 DROP TABLE, TRUNCATE in application code error
VG001 SELECT * — over-fetching columns warning
VG005 LIKE '%...' — leading wildcard kills indexes warning
VG008 CREATE INDEX without CONCURRENTLY — blocks writes warning
VG101 ORM model references a column that migrations dropped error
VG105 Query SELECTs a column that doesn't exist in schema error

See all 19 rules with full descriptions, examples, and severity levels.


Not Regex — Source-Aware Analysis

Most SQL linters use regex. Valk Guard walks real source structure instead. It compiles and walks the actual AST of your Go and Python code, and for C# it invokes a Roslyn AST extractor for EF Core raw SQL and deterministic query-builder calls. It understands ORM builder chains and raw execution APIs as first-class SQL sources — no .sql file required.

Go + Goqu — walks builder chains via go/ast

Python + SQLAlchemy — parses ORM chains via Python AST

C# + EF Core — extracts raw SQL and deterministic LINQ via Roslyn

Goqu AST scanning demo SQLAlchemy AST scanning demo C# EF Core AST scanning demo

No raw SQL in those files. Valk Guard synthesizes SQL from the ORM calls, parses it with a PostgreSQL grammar, and runs all 19 rules against it (a handful of checks use targeted regex on parser-extracted clauses when the parser AST doesn't expose the needed field; Goqu, SQLAlchemy, and C# EF Core source scanning are AST-based).

Source How it works
Raw SQL (.sql) Multi-statement parser with dollar-quoting, nested block comments
Go (go/ast) Extracts SQL from db.Query, db.Exec, db.QueryRow and context variants
Goqu Walks builder chains (From/Join/Where/Limit/ForUpdate) via Go AST
SQLAlchemy Parses ORM chains (query/select/join/filter) via Python AST
C# (EF Core) Extracts SQL from ExecuteSql*, FromSql*, SqlQuery*, and synthesizes SQL from deterministic DbSet/LINQ chains

For schema-drift rules (VG101+), it also reads ORM model definitions — Go struct tags (db, gorm) and Python __tablename__ / Column(...) — and cross-references them against your migration DDL.

C# note: query-builder synthesis is intentionally conservative. It covers deterministic DbSet/LINQ shapes such as Where, Select, Take, Skip, Include, Join, GroupJoin, SelectMany, OrderBy, GroupBy, Having, Distinct, ExecuteDelete, ExecuteUpdate, Count, Any, All, Sum, Min, Max, Average, IN/NOT IN via Contains, LIKE/ILIKE, and raw FOR UPDATE via FromSql/ExecuteSql; uncertain dynamic expressions are skipped. ExecuteSqlRaw format normalization rewrites {0} placeholders to $1, so literal {0} text inside raw SQL is a known caveat.


How It Compares

Valk Guard SQL formatters/linters DB-connected advisors Schema-only drift checks
Needs a running database No Usually no Usually yes Usually no
Scans app source (.go, .py, .cs) Yes Rarely No Rarely
Understands ORM/query builders Yes Rarely No Sometimes
Checks schema drift against models Yes Rarely Sometimes Yes
Fits PR review workflows Yes Often Sometimes Often
Auto-fixes SQL No Sometimes No No
Dialect coverage PostgreSQL only Often multi-dialect Varies Varies
Primary value SQL + ORM static analysis SQL style and formatting Live query/runtime insights Migration/model consistency

Valk Guard's niche: static analysis across SQL + ORM code with schema-drift detection, no infrastructure required.


CI / GitHub Actions

Valk Guard is built for CI. This is the minimal full-repo reviewer step; use the copy-paste workflows below for complete jobs:

permissions:
  contents: read
  pull-requests: write

jobs:
  pr-review:
    if: github.event_name == 'pull_request'
    steps:
      - uses: reviewdog/action-setup@v1

      - name: Run valk-guard
        run: |
          set +e
          valk-guard scan . --config .valk-guard.yaml --format rdjsonl > valk-guard.rdjsonl
          code=$?
          set -e

          if [ "$code" -gt 1 ]; then
            exit "$code"
          fi

      - name: Post review comments
        env:
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          reviewdog \
            -f=rdjsonl \
            -name="valk-guard" \
            -reporter=github-pr-review \
            -filter-mode=added \
            -fail-level=none \
            < valk-guard.rdjsonl

Findings (exit 1) are non-blocking. Config/parser errors (exit 2+) fail the job.

Copy-paste workflows:


Live Demo PRs

See valk-guard reviewing real code in ValkDB/valk-guard-example:


Installation

Download a Binary (easiest)

Grab a pre-built binary from GitHub Releases for Linux, macOS, or Windows (amd64/arm64).

Install via Go

go install github.com/valkdb/valk-guard/cmd/valk-guard@latest

If valk-guard is still not found, the install likely succeeded but your Go bin directory is not on PATH yet.

macOS / Linux:

export PATH="$(go env GOPATH)/bin:$PATH"

Windows PowerShell:

$env:Path += ";$(go env GOPATH)\bin"

If you use GOBIN, add that directory instead of $(go env GOPATH)/bin.

Pin in CI (recommended)

go install github.com/valkdb/valk-guard/cmd/valk-guard@vX.Y.Z

Why pin: avoids surprise behavior changes, keeps output processing stable, makes builds reproducible.

Build From Source

git clone https://github.com/ValkDB/valk-guard.git
cd valk-guard
make build

# Try the built-in sample inputs in this repo.
# These intentionally produce findings, so exit code 1 is expected.
./valk-guard scan testdata/sql
./valk-guard scan testdata/python

# Or scan the whole repo / a specific path in your own project.
./valk-guard scan .
./valk-guard scan ./path/to/folder
./valk-guard scan ./path/to/file.sql

# Optional: install into GOBIN or $(go env GOPATH)/bin
make install

Requirements

  • Go >= 1.25.8 for building from source
  • Python >= 3.6 only when scanning .py files for SQLAlchemy usage. No pip packages needed — Valk Guard ships an embedded script using only stdlib (ast, json). If SQLAlchemy candidate files are present and python3 is missing or too old, the scan fails fast with an error.
  • .NET SDK >= 8.0 only when scanning .cs files. The scanner sends C# files to an embedded Roslyn extractor so candidate detection is syntax-based instead of substring-based. The embedded extractor is materialized under the user cache directory, published once as a self-contained binary for the current OS/architecture, and reused until its bundled source changes. Disable it with sources.csharp: false if needed.

Configuration

Zero config works out of the box. To customize, create a .valk-guard.yaml:

exclude:
  - "vendor/**"
  - "generated/**"

# Optional: override which SQL files build the schema snapshot.
# If omitted, Valk Guard uses these defaults:
#   migrations/, migration/, migrate/
migration_paths:
  - "db/migrations"
  - "schema/**/*.sql"

# Optional: disable whole source scanners/model extractors.
# Missing entries default to true.
sources:
  sql: true
  go: true       # database/sql-style Go scanning
  goqu: true
  sqlalchemy: true # aliases: python, py
  csharp: true     # aliases: cs, c#, dotnet

rules:
  VG001:
    severity: warning
    engines: [all]       # all | sql | go | goqu | sqlalchemy | csharp
  VG007:
    enabled: false

go_model:
  mapping_mode: strict   # strict | balanced | permissive

Reference: .valk-guard.yaml.example Source config details: docs/source-config.md

Inline Suppression

-- valk-guard:disable VG001
SELECT * FROM users;

Works in Go (//), Python (#), and C# (//) too. Full guide: docs/suppression.md

Exit Codes

Code Meaning
0 No findings
1 Findings reported (any severity)
2 Config, runtime, or parser error

How It Works

flowchart LR
  subgraph S1["1. Source Inputs"]
    A1[".sql files"]
    A2["Go code"]
    A3["Goqu usage"]
    A4["Python SQLAlchemy"]
    A5["C# EF Core"]
  end

  subgraph S2["2. Statement Extraction"]
    B1["Raw SQL Scanner"]
    B2["Go AST Scanner"]
    B3["Goqu Scanner"]
    B4["SQLAlchemy Python AST Scanner"]
    B6["C# EF Core Roslyn AST Scanner"]
    B5["Statements with file/line mapping"]
  end

  subgraph S3["3. Parsing and Schema Context"]
    C1["postgresparser"]
    C2["DDL -> Schema Snapshot"]
    C3["Go Model Extractor"]
    C4["Python Model Extractor"]
    C5["Model Snapshots"]
  end

  subgraph S4["4. Rule Evaluation"]
    D1["Query Rules VG001-VG008"]
    D2["Query-Schema Rules VG105-VG108"]
    D3["Model Schema Rules VG101-VG104 and VG109-VG111"]
  end

  subgraph S5["5. Output"]
    E0["Findings"]
    E1["terminal"]
    E2["json"]
    E3["sarif"]
    E4["rdjsonl"]
  end

  A1 --> B1 --> B5
  A2 --> B2 --> B5
  A3 --> B3 --> B5
  A4 --> B4 --> B5
  A5 --> B6 --> B5

  B5 --> C1
  C1 --> D1
  C1 --> C2
  C1 --> D2

  A2 --> C3 --> C5
  A4 --> C4 --> C5

  C2 --> D2
  C5 --> D2
  C2 --> D3
  C5 --> D3

  D1 --> E0
  D2 --> E0
  D3 --> E0
  E0 --> E1
  E0 --> E2
  E0 --> E3
Loading

Roadmap

Track progress and vote on what matters to you:

  • Deeper builder semantics — aliases, nested subqueries, richer predicate trees
  • More ORM integrations with AST-backed synthetic SQL
  • SQLAlchemy 2.0 mapped_column() support — modern model extraction
  • Custom rule authoring — define your own rules in YAML or Go
  • Severity-gated CI — block PRs only on errors, not warnings

Documentation


Development

make build      # build binary
make test       # run tests (-race)
make lint       # golangci-lint
make cover      # coverage report
make check      # fmt + vet + lint + test

Contributing / Security / License