gxsql

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 13 Imported by: 0

README

gxsql

Note: The features and direction of gxsql are defined and guided by me, while almost all of the code has been generated with AI assistance. It works for me and is used in my own production projects. Contributions are very welcome — please feel free to open a pull request.

gxsql is a SQL-native data quality assertion framework for Go. It validates database tables through database/sql, renders each expectation as SQL, and evaluates checks in the database instead of loading whole tables into application memory. Validation is collect-all: every expectation runs in declaration order and one report captures all passes and failures. Execution errors are different — by default they stop evaluation and return an error; use gxsql.ContinueOnError() when later expectations should still run after per-expectation database errors.

Install

go get github.com/busyminds/gxsql

gxsql requires Go 1.24 or newer.

The core package is driver-neutral and has no runtime dependencies outside the Go standard library. gxsql does not choose a SQL driver for you: open a *sql.DB with your own database/sql driver, then select the dialect explicitly when validating. The examples below use github.com/jackc/pgx/v5/stdlib, modernc.org/sqlite, github.com/duckdb/duckdb-go/v2, and github.com/go-sql-driver/mysql only as conformance/integration drivers.

Support matrix

Support levels used in this module:

  • supported — covered by the current release docs and CI matrix.
  • built-in — ships a first-party Dialect renderer in package gxsql. Does not imply a CI conformance job or bundled driver.
  • experimental — not used for first-release claims; may change without notice.
  • community-maintained — works through a caller-selected database/sql driver, but the engine/driver stack is owned outside gxsql.
  • expected-to-work — outside the published matrix, but should work if it satisfies the database/sql and dialect contracts.

Built-in dialect renderers are gxsql.Postgres(), gxsql.SQLite(), gxsql.DuckDB(), and gxsql.MySQL(). The matrix below separates dialect/API support from engines exercised in CI conformance jobs.

Area Level Floor / active coverage Notes
Go toolchain supported minimum Go 1.24; actively tested on Go 1.24.x and 1.26.x required to build and run the module
Ubuntu supported ubuntu-24.04 in CI first-class CI target
PostgreSQL supported PostgreSQL 16 in CI via github.com/jackc/pgx/v5/stdlib built-in gxsql.Postgres(); the driver is a conformance-only test dependency
SQLite supported SQLite 3.50.4 in CI via modernc.org/sqlite v1.39.1 built-in gxsql.SQLite(); the driver is a conformance-only test dependency
DuckDB supported DuckDB 1.5.4 in CI via github.com/duckdb/duckdb-go/v2 v2.10504.0 built-in gxsql.DuckDB(); the driver is a conformance-only test dependency
MySQL supported MySQL 8.4 in CI via github.com/go-sql-driver/mysql v1.10.0 built-in gxsql.MySQL(); the driver is a conformance-only test dependency

gxsql is intentionally driver-neutral: the core package validates against a caller-selected database/sql driver, while PostgreSQL, SQLite, DuckDB, and MySQL appear in the matrix as CI conformance paths rather than bundled runtime dependencies.

Example entry points

The most common entry points are below and are expanded in the rest of this README:

  1. ValidateTable quick start.
  2. Report gating with report.Err() / report.Failures().
  3. gxsqltest.Check and gxsqltest.Require for testing.T.
  4. ExportReport for machine-readable JSON export.
  5. TrustedCountQuery / CustomCount for portable join and aggregate counts.
  6. WithMaxFailedCount for a known failed-row allowance.

Quick start

package main

import (
    "context"
    "database/sql"
    "log"
    "time"

    _ "github.com/jackc/pgx/v5/stdlib" // or your database/sql driver
    "github.com/busyminds/gxsql"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    db, err := sql.Open("pgx", "postgres://localhost/mydb?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    suite := gxsql.NewSuite(
        gxsql.RowCount().GreaterOrEqual(1),
        gxsql.Int("age").Between(0, 120),
        gxsql.String("email").NotEmpty(),
        gxsql.Column("id").Unique(),
    )

    report, err := suite.ValidateTable(ctx, db, gxsql.Table("users"),
        gxsql.WithDialect(gxsql.Postgres()),
        gxsql.WithKey("id"),
    )
    if err != nil {
        log.Fatalf("gxsql execution error: %v", err)
    }
    if err := report.Err(); err != nil {
        log.Fatalf("data quality check failed: %v", err)
    }
}

Scoped validation

Use TrustedScope with WithScope to limit every expectation to rows matching a caller-defined predicate. Predicate text is trusted Go-code input, not an untrusted SQL sandbox: callers must not pass user-authored predicate text. Keep predicate text fixed in Go, use ? placeholders, and pass each dynamic value as a separate argument so the dialect renderer and database/sql bind the values without string interpolation. The following examples assume ctx, db, and suite from the quick start:

tenantID := "tenant-acme"
tenantScope := gxsql.TrustedScope("tenant-acme", "tenant_id = ?", tenantID)

tenantReport, err := suite.ValidateTable(ctx, db, gxsql.Table("users"),
    gxsql.WithDialect(gxsql.Postgres()),
    gxsql.WithScope(tenantScope),
)
if err != nil {
    log.Fatal(err)
}
if err := tenantReport.Err(); err != nil {
    log.Fatal(err)
}
batchID := int64(42)
batchScope := gxsql.TrustedScope("batch-42", "batch_id = ?", batchID)

batchReport, err := suite.ValidateTable(ctx, db, gxsql.Table("events"),
    gxsql.WithDialect(gxsql.Postgres()),
    gxsql.WithScope(batchScope),
)
if err != nil {
    log.Fatal(err)
}
if err := batchReport.Err(); err != nil {
    log.Fatal(err)
}

Use a half-open time window (>= start and < end) with both bounds supplied as separate values:

start := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2025, time.January, 2, 0, 0, 0, 0, time.UTC)
windowScope := gxsql.TrustedScope(
    "events-2025-01-01",
    "event_at >= ? AND event_at < ?",
    start,
    end,
)

windowReport, err := suite.ValidateTable(ctx, db, gxsql.Table("events"),
    gxsql.WithDialect(gxsql.Postgres()),
    gxsql.WithScope(windowScope),
)
if err != nil {
    log.Fatal(err)
}
if err := windowReport.Err(); err != nil {
    log.Fatal(err)
}

Report.ScopeID and the exported JSON scope.id carry caller identity only; they do not serialize the scope predicate text or bound arguments. Default errors, Report.String() display output, and default ExportReport output omit those scope fields. Ordinary samples and failed keys remain subject to the usual report redaction guidance. For production validation, use a read-only database role (ideally limited to validation views) and set a context deadline on every ValidateTable call.

Custom count checks

Use TrustedCountQuery and CustomCount when a built-in expectation cannot express the rule but a trusted SQL count can. Template SQL is Go-code input that your team reviews; it is not a sandbox for untrusted SQL. Never insert user-authored SQL into templates or interpolate identifiers or values into template text.

The library renders {{target}} from the validated TableRef and {{scope}} from WithScope (or TRUE when unscoped). A template must contain exactly one {{target}} and one {{scope}}, both outside SQL strings and comments. Place both markers in syntactically valid SQL and qualify scope column references when the query uses table aliases. Custom ? placeholders must come after {{scope}}; bound argument order is scope arguments first, then custom arguments. Preflight rejects invalid markers, placeholder placement, and arity mismatches before any custom-count SQL runs.

The query must return one row and one non-negative signed-integer count; textual numerics are not coerced. Results use KindCustom with a complete FailedCount and RowDenominatorUnavailable; samples and failed keys are unavailable.

joinCount := gxsql.TrustedCountQuery(`SELECT COUNT(*)
FROM {{target}} AS o
JOIN accounts AS a ON a.id = o.account_id
WHERE {{scope}} AND a.status = ?`, "inactive")

suite := gxsql.NewSuite(gxsql.CustomCount("inactive account orders", joinCount))
report, err := suite.ValidateTable(ctx, db, gxsql.Table("order_lines"),
    gxsql.WithDialect(gxsql.Postgres()),
    gxsql.WithScope(gxsql.TrustedScope("tenant-a", "o.tenant_id = ?", tenantID)),
)

Why gxsql

  • SQL-native validation — expectations render to SQL and run in the database instead of loading whole tables into Go memory.
  • Collect-all results for data failures — assertion failures do not stop later checks; one report holds every pass and failure. Execution errors stop evaluation unless ContinueOnError() is supplied.
  • Actionable failure reporting — per-row checks include failed counts, capped sample values, and optional failed-row keys capped by default.
  • database/sql compatible — works with any driver that satisfies the narrow gxsql.DB interface.
  • Standard-library-only core — no third-party dependencies in the public API.
  • Test integration — the gxsqltest subpackage provides Check and Require adapters for *testing.T.

When to use gxsql

Use gxsql when you need to:

  • Gate deployments or ETL jobs on database table quality.
  • Audit production tables without pulling all rows into application memory.
  • Run CI checks against integration-test databases.
  • Collect every data-quality failure in one report instead of failing on the first check.

When not to use gxsql

  • In-memory Go datagxsql validates database tables only; load rows into Go and validate in memory with a different approach.
  • Non-SQL storesgxsql validates tables through database/sql only.
  • Custom expectation types — built-in expectations are constructed via the provided builders; Expectation is sealed and not an extension point.

Dialect notes

  • Built-in dialects are gxsql.Postgres(), gxsql.SQLite(), gxsql.DuckDB(), and gxsql.MySQL().
  • Pass gxsql.WithDialect(...) explicitly in production code, tests, and examples; ValidateTable defaults to PostgreSQL when no dialect is supplied.
  • gxsql.DuckDB() renders double-quoted identifiers, $1, $2, … placeholders, and LENGTH(expr) for string-length checks. Import and open a compatible database/sql DuckDB driver yourself — gxsql does not bundle one.
  • gxsql.MySQL() renders backtick-quoted identifiers, ? placeholders, and CHAR_LENGTH(expr) for string-length checks. Import and open github.com/go-sql-driver/mysql or another compatible database/sql MySQL driver yourself — gxsql does not bundle one. The supported CI baseline is MySQL 8.4; MariaDB is not part of the supported matrix.
  • String-length expectations use the dialect’s SQL length function, not Go rune counting.
  • Other engines are possible only through a correct Dialect implementation; they are not part of the built-in dialect set.

Real-engine conformance

CI runs one shared conformance kit against PostgreSQL 16, SQLite 3.50.4, DuckDB 1.5.4, and MySQL 8.4 using the integration-only drivers github.com/duckdb/duckdb-go/v2 (v2.10504.0) and github.com/go-sql-driver/mysql (v1.10.0). The kit exercises identifier qualification, bound placeholders, null and text/byte scans, single and composite keys, ordering and diagnostic caps, empty targets, cancellation, database/scan errors, ContinueOnError, and transaction-compatible gxsql.DB handles. The fake driver remains for exact query-shape and deterministic failure-path tests.

Run the SQLite fixture locally from the integration module:

cd integration
go test -race -run '^TestSQLiteConformance$' ./...

Run PostgreSQL conformance by supplying an isolated database:

cd integration
GXSQL_POSTGRES_DSN='postgres://user:password@localhost:5432/gxsql?sslmode=disable' \
  go test -race -run '^TestPostgresConformance$' ./...

Run DuckDB conformance locally from the integration module:

cd integration
go test -race -run '^TestDuckDBConformance$' ./...

Core concepts

Concept Description
Suite An ordered set of expectations from gxsql.NewSuite(...). Results appear in the same declaration order.
Expectation One data-quality assertion over a table, built with RowCount, Column, Int, Float, String, or CustomCount.
TableRef Names the table under test: gxsql.Table("users") or gxsql.SchemaTable("public", "users"). Identifiers must match ^[A-Za-z_][A-Za-z0-9_]*$.
Dialect Renders identifiers, placeholders, and string-length expressions. Built-in: gxsql.Postgres(), gxsql.SQLite(), gxsql.DuckDB(), and gxsql.MySQL(). ValidateTable defaults to PostgreSQL when no dialect is supplied; pass gxsql.WithDialect(...) explicitly.
Report / Result A Report holds one Result per expectation. Use report.OK(), report.Failures(), report.Err(), and report.String() to gate and inspect outcomes.

Validation failures do not make ValidateTable return an error. The returned error means SQL execution or configuration failed (invalid identifiers, database errors, context cancellation, and similar). Use report.Err() to gate on data quality.

Expectation examples

suite := gxsql.NewSuite(
    // Table-level row count
    gxsql.RowCount().Between(100, 10_000),

    // Per-row numeric checks
    gxsql.Int("age").Between(0, 120),
    gxsql.Float("score").GreaterOrEqual(0),

    // String checks
    gxsql.String("email").NotEmpty(),
    gxsql.String("code").LenBetween(3, 10),

    // Generic column checks
    gxsql.Column("status").NotNull(),
    gxsql.Column("status").In("active", "pending", "closed"),
    gxsql.Column("email").Unique(),
    gxsql.Column("country").DistinctCount().GreaterOrEqual(1),

    // Numeric aggregates (vacuous pass when the column is all NULL)
    gxsql.Int("amount").AverageBetween(0, 1_000),
    gxsql.Int("amount").MinGreaterOrEqual(0),
    gxsql.Int("amount").MaxLessOrEqual(1_000_000),
)

Per-row checks set Total to the table row count and populate FailedCount, FailedPercent, SampleValues, and optionally FailedKeys on failure. Table-level checks (row count, distinct count, aggregates) append observed values to Result.Name (for example row count >= 1: got 42).

Failed rows and reports

By default, per-row failures include failed counts and capped sample values (DefaultSampleCap is 20). When neither WithKey nor SummaryOnly() is supplied, ValidateTable uses summary-only mode internally (no complete failed-row keys).

// Counts plus capped samples only
report, err := suite.ValidateTable(ctx, db, gxsql.Table("users"),
    gxsql.WithDialect(gxsql.Postgres()),
    gxsql.SummaryOnly(),
)

// Record failing row identities by key columns (capped by DefaultFailedKeysCap = 100)
report, err := suite.ValidateTable(ctx, db, gxsql.Table("users"),
    gxsql.WithDialect(gxsql.Postgres()),
    gxsql.WithKey("id"),
    gxsql.WithSampleCap(5),
    gxsql.WithFailedKeysCap(0), // unlimited keys when every failing row is needed
)

Inspect failures after gating:

if err := report.Err(); err != nil {
    var ve *gxsql.ValidationError
    if errors.As(err, &ve) {
        for _, res := range ve.Report.Failures() {
            fmt.Println(res.String())
        }
    }
}

Use gxsql.ContinueOnError() when you want database errors recorded on individual Result.Err values while later expectations still run. Inspect report.Err() and per-result errors — a nil top-level error is not success in that mode.

Bounded failure tolerance

Use WithMaxFailedCount when a known number of bad rows may remain visible without failing the run. Wrap one eligible per-row or uniqueness expectation with an inclusive non-negative maximum failed-row count. Equality passes. There is no percentage, pass-rate, Mostly, rounding, or compound policy.

suite := gxsql.NewSuite(
    gxsql.WithMaxFailedCount(2, gxsql.String("email").NotEmpty()),
)

report, err := suite.ValidateTable(ctx, db, gxsql.Table("users"),
    gxsql.WithDialect(gxsql.Postgres()),
    gxsql.WithKey("id"),
)
if err != nil {
    log.Fatalf("gxsql execution error: %v", err)
}
if err := report.Err(); err != nil {
    log.Fatalf("data quality check failed: %v", err)
}
for _, result := range report.Results {
    if result.Tolerated {
        // raw FailedCount, samples, and keys remain for remediation
        fmt.Println(result.String())
    }
}

In that example, two failed rows pass the inclusive bound (FailedCount <= 2) with Success: true and Tolerated: true. A third failed row fails the policy. Tolerance changes only the policy verdict. Raw Total, FailedCount, FailedPercent, samples, and failed keys stay intact under the normal cap settings. Empty evaluated populations pass without division by zero or NaN and are not tolerated. Scope remains the evaluated population for all raw counts.

report.OK() and report.Err() treat tolerated results as successful; report.Failures() omits them. They remain explicit in Result.Tolerated, Report.String(), and exported JSON. For remediation, walk Report.Results and inspect Result.Tolerated—do not rely on Failures() alone.

Only per-row and uniqueness expectations qualify. Wrapping a table-level, aggregate, distinct-count, row-count, or custom-count declaration fails preflight. Execution and configuration errors are never tolerated. Export stays privacy-safe by default: JSON exposes the tolerance flag, configured bound, and raw counts; samples, keys, query diagnostics, and arguments keep their existing opt-in and redaction rules. See the results, suite, reports, and export references for eligibility, display, and privacy-safe JSON fields.

Testing with gxsqltest

The gxsqltest package adapts suite validation to Go's testing package:

import (
    "context"
    "testing"

    "github.com/busyminds/gxsql"
    "github.com/busyminds/gxsql/gxsqltest"
)

func TestUsers(t *testing.T) {
    ctx := context.Background()
    // db and suite from setup...

    gxsqltest.Require(t, ctx, suite, db, gxsql.Table("users"),
        gxsql.WithDialect(gxsql.SQLite()),
    )
}
  • Check reports failures with t.Errorf, continues the test, and returns true when every expectation passes.
  • Require calls t.Fatalf on execution or validation failure and stops the test.

Operational notes

gxsql executes SQL against the database. Each per-row expectation issues at least two full-table COUNT(*) queries (total rows plus failing rows). Plan query cost on large tables and set context deadlines on every ValidateTable call.

Control Default Effect
WithSampleCap(n) 20 Caps SampleValues
WithFailedKeysCap(n) 100 Caps FailedKeys when WithKey is set; zero means unlimited
SummaryOnly() implicit without WithKey No failed-row keys loaded
WithKey(...) off Loads failing row keys (capped by default)

Key mode guidance: WithKey suits low failure rates or when you need row identities for remediation. On very large tables with widespread failures, prefer SummaryOnly() or pass WithFailedKeysCap(0) only when you accept unbounded key retention.

In / NotIn list size: each value becomes a bound placeholder. Lists in the low thousands are usually fine; beyond that, split into multiple expectations or use a lookup table join outside gxsql.

Database privileges: ValidateTable inherits the connection's permissions. Use a read-only role scoped to validation views in production.

Report output: Report.String() and gxsqltest.Check/Require may embed sample values in logs. Redact before shipping to observability backends when columns may contain PII or secrets.

Machine identity and export

Attach stable result IDs with gxsql.WithID(id, expectation) for CI/ETL joins. IDs are optional for ad-hoc runs: when omitted, Result.ID stays empty and export JSON omits the id field. Blank or duplicate IDs fail preflight before SQL. Every built-in expectation exposes a library-defined Kind on Result. Display Name text may change with observed values; ID and Kind stay stable across equivalent runs.

Export encoding-only JSON for CI and audits:

dto, err := gxsql.ExportReport(report,
    gxsql.IncludeSamples(),
    gxsql.IncludeFailedKeys(),
)
// dto.SchemaVersion == gxsql.ExportSchemaVersion ("gxsql.report.v1")

By default, ExportReport omits samples, failed keys, query text, and bound arguments. policy_verdict is pass or fail only when Result.Err == nil; any Err yields unevaluated (execution/config failure, not a data-quality verdict). execution_outcome is unchanged. Configured thresholds export in facts.configured_* keys; default display_name redacts bound literals.

Opt in with IncludeSamples, IncludeFailedKeys, IncludeCapturedDiagnostics (redacted table-free SQL), and IncludeCapturedArguments (also enables normalized capped args; requires CaptureQueryDiagnostics() at validate time). Redactor failures fail closed with no partial JSON. v1 is encode-only — no public decoder is promised.

See API reference for export field policy, value encodings, and privacy defaults.

Migration notes (pre-v1)

Change Action
Machine consumption used Result.Name only Wrap expectations with WithID and gate on Kind
Logs included sample values by default Use ExportReport privacy defaults; opt in to sensitive fields
Empty In() / NotIn() Still configuration errors before SQL; use ContinueOnError() to collect slots
Exact-zero failure gating only Wrap eligible per-row or uniqueness expectations with WithMaxFailedCount
Warning / info severity Not implemented

Documentation

Documentation

Overview

Package gxsql validates database table contents through database/sql without loading whole tables into application memory.

Build a suite from expectations, then validate a table with the dialect that matches the database connection:

suite := gxsql.NewSuite(
	gxsql.RowCount().GreaterOrEqual(1),
	gxsql.Int("age").Between(0, 120),
	gxsql.String("email").NotEmpty(),
)

report, err := suite.ValidateTable(ctx, db, gxsql.Table("users"),
	gxsql.WithDialect(gxsql.Postgres()),
)
if err != nil {
	// Configuration or execution error; no complete report is available.
}
if err := report.Err(); err != nil {
	// Validation completed, but one or more policies failed.
}

For scoped validation, use TrustedScope with WithScope. The scope predicate limits every expectation to matching rows and uses dialect-neutral ? placeholders; each value is bound separately through the arguments:

tenantID := "tenant-acme"
scope := gxsql.TrustedScope("tenant-acme", "tenant_id = ?", tenantID)
report, err := suite.ValidateTable(ctx, db, gxsql.Table("users"),
	gxsql.WithDialect(gxsql.Postgres()),
	gxsql.WithScope(scope),
)

Scope predicates are trusted Go-code input, not a sandbox for untrusted SQL. Callers must not pass user-authored predicate text. Report.ScopeID and the exported scope.id carry caller identity only; default errors and display output, and ExportReport, do not serialize the scope predicate text or bound arguments. Other report samples remain subject to their usual redaction requirements. In production, use a read-only database role and a context deadline for each validation.

