sqlguard

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 14, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

postgres-sqlguard

A turquoise gopher holding a sword protectively in front of a friendly blue elephant-shaped database

Lint Tests Codecov Go Reference License

Stop dangerous PostgreSQL statements before they reach your database.

postgres-sqlguard is a driver-independent Go library that parses complete SQL with a real PostgreSQL grammar and applies only the safety rules your application explicitly enables. It needs no database connection and never executes SQL.

Why SQLGuard

  • PostgreSQL-aware — decisions come from parsed structure, not regexes or keyword matching.
  • Fail closed — malformed input is rejected before any rule runs, and every top-level statement and statement-bearing CTE is inspected.
  • Explicit policy — built-in and application rules compose in a stable, deterministic order; nothing is enabled globally or implicitly.
  • Driver-independent — keep the validation core separate from pgx, database/sql, and connection lifecycle choices.
  • Privacy-safe — typed errors and bounded observability events never expose SQL text, literals, arguments, credentials, or raw parser diagnostics.
  • CGO or no-CGO — use the faster native parser by default or build the same public API with an automatically selected WebAssembly backend.

Install

go get github.com/almostinf/postgres-sqlguard@latest

SQLGuard requires Go 1.26. CGO_ENABLED=1 also requires a working C compiler; CGO_ENABLED=0 requires no compiler or custom build tag.

Quick start

package main

import (
	"context"
	"errors"
	"fmt"

	sqlguard "github.com/almostinf/postgres-sqlguard"
	"github.com/almostinf/postgres-sqlguard/pkg/rules"
)

func main() {
	engine, err := sqlguard.NewEngine(
		sqlguard.EngineOptions{},
		rules.NewUpdateRequiresWhere(),
	)
	if err != nil {
		fmt.Println("configure sqlguard:", err)

		return
	}

	err = engine.Validate(
		context.Background(),
		"UPDATE accounts SET active = false",
	)

	var violation *sqlguard.Violation
	if errors.As(err, &violation) {
		fmt.Println("rejected by", violation.RuleID())
	}
}

Output:

rejected by update_requires_where

The equivalent executable example is compiled and run by the test suite.

Built-in policies

Import the optional rules from github.com/almostinf/postgres-sqlguard/pkg/rules:

Constructor What it prevents
NewUpdateRequiresWhere UPDATE without a syntactic WHERE clause
NewDeleteRequiresWhere DELETE without a syntactic WHERE clause
NewInsertRequiresColumns INSERT without an explicit target-column list
NewDenyTruncate Every TRUNCATE statement
NewDenyDropTable Every DROP TABLE statement
NewDenyAlterTable Every ALTER TABLE statement

Register only what your application needs. Comments and string literals cannot fake an operation or a required clause because the rules inspect the parsed tree. A syntactic predicate such as WHERE TRUE is still a WHERE clause; SQLGuard does not attempt semantic tautology analysis.

Read the validation guide for custom rules, prepared validation, traversal, typed errors, context, concurrency, and the complete privacy boundary.

How validation works

The engine parses the entire input before evaluating policy. For valid SQL it visits top-level statements in input order, then statement-bearing CTEs depth-first, and runs rules in registration order. The first rejection returns a typed Violation. Parser failures return a typed ParseError; callers use standard errors.As and errors.Is inspection rather than parsing messages.

Constructed engines and prepared values are safe for concurrent use. A custom rule or observability sink may be invoked concurrently and therefore owns the synchronization of its mutable state.

Choose a parser backend

Both builds expose the same API, PostgreSQL 17 grammar, parser-neutral rule view, validation results, and privacy guarantees. Selection happens at build time:

Build Best for Trade-off
CGO_ENABLED=1 Lowest validation cost and smaller binaries Requires a working C toolchain
CGO_ENABLED=0 Simple cross-compilation and environments without a C compiler Higher cold-start time, memory, and binary size

