Skip to content

Goldziher/scythe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

146 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Scythe

Write SQL. Generate type-safe code. In any language.


Scythe compiles annotated SQL into type-safe database access code. You write SQL queries, scythe generates the boilerplate -- structs, functions, type mappings -- in 10 languages across 10 databases with 70+ backend drivers. Built-in linting (93 rules) and formatting catch SQL bugs before they ship.

Installation

cargo install scythe-cli
# or
brew install Goldziher/tap/scythe  # uses pre-built binaries for faster install

Pre-commit / prek

Scythe provides pre-commit / prek hooks for SQL formatting and linting:

repos:
  - repo: https://github.com/Goldziher/scythe
    rev: v0.6.0
    hooks:
      - id: scythe-fmt       # Format SQL files
      - id: scythe-lint      # Lint SQL with auto-fix
      - id: scythe-generate  # Regenerate code on SQL changes
      - id: scythe-check     # Validate SQL without generating

See Pre-commit Hooks for all available hooks and configuration options.

Quick Start

1. Write annotated SQL queries:

-- @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. Configure scythe.toml:

[scythe]
version = "1"

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

[[sql.gen]]
backend = "rust-sqlx"

3. Generate code:

scythe generate

4. Use the generated code:

Scythe knows o.total and o.notes are nullable (right side of LEFT JOIN) and generates precise types:

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

pub async fn get_user_orders(
    pool: &sqlx::PgPool, status: &str,
) -> Result<Vec<GetUserOrdersRow>, sqlx::Error> {
    sqlx::query_as!(GetUserOrdersRow,
        "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", status)
        .fetch_all(pool).await
}
Python (psycopg3)
@dataclass
class GetUserOrdersRow:
    id: int
    name: str
    total: decimal.Decimal | None
    notes: str | None

async def get_user_orders(
    conn: AsyncConnection, *, status: str,
) -> list[GetUserOrdersRow]:
    cur = await conn.execute(
        "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 = %(status)s",
        {"status": status},
    )
    rows = await cur.fetchall()
    return [GetUserOrdersRow(id=r[0], name=r[1], total=r[2], notes=r[3]) for r in rows]
TypeScript (pg)
interface GetUserOrdersRow {
    id: number;
    name: string;
    total: string | null;
    notes: string | null;
}

export async function getUserOrders(
    client: PoolClient, status: string,
): Promise<GetUserOrdersRow[]> {
    const { rows } = await client.query<GetUserOrdersRow>(
        `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`, [status]);
    return rows;
}
Go (pgx)
type GetUserOrdersRow struct {
    ID    int32   `json:"id"`
    Name  string  `json:"name"`
    Total *string `json:"total"`
    Notes *string `json:"notes"`
}

func GetUserOrders(ctx context.Context, pool *pgxpool.Pool, status string) ([]GetUserOrdersRow, error) {
    rows, err := pool.Query(ctx,
        "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",
        status)
    // ... scan rows into []GetUserOrdersRow
}
Java (JDBC)
public record GetUserOrdersRow(
    int id,
    String name,
    @Nullable java.math.BigDecimal total,
    @Nullable String notes
) {}

public static List<GetUserOrdersRow> getUserOrders(
    Connection conn, String status
) throws SQLException {
    // PreparedStatement + ResultSet scanning
}
Kotlin (JDBC)
data class GetUserOrdersRow(
    val id: Int,
    val name: String,
    val total: java.math.BigDecimal?,
    val notes: String?,
)

fun getUserOrders(conn: Connection, status: String): List<GetUserOrdersRow> {
    conn.prepareStatement("...").use { ps ->
        ps.setObject(1, status)
        ps.executeQuery().use { rs -> /* scan rows */ }
    }
}
C# (Npgsql)
public record GetUserOrdersRow(
    int Id, string Name, decimal? Total, string? Notes
);

public static async Task<List<GetUserOrdersRow>> GetUserOrders(
    NpgsqlConnection conn, string status
) {
    await using var cmd = new NpgsqlCommand("...", conn);
    cmd.Parameters.AddWithValue("p1", status);
    await using var reader = await cmd.ExecuteReaderAsync();
    // read rows
}
Elixir (Postgrex)
defmodule GetUserOrdersRow do
  @type t :: %__MODULE__{
    id: integer(), name: String.t(),
    total: Decimal.t() | nil, notes: String.t() | nil
  }
  defstruct [:id, :name, :total, :notes]
end

@spec get_user_orders(pid(), String.t()) :: {:ok, [%GetUserOrdersRow{}]} | {:error, term()}
def get_user_orders(conn, status) do
  case Postgrex.query(conn, "...", [status]) do
    {:ok, %{rows: rows}} -> {:ok, Enum.map(rows, &to_struct/1)}
    {:error, err} -> {:error, err}
  end
end
Ruby (pg)
module Queries
  GetUserOrdersRow = Data.define(:id, :name, :total, :notes)

  def self.get_user_orders(conn, status)
    result = conn.exec_params(
      "SELECT u.id, u.name, o.total, o.notes ...", [status])
    result.map do |row|
      GetUserOrdersRow.new(
        id: row["id"].to_i, name: row["name"],
        total: row["total"], notes: row["notes"])
    end
  end
end
PHP (PDO)
readonly class GetUserOrdersRow {
    public function __construct(
        public int $id, public string $name,
        public ?string $total, public ?string $notes,
    ) {}
}

final class Queries {
    public static function getUserOrders(
        \PDO $pdo, string $status
    ): \Generator {
        $stmt = $pdo->prepare("SELECT ...");
        $stmt->execute(["p1" => $status]);
        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
            yield GetUserOrdersRow::fromRow($row);
        }
    }
}

See the full quickstart for complete examples with imports and full function bodies.

Features

  • 10 languages -- Rust, Python, TypeScript, Go, Java, Kotlin, C#, Elixir, Ruby, PHP
  • 10 databases -- PostgreSQL, MySQL, SQLite, DuckDB, CockroachDB, MSSQL, Oracle, MariaDB, Redshift, Snowflake
  • 70+ backend drivers -- sqlx, tokio-postgres, psycopg3, asyncpg, pg, postgres.js, pgx, JDBC, R2DBC, Exposed, Npgsql, PDO, tiberius, oracledb, pyodbc, and more
  • 93 lint rules -- UPDATE without WHERE, SELECT *, NULL comparisons, leading wildcard LIKE, plus 71 sqruff rules
  • SQL formatting -- consistent style via sqruff integration
  • Smart type inference -- nullability from JOINs, COALESCE, window functions, CASE WHEN, aggregates
  • @optional parameters -- SQL rewriting for conditional filters (WHERE ($1 IS NULL OR col = $1))
  • :batch execution -- bulk inserts and batch operations
  • @returns :grouped -- result grouping with @group_by for grouped query results
  • R2DBC reactive backends -- non-blocking database access for Java and Kotlin
  • Kotlin Exposed -- first-class Exposed ORM backend for Kotlin
  • Configurable row types -- Pydantic, msgspec, Zod, dataclass, interface per backend
  • CTEs and window functions -- ROW_NUMBER, RANK, LAG, LEAD, recursive CTEs with correct type inference
  • Enums, composites, arrays -- PostgreSQL types mapped to language-native equivalents
  • Custom type overrides -- map ltree, citext, PostGIS geometry to any target type

Supported Languages

Language PostgreSQL MySQL SQLite DuckDB CockroachDB MSSQL Oracle MariaDB Redshift Snowflake
Rust sqlx, tokio-postgres sqlx sqlx -- sqlx tiberius sibyl sqlx sqlx --
Python psycopg3, asyncpg aiomysql aiosqlite python-duckdb psycopg3 pyodbc oracledb aiomysql psycopg3 snowflake-connector
TypeScript pg, postgres.js mysql2 better-sqlite3 typescript-duckdb pg mssql oracledb mysql2 pg snowflake-sdk
Go pgx database/sql database/sql database/sql pgx go-mssqldb godror database/sql pgx gosnowflake
Java JDBC, R2DBC JDBC JDBC JDBC JDBC JDBC, R2DBC JDBC, R2DBC JDBC JDBC JDBC
Kotlin JDBC, R2DBC, Exposed JDBC JDBC JDBC JDBC JDBC, R2DBC JDBC, R2DBC JDBC JDBC JDBC
C# Npgsql MySqlConnector Microsoft.Data.Sqlite -- Npgsql Microsoft.Data.SqlClient ODP.NET MySqlConnector Npgsql Snowflake.Data
Ruby pg, Trilogy mysql2, Trilogy sqlite3 -- pg tiny_tds ruby-oci8 mysql2 pg --
PHP PDO, AMPHP PDO PDO -- PDO PDO PDO PDO PDO PDO
Elixir Postgrex, Ecto MyXQL Exqlite -- Postgrex tds jamdb_oracle MyXQL Postgrex --

Documentation

Full documentation at goldziher.github.io/scythe:

  • Quickstart -- zero to generated code in 5 minutes
  • Philosophy -- why compile SQL instead of using an ORM
  • Alternatives -- how scythe compares to sqlc, SQLDelight, jOOQ, and ORMs
  • Custom Types -- type overrides for PostgreSQL extensions
  • Configuration -- full scythe.toml reference
  • Annotations -- @name, @returns, @optional, @nullable, @json, and more
  • Lint Rules -- all 93 rules with codes and examples

Contributing

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

License

MIT

About

Polyglot SQL compiler and linter.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors