| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Production-safe SQL query analyzer for Go applications.
Detects slow queries, dangerous SQL patterns, and performance issues — both at runtime and statically. Think of it as golangci-lint for SQL queries.
go get github.com/KARTIKrocks/sqlguardCLI tool:
go install github.com/KARTIKrocks/sqlguard/cmd/sqlguard@latest| Rule | Severity | Description |
|---|---|---|
| select-star | WARNING | SELECT * — selects all columns unnecessarily |
| leading-wildcard | WARNING | LIKE '%...' (and ILIKE) — index cannot be used |
| non-sargable-predicate | WARNING | WHERE LOWER(col) = ... — function on column defeats its index |
| add-not-null-without-default | WARNING | ALTER TABLE ... ADD COLUMN ... NOT NULL without DEFAULT — fails / rewrites a populated table |
| implicit-join | WARNING | FROM a, b — comma join; a forgotten condition becomes a cartesian product |
| cartesian-join | WARNING | Multiple tables with no join condition or WHERE — a cartesian product (incl. CROSS JOIN) |
| in-list-too-large | WARNING | IN (...) value list with more than max-length (default 100) elements |
| large-offset | WARNING | OFFSET above threshold (default 1000) — deep pagination scans/discards skipped rows |
| select-distinct | INFO | SELECT DISTINCT — often masks duplicate rows from an unintended join |
| delete-without-where | CRITICAL | DELETE without WHERE — deletes all rows |
| update-without-where | CRITICAL | UPDATE without WHERE — updates all rows |
| insert-without-columns | WARNING | INSERT without an explicit column list (VALUES or ... SELECT) — breaks on schema change |
| select-without-limit | WARNING | SELECT without LIMIT or WHERE — may return excessive rows |
| orderby-without-limit | INFO | ORDER BY without LIMIT — sorts entire result set |
| n-plus-one | WARNING | Same query pattern repeated N times (runtime only) |
| slow-query | WARNING | Query exceeds latency threshold (runtime only) |
| seq-scan | WARNING | Sequential scan detected via EXPLAIN (postgres) |
| full-table-scan | WARNING | Full table scan detected via EXPLAIN (mysql) |
| high-cost | WARNING | High cost operation in query plan |
| no-index-used | WARNING | No index used for a table access detected via EXPLAIN (mysql) |
| filesort | INFO | Using filesort in the query plan — ORDER BY not covered by an index (mysql) |
Drop a .sqlguard.yml at your project root. sqlguard discovers it by walking up from the scanned (or working) directory until it finds the file or the git root. The CLI takes --config <path> and --no-config; the file is optional — without it every rule runs at its default. A fully-commented template lives at .sqlguard.example.yml.
version: 1
rules:
disable: [orderby-without-limit]
severity:
select-star: info # info | warning | critical | off
select-without-limit: "off" # "off" disables the rule
settings:
leading-wildcard:
min-length: 3 # ignore short LIKE '%x%' patterns
in-list-too-large:
max-length: 100 # flag IN (...) lists longer than this
large-offset:
threshold: 1000 # flag literal OFFSET above this
redact: true # redact literals out of Result.Query (default)
slow-query:
threshold: 200ms # runtime middleware threshold
dedup:
window: 1m # report each repeated finding at most once per window ("0" disables)
scan:
exclude-paths: ["(^|/)legacy/"] # static scanner only, regexUnknown keys and rule names are warnings, not errors, so a config written for a newer sqlguard still loads on an older binary; set strict: true to make them fatal. only: [rule, ...] switches to whitelist mode.
Inline suppressions — no config required:
SELECT * FROM users -- sqlguard:ignore
DELETE FROM users /* sqlguard:ignore:delete-without-where */// sqlguard:ignore
db.Exec("DELETE FROM users")
db.Query("SELECT * FROM users") // sqlguard:ignore:select-starIn-SQL directives work at runtime and in the static scanner; the Go-source form is honored by the scanner when it sits on or directly above the call.
Apply the same config to the runtime middleware:
opts, _ := config.Middleware("", ".") // discover from cwd
sqlguard.Register("sqlguard-pg", "pgx", opts...)sqlguard's findings flow into logs, so by default it never emits raw literal values. Before any Result leaves the process its Query is redacted — single-quoted strings and numeric literals become ?, while keywords, identifiers (including "quoted" / `backtick` names) and structure are preserved:
[SQLGUARD WARNING] select-star Query: SELECT * FROM users WHERE email = ?
Every Result also carries a Fingerprint: the redacted query with whitespace collapsed and IN (?, ?, ?) folded to (?). It is a stable, PII-free, low-cardinality identity — safe as a metrics label or log key, and the same value the N+1 detector groups on. The JSON reporter emits it as fingerprint.
Opt out only where the query text is trusted (local debugging):
a := analyzer.Default().WithRawQuery() // standalone analyzer
sqlguard.Register("pg", "pgx", middleware.WithAnalyzer(a))or redact: false in .sqlguard.yml. Fingerprint is populated either way.
sqlguard wraps at the database/sql driver layer, so you get back a real *sql.DB and every query is analyzed automatically — including queries issued by ORMs and query builders (sqlc, ent, sqlx, gorm, pgx-stdlib). There is no wrapper type to thread through your code and no method list to keep in sync.
import (
"database/sql"
"github.com/KARTIKrocks/sqlguard"
"github.com/KARTIKrocks/sqlguard/middleware"
"time"
)
func main() {
// Register an analyzed driver by wrapping an existing one...
sqlguard.Register("sqlguard-pg", "postgres",
middleware.WithSlowQueryThreshold(500*time.Millisecond),
middleware.WithN1Detection(5, 2*time.Second),
)
db, _ := sql.Open("sqlguard-pg", "...") // db is a plain *sql.DB
// ...or wrap a driver.Connector directly (e.g. pgx stdlib):
// db := sqlguard.OpenDB(connector, middleware.WithN1Detection(5, time.Second))
// Use as normal — warnings are logged automatically
db.Query("SELECT * FROM users")
// Output:
// [SQLGUARD WARNING] select-star
// Query: SELECT * FROM users
// Issue: SELECT * detected. Selecting all columns can hurt performance.
// Fix: Select only the columns you need.
}The middleware detects when the same query pattern executes repeatedly — a classic N+1 problem:
sqlguard.Register("sqlguard-pg", "postgres",
middleware.WithN1Detection(5, 2*time.Second), // flag after 5 similar queries in 2s
)
db, _ := sql.Open("sqlguard-pg", "...")N+1 patterns are detected within the configured time window. On the raw database/sql driver path you get back a plain *sql.DB, so detection is process-wide (windowed) — there is no handle to scope it per request. The integration adapters (gormguard, pgxguard, sqlxguard, bunguard, xormguard, entguard) hold the guard and expose ResetN1() to scope detection to a single unit of work; call it at a request boundary.
A recurring query would otherwise re-emit the same static warning on every execution. By default the runtime middleware reports each finding (rule + query fingerprint) at most once per minute, so a hot query doesn't flood your logs. Tune or disable it:
sqlguard.Register("sqlguard-pg", "postgres",
middleware.WithFindingDedup(5*time.Minute), // quieter
)
sqlguard.Register("sqlguard-pg", "postgres",
middleware.WithFindingDedup(0), // disable: report every occurrence
)Or set dedup.window in .sqlguard.yml. Slow-query and N+1 findings have their own emission policy and are unaffected.
The middleware also memoizes analysis per distinct query string (an LRU keyed on the exact query — correct even for the literal-sensitive rules), so a recurring query is parsed and rule-checked once rather than on every execution. A repeated query then costs a cache lookup instead of a full parse (≈1000× cheaper, zero allocations in the repeat case). Default 1024 entries; tune with middleware.WithAnalysisCacheSize(n) or disable with n == 0.
Scan your Go source code for SQL issues without running the application:
# Scan current directory
sqlguard scan .
# Scan specific package
sqlguard scan ./internal/repository
# JSON output (for CI pipelines)
sqlguard scan --format json ./...Exit code is 1 when issues are found, 0 when clean — works with CI/CD pipelines.
Connect to a live database and analyze query plans:
# PostgreSQL
sqlguard explain --db "postgres://user:pass@localhost/mydb?sslmode=disable" \
"SELECT * FROM orders WHERE user_id = 42"
# MySQL
sqlguard explain --dialect mysql --db "user:pass@tcp(localhost:3306)/mydb" \
"SELECT * FROM orders WHERE user_id = 42"
# JSON output
sqlguard explain --db "..." --format json "SELECT * FROM orders"Detects sequential scans, missing indexes, filesort, and high-cost operations.
For safety the EXPLAIN runs inside a transaction that is always rolled back, and ANALYZE is never used — the statement is planned, never executed. Input is validated with a comment- and string-literal-aware multi-statement check (a ; hidden in a comment or string can't smuggle a second statement). Only SELECT/WITH is allowed by default; pass --allow-dml to EXPLAIN an INSERT/UPDATE/DELETE (still rolled back). DDL/SET/transaction-control is always refused.
The transaction is additionally read-only everywhere except MySQL/MariaDB under --allow-dml: those servers reject every statement in a read-only transaction (error 1792), including an EXPLAIN that only plans it. There the guarantee rests on the validation, on plain EXPLAIN never executing the statement, and on the unconditional rollback.
MariaDB works through --dialect mysql. The MySQL plan is requested as EXPLAIN FORMAT=TRADITIONAL, since MySQL 9 defaults @@explain_format to TREE.
go get github.com/KARTIKrocks/sqlguard/integrations/gormguardimport (
"github.com/KARTIKrocks/sqlguard/integrations/gormguard"
"github.com/KARTIKrocks/sqlguard/middleware"
)
gormDB, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
// Register as GORM plugin — hooks into all queries automatically
gormguard.Register(gormDB)
// Or customize via the standard middleware options
gormguard.Register(gormDB,
middleware.WithSlowQueryThreshold(500*time.Millisecond),
middleware.WithN1Detection(10, time.Second),
)go get github.com/KARTIKrocks/sqlguard/integrations/sqlxguardimport (
"github.com/KARTIKrocks/sqlguard/integrations/sqlxguard"
"github.com/KARTIKrocks/sqlguard/middleware"
)
sqlxDB := sqlx.MustConnect("postgres", dsn)
db := sqlxguard.WrapSqlx(sqlxDB,
middleware.WithSlowQueryThreshold(500*time.Millisecond),
)
var users []User
db.Select(&users, "SELECT * FROM users") // warns about SELECT *The database/sql driver wrapper covers pgx-stdlib (pgx/v5/stdlib). For the native pgx APIs (pgxpool.Pool, pgx.Conn — which bypass database/sql entirely) use pgxguard. It hooks pgx's own tracer seam, so every Query/QueryRow/Exec and every SendBatch is analyzed without a wrapper type or a method list.
go get github.com/KARTIKrocks/sqlguard/integrations/pgxguardimport (
"github.com/KARTIKrocks/sqlguard/integrations/pgxguard"
"github.com/KARTIKrocks/sqlguard/middleware"
"github.com/jackc/pgx/v5/pgxpool"
)
cfg, _ := pgxpool.ParseConfig(dsn)
pgxguard.ApplyPool(cfg,
middleware.WithSlowQueryThreshold(50*time.Millisecond),
middleware.WithN1Detection(10, time.Second),
)
pool, _ := pgxpool.NewWithConfig(ctx, cfg)Apply (for *pgx.ConnConfig) and ApplyPool (for *pgxpool.Config) compose with any tracer already installed via pgx's own multitracer, so sqlguard coexists with otelpgx, ddtrace and friends rather than silently overwriting them. Configuration is the standard middleware.Option set — same as the driver wrapper, no parallel surface to learn.
Coverage: Query / QueryRow / Exec (via pgx.QueryTracer) and SendBatch (via pgx.BatchTracer). Prepared-statement execution is already covered by QueryTracer, so PrepareTracer is deliberately omitted to avoid double-reporting. CopyFrom carries no SQL and is out of scope.
bun and xorm build SQL through their own query layers and expose native before/after hook seams. bunguard and xormguard plug into those seams and run every statement through the same shared core — same middleware.Option set, no parallel surface.
go get github.com/KARTIKrocks/sqlguard/integrations/bunguard
go get github.com/KARTIKrocks/sqlguard/integrations/xormguard// bun — register a QueryHook
db.AddQueryHook(bunguard.New(
middleware.WithSlowQueryThreshold(500*time.Millisecond),
middleware.WithN1Detection(10, time.Second),
))
// xorm — register a Hook
engine.AddHook(xormguard.New(
middleware.WithSlowQueryThreshold(500*time.Millisecond),
))ent runs on database/sql, so the simplest coverage is to point entsql at a *sql.DB from sqlguard.Register/OpenDB. entguard is the dedicated alternative: it decorates ent's own dialect.Driver, so it covers every Exec/Query (and transactions it opens) regardless of how the *sql.DB was created.
go get github.com/KARTIKrocks/sqlguard/integrations/entguarddrv, _ := entsql.Open(dialect.Postgres, dsn)
guarded := entguard.Wrap(drv,
middleware.WithSlowQueryThreshold(500*time.Millisecond),
middleware.WithN1Detection(10, time.Second),
)
client := ent.NewClient(ent.Driver(guarded))Every adapter (gormguard, bunguard, xormguard, entguard, pgxguard, sqlxguard) exposes a ResetN1() you can call at a per-request boundary to scope N+1 detection to one unit of work.
By default the analyzer uses a zero-dependency fallback parser: it strips SQL comments and string-literal contents before pattern matching, so keywords inside comments/strings and identifiers like update_at no longer cause false positives. It never errors — SQL it can't fully understand still yields a best-effort result, so analysis never breaks your query path.
For exact, structural analysis, opt into a real grammar. These live in separate modules so the core stays dependency-free:
go get github.com/KARTIKrocks/sqlguard/parsers/pgparser # PostgreSQL (pure Go, no cgo)
go get github.com/KARTIKrocks/sqlguard/parsers/mysqlparser # MySQL (pure Go, no cgo)import (
"github.com/KARTIKrocks/sqlguard"
"github.com/KARTIKrocks/sqlguard/middleware"
"github.com/KARTIKrocks/sqlguard/parsers/pgparser"
)
sqlguard.Register("sqlguard-pg", "pgx", middleware.WithParser(pgparser.New()))
db, _ := sql.Open("sqlguard-pg", dsn)
// Or with the standalone analyzer:
a := analyzer.Default().WithParser(pgparser.New())A real parser drives the false-positive-prone facts (statement kind, WHERE/LIMIT/ORDER BY/FROM presence, SELECT *, SELECT DISTINCT, OFFSET, explicit INSERT columns) from the AST instead of regex. CTEs, subqueries, and dialect syntax are handled correctly; anything the grammar rejects (dynamic SQL, driver placeholders) transparently degrades to the fallback parser.
A few facts stay lexical heuristics even with a real parser, because they read literal values the AST discards or are intentionally text-level: IN-list size (in-list-too-large), comma/cartesian joins (implicit-join / cartesian-join), and the literal/text checks (leading-wildcard, non-sargable-predicate, add-not-null-without-default). These keep their zero-dependency, best-effort behavior regardless of the parser.
import "github.com/KARTIKrocks/sqlguard/analyzer"
// Create analyzer with only the rules you want
a := analyzer.New(
analyzer.CheckDeleteWithoutWhere,
analyzer.CheckUpdateWithoutWhere,
)
// Or use all defaults
a := analyzer.Default()
// Analyze a query
results := a.Analyze("DELETE FROM users")
for _, r := range results {
fmt.Printf("[%s] %s: %s\n", r.Severity, r.RuleName, r.Message)
}make help # List all targets
make all # tidy, fmt, vet, lint, build, test (all modules)
make build # Compile all modules; `make cli` builds bin/sqlguard
make test # Run tests across all modules (test-race adds -race)
make lint # Run golangci-lint across all modules
make fmt # gofmt -s + goimports
make tidy # go mod tidy across all modules
make install # Install the CLI to $GOPATH/binThe middleware wraps the database/sql driver chain, so every query is analyzed regardless of how it's issued (Query/Exec/Prepare/Tx, context variants, and any ORM/query builder on top — sqlc, ent, sqlx, gorm, pgx-stdlib). There is no method allowlist to keep in sync; you get back a real *sql.DB.
Opt-in adapter modules, each built on the same middleware.Guard core, extend coverage to APIs that bypass or sit above the database/sql driver path:
All six inherit redaction-by-default, stable fingerprints, the parser seam, and slow-query/N+1 detection from the shared core, and expose ResetN1() for per-request scoping.
| Back | FazBrowse Home | New Git URL |