grizzle

package module
v0.1.0-rc.1 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

README

Grizzle

CI Go Reference License

Grizzle is a declarative, in-process database automigration library for Go. On boot it syncs PostgreSQL and SQLite schemas from a standard schema.sql, without numbered migration files, version collisions, or an external CLI in your production image.

flowchart LR
    Schema["schema.sql<br/>desired state"] --> Shadow["Shadow sandbox<br/>compile & validate"]
    Shadow -->|desired| Diff["Diff engine<br/>pure comparison"]

    Live[("Live database<br/>PostgreSQL / SQLite")] --> Lock["Advisory lock<br/>mutual exclusion"]
    Lock --> LiveInspect["Catalog inspect<br/>live state"]
    LiveInspect -->|actual| Diff

    Diff --> Plan["Migration plan<br/>steps & hash"]
    Plan --> Gate{"Hazard gate<br/>safety checks"}
    Gate -->|Approved| Apply["Apply migration<br/>tx & concurrently"]
    Apply --> Live

Why Grizzle?

  • Standard SQL as the source of truth: Write ordinary CREATE TABLE and CREATE INDEX statements. No Go struct tags, ORM annotations, or proprietary DSL.
  • In-process only: Runs inside your Go binary. No Node.js, Python, Docker, or CLI sidecar required at runtime.
  • Safe defaults: Destructive drops are blocked (AllowDrop: false). Risky structural changes hit a hazard gate before apply.
  • Multi-pod mutual exclusion: Session advisory locks coordinate concurrent Kubernetes replicas, with lock-contention retry and a post-lock re-diff.
  • PostgreSQL-friendly apply: Indexes use CREATE INDEX CONCURRENTLY outside transactions by default; foreign keys are added as NOT VALID and validated in a separate transaction.
  • SQLite parity: 12-step table rebuild, column renames, and foreign-key checks via pure Go (modernc.org/sqlite).
  • Exportable plans: Writes versioned migration artifacts for sql, goose, and atlas with deterministic plan hashes.

Quickstart

1. PostgreSQL (with pgx)
-- schema.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    full_name TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_users_created_at ON users (created_at);
package main

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

	_ "github.com/jackc/pgx/v5/stdlib"
	"github.com/muandane/grizzle"
)

//go:embed schema.sql
var schemaSQL string

func main() {
	ctx := context.Background()

	db, err := sql.Open("pgx", "postgres://postgres:secret@localhost:5432/myapp?sslmode=disable")
	if err != nil {
		log.Fatalf("database connect error: %v", err)
	}
	defer db.Close()

	opts := grizzle.Options{
		SchemaSQL:        schemaSQL,
		AllowDrop:        false, // Default-deny drops
		StrictScope:      true,  // Protect unmanaged tables (asynq, postgis, etc.)
		IncludeTables:    []string{"users"},
		LockTimeout:      5 * time.Second,
		StatementTimeout: 2 * time.Minute,
	}

	// 1. Inspect and compute deterministic migration plan
	plan, err := grizzle.PlanDiff(ctx, db, opts)
	if err != nil {
		log.Fatalf("planning failed: %v", err)
	}

	// 2. Apply plan with cryptographic approval hash verification
	err = grizzle.Apply(ctx, db, plan, grizzle.ApplyOpts{
		ExpectedHash: plan.Hash(),
	})
	if err != nil {
		log.Fatalf("migration failed: %v", err)
	}

	log.Println("Database synchronized. Starting service...")
}
2. SQLite (embedded, zero Cgo)
-- schema.sql
CREATE TABLE documents (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_documents_created ON documents (created_at);
package main

import (
	"context"
	"database/sql"
	_ "embed"
	"log"

	"github.com/muandane/grizzle"
	_ "modernc.org/sqlite"
)

//go:embed schema.sql
var schemaSQL string

func main() {
	ctx := context.Background()

	db, err := sql.Open("sqlite", "app.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)")
	if err != nil {
		log.Fatalf("database open error: %v", err)
	}
	defer db.Close()

	err = grizzle.Sync(ctx, db, grizzle.Options{
		SchemaSQL: schemaSQL,
	})
	if err != nil {
		log.Fatalf("sqlite sync failed: %v", err)
	}

	log.Println("SQLite database initialized successfully.")
}

Supported engines

Engine Versions Drivers Capabilities
PostgreSQL 14, 15, 16, 17, 18 (tested in CI) pgx/v5, lib/pq, database/sql Session advisory locks, CREATE INDEX CONCURRENTLY, ADD CONSTRAINT ... NOT VALID + separate VALIDATE CONSTRAINT, invalid index repair, unmanaged object dependency tracking (UNMANAGED_DEPENDENCY).
SQLite 3.35+ modernc.org/sqlite (pure Go), mattn/go-sqlite3 12-step table rebuild with trigger/view preservation, keyset batch copying, savepoint isolation, generated columns (VIRTUAL / STORED), native column renames, PRAGMA foreign_key_check.

CI runs the PostgreSQL integration matrix on 14–18 (16 on pull requests).


Contributing

Full guidelines: CONTRIBUTING.md.

Local development uses devenv (Nix) as the single source of truth for Go, golangci-lint, PostgreSQL 16, git hooks, and quality-gate scripts. There is no Makefile; do not add one.

Prerequisites
  1. Install devenv (2.x) and direnv.
  2. From the repo root, run direnv allow (or devenv shell). That provisions tools and installs local git hooks.
  3. Run devenv up to start the background PostgreSQL service (required for integration tests).

DATABASE_URL / POSTGRES_DSN default to:

postgres://127.0.0.1:5432/grizzle_test?sslmode=disable
Day-to-day commands

These are available inside the devenv shell and mirror CI:

Command What it does
fmt Format both Go modules
vet go vet on both modules
lint golangci-lint on both modules
test Unit tests with the race detector
test-integration Integration tests against local PostgreSQL (needs devenv up)
ci Full local gate: lint + vet + unit + integration
golden-update Regenerate golden plan/export fixtures after intentional changes
db-shell / db-reset / clean psql into the test DB, wipe schema, remove test artifacts
Before you open a PR
  1. Respect the layering rules in CONTRIBUTING.md (pure core must stay free of I/O).
  2. Pass lint and ci (including -race).
  3. Use Conventional Commits (feat:, fix:, docs:, …).
  4. Document public API changes in docs/SPEC.md and CHANGELOG.md.
  5. Do not log or commit secrets or connection credentials.

Git hooks (installed on shell entry):

Hook Checks
pre-commit gofmt, go vet, golangci-lint (both modules)
pre-push go test -race -count=1 ./... (both modules)

Integration tests are not hooked (they need devenv up). Run ci before a PR that touches internal/exec, internal/history, or the public API.


Five safety defaults

Capability TypeORM (synchronize) Drizzle (drizzle-kit push) Atlas CLI Ent (entgo.io) Grizzle
Runtime topology In-process Node.js External Node CLI External Go binary / Docker In-process Go ORM In-process Go library
Distributed locking None (races on multi-pod boot) None (developer CLI) Advisory locks Custom driver hook pg_advisory_lock + post-lock re-diff
Default drop policy Drops unmapped tables/cols Prompts in TTY; drops with --force Configurable flags Drops disabled (WithDropColumn(false)) Hard-blocked (AllowDrop: false)
Shadow validation None (runs on live DB) None (runs on live DB) Dev DB container In-memory parser Isolated shadow schema (_grizzle_shadow)
Pre-execution hazard gate None None Analyzer rules (Pro/Cloud) None Structural Plan.Hazards() gate
Plan hash & drift check None None Directory checksums None Plan.Hash() + Check()

Details and citations: docs/SAFETY.md.


Schema linting

grizzle.Lint statically checks the desired schema IR (pure, database-free, deterministic) before anything reaches a migration. Diagnostics carry a rule ID, severity (ERROR / WARNING / INFO), and target; render them as text, JSON, or GitHub Actions annotations (LintFormatText / LintFormatJSON / LintFormatGitHub).

Rule Severity Check
L001 ERROR Table has no primary key (breaks logical replication; partitions skipped)
L002 WARNING Foreign key columns not covered by any index (parent changes sequential-scan the table)
L003 WARNING Table / column / index / enum names not lowercase snake_case
L004 WARNING Legacy SERIAL / nextval default instead of GENERATED ALWAYS AS IDENTITY
L005 WARNING CHECK constraint name not lowercase snake_case
L006 WARNING Duplicate CHECK constraint expression on the same table
L007 INFO CHECK constraint relies on PostgreSQL's auto-generated name; prefer an explicit CONSTRAINT name CHECK
// LintSchema with no rules uses DefaultLintRules() (L001..L007).
diags := grizzle.LintSchema(schemaIR)
if grizzle.LintHasErrors(diags) {
    log.Fatalf("schema lint failed:\n%s", mustFormatLint(diags))
}

Details: docs/SPEC.md.


Architecture

Dependency direction is one-way:

graph TD
    classDef pure fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px;

    API["grizzle (public API)<br/>Sync · PlanDiff · Apply · Check · Export"]

    API -->|runs| Exec["exec<br/>locks, tx, retries"]
    API -->|formats| Export["export<br/>SQL / Goose / Atlas"]:::pure

    Exec -->|audits| History["history<br/>audit table"]
    Exec -->|delegates| Dialect["dialect (postgres, sqlite)<br/>introspect + render DDL"]
    Exec -->|computes| Diff["diff<br/>desired vs actual"]:::pure

    History -->|queries| Dialect
    Dialect -->|renders| Plan["plan<br/>steps, hazards, hash"]:::pure
    Dialect -->|builds| Schema["schema + scope<br/>models, filters"]:::pure

    Diff -->|generates| Plan
    Diff -->|compares| Schema
    Export -->|serializes| Plan
Runtime flow
flowchart LR
    Introspect["1. Introspect<br/>catalog state"] -->|SchemaIR| Diff["2. Diff<br/>compute delta"]
    Diff -->|Changes| Plan["3. Plan<br/>hazard gating"]
    Plan -->|Steps| Render["4. Render<br/>dialect DDL"]
    Render -->|SQL| Exec["5. Exec<br/>locks & tx groups"]
  • Pure core (green: diff, plan, schema, scope, export): No database/sql, context, network, or file I/O. Deterministic and unit-testable.
  • Dialect layer (dialect/postgres, dialect/sqlite): Catalog introspection and dialect-specific DDL.
  • Execution layer (exec, history): Connection pools, advisory locks, timeouts, transaction grouping, failure recording.
  • Public facade (grizzle): Minimal wiring and public type aliases.

When to use Grizzle vs goose vs Atlas

Scenario Recommend Why
Go app boot automigration Grizzle In-process on boot, single binary, standard schema.sql, advisory locking, no deployment sidecar.
Hand-written sequential SQL goose Manual data transforms, ETL backfills, or numbered scripts (001_init.sql). Bootstrap with grizzle export --format goose.
Polyglot / Terraform / CI pipelines Atlas Multi-language stacks, Atlas Kubernetes Operator, or Atlas Cloud governance. Bootstrap with grizzle export --format atlas.

Stability (v0.1.0)

  • Stable enough for production use: Sync, PlanDiff, Apply, Check, Export, both dialects, advisory locking, invalid index recovery, unmanaged object protection, and hazard gates are covered by unit and integration tests.
  • Experimental: Column renames (Options.Renames) and expand-and-contract migrations (Options.ExpandContract, Options.Backfill) work and are golden-tested, but are marked // Experimental: because their config shapes may change before v1.0.

CLI

# Generate deterministic migration plan JSON
grizzle plan --dsn "$DATABASE_URL" --schema schema.sql --out plan.json

# Apply pre-approved plan with strict drift verification
grizzle apply --dsn "$DATABASE_URL" --plan plan.json --accept-hazard DROP_COLUMN

# Check live database for schema drift in CI (exits 0 if clean, 4 on drift)
grizzle check --dsn "$DATABASE_URL" --schema schema.sql

# Export planned migration to Goose or Atlas format
grizzle export --plan plan.json --format goose --out ./migrations

# Experimental: zero-downtime expand/contract planning with explicit renames
# (expand: new columns added alongside existing; contract: separate drop plan)
grizzle plan --dsn "$DATABASE_URL" --schema schema.sql --out expand.json \
  --rename users.full_name=display_name --expand-contract
grizzle plan --dsn "$DATABASE_URL" --schema schema.sql --out contract.json \
  --rename users.full_name=display_name --allow-drop

--rename old=new (optionally table-qualified table.old=new) can be repeated and is accepted by plan, apply, and check. --expand-contract stages new columns alongside existing ones and defers drops to a separate contract plan. Backfilling renamed columns is library-only (Options.Backfill); see examples/expand-contract.

Exit codes
Code Meaning
0 Success / clean schema / no-op
1 Execution or connection error
2 Blocked by unaccepted hazard
3 Plan hash drift during apply (ErrPlanDrift)
4 Schema drift during check

Examples


Documentation


License

Apache License 2.0. See LICENSE.

Documentation

Overview

Package grizzle provides declarative, in-process database schema automigration for Go.

Grizzle synchronizes PostgreSQL and SQLite database schemas directly from standard schema.sql DDL definitions on application startup, eliminating sequence file collisions, manual migration versioning, and external CLI/Docker runtime dependencies.

Core Concepts

  • Declarative Schema: Write standard SQL DDL (schema.sql) as the single source of truth.
  • In-Process Execution: Runs natively within your Go binary without needing external CLI binaries.
  • Shadow Sandbox: Replays and validates desired schema state inside a temporary shadow schema or SQLite in-memory sandbox.
  • Deterministic Diff: Pure schema comparison detecting missing tables, columns, indexes, constraints, and types.
  • Safe Execution: Dedicated advisory locking, multi-node contention retry, hazard gating, and transactional execution.
  • Zero-Downtime Operations: Safe non-transactional enum expansions (PostgreSQL ALTER TYPE ... ADD VALUE) and concurrent index generation.

Subpackages & Modules

  • griztest: Testing primitives (MustSync, MustPlan, Reset) for effortless test database setup and teardown.
  • otelgrizzle: Optional OpenTelemetry tracing integration (github.com/muandane/grizzle/otelgrizzle).

Basic Usage

db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
    log.Fatal(err)
}
defer db.Close()

err = grizzle.Sync(ctx, db, grizzle.Options{
    SchemaSQL: schemaSQL,
})
if err != nil {
    log.Fatalf("migration failed: %v", err)
}

Index

Examples

Constants

View Source
const (
	// PartitionStrategyRange indicates range-based partitioning.
	PartitionStrategyRange = schema.PartitionStrategyRange
	// PartitionStrategyList indicates list-based partitioning.
	PartitionStrategyList = schema.PartitionStrategyList
	// PartitionStrategyHash indicates hash-based partitioning.
	PartitionStrategyHash = schema.PartitionStrategyHash
)

PartitionStrategy constants

View Source
const (
	ChangeCreateEnum         = plan.ChangeCreateEnum
	ChangeAlterEnum          = plan.ChangeAlterEnum
	ChangeCreateTable        = plan.ChangeCreateTable
	ChangeDropTable          = plan.ChangeDropTable
	ChangeAddColumn          = plan.ChangeAddColumn
	ChangeDropColumn         = plan.ChangeDropColumn
	ChangeAlterColumn        = plan.ChangeAlterColumn
	ChangeCreateIndex        = plan.ChangeCreateIndex
	ChangeDropIndex          = plan.ChangeDropIndex
	ChangeAddFK              = plan.ChangeAddFK
	ChangeDropFK             = plan.ChangeDropFK
	ChangeAddCheck           = plan.ChangeAddCheck
	ChangeDropCheck          = plan.ChangeDropCheck
	ChangeValidateConstraint = plan.ChangeValidateConstraint
	ChangeRenameColumn       = plan.ChangeRenameColumn
	ChangeAttachPartition    = plan.ChangeAttachPartition
	ChangeDetachPartition    = plan.ChangeDetachPartition
)

ChangeType constants

View Source
const (
	HazardDropTable              = plan.HazardDropTable
	HazardDropColumn             = plan.HazardDropColumn
	HazardTypeNarrow             = plan.HazardTypeNarrow
	HazardNotNullNoDefault       = plan.HazardNotNullNoDefault
	HazardIndexBuild             = plan.HazardIndexBuild
	HazardDropIndex              = plan.HazardDropIndex
	HazardDropFK                 = plan.HazardDropFK
	HazardDropCheck              = plan.HazardDropCheck
	HazardCheckValidateScan      = plan.HazardCheckValidateScan
	HazardRenameAmbiguous        = plan.HazardRenameAmbiguous
	HazardUnmanagedDependency    = plan.HazardUnmanagedDependency
	HazardGeneratedRewrite       = plan.HazardGeneratedRewrite
	HazardPartitionAttachScan    = plan.HazardPartitionAttachScan
	HazardPartitionPendingDetach = plan.HazardPartitionPendingDetach
)

HazardCode constants

View Source
const (
	// UnmanagedView indicates a standard SQL VIEW.
	UnmanagedView = schema.UnmanagedView
	// UnmanagedMaterialized indicates a MATERIALIZED VIEW.
	UnmanagedMaterialized = schema.UnmanagedMaterialized
	// UnmanagedTrigger indicates a database trigger attached to a table.
	UnmanagedTrigger = schema.UnmanagedTrigger
	// UnmanagedFunction indicates a stored procedure or function.
	UnmanagedFunction = schema.UnmanagedFunction
	// UnmanagedSequence indicates an unmanaged database sequence.
	UnmanagedSequence = schema.UnmanagedSequence
	// UnmanagedEnum indicates an unmanaged custom enum type.
	UnmanagedEnum = schema.UnmanagedEnum
	// UnmanagedDomain indicates an unmanaged domain type.
	UnmanagedDomain = schema.UnmanagedDomain
)
View Source
const (
	HazardLevelCritical = plan.HazardLevelCritical
	HazardLevelWarning  = plan.HazardLevelWarning
	HazardLevelNotice   = plan.HazardLevelNotice
)