On an Apple M4 Pro with Go 1.26, five-run median direct-validation latency ranged from 5,821–75,779 ns/op for CGO and 13,742–164,908 ns/op for no-CGO across the four inputs below.

When validation runs immediately before a driver call, that measured in-process work corresponds to the following time per check:

Input CGO no-CGO
Simple 0.000005821 s 0.000013742 s
Medium 0.000075779 s 0.000164908 s
Multi-statement 0.000034075 s 0.000068819 s
Nested CTE 0.000048213 s 0.000099737 s

Grouped bars comparing median CGO and no-CGO direct validation latency across four SQL complexity levels Grouped bars comparing median allocated bytes per CGO and no-CGO validation across four SQL complexity levels

Bars comparing median cold-process maximum resident memory for CGO and no-CGO size probes Bars comparing stripped linked CGO and no-CGO size-probe binaries

These 2026-09-14 measurements come from one documented host, not portable limits or predictions. They measure in-process validation without a database round trip, so the seconds above are the validator's absolute runtime rather than a measured end-to-end delta against an unguarded database call. ns/op is execution time, not sampled CPU utilization. See the release benchmark evidence for raw runs, exact units, medians, environment, methodology, and regeneration, or Parser backends for the supported matrix and backend details.

Observability

Observability is opt-in through EngineOptions. Use your own Metrics and Logger implementations or the official packages:

Sink failures and panics never replace validation results or weaken an enforce-mode rejection. Read the observability guide for configuration and failure semantics.

Integration boundaries

SQLGuard validates only the calls your application sends through it. It does not intercept driver traffic, bind arguments, enforce PostgreSQL privileges, replace transactions or constraints, or prove that arbitrary SQL is safe for a particular business operation.

The repository includes compiling examples for a narrow pgx wrapper and a bounded prepared-validation cache. They demonstrate patterns, not stable production adapters. Read Integrating with pgx before adopting their boundary.

Documentation

License

Licensed under Apache-2.0. The project retains this permissive license for its explicit contributor patent grant; see the licensing and attribution review for the decision, redistribution notes, and third-party scope.

Documentation

Overview

Package sqlguard validates PostgreSQL statements against explicitly registered safety rules without requiring a database connection or driver.

An Engine parses the complete input with the PostgreSQL 17 grammar before it invokes any Rule. It visits top-level statements in input order and, for each statement, visits the root followed by statement-bearing CTEs depth-first in declaration order. Rules run in registration order for every visited statement, and validation stops at the first rejection.

A minimal setup is:

engine, err := sqlguard.NewEngine(sqlguard.EngineOptions{}, applicationRule)
if err != nil {
	return err
}

if err := engine.Validate(ctx, query); err != nil {
	return err
}

Parser failures and rule violations expose only bounded metadata through ParseError and Violation. Their messages and unwrap chains never retain the submitted SQL or raw parser diagnostics.

Observability is disabled by default. EngineOptions can independently enable Metrics and Logger implementations. For every terminal result, Engine calls each enabled implementation synchronously with bounded ValidationEvent metadata. Sink errors and panics are contained independently and never change the validation result or weaken enforce behavior. Events never contain SQL, arguments, errors, parser diagnostics, or caller-context values.

Engine is safe for concurrent validation after construction. The same Rule, Metrics, or Logger instance may be called concurrently, so implementations must be immutable or protect their own state. Context cancellation is checked before and after parsing and before every rule call. The synchronous CGO parser cannot be interrupted while its C call is in progress; cancellation is observed when parsing returns.

Example
package main

import (
	"context"
	"errors"
	"fmt"

	sqlguard "github.com/almostinf/postgres-sqlguard"
	"github.com/almostinf/postgres-sqlguard/pkg/rules"
)

