Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Custom Checks

Built-in checks cover common Postgres migration hazards, but every project has unique rules — naming conventions, banned operations, team policies. Custom checks let you enforce these with simple Rhai scripts.

Write your checks as .rhai files, point custom_checks_dir at the directory in diesel-guard.toml, and diesel-guard will run them alongside the built-in checks.

Quick Start

  1. Create a directory for your checks:
mkdir checks
  1. Write a check script (e.g., checks/require_concurrent_index.rhai):
let stmt = node.IndexStmt;
if stmt == () { return; }

if !stmt.concurrent {
    let idx_name = if stmt.idxname != "" { stmt.idxname } else { "(unnamed)" };
    #{
        operation: "INDEX without CONCURRENTLY: " + idx_name,
        problem: "Creating index '" + idx_name + "' without CONCURRENTLY blocks writes on the table.",
        safe_alternative: "Use CREATE INDEX CONCURRENTLY:\n  CREATE INDEX CONCURRENTLY " + idx_name + " ON ...;"
    }
}
  1. Add to diesel-guard.toml:
custom_checks_dir = "checks"
  1. Run as usual:
diesel-guard check migrations/

How It Works

  • Each .rhai script is called once per SQL statement in the migration
  • The node variable contains the pg_query AST for that statement (a nested map)
  • The config variable exposes the current diesel-guard.toml settings (e.g., config.postgres_version)
  • Scripts match on a specific node type: let stmt = node.IndexStmt;
  • If the node doesn’t match, node.IndexStmt returns () — early-return with if stmt == () { return; }
  • Return () for no violation, a map for one, or an array of maps for multiple
  • Map keys: operation, problem, safe_alternative (all required strings)

The config Variable

config gives scripts access to the user’s configuration. Use it to make version-aware checks:

// Only flag this on Postgres < 14
if config.postgres_version != () && config.postgres_version >= 14 { return; }

Available fields:

FieldTypeDescription
config.postgres_versioninteger or ()Target PG major version, or () if unset
config.check_downboolWhether down migrations are checked
config.disable_checksarrayCheck names that are disabled

Using dump-ast

Use dump-ast to inspect the AST for any SQL statement. This is the easiest way to discover which fields are available:

diesel-guard dump-ast --sql "CREATE INDEX idx_users_email ON users(email);"

Key fields and how they map to Rhai (using IndexStmt as an example):

JSON pathRhai accessDescription
IndexStmt.concurrentstmt.concurrentWhether CONCURRENTLY was specified
IndexStmt.idxnamestmt.idxnameIndex name
IndexStmt.uniquestmt.uniqueWhether it’s a UNIQUE index
IndexStmt.relation.relnamestmt.relation.relnameTable name
IndexStmt.index_paramsstmt.index_paramsArray of indexed columns

Return Values

No violation — return () (either explicitly or by reaching the end of the script):

let stmt = node.IndexStmt;
if stmt == () { return; }

if stmt.concurrent {
    return;  // All good, CONCURRENTLY is used
}

Single violation — return a map with operation, problem, and safe_alternative:

#{
    operation: "INDEX without CONCURRENTLY: idx_users_email",
    problem: "Creating index without CONCURRENTLY blocks writes on the table.",
    safe_alternative: "Use CREATE INDEX CONCURRENTLY."
}

Multiple violations — return an array of maps:

let violations = [];
for rel in stmt.relations {
    violations.push(#{
        operation: "TRUNCATE: " + rel.node.RangeVar.relname,
        problem: "TRUNCATE acquires ACCESS EXCLUSIVE lock.",
        safe_alternative: "Use batched DELETE instead."
    });
}
violations

Common AST Node Types

