feat(jdbc): Implement named parameter support in PreparedStatement#7
feat(jdbc): Implement named parameter support in PreparedStatement#7rovshan-b wants to merge 3 commits into
Conversation
mateusaubin
left a comment
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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));
Add named bind parameter support (
:namesyntax)The Arrow Flight JDBC driver previously only supported positional
?parameters. This PR addsnamed parameter support entirely on the client side — no server changes required.
Usage
conn.prepareStatement()always returns aNamedPreparedStatement(which extendsPreparedStatement), so the cast is always safe — regardless of whether the SQL uses namedor 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
NamedPreparedStatement.javaPreparedStatement, declares all named-paramsetXxx(String, …)methodsNamedParamStatement.javaNamedPreparedStatementdecorator; resolves each name to its 1-based positional index(es) and forwards to the delegateutils/NamedSqlParser.java:name→?, builds name→index maps, rejects mixed?/:namequeriesArrowFlightConnection.javaprepareStatement(String, …)variants to unconditionally wrap inNamedParamStatementutils/NamedSqlParserTest.javaNamedParamStatementTest.javaNotes
:name→?is client-side only — the server receives a standard positional query unchanged.col::int) are correctly left untouched.:nametokens inside string literals ('…',"…") and comments (--,/* */) are ignored.?and:namein the same query throwsSQLException.SQLException("Unknown parameter name: '<name>'").NamedParamStatementis package-private — callers always program against theNamedPreparedStatementinterface.