HazardLevel constants

View Source
const (
	// ExportFormatSQL targets raw SQL files with transaction markers.
	ExportFormatSQL = export.FormatSQL
	// ExportFormatGoose targets Goose-compatible migration files.
	ExportFormatGoose = export.FormatGoose
	// ExportFormatAtlas targets Atlas-compatible migration files.
	ExportFormatAtlas = export.FormatAtlas
)
View Source
const (
	// LintSeverityError marks a structural anti-pattern that must be fixed.
	LintSeverityError = lint.SeverityError
	// LintSeverityWarning marks a recommendation that may be ignored deliberately.
	LintSeverityWarning = lint.SeverityWarning
	// LintSeverityInfo marks a stylistic suggestion with no correctness impact.
	LintSeverityInfo = lint.SeverityInfo
)

Variables

View Source
var (
	// ErrEmptySchema is returned when the provided SchemaSQL string is empty or contains only whitespace.
	ErrEmptySchema = plan.ErrEmptySchema

	// ErrDestructiveBlocked is returned when a destructive operation (such as DROP TABLE or DROP COLUMN)
	// is rejected by the configured drop safety policy.
	ErrDestructiveBlocked = plan.ErrDestructiveBlocked

	// ErrLockAcquisition is returned when acquiring the database advisory lock fails or times out.
	ErrLockAcquisition = plan.ErrLockAcquisition

	// ErrCompilationFailed is returned when the user's schema.sql fails to compile/execute inside the shadow schema.
	ErrCompilationFailed = plan.ErrCompilationFailed

	// ErrExecutionFailed is returned when applying the planned DDL migration steps to the target schema fails.
	ErrExecutionFailed = plan.ErrExecutionFailed

	// ErrInspectionFailed is returned when inspecting the schema metadata fails.
	ErrInspectionFailed = plan.ErrInspectionFailed

	// ErrUnsupportedDialect is returned when an unrecognized database driver or dialect is provided.
	ErrUnsupportedDialect = plan.ErrUnsupportedDialect

	// ErrHazardBlocked is returned when a migration plan contains critical hazards not explicitly accepted in AcceptHazards.
	ErrHazardBlocked = plan.ErrHazardBlocked

	// ErrPlanDrift is returned when the approved plan hash differs from the recomputed plan hash after lock acquisition.
	ErrPlanDrift = plan.ErrPlanDrift

	// ErrDrift is returned by Check when the live database schema differs from the desired schema.
	ErrDrift = plan.ErrDrift

	// ErrLockTimeout is returned when an advisory lock cannot be acquired within the configured lock timeout.
	ErrLockTimeout = plan.ErrLockTimeout

	// ErrInvalidOptions is returned when provided Options fail validation.
	ErrInvalidOptions = plan.ErrInvalidOptions

	// ErrStrictScope is returned when StrictScope is enabled but no IncludeTables are specified.
	ErrStrictScope = plan.ErrStrictScope

	// ErrPartitionConversion is returned when attempting to convert a regular table to a partitioned table or vice versa in-place.
	ErrPartitionConversion = plan.ErrPartitionConversion

	// ErrUnsupportedMultiSchema is returned when multiple schemas are configured on SQLite.
	ErrUnsupportedMultiSchema = plan.ErrUnsupportedMultiSchema

	// ErrPartitionKeyNotInUnique is returned when a primary key or unique constraint on a partitioned table does not include all partition key columns.
	ErrPartitionKeyNotInUnique = plan.ErrPartitionKeyNotInUnique

	// ErrAfterSyncFailed is returned when the AfterSync hook fails after the migration steps have already committed.
	ErrAfterSyncFailed = plan.ErrAfterSyncFailed

	// ErrSeedFailed is returned when seed SQL execution fails; the seed
	// transaction is rolled back and the schema is left intact.
	ErrSeedFailed = plan.ErrSeedFailed
)

Functions

func Apply

func Apply(ctx context.Context, db *sql.DB, p *Plan, opts ApplyOpts) error

Apply applies an approved migration plan to the database. If ExpectedHash is provided and the plan recomputed post-lock differs, Apply aborts with ErrPlanDrift.

func Check

func Check(ctx context.Context, db *sql.DB, opts Options) error

Check inspects the live database and returns ErrDrift (wrapped in DriftError) if the schema differs from the desired schema specified in opts.SchemaSQL. It is strictly read-only, never modifies the database, and never auto-fixes drift.

func GenerateLockID

func GenerateLockID(dbName, schemaName string) int64

GenerateLockID produces a deterministic 64-bit integer lock ID from db and schema names.

func LintFormatGitHub

func LintFormatGitHub(w io.Writer, diags []LintDiagnostic) error

LintFormatGitHub renders diagnostics as GitHub Actions annotations.

func LintFormatJSON

func LintFormatJSON(w io.Writer, diags []LintDiagnostic) error

LintFormatJSON renders diagnostics as a JSON array.

func LintFormatText

func LintFormatText(w io.Writer, diags []LintDiagnostic) error

LintFormatText renders diagnostics as human-readable text.

func LintHasErrors

func LintHasErrors(diags []LintDiagnostic) bool

LintHasErrors reports whether any diagnostic has severity ERROR.

func Seed

func Seed(ctx context.Context, db *sql.DB, seedSQL string, opts ...Option) error

Seed executes idempotent seed SQL against the database. The seed runs in a single transaction: either the whole script commits with an 'applied' grizzle_history record keyed by the seed's content hash, or nothing persists. If the same seed hash was already applied and force is false, the seed is skipped. Forced re-runs execute the script again, so seed scripts should tolerate re-execution (IF NOT EXISTS, ON CONFLICT...).

Dialect and target schema are taken from opts (auto-detected when unset).

func SeedHash

func SeedHash(seedSQL string) string

SeedHash returns the deterministic sha256 identity of a seed script. Leading and trailing whitespace is ignored so reformatted-but-identical seeds do not re-run.

func Sync

func Sync(ctx context.Context, db *sql.DB, opts Options) error

