Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Latest commit

 

History

744 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 does Docs
Type inference that reads the query Nullability 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
59 built-in rules 24 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 audit Security 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 check Verifies committed code still matches the SQL it was generated from, via 11 provenance rules. Given --database-url it adds 7 schema-drift rules against a live PostgreSQL catalog. CLI reference
scythe inspect Live-database health checks: foreign keys without covering indexes, tables with policies but RLS disabled, duplicate indexes. PostgreSQL (13 checks) and MySQL/MariaDB (4 checks). Inspect
Output you can shape Row 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

Install scythe in a GitHub Actions workflow with the moving major-version action:

- name: Install scythe
  id: scythe
  uses: Goldziher/scythe@v0
- run: scythe --version

v0 tracks the latest compatible action. Pin the downloaded CLI independently with version: 0.18.1, or set cache: false to disable the version-and-platform cache. The action verifies the release SHA-256 checksum, adds scythe to PATH, and exposes version, target, and install-dir outputs.

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

Pre-commit / prek hooks
repos:
  - repo: https://github.com/Goldziher/scythe
    rev: v0.18.1
    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 a configured 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, native Poly producer setup, 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
Language PostgreSQL MySQL SQLite DuckDB CockroachDB MSSQL Oracle MariaDB Redshift Snowflake
Rust sqlx, tokio-postgres sqlx sqlx -- sqlx, tokio-postgres tiberius sibyl sqlx sqlx, tokio-postgres --
Python psycopg3, asyncpg aiomysql aiosqlite duckdb psycopg3, asyncpg pyodbc oracledb aiomysql psycopg3, asyncpg snowflake-connector
TypeScript postgres.js, pg, Kysely mysql2, Kysely better-sqlite3, Kysely, node:sqlite, wasm-sqlite duckdb-node postgres.js, pg, Kysely mssql, Kysely oracledb mysql2, Kysely postgres.js, pg, Kysely snowflake-sdk
JavaScript postgres.js, pg mysql2 better-sqlite3 -- postgres.js, pg -- -- mysql2 postgres.js, pg --
Go pgx database/sql database/sql database/sql pgx database/sql godror database/sql pgx gosnowflake
Java JDBC, R2DBC JDBC, R2DBC JDBC, R2DBC JDBC JDBC, R2DBC JDBC JDBC JDBC, R2DBC JDBC JDBC
Kotlin JDBC, R2DBC, Exposed JDBC, R2DBC JDBC, R2DBC JDBC JDBC, R2DBC, Exposed JDBC JDBC JDBC, R2DBC JDBC JDBC
C# Npgsql MySqlConnector Microsoft.Data.Sqlite -- Npgsql Microsoft.Data.SqlClient ODP.NET MySqlConnector Npgsql Snowflake.Data
Elixir Postgrex, Ecto MyXQL Exqlite -- Postgrex, Ecto tds jamdb_oracle MyXQL Postgrex --
Ruby pg mysql2, trilogy sqlite3 -- pg tiny_tds ruby-oci8 mysql2, trilogy pg --
PHP PDO, AMPHP PDO, AMPHP PDO -- PDO, AMPHP PDO -- PDO, AMPHP PDO PDO

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

About

SQL as a first-class language: formatting, linting, security auditing and pre-commit hooks for your .sql files, plus type-safe code generation for 10 languages across 10 databases.

Topics

Resources

Contributing

Stars

71 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages