README.md

August 9, 2026 · View on GitHub

Scythe

Write SQL. Get type-safe code. In ten languages.

Scythe compiles annotated .sql files into database access code — row types, query functions and type mappings — that stays in sync with your schema. It reads your SQL statically, so generation needs no database connection and no network: it runs in a pre-commit hook.

10 languages · 10 databases · 56 backends · 58 lint and audit rules · nullability inferred through JOINs, CTEs and window functions

crates.io Homebrew CI License: MIT Docs Discord

Docs · Install · What you get · Quick start · Commands · Compare


Why scythe

Every application that talks to a database needs glue: code that maps parameters in, maps result rows out, and keeps the types aligned. It is tedious, easy to get subtly wrong, and it changes every time the schema does.

Scythe deletes that layer. You keep writing SQL — the language your database already optimizes and your team already knows — and the mapping code is compiled from it. The generated code is readable, has no runtime dependency beyond your driver, and carries a provenance header so scythe check can tell you when it has drifted from the schema it came from.

Where an ORM still wins: if your users choose the database, an ORM abstracts dialect differences at runtime and scythe does not. Scythe targets one engine per configuration block, which is what buys you engine-specific features and a real query planner. If you control the database, that trade is worth making.

What you get

What it doesDocs
Type inference that reads the queryNullability propagated through LEFT/RIGHT/FULL joins, COALESCE, CASE WHEN, aggregates and window functions — not just column constraints. CTEs (including recursive), RETURNING, enums, composites, arrays and JSON map to language-native types.Type inference
58 built-in rules23 lint rules (UPDATE without WHERE, NULL compared with =, leading-wildcard LIKE, SELECT *) and 35 audit rules, plus sqruff's style rules through scythe fmt.Lint rules · Linting
scythe auditSecurity and migration-safety scanning: dangerous functions, GRANT to PUBLIC, literal passwords, SELECT * over PII, plus 19 migration rules for locking ALTERs and destructive DDL. Human, SARIF or JSON.Audit
scythe checkVerifies committed code still matches the SQL it was generated from, via 8 provenance rules. Given --database-url it adds 7 schema-drift rules against a live PostgreSQL catalog.CLI reference
scythe inspectLive-database health checks: foreign keys without covering indexes, tables with policies but RLS disabled, duplicate indexes. PostgreSQL only.Inspect
Output you can shapeRow types as Pydantic, msgspec, dataclasses, Zod schemas or plain interfaces, depending on backend; structs_only (TypeScript and rust-sqlx) for a types-only package; type overrides for ltree, citext or PostGIS.Configuration · Custom types
Annotations beyond :one / :many@optional compiles a parameter into a conditional filter, :batch for bulk operations, @returns :grouped with @group_by for nested results.Annotations
Coming from sqlc?scythe migrate converts an existing sqlc.yaml into a scythe.toml.Migration from sqlc

Installation

cargo install scythe-cli
cargo binstall scythe-cli              # prebuilt binary, no compile
brew install Goldziher/tap/scythe

No Rust toolchain required — both wrappers download the prebuilt binary for your platform and verify its checksum:

npm install --save-dev scythe-cli
pip install scythe-sql

See Installation for supported platforms, proxy configuration and cache control.

Pre-commit / prek hooks
repos:
  - repo: https://github.com/Goldziher/scythe
    rev: v0.14.0
    hooks:
      - id: scythe-fmt        # format SQL
      - id: scythe-lint       # lint with auto-fix
      - id: scythe-audit      # SC-SEC*/SC-RLS*/SC-MIG*/SC-CHK*
      - id: scythe-inspect    # live-DB checks, needs $DATABASE_URL
      - id: scythe-generate   # regenerate on SQL changes
      - id: scythe-check      # validate without generating

The hooks declare language: rust, so pre-commit compiles scythe from source on first use — a few minutes. If you already have the binary on PATH (via brew, npm or pip above), add language: system to a hook to use it directly instead.

See Pre-commit hooks for the full table and per-hook configuration.

Quick start

1. Annotate a query. status is an enum column; o.total and o.notes sit on the right side of a LEFT JOIN.

-- @name GetUserOrders
-- @returns :many
SELECT u.id, u.name, o.total, o.notes
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = \$1;

2. Point scythe.toml at it.

[scythe]
version = "1"

[[sql]]
name = "main"
engine = "postgresql"
schema = ["sql/schema.sql"]
queries = ["sql/queries.sql"]

[[sql.gen]]
backend = "python-psycopg3"
output = "src/generated"

3. Generate.

scythe generate

4. Use it. total and notes are | None because the join can fail to match; id and name are not. The enum became a real UserStatus, and the parameter is typed with it.

@dataclass(frozen=True, slots=True)
class GetUserOrdersRow:
    """Row type for GetUserOrders query."""

    id: int
    name: str
    total: decimal.Decimal | None
    notes: str | None


async def get_user_orders(conn: AsyncConnection, *, status: UserStatus) -> list[GetUserOrdersRow]:
    """Execute GetUserOrders query."""
The same row type in all ten languages

Every block below is real output for the query above, taken from the projects scythe's CI compiles on every run. Note where each language expresses "nullable" differently, and how NUMERIC lands on the right native decimal type in each.

Rust (rust-sqlx)

