Scythe vs jOOQ¶
jOOQ and scythe share the same core belief: SQL should not be hidden behind an ORM. Both tools generate type-safe code from SQL. They differ in how SQL is authored, when it runs, and what ecosystems they support.
Feature Comparison¶
| Feature | jOOQ | Scythe |
|---|---|---|
| SQL authoring | Java/Kotlin DSL | Plain .sql files |
| SQL execution | Built at runtime via DSL | Compiled at build time, static in generated code |
| Schema source | Live database connection (JDBC) | .sql schema files (no running DB required) |
| Type inference | From database metadata at codegen time | Static analysis of SQL files |
| Language targets | Java, Kotlin | Rust, Python, TypeScript, Go, Java, Kotlin, C#, Elixir, Ruby, PHP |
| SQL linting | None | 93 rules (correctness, performance, style) |
| SQL formatting | None | Integrated via sqruff |
| Dynamic queries | DSL supports runtime query composition | Static queries only (defined at compile time) |
| PostgreSQL | Open source | MIT |
| MySQL | Open source | MIT |
| SQLite | Open source | MIT |
| Oracle | Commercial license required | MIT |
| SQL Server | Commercial license required | MIT |
| DB2 | Commercial license required | MIT |
| Runtime dependencies | jOOQ runtime library | Database driver only |
| Build requirements | JVM + running database | Scythe binary only |
Code Comparison¶
jOOQ: Java DSL¶
// Schema codegen requires a running database
// $ java -cp jooq-codegen.jar org.jooq.codegen.GenerationTool config.xml
// Query construction at runtime
Result<Record3<Integer, String, String>> result =
ctx.select(USERS.ID, USERS.NAME, USERS.EMAIL)
.from(USERS)
.where(USERS.STATUS.eq("active"))
.and(USERS.CREATED_AT.gt(LocalDateTime.now().minusDays(30)))
.orderBy(USERS.CREATED_AT.desc())
.limit(10)
.fetch();
for (Record3<Integer, String, String> row : result) {
Integer id = row.value1();
String name = row.value2();
String email = row.value3();
}
Scythe: Plain SQL + Generated Code¶
-- queries/get_active_users.sql
-- @name GetActiveUsers
-- @param min_date :timestamptz
SELECT id, name, email
FROM users
WHERE status = 'active'
AND created_at > :min_date
ORDER BY created_at DESC
LIMIT 10;
Generated code (Java example):
// Generated by scythe — do not edit
public record GetActiveUsersRow(
int id,
String name,
String email
) {}
public List<GetActiveUsersRow> getActiveUsers(
Connection connection,
OffsetDateTime minDate
) throws SQLException {
// ... prepared statement execution
}
Generated code (Python example):
# Generated by scythe — do not edit
@dataclass
class GetActiveUsersRow:
id: int
name: str
email: str
async def get_active_users(
connection: AsyncConnection,
min_date: datetime,
) -> list[GetActiveUsersRow]:
...
Generated code (Rust example):
// Generated by scythe — do not edit
#[derive(Debug, Clone)]
pub struct GetActiveUsersRow {
pub id: i32,
pub name: String,
pub email: String,
}
pub async fn get_active_users(
pool: &PgPool,
min_date: OffsetDateTime,
) -> Result<Vec<GetActiveUsersRow>, sqlx::Error> {
// ...
}
Key differences visible in the examples:
- No API to learn. The scythe input is standard SQL. The jOOQ input is a Java DSL that mirrors SQL but requires learning method names, chaining rules, and type parameter conventions.
- No runtime query construction. Scythe's generated code contains the SQL string as a constant. jOOQ assembles the SQL string from DSL method calls on every execution.
- Multi-language output. The same
.sqlfile produces typed code for any of scythe's 10 target languages. jOOQ generates Java (usable from Kotlin).
Licensing¶
jOOQ uses a dual-license model:
- Open Source Edition (Apache 2.0): supports PostgreSQL, MySQL, MariaDB, SQLite, H2, HSQLDB, Derby.
- Commercial Edition (paid): required for Oracle, SQL Server, DB2, Informix, Firebird, and others.
Scythe is MIT-licensed. All database backends, all features, all languages. No commercial tiers.
For teams using Oracle or SQL Server, this is a meaningful cost difference. jOOQ commercial licenses are per-developer, per-year.
Dynamic Queries¶
jOOQ's runtime DSL allows conditional query construction:
var query = ctx.select(USERS.ID, USERS.NAME).from(USERS);
if (filterByStatus) {
query = query.where(USERS.STATUS.eq(status));
}
if (filterByDate) {
query = query.and(USERS.CREATED_AT.gt(minDate));
}
Scythe does not support dynamic query composition. Queries are fixed at compile time. If you need conditional logic, you write separate queries or use SQL-level conditionals (WHERE (:filter_status IS NULL OR status = :filter_status)).
This is a deliberate trade-off. Dynamic queries are powerful but harder to analyze statically, harder to lint, and harder to optimize. If your application relies heavily on dynamically composed queries, jOOQ handles that better.
When to Choose jOOQ¶
- Your stack is Java or Kotlin exclusively.
- You need runtime query composition (conditional WHERE clauses, dynamic column selection).
- You prefer building queries in application code rather than maintaining separate SQL files.
- You are comfortable with the commercial license cost for non-open-source databases.
When to Choose Scythe¶
- You want to write plain SQL and version it alongside your application code.
- You need generated code in multiple languages (polyglot services, shared schemas across teams).
- You want SQL linting and formatting integrated into the build pipeline.
- You use Oracle, SQL Server, or DB2 and do not want to pay per-developer license fees.
- You want code generation without a running database (CI pipelines, offline development).
- You prefer zero runtime overhead — no DSL evaluation, no SQL string assembly.