diff --git a/src/main/java/com/databricks/jdbc/api/impl/BatchParameterSet.java b/src/main/java/com/databricks/jdbc/api/impl/BatchParameterSet.java
new file mode 100644
index 000000000..f2fe4dd15
--- /dev/null
+++ b/src/main/java/com/databricks/jdbc/api/impl/BatchParameterSet.java
@@ -0,0 +1,77 @@
+package com.databricks.jdbc.api.impl;
+
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Immutable, position-ordered snapshot of one prepared-statement parameter set.
+ *
+ *
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.
+ */
+public final class BatchParameterSet {
+
+ private final List parameters;
+
+ private BatchParameterSet(List parameters) {
+ this.parameters = List.copyOf(parameters);
+ }
+
+ public static BatchParameterSet from(Map parameterBindings) {
+ Objects.requireNonNull(parameterBindings, "parameterBindings");
+ List orderedParameters =
+ parameterBindings.entrySet().stream()
+ .sorted(Comparator.comparingInt(Map.Entry::getKey))
+ .map(BatchParameterSet::snapshotParameter)
+ .collect(Collectors.toList());
+ return new BatchParameterSet(orderedParameters);
+ }
+
+ public List getParameters() {
+ return parameters;
+ }
+
+ public int size() {
+ return parameters.size();
+ }
+
+ public boolean isEmpty() {
+ return parameters.isEmpty();
+ }
+
+ private static ImmutableSqlParameter snapshotParameter(
+ Map.Entry entry) {
+ ImmutableSqlParameter parameter = entry.getValue();
+ return ImmutableSqlParameter.builder()
+ .cardinal(entry.getKey() - 1)
+ .type(parameter.type())
+ .value(snapshotValue(parameter.value()))
+ .build();
+ }
+
+ private static Object snapshotValue(Object value) {
+ if (value instanceof Timestamp) {
+ Timestamp timestamp = (Timestamp) value;
+ Timestamp copy = new Timestamp(timestamp.getTime());
+ copy.setNanos(timestamp.getNanos());
+ return copy;
+ }
+ if (value instanceof Date) {
+ return new Date(((Date) value).getTime());
+ }
+ if (value instanceof Time) {
+ return new Time(((Time) value).getTime());
+ }
+ if (value instanceof byte[]) {
+ return ((byte[]) value).clone();
+ }
+ return value;
+ }
+}
diff --git a/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java b/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java
index dfa4b70f9..62233e70c 100644
--- a/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java
+++ b/src/main/java/com/databricks/jdbc/api/impl/DatabricksConnectionContext.java
@@ -1480,6 +1480,11 @@ public boolean isBatchedInsertsEnabled() {
return getParameter(DatabricksJdbcUrlParams.ENABLE_BATCHED_INSERTS).equals("1");
}
+ @Override
+ public boolean isNativeBatchingEnabled() {
+ return getParameter(DatabricksJdbcUrlParams.ENABLE_NATIVE_BATCHING).equals("1");
+ }
+
@Override
public List getNonRowcountQueryPrefixes() {
String prefixesStr = getParameter(DatabricksJdbcUrlParams.NON_ROWCOUNT_QUERY_PREFIXES);
diff --git a/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java b/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java
index fb0d745a7..c3851296c 100644
--- a/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java
+++ b/src/main/java/com/databricks/jdbc/api/internal/IDatabricksConnectionContext.java
@@ -426,6 +426,9 @@ default int getHeartbeatIntervalSeconds() {
/** Returns whether batched INSERT optimization is enabled */
boolean isBatchedInsertsEnabled();
+ /** Returns whether native parameter batch execution is enabled */
+ boolean isNativeBatchingEnabled();
+
/** Returns whether transaction-related method calls should be ignored */
boolean getIgnoreTransactions();
diff --git a/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java b/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java
index 7fea2fe2c..bf6531432 100644
--- a/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java
+++ b/src/main/java/com/databricks/jdbc/common/DatabricksJdbcUrlParams.java
@@ -194,6 +194,7 @@ public enum DatabricksJdbcUrlParams {
"Timeout in seconds for metadata polling operations (e.g. GetTables, GetColumns). 0 means no timeout",
"300"),
ENABLE_BATCHED_INSERTS("EnableBatchedInserts", "Enable batched INSERT optimization", "0"),
+ ENABLE_NATIVE_BATCHING("EnableNativeBatching", "Enable native parameter batch execution", "0"),
ENABLE_SQL_VALIDATION_FOR_IS_VALID(
"EnableSQLValidationForIsValid",
"Enable SQL query execution for connection validation in isValid() method",
diff --git a/src/test/java/com/databricks/jdbc/api/impl/BatchParameterSetTest.java b/src/test/java/com/databricks/jdbc/api/impl/BatchParameterSetTest.java
new file mode 100644
index 000000000..5527324f4
--- /dev/null
+++ b/src/test/java/com/databricks/jdbc/api/impl/BatchParameterSetTest.java
@@ -0,0 +1,106 @@
+package com.databricks.jdbc.api.impl;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.databricks.jdbc.model.core.ColumnInfoTypeName;
+import java.sql.Timestamp;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class BatchParameterSetTest {
+
+ @Test
+ void ordersParametersByJdbcIndexAndUsesZeroBasedOrdinals() {
+ Map bindings = new HashMap<>();
+ bindings.put(3, parameter(99, "third", ColumnInfoTypeName.STRING));
+ bindings.put(1, parameter(99, "first", ColumnInfoTypeName.STRING));
+ bindings.put(2, parameter(99, "second", ColumnInfoTypeName.STRING));
+
+ BatchParameterSet parameterSet = BatchParameterSet.from(bindings);
+
+ assertEquals(List.of("first", "second", "third"), values(parameterSet));
+ assertEquals(List.of(0, 1, 2), ordinals(parameterSet));
+ }
+
+ @Test
+ void preservesSparseIndexesWithoutValidation() {
+ Map bindings = new HashMap<>();
+ bindings.put(3, parameter(3, "third", ColumnInfoTypeName.STRING));
+ bindings.put(1, parameter(1, "first", ColumnInfoTypeName.STRING));
+
+ BatchParameterSet parameterSet = BatchParameterSet.from(bindings);
+
+ assertEquals(List.of("first", "third"), values(parameterSet));
+ assertEquals(List.of(0, 2), ordinals(parameterSet));
+ }
+
+ @Test
+ void allowsEmptyParameterSet() {
+ BatchParameterSet parameterSet = BatchParameterSet.from(Map.of());
+
+ assertTrue(parameterSet.isEmpty());
+ assertEquals(0, parameterSet.size());
+ }
+
+ @Test
+ void snapshotsBindingsAndMutableValues() {
+ Timestamp timestamp = Timestamp.valueOf("2026-08-10 12:34:56.123456789");
+ byte[] bytes = new byte[] {1, 2, 3};
+ Map bindings = new HashMap<>();
+ bindings.put(1, parameter(1, timestamp, ColumnInfoTypeName.TIMESTAMP));
+ bindings.put(2, parameter(2, bytes, ColumnInfoTypeName.BINARY));
+
+ BatchParameterSet parameterSet = BatchParameterSet.from(bindings);
+ bindings.clear();
+ timestamp.setTime(0);
+ bytes[0] = 9;
+
+ assertFalse(parameterSet.isEmpty());
+ assertEquals(
+ Timestamp.valueOf("2026-08-10 12:34:56.123456789"),
+ parameterSet.getParameters().get(0).value());
+ assertArrayEquals(new byte[] {1, 2, 3}, (byte[]) parameterSet.getParameters().get(1).value());
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> parameterSet.getParameters().add(parameter(3, "extra", ColumnInfoTypeName.STRING)));
+ }
+
+ @Test
+ void preservesNullValueAndType() {
+ BatchParameterSet parameterSet =
+ BatchParameterSet.from(Map.of(1, parameter(1, null, ColumnInfoTypeName.DECIMAL)));
+
+ ImmutableSqlParameter parameter = parameterSet.getParameters().get(0);
+ assertNull(parameter.value());
+ assertEquals(ColumnInfoTypeName.DECIMAL, parameter.type());
+ assertEquals(0, parameter.cardinal());
+ }
+
+ private ImmutableSqlParameter parameter(
+ int cardinal, Object value, ColumnInfoTypeName columnInfoTypeName) {
+ return ImmutableSqlParameter.builder()
+ .cardinal(cardinal)
+ .value(value)
+ .type(columnInfoTypeName)
+ .build();
+ }
+
+ private List