Custom count checks use TrustedCountQuery and CustomCount. The SQL template is trusted Go-code input reviewed by the application, not a sandbox for untrusted text; callers must never insert user-authored SQL into templates. A template contains exactly one {{target}} and one {{scope}}, both outside SQL strings and comments. The library renders {{target}} from the validated TableRef and {{scope}} from WithScope (or TRUE when unscoped). Custom ? placeholders must follow {{scope}}; bound arguments are scope values first, then custom values. The query returns one row and one column. Signed integer driver values (int through int64) are accepted; textual numerics are not coerced. The count must be non-negative and uses RowDenominatorUnavailable: no total, percentage, samples, or failed keys. Default reports, errors, and ExportReport omit template SQL and arguments, including driver-error text. CaptureQueryDiagnostics is an opt-in export-only path subject to existing redactors.

Bounded failure tolerance uses WithMaxFailedCount around an eligible per-row or uniqueness expectation. max is an inclusive non-negative failed-row bound; equality passes. Tolerance changes only the policy verdict—raw Total, FailedCount, FailedPercent, samples, and failed keys stay under existing caps. Gate with Report.Err; tolerated results count as successful and are omitted from Report.Failures, so inspect Report.Results and Result.Tolerated for remediation. Table-level, aggregate, distinct-count, row-count, and custom-count wrappers fail preflight. Execution and configuration errors are never tolerated:

suite := gxsql.NewSuite(
	gxsql.WithMaxFailedCount(2, gxsql.String("email").NotEmpty()),
)
report, err := suite.ValidateTable(ctx, db, gxsql.Table("users"),
	gxsql.WithDialect(gxsql.Postgres()),
)
if err != nil {
	// Configuration or execution error; no complete report is available.
}
if err := report.Err(); err != nil {
	// Above-bound or other policy failures.
}

Failed policies are collected in declaration order. Use WithKey to retain failed-row identities, WithID to give expectations stable machine identity, and ExportReport for the versioned JSON DTO. For Go tests, use the gxsqltest subpackage's Check or Require helper.

Example
package main

import (
	"context"
	"fmt"

	"github.com/busyminds/gxsql"
	"github.com/busyminds/gxsql/gxsqltest"
)

type recorderT struct {
	errors int
	fatals int
}

func (r *recorderT) Helper() {}
func (r *recorderT) Errorf(string, ...any) {
	r.errors++
}
func (r *recorderT) Fatalf(string, ...any) {
	r.fatals++
}

func main() {
	checkT := &recorderT{}
	fmt.Println(gxsqltest.Check(
		checkT,
		context.Background(),
		gxsql.NewSuite(gxsql.Column("status").In()),
		nil,
		gxsql.Table("users"),
		gxsql.WithDialect(gxsql.Postgres()),
	))
	fmt.Println(checkT.errors, checkT.fatals)

	requireT := &recorderT{}
	gxsqltest.Require(
		requireT,
		context.Background(),
		gxsql.NewSuite(gxsql.Column("status").In()),
		nil,
		gxsql.Table("users"),
		gxsql.WithDialect(gxsql.Postgres()),
	)
	fmt.Println(requireT.errors, requireT.fatals)

}
Output:
false
1 0
0 1

Index

Examples

Constants

View Source
const DefaultFailedKeysCap = 100

DefaultFailedKeysCap is the default maximum failed-row keys retained per Result. Use WithFailedKeysCap(0) for unlimited retention when every failing key is required for remediation.

View Source
const DefaultSampleCap = 20

DefaultSampleCap is the default maximum offending sample values retained per failing per-row result. Override per suite with Suite.WithSampleCap or per run with WithSampleCap. A cap of zero disables sample collection.

View Source
const ExportSchemaVersion = "gxsql.report.v1"

ExportSchemaVersion is the top-level schema version for ExportedReport JSON.

View Source
const MaxExportedArgumentCount = 256

MaxExportedArgumentCount is the maximum number of exported bound arguments.

View Source
const MaxExportedErrorMessageRunes = 512

MaxExportedErrorMessageRunes is the maximum rune length of export-safe errors.

View Source
const MaxExportedQueryTextRunes = 4096

MaxExportedQueryTextRunes is the maximum rune length of exported SQL text.

Variables

View Source
var (
	ErrCategoryInvalidConfig = &categoryMarker{CategoryInvalidConfig}
	ErrCategoryUnsupported   = &categoryMarker{CategoryUnsupported}
	ErrCategoryRendering     = &categoryMarker{CategoryRendering}
	ErrCategoryDatabase      = &categoryMarker{CategoryDatabase}
	ErrCategoryScan          = &categoryMarker{CategoryScan}
	ErrCategoryContext       = &categoryMarker{CategoryContext}
	ErrCategoryObserver      = &categoryMarker{CategoryObserver}
)

ErrCategory* values are category markers for errors.Is against a typed ErrorCategory. For example, errors.Is(err, ErrCategoryInvalidConfig) reports whether err is or wraps a CategorizedError with that category.

Functions

This section is empty.

Types

type CategorizedError

type CategorizedError struct {
	// Category is the machine-facing failure class.
	Category ErrorCategory
	// Err is the underlying cause exposed through Unwrap.
	Err error
}

CategorizedError attaches a stable category to an underlying failure.

func (*CategorizedError) Error

func (e *CategorizedError) Error() string

Error returns a diagnostic message including the category and wrapped error.

func (*CategorizedError) Is

func (e *CategorizedError) Is(target error) bool

Is reports whether target matches the category marker or unwrap chain. Category markers match by ErrorCategory; other targets delegate to Err.

func (*CategorizedError) Unwrap

func (e *CategorizedError) Unwrap() error

Unwrap returns the underlying error for errors.Is and errors.As traversal.

type ColumnBuilder

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

ColumnBuilder is the entry point for column checks that apply to any SQL type. Construct one with Column. Most methods return per-row Expectation values evaluated with RowDenominatorAvailable; ColumnBuilder.DistinctCount returns a table-level expectation evaluated with RowDenominatorUnavailable. Invalid column identifiers fail suite preflight before SQL runs.

func Column

func Column(name string) ColumnBuilder

Column returns a builder for nullability, membership, uniqueness, and distinct-count checks on name. name must satisfy Dialect identifier rules.

func (ColumnBuilder) DistinctCount

func (c ColumnBuilder) DistinctCount() DistinctCountBuilder

DistinctCount starts a distinct-count expectation builder for the column.

func (ColumnBuilder) In

func (c ColumnBuilder) In(vals ...any) Expectation

In asserts the column value is one of vals. SQL NULL in the column fails. Each entry in vals must be non-nil; an empty list or a nil entry returns a configuration error. Very large lists expand the query (many placeholders); keep lists in the low thousands or use a lookup-table join outside gxsql.

func (ColumnBuilder) IsNull

func (c ColumnBuilder) IsNull() Expectation

IsNull returns a per-row expectation that the column is SQL NULL. Non-NULL values fail. An empty table passes vacuously. Results use KindIsNull.

func (ColumnBuilder) NotIn

func (c ColumnBuilder) NotIn(vals ...any) Expectation

NotIn asserts the column value is not one of vals. SQL NULL in the column fails. Each entry in vals must be non-nil; an empty list or a nil entry returns a configuration error. Very large lists expand the query (many placeholders); keep lists in the low thousands or split checks.

func (ColumnBuilder) NotNull

func (c ColumnBuilder) NotNull() Expectation

NotNull returns a per-row expectation that the column is not SQL NULL. NULL values fail. An empty table passes vacuously. Results use KindNotNull.

func (ColumnBuilder) Unique

func (c ColumnBuilder) Unique() Expectation

Unique returns a per-row expectation that each non-NULL value appears at most once. NULL values are ignored and do not participate in duplicate detection. Every row in a duplicate group fails. An empty table passes vacuously. Failing rows may include sample values (capped by DefaultSampleCap or WithSampleCap) and failed keys when WithKey is set. Results use KindUnique.

type CountQuery added in v0.2.0

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

CountQuery is an immutable trusted SQL count template with bound arguments. Construct it with TrustedCountQuery. Validation is deferred until the query is used with CustomCount and evaluated by Suite.ValidateTable.

func TrustedCountQuery added in v0.2.0

func TrustedCountQuery(template string, args ...any) CountQuery

TrustedCountQuery returns an immutable CountQuery from trusted Go-code SQL template input and bound argument values. The template must contain exactly one {{target}} and one {{scope}} marker; validation is deferred until Suite.ValidateTable.

Example
package main

import (
	"fmt"

	"github.com/busyminds/gxsql"
)

func main() {
	query := gxsql.TrustedCountQuery(
		"SELECT COUNT(*) FROM {{target}} WHERE {{scope}} AND status = ?",
		"pending",
	)
	exp := gxsql.CustomCount("violating orders", query)
	fmt.Println(exp.Name())

}
Output:
violating orders

type DB

type DB interface {
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}

DB is the narrow database interface Suite.ValidateTable executes against. Implementations must honor context cancellation on context.Context.

type Dialect

type Dialect interface {
	QuoteIdent(name string) (string, error)
	Placeholder(n int) string
	StringLength(expr string) string
}

Dialect renders dialect-specific SQL fragments for identifiers, bound parameters, and string-length expressions. Pass a concrete implementation to WithDialect. [QuoteIdent] must reject empty or invalid names. A nil dialect is a run-level configuration error before preflight or SQL, not a rendering-time error.

func DuckDB

func DuckDB() Dialect

DuckDB returns the DuckDB Dialect. Identifiers are double-quoted after validation; placeholders are positional $n; string length uses LENGTH. Pair with WithDialect when validating DuckDB tables.

func MySQL

func MySQL() Dialect

MySQL returns the MySQL Dialect. Identifiers are backtick-quoted after validation; placeholders are ?; string length uses CHAR_LENGTH. Pair with WithDialect when validating MySQL tables.

func Postgres

func Postgres() Dialect

Postgres returns the PostgreSQL Dialect. Identifiers are double-quoted after validation; placeholders are positional $n; string length uses CHAR_LENGTH. Pair with WithDialect when validating PostgreSQL tables.

func SQLite

func SQLite() Dialect

SQLite returns the SQLite Dialect. Identifiers are double-quoted after validation; placeholders are ?; string length uses LENGTH. Pair with WithDialect when validating SQLite tables.

type DistinctCountBuilder

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

DistinctCountBuilder builds table-level COUNT(DISTINCT column) expectations. Start from ColumnBuilder.DistinctCount. SQL NULL values are excluded from the count; an empty table or all-NULL column yields 0. Results set RowDenominatorUnavailable, append the observed count to Name, and store it in ResultFacts.ObservedCount. Invalid column identifiers fail suite preflight before SQL runs.

func (DistinctCountBuilder) Between

func (b DistinctCountBuilder) Between(lo, hi int) Expectation

Between asserts lo <= distinct count <= hi (inclusive).

func (DistinctCountBuilder) Equal

func (b DistinctCountBuilder) Equal(want int) Expectation

Equal asserts the column has exactly want distinct non-null values.

func (DistinctCountBuilder) GreaterOrEqual

func (b DistinctCountBuilder) GreaterOrEqual(bound int) Expectation

GreaterOrEqual asserts distinct count >= bound.

func (DistinctCountBuilder) GreaterThan

func (b DistinctCountBuilder) GreaterThan(bound int) Expectation

GreaterThan asserts distinct count > bound.

func (DistinctCountBuilder) LessOrEqual

func (b DistinctCountBuilder) LessOrEqual(bound int) Expectation

LessOrEqual asserts distinct count <= bound.

func (DistinctCountBuilder) LessThan

func (b DistinctCountBuilder) LessThan(bound int) Expectation

LessThan asserts distinct count < bound.

type ErrorCategory

type ErrorCategory string

ErrorCategory is a closed vocabulary of machine-facing failure classes.

const (
	// CategoryInvalidConfig marks expectation or run configuration rejected
	// before or during validation setup.
	CategoryInvalidConfig ErrorCategory = "invalid_config"
	// CategoryUnsupported marks a requested capability the library does not provide.
	CategoryUnsupported ErrorCategory = "unsupported"
	// CategoryRendering marks SQL identifier or fragment rendering failures.
	CategoryRendering ErrorCategory = "rendering"
	// CategoryDatabase marks database/sql execution failures unrelated to scanning.
	CategoryDatabase ErrorCategory = "database"
	// CategoryScan marks row iteration or column scan failures.
	CategoryScan ErrorCategory = "scan"
	// CategoryContext marks context cancellation or deadline exceeded.
	CategoryContext ErrorCategory = "context"
	// CategoryObserver marks export redaction or normalization failures.
	CategoryObserver ErrorCategory = "observer"
)

type ExecutionOutcome

type ExecutionOutcome string

ExecutionOutcome classifies how validation ran, distinct from policy verdict.

const (
	// ExecutionOutcomeOK means the expectation ran and its policy passed.
	ExecutionOutcomeOK ExecutionOutcome = "ok"
	// ExecutionOutcomePolicyFailure means the expectation ran and its policy failed.
	ExecutionOutcomePolicyFailure ExecutionOutcome = "policy_failure"
	// ExecutionOutcomeExecutionFailure means execution failed after evaluation began.
	ExecutionOutcomeExecutionFailure ExecutionOutcome = "execution_failure"
	// ExecutionOutcomeConfigFailure means preflight configuration prevented execution.
	ExecutionOutcomeConfigFailure ExecutionOutcome = "config_failure"
)

type Expectation

type Expectation interface {
	Name() string
	// contains filtered or unexported methods
}

Expectation is the sealed unit of SQL validation over a table. Construct expectations with the column, row-count, and aggregate builders; its unexported methods prevent implementations outside package gxsql. The [Name] method supplies display text, while Suite.ValidateTable reports a library-defined Result.Kind. Attach a stable result ID with WithID.

func CustomCount added in v0.2.0

func CustomCount(name string, query CountQuery) Expectation

CustomCount returns an expectation that executes query and treats the returned count as the expectation-specific failure count. Results use KindCustom with RowDenominatorUnavailable and expose Result.FailedCount without a row denominator, samples, or failed keys. name must be non-blank; validation is deferred until Suite.ValidateTable.

Example
package main

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"fmt"
	"io"
	"sync"

	"github.com/busyminds/gxsql"
)

const exampleDriverName = "gxsql_example_test"

var registerExampleDriverOnce sync.Once

func openExampleDB(scenario string) (*sql.DB, error) {
	registerExampleDriverOnce.Do(func() {
		sql.Register(exampleDriverName, exampleDriver{})
	})
	return sql.Open(exampleDriverName, scenario)
}

type exampleDriver struct{}

func (exampleDriver) Open(name string) (driver.Conn, error) {
	return exampleConn{scenario: name}, nil
}

type exampleConn struct {
	scenario string
}

func (c exampleConn) Prepare(string) (driver.Stmt, error) { return nil, driver.ErrSkip }
func (c exampleConn) Close() error                        { return nil }
func (c exampleConn) Begin() (driver.Tx, error)           { return nil, driver.ErrSkip }

func (c exampleConn) QueryContext(_ context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
	switch c.scenario {
	case "pass":
		return c.passQuery(query, args)
	case "keys":
		return c.keysQuery(query, args)
	case "customcount":
		return c.customCountQuery(query, args)
	case "error":
		return nil, fmt.Errorf("example driver: query failed")
	default:
		return nil, fmt.Errorf("example driver: unknown scenario %q", c.scenario)
	}
}

func (exampleConn) passQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "users"`:
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	case `SELECT COUNT(*) FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ?`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(0)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected query %q", query)
	}
}

func (exampleConn) customCountQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "orders" WHERE TRUE AND status = ?`:
		if err := expectArgs(args, "pending"); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected custom count query")
	}
}

func (exampleConn) keysQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "users"`:
		return newExampleRows([]string{"count"}, []driver.Value{int64(3)}), nil
	case `SELECT COUNT(*) FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ?`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	case `SELECT "id" FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ? ORDER BY "id"`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"id"}, []driver.Value{int64(2)}, []driver.Value{int64(3)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected query %q", query)
	}
}

func expectArgs(args []driver.NamedValue, want ...any) error {
	if len(args) != len(want) {
		return fmt.Errorf("example driver: arg count = %d, want %d", len(args), len(want))
	}
	for i := range want {
		if args[i].Value != want[i] {
			return fmt.Errorf("example driver: arg %d = %v, want %v", i, args[i].Value, want[i])
		}
	}
	return nil
}

type exampleRows struct {
	cols []string
	rows [][]driver.Value
	idx  int
}

func newExampleRows(cols []string, rows ...[]driver.Value) driver.Rows {
	return &exampleRows{cols: cols, rows: rows}
}

func (r *exampleRows) Columns() []string { return r.cols }
func (r *exampleRows) Close() error      { return nil }

func (r *exampleRows) Next(dest []driver.Value) error {
	if r.idx >= len(r.rows) {
		return io.EOF
	}
	copy(dest, r.rows[r.idx])
	r.idx++
	return nil
}

func main() {
	db, err := openExampleDB("customcount")
	if err != nil {
		panic(err)
	}
	defer db.Close()

	query := gxsql.TrustedCountQuery(
		"SELECT COUNT(*) FROM {{target}} WHERE {{scope}} AND status = ?",
		"pending",
	)
	suite := gxsql.NewSuite(
		gxsql.CustomCount("violating orders", query),
	)
	report, err := suite.ValidateTable(
		context.Background(), db, gxsql.Table("orders"),
		gxsql.WithDialect(gxsql.SQLite()),
	)
	if err != nil {
		panic(err)
	}

	fmt.Println(report.OK())
	fmt.Println(report.Results[0].FailedCount)

}
Output:
false
2

func WithID

func WithID(id string, exp Expectation) Expectation

WithID decorates an expectation with a caller-supplied stable Result.ID. Blank or whitespace-only IDs are rejected during suite preflight. Duplicate IDs across one suite are also rejected before SQL runs. Wrapped behavior, Result.Kind, and display Result.Name are preserved; IDs are never derived from Name.

func WithMaxFailedCount added in v0.2.0

func WithMaxFailedCount(max int, exp Expectation) Expectation

WithMaxFailedCount wraps an eligible expectation with an inclusive maximum failed-row allowance. Only per-row and uniqueness expectations qualify; table-level, aggregate, distinct-count, row-count, and custom-count declarations are rejected at ValidateTable preflight. Negative max values are also rejected at preflight. The wrapper is immutable and may nest with WithID in either order.

Tolerance changes only the policy verdict. Raw Total, FailedCount, FailedPercent, SampleValues, and FailedKeys are preserved under their existing caps. A nonzero raw failure count at or below max yields Success true and Tolerated true; raw-zero and empty populations pass without being tolerated; above-bound and error results cannot be tolerated.

Example
package main

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"fmt"
	"io"
	"sync"

	"github.com/busyminds/gxsql"
)

const exampleDriverName = "gxsql_example_test"

var registerExampleDriverOnce sync.Once

func openExampleDB(scenario string) (*sql.DB, error) {
	registerExampleDriverOnce.Do(func() {
		sql.Register(exampleDriverName, exampleDriver{})
	})
	return sql.Open(exampleDriverName, scenario)
}

type exampleDriver struct{}

func (exampleDriver) Open(name string) (driver.Conn, error) {
	return exampleConn{scenario: name}, nil
}

type exampleConn struct {
	scenario string
}

func (c exampleConn) Prepare(string) (driver.Stmt, error) { return nil, driver.ErrSkip }
func (c exampleConn) Close() error                        { return nil }
func (c exampleConn) Begin() (driver.Tx, error)           { return nil, driver.ErrSkip }

func (c exampleConn) QueryContext(_ context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
	switch c.scenario {
	case "pass":
		return c.passQuery(query, args)
	case "keys":
		return c.keysQuery(query, args)
	case "customcount":
		return c.customCountQuery(query, args)
	case "error":
		return nil, fmt.Errorf("example driver: query failed")
	default:
		return nil, fmt.Errorf("example driver: unknown scenario %q", c.scenario)
	}
}

func (exampleConn) passQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "users"`:
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	case `SELECT COUNT(*) FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ?`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(0)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected query %q", query)
	}
}

func (exampleConn) customCountQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "orders" WHERE TRUE AND status = ?`:
		if err := expectArgs(args, "pending"); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected custom count query")
	}
}

func (exampleConn) keysQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "users"`:
		return newExampleRows([]string{"count"}, []driver.Value{int64(3)}), nil
	case `SELECT COUNT(*) FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ?`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	case `SELECT "id" FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ? ORDER BY "id"`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"id"}, []driver.Value{int64(2)}, []driver.Value{int64(3)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected query %q", query)
	}
}

func expectArgs(args []driver.NamedValue, want ...any) error {
	if len(args) != len(want) {
		return fmt.Errorf("example driver: arg count = %d, want %d", len(args), len(want))
	}
	for i := range want {
		if args[i].Value != want[i] {
			return fmt.Errorf("example driver: arg %d = %v, want %v", i, args[i].Value, want[i])
		}
	}
	return nil
}

type exampleRows struct {
	cols []string
	rows [][]driver.Value
	idx  int
}

func newExampleRows(cols []string, rows ...[]driver.Value) driver.Rows {
	return &exampleRows{cols: cols, rows: rows}
}

func (r *exampleRows) Columns() []string { return r.cols }
func (r *exampleRows) Close() error      { return nil }

func (r *exampleRows) Next(dest []driver.Value) error {
	if r.idx >= len(r.rows) {
		return io.EOF
	}
	copy(dest, r.rows[r.idx])
	r.idx++
	return nil
}

func main() {
	db, err := openExampleDB("keys")
	if err != nil {
		panic(err)
	}
	defer db.Close()

	report, err := gxsql.NewSuite(
		gxsql.WithMaxFailedCount(2, gxsql.Int("age").Between(0, 120)),
	).WithSampleCap(0).ValidateTable(
		context.Background(), db, gxsql.Table("users"),
		gxsql.WithDialect(gxsql.SQLite()),
		gxsql.WithKey("id"),
		gxsql.WithFailedKeysCap(0),
	)
	if err != nil {
		panic(err)
	}

	fmt.Println(report.OK())
	fmt.Println(report.Results[0].Tolerated)
	fmt.Println(*report.Results[0].Facts.ConfiguredMaxFailedCount)
	fmt.Println(report.String())

}
Output:
true
true
2
gxsql report: 1/1 expectations passed
  ✓ age between [0,120]  tolerated  2/3 failed (66.7%)  e.g. [] @ [[2] [3]]

