Configuration
Scythe is configured via scythe.toml in your project root.
Relative paths in scythe.toml — schema, queries glob patterns, and
output directories — resolve relative to the directory containing
scythe.toml, not the current working directory the CLI is invoked from.
Running scythe generate --config /path/to/project/scythe.toml from any
directory behaves identically to running cd /path/to/project && scythe generate. A glob pattern that matches no files is a hard error; if you hit
one unexpectedly after upgrading, check that the pattern is written relative
to the config file, not to your shell’s current directory. Absolute paths and
patterns are always used as-is.
Full Reference
Section titled “Full Reference”# Required: scythe metadata[scythe]version = "1"
# One or more SQL blocks. Each block defines a schema + queries + output target.[[sql]]name = "main" # Block name (used in CLI output)engine = "postgresql" # Database engine: postgresql, mysql, sqliteschema = ["sql/schema/*.sql"] # Glob patterns for DDL filesqueries = ["sql/queries/*.sql"] # Glob patterns for annotated query filesoutput = "src/generated" # Output directory for generated code
# Optional: code generation settings[sql.gen.rust]target = "sqlx" # Backend target (e.g. sqlx, tokio-postgres)derive = ["Debug", "Clone", "serde::Serialize"] # Extra derive macros on structsserde = true # Add serde derives
# Optional: type overrides[[sql.type_overrides]]column = "users.metadata" # Specific column to overridetype = "json" # Neutral type to use
[[sql.type_overrides]]db_type = "uuid" # Override all columns whose *neutral* type is thistype = "string" # Neutral type to map to
# Optional: lint configuration[lint]
# Set severity by category (naming, safety, style, performance, antipattern, codegen,# security, migration, provenance, drift)[lint.categories]safety = "error"naming = "warn"performance = "warn"
# Override severity for individual rules[lint.rules]"SC-S03" = "off" # Disable SELECT * warning"SC-N03" = "error" # Promote query naming to errorFields
Section titled “Fields”[scythe]
Section titled “[scythe]”| Field | Type | Required | Description |
|---|---|---|---|
version |
string | yes | Config version. Currently "1". |
[[sql]]
Section titled “[[sql]]”| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Name for this SQL block. |
engine |
string | yes | Database dialect: postgresql, mysql, sqlite, duckdb, cockroachdb, mssql, oracle, mariadb, redshift, snowflake. |
schema |
string[] | yes | Glob patterns for schema DDL files. Relative patterns resolve against the config file’s directory. |
queries |
string[] | yes | Glob patterns for annotated query files. Relative patterns resolve against the config file’s directory. |
output |
string | no | Legacy. Output directory used only as the default for the legacy [sql.gen.<lang>] table form below. Ignored by the [[sql.gen]] array form – each array target needs its own output key. Defaults to "generated" when gen is absent entirely. A relative path resolves against the config file’s directory. |
gen |
table | no | Code generation options per language. |
type_overrides |
array | no | Type mapping overrides. |
[[sql.gen]] (recommended for v0.2.0+)
Section titled “[[sql.gen]] (recommended for v0.2.0+)”The new array syntax allows generating code for multiple backends from a single SQL block:
[[sql]]name = "main"engine = "postgresql"schema = ["sql/schema/*.sql"]queries = ["sql/queries/*.sql"]
[[sql.gen]]backend = "rust-sqlx"output = "src/generated/rust"
[[sql.gen]]backend = "typescript-pg"output = "src/generated/ts"
[[sql.gen]]backend = "python-duckdb"output = "src/generated/duckdb"
[[sql.gen]]backend = "java-r2dbc"output = "src/generated/java-r2dbc"
[[sql.gen]]backend = "kotlin-exposed"output = "src/generated/kotlin-exposed"| Field | Type | Required | Description |
|---|---|---|---|
backend |
string | yes | Full backend name (e.g. rust-sqlx, typescript-pg, python-aiomysql). |
output |
string | yes | Output directory for this backend’s generated code. A relative path resolves against the config file’s directory. |
manifest |
string | no | Path to a partial manifest merged over the backend’s built-in one. A relative path resolves against the config file’s directory. See below. |
row_type |
string | no | Row type style for generated code. See below. |
outer_join_unions |
bool | no | Emit outer-join nullability as a discriminated union. TypeScript backends only. See below. |
structs_only |
bool | no | Suppress generated query functions, emitting only row/struct types. TypeScript backends and rust-sqlx. |
field_case |
string | no | Case convention for generated column/param field names. TypeScript and Java/Kotlin backends. See below. |
namespace |
string | no | PHP namespace for generated code. PHP backends only. See below. |
extension_functions |
bool | no | Generate idiomatic Kotlin extension functions. Kotlin backends only. See below. |
serde |
bool | no | Add serde::Serialize, serde::Deserialize to every generated struct and enum derive list. rust-sqlx, rust-tokio-postgres, rust-tiberius, rust-sibyl only. |
derive |
string | no | Comma-separated list of additional derive macros to append (e.g. derive = "PartialEq, Hash"). rust-sqlx, rust-tokio-postgres, rust-tiberius, rust-sibyl only. |
row_type, outer_join_unions, structs_only, and field_case are the only options every
TypeScript backend accepts (typescript-pg, typescript-postgres, typescript-kysely,
typescript-mysql2, typescript-mssql, typescript-oracledb, typescript-snowflake,
typescript-duckdb, typescript-better-sqlite3, typescript-node-sqlite,
typescript-wasm-sqlite); namespace and extension_functions are not on that list. Setting either
on a TypeScript target fails with the unknown option error shown below — see the caution box.
The four javascript-* names (javascript-pg, javascript-postgres, javascript-mysql2,
javascript-better-sqlite3) accept the same four keys, but reject three of their values outright:
row_type = "zod", outer_join_unions = true, and field_case = "camelCase" each need
TypeScript-only syntax a plain .js file cannot carry, so generation aborts with an error naming the
TypeScript backend to use instead. See
JavaScript output (JSDoc).
row_type
Section titled “row_type”Controls what data structure is used for generated row types. Available options depend on the backend language:
Python backends:
| Value | Description |
|---|---|
"dataclass" |
(default) Standard library @dataclass |
"pydantic" |
Pydantic BaseModel with validation |
"msgspec" |
msgspec Struct for high-performance serialization |
[[sql.gen]]backend = "python-psycopg3"output = "src/generated"row_type = "pydantic"TypeScript backends:
| Value | Description |
|---|---|
"interface" |
(default) TypeScript interface |
"zod" |
Zod schema with inferred types |
[[sql.gen]]backend = "typescript-pg"output = "src/generated"row_type = "zod"Other languages use their standard row type and do not currently support row_type configuration.
outer_join_unions
Section titled “outer_join_unions”TypeScript backends only. Off by default.
For an outer join, scythe emits per-column optionality by default. That is sound but imprecise: it admits rows the query can never produce.
Given orders.total NOT NULL and orders.notes nullable:
-- @name GetUserOrders-- @returns :manySELECT u.id, u.name, o.total, o.notesFROM users uLEFT JOIN orders o ON u.id = o.user_idWHERE u.status = $1;the default shape allows { total: null, notes: "gift" }, which is
unreachable — total is null exactly when no order matched, and then notes
is null too.
[[sql.gen]]backend = "typescript-pg"output = "src/generated"outer_join_unions = trueEvery column projected from the outer-joined relation shares one match-bit, so they are grouped into a union:
export type GetUserOrdersRow = { id: number; name: string;} & ( | { total: string; notes: string | null } | { total: null; notes: null });The union is only emitted when the joined relation projects at least one
NOT NULL column — that column is the discriminant. Without one, every column
was independently nullable anyway and the flat shape is already exact, so
scythe keeps it. Two independently outer-joined relations each get their own
alternative.
Per-column optionality remains the default and the cross-target shape: Go, Java, C# and PHP cannot express this cleanly.
field_case
Section titled “field_case”Accepted by 16 backends: the 11 TypeScript backends listed above, plus java-jdbc, java-r2dbc,
kotlin-jdbc, kotlin-r2dbc, and kotlin-exposed. Controls the case of generated
row/interface/record/data-class field names and function parameter names — the SQL side (column
names, bound parameter order) is unaffected.
| Value | Description |
|---|---|
"snake_case" |
(default) Field names mirror SQL column/param names as-is |
"camelCase" |
Field names are converted to camelCase |
Those two values are the whole vocabulary; anything else fails scythe generate with an error naming
the backend and the rejected value.
[[sql.gen]]backend = "typescript-pg"output = "src/generated"field_case = "camelCase"-- @name GetUser-- @returns :oneSELECT id, user_name, created_at FROM users WHERE id = $1;export interface GetUserRow { id: number; userName: string; createdAt: Date;}On the ten driver-backed TypeScript backends, renaming a field is not just a label change: the driver
still returns rows keyed by the original SQL column name, so field_case = "camelCase" also switches
the function body from a blind cast of the driver’s row to a field-by-field reconstruction
(row['user_name'] read into a userName property). Without that remap the generated code would
still type-check under tsc but every field would read back undefined at runtime. The Java/Kotlin
backends need no such remap — each column is read by an explicit getter keyed by the raw SQL column
name, so only the declared field name changes. See
Java and
Kotlin.
The four javascript-* backends accept field_case = "snake_case" but reject "camelCase": the
remap needs a TypeScript as T assertion that a plain .js file cannot carry. The error points at
the matching typescript-* backend.
Two SQL identifiers that collapse onto the same generated name under the active field_case are a
hard error, not a silent last-write-wins — this also applies under the default snake_case, since
quoted SQL (SELECT "USER_ID", user_id FROM t) can produce two distinct column names that resolve
to one field:
error: columns 'user_id' and 'userId' both resolve to field name 'userId' under field_case = "camelCase" -- alias one of them in SQL, or set field_case = "snake_case"field_case is a backend option, not a manifest field: a manifest’s [naming] table cannot set it
(see manifest below), so a partial manifest override cannot silently disable it.
namespace
Section titled “namespace”Controls the PHP namespace declaration emitted at the top of every generated file. Applies to php-pdo and php-amphp backends.
| Value | Description |
|---|---|
"App\\Generated" |
(default) |
| any valid PHP namespace | Emits namespace <value>; |
"" (empty string) |
Omits the namespace declaration entirely |
[[sql.gen]]backend = "php-pdo"output = "src/generated"namespace = "App\\Database\\Generated"Set namespace = "" for scripts or frameworks that do not use namespaces.
extension_functions
Section titled “extension_functions”Generates query functions as idiomatic Kotlin extension functions on the connection receiver, instead of taking the connection as the first parameter. Applies to kotlin-jdbc and kotlin-r2dbc. Default false (non-breaking).
| Value | Description |
|---|---|
false |
(default) fun getUser(conn: Connection, id: Int): UserRow? |
true |
fun Connection.getUser(id: Int): UserRow?, called as connection.getUser(id) |
[[sql.gen]]backend = "kotlin-jdbc"output = "src/generated"extension_functions = trueWhen enabled, value-returning functions use expression bodies, and kotlin-r2dbc becomes a suspend extension on io.r2dbc.spi.Connection (the caller owns the connection lifecycle).
manifest
Section titled “manifest”Each backend ships with a built-in manifest holding its type mappings, naming conventions, and import rules. manifest points at a partial manifest that is merged over it, so you can retarget a few mappings without restating the rest.
[[sql.gen]]backend = "rust-sqlx"output = "src/db"manifest = "manifests/rust-sqlx-custom.toml"[types.scalars]decimal = "bigdecimal::BigDecimal"
[imports.rules]"bigdecimal::" = "use bigdecimal::BigDecimal;"The path resolves against the directory containing scythe.toml, not the directory you run scythe from — the same rule every other path in the config follows. Generated output is therefore identical no matter where the command is invoked.
The override is per target. A backend name alone does not identify a manifest: rust-sqlx covers five engines and java-jdbc nine, and each engine has its own type mappings. Because manifest sits on a [[sql.gen]] target, it inherits that target’s engine from the enclosing [[sql]] block, and two targets naming the same backend under different engines each get their own override.
Merge rules
Section titled “Merge rules”| Section | Granularity | New keys |
|---|---|---|
[types.scalars] |
per key | rejected |
[types.containers] |
per key | rejected |
[types.docblock_containers] |
per key | rejected |
[imports.rules] |
per key | allowed |
[naming] |
per field, whole value | rejected |
Map-valued tables merge one key at a time: a key you list replaces exactly that entry, and every key you omit keeps its built-in value. [naming] fields replace whole values; omitted fields inherit.
[naming] accepts four fields — struct_case, fn_case, enum_variant_case and row_suffix. The list is an allowlist rather than a mirror of the manifest, so any other naming field is a parse error, not a silent no-op.
[types.scalars], [types.containers] and [types.docblock_containers] are replace-only. Neutral type names (int32, datetime_tz, array, …) are a fixed vocabulary, so a key outside it is a typo — and a silently accepted typo would leave the original mapping in place and generate code you did not ask for. [imports.rules] does accept new keys, because its keys are prefixes of the generated language types, which necessarily change when you retarget a scalar.
[types.docblock_containers] maps the same container names as [types.containers], but is used only where the target language accepts a type in a comment rather than in a native type position — a PHPStan docblock today. A container it omits falls back to [types.containers], so only the PHP backends declare it: array maps to a bare array natively, because public array<string> $tags is a PHP parse error, and to array<{T}> in the docblock, because a bare array is array<mixed, mixed> at PHPStan level 9. Its accepted keys are the container vocabulary, not just the ones the backend already overrides for docblocks.
There is no [backend] section. name, language, file_extension, and engine are identity, not configuration.
manifest is read by scythe generate and exists only on the [[sql.gen]] array form; the legacy [sql.gen.rust] syntax below has no equivalent key.
Errors
Section titled “Errors”Every problem fails scythe generate and names the backend, the resolved absolute path, and the offending key. Nothing falls back to the built-in manifest silently.
error: backend 'rust-sqlx': invalid manifest override '/repo/manifests/rust-sqlx-custom.toml': manifest error: unknown [types.scalars] key 'int_64' (did you mean 'int64'?); this table may only override mappings the backend already definesA missing file is an error, not a fallback:
error: backend 'rust-sqlx': failed to read manifest override '/repo/manifests/nope.toml': No such file or directory (os error 2)[sql.gen.rust], [sql.gen.python], [sql.gen.typescript], [sql.gen.go], [sql.gen.kotlin] (legacy)
Section titled “[sql.gen.rust], [sql.gen.python], [sql.gen.typescript], [sql.gen.go], [sql.gen.kotlin] (legacy)”The legacy syntax is still supported but limited to a single backend per language. Each table takes a
target string that resolves to a full backend name; [sql.gen.rust] additionally accepts derive
and serde.
[sql.gen.rust]target = "sqlx" # -> rust-sqlxderive = ["Debug", "Clone", "serde::Serialize"]serde = true
[sql.gen.python]target = "psycopg3" # -> python-psycopg3
[sql.gen.typescript]target = "pg" # -> typescript-pg
[sql.gen.go]target = "pgx" # -> go-pgx
[sql.gen.kotlin]target = "jdbc" # -> kotlin-jdbc| Table | Field | Type | Required | Accepted target values |
|---|---|---|---|---|
[sql.gen.rust] |
target |
string | yes | sqlx, tokio-postgres, tiberius, sibyl |
[sql.gen.rust] |
derive |
string[] | no | Additional derive macros for generated structs. |
[sql.gen.rust] |
serde |
bool | no | Add serde Serialize/Deserialize derives. |
[sql.gen.python] |
target |
string | yes | psycopg3, asyncpg, aiomysql, aiosqlite, duckdb, pyodbc, oracledb, snowflake |
[sql.gen.typescript] |
target |
string | yes | pg, postgres, mysql2, better-sqlite3, node-sqlite, duckdb, wasm-sqlite, kysely, mssql, oracledb, snowflake |
[sql.gen.go] |
target |
string | yes | pgx, database-sql, godror, gosnowflake |
[sql.gen.kotlin] |
target |
string | yes | jdbc, exposed, r2dbc |
target resolves to a backend name by prefixing the language (e.g. [sql.gen.python] target = "psycopg3" resolves to the python-psycopg3 backend). derive and serde are Rust-only; the other
four tables accept only target. Anything beyond a single backend per language, or backend options
like row_type and field_case, requires the [[sql.gen]] array form above.
[[sql.type_overrides]]
Section titled “[[sql.type_overrides]]”| Field | Type | Description |
|---|---|---|
column |
string | Target a specific column (table.column). If both column and db_type are set on the same entry, column wins silently — nothing rejects setting both. |
db_type |
string | Target all columns whose neutral type matches this value (see Custom Types for what “neutral type” means here). |
type |
string | Neutral type to use (e.g. string, json, int64). |
[lint]
Section titled “[lint]”See Linting for the full list of rules and categories.
[lint.sqruff]
Section titled “[lint.sqruff]”Controls the sqruff style-rule integration used by scythe lint (not scythe fmt, which always runs
sqruff’s default rule set and ignores this table entirely – except for LT01, which is excluded
under scythe fmt too; see Formatting).
[lint.sqruff]enabled = true # set to false to skip sqruff entirely under `scythe lint`
[lint.sqruff.rules]"LT01" = "off" # bare sqruff codes, not the SQ- prefix scythe uses in output| Field | Type | Required | Description |
|---|---|---|---|
enabled |
bool | no | When false, scythe lint skips sqruff entirely (no sqruff findings). Does not affect scythe fmt, which never reads [lint.sqruff]. |
rules |
table | no | Per-rule status keyed by bare sqruff code (e.g. "LT01"). Only "off" is supported – sqruff has no per-rule severity, so any other value is rejected as a config error, as is an unrecognized rule code. A rejected table fails the entire scythe lint run, scythe’s own rules included. Inert when enabled = false. See below. |
See Linting for details.
[inspect]
Section titled “[inspect]”Configures scythe inspect, the live-database health-check command. See the
Inspect guide for the full
field reference, including [inspect.severity_overrides], [[inspect.suppression]], and
[[inspect.check]] for user-defined checks (IDs must be prefixed USER-INS-).
[inspect]database_url = "postgres://localhost/dev"api_schemas = ["public", "api"]extra_rules = ["./inspect-rules.toml"][audit]
Section titled “[audit]”Configures scythe audit’s user-defined rules. See the Audit guide
for the full field reference and available matchers. Custom rule IDs must be prefixed USER-.
[audit]extra_rules = ["./security_rules.toml"]
[[audit.rule]]id = "USER-001"name = "no-debug-functions"severity = "error"description = "calls to debug-only functions should not ship"message = "call to debug function `{func}` — remove before merging"matcher = "function_name_in_set"
[audit.rule.matcher_args]functions = ["dump_internal_state", "debug_print"]Multiple SQL Blocks
Section titled “Multiple SQL Blocks”You can define multiple [[sql]] blocks for different databases or schemas:
[scythe]version = "1"
[[sql]]name = "users"engine = "postgresql"schema = ["sql/users/schema.sql"]queries = ["sql/users/queries/*.sql"]output = "src/generated/users"
[[sql]]name = "analytics"engine = "postgresql"schema = ["sql/analytics/schema.sql"]queries = ["sql/analytics/queries/*.sql"]output = "src/generated/analytics"Engine Aliases
Section titled “Engine Aliases”Aliases resolve to one of six SqlDialect variants (crates/scythe-core/src/dialect.rs).
duckdb, redshift, cockroachdb, and crdb are not separate dialects — they alias directly to
the PostgreSQL dialect and get identical parsing and type resolution.
| Alias | Resolves to |
|---|---|
postgresql, postgres, pg, cockroachdb, crdb, duckdb, redshift |
PostgreSQL |
mysql, mariadb |
MySQL |
sqlite, sqlite3 |
SQLite |
mssql, sqlserver, tsql |
MsSql |
oracle |
Oracle |
snowflake |
Snowflake |