Sync synchronizes the target database schema to match the desired state in opts.SchemaSQL.

Example
package main

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

	"github.com/muandane/grizzle"
	_ "modernc.org/sqlite"
)

func main() {
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil {
		log.Fatalf("failed to open database: %v", err)
	}
	defer func() { _ = db.Close() }()

	schema := `
CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    email TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL
);`

	err = grizzle.Sync(context.Background(), db, grizzle.Options{
		Dialect:   grizzle.DialectSQLite,
		SchemaSQL: schema,
	})
	if err != nil {
		log.Fatalf("sync failed: %v", err)
	}

	var count int
	_ = db.QueryRow("SELECT count(*) FROM users;").Scan(&count)
	fmt.Printf("Users table exists with row count: %d\n", count)

}
Output:
Users table exists with row count: 0

Types

type ApplyOpts

type ApplyOpts struct {
	// ExpectedHash is the approved plan hash. If non-empty, Apply recomputes the plan after
	// acquiring the lock and verifies the recomputed hash matches ExpectedHash.
	// If it does not match, Apply aborts with ErrPlanDrift.
	ExpectedHash string

	// AcceptHazards specifies explicitly accepted critical hazards.
	AcceptHazards []HazardCode

	// Backfill hook function run outside the DDL lock window in batches during staged expand migration.
	Backfill BackfillFunc

	// BeforeSync runs once before any migration steps or locks are executed.
	BeforeSync SyncHook

	// AfterSync runs once after all migration steps and history recording succeed.
	AfterSync SyncHook

	// BeforeStep executes immediately prior to executing each plan step.
	BeforeStep StepHook

	// AfterStep executes immediately following the successful execution of each plan step.
	AfterStep StepHook

	// SQLiteRebuildThreshold defines the row count threshold above which SQLite table rebuilds
	// chunk data copying by keyset. Defaults to 100000.
	SQLiteRebuildThreshold int

	// SQLiteRebuildBatchSize defines the chunk size when copying data in batches during SQLite table rebuilds.
	// Defaults to 10000.
	SQLiteRebuildBatchSize int

	// LockNamespace specifies the application namespace string used for PostgreSQL advisory locking (defaults to "grizzle").
	LockNamespace string

	// Tracer specifies an optional tracer for observing plan application.
	Tracer Tracer
}

ApplyOpts configures verification and execution parameters when applying an approved Plan.

type BackfillFunc

type BackfillFunc func(ctx context.Context, tx *sql.Tx, table, oldCol, newCol string) error

BackfillFunc defines the hook function signature for batch backfilling columns outside the DDL lock window.

type ChangeType

type ChangeType = plan.ChangeType

ChangeType describes the classification of a schema change.

type CheckConstraintIR

type CheckConstraintIR = schema.CheckConstraint

CheckConstraintIR represents the intermediate representation of a CHECK constraint.

type ColumnIR

type ColumnIR = schema.Column

ColumnIR represents the intermediate representation of a database column.

type DependencyRef

type DependencyRef = schema.DependencyRef

DependencyRef represents a reference to a table or column that an unmanaged object depends on.

type DestructiveViolationError

type DestructiveViolationError = plan.DestructiveViolationError

DestructiveViolationError reports all destructive steps that were rejected by the active safety policy.

type Dialect

type Dialect string

Dialect specifies the target SQL database dialect.

const (
	// DialectAuto instructs Grizzle to auto-detect the engine from the driver type.
	DialectAuto Dialect = ""
	// DialectPostgres specifies PostgreSQL (13+).
	DialectPostgres Dialect = "postgres"
	// DialectSQLite specifies SQLite (3.35+).
	DialectSQLite Dialect = "sqlite"
)

type DriftError

type DriftError = plan.DriftError

DriftError reports unapplied differences between the live schema and the desired schema.

type DropPolicy

type DropPolicy = plan.DropPolicy

DropPolicy defines the safety behavior for destructive drops.

type DryRunResult

type DryRunResult = exec.DryRunResult

DryRunResult reports the outcome of a live dry-run verification.

func DryRunVerify

func DryRunVerify(ctx context.Context, db *sql.DB, opts Options) (*DryRunResult, error)

DryRunVerify executes a live dry-run rollback against the target database: the planned DDL runs against real data inside transactions that are unconditionally rolled back, so constraints, casts, and check expressions are verified without persisting any change. Non-transactional steps (CREATE INDEX CONCURRENTLY, ALTER TYPE ... ADD VALUE, DETACH PARTITION CONCURRENTLY) are skipped and reported in DryRunResult.UnverifiedNonTx. No records are written to grizzle_history, and hooks do not run unless opts.ExecuteHooksInDryRun is set.

type EnumIR

type EnumIR = schema.Enum

EnumIR represents the intermediate representation of an enum type.

type ExportArtifact

type ExportArtifact = export.Artifact

ExportArtifact represents a generated migration file ready to be saved.

func Export

func Export(p *Plan, format ExportFormat, version string, timestamp ...time.Time) ([]ExportArtifact, error)

Export serializes the given plan into migration files for the requested tool format.

type ExportFormat

type ExportFormat = export.Format

ExportFormat defines supported target formats for migration export.

type ForeignKeyIR

type ForeignKeyIR = schema.ForeignKey

ForeignKeyIR represents the intermediate representation of a foreign key relationship.

type GeneratedColumn

type GeneratedColumn = schema.GeneratedColumn

GeneratedColumn describes a computed or generated column.

type Hazard

type Hazard = plan.Hazard

Hazard describes an operation that could cause data loss or service disruption.

type HazardCode

type HazardCode = plan.HazardCode

HazardCode identifies the specific type of migration hazard detected.

type HazardError

type HazardError = plan.HazardError

HazardError reports all critical hazards that were not explicitly accepted.

type HazardLevel

type HazardLevel = plan.HazardLevel

HazardLevel indicates the severity of potential data loss or downtime.

type HookContext

type HookContext = exec.HookContext

HookContext provides invocation context and database access for a step hook. DBTX is bound to the executor of the pending step: a *sql.Tx for transactional groups or a *sql.Conn for non-transactional steps.

