Overview
1
Create a schema

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)
);
2
Write annotated queries

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;
3
Create scythe.toml

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.

4
Generate code
scythe generate

Scythe parses the schema, analyzes the queries, runs type inference, and writes generated code to the configured output directory.

5
Review the generated code

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 None

Every other backend follows the same shape: a typed row struct or class, and a function whose parameters and return type match your SQL exactly.

6
Validate and lint
scythe check   # validates SQL parsing and type resolution
scythe lint    # checks for correctness, performance, and style issues

What 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#

Updated

Was this page helpful?