Skip to content

Commit fdbcfbc

Browse files
authored
Fix leak in preparedStatement + fix interpolation in metadata (#854)
1 parent e616d0b commit fdbcfbc

File tree

4 files changed

+98
-26
lines changed

4 files changed

+98
-26
lines changed

NEXT_CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### Added
66
- Added support for DoD (.mil) domains
7-
- Support to fetch metadata in PreparedStatement for SELECT queries before executing the query.
7+
- Enables fetching of metadata for SELECT queries using PreparedStatement prior to setting parameters or executing the query.
88
- Added support for SSL client certificate authentication via keystore configuration parameters: SSLKeyStore, SSLKeyStorePwd, SSLKeyStoreType, and SSLKeyStoreProvider.
99

1010

src/main/java/com/databricks/jdbc/api/impl/DatabricksPreparedStatement.java

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
import static com.databricks.jdbc.common.util.DatabricksTypeUtil.getDatabricksTypeFromSQLType;
55
import static com.databricks.jdbc.common.util.DatabricksTypeUtil.inferDatabricksType;
66
import static com.databricks.jdbc.common.util.SQLInterpolator.interpolateSQL;
7+
import static com.databricks.jdbc.common.util.SQLInterpolator.surroundPlaceholdersWithQuotes;
78
import static com.databricks.jdbc.common.util.ValidationUtil.throwErrorIfNull;
89

910
import com.databricks.jdbc.common.StatementType;
1011
import com.databricks.jdbc.common.util.DatabricksTypeUtil;
11-
import com.databricks.jdbc.dbclient.impl.common.StatementId;
1212
import com.databricks.jdbc.exception.*;
1313
import com.databricks.jdbc.log.JdbcLogger;
1414
import com.databricks.jdbc.log.JdbcLoggerFactory;
@@ -45,6 +45,18 @@ public DatabricksPreparedStatement(DatabricksConnection connection, String sql)
4545
this.databricksBatchParameterMetaData = new ArrayList<>();
4646
}
4747

