Skip to content

Commit 4eb0f32

Browse files
committed
#343 Improve call escape handling and configuration
Improve test coverage
1 parent 8008597 commit 4eb0f32

13 files changed

Lines changed: 273 additions & 58 deletions

src/main/org/firebirdsql/jdbc/FBCallableStatement.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313

1414
import org.firebirdsql.gds.ng.LockCloseable;
1515
import org.firebirdsql.gds.ng.StatementType;
16+
import org.firebirdsql.jdbc.FBObjectListener.BlobListener;
17+
import org.firebirdsql.jdbc.FBObjectListener.StatementListener;
18+
import org.firebirdsql.jdbc.escape.CallEscapeHandling;
1619
import org.firebirdsql.jdbc.escape.FBEscapedCallParser;
20+
import org.firebirdsql.jdbc.escape.JdbcEscapeParser;
1721
import org.firebirdsql.jdbc.field.FBField;
1822
import org.firebirdsql.jdbc.field.TypeConversionException;
1923
import org.firebirdsql.util.InternalApi;
@@ -31,7 +35,7 @@
3135
import static org.firebirdsql.jdbc.SQLStateConstants.SQL_STATE_NO_RESULT_SET;
3236

3337
/**
34-
* Implementation of {@link java.sql.CallableStatement}.
38+
* Implementation of {@link java.sql.CallableStatement} (the <em>V1</em> implementation).
3539
* <p>
3640
* This class is internal API of Jaybird. Future versions may radically change, move, or make inaccessible this type.
3741
* For the public API, refer to the {@link java.sql.CallableStatement} and {@link FirebirdCallableStatement} interfaces.
@@ -51,8 +55,8 @@ public class FBCallableStatement extends FBPreparedStatement implements Callable
5155
protected FBProcedureCall procedureCall;
5256

5357
protected FBCallableStatement(FBConnection connection, String sql, ResultSetBehavior rsBehavior,
54-
StoredProcedureMetaData storedProcMetaData, FBObjectListener.StatementListener statementListener,
55-
FBObjectListener.BlobListener blobListener) throws SQLException {
58+
StoredProcedureMetaData storedProcMetaData, StatementListener statementListener, BlobListener blobListener)
59+
throws SQLException {
5660
super(connection, rsBehavior, statementListener, blobListener);
5761
var parser = new FBEscapedCallParser(getEscapeParser(), connection.getServerVersion());
5862

@@ -65,6 +69,12 @@ protected FBCallableStatement(FBConnection connection, String sql, ResultSetBeha
6569
storedProcMetaData.updateSelectability(procedureCall);
6670
}
6771

72+
@Override
73+
JdbcEscapeParser getEscapeParser() throws SQLException {
74+
// This implementation handles the transformation of the call escape itself, so the escape parser shouldn't
75+
return super.getEscapeParser().with(CallEscapeHandling.IGNORED);
76+
}
77+
6878
@Override
6979
public void close() throws SQLException {
7080
try (var ignored = withLock()) {

src/main/org/firebirdsql/jdbc/FBConnection.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.firebirdsql.jaybird.util.SearchPathHelper;
2929
import org.firebirdsql.jaybird.xca.FBLocalTransaction;
3030
import org.firebirdsql.jaybird.xca.FBManagedConnection;
31+
import org.firebirdsql.jdbc.FBObjectListener.BlobListener;
32+
import org.firebirdsql.jdbc.FBObjectListener.StatementListener;
3133
import org.firebirdsql.jdbc.InternalTransactionCoordinator.MetaDataTransactionCoordinator;
3234
import org.firebirdsql.jdbc.escape.FBEscapedParser;
3335
import org.firebirdsql.util.InternalApi;
@@ -821,8 +823,8 @@ protected PreparedStatement prepareStatement(String sql, int resultSetType, int
821823
Optional<PreparedStatement> txStmt = prepareIfTransactionStatement(sql, rsBehavior);
822824
if (txStmt.isPresent()) return txStmt.get();
823825

824-
FBObjectListener.StatementListener coordinator = txCoordinator;
825-
FBObjectListener.BlobListener blobCoordinator = txCoordinator;
826+
StatementListener coordinator = txCoordinator;
827+
BlobListener blobCoordinator = txCoordinator;
826828
if (metaData) {
827829
coordinator = new MetaDataTransactionCoordinator(txCoordinator);
828830
blobCoordinator = null;
@@ -927,13 +929,26 @@ public CallableStatement prepareCall(String sql, int resultSetType, int resultSe
927929
storedProcedureMetaData = StoredProcedureMetaDataFactory.getInstance(this);
928930
}
929931

930-
var stmt = new FBCallableStatement(this, sql, rsBehavior, storedProcedureMetaData, txCoordinator,
932+
CallableStatement stmt = createCallableStatement(sql, rsBehavior, storedProcedureMetaData, txCoordinator,
931933
txCoordinator);
932934
activeStatements.add(stmt);
933935
return stmt;
934936
}
935937
}
936938

939+
final FirebirdCallableStatement createCallableStatement(String sql, ResultSetBehavior rsBehavior,
940+
StoredProcedureMetaData storedProcedureMetaData, StatementListener statementListener,
941+
BlobListener blobListener) throws SQLException {
942+
return switch (getCallableImplementation().toUpperCase(Locale.ROOT)) {
943+
case PropertyConstants.CALLABLE_IMPLEMENTATION_V1 -> new FBCallableStatement(this, sql, rsBehavior,
944+
storedProcedureMetaData, statementListener, blobListener);
945+
case PropertyConstants.CALLABLE_IMPLEMENTATION_V2 -> throw new FBDriverNotCapableException(
946+
"Callable implementation V2 not yet available");
947+
default -> throw new FBDriverNotCapableException(
948+
"Unknown callable implementation: %s".formatted(getCallableImplementation()));
949+
};
950+
}
951+
937952
@Override
938953
public Map<String, Class<?>> getTypeMap() throws SQLException {
939954
return new HashMap<>();
@@ -1596,6 +1611,10 @@ final boolean isEscapeProcessing() {
15961611
return connectionProperties().isEscapeProcessing();
15971612
}
15981613

1614+
final String getCallableImplementation() {
1615+
return connectionProperties().getCallableImplementation();
1616+
}
1617+
15991618
final GDSServerVersion getServerVersion() throws SQLException {
16001619
return getGDSHelper().getServerVersion();
16011620
}

src/main/org/firebirdsql/jdbc/FBPreparedStatement.java

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,32 +67,38 @@ public class FBPreparedStatement extends FBStatement implements FirebirdPrepared
6767
private final boolean metaDataQuery;
6868

6969
/**
70-
* This flag is needed to guarantee the correct behavior in case when it
71-
* was created without controlling Connection object (in some metadata
72-
* queries we have only GDSHelper instance)
70+
* Marks the statement as a standalone statement.
71+
* <p>
72+
* This ensures correct behaviour when created without an owning Connection object (in some metadata queries we only
73+
* have a GDSHelper instance).
74+
* </p>
7375
*/
7476
private final boolean standaloneStatement;
7577