func main() {
	engine, err := sqlguard.NewEngine(
		sqlguard.EngineOptions{},
		rules.NewUpdateRequiresWhere(),
	)
	if err != nil {
		fmt.Println("configure sqlguard:", err)

		return
	}

	err = engine.Validate(
		context.Background(),
		"UPDATE accounts SET active = false",
	)

	var violation *sqlguard.Violation
	if errors.As(err, &violation) {
		fmt.Println("rejected by", violation.RuleID())
	}

}
Output:
rejected by update_requires_where

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrInvalidPrepared = errors.New("sqlguard: invalid prepared value")

ErrInvalidPrepared indicates that prepared validation received a zero or otherwise invalid Prepared value. It contains no SQL or parsed structure.

Functions

This section is empty.

Types

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is an immutable Validator configured with independently optional observability implementations and the ordered Rules supplied explicitly at construction. It has no global registry or implicit default rules. Once constructed, an Engine is safe for concurrent use when its rules and observability implementations satisfy their concurrency contracts.

func NewEngine

func NewEngine(options EngineOptions, rules ...Rule) (*Engine, error)

NewEngine constructs an Engine from options and rules in registration order. Zero-valued options disable observability. A nil Metrics or Logger interface disables that sink, while an interface containing a typed nil is invalid. It returns no partially configured Engine when any option or registration is invalid.

func (*Engine) Prepare

func (e *Engine) Prepare(ctx context.Context, sql string) (Prepared, error)

Prepare synchronously parses the complete SQL input once without evaluating rules. A successful preparation emits no terminal validation outcome. Parse and context failures are emitted once through the Engine's observability implementations before the error is returned.

func (*Engine) Validate

func (e *Engine) Validate(ctx context.Context, sql string) error

Validate prepares the complete SQL input and validates the resulting parsed representation. It preserves the same errors, traversal, context behavior, and single terminal outcome as calling Prepare followed by ValidatePrepared.

func (*Engine) ValidatePrepared

func (e *Engine) ValidatePrepared(ctx context.Context, prepared Prepared) error

ValidatePrepared evaluates every registered rule against a successfully prepared value in deterministic statement and registration order. It checks the caller context before prepared-value validity and before each rule, returns the first failure, and emits exactly one terminal outcome.

type EngineOptions

type EngineOptions struct {
	// Metrics records terminal validation events. A nil value disables metrics.
	Metrics Metrics

	// Logger logs terminal validation events. A nil value disables logging.
	Logger Logger
}

EngineOptions configures independently optional observability implementations for an Engine. Its zero value disables both metrics and logging. A nil interface disables its sink; an interface containing a typed nil is invalid and causes NewEngine to fail.

type Kind

type Kind string

Kind identifies a stable structural PostgreSQL AST node kind.

type Logger

type Logger interface {
	// LogValidation logs one terminal validation event.
	LogValidation(event ValidationEvent) error
}

Logger logs terminal validation events. An Engine calls an enabled Logger implementation synchronously once per validation and contains any returned error or panic. An Engine may call the same implementation concurrently, so implementations must either be immutable or synchronize their own state.

type Metrics

type Metrics interface {
	// RecordValidation records one terminal validation event.
	RecordValidation(event ValidationEvent) error
}

Metrics records terminal validation events. An Engine calls an enabled Metrics implementation synchronously once per validation and contains any returned error or panic. An Engine may call the same implementation concurrently, so implementations must either be immutable or synchronize their own state.

type Node

type Node struct {
	// contains filtered or unexported fields
}

Node is an immutable view of one structural PostgreSQL AST node.

func (Node) Bool

func (n Node) Bool(name string) (bool, bool)

Bool returns a named boolean field.

func (Node) Bytes

func (n Node) Bytes(name string) ([]byte, bool)

Bytes returns a snapshot of a named bytes field.

func (Node) Child

func (n Node) Child(name string) (Node, bool)

Child returns a named field when it contains a node.

func (Node) Children

func (n Node) Children(name string) []Node

Children returns a snapshot of node values stored in a named list field.

func (Node) Enum

func (n Node) Enum(name string) (string, bool)

Enum returns the symbolic value of a named enum field.