type IndexIR

type IndexIR = schema.Index

IndexIR represents the intermediate representation of a database index.

type LintDiagnostic

type LintDiagnostic = lint.Diagnostic

LintDiagnostic is a single static lint finding against a schema element.

func LintSchema

func LintSchema(s *SchemaIR, rules ...LintRule) []LintDiagnostic

LintSchema runs rules (defaulting to DefaultLintRules) against the compiled schema IR and returns deterministically sorted diagnostics.

type LintRule

type LintRule = lint.Rule

LintRule is a pure check executed against the desired schema IR.

func DefaultLintRules

func DefaultLintRules() []LintRule

DefaultLintRules returns the built-in lint rule set (L001..L004).

type LintSeverity

type LintSeverity = lint.Severity

LintSeverity indicates how strongly a lint diagnostic should block a release.

type Option

type Option func(*Options)

Option represents a functional option for configuring Options.

func WithAcceptHazards

func WithAcceptHazards(hazards ...plan.HazardCode) Option

WithAcceptHazards configures explicitly accepted critical hazard codes.

func WithAfterStep

func WithAfterStep(fn StepHook) Option

WithAfterStep registers a hook that runs immediately after each successful plan step.

func WithAfterSync

func WithAfterSync(fn SyncHook) Option

WithAfterSync registers a hook that runs once after all migration steps and history recording succeed.

func WithAllowDrop

func WithAllowDrop(allow bool) Option

WithAllowDrop sets the general drop permission.

func WithBackfill

func WithBackfill(fn BackfillFunc) Option

WithBackfill configures the batch backfill hook function for staged expand migrations.

Experimental: backfill batching semantics (batch size, ordering, error handling) may change before the 1.0 release. There is no CLI equivalent; backfill is library-only.

func WithBeforeStep

func WithBeforeStep(fn StepHook) Option

WithBeforeStep registers a hook that runs immediately prior to each plan step.

func WithBeforeSync

func WithBeforeSync(fn SyncHook) Option

WithBeforeSync registers a hook that runs once before any migration steps or locks are executed.

func WithDialect

func WithDialect(d Dialect) Option

WithDialect sets the database dialect.

func WithDryRun

func WithDryRun() Option

WithDryRun enables dry-run mode: the planned SQL is validated against the live database without executing it.

func WithDryRunLockTimeout

func WithDryRunLockTimeout(d time.Duration) Option

WithDryRunLockTimeout sets the lock wait bound for live dry-run verification.

func WithExcludeTables

func WithExcludeTables(tables ...string) Option

WithExcludeTables sets excluded tables and glob patterns.

func WithExecuteHooksInDryRun

func WithExecuteHooksInDryRun() Option

WithExecuteHooksInDryRun allows BeforeStep/AfterStep hooks to run during live dry-run verification.

func WithExpandContract

func WithExpandContract(expand bool) Option

WithExpandContract enables or disables staged expand-and-contract zero-downtime migrations.

Experimental: staged expand-and-contract plans are under active development; the contract phase and hazard surface may change before the 1.0 release.

func WithIncludeTables

func WithIncludeTables(tables ...string) Option

WithIncludeTables sets included tables.

func WithLockNamespace

func WithLockNamespace(ns string) Option

WithLockNamespace sets the application namespace string for PostgreSQL advisory locks.

func WithLockTimeout

func WithLockTimeout(d time.Duration) Option

WithLockTimeout sets the maximum duration to wait for acquiring locks.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the structured logger.

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries sets the maximum retry attempts upon lock timeout conflict (SQLSTATE 55P03).

func WithNonConcurrentIndexes

func WithNonConcurrentIndexes(disabled bool) Option

WithNonConcurrentIndexes controls whether PostgreSQL index creation should run inside the transaction.

func WithRandFloat

func WithRandFloat(fn func() float64) Option

WithRandFloat sets a custom random float function for deterministic backoff jitter in tests.

func WithRenames

func WithRenames(renames map[string]string) Option

WithRenames sets the explicit column rename mapping.

Experimental: the rename mapping format and RENAME_AMBIGUOUS semantics may change before the 1.0 release.

func WithSQLiteRebuildBatching

func WithSQLiteRebuildBatching(threshold, batchSize int) Option

WithSQLiteRebuildBatching sets the threshold and batch size for chunked keyset copying during SQLite rebuilds.

func WithSeedForce

func WithSeedForce(force bool) Option

WithSeedForce re-runs the seed even when the same seed hash was already applied.

func WithSeedSQL

func WithSeedSQL(seedSQL string) Option

WithSeedSQL attaches idempotent seed SQL executed after a successful sync.

func WithStatementTimeout

func WithStatementTimeout(d time.Duration) Option

WithStatementTimeout sets the maximum duration for any single migration DDL statement.

func WithStrictScope

func WithStrictScope(strict bool) Option

WithStrictScope enables strict scoping mode requiring non-empty IncludeTables.

func WithTargetSchema

func WithTargetSchema(schema string) Option

WithTargetSchema sets the target schema. Deprecated: Use WithTargetSchemas for multi-schema support.

func WithTargetSchemas

func WithTargetSchemas(schemas ...string) Option

WithTargetSchemas sets the target schemas to manage.

func WithTracer

func WithTracer(t Tracer) Option

WithTracer sets the tracer.

type Options