type ExpectationKind

type ExpectationKind string

ExpectationKind is a stable machine identifier for a built-in expectation type. Custom expectations use KindCustom unless a decorator assigns another kind.

const (
	// KindRowCountEqual marks an expectation from RowCount().Equal.
	KindRowCountEqual ExpectationKind = "row_count_equal"
	// KindRowCountBetween marks an expectation from RowCount().Between.
	KindRowCountBetween ExpectationKind = "row_count_between"
	// KindRowCountGreaterThan marks an expectation from RowCount().GreaterThan.
	KindRowCountGreaterThan ExpectationKind = "row_count_greater_than"
	// KindRowCountGreaterEqual marks an expectation from RowCount().GreaterOrEqual.
	KindRowCountGreaterEqual ExpectationKind = "row_count_greater_or_equal"
	// KindRowCountLessThan marks an expectation from RowCount().LessThan.
	KindRowCountLessThan ExpectationKind = "row_count_less_than"
	// KindRowCountLessEqual marks an expectation from RowCount().LessOrEqual.
	KindRowCountLessEqual ExpectationKind = "row_count_less_or_equal"

	// KindIsNull marks an expectation from Column().IsNull.
	KindIsNull ExpectationKind = "is_null"
	// KindNotNull marks an expectation from Column().NotNull.
	KindNotNull ExpectationKind = "not_null"
	// KindIn marks an expectation from Column().In.
	KindIn ExpectationKind = "in"
	// KindNotIn marks an expectation from Column().NotIn.
	KindNotIn ExpectationKind = "not_in"
	// KindUnique marks an expectation from Column().Unique.
	KindUnique ExpectationKind = "unique"
	// KindBetween marks an expectation from NumberColumn.Between.
	KindBetween ExpectationKind = "between"
	// KindGreaterThan marks an expectation from NumberColumn.GreaterThan.
	KindGreaterThan ExpectationKind = "greater_than"
	// KindLessThan marks an expectation from NumberColumn.LessThan.
	KindLessThan ExpectationKind = "less_than"
	// KindGreaterOrEqual marks an expectation from NumberColumn.GreaterOrEqual.
	KindGreaterOrEqual ExpectationKind = "greater_or_equal"
	// KindLessOrEqual marks an expectation from NumberColumn.LessOrEqual.
	KindLessOrEqual ExpectationKind = "less_or_equal"
	// KindNotEmpty marks an expectation from StringColumn.NotEmpty.
	KindNotEmpty ExpectationKind = "not_empty"
	// KindEmpty marks an expectation from StringColumn.Empty.
	KindEmpty ExpectationKind = "empty"
	// KindLenEqual marks an expectation from StringColumn.LenEqual.
	KindLenEqual ExpectationKind = "len_equal"
	// KindLenBetween marks an expectation from StringColumn.LenBetween.
	KindLenBetween ExpectationKind = "len_between"

	// KindDistinctCountEqual marks an expectation from DistinctCountBuilder.Equal.
	KindDistinctCountEqual ExpectationKind = "distinct_count_equal"
	// KindDistinctCountBetween marks an expectation from DistinctCountBuilder.Between.
	KindDistinctCountBetween ExpectationKind = "distinct_count_between"
	// KindDistinctCountGreaterThan marks an expectation from DistinctCountBuilder.GreaterThan.
	KindDistinctCountGreaterThan ExpectationKind = "distinct_count_greater_than"
	// KindDistinctCountGreaterEqual marks an expectation from DistinctCountBuilder.GreaterOrEqual.
	KindDistinctCountGreaterEqual ExpectationKind = "distinct_count_greater_or_equal"
	// KindDistinctCountLessThan marks an expectation from DistinctCountBuilder.LessThan.
	KindDistinctCountLessThan ExpectationKind = "distinct_count_less_than"
	// KindDistinctCountLessEqual marks an expectation from DistinctCountBuilder.LessOrEqual.
	KindDistinctCountLessEqual ExpectationKind = "distinct_count_less_or_equal"

	// KindAverageBetween marks an expectation from NumberColumn.AverageBetween.
	KindAverageBetween ExpectationKind = "average_between"
	// KindMinGreaterOrEqual marks an expectation from NumberColumn.MinGreaterOrEqual.
	KindMinGreaterOrEqual ExpectationKind = "min_greater_or_equal"
	// KindMaxLessOrEqual marks an expectation from NumberColumn.MaxLessOrEqual.
	KindMaxLessOrEqual ExpectationKind = "max_less_or_equal"

	// KindCustom is the explicit kind for expectations without built-in metadata.
	KindCustom ExpectationKind = "custom"
)

type ExportOption

type ExportOption func(*exportConfig)

ExportOption configures ExportReport.

func IncludeCapturedArguments

func IncludeCapturedArguments() ExportOption

IncludeCapturedArguments exports normalized bound arguments alongside captured query text. Requires CaptureQueryDiagnostics at validation time.

func IncludeCapturedDiagnostics

func IncludeCapturedDiagnostics() ExportOption

IncludeCapturedDiagnostics exports captured SQL text from results that were validated with CaptureQueryDiagnostics. Omitted by default.

func IncludeFailedKeys

func IncludeFailedKeys() ExportOption

IncludeFailedKeys exports normalized FailedKeys. Omitted by default.

func IncludeSamples

func IncludeSamples() ExportOption

IncludeSamples exports normalized SampleValues. Omitted by default.

func WithArgsRedactor

func WithArgsRedactor(fn Redactor) ExportOption

WithArgsRedactor applies fn to each captured argument before export.

func WithKeyRedactor

func WithKeyRedactor(fn Redactor) ExportOption

WithKeyRedactor applies fn to each failed key before export.

func WithQueryRedactor

func WithQueryRedactor(fn Redactor) ExportOption

WithQueryRedactor applies fn to captured query text after identifiers are redacted and query text may be truncated to MaxExportedQueryTextRunes; fn runs after that initial truncation and its output is truncated again. fn must return a string; errors and panics fail export closed.

func WithSampleRedactor

func WithSampleRedactor(fn Redactor) ExportOption

WithSampleRedactor applies fn to each sample value before export.

type ExportedCaps

type ExportedCaps struct {
	// SamplesReturned is the number of exported samples.
	SamplesReturned int `json:"samples_returned,omitempty"`
	// SamplesTruncated is true when more samples existed than were retained.
	SamplesTruncated bool `json:"samples_truncated,omitempty"`
	// KeysReturned is the number of exported failed keys.
	KeysReturned int `json:"keys_returned,omitempty"`
	// KeysTruncated is true when more failing keys existed than were retained.
	KeysTruncated bool `json:"keys_truncated,omitempty"`
}

ExportedCaps reports diagnostic truncation metadata.

type ExportedCounts

type ExportedCounts struct {
	// Total is the evaluated row population; omitted when row_denominator is unavailable.
	Total *int `json:"total,omitempty"`
	// Failed is the number of failing rows when row_denominator is available.
	Failed *int `json:"failed,omitempty"`
	// FailedPercent is the percentage of failing rows; omitted when unavailable.
	FailedPercent *float64 `json:"failed_percent,omitempty"`
}

ExportedCounts holds row population metrics.

type ExportedDiagnostics

type ExportedDiagnostics struct {
	// Query is redacted SQL text when IncludeCapturedDiagnostics is enabled.
	Query string `json:"query,omitempty"`
	// Args holds normalized bound arguments when IncludeCapturedArguments is enabled.
	Args []NormalizedValue `json:"args,omitempty"`
	// QueryTruncated is true when query text exceeded MaxExportedQueryTextRunes.
	QueryTruncated bool `json:"query_truncated,omitempty"`
	// ArgsTruncated is true when argument count exceeded MaxExportedArgumentCount.
	ArgsTruncated bool `json:"args_truncated,omitempty"`
}

ExportedDiagnostics holds optional redacted SQL diagnostics.

type ExportedError

type ExportedError struct {
	// Category is the machine-facing failure class.
	Category ErrorCategory `json:"category"`
	// Message is diagnostic detail safe for export.
	Message string `json:"message"`
}

ExportedError is a categorized export-safe error representation.

type ExportedFacts

type ExportedFacts struct {
	// ObservedCount is a table-level integer observation when set.
	ObservedCount *int `json:"observed_count,omitempty"`
	// ObservedFloat is a normalized floating-point observation when set.
	ObservedFloat *NormalizedValue `json:"observed_float,omitempty"`
	// ConfiguredCount is the exact-count threshold for equal-style expectations.
	ConfiguredCount *int `json:"configured_count,omitempty"`
	// ConfiguredCountLower and ConfiguredCountUpper are inclusive integer bounds.
	ConfiguredCountLower *int `json:"configured_count_lower,omitempty"`
	ConfiguredCountUpper *int `json:"configured_count_upper,omitempty"`
	// ConfiguredFloatLower, ConfiguredFloatUpper, and ConfiguredFloatBound are
	// thresholds for floating-point aggregate expectations.
	ConfiguredFloatLower *NormalizedValue `json:"configured_float_lower,omitempty"`
	ConfiguredFloatUpper *NormalizedValue `json:"configured_float_upper,omitempty"`
	ConfiguredFloatBound *NormalizedValue `json:"configured_float_bound,omitempty"`
	// ConfiguredBound, ConfiguredBoundLower, and ConfiguredBoundUpper hold
	// per-row comparison thresholds with driver-bound types.
	ConfiguredBound      *NormalizedValue `json:"configured_bound,omitempty"`
	ConfiguredBoundLower *NormalizedValue `json:"configured_bound_lower,omitempty"`
	ConfiguredBoundUpper *NormalizedValue `json:"configured_bound_upper,omitempty"`
	// ConfiguredMaxFailedCount is the inclusive WithMaxFailedCount bound when
	// that decorator was applied.
	ConfiguredMaxFailedCount *int `json:"configured_max_failed_count,omitempty"`
}

ExportedFacts holds structured observations and configured thresholds.

type ExportedReport

type ExportedReport struct {
	// SchemaVersion identifies the export contract. Always ExportSchemaVersion.
	SchemaVersion string `json:"schema_version"`
	// Target names the validated table when Report.Target is set; omitted when unavailable.
	Target *ExportedTarget `json:"target,omitempty"`
	// Scope names the validation scope when available; omitted when unavailable.
	Scope *ExportedScope `json:"scope,omitempty"`
	// Results preserves declaration order from Report.Results.
	Results []ExportedResult `json:"results"`
}

ExportedReport is the versioned JSON DTO produced by ExportReport.

func ExportReport

func ExportReport(report Report, opts ...ExportOption) (ExportedReport, error)

ExportReport converts report into a versioned JSON DTO. On error, no partial DTO is returned. Query text, bound arguments, samples, and failed keys are omitted unless explicitly enabled via ExportOption.

type ExportedResult

type ExportedResult struct {
	// ID is the caller-supplied stable result identifier; omitted when empty.
	ID string `json:"id,omitempty"`
	// Kind is the library-defined expectation kind.
	Kind ExpectationKind `json:"kind"`
	// DisplayName is the human-oriented result name with configured bounds redacted.
	DisplayName string `json:"display_name"`
	// Column is the validated column when applicable; omitted when empty.
	Column string `json:"column,omitempty"`
	// PolicyVerdict is pass, fail, or unevaluated when no policy verdict was produced.
	PolicyVerdict PolicyVerdict `json:"policy_verdict"`
	// ExecutionOutcome distinguishes policy failure from execution/config failure.
	ExecutionOutcome ExecutionOutcome `json:"execution_outcome"`
	// Tolerated is true when a nonzero raw failure count passed within
	// WithMaxFailedCount. Omitted unless true.
	Tolerated bool `json:"tolerated,omitempty"`
	// RowDenominator reports whether total and failed_percent are meaningful.
	RowDenominator RowDenominator `json:"row_denominator"`
	// Counts holds row counts when applicable.
	Counts *ExportedCounts `json:"counts,omitempty"`
	// Facts holds machine-readable observations separate from display text.
	Facts *ExportedFacts `json:"facts,omitempty"`
	// Caps reports diagnostic truncation when samples or keys are exported.
	Caps *ExportedCaps `json:"caps,omitempty"`
	// Samples holds normalized failing sample values; omitted unless explicitly included.
	Samples []NormalizedValue `json:"samples,omitempty"`
	// FailedKeys holds normalized failing row keys; omitted unless explicitly included.
	FailedKeys []NormalizedValue `json:"failed_keys,omitempty"`
	// Diagnostics holds redacted query diagnostics; omitted unless explicitly included.
	Diagnostics *ExportedDiagnostics `json:"diagnostics,omitempty"`
	// Errors holds categorized failures in stable order; omitted when empty.
	Errors []ExportedError `json:"errors,omitempty"`
}

ExportedResult is one exported expectation outcome.

type ExportedScope

type ExportedScope struct {
	// ID is a stable scope identifier when scope is available.
	ID string `json:"id,omitempty"`
}

ExportedScope identifies a validation scope by its stable ID. Predicate and bound arguments are not included in exports.

type ExportedTarget

type ExportedTarget struct {
	// Schema is the optional schema qualifier; omitted when empty.
	Schema string `json:"schema,omitempty"`
	// Table is the table name.
	Table string `json:"table"`
}

ExportedTarget identifies the table validated by ValidateTable.

type NormalizedValue

type NormalizedValue struct {
	// Kind identifies the encoding. One of: null, bool, string, json_integer,
	// json_number, integer_string, decimal_string, bytes_base64, time_rfc3339,
	// composite, non_finite, unsupported.
	Kind string `json:"kind"`
	// Value holds the encoded payload. Omitted when Kind is null.
	Value any `json:"value,omitempty"`
	// Exact reports lossless encoding. Omitted when false.
	Exact bool `json:"exact,omitempty"`
}

NormalizedValue is a tagged, JSON-safe encoding of a SQL-returned scalar, bound argument, or diagnostic value. Integer magnitudes outside the exact JSON integer range use integer_string. Finite integral float64 values within the exact JSON integer range encode as json_integer with Exact true; non-integral floats use json_number, preserving signed zero as -0.0; other lossy encodings such as invalid UTF-8 strings (replaced with U+FFFD) and unsupported types (structural type name only) set Exact false.

type NumberColumn

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

NumberColumn is the entry point for ordered numeric comparisons and aggregates on one column. Construct one with Int or Float.

func Float

func Float(name string) NumberColumn

Float returns a builder for floating-point column checks and table-level aggregates on name. Aggregate comparisons use float64 thresholds.

func Int

func Int(name string) NumberColumn

Int returns a builder for integer or general numeric column checks and table-level aggregates on name.

func (NumberColumn) AverageBetween

func (c NumberColumn) AverageBetween(lo, hi float64) Expectation

AverageBetween returns a table-level expectation that AVG(column) lies in [lo, hi] (inclusive). Build it from a numeric column via Int or Float. SQL NULL values are excluded from the aggregate. When every value is NULL the check passes vacuously. Results use KindAverageBetween, set RowDenominatorUnavailable, and append the observed average to Name on evaluation. Invalid column identifiers fail suite preflight before SQL runs.

func (NumberColumn) Between

func (c NumberColumn) Between(lo, hi any) Expectation

Between returns a per-row expectation that lo <= value <= hi (inclusive). SQL NULL fails. An empty table passes vacuously. Results use KindBetween.

func (NumberColumn) GreaterOrEqual

func (c NumberColumn) GreaterOrEqual(bound any) Expectation

GreaterOrEqual returns a per-row expectation that value >= bound. SQL NULL fails. An empty table passes vacuously. Results use KindGreaterOrEqual.

func (NumberColumn) GreaterThan

func (c NumberColumn) GreaterThan(bound any) Expectation

GreaterThan returns a per-row expectation that value > bound. SQL NULL fails. An empty table passes vacuously. Results use KindGreaterThan.

func (NumberColumn) LessOrEqual

func (c NumberColumn) LessOrEqual(bound any) Expectation

LessOrEqual returns a per-row expectation that value <= bound. SQL NULL fails. An empty table passes vacuously. Results use KindLessOrEqual.

func (NumberColumn) LessThan

func (c NumberColumn) LessThan(bound any) Expectation

LessThan returns a per-row expectation that value < bound. SQL NULL fails. An empty table passes vacuously. Results use KindLessThan.

func (NumberColumn) MaxLessOrEqual

func (c NumberColumn) MaxLessOrEqual(bound float64) Expectation

MaxLessOrEqual returns a table-level expectation that MAX(column) <= bound. Build it from a numeric column via Int or Float. SQL NULL values are excluded. When every value is NULL the check passes vacuously. Results use KindMaxLessOrEqual, set RowDenominatorUnavailable, and append the observed maximum to Name on evaluation. Invalid column identifiers fail suite preflight before SQL runs.

func (NumberColumn) MinGreaterOrEqual

func (c NumberColumn) MinGreaterOrEqual(bound float64) Expectation

MinGreaterOrEqual returns a table-level expectation that MIN(column) >= bound. Build it from a numeric column via Int or Float. SQL NULL values are excluded. When every value is NULL the check passes vacuously. Results use KindMinGreaterOrEqual, set RowDenominatorUnavailable, and append the observed minimum to Name on evaluation. Invalid column identifiers fail suite preflight before SQL runs.

type Option

type Option func(*validateConfig)

Option configures a ValidateTable run.

func CaptureQueryDiagnostics

func CaptureQueryDiagnostics() Option

CaptureQueryDiagnostics records SQL text and bound arguments on each Result for optional export via IncludeCapturedDiagnostics. Captured content is never exposed through default Result serialization or default export.

func ContinueOnError

func ContinueOnError() Option

ContinueOnError records expectation preflight issues and execution errors on individual results and keeps evaluating later expectations. Preflight issues occupy their declaration-order slots with Result.Err set. Run-level option errors (invalid dialect, caps, key columns, or scope) still abort ValidateTable with a top-level error before evaluation starts. By default, the first preflight or execution error aborts ValidateTable with a zero report.

func SummaryOnly

func SummaryOnly() Option

SummaryOnly disables complete failed-row identity and returns counts plus capped samples only.

func WithDialect

func WithDialect(d Dialect) Option

WithDialect selects the SQL dialect used to render queries.

func WithFailedKeysCap

func WithFailedKeysCap(n int) Option

WithFailedKeysCap overrides the per-result failed-key cap for one ValidateTable call. Zero means unlimited. FailedCount and FailedPercent remain complete when keys are capped.

func WithKey

func WithKey(columns ...string) Option

WithKey enables failed-row identity using the given key columns. Disables summary-only mode. Key column names must pass identifier validation. Failed keys are capped by DefaultFailedKeysCap unless WithFailedKeysCap(0) is set.

func WithSampleCap

func WithSampleCap(n int) Option

WithSampleCap overrides the per-result sample cap for one ValidateTable call. Zero disables sample collection.

func WithScope added in v0.2.0

func WithScope(scope Scope) Option

WithScope limits every expectation to rows matching the trusted predicate. Scope configuration is validated when ValidateTable starts.

type PolicyVerdict

type PolicyVerdict string

PolicyVerdict is the exported policy state of an expectation: pass, fail, or unevaluated when the source Result has an error.

const (
	// PolicyVerdictPass means the expectation ran and its policy passed.
	PolicyVerdictPass PolicyVerdict = "pass"
	// PolicyVerdictFail means the expectation ran and its policy failed.
	PolicyVerdictFail PolicyVerdict = "fail"
	// PolicyVerdictUnevaluated means the source Result has an execution or
	// configuration error, so its policy outcome is unavailable.
	PolicyVerdictUnevaluated PolicyVerdict = "unevaluated"
)

type PreflightErrors

type PreflightErrors struct {
	// Issues lists every configuration failure in declaration order.
	Issues []PreflightIssue
}

PreflightErrors collects every configuration issue found before SQL starts. Returned by ValidateTable when ContinueOnError is not set. Use errors.As to inspect Issues; errors.Is matches ErrCategoryInvalidConfig on each issue.

func (*PreflightErrors) Error

func (e *PreflightErrors) Error() string

Error summarizes the collected configuration failures.

func (*PreflightErrors) Unwrap

func (e *PreflightErrors) Unwrap() []error

Unwrap returns each issue error for multi-error inspection.

type PreflightIssue

type PreflightIssue struct {
	// Index is the expectation position in the suite.
	Index int
	// ID is the caller-supplied expectation identifier when present.
	ID string
	// Err is the categorized configuration error for this slot.
	Err error
}

PreflightIssue records one expectation configuration failure.

type Redactor

type Redactor func(any) (any, error)

Redactor transforms a value before export. A returned error or panic fails export closed without emitting raw diagnostic content.

type Report

type Report struct {
	// Results preserves declaration order, including slots recorded under
	// ContinueOnError.
	Results []Result
	// Target names the validated table. Set by ValidateTable; nil when unavailable
	// (for example when a Report is assembled manually).
	Target *TableRef
	// ScopeID identifies the validation scope. It is empty when validation is
	// unscoped.
	ScopeID string
}

Report aggregates the results of every expectation in a suite.

func (Report) Err

func (r Report) Err() error

Err returns nil when the report is OK, otherwise a *ValidationError carrying the full report for gating and inspection.

func (Report) Failures

func (r Report) Failures() []Result

Failures returns results with Success false, including configuration and execution failures recorded under ContinueOnError.

func (Report) OK

func (r Report) OK() bool

OK reports whether every expectation passed (Success is true for all results).

func (Report) String

func (r Report) String() string

String renders a Report summary header ("gxsql report: passed/total expectations passed") followed by one indented Result.String line per entry in declaration order.