func (Node) Float

func (n Node) Float(name string) (float64, bool)

Float returns a named floating-point field.

func (Node) Int

func (n Node) Int(name string) (int64, bool)

Int returns a named signed-integer field.

func (Node) Kind

func (n Node) Kind() Kind

Kind returns the structural node kind.

func (Node) String

func (n Node) String(name string) (string, bool)

String returns a named string field.

func (Node) Uint

func (n Node) Uint(name string) (uint64, bool)

Uint returns a named unsigned-integer field.

func (Node) Walk

func (n Node) Walk(visit func(Node) bool)

Walk visits this node and its descendants in structural pre-order. Returning false from visit stops traversal. A nil visitor performs no work.

type ParseError

type ParseError struct {
	// contains filtered or unexported fields
}

ParseError reports that PostgreSQL parsing failed before rule evaluation. It contains only a bounded category and never retains backend diagnostics.

func (*ParseError) Category

func (e *ParseError) Category() ParseErrorCategory

Category returns the bounded parser-failure category.

func (*ParseError) Error

func (*ParseError) Error() string

Error returns a constant message that cannot disclose parser input.

type ParseErrorCategory

type ParseErrorCategory string

ParseErrorCategory identifies a bounded class of parsing failure. Categories are safe for programmatic handling and never contain parser diagnostics.

const (
	// ParseErrorUnknown indicates a parsing failure without a more specific
	// public category.
	ParseErrorUnknown ParseErrorCategory = "unknown"

	// ParseErrorSyntax indicates that PostgreSQL rejected the input syntax.
	ParseErrorSyntax ParseErrorCategory = "syntax"
)

type Prepared

type Prepared struct {
	// contains filtered or unexported fields
}

Prepared is an opaque immutable representation of completely parsed SQL. A successfully prepared value may be copied and validated concurrently by any Engine. The zero value is invalid and is rejected by ValidatePrepared.

type Rule

type Rule interface {
	// ID returns a stable identifier used for programmatic error handling.
	ID() string

	// Evaluate inspects one immutable parsed statement and returns a bounded
	// decision without constructing or returning an error.
	Evaluate(ctx context.Context, statement Statement) RuleResult
}

Rule evaluates one parsed statement. The same Rule instance may be invoked concurrently by separate validation calls, so implementations must either be immutable or synchronize their own state.

type RuleResult

type RuleResult struct {
	// contains filtered or unexported fields
}

RuleResult is the bounded outcome of Rule evaluation. Its zero value rejects validation so accidentally omitted decisions fail closed.

func Allow

func Allow() RuleResult

Allow returns a result that permits validation to continue.

func Reject

func Reject() RuleResult

Reject returns a result that stops validation with a policy violation.

func (RuleResult) Rejected

func (r RuleResult) Rejected() bool

Rejected reports whether validation must stop for this result.

type Statement

type Statement struct {
	// contains filtered or unexported fields
}

Statement is an immutable view of one statement root. Its values are valid for the duration of a Rule call and may be copied, but rules must not retain them after Evaluate returns.

func (Statement) Kind

func (s Statement) Kind() Kind

Kind returns the statement root kind.

func (Statement) Root

func (s Statement) Root() Node

Root returns the statement root as a generic read-only node.

func (Statement) Walk

func (s Statement) Walk(visit func(Node) bool)

Walk visits the statement root and its descendants in structural pre-order. Returning false from visit stops traversal. A nil visitor performs no work.

type ValidationEvent

type ValidationEvent struct {
	// contains filtered or unexported fields
}

ValidationEvent is immutable bounded metadata for one terminal validation result. It never contains SQL, arguments, errors, parser diagnostics, or caller-context values.

func (ValidationEvent) Mode

Mode returns the bounded validation execution mode.

func (ValidationEvent) Outcome

func (e ValidationEvent) Outcome() ValidationOutcome

Outcome returns the bounded terminal validation result.

func (ValidationEvent) RuleID

