Skip to content

feat(jdbc): Implement named parameter support in PreparedStatement#7

Open
rovshan-b wants to merge 3 commits into
mainfrom
feature/arrow-support-named-bind-variables
Open

feat(jdbc): Implement named parameter support in PreparedStatement#7
rovshan-b wants to merge 3 commits into
mainfrom
feature/arrow-support-named-bind-variables

Conversation

@rovshan-b
Copy link
Copy Markdown

@rovshan-b rovshan-b commented Apr 29, 2026

Add named bind parameter support (:name syntax)

The Arrow Flight JDBC driver previously only supported positional ? parameters. This PR adds
named parameter support entirely on the client side — no server changes required.

Usage

// Before — positional only
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM orders WHERE id = ? AND status = ?");
ps.setInt(1, 42);
ps.setString(2, "active");

// After — named parameters (cast to NamedPreparedStatement to access named setters)
NamedPreparedStatement ps = (NamedPreparedStatement) conn.prepareStatement(
    "SELECT * FROM orders WHERE id = :id AND status = :status");
ps.setInt("id", 42);
ps.setString("status", "active");

// Duplicate name — both slots receive the same value automatically
NamedPreparedStatement ps = (NamedPreparedStatement) conn.prepareStatement(
    "SELECT * FROM events WHERE start_date = :date OR end_date = :date");
ps.setDate("date", someDate);

conn.prepareStatement() always returns a NamedPreparedStatement (which extends
PreparedStatement), so the cast is always safe — regardless of whether the SQL uses named
or positional parameters. This matches the pattern of Oracle's OraclePreparedStatement.
All existing setXxx(int index, …) positional calls continue to work unchanged on the same object.

What's in this PR

File Change
NamedPreparedStatement.java New — public interface extending PreparedStatement, declares all named-param setXxx(String, …) methods
NamedParamStatement.java New (package-private) — NamedPreparedStatement decorator; resolves each name to its 1-based positional index(es) and forwards to the delegate
utils/NamedSqlParser.java New — single-pass SQL scanner, translates :name?, builds name→index maps, rejects mixed ?/:name queries
ArrowFlightConnection.java Override all prepareStatement(String, …) variants to unconditionally wrap in NamedParamStatement
utils/NamedSqlParserTest.java 11 pure unit tests (string literals, comments, PG casts, mixed-param rejection, etc.)
NamedParamStatementTest.java 6 integration tests against the mock Flight SQL server

Notes

  • Translation from :name? is client-side only — the server receives a standard positional query unchanged.
  • PostgreSQL-style casts (col::int) are correctly left untouched.
  • :name tokens inside string literals ('…', "…") and comments (--, /* */) are ignored.
  • Mixing ? and :name in the same query throws SQLException.
  • Unknown parameter names throw SQLException("Unknown parameter name: '<name>'").
  • NamedParamStatement is package-private — callers always program against the NamedPreparedStatement interface.

@mateusaubin mateusaubin self-requested a review May 11, 2026 17:27
Copy link
Copy Markdown
Collaborator

@mateusaubin mateusaubin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very nice, clean implementation. a few suggestions to centralize our changes and minimize surface area to upstream arrow but very nice.

also consider fixes needed to make mvn clean spotless:apply install succeed

* parameters.
*/
public static ParseResult parse(String sql) throws SQLException {
if (sql == null) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: null or empty? otherwise we will initialize all structures for empty string

* indices. Callers should use the {@link NamedPreparedStatement} interface — never this class
* directly.
*/
class NamedParamStatement implements NamedPreparedStatement {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: NamedParamStatement is, at the same time, similar but different enough from the interface name that causes confusion to understand without diving deeper into.

suggest following a more "standard" convention for default implementations of an interface, such as :

  • NamedPreparedStatementImpl: classic *Impl suffix, widely used in Java;
  • DefaultNamedPreparedStatement: Default* prefix, common in Spring/JDK

*/
class NamedParamStatement implements NamedPreparedStatement {

private final PreparedStatement delegate;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: NamedParamStatement implements the full PreparedStatement interface, which forces ~50 boilerplate pass-through methods. Consider introducing a ForwardingPreparedStatement abstract base:

abstract class ForwardingPreparedStatement implements PreparedStatement {
    protected abstract PreparedStatement delegate();

    @Override public ResultSet executeQuery() throws SQLException { return delegate().executeQuery(); }
    @Override public int executeUpdate() throws SQLException { return delegate().executeUpdate(); }
    // ... all pass-throughs
}

NamedParamStatement then extends it and only declares the named-setter methods — the class shrinks from ~700 lines to ~50.

This is the same pattern Guava uses for ForwardingCollection / ForwardingMap. The forwarding base is also reusable for any future PreparedStatement decorator in this driver.

* ResultSet rs = ps.executeQuery();
* }</pre>
*/
public interface NamedPreparedStatement extends PreparedStatement {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: PreparedStatement also includes a bunch of more "esoteric" types (like Blob, Clob, etc...) which are missing from here.

why are we skipping those?

}
}

return new ParseResult(out.toString(), nameToIndices, orderedNames);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kudos for this implementation, definitely not trivial 👏

public PreparedStatement prepareStatement(
String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
NamedSqlParser.ParseResult parsed = NamedSqlParser.parse(sql);
return new NamedParamStatement(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

polish: the 6 prepareStatement overrides in ArrowFlightConnection all repeat the same parse-then-wrap pattern. A static factory on NamedParamStatement accepting a functional interface could reduce each to a one-liner:

@FunctionalInterface
interface PrepareFunction {
  PreparedStatement prepare(String sql) throws SQLException;
}

static NamedPreparedStatement wrap(String sql, PrepareFunction fn) throws SQLException {
  ParseResult parsed = NamedSqlParser.parse(sql);
  return new NamedParamStatement(fn.prepare(parsed.positionalSql), parsed);
}

Usage:

return NamedParamStatement.wrap(sql, s -> super.prepareStatement(s, resultSetType, resultSetConcurrency));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants