Create sql/schema.sql:
CREATE TYPE user_status AS ENUM ('active', 'inactive', 'banned');
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
status user_status NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
total NUMERIC NOT NULL,
weight_kg NUMERIC,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE user_tags (
user_id INTEGER NOT NULL REFERENCES users(id),
tag_id INTEGER NOT NULL REFERENCES tags(id),
PRIMARY KEY (user_id, tag_id)
);Create sql/queries.sql:
-- @name GetUserById
-- @returns :one
SELECT * FROM users WHERE id = $1;
-- @name ListActiveUsers
-- @returns :many
SELECT * FROM users WHERE status = 'active';
-- @name UpdateUserEmail
-- @returns :exec
UPDATE users SET email = $2 WHERE id = $1;Pick the backend for your language. This example targets Rust with sqlx:
[scythe]
version = "1"
[[sql]]
name = "main"
engine = "postgresql"
schema = ["sql/schema.sql"]
queries = ["sql/queries.sql"]
[[sql.gen]]
backend = "rust-sqlx"
output = "src/generated"The same schema and queries work unmodified against python-psycopg3, typescript-pg, go-pgx, java-jdbc, kotlin-jdbc, csharp-npgsql, elixir-postgrex, ruby-pg, or php-pdo — just change backend. Some backends accept an optional row_type (Python: pydantic or msgspec; TypeScript: zod). See Configuration for the full option list.
scythe generateScythe parses the schema, analyzes the queries, runs type inference, and writes generated code to the configured output directory.
Rust (rust-sqlx):
#[derive(sqlx::FromRow)]
pub struct User {
pub id: i32,
pub name: String,
pub email: String,
pub status: UserStatus,
pub created_at: chrono::DateTime<chrono::Utc>,
}
pub async fn get_user_by_id(pool: &sqlx::PgPool, id: i32) -> Result<Option<User>, sqlx::Error> {
sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_optional(pool)
.await
}Python (python-psycopg3):
@dataclass(frozen=True, slots=True)
async def get_user_by_id(conn: AsyncConnection, *, id: int) -> User | None:
cur = await conn.execute("SELECT * FROM users WHERE id = %(id)s", {"id": id})
row = await cur.fetchone()
return User(*row) if row else NoneEvery other backend follows the same shape: a typed row struct or class, and a function whose parameters and return type match your SQL exactly.
scythe check # validates SQL parsing and type resolution
scythe lint # checks for correctness, performance, and style issuesWhat You Just Did#
You compiled a schema and three queries into fully typed code with zero hand-written glue. The generated functions match your SQL's parameter and result shapes exactly, including nullability inferred from LEFT JOINs and constraints.
Next Steps#
- Configuration reference — every
scythe.tomloption, including multi-backend output. - Annotations and type inference — how
@nullable,@nonnull, and JOIN inference work together. - Linting — the 59 rules
scythe lintruns by default.