func (e ValidationEvent) RuleID() string

RuleID returns the stable rejecting rule identifier for a policy violation and an empty string for every other outcome.

type ValidationMode

type ValidationMode string

ValidationMode identifies a bounded validation execution mode for observability implementations.

const (
	// ValidationModeEnforce identifies validation that rejects unsafe or
	// unparseable input.
	ValidationModeEnforce ValidationMode = "enforce"
)

type ValidationOutcome

type ValidationOutcome string

ValidationOutcome identifies a bounded terminal validation result for observability implementations.

const (
	// ValidationOutcomeAllowed indicates that parsing and all registered rule
	// evaluations succeeded.
	ValidationOutcomeAllowed ValidationOutcome = "allowed"

	// ValidationOutcomePolicyViolation indicates that a registered rule
	// rejected a statement.
	ValidationOutcomePolicyViolation ValidationOutcome = "policy_violation"

	// ValidationOutcomeParserFailure indicates that the PostgreSQL parser
	// rejected the complete input.
	ValidationOutcomeParserFailure ValidationOutcome = "parser_failure"

	// ValidationOutcomeCanceled indicates that validation stopped because the
	// caller context was canceled or its deadline expired.
	ValidationOutcomeCanceled ValidationOutcome = "canceled"

	// ValidationOutcomeInvalidPrepared indicates that prepared validation
	// received a zero or otherwise invalid Prepared value.
	ValidationOutcomeInvalidPrepared ValidationOutcome = "invalid_prepared"
)

type Validator

type Validator interface {
	// Validate checks the complete SQL input and returns the first failure.
	Validate(ctx context.Context, sql string) error
}

Validator checks SQL against a configured policy without requiring a database connection or a database-driver dependency.

type Violation

type Violation struct {
	// contains filtered or unexported fields
}

Violation reports that a validation rule rejected a statement. It stores only the stable identifier of the responsible rule.

func (*Violation) Error

func (*Violation) Error() string

Error returns a constant message that cannot disclose validation input.

func (*Violation) RuleID

func (e *Violation) RuleID() string

RuleID returns the stable identifier of the rule that rejected a statement.

Directories

Path Synopsis
example
pgx
Package pgxexample demonstrates how to place a sqlguard validator in front of a narrow pgx execution boundary.
Package pgxexample demonstrates how to place a sqlguard validator in front of a narrow pgx execution boundary.
pgx-prepared
Package pgxprepared demonstrates a bounded cache of SQLGuard prepared values in front of pgx-style Exec, Query, and QueryRow operations.
Package pgxprepared demonstrates a bounded cache of SQLGuard prepared values in front of pgx-style Exec, Query, and QueryRow operations.
internal
parser
Package parser contains the internal PostgreSQL parsing boundary.
Package parser contains the internal PostgreSQL parsing boundary.
testutil
Package testutil contains helpers shared by postgres-sqlguard tests.
Package testutil contains helpers shared by postgres-sqlguard tests.
testutil/cmd/benchcharts command
Command benchcharts generates deterministic SVG charts from release benchmark evidence.
Command benchcharts generates deterministic SVG charts from release benchmark evidence.
testutil/cmd/sizeprobe command
Package main provides the representative executable used to compare parser-backend footprint.
Package main provides the representative executable used to compare parser-backend footprint.
pkg
observability/prometheus
Package prometheus provides the official Prometheus metrics integration for postgres-sqlguard validation outcomes.
Package prometheus provides the official Prometheus metrics integration for postgres-sqlguard validation outcomes.
observability/slog
Package slog provides the official log/slog integration for postgres-sqlguard validation outcomes.
Package slog provides the official log/slog integration for postgres-sqlguard validation outcomes.
rules
Package rules provides opt-in PostgreSQL-aware validation rules for use with sqlguard.
Package rules provides opt-in PostgreSQL-aware validation rules for use with sqlguard.

Jump to

Keyboard shortcuts

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