Annotations
Scythe uses SQL comment annotations to control code generation. All annotations start with -- @.
@name (required)
Section titled “@name (required)”Names the query. Used as the generated function and struct name.
-- @name GetUserById-- @returns :oneSELECT id, name FROM users WHERE id = $1;Generates: get_user_by_id() function and GetUserByIdRow struct.
@returns (required)
Section titled “@returns (required)”Specifies the query return type. Must include a colon prefix.
| Value | Description | Use Case |
|---|---|---|
:one |
Returns exactly one row (errors if missing) | SELECT … WHERE id = $1 |
:opt |
Returns zero or one row (nullable/optional) | SELECT … WHERE email = $1 |
:many |
Returns multiple rows | SELECT … WHERE status = $1 |
:exec |
Returns nothing | INSERT, UPDATE, DELETE without RETURNING |
:exec_result |
Returns affected row count | UPDATE/DELETE when you need the count |
:exec_rows |
Returns affected rows | Similar to exec_result |
:batch |
Batch execution | Bulk inserts |
:grouped |
Returns rows grouped by a key | JOIN queries with parent-child nesting |
-- @name ListActiveUsers-- @returns :manySELECT id, name, email FROM users WHERE status = 'active';
-- @name DeleteUser-- @returns :execDELETE FROM users WHERE id = $1;@group_by
Section titled “@group_by”Specifies which table’s columns become the parent struct when using @returns :grouped. All other selected columns become children collected into a nested list.
Format: -- @group_by table.column
This annotation is required when @returns :grouped is used and produces an error if omitted.
-- @name GetUsersWithOrders-- @returns :grouped-- @group_by users.idSELECT u.id, u.name, u.email, o.id AS order_id, o.total, o.created_at AS order_dateFROM users uJOIN orders o ON o.user_id = u.idWHERE u.status = 'active';This generates a parent struct containing the users columns (id, name, email) with a nested collection of child structs containing the orders columns (order_id, total, order_date). The exact shape depends on the backend language – for example, Rust generates a Vec<ChildRow> field, Python generates a list[ChildRow] field, and so on.
@optional
Section titled “@optional”Marks a query parameter as optional. Scythe rewrites the SQL at generation time so that passing NULL for the parameter skips the filter condition entirely.
-- @name ListUsers-- @returns :many-- @optional statusSELECT id, name, email FROM users WHERE status = $1;Scythe rewrites WHERE status = $1 into WHERE ($1 IS NULL OR status = $1). At runtime, passing NULL returns all rows; passing a value filters normally.
Supported operators
Section titled “Supported operators”@optional works with the following comparison operators:
| Operator | Rewritten form |
|---|---|
= |
($1 IS NULL OR col = $1) |
<> |
($1 IS NULL OR col <> $1) |
!= |
($1 IS NULL OR col != $1) |
> |
($1 IS NULL OR col > $1) |
< |
($1 IS NULL OR col < $1) |
>= |
($1 IS NULL OR col >= $1) |
<= |
($1 IS NULL OR col <= $1) |
LIKE |
($1 IS NULL OR col LIKE $1) |
ILIKE |
($1 IS NULL OR col ILIKE $1) |
Multiple optional parameters
Section titled “Multiple optional parameters”-- @name SearchUsers-- @returns :many-- @optional status-- @optional name_patternSELECT id, name, email FROM usersWHERE status = $1 AND name ILIKE $2;Parameter name validation
Section titled “Parameter name validation”Parameter names in @optional are validated against the query. If the name does not match any parameter, scythe produces an error – catching typos at generation time rather than at runtime.
@param
Section titled “@param”Documents a query parameter. Does not affect code generation, but adds documentation to generated code.
-- @name GetUser-- @returns :one-- @param id: the user's unique identifier-- @param status: filter by account statusSELECT id, name FROM users WHERE id = $1 AND status = $2;Format: -- @param <name>: <description> or -- @param <name> (without description).
Positional form
Section titled “Positional form”A second, distinct @param form binds a 1-based position to a name and, unlike the docs-only form
above, changes generated code: it renames the generated parameter from the pN fallback the analyzer
would otherwise infer.
-- @name GetUser-- @returns :one-- @param $1 user_id: the user's unique identifierSELECT id, name FROM users WHERE id = $1;Format: -- @param $N name[: description]. The $N token must come first; anything else after
@param is parsed as the docs-only form.
@nullable
Section titled “@nullable”Forces specific columns to be nullable in generated code, overriding the inferred nullability.
-- @name GetUserProfile-- @returns :one-- @nullable bio, avatar_urlSELECT id, name, bio, avatar_url FROM users WHERE id = $1;Accepts a comma-separated list of column names.
@nonnull
Section titled “@nonnull”Forces specific columns to be non-nullable in generated code, overriding the inferred nullability.
-- @name GetUserWithDefaults-- @returns :one-- @nonnull statusSELECT id, name, COALESCE(status, 'active') AS status FROM users WHERE id = $1;Useful when you know a column cannot be null due to application logic that the analyzer cannot infer.
Maps a column to a typed JSON struct instead of a generic JSON value.
-- @name GetEvent-- @returns :one-- @json data = EventDataSELECT id, data FROM events WHERE id = $1;Format: -- @json <column> = <TypeName>. The generated code will use Json<EventData> (Rust) or equivalent typed wrapper instead of a raw JSON value.
@deprecated
Section titled “@deprecated”Marks a query as deprecated. The generated code will include deprecation annotations in languages that support them.
-- @name GetUserV1-- @returns :one-- @deprecated Use GetUserV2 insteadSELECT id, name FROM users WHERE id = $1;In Rust, this generates #[deprecated(note = "Use GetUserV2 instead")] on the function.
Complete Example
Section titled “Complete Example”-- @name GetOrderDetails-- @returns :one-- @param order_id: the order to look up-- @optional status-- @nullable discount_code-- @nonnull total-- @json metadata = OrderMetadata-- @deprecated Use GetOrderDetailsV2 for new codeSELECT o.id, o.total, o.discount_code, o.metadata, u.name AS customer_nameFROM orders oJOIN users u ON o.user_id = u.idWHERE o.id = $1;Unrecognized Annotations
Section titled “Unrecognized Annotations”An annotation scythe does not recognize (-- @foo bar) is not a parse error. It is captured
verbatim as a custom annotation and exposed to crate consumers who want to layer their own
annotation vocabulary on top of scythe. This means a typo in a known annotation name (@nullible
instead of @nullable) is silently accepted rather than rejected – double-check spelling manually.
Case Insensitivity
Section titled “Case Insensitivity”Annotation keywords are case-insensitive. These are equivalent:
-- @name GetUser-- @Name GetUser-- @NAME GetUserThe annotation value (e.g., the query name) preserves its original casing.