type Result

type Result struct {
	// ID is the optional caller-supplied stable identifier from WithID.
	ID string
	// Kind is the library-defined machine identifier for the expectation.
	Kind ExpectationKind
	// Name is human-readable display text and is not machine identity.
	Name string
	// Column is the validated SQL column for per-row checks and aggregates.
	Column string
	// Success is the policy verdict. False when the check fails or Result.Err is set.
	Success bool
	// RowDenominator states whether Total and FailedPercent describe rows.
	RowDenominator RowDenominator
	// Total is the evaluated row population when RowDenominator is available.
	Total int
	// FailedCount is the complete expectation-specific failure count; for per-row
	// checks it counts failing rows, while CustomCount may count groups or other
	// query-defined failures.
	FailedCount int
	// FailedPercent is the percentage of failing rows when the denominator is available.
	FailedPercent float64
	// Facts contains structured observations and configured thresholds.
	Facts ResultFacts
	// SampleValues holds capped offending column values on per-row failure.
	SampleValues []any
	// FailedKeys holds failing row keys in WithKey column order, capped unless
	// WithFailedKeysCap(0) selects unlimited retention.
	FailedKeys []RowKey
	// Err is a categorized configuration or execution failure when non-nil.
	Err error

	// Tolerated is true when a nonzero raw FailedCount passed within a
	// configured WithMaxFailedCount bound. Clean passes, above-bound failures,
	// empty populations, and errors are never tolerated.
	Tolerated bool
	// contains filtered or unexported fields
}

Result is the outcome of one expectation over a single ValidateTable run.

Per-row SQL expectations set RowDenominatorAvailable, Total to the table row count, and populate FailedCount, FailedPercent, SampleValues, and optionally FailedKeys on failure. FailedKeys are capped unless WithFailedKeysCap(0) selects unlimited retention. Table-level checks use RowDenominatorUnavailable; custom counts still expose their expectation-specific FailedCount while Success carries the verdict and Facts carry observed values while Name holds human-oriented display text.

func (Result) String

func (r Result) String() string

String renders one Result as a single human-readable line prefixed with ✓ or ✗. Successful per-row results include Total only when Total is greater than zero. Tolerated results explicitly say "tolerated" and still show raw FailedCount, Total, FailedPercent, sample values, and failed keys under the existing display caps. Failing per-row results include FailedCount, FailedPercent, sample values, and failed keys. Table-level results omit row denominators; failing lines show Name only when FailedCount is zero. Sample values and failed keys are truncated to maxDisplay (10) entries with an ellipsis.

type ResultFacts

type ResultFacts struct {
	// ObservedCount is a table-level integer observation when set.
	ObservedCount *int
	// ObservedFloat is a floating-point aggregate observation when set.
	ObservedFloat *float64

	// ConfiguredCount is the exact-count threshold for equal-style expectations.
	ConfiguredCount *int
	// ConfiguredCountLower is the inclusive lower integer bound.
	ConfiguredCountLower *int
	// ConfiguredCountUpper is the inclusive upper integer bound.
	ConfiguredCountUpper *int
	// ConfiguredFloatLower is the inclusive lower floating-point bound.
	ConfiguredFloatLower *float64
	// ConfiguredFloatUpper is the inclusive upper floating-point bound.
	ConfiguredFloatUpper *float64
	// ConfiguredFloatBound is the single-sided floating-point threshold.
	ConfiguredFloatBound *float64
	// ConfiguredBound is the per-row comparison threshold with a driver-bound type.
	ConfiguredBound any
	// ConfiguredBoundLower is the inclusive per-row lower bound.
	ConfiguredBoundLower any
	// ConfiguredBoundUpper is the inclusive per-row upper bound.
	ConfiguredBoundUpper any

	// ConfiguredMaxFailedCount is the inclusive maximum failed-row bound when a
	// WithMaxFailedCount policy decorated this result. Nil means no tolerance
	// was applied.
	ConfiguredMaxFailedCount *int
}

ResultFacts holds machine-readable observations and configured thresholds separate from display text. Threshold fields are populated by built-in expectations at construction time and must not be parsed from Name.

type RowCountBuilder

type RowCountBuilder struct{}

RowCountBuilder builds table-level COUNT(*) expectations. Start with RowCount. An empty table yields count 0 (not a vacuous pass). Results set RowDenominatorUnavailable, append the observed count to Name, and store it in ResultFacts.ObservedCount.

func RowCount

func RowCount() RowCountBuilder

RowCount returns a builder for table row-count checks.

func (RowCountBuilder) Between

func (RowCountBuilder) Between(lo, hi int) Expectation

Between returns a table-level expectation that lo <= row count <= hi (inclusive). Results use KindRowCountBetween.

func (RowCountBuilder) Equal

func (RowCountBuilder) Equal(want int) Expectation

Equal returns a table-level expectation of exactly want rows. Results use KindRowCountEqual.

func (RowCountBuilder) GreaterOrEqual

func (RowCountBuilder) GreaterOrEqual(bound int) Expectation

GreaterOrEqual returns a table-level expectation that row count >= bound. Results use KindRowCountGreaterEqual.

func (RowCountBuilder) GreaterThan

func (RowCountBuilder) GreaterThan(bound int) Expectation

GreaterThan returns a table-level expectation that row count > bound. Results use KindRowCountGreaterThan.

func (RowCountBuilder) LessOrEqual

func (RowCountBuilder) LessOrEqual(bound int) Expectation

LessOrEqual returns a table-level expectation that row count <= bound. Results use KindRowCountLessEqual.

func (RowCountBuilder) LessThan

func (RowCountBuilder) LessThan(bound int) Expectation

LessThan returns a table-level expectation that row count < bound. Results use KindRowCountLessThan.

type RowDenominator

type RowDenominator string

RowDenominator reports whether Total and FailedPercent are meaningful for a result. Table-level checks use RowDenominatorUnavailable so zero totals are not confused with an empty evaluated population.

const (
	// RowDenominatorAvailable marks per-row expectations where Total is the
	// table row count and FailedPercent is computed from FailedCount.
	RowDenominatorAvailable RowDenominator = "available"
	// RowDenominatorUnavailable marks table-level expectations where row counts
	// and percentages are not meaningful.
	RowDenominatorUnavailable RowDenominator = "unavailable"
)

type RowKey

type RowKey []any

RowKey identifies a failing table row by caller-supplied key column values in the same order as the WithKey columns passed to ValidateTable.

type Scope added in v0.2.0

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

Scope is an immutable scope definition containing a caller identity, a dialect-neutral predicate authored with ? placeholders, and bound values. Its fields are intentionally unexported so scope data cannot be modified after construction.

func TrustedScope added in v0.2.0

func TrustedScope(id, predicate string, args ...any) Scope

TrustedScope constructs a Scope from trusted Go-code predicate input. Validation is deferred until the scope is attached to ValidateTable.

type StringColumn

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

StringColumn is the entry point for string emptiness and length checks on one column. Construct one with String. Length predicates use the active Dialect.StringLength expression.

func String

func String(name string) StringColumn

String returns a builder for string column checks on name.

func (StringColumn) Empty

func (c StringColumn) Empty() Expectation

Empty returns a per-row expectation that the string is empty (SQL NULL fails). An empty table passes vacuously. Results use KindEmpty.

func (StringColumn) LenBetween

func (c StringColumn) LenBetween(lo, hi int) Expectation

LenBetween returns a per-row expectation that lo <= dialect string-length <= hi (inclusive). SQL NULL fails. Length uses Dialect.StringLength. An empty table passes vacuously. Results use KindLenBetween.

func (StringColumn) LenEqual

func (c StringColumn) LenEqual(n int) Expectation

LenEqual returns a per-row expectation that the dialect string-length of the column equals n. SQL NULL fails. Length uses Dialect.StringLength. An empty table passes vacuously. Results use KindLenEqual.

func (StringColumn) NotEmpty

func (c StringColumn) NotEmpty() Expectation

NotEmpty returns a per-row expectation that the string is non-empty after trimming is not applied—only SQL NULL and the empty string fail. An empty table passes vacuously. Results use KindNotEmpty.

type Suite

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

Suite is an ordered set of SQL expectations over a database table.

Example (Basic)
package main

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"fmt"
	"io"
	"sync"

	"github.com/busyminds/gxsql"
)

const exampleDriverName = "gxsql_example_test"

var registerExampleDriverOnce sync.Once

func openExampleDB(scenario string) (*sql.DB, error) {
	registerExampleDriverOnce.Do(func() {
		sql.Register(exampleDriverName, exampleDriver{})
	})
	return sql.Open(exampleDriverName, scenario)
}

type exampleDriver struct{}

func (exampleDriver) Open(name string) (driver.Conn, error) {
	return exampleConn{scenario: name}, nil
}

type exampleConn struct {
	scenario string
}

func (c exampleConn) Prepare(string) (driver.Stmt, error) { return nil, driver.ErrSkip }
func (c exampleConn) Close() error                        { return nil }
func (c exampleConn) Begin() (driver.Tx, error)           { return nil, driver.ErrSkip }

func (c exampleConn) QueryContext(_ context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
	switch c.scenario {
	case "pass":
		return c.passQuery(query, args)
	case "keys":
		return c.keysQuery(query, args)
	case "customcount":
		return c.customCountQuery(query, args)
	case "error":
		return nil, fmt.Errorf("example driver: query failed")
	default:
		return nil, fmt.Errorf("example driver: unknown scenario %q", c.scenario)
	}
}

func (exampleConn) passQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "users"`:
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	case `SELECT COUNT(*) FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ?`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(0)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected query %q", query)
	}
}

func (exampleConn) customCountQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "orders" WHERE TRUE AND status = ?`:
		if err := expectArgs(args, "pending"); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected custom count query")
	}
}