#[derive(Debug, Clone, sqlx::FromRow)]
pub struct GetUserOrdersRow {
    pub id: i32,
    pub name: String,
    pub total: Option<rust_decimal::Decimal>,
    pub notes: Option<String>,
}

TypeScript (typescript-pg)

export interface GetUserOrdersRow {
	id: number;
	name: string;
	total: string | null;
	notes: string | null;
}

Go (go-pgx)

type GetUserOrdersRow struct {
	Id int32 `json:"id"`
	Name string `json:"name"`
	Total *decimal.Decimal `json:"total"`
	Notes *string `json:"notes"`
}

Java (java-jdbc)

public record GetUserOrdersRow(
    int id,
    String name,
    @Nullable java.math.BigDecimal total,
    @Nullable String notes
) {}

Kotlin (kotlin-jdbc)

data class GetUserOrdersRow(
    val id: Int,
    val name: String,
    val total: java.math.BigDecimal?,
    val notes: String?,
)

C# (csharp-npgsql)

public record GetUserOrdersRow(
    int Id,
    string Name,
    decimal? Total,
    string? Notes
);

Elixir (elixir-postgrex)

defmodule GetUserOrdersRow do
  @moduledoc "Row type for GetUserOrders queries."

  @type t :: %__MODULE__{
    id: integer(),
    name: String.t(),
    total: Decimal.t() | nil,
    notes: String.t() | nil
  }
  defstruct [:id, :name, :total, :notes]
end

Ruby (ruby-pg)

GetUserOrdersRow = Data.define(:id, :name, :total, :notes)

PHP (php-pdo)

readonly class GetUserOrdersRow {
    public function __construct(
        public int $id,
        public string $name,
        public ?string $total,
        public ?string $notes,
    ) {}
}

Plain JavaScript is available too: the four javascript-* backends emit ESM .js with the types carried in JSDoc, checkable under tsc --checkJs --strict with no build step.

The quickstart walks the whole flow with full function bodies for every language.

Commands

scythe generate                       # compile SQL to code
scythe check                          # is the committed code still in sync?
scythe lint sql/                      # 23 correctness and performance rules
scythe audit sql/ --format sarif      # 35 security and migration-safety rules
scythe fmt sql/                       # format via sqruff
scythe inspect $DATABASE_URL          # live-database health checks
scythe migrate sqlc.yaml              # convert an sqlc config

check and audit exit 2 on error-severity findings and 1 on operational failure, so CI can tell the two apart. Full flags and exit codes are in the CLI reference.

Language and database support

Ten languages — Rust, Python, TypeScript, Go, Java, Kotlin, C#, Elixir, Ruby and PHP, plus plain JavaScript via the four javascript-* backends — across PostgreSQL, MySQL, MariaDB, SQLite, DuckDB, CockroachDB, MSSQL, Oracle, Redshift and Snowflake. Coverage is not uniform: not every language has a driver for every engine.

Driver matrix
LanguagePostgreSQLMySQLSQLiteDuckDBCockroachDBMSSQLOracleMariaDBRedshiftSnowflake
Rustsqlx, tokio-postgressqlxsqlx--sqlx, tokio-postgrestiberiussibylsqlxsqlx, tokio-postgres--
Pythonpsycopg3, asyncpgaiomysqlaiosqliteduckdbpsycopg3, asyncpgpyodbcoracledbaiomysqlpsycopg3, asyncpgsnowflake-connector
TypeScriptpostgres.js, pg, Kyselymysql2, Kyselybetter-sqlite3, Kysely, node:sqlite, wasm-sqliteduckdb-nodepostgres.js, pg, Kyselymssql, Kyselyoracledbmysql2, Kyselypostgres.js, pg, Kyselysnowflake-sdk
JavaScriptpostgres.js, pgmysql2better-sqlite3--postgres.js, pg----mysql2postgres.js, pg--
Gopgxdatabase/sqldatabase/sqldatabase/sqlpgxdatabase/sqlgodrordatabase/sqlpgxgosnowflake
JavaJDBC, R2DBCJDBC, R2DBCJDBC, R2DBCJDBCJDBC, R2DBCJDBCJDBCJDBC, R2DBCJDBCJDBC
KotlinJDBC, R2DBC, ExposedJDBC, R2DBCJDBC, R2DBCJDBCJDBC, R2DBC, ExposedJDBCJDBCJDBC, R2DBCJDBCJDBC
C#NpgsqlMySqlConnectorMicrosoft.Data.Sqlite--NpgsqlMicrosoft.Data.SqlClientODP.NETMySqlConnectorNpgsqlSnowflake.Data
ElixirPostgrex, EctoMyXQLExqlite--Postgrex, Ectotdsjamdb_oracleMyXQLPostgrex--
Rubypgmysql2, trilogysqlite3--pgtiny_tdsruby-oci8mysql2, trilogypg--
PHPPDO, AMPHPPDO, AMPHPPDO--PDO, AMPHPPDO--PDO, AMPHPPDOPDO

The CockroachDB column matches PostgreSQL by construction: normalize_engine folds cockroachdb into postgresql before a backend's engine support is consulted. Redshift does not fold that way — it needs a per-backend *.redshift.toml manifest, which is why its column is narrower.

See the backend overview for per-backend options and emitted-code notes.

Documentation

Full documentation at goldziher.github.io/scythe.

Contributing

See CONTRIBUTING.md for setup, architecture, and how to add backends, engines or lint rules.

License

MIT