48+
DatabricksPreparedStatement(
49+
DatabricksConnection connection,
50+
String sql,
51+
boolean interpolateParameters,
52+
DatabricksParameterMetaData databricksParameterMetaData) {
53+
super(connection);
54+
this.sql = sql;
55+
this.interpolateParameters = interpolateParameters;
56+
this.databricksParameterMetaData = databricksParameterMetaData;
57+
this.databricksBatchParameterMetaData = new ArrayList<>();
58+
}
59+
4860
@Override
4961
public ResultSet executeQuery() throws SQLException {
5062
LOGGER.debug("public ResultSet executeQuery()");
@@ -173,13 +185,13 @@ public void setString(int parameterIndex, String x) throws SQLException {
173185
}
174186

175187
/*
176-
* Sets the designated parameter to the given array of bytes. The driver converts this to hex literal in the format X'hex' and interpolate it into the SQL statement.
177-
* Works only when supportManyParameters is enabled in the connection string.
188+
* Sets the designated parameter to the given array of bytes. The driver converts this to hex literal in the format X'hex' and interpolate it into the SQL statement.
189+
* Works only when supportManyParameters is enabled in the connection string.
178190
179-
* @param parameterIndex – the first parameter is 1, the second is 2, ...
180-
* @param x – the parameter value
181-
* @throws SQLException - if a database access error occurs or this method is called on a closed PreparedStatement
182-
* @throws DatabricksSQLFeatureNotSupportedException - if parameter interpolation is not enabled
191+
* @param parameterIndex – the first parameter is 1, the second is 2, ...
192+
* @param x – the parameter value
193+
* @throws SQLException - if a database access error occurs or this method is called on a closed PreparedStatement
194+
* @throws DatabricksSQLFeatureNotSupportedException - if parameter interpolation is not enabled
183195
*/
184196
@Override
185197
public void setBytes(int parameterIndex, byte[] x) throws SQLException {
@@ -834,18 +846,20 @@ private DatabricksResultSet interpolateIfRequiredAndExecute(StatementType statem
834846
* @throws DatabricksSQLException if there is an error executing the DESCRIBE QUERY command
835847
*/
836848
private ResultSetMetaData getMetaDataFromDescribeQuery() throws DatabricksSQLException {
837-
838-
try (DatabricksResultSet metadataResultSet = executeDescribeQueryCommand()) {
849+
String describeQuerySQL = "DESCRIBE QUERY " + surroundPlaceholdersWithQuotes(sql);
850+
try (DatabricksPreparedStatement preparedStatement =
851+
new DatabricksPreparedStatement(
852+
connection, describeQuerySQL, interpolateParameters, databricksParameterMetaData);
853+
ResultSet metadataResultSet = preparedStatement.executeQuery(); ) {
839854
ArrayList<String> columnNames = new ArrayList<>();
840855
ArrayList<String> columnDataTypes = new ArrayList<>();
841856

842857
while (metadataResultSet.next()) {
843858
columnNames.add(metadataResultSet.getString(1));
844859
columnDataTypes.add(metadataResultSet.getString(2));
845860
}
846-
847861
return new DatabricksResultSetMetaData(
848-
StatementId.deserialize(metadataResultSet.getStatementId()),
862+
preparedStatement.getStatementId(),
849863
columnNames,
850864
columnDataTypes,
851865
this.connection.getConnectionContext());
@@ -856,18 +870,4 @@ private ResultSetMetaData getMetaDataFromDescribeQuery() throws DatabricksSQLExc
856870
errorMessage, e, DatabricksDriverErrorCode.EXECUTE_STATEMENT_FAILED);
857871
}
858872
}
859-
860-
private DatabricksResultSet executeDescribeQueryCommand() throws SQLException {
861-
String interpolatedSql =
862-
this.interpolateParameters
863-
? interpolateSQL(sql, this.databricksParameterMetaData.getParameterBindings())
864-
: sql;
865-
866-
interpolatedSql = "DESCRIBE QUERY " + interpolatedSql;
867-
Map<Integer, ImmutableSqlParameter> paramMap =
868-
this.interpolateParameters
869-
? new HashMap<>()
870-
: this.databricksParameterMetaData.getParameterBindings();
871-
return executeInternal(interpolatedSql, paramMap, StatementType.QUERY);
872-
}
873873
}

src/main/java/com/databricks/jdbc/common/util/SQLInterpolator.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import com.databricks.jdbc.exception.DatabricksValidationException;
77
import com.databricks.sdk.service.sql.ColumnInfoTypeName;
88
import java.util.Map;
9+
import java.util.regex.Matcher;
10+
import java.util.regex.Pattern;
911

1012
public class SQLInterpolator {
1113
private static String escapeApostrophes(String input) {
@@ -67,4 +69,22 @@ public static String interpolateSQL(String sql, Map<Integer, ImmutableSqlParamet
6769
}
6870
return sb.toString();
6971
}
72+
73+
/**
74+
* Surrounds unquoted placeholders (?) with single quotes, preserving already quoted ones. This is
75+
* crucial for DESCRIBE QUERY commands as unquoted placeholders will cause a parse_syntax_error.
76+
*/
77+
public static String surroundPlaceholdersWithQuotes(String sql) {
78+
if (sql == null || sql.isEmpty()) {
79+
return sql;
80+
}
81+
// This pattern matches any '?' that is NOT already inside single quotes
82+
StringBuilder sb = new StringBuilder();
83+
Matcher m = Pattern.compile("(?<!')\\?(?!')").matcher(sql);
84+
while (m.find()) {
85+
m.appendReplacement(sb, "'?'");
86+
}
87+
m.appendTail(sb);
88+
return sb.toString();
89+
}
7090
}

src/test/java/com/databricks/jdbc/common/util/SQLInterpolatorTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
import com.databricks.jdbc.exception.DatabricksValidationException;
1010
import java.util.HashMap;
1111
import java.util.Map;
12+
import java.util.stream.Stream;
1213
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.params.ParameterizedTest;
15+
import org.junit.jupiter.params.provider.Arguments;
16+
import org.junit.jupiter.params.provider.MethodSource;
1317

1418
public class SQLInterpolatorTest {
1519

@@ -87,4 +91,52 @@ public void testBinaryType() throws DatabricksValidationException {
8791
String expected = "INSERT INTO sales (id, data) VALUES (101, X'0102030405')";
8892
assertEquals(expected, SQLInterpolator.interpolateSQL(sql, params));
8993
}
94+
95+
private static Stream<Arguments> providePlaceholderQuotingTestCases() {
96+
return Stream.of(
97+
// Basic placeholder quoting
98+
Arguments.of(
99+
"SELECT * FROM table WHERE id = ?",
100+
"SELECT * FROM table WHERE id = '?'",
101+
"Basic placeholder quoting"),
102+
103+
// Multiple placeholders
104+
Arguments.of(
105+
"SELECT * FROM table WHERE id = ? AND name = ?",
106+
"SELECT * FROM table WHERE id = '?' AND name = '?'",
107+
"Multiple placeholders"),
108+
109+
// Already quoted placeholders
110+
Arguments.of(
111+
"SELECT * FROM table WHERE id = '?' AND name = ?",
112+
"SELECT * FROM table WHERE id = '?' AND name = '?'",
113+
"Already quoted placeholders"),
114+
115+
// Mixed quoted and unquoted placeholders
116+
Arguments.of(
117+
"SELECT * FROM table WHERE id = '?' AND name = ? AND age = '?'",
118+
"SELECT * FROM table WHERE id = '?' AND name = '?' AND age = '?'",
119+
"Mixed quoted and unquoted placeholders"),
120+
121+
// Null input
122+
Arguments.of(null, null, "Null input"),
123+
124+
// Empty input
125+
Arguments.of("", "", "Empty input"),
126+
127+
// No placeholders
128+
Arguments.of("SELECT * FROM table", "SELECT * FROM table", "No placeholders"),
129+
130+
// Complex query with multiple conditions
131+
Arguments.of(
132+
"SELECT * FROM table WHERE id = ? AND (name = ? OR age = ?) AND status = ?",
133+
"SELECT * FROM table WHERE id = '?' AND (name = '?' OR age = '?') AND status = '?'",
134+
"Complex query with multiple conditions"));
135+
}
136+
137+
@ParameterizedTest(name = "{2}")
138+
@MethodSource("providePlaceholderQuotingTestCases")
139+
public void testSurroundPlaceholdersWithQuotes(String input, String expected, String testName) {
140+
assertEquals(expected, SQLInterpolator.surroundPlaceholdersWithQuotes(input), testName);
141+
}
90142
}

0 commit comments

Comments
 (0)