Skip to content

Java (JDBC, R2DBC)

Backends: java-jdbc, java-r2dbc

java-jdbc supports 9 engines (PostgreSQL, MySQL, MariaDB, SQLite, DuckDB, MSSQL, Redshift, Snowflake, Oracle). java-r2dbc supports PostgreSQL, MySQL, MariaDB, and SQLite. The examples on this page use PostgreSQL.

Generated files carry a provenance header as their first line, e.g. // scythe:provenance v=0.15.0 backend=java-jdbc engine=postgresql schema=sch1:... queries=q1:... (integration_tests/java-jdbc/src/main/java/generated/Queries.java:1).

Generated record-component names are snake_case by default, mirroring the SQL column name – not camelCase (field_case defaults to snake_case; crates/scythe-backend/src/naming.rs). created_at stays created_at, not createdAt.

Both backends on this page accept a field_case option – snake_case or camelCase, and nothing else – to opt into camelCase fields. It is a [[sql.gen]] target key only; a manifest cannot set it, because NamingConfig::field_case carries #[serde(skip)] (crates/scythe-backend/src/naming.rs). A field_case key under a full manifest’s [naming] table is ignored, and naming it in a partial manifest override is a parse error.

[[sql.gen]]
backend = "java-jdbc"
output = "src/generated/java"
field_case = "camelCase"

camelCase renames record components and query-function parameters – created_at becomes createdAt. It does not change decoding: both backends on this page read the ResultSet/Row by the raw SQL column name (rs.getObject("created_at", ...), row.get("created_at", ...)), and only the declared field name changes (crates/scythe-codegen/src/backends/java_jdbc.rs, java_r2dbc.rs). Two SQL identifiers that collapse onto the same generated name are a hard error, not last-write-wins. See field_case in the Configuration guide for the full option reference.

-- @name GetUser
-- @returns :one
SELECT id, name, email, created_at FROM users WHERE id = $1;
-- @name ListUsers
-- @returns :many
SELECT id, name FROM users ORDER BY name LIMIT $1;
-- @name CreateUser
-- @returns :exec
INSERT INTO users (name, email) VALUES ($1, $2);

Schema:

CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
public record GetUserRow(
int id,
String name,
@Nullable String email,
java.time.OffsetDateTime created_at
) {
public static GetUserRow fromResultSet(ResultSet rs) throws SQLException {
return new GetUserRow(
rs.getInt("id"),
rs.getString("name"),
rs.getString("email"),
rs.getObject("created_at", java.time.OffsetDateTime.class)
);
}
}

:one – returns @Nullable T, null on no rows

Section titled “:one – returns @Nullable T, null on no rows”
public static @Nullable GetUserRow getUser(Connection conn, int id) throws SQLException {
try (var ps = conn.prepareStatement(
"SELECT id, name, email, created_at FROM users WHERE id = ?")) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return GetUserRow.fromResultSet(rs);
}
return null;
}
}
}
public record ListUsersRow(int id, String name) {
public static ListUsersRow fromResultSet(ResultSet rs) throws SQLException {
return new ListUsersRow(rs.getInt("id"), rs.getString("name"));
}
}
public static List<ListUsersRow> listUsers(Connection conn, long limit) throws SQLException {
try (var ps = conn.prepareStatement(
"SELECT id, name FROM users ORDER BY name LIMIT ?")) {
ps.setLong(1, limit);
try (ResultSet rs = ps.executeQuery()) {
List<ListUsersRow> result = new ArrayList<>();
while (rs.next()) {
result.add(ListUsersRow.fromResultSet(rs));
}
return result;
}
}
}
public static void createUser(Connection conn, String name, @Nullable String email)
throws SQLException {
try (var stmt = conn.prepareStatement(
"INSERT INTO users (name, email) VALUES (?, ?)")) {
stmt.setString(1, name);
stmt.setString(2, email);
stmt.executeUpdate();
}
}

Backend: java-r2dbc | Library: R2DBC with Project Reactor | Engines: PostgreSQL, MySQL, MariaDB, SQLite

Generates reactive code using Mono<T> for :one/:exec queries and Flux<T> for :many queries. Requires a ConnectionFactory instead of a JDBC Connection.

Row types are plain records with no fromRow method – row-to-object mapping is inlined directly into each query function (crates/scythe-codegen/src/backends/java_r2dbc.rs):

public record GetUserRow(
int id,
String name,
@Nullable String email,
java.time.OffsetDateTime created_at
) {}

Resource handling uses Mono.usingWhen(...) / Flux.usingWhen(...), not flatMap + doFinally. Binds are zero-based positional integers.bind(0, x), .bind(1, y), … – not "$1" string keys, even against PostgreSQL:

public static Mono<GetUserRow> getUser(ConnectionFactory cf, int id) {
return Mono.usingWhen(
Mono.from(cf.create()),
conn -> {
var stmt = conn.createStatement("SELECT id, name, email, created_at FROM users WHERE id = $1");
stmt.bind(0, id);
return Mono.from(stmt.execute())
.flatMap(result -> Mono.from(result.map((row, meta) ->
new GetUserRow(
row.get("id", Integer.class),
row.get("name", String.class),
row.get("email", String.class),
row.get("created_at", java.time.OffsetDateTime.class)
))));
},
conn -> Mono.from(conn.close())
);
}
public static Flux<ListUsersRow> listUsers(ConnectionFactory cf, long limit) {
return Flux.usingWhen(
cf.create(),
conn -> {
var stmt = conn.createStatement("SELECT id, name FROM users ORDER BY name LIMIT $1");
stmt.bind(0, limit);
return Flux.from(stmt.execute())
.flatMap(result -> result.map((row, meta) ->
new ListUsersRow(
row.get("id", Integer.class),
row.get("name", String.class)
)));
},
conn -> Mono.from(conn.close())
);
}
public static Mono<Void> createUser(ConnectionFactory cf, String name, @Nullable String email) {
return Mono.usingWhen(
Mono.from(cf.create()),
conn -> {
var stmt = conn.createStatement("INSERT INTO users (name, email) VALUES ($1, $2)");
stmt.bind(0, name);
stmt.bind(1, email);
return Mono.from(stmt.execute())
.flatMap(result -> Mono.from(result.getRowsUpdated()))
.then();
},
conn -> Mono.from(conn.close())
);
}
CREATE TYPE user_status AS ENUM ('active', 'inactive', 'banned');
public enum UserStatus {
ACTIVE("active"),
INACTIVE("inactive"),
BANNED("banned");
private final String value;
UserStatus(String value) { this.value = value; }
public String getValue() { return value; }
}
SQL Type Neutral Java
INTEGER int32 int
BIGINT int64 long
TEXT string String
BOOLEAN bool boolean
BYTEA bytes byte[]
UUID uuid java.util.UUID
NUMERIC decimal java.math.BigDecimal
DATE date java.time.LocalDate
TIMESTAMPTZ datetime_tz java.time.OffsetDateTime
JSON json String
TEXT[] array<string> String
nullable nullable @Nullable T