type Options struct {
	// Dialect explicitly defines the database engine (DialectPostgres or DialectSQLite).
	Dialect Dialect

	// SchemaSQL contains the complete DDL representing the desired state.
	SchemaSQL string

	// TargetSchema is the schema to manage (defaults to "public" for Postgres, "main" for SQLite).
	// Deprecated: Use TargetSchemas for multi-schema support.
	TargetSchema string

	// TargetSchemas specifies the database schemas to manage (defaults to [TargetSchema] or ["public"] for Postgres).
	TargetSchemas []string

	// ShadowSchema is the temporary schema name used for validation (defaults to "_grizzle_shadow").
	ShadowSchema string

	// AllowDrop permits all destructive operations when set to true.
	// Defaults to false for zero data loss.
	AllowDrop bool

	// Granular drop overrides (nil inherits from AllowDrop):
	AllowDropTable  *bool
	AllowDropColumn *bool
	AllowDropIndex  *bool
	AllowDropFK     *bool
	AllowDropCheck  *bool

	// ExcludeTables defines table names or glob patterns (e.g. "spatial_ref_sys", "asynq_*")
	// that Grizzle will never manage, alter, or drop.
	ExcludeTables []string

	// IncludeTables limits Grizzle's management scope to only the specified tables or patterns.
	IncludeTables []string

	// StrictScope requires IncludeTables to be non-empty. When true and IncludeTables is empty,
	// operations fail immediately with ErrStrictScope.
	// Strongly recommended for production environments to avoid accidental alterations or drops
	// of untracked tables.
	StrictScope bool

	// AcceptHazards lists critical hazard codes that are explicitly approved to execute.
	// Critical hazards not present in this list will cause Apply to fail with ErrHazardBlocked.
	AcceptHazards []plan.HazardCode

	// NonConcurrentIndexes opts out of emitting CONCURRENTLY for PostgreSQL index creation/drops.
	// When true, index operations are created inside the transaction.
	NonConcurrentIndexes bool

	// LockNamespace specifies the application namespace string used for PostgreSQL advisory locking (defaults to "grizzle").
	LockNamespace string

	// LockID is an optional explicit 64-bit integer used for the PostgreSQL advisory lock (pg_advisory_xact_lock).
	// If 0, Grizzle derives 2-int per-schema advisory locks using (hash32(LockNamespace), hash32(schema)).
	LockID int64

	// LockTimeout specifies the maximum time to wait when acquiring locks (defaults to 5s).
	LockTimeout time.Duration

	// StatementTimeout specifies the maximum execution time for any single DDL statement (defaults to 5m).
	StatementTimeout time.Duration

	// MaxRetries specifies the number of retry attempts upon lock timeout (SQLSTATE 55P03, defaults to 3).
	MaxRetries int

	// RandFloat provides an optional random source func returning in [0.0, 1.0) for deterministic jitter in tests.
	RandFloat func() float64

	// Renames maps old column names to new column names (e.g. "users.old_col": "new_col" or "old_col": "new_col")
	// to explicitly disambiguate column renames instead of treating them as DROP + ADD.
	//
	// Experimental: the rename mapping format and RENAME_AMBIGUOUS semantics
	// may change before the 1.0 release.
	Renames map[string]string

	// ExpandContract enables staged expand-and-contract zero-downtime migrations (ZDM).
	// In expand mode, renamed or modified columns are added alongside existing columns,
	// delaying destructive drops to a later, separately approved plan.
	//
	// Experimental: staged expand-and-contract plans are under active development;
	// the contract phase and hazard surface may change before the 1.0 release.
	ExpandContract bool

	// Backfill hook function run outside the DDL lock window in batches during staged expand migration.
	//
	// Experimental: backfill batching semantics (batch size, ordering, error
	// handling) may change before the 1.0 release. There is no CLI equivalent;
	// backfill is library-only.
	Backfill BackfillFunc

	// BeforeSync runs once before any migration steps or locks are executed.
	// If it returns an error, the migration aborts before any DDL runs and no
	// history record is written.
	BeforeSync SyncHook

	// AfterSync runs once after all migration steps and history recording succeed.
	// If it returns an error, the DDL has already committed; the error is
	// wrapped with ErrAfterSyncFailed.
	AfterSync SyncHook

	// BeforeStep executes immediately prior to executing each plan step.
	// If it returns an error, the pending step is not executed; in a
	// transactional group the transaction is rolled back and history records
	// the failure with error "before_step hook: ...".
	BeforeStep StepHook

	// AfterStep executes immediately following the successful execution of each
	// plan step. If it returns an error in a transactional group, the
	// transaction is rolled back (reverting the step); in a non-transactional
	// group the step has already committed and history records "partial".
	AfterStep StepHook

	// DryRun returns the planned SQL statements without executing them on the live database.
	DryRun bool

	// DryRunLockTimeout bounds how long live dry-run verification waits on
	// table locks before failing fast (defaults to 2s).
	DryRunLockTimeout time.Duration

	// ExecuteHooksInDryRun allows BeforeStep/AfterStep hooks to run during
	// live dry-run verification. Defaults to false to avoid accidental
	// external side effects (webhooks, message publishing, etc.).
	ExecuteHooksInDryRun bool

	// SeedSQL contains idempotent data-seed SQL executed after a successful
	// sync (Sync DDL → AfterSync → Seed). The seed runs in a single
	// transaction and is skipped when the same seed (by content hash) was
	// already applied, unless SeedForce is set.
	SeedSQL string

	// SeedForce re-runs the seed even when the same seed hash was already
	// applied.
	SeedForce bool

	// SQLiteRebuildThreshold defines the row count threshold above which SQLite table rebuilds
	// chunk data copying by keyset to prevent journal memory exhaustion.
	// Defaults to 100000. Set to 0 to disable batching.
	SQLiteRebuildThreshold int

	// SQLiteRebuildBatchSize defines the chunk size when copying data in batches during SQLite table rebuilds.
	// Defaults to 10000.
	SQLiteRebuildBatchSize int

	// Logger accepts a structured logger (*slog.Logger) for migration events.
	Logger *slog.Logger

	// Tracer specifies an optional tracer (OpenTelemetry or custom) for observing migrations.
	Tracer Tracer
}

Options configures the schema synchronization process.

func (*Options) Validate

func (o *Options) Validate() error

Validate checks whether the options are consistent and valid.

type PartitionKey

type PartitionKey = schema.PartitionKey

PartitionKey defines how a partitioned table is divided.

type PartitionOf

type PartitionOf = schema.PartitionOf

PartitionOf defines the relationship of a partition table to its parent partitioned table.

type PartitionStrategy

type PartitionStrategy = schema.PartitionStrategy

PartitionStrategy represents the partitioning method used by a partitioned table.

type Plan

type Plan = plan.Plan

