This model normalizes JDBC's one-based parameter indexes to zero-based wire ordinals. It does
- * not validate parameter completeness, index continuity, or consistency with other parameter sets;
- * those validations remain the backend's responsibility.
+ *
This model preserves JDBC's one-based parameter indexes. Transport adapters are responsible
+ * for converting them to protocol-specific wire ordinals. It does not validate parameter
+ * completeness, index continuity, or consistency with other parameter sets; those validations
+ * remain the backend's responsibility.
*/
public final class BatchParameterSet {
private final List parameters;
+ private final Map parameterBindings;
private BatchParameterSet(List parameters) {
this.parameters = List.copyOf(parameters);
+ Map bindings = new LinkedHashMap<>();
+ this.parameters.forEach(parameter -> bindings.put(parameter.cardinal(), parameter));
+ this.parameterBindings = Collections.unmodifiableMap(bindings);
}
public static BatchParameterSet from(Map parameterBindings) {
@@ -38,6 +45,10 @@ public List getParameters() {
return parameters;
}
+ public Map getParameterBindings() {
+ return parameterBindings;
+ }
+
public int size() {
return parameters.size();
}
@@ -50,7 +61,7 @@ private static ImmutableSqlParameter snapshotParameter(
Map.Entry entry) {
ImmutableSqlParameter parameter = entry.getValue();
return ImmutableSqlParameter.builder()
- .cardinal(entry.getKey() - 1)
+ .cardinal(entry.getKey())
.type(parameter.type())
.value(snapshotValue(parameter.value()))
.build();
diff --git a/src/main/java/com/databricks/jdbc/api/impl/DatabricksPreparedStatement.java b/src/main/java/com/databricks/jdbc/api/impl/DatabricksPreparedStatement.java
index 32e856aac..27aea28d6 100644
--- a/src/main/java/com/databricks/jdbc/api/impl/DatabricksPreparedStatement.java
+++ b/src/main/java/com/databricks/jdbc/api/impl/DatabricksPreparedStatement.java
@@ -33,7 +33,7 @@ public class DatabricksPreparedStatement extends DatabricksStatement implements
JdbcLoggerFactory.getLogger(DatabricksPreparedStatement.class);
private final String sql;
private DatabricksParameterMetaData databricksParameterMetaData;
- private List databricksBatchParameterMetaData;
+ private List batchParameterSets;
private final boolean interpolateParameters;
private final int CHUNK_SIZE = 8192;
@@ -43,7 +43,7 @@ public DatabricksPreparedStatement(DatabricksConnection connection, String sql)
this.sql = sql;
this.interpolateParameters = connection.getConnectionContext().supportManyParameters();
this.databricksParameterMetaData = new DatabricksParameterMetaData(sql);
- this.databricksBatchParameterMetaData = new ArrayList<>();
+ this.batchParameterSets = new ArrayList<>();
// Cache whether this statement should return a ResultSet (based on SQL and config)
this.shouldReturnResultSet = shouldReturnResultSetWithConfig(sql);
}
@@ -58,7 +58,7 @@ public DatabricksPreparedStatement(DatabricksConnection connection, String sql)
this.sql = sql;
this.interpolateParameters = interpolateParameters;
this.databricksParameterMetaData = databricksParameterMetaData;
- this.databricksBatchParameterMetaData = new ArrayList<>();
+ this.batchParameterSets = new ArrayList<>();
// Cache whether this statement should return a ResultSet (based on SQL and config)
this.shouldReturnResultSet = shouldReturnResultSetWithConfig(sql);
}
@@ -94,7 +94,7 @@ public int executeUpdate() throws SQLException {
}
@Override
- public int[] executeBatch() throws DatabricksBatchUpdateException {
+ public int[] executeBatch() throws SQLException {
LOGGER.debug("public int executeBatch()");
long[] largeUpdateCount = executeLargeBatch();
int[] updateCount = new int[largeUpdateCount.length];
@@ -107,10 +107,10 @@ public int[] executeBatch() throws DatabricksBatchUpdateException {
}
@Override
- public long[] executeLargeBatch() throws DatabricksBatchUpdateException {
+ public long[] executeLargeBatch() throws SQLException {
LOGGER.debug("public long executeLargeBatch()");
- if (databricksBatchParameterMetaData.isEmpty()) {
+ if (batchParameterSets.isEmpty()) {
return new long[0];
}
@@ -121,18 +121,41 @@ public long[] executeLargeBatch() throws DatabricksBatchUpdateException {
connection,
interpolateParameters,
(sqlToExecute, params, statementType, closeStatement) ->
- executeInternal(sqlToExecute, params, statementType, closeStatement));
-
- long[] updateCounts = batchExecutor.executeBatch(databricksBatchParameterMetaData);
+ executeInternal(sqlToExecute, params, statementType, closeStatement),
+ new PreparedStatementBatchExecutor.NativeBatchExecutor() {
+ @Override
+ public boolean isSupported() {
+ return supportsNativeParameterBatching();
+ }
+
+ @Override
+ public long[] execute(String sql, List parameterSets)
+ throws SQLException {
+ return executeNativeBatchInternal(sql, parameterSets);
+ }
+ });
+
+ long[] updateCounts;
+ try {
+ updateCounts = batchExecutor.executeBatch(batchParameterSets);
+ } catch (NativeBatchResultException e) {
+ // The backend already completed the batch. Clear it before propagating the count-read error
+ // so a caller retry cannot insert the same rows again.
+ clearBatchAfterExecution();
+ throw e;
+ }
// Clear the batch after successful execution per JDBC spec
+ clearBatchAfterExecution();
+ return updateCounts;
+ }
+
+ private void clearBatchAfterExecution() {
try {
clearBatch();
} catch (SQLException e) {
- LOGGER.error("Failed to clear batch after successful execution", e);
+ LOGGER.error("Failed to clear batch after execution", e);
}
-
- return updateCounts;
}
@Override
@@ -371,7 +394,8 @@ public boolean execute() throws SQLException {
@Override
public void addBatch() {
LOGGER.debug("public void addBatch()");
- this.databricksBatchParameterMetaData.add(databricksParameterMetaData);
+ this.batchParameterSets.add(
+ BatchParameterSet.from(databricksParameterMetaData.getParameterBindings()));
this.databricksParameterMetaData = new DatabricksParameterMetaData(sql);
}
@@ -380,7 +404,7 @@ public void clearBatch() throws DatabricksSQLException {
LOGGER.debug("public void clearBatch()");
checkIfClosed();
this.databricksParameterMetaData = new DatabricksParameterMetaData(sql);
- this.databricksBatchParameterMetaData = new ArrayList<>();
+ this.batchParameterSets = new ArrayList<>();
}
@Override
@@ -755,7 +779,7 @@ private void checkLength(long targetLength, long sourceLength) throws SQLExcepti
}
private void checkIfBatchOperation() throws DatabricksSQLException {
- if (!this.databricksBatchParameterMetaData.isEmpty()) {
+ if (!this.batchParameterSets.isEmpty()) {
String errorMessage =
"Batch must either be executed with executeBatch() or cleared with clearBatch()";
LOGGER.error(errorMessage);
diff --git a/src/main/java/com/databricks/jdbc/api/impl/DatabricksResultSet.java b/src/main/java/com/databricks/jdbc/api/impl/DatabricksResultSet.java
index cde481ccf..e0fbb4ccf 100644
--- a/src/main/java/com/databricks/jdbc/api/impl/DatabricksResultSet.java
+++ b/src/main/java/com/databricks/jdbc/api/impl/DatabricksResultSet.java
@@ -62,6 +62,7 @@ enum ResultSetType {
private static final JdbcLogger LOGGER = JdbcLoggerFactory.getLogger(DatabricksResultSet.class);
protected static final String AFFECTED_ROWS_COUNT = "num_affected_rows";
+ private static final String REPEAT_COUNT = "repeat";
private final ExecutionStatus executionStatus;
private final StatementId statementId;
private final IExecutionResult executionResult;
@@ -2310,6 +2311,44 @@ public long getUpdateCount() throws SQLException {
return updateCount;
}
+ long[] getBatchUpdateCounts(int expectedCount) throws SQLException {
+ checkIfClosed();
+ if (resultSetMetaData.getColumnNameIndex(AFFECTED_ROWS_COUNT) < 1) {
+ throw new DatabricksSQLException(
+ "Native batch result is missing column " + AFFECTED_ROWS_COUNT,
+ DatabricksDriverErrorCode.RESULT_SET_ERROR);
+ }
+
+ long[] counts = new long[expectedCount];
+ int index = 0;
+ boolean hasRepeatCount = resultSetMetaData.getColumnNameIndex(REPEAT_COUNT) > 0;
+ countingUpdateRows = true;
+ try {
+ while (next()) {
+ long repeatCount = hasRepeatCount ? getLong(REPEAT_COUNT) : 1;
+ if (repeatCount < 1 || repeatCount > expectedCount - index) {
+ throw new DatabricksSQLException(
+ "Native batch returned an invalid repeat count: " + repeatCount,
+ DatabricksDriverErrorCode.RESULT_SET_ERROR);
+ }
+ long affectedRows = getLong(AFFECTED_ROWS_COUNT);
+ for (long repeated = 0; repeated < repeatCount; repeated++) {
+ counts[index++] = affectedRows;
+ }
+ }
+ } finally {
+ countingUpdateRows = false;
+ }
+
+ if (index != expectedCount) {
+ throw new DatabricksSQLException(
+ String.format(
+ "Native batch returned %d update counts for %d parameter sets", index, expectedCount),
+ DatabricksDriverErrorCode.RESULT_SET_ERROR);
+ }
+ return counts;
+ }
+
@Override
public boolean hasUpdateCount() throws SQLException {
checkIfClosed();
diff --git a/src/main/java/com/databricks/jdbc/api/impl/DatabricksStatement.java b/src/main/java/com/databricks/jdbc/api/impl/DatabricksStatement.java
index d1dd0d30e..f9493c50b 100644
--- a/src/main/java/com/databricks/jdbc/api/impl/DatabricksStatement.java
+++ b/src/main/java/com/databricks/jdbc/api/impl/DatabricksStatement.java
@@ -866,6 +866,15 @@ DatabricksResultSet executeInternal(
LOGGER.debug(stackTraceMessage);
CompletableFuture futureResultSet =
getFutureResult(sql, params, statementType);
+ return waitForExecutionResult(sql, stackTraceMessage, futureResultSet, closeStatement);
+ }
+
+ private DatabricksResultSet waitForExecutionResult(
+ String sql,
+ String stackTraceMessage,
+ CompletableFuture futureResultSet,
+ boolean closeStatement)
+ throws SQLException {
try {
resultSet =
timeoutInSeconds == 0
@@ -938,6 +947,38 @@ DatabricksResultSet executeInternal(
return result;
}
+ boolean supportsNativeParameterBatching() {
+ try {
+ IDatabricksClient client = connection.getSession().getDatabricksClient();
+ return client.supportsNativeParameterBatching(connection.getSession().getComputeResource());
+ } catch (DatabricksSQLException e) {
+ LOGGER.warn("Unable to determine native batch capability, using legacy execution", e);
+ return false;
+ }
+ }
+
+ long[] executeNativeBatchInternal(String sql, List parameterSets)
+ throws SQLException {
+ resetForNewExecution();
+ DatabricksThreadContextHolder.setStatementType(StatementType.UPDATE);
+ String stackTraceMessage =
+ format(
+ "DatabricksResultSet executeNativeBatchInternal(String sql = %s, parameterSetCount = %s)",
+ sql, parameterSets.size());
+ LOGGER.debug(stackTraceMessage);
+ DatabricksResultSet result =
+ waitForExecutionResult(
+ sql,
+ stackTraceMessage,
+ getFutureBatchResult(sql, parameterSets, StatementType.UPDATE),
+ true);
+ try {
+ return result.getBatchUpdateCounts(parameterSets.size());
+ } catch (SQLException e) {
+ throw new NativeBatchResultException(e);
+ }
+ }
+
CompletableFuture getFutureResult(
String sql, Map params, StatementType statementType) {
return CompletableFuture.supplyAsync(
@@ -954,6 +995,21 @@ CompletableFuture getFutureResult(
executor);
}
+ private CompletableFuture getFutureBatchResult(
+ String sql, List parameterSets, StatementType statementType) {
+ return CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ String sqlString = escapeProcessing ? StringUtil.convertJdbcEscapeSequences(sql) : sql;
+ sqlString = StringUtil.removeRedundantEscapeClause(sqlString);
+ return getBatchResultFromClient(sqlString, parameterSets, statementType);
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ },
+ executor);
+ }
+
DatabricksResultSet getResultFromClient(
String sql, Map params, StatementType statementType)
throws SQLException {
@@ -968,6 +1024,19 @@ DatabricksResultSet getResultFromClient(
null /* metadataOperationType */);
}
+ private DatabricksResultSet getBatchResultFromClient(
+ String sql, List parameterSets, StatementType statementType)
+ throws SQLException {
+ IDatabricksClient client = connection.getSession().getDatabricksClient();
+ return client.executeStatementBatch(
+ sql,
+ connection.getSession().getComputeResource(),
+ parameterSets,
+ statementType,
+ connection.getSession(),
+ this);
+ }
+
void checkIfClosed() throws DatabricksSQLException {
if (isClosed) {
throw new DatabricksSQLException(
diff --git a/src/main/java/com/databricks/jdbc/api/impl/LegacyPreparedStatementBatchExecutor.java b/src/main/java/com/databricks/jdbc/api/impl/LegacyPreparedStatementBatchExecutor.java
index 1ed4eb9fa..05663b1c9 100644
--- a/src/main/java/com/databricks/jdbc/api/impl/LegacyPreparedStatementBatchExecutor.java
+++ b/src/main/java/com/databricks/jdbc/api/impl/LegacyPreparedStatementBatchExecutor.java
@@ -41,21 +41,26 @@ class LegacyPreparedStatementBatchExecutor {
this.statementExecutor = statementExecutor;
}
- long[] executeBatch(List batchParameterMetaData)
+ long[] executeBatch(List batchParameterSets)
throws DatabricksBatchUpdateException {
- if (batchParameterMetaData.isEmpty()) {
+ if (batchParameterSets.isEmpty()) {
return new long[0];
}
// Try to optimize INSERT statements with multi-row batching
if (canUseBatchedInsert()) {
- return executeBatchedInsert(batchParameterMetaData);
+ return executeBatchedInsert(batchParameterSets);
} else {
// Fall back to individual execution for non-INSERT or incompatible statements
- return executeIndividualStatements(batchParameterMetaData);
+ return executeIndividualStatements(batchParameterSets);
}
}
+ long[] executeIndividually(List batchParameterSets)
+ throws DatabricksBatchUpdateException {
+ return executeIndividualStatements(batchParameterSets);
+ }
+
private boolean canUseBatchedInsert() {
// Check if batched inserts are enabled via connection property
if (!connection.getConnectionContext().isBatchedInsertsEnabled()) {
@@ -76,9 +81,9 @@ private boolean canUseBatchedInsert() {
}
}
- private long[] executeBatchedInsert(List batchParameterMetaData)
+ private long[] executeBatchedInsert(List batchParameterSets)
throws DatabricksBatchUpdateException {
- LOGGER.debug("Executing batched INSERT with {} rows", batchParameterMetaData.size());
+ LOGGER.debug("Executing batched INSERT with {} rows", batchParameterSets.size());
try {
InsertStatementParser.InsertInfo insertInfo = InsertStatementParser.parseInsertStrict(sql);
@@ -98,7 +103,7 @@ private long[] executeBatchedInsert(List batchParam
"BatchInsertSize must be at least 1, got: " + configuredBatchSize,
DatabricksDriverErrorCode.INVALID_STATE);
}
- maxRowsPerChunk = Math.min(configuredBatchSize, batchParameterMetaData.size());
+ maxRowsPerChunk = Math.min(configuredBatchSize, batchParameterSets.size());
} else {
// When using parameterized queries, respect the 256 parameter limit from Databricks
// backend
@@ -113,13 +118,13 @@ private long[] executeBatchedInsert(List batchParam
}
}
- long[] allUpdateCounts = new long[batchParameterMetaData.size()];
+ long[] allUpdateCounts = new long[batchParameterSets.size()];
// Process batches in chunks
for (int startIndex = 0;
- startIndex < batchParameterMetaData.size();
+ startIndex < batchParameterSets.size();
startIndex += maxRowsPerChunk) {
- int endIndex = Math.min(startIndex + maxRowsPerChunk, batchParameterMetaData.size());
+ int endIndex = Math.min(startIndex + maxRowsPerChunk, batchParameterSets.size());
int chunkSize = endIndex - startIndex;
// Build multi-row INSERT for this chunk
@@ -128,7 +133,7 @@ private long[] executeBatchedInsert(List batchParam
int paramIndex = 1;
for (int i = startIndex; i < endIndex; i++) {
- DatabricksParameterMetaData batchParams = batchParameterMetaData.get(i);
+ BatchParameterSet batchParams = batchParameterSets.get(i);
Map rowParams = batchParams.getParameterBindings();
for (int j = 1; j <= rowParams.size(); j++) {
if (rowParams.containsKey(j)) {
@@ -161,7 +166,7 @@ private long[] executeBatchedInsert(List batchParam
} catch (Exception e) {
// Unexpected exception - mark all as failed
LOGGER.error("Unexpected error executing batched INSERT: {}", e.getMessage(), e);
- long[] failedCounts = new long[batchParameterMetaData.size()];
+ long[] failedCounts = new long[batchParameterSets.size()];
for (int i = 0; i < failedCounts.length; i++) {
failedCounts[i] = Statement.EXECUTE_FAILED;
}
@@ -170,22 +175,17 @@ private long[] executeBatchedInsert(List batchParam
}
}
- private long[] executeIndividualStatements(
- List batchParameterMetaData)
+ private long[] executeIndividualStatements(List batchParameterSets)
throws DatabricksBatchUpdateException {
- LOGGER.debug("Executing batch individually with {} statements", batchParameterMetaData.size());
- long[] largeUpdateCount = new long[batchParameterMetaData.size()];
+ LOGGER.debug("Executing batch individually with {} statements", batchParameterSets.size());
+ long[] largeUpdateCount = new long[batchParameterSets.size()];
- for (int sqlQueryIndex = 0; sqlQueryIndex < batchParameterMetaData.size(); sqlQueryIndex++) {
- DatabricksParameterMetaData databricksParameterMetaData =
- batchParameterMetaData.get(sqlQueryIndex);
+ for (int sqlQueryIndex = 0; sqlQueryIndex < batchParameterSets.size(); sqlQueryIndex++) {
+ BatchParameterSet batchParameterSet = batchParameterSets.get(sqlQueryIndex);
try {
DatabricksResultSet resultSet =
statementExecutor.execute(
- sql,
- databricksParameterMetaData.getParameterBindings(),
- StatementType.UPDATE,
- false);
+ sql, batchParameterSet.getParameterBindings(), StatementType.UPDATE, false);
largeUpdateCount[sqlQueryIndex] = resultSet.getUpdateCount();
} catch (Exception e) {
LOGGER.error(
diff --git a/src/main/java/com/databricks/jdbc/api/impl/NativeBatchResultException.java b/src/main/java/com/databricks/jdbc/api/impl/NativeBatchResultException.java
new file mode 100644
index 000000000..e121b0bb3
--- /dev/null
+++ b/src/main/java/com/databricks/jdbc/api/impl/NativeBatchResultException.java
@@ -0,0 +1,22 @@
+package com.databricks.jdbc.api.impl;
+
+import com.databricks.jdbc.exception.DatabricksSQLException;
+import com.databricks.jdbc.model.telemetry.enums.DatabricksDriverErrorCode;
+import java.sql.SQLException;
+
+/**
+ * Indicates that a native batch succeeded but its JDBC update counts could not be read.
+ *
+ *