SQLNode TypeKey Fields
CREATE TABLECreateStmtrelation.relname, relation.relpersistence, table_elts
CREATE INDEXIndexStmtidxname, concurrent, unique, relation, index_params
ALTER TABLEAlterTableStmtrelation, cmds (array of AlterTableCmd)
DROP TABLE/INDEX/...DropStmtremove_type, objects, missing_ok, behavior
ALTER TABLE RENAMERenameStmtrename_type, relation, subname, newname
TRUNCATETruncateStmtrelations (array of Node-wrapped RangeVar)
CREATE EXTENSIONCreateExtensionStmtextname, if_not_exists
REINDEXReindexStmtkind, concurrent, relation

Note: Column definitions (ColumnDef) are nested inside CreateStmt.table_elts and AlterTableCmd.def, not top-level nodes. Use dump-ast to explore the nesting for ALTER TABLE ADD COLUMN statements.

pg:: Constants

Protobuf enum fields like DropStmt.remove_type and AlterTableCmd.subtype are integer values. Instead of hard-coding magic numbers, use the built-in pg:: module:

// Instead of: stmt.remove_type == 42
if stmt.remove_type == pg::OBJECT_TABLE { ... }

ObjectType

Used by DropStmt.remove_type, RenameStmt.rename_type, etc.

ConstantDescription
pg::OBJECT_INDEXIndex
pg::OBJECT_TABLETable
pg::OBJECT_COLUMNColumn
pg::OBJECT_DATABASEDatabase
pg::OBJECT_SCHEMASchema
pg::OBJECT_SEQUENCESequence
pg::OBJECT_VIEWView
pg::OBJECT_FUNCTIONFunction
pg::OBJECT_EXTENSIONExtension
pg::OBJECT_TRIGGERTrigger
pg::OBJECT_TYPEType

AlterTableType

Used by AlterTableCmd.subtype.

ConstantDescription
pg::AT_ADD_COLUMNADD COLUMN
pg::AT_COLUMN_DEFAULTSET DEFAULT / DROP DEFAULT
pg::AT_DROP_NOT_NULLDROP NOT NULL
pg::AT_SET_NOT_NULLSET NOT NULL
pg::AT_DROP_COLUMNDROP COLUMN
pg::AT_ALTER_COLUMN_TYPEALTER COLUMN TYPE
pg::AT_ADD_CONSTRAINTADD CONSTRAINT
pg::AT_DROP_CONSTRAINTDROP CONSTRAINT
pg::AT_VALIDATE_CONSTRAINTVALIDATE CONSTRAINT

ConstrType

Used by Constraint.contype.

ConstantDescription
pg::CONSTR_NOTNULLNOT NULL
pg::CONSTR_DEFAULTDEFAULT
pg::CONSTR_IDENTITYIDENTITY
pg::CONSTR_GENERATEDGENERATED
pg::CONSTR_CHECKCHECK
pg::CONSTR_PRIMARYPRIMARY KEY
pg::CONSTR_UNIQUEUNIQUE
pg::CONSTR_EXCLUSIONEXCLUSION
pg::CONSTR_FOREIGNFOREIGN KEY

DropBehavior

Used by DropStmt.behavior.

ConstantDescription
pg::DROP_RESTRICTRESTRICT (default)
pg::DROP_CASCADECASCADE

Examples

The examples/ directory contains ready-to-use scripts covering common patterns — naming conventions, banned operations, version-aware checks, and more. Browse them to get started or use as templates for your own checks.

Disabling Custom Checks

Custom checks can be disabled in diesel-guard.toml using the filename stem as the check name:

# Disables checks/require_concurrent_index.rhai
disable_checks = ["require_concurrent_index"]

safety-assured blocks also suppress custom check violations — any SQL inside a safety-assured block is skipped by all checks, both built-in and custom.

Debugging Tips

  • Inspect the AST: Use diesel-guard dump-ast --sql "..." to see exactly what fields are available
  • Runtime errors: Invalid field access or type errors produce stderr warnings — the check is skipped but other checks continue
  • Compilation errors: Syntax errors in .rhai files are reported at startup
  • Infinite loops: Scripts that exceed the operations limit are terminated safely with a warning