Plan is the complete migration plan containing sequenced steps, policy, and hashes.

func ParsePlanJSON

func ParsePlanJSON(data []byte) (*Plan, string, error)

ParsePlanJSON parses a serialized plan JSON string or bytes into a *Plan, returning the recorded plan hash.

func PlanDiff

func PlanDiff(ctx context.Context, db *sql.DB, opts Options) (*Plan, error)

PlanDiff inspects the live database and computes the planned migration steps without applying them.

Example
package main

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

	"github.com/muandane/grizzle"
	_ "modernc.org/sqlite"
)

func main() {
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil {
		log.Fatalf("failed to open database: %v", err)
	}
	defer func() { _ = db.Close() }()

	schema := `
CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    sku TEXT NOT NULL UNIQUE,
    price INTEGER NOT NULL DEFAULT 0
);`

	plan, err := grizzle.PlanDiff(context.Background(), db, grizzle.Options{
		Dialect:   grizzle.DialectSQLite,
		SchemaSQL: schema,
	})
	if err != nil {
		log.Fatalf("plan diff failed: %v", err)
	}

	fmt.Printf("Plan steps to execute: %d\n", len(plan.Steps))

}
Output:
Plan steps to execute: 1

type PlanDocument

type PlanDocument = plan.Document

PlanDocument represents a complete, serialized migration plan artifact including hash, hazards, scope, and options digest.

type PrimaryKeyIR

type PrimaryKeyIR = schema.PrimaryKey

PrimaryKeyIR represents the intermediate representation of a table primary key.

type SchemaIR

type SchemaIR = schema.Schema

SchemaIR represents the database-agnostic schema intermediate representation.

func CompileSchema

func CompileSchema(ctx context.Context, db *sql.DB, opts Options) (*SchemaIR, error)

CompileSchema compiles opts.SchemaSQL into the desired SchemaIR without diffing or executing anything. For PostgreSQL the compilation happens in the shadow schema inside an always-rolled-back transaction (requires a connection to the target database); for SQLite it happens in an in-memory database, making it fully offline.

type ScopeDocument

type ScopeDocument = plan.ScopeDocument

ScopeDocument represents the scope section of a plan document.

type Span

type Span = exec.Span

Span represents an active trace span recorded by a Tracer.

type Step

type Step = plan.Step

Step represents a single atomic migration step to execute.

type StepHook

type StepHook = exec.StepHook

StepHook executes custom imperative code immediately before or after each plan step. Hooks must be idempotent: if a later step fails and the migration is retried or resumed after partial execution, hooks may be invoked again.

type SyncHook

type SyncHook = exec.SyncHook

SyncHook executes custom imperative code once before or after the entire synchronization. It receives a dedicated connection (not a transaction) because the execution may contain non-transactional statements.

type TableIR

type TableIR = schema.Table

TableIR represents the intermediate representation of a database table.

type Tracer

type Tracer = exec.Tracer

Tracer defines the interface for tracing Grizzle lifecycle events.

type UnmanagedKind

type UnmanagedKind = schema.UnmanagedKind

UnmanagedKind represents the category of an unmanaged database object.

type UnmanagedObject

type UnmanagedObject = schema.UnmanagedObject

UnmanagedObject represents an unmanaged database object detected during introspection.

Directories

Path Synopsis
cmd
grizzle command
Package main implements the grizzle CLI.
Package main implements the grizzle CLI.
examples
expand-contract command
Package main demonstrates zero-downtime expand/contract migrations with Grizzle: a column rename executed in two separately-approved stages with a library-side backfill between them.
Package main demonstrates zero-downtime expand/contract migrations with Grizzle: a column rename executed in two separately-approved stages with a library-side backfill between them.
postgres-stdlib command
sqlc-workflow command
sqlc-workflow/db
Package db provides type-safe database queries.
Package db provides type-safe database queries.
sqlite-embedded command
Package griztest provides testing utilities and ergonomic primitives for testing applications powered by Grizzle schema migrations.
Package griztest provides testing utilities and ergonomic primitives for testing applications powered by Grizzle schema migrations.
internal
dialect
Package dialect defines the interface for database-specific introspection, DDL rendering, and transactional locking behaviors.
Package dialect defines the interface for database-specific introspection, DDL rendering, and transactional locking behaviors.
dialect/postgres
Package postgres implements PostgreSQL-specific introspection, DDL rendering, advisory locking, and shadow schema compilation.
Package postgres implements PostgreSQL-specific introspection, DDL rendering, advisory locking, and shadow schema compilation.
dialect/sqlite
Package sqlite implements SQLite-specific schema introspection, in-memory shadow validation, and the 12-step table rebuild engine.
Package sqlite implements SQLite-specific schema introspection, in-memory shadow validation, and the 12-step table rebuild engine.
diff
Package diff performs pure relational comparison between live and desired database schemas.
Package diff performs pure relational comparison between live and desired database schemas.
exec
Package exec coordinates statement execution, transactional locking, and safety policy enforcement.
Package exec coordinates statement execution, transactional locking, and safety policy enforcement.
history
Package history manages migration history records and schema drift tracking.
Package history manages migration history records and schema drift tracking.
lint
Package lint provides static, database-free linting of a desired schema IR.
Package lint provides static, database-free linting of a desired schema IR.
plan
Package plan models migration steps, execution plans, safety policies, and hazard detection.
Package plan models migration steps, execution plans, safety policies, and hazard detection.
schema
Package schema defines the pure, engine-agnostic relational schema representation (Table, Column, Index, Constraint, Schema) and normalization logic.
Package schema defines the pure, engine-agnostic relational schema representation (Table, Column, Index, Constraint, Schema) and normalization logic.
scope
Package scope handles table filtering and unmanaged-table boundaries (PostGIS, background queues, and user-specified include/exclude patterns).
Package scope handles table filtering and unmanaged-table boundaries (PostGIS, background queues, and user-specified include/exclude patterns).
testutil
Package testutil provides shared test infrastructure for integration tests.
Package testutil provides shared test infrastructure for integration tests.
otelgrizzle module

Jump to

Keyboard shortcuts

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