func (exampleConn) keysQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "users"`:
		return newExampleRows([]string{"count"}, []driver.Value{int64(3)}), nil
	case `SELECT COUNT(*) FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ?`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	case `SELECT "id" FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ? ORDER BY "id"`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"id"}, []driver.Value{int64(2)}, []driver.Value{int64(3)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected query %q", query)
	}
}

func expectArgs(args []driver.NamedValue, want ...any) error {
	if len(args) != len(want) {
		return fmt.Errorf("example driver: arg count = %d, want %d", len(args), len(want))
	}
	for i := range want {
		if args[i].Value != want[i] {
			return fmt.Errorf("example driver: arg %d = %v, want %v", i, args[i].Value, want[i])
		}
	}
	return nil
}

type exampleRows struct {
	cols []string
	rows [][]driver.Value
	idx  int
}

func newExampleRows(cols []string, rows ...[]driver.Value) driver.Rows {
	return &exampleRows{cols: cols, rows: rows}
}

func (r *exampleRows) Columns() []string { return r.cols }
func (r *exampleRows) Close() error      { return nil }

func (r *exampleRows) Next(dest []driver.Value) error {
	if r.idx >= len(r.rows) {
		return io.EOF
	}
	copy(dest, r.rows[r.idx])
	r.idx++
	return nil
}

func main() {
	db, err := openExampleDB("pass")
	if err != nil {
		panic(err)
	}
	defer db.Close()

	suite := gxsql.NewSuite(
		gxsql.RowCount().Equal(2),
		gxsql.Int("age").Between(0, 120),
	)
	report, err := suite.ValidateTable(
		context.Background(), db, gxsql.Table("users"),
		gxsql.WithDialect(gxsql.SQLite()),
	)
	if err != nil {
		panic(err)
	}

	fmt.Println(report.OK())
	fmt.Println(report.String())

}
Output:
true
gxsql report: 2/2 expectations passed
  ✓ row count == 2: got 2
  ✓ age between [0,120] (2 rows)
Example (ContinueOnError)
package main

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"fmt"
	"io"
	"sync"

	"github.com/busyminds/gxsql"
)

const exampleDriverName = "gxsql_example_test"

var registerExampleDriverOnce sync.Once

func openExampleDB(scenario string) (*sql.DB, error) {
	registerExampleDriverOnce.Do(func() {
		sql.Register(exampleDriverName, exampleDriver{})
	})
	return sql.Open(exampleDriverName, scenario)
}

type exampleDriver struct{}

func (exampleDriver) Open(name string) (driver.Conn, error) {
	return exampleConn{scenario: name}, nil
}

type exampleConn struct {
	scenario string
}

func (c exampleConn) Prepare(string) (driver.Stmt, error) { return nil, driver.ErrSkip }
func (c exampleConn) Close() error                        { return nil }
func (c exampleConn) Begin() (driver.Tx, error)           { return nil, driver.ErrSkip }

func (c exampleConn) QueryContext(_ context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
	switch c.scenario {
	case "pass":
		return c.passQuery(query, args)
	case "keys":
		return c.keysQuery(query, args)
	case "customcount":
		return c.customCountQuery(query, args)
	case "error":
		return nil, fmt.Errorf("example driver: query failed")
	default:
		return nil, fmt.Errorf("example driver: unknown scenario %q", c.scenario)
	}
}

func (exampleConn) passQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "users"`:
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	case `SELECT COUNT(*) FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ?`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(0)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected query %q", query)
	}
}

func (exampleConn) customCountQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "orders" WHERE TRUE AND status = ?`:
		if err := expectArgs(args, "pending"); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected custom count query")
	}
}

func (exampleConn) keysQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "users"`:
		return newExampleRows([]string{"count"}, []driver.Value{int64(3)}), nil
	case `SELECT COUNT(*) FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ?`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	case `SELECT "id" FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ? ORDER BY "id"`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"id"}, []driver.Value{int64(2)}, []driver.Value{int64(3)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected query %q", query)
	}
}

func expectArgs(args []driver.NamedValue, want ...any) error {
	if len(args) != len(want) {
		return fmt.Errorf("example driver: arg count = %d, want %d", len(args), len(want))
	}
	for i := range want {
		if args[i].Value != want[i] {
			return fmt.Errorf("example driver: arg %d = %v, want %v", i, args[i].Value, want[i])
		}
	}
	return nil
}

type exampleRows struct {
	cols []string
	rows [][]driver.Value
	idx  int
}

func newExampleRows(cols []string, rows ...[]driver.Value) driver.Rows {
	return &exampleRows{cols: cols, rows: rows}
}

func (r *exampleRows) Columns() []string { return r.cols }
func (r *exampleRows) Close() error      { return nil }

func (r *exampleRows) Next(dest []driver.Value) error {
	if r.idx >= len(r.rows) {
		return io.EOF
	}
	copy(dest, r.rows[r.idx])
	r.idx++
	return nil
}

func main() {
	db, err := openExampleDB("error")
	if err != nil {
		panic(err)
	}
	defer db.Close()

	report, err := gxsql.NewSuite(
		gxsql.Int("age").Between(0, 120),
		gxsql.String("email").NotEmpty(),
	).ValidateTable(
		context.Background(), db, gxsql.Table("users"),
		gxsql.WithDialect(gxsql.SQLite()),
		gxsql.ContinueOnError(),
	)

	fmt.Println(err == nil)
	fmt.Println(report.Results[0].Err != nil && report.Results[1].Err != nil)
	fmt.Println(report.String())

}
Output:
true
true
gxsql report: 0/2 expectations passed
  ✗ age between [0,120]
  ✗ email not empty
Example (FailedRowKeysAndSummary)
package main

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"fmt"
	"io"
	"sync"

	"github.com/busyminds/gxsql"
)

const exampleDriverName = "gxsql_example_test"

var registerExampleDriverOnce sync.Once

func openExampleDB(scenario string) (*sql.DB, error) {
	registerExampleDriverOnce.Do(func() {
		sql.Register(exampleDriverName, exampleDriver{})
	})
	return sql.Open(exampleDriverName, scenario)
}

type exampleDriver struct{}

func (exampleDriver) Open(name string) (driver.Conn, error) {
	return exampleConn{scenario: name}, nil
}

type exampleConn struct {
	scenario string
}

func (c exampleConn) Prepare(string) (driver.Stmt, error) { return nil, driver.ErrSkip }
func (c exampleConn) Close() error                        { return nil }
func (c exampleConn) Begin() (driver.Tx, error)           { return nil, driver.ErrSkip }

func (c exampleConn) QueryContext(_ context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
	switch c.scenario {
	case "pass":
		return c.passQuery(query, args)
	case "keys":
		return c.keysQuery(query, args)
	case "customcount":
		return c.customCountQuery(query, args)
	case "error":
		return nil, fmt.Errorf("example driver: query failed")
	default:
		return nil, fmt.Errorf("example driver: unknown scenario %q", c.scenario)
	}
}

func (exampleConn) passQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "users"`:
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	case `SELECT COUNT(*) FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ?`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(0)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected query %q", query)
	}
}

func (exampleConn) customCountQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "orders" WHERE TRUE AND status = ?`:
		if err := expectArgs(args, "pending"); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected custom count query")
	}
}

func (exampleConn) keysQuery(query string, args []driver.NamedValue) (driver.Rows, error) {
	switch query {
	case `SELECT COUNT(*) FROM "users"`:
		return newExampleRows([]string{"count"}, []driver.Value{int64(3)}), nil
	case `SELECT COUNT(*) FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ?`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"count"}, []driver.Value{int64(2)}), nil
	case `SELECT "id" FROM "users" WHERE "age" IS NULL OR "age" < ? OR "age" > ? ORDER BY "id"`:
		if err := expectArgs(args, int64(0), int64(120)); err != nil {
			return nil, err
		}
		return newExampleRows([]string{"id"}, []driver.Value{int64(2)}, []driver.Value{int64(3)}), nil
	default:
		return nil, fmt.Errorf("example driver: unexpected query %q", query)
	}
}

func expectArgs(args []driver.NamedValue, want ...any) error {
	if len(args) != len(want) {
		return fmt.Errorf("example driver: arg count = %d, want %d", len(args), len(want))
	}
	for i := range want {
		if args[i].Value != want[i] {
			return fmt.Errorf("example driver: arg %d = %v, want %v", i, args[i].Value, want[i])
		}
	}
	return nil
}

type exampleRows struct {
	cols []string
	rows [][]driver.Value
	idx  int
}

func newExampleRows(cols []string, rows ...[]driver.Value) driver.Rows {
	return &exampleRows{cols: cols, rows: rows}
}

func (r *exampleRows) Columns() []string { return r.cols }
func (r *exampleRows) Close() error      { return nil }

func (r *exampleRows) Next(dest []driver.Value) error {
	if r.idx >= len(r.rows) {
		return io.EOF
	}
	copy(dest, r.rows[r.idx])
	r.idx++
	return nil
}

func main() {
	db, err := openExampleDB("keys")
	if err != nil {
		panic(err)
	}
	defer db.Close()

	report, err := gxsql.NewSuite(gxsql.Int("age").Between(0, 120)).WithSampleCap(0).ValidateTable(
		context.Background(), db, gxsql.Table("users"),
		gxsql.WithDialect(gxsql.SQLite()),
		gxsql.WithKey("id"),
		gxsql.WithFailedKeysCap(0),
	)
	if err != nil {
		panic(err)
	}

	fmt.Println(report.OK())
	fmt.Println(report.String())
	fmt.Println(report.Results[0].FailedKeys)

}
Output:
false
gxsql report: 0/1 expectations passed
  ✗ age between [0,120]  2/3 failed (66.7%)  e.g. [] @ [[2] [3]]
[[2] [3]]

func NewSuite

func NewSuite(exps ...Expectation) *Suite

NewSuite builds a suite from the given expectations, evaluated in order.

func (*Suite) ValidateTable

func (s *Suite) ValidateTable(
	ctx context.Context,
	db DB,
	table TableRef,
	opts ...Option,
) (Report, error)

ValidateTable runs every expectation in declaration order (collect-all, never fail-fast on policy failures) and returns the aggregated Report. It uses Postgres when WithDialect is not supplied.

Validation-policy failures are returned as (report, nil); gate with report.Err(), which yields a *ValidationError recoverable via errors.As. Run-level option errors (including invalid scope) abort with (zero Report, err) before evaluation starts. Expectation preflight failures collected before SQL starts return a zero Report and *PreflightErrors unless ContinueOnError is set, each affected slot records Result.Err. The first database, rendering, scan, or context error aborts with a zero Report and error unless ContinueOnError is set, when the error is recorded on that result and evaluation continues.

func (*Suite) WithFailedKeysCap

func (s *Suite) WithFailedKeysCap(n int) *Suite

WithFailedKeysCap sets the maximum failed-row keys retained per Result and returns the suite for chaining. Zero means unlimited. FailedCount and FailedPercent remain complete when keys are capped.

func (*Suite) WithSampleCap

func (s *Suite) WithSampleCap(n int) *Suite

WithSampleCap sets the maximum sample values retained per Result and returns the suite for chaining. Zero disables sample collection.

type TableRef

type TableRef struct {
	Schema string
	Name   string
}

TableRef names a database table for Suite.ValidateTable. Schema and Name are quoted separately by the active Dialect; raw strings are never concatenated into SQL unquoted.

func SchemaTable

func SchemaTable(schema, name string) TableRef

SchemaTable returns a schema-qualified table reference. Both schema and name must satisfy identifier validation when rendered.

func Table

func Table(name string) TableRef

Table returns an unqualified table reference. Name must satisfy identifier validation when rendered.

type ValidationError

type ValidationError struct {
	// Report is the complete validation outcome, including passing results.
	Report Report
}

ValidationError wraps a failed Report as an error for runtime gating. Recover the full report via errors.As and the Report field.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error summarizes the number and display names of failed expectations.

Directories

Path Synopsis
Package gxsqltest adapts a gxsql.Suite to Go tests.
Package gxsqltest adapts a gxsql.Suite to Go tests.
internal
conformance
Package conformance contains the shared real-engine contract tests for gxsql dialects.
Package conformance contains the shared real-engine contract tests for gxsql dialects.

Jump to

Keyboard shortcuts

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