7678
/**
77-
* This flag is needed to prevent throwing an exception for the case when
78-
* result set is returned for INSERT statement and the statement should
79-
* return the generated keys.
79+
* Marks the statement as a generated keys statement.
80+
* <p>
81+
* This prevents throwing an exception when a result set is returned for statements with a {@code RETURNING} clause
82+
* when executed with {@code executeUpdate}.
83+
* </p>
8084
*/
8185
private final boolean generatedKeys;
8286

8387
private FBField[] fields = FIELDS_NOT_INITIALIZED;
8488

85-
// we need to handle procedure execution separately,
86-
// because in this case we must send out_xsqlda to the server.
89+
// We handle procedure execution separately, because we must send out_xsqlda to the server
8790
private boolean isExecuteProcedureStatement;
8891

8992
private final FBObjectListener.BlobListener blobListener;
9093
private RowValue fieldValues = RowValue.EMPTY_ROW_VALUE;
9194
private @Nullable Batch batch;
9295

9396
/**
94-
* Create instance of this class for the specified result set type and concurrency. This constructor is used only in
95-
* {@link FBCallableStatement} since the statement is prepared right before the execution.
97+
* Create instance of this class for the specified result set type and concurrency.
98+
* <p>
99+
* This constructor is for use in {@link FBCallableStatement} and {@link FBCallableStatementV2} only, since there
100+
* the statement is prepared right before execution.
101+
* </p>
96102
*
97103
* @param connection
98104
* connection to be used
@@ -247,6 +253,24 @@ public final void setEscapeProcessing(boolean enable) throws SQLException {
247253
change of escapeProcessing will ensure that we comply with the specified behaviour. */
248254
}
249255

256+
/**
257+
* Internal access to forcibly set escape processing.
258+
* <p>
259+
* The normal method {@link #setEscapeProcessing(boolean)} is a no-op, but {@link FBCallableStatementV2}, which
260+
* wraps a {@code FBPreparedStatement}, needs to disable escape processing on the wrapped instance.
261+
* </p>
262+
*
263+
* @param enable
264+
* {@code true} to enable escape processing; {@code false} to disable it
265+
* @throws SQLException
266+
* if this method is called on a closed statement
267+
* @see #setEscapeProcessing(boolean)
268+
* @since 7
269+
*/
270+
void forceEscapeProcessing(@SuppressWarnings("SameParameterValue") boolean enable) throws SQLException {
271+
super.setEscapeProcessing(enable);
272+
}
273+
250274
public FirebirdParameterMetaData getFirebirdParameterMetaData() throws SQLException {
251275
return new FBParameterMetaData(fbStatement.getParameterDescriptor(), connection);
252276
}

src/main/org/firebirdsql/jdbc/FBStatement.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,7 @@ private boolean needsScrollableCursorEnabled() {
897897
&& fbStatement.supportsFetchScroll();
898898
}
899899

900-
final JdbcEscapeParser getEscapeParser() throws SQLException {
900+
JdbcEscapeParser getEscapeParser() throws SQLException {
901901
return isEscapeProcessing() ? connection.getEscapeParser() : JdbcEscapeParser.noEscapeParser();
902902
}
903903

src/main/org/firebirdsql/jdbc/FirebirdPreparedStatement.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-FileCopyrightText: Copyright 2003-2004 Roman Rokytskyy
22
// SPDX-FileCopyrightText: Copyright 2005 Gabriel Reid
3-
// SPDX-FileCopyrightText: Copyright 2014-2024 Mark Rotteveel
3+
// SPDX-FileCopyrightText: Copyright 2014-2026 Mark Rotteveel
44
// SPDX-FileCopyrightText: Copyright 2019 Vasiliy Yashkov
55
// SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause
66
package org.firebirdsql.jdbc;
@@ -17,7 +17,14 @@
1717
*/
1818
@SuppressWarnings("unused")
1919
public interface FirebirdPreparedStatement extends FirebirdStatement, PreparedStatement {
20-
20+
21+
/**
22+
* Statement type is not known, or no statement is prepared.
23+
*
24+
* @since 7
25+
*/
26+
int TYPE_UNKNOWN = 0;
27+
2128
/** A {@code SELECT} or multi-row DML with {@code RETURNING} statement. */
2229
int TYPE_SELECT = ISCConstants.isc_info_sql_stmt_select;
2330

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-FileCopyrightText: Copyright 2026 Mark Rotteveel
2+
// SPDX-License-Identifier: LGPL-2.1-or-later
3+
package org.firebirdsql.jdbc.escape;
4+
5+
/**
6+
* Handling of call escapes by {@link FBEscapedParser}.
7+
*
8+
* @since 7
9+
*/
10+
public enum CallEscapeHandling {
11+
12+
/**
13+
* Call escapes are processed and converted to {@code EXECUTE PROCEDURE}.
14+
*/
15+
TO_EXECUTE_PROCEDURE,
16+
/**
17+
* Call escapes are ignored and remain part of the returned statement text.
18+
*/
19+
IGNORED,
20+
21+
}

src/main/org/firebirdsql/jdbc/escape/DisabledEscapeParser.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ public String toNative(String sql) {
1919
return sql;
2020
}
2121

22+
@Override
23+
public JdbcEscapeParser with(CallEscapeHandling callEscapeHandling) {
24+
return this;
25+
}
26+
2227
static JdbcEscapeParser getInstance() {
2328
return INSTANCE;
2429
}

src/main/org/firebirdsql/jdbc/escape/FBEscapedParser.java

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,17 @@ public final class FBEscapedParser implements JdbcEscapeParser {
6060

6161
private final AbstractVersion firebirdVersion;
6262
private final QuoteStrategy quoteStrategy;
63+
private final CallEscapeHandling callEscapeHandling;
6364

64-
private FBEscapedParser(AbstractVersion firebirdVersion, QuoteStrategy quoteStrategy) {
65+
private FBEscapedParser(AbstractVersion firebirdVersion, QuoteStrategy quoteStrategy,
66+
CallEscapeHandling callEscapeHandling) {
6567
this.firebirdVersion = firebirdVersion;
6668
this.quoteStrategy = quoteStrategy;
69+
this.callEscapeHandling = callEscapeHandling;
6770
}
6871

6972
/**
70-
* Get an instance of the escape parser for the specified Firebird version and quote strategy.
73+
* Instance of the escape parser for the specified Firebird version and quote strategy, processing call escapes.
7174
*
7275
* @param firebirdVersion
7376
* Firebird version
@@ -76,8 +79,32 @@ private FBEscapedParser(AbstractVersion firebirdVersion, QuoteStrategy quoteStra
7679
* @since 7
7780
*/
7881
public static FBEscapedParser of(AbstractVersion firebirdVersion, QuoteStrategy quoteStrategy) {
79-
assert !(firebirdVersion instanceof OdsVersion) : "Do not pass OdsVersion to FBEscapedParser.of(...)";
80-
return new FBEscapedParser(firebirdVersion, quoteStrategy);
82+
return of(firebirdVersion, quoteStrategy, CallEscapeHandling.TO_EXECUTE_PROCEDURE);
83+
}
84+
85+
/**
86+
* Instance of the escape parser for the specified Firebird version, quote strategy, and escape handling.
87+
*
88+
* @param firebirdVersion
89+
* Firebird version
90+
* @param quoteStrategy
91+
* quote strategy
92+
* @param callEscapeHandling
93+
* call escape handling
94+
* @since 7
95+
*/
96+
public static FBEscapedParser of(AbstractVersion firebirdVersion, QuoteStrategy quoteStrategy,
97+
CallEscapeHandling callEscapeHandling) {
98+
assert!(firebirdVersion instanceof OdsVersion) : "Do not pass OdsVersion to FBEscapedParser.of(...)";
99+
return new FBEscapedParser(firebirdVersion, quoteStrategy, callEscapeHandling);
100+
}
101+
102+
@Override
103+
public FBEscapedParser with(CallEscapeHandling callEscapeHandling) {
104+
if (this.callEscapeHandling == callEscapeHandling) {
105+
return this;
106+
}
107+
return new FBEscapedParser(firebirdVersion, quoteStrategy, callEscapeHandling);
81108
}
82109

83110
AbstractVersion firebirdVersion() {
@@ -89,6 +116,11 @@ QuoteStrategy quoteStrategy() {
89116
return quoteStrategy;
90117
}
91118

119+
@SuppressWarnings("unused")
120+
CallEscapeHandling callEscapeHandling() {
121+
return callEscapeHandling;
122+
}
123+
92124
/**
93125
* Check if the target SQL contains at least one of the escaped syntax commands. This method performs a simple regex
94126
* match, so it may report that SQL contains escaped syntax when the <code>"&#123;"</code> is followed by
@@ -97,7 +129,7 @@ QuoteStrategy quoteStrategy() {
97129
*
98130
* @param sql
99131
* to test
100-
* @return {@code true} if the {@code sql} is suspected to contain escaped syntax.
132+
* @return {@code true} if the {@code sql} seems to contain escaped syntax
101133
*/
102134
private static boolean checkForEscapes(String sql) {
103135
return CHECK_ESCAPE_PATTERN.matcher(sql).find();
@@ -281,14 +313,22 @@ private static void toTimestampString(final StringBuilder target, final CharSequ
281313
* Converts the escaped procedure call syntax into the native procedure call.
282314
*
283315
* @param target
284-
* Target StringBuilder to append native procedure call to.
316+
* target StringBuilder to append native procedure call to
285317
* @param procedureCall
286-
* part of {call proc_name(...)} without curly braces and "call"
287-
* word.
318+
* part of {call proc_name(...)} including curly braces and "call" keyword
288319
*/
289-
private void convertProcedureCall(final StringBuilder target, final String procedureCall) throws SQLException {
320+
private void convertProcedureCall(StringBuilder target, String procedureCall) throws SQLException {
321+
if (callEscapeHandling == CallEscapeHandling.IGNORED) {
322+
target.append(procedureCall);
323+
} else {
324+
convertProcedureCall0(target, procedureCall);
325+
}
326+
}
327+
328+
private void convertProcedureCall0(StringBuilder target, String procedureCall) throws SQLException {
290329
var tempParser = new FBEscapedCallParser(this);
291330
FBProcedureCall call = tempParser.parseCall(procedureCall);
331+
// TODO Should this be called? It result in exceptions, see FBEscapedParserTest#testCallEscapeHandling
292332
call.checkParameters();
293333
target.append(call.getSQL(quoteStrategy));
294334
}

src/main/org/firebirdsql/jdbc/escape/JdbcEscapeParser.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ public sealed interface JdbcEscapeParser permits DisabledEscapeParser, FBEscaped
1717

1818
String toNative(String sql) throws SQLException;
1919

20+
/**
21+
* Returns an instance with the appropriate call escape handling.
22+
* <p>
23+
* If call escape handling is already as configured, or if call escape handling is irrelevant, the same instance may
24+
* be returned.
25+
* </p>
26+
*
27+
* @param callEscapeHandling
28+
* desired call escape handling
29+
* @return appropriate instance
30+
*/
31+
JdbcEscapeParser with(CallEscapeHandling callEscapeHandling);
32+
2033
static JdbcEscapeParser noEscapeParser() {
2134
return DisabledEscapeParser.getInstance();
2235
}

0 commit comments

Comments
 (0)