diff --git a/pom.xml b/pom.xml
index 05a7fcec..1afaeaa4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -344,17 +344,17 @@
COMPLEXITY
COVEREDRATIO
- 0.42
+ 0.43
INSTRUCTION
COVEREDRATIO
- 53%
+ 53.5%
LINE
MISSEDCOUNT
- 1490
+ 1480
diff --git a/src/test/java/com/datastax/cdm/cql/statement/TargetInsertStatementTest.java b/src/test/java/com/datastax/cdm/cql/statement/TargetInsertStatementTest.java
index 0b58e07f..867c9bc8 100644
--- a/src/test/java/com/datastax/cdm/cql/statement/TargetInsertStatementTest.java
+++ b/src/test/java/com/datastax/cdm/cql/statement/TargetInsertStatementTest.java
@@ -22,8 +22,10 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
import com.datastax.cdm.cql.CommonMocks;
+import com.datastax.cdm.properties.KnownProperties;
import com.datastax.oss.driver.api.core.cql.*;
import com.datastax.oss.driver.api.core.type.DataTypes;
@@ -343,4 +345,160 @@ public void bind_withNonEmptyMap_shouldCallSet() {
verify(boundStatement, atLeastOnce()).set(anyInt(), any(), any(Class.class));
}
+ // ---- bind(): input validation and bind-index ordering ----
+
+ /**
+ * When TTL columns are enabled but no TTL value is supplied, bind() rejects the input with a RuntimeException
+ * naming the missing TTL property.
+ */
+ @Test
+ public void bind_checkBindInputsThrowsWhenTtlMissing() {
+ when(writetimeTTLFeature.isEnabled()).thenReturn(true);
+ when(writetimeTTLFeature.hasTTLColumns()).thenReturn(true);
+ targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
+
+ RuntimeException ex = assertThrows(RuntimeException.class,
+ () -> targetInsertStatement.bind(originRow, targetRow, null, null, null, null));
+ assertEquals(KnownProperties.ORIGIN_TTL_NAMES + " specified, but no TTL value was provided", ex.getMessage());
+ }
+
+ /**
+ * When writetime columns are enabled but no writetime value is supplied, bind() rejects the input with a
+ * RuntimeException naming the missing writetime property.
+ */
+ @Test
+ public void bind_checkBindInputsThrowsWhenWriteTimeMissing() {
+ when(writetimeTTLFeature.isEnabled()).thenReturn(true);
+ when(writetimeTTLFeature.hasWritetimeColumns()).thenReturn(true);
+ targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
+
+ RuntimeException ex = assertThrows(RuntimeException.class,
+ () -> targetInsertStatement.bind(originRow, targetRow, null, null, null, null));
+ assertEquals(KnownProperties.ORIGIN_WRITETIME_NAMES + " specified, but no WriteTime value was provided",
+ ex.getMessage());
+ }
+
+ /**
+ * bind() binds every target column exactly once at sequential indices 0, 1, 2, ...
+ */
+ @Test
+ public void bind_bindsAllColumnsAtSequentialIndices() {
+ targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
+
+ BoundStatement result = targetInsertStatement.bind(originRow, targetRow, null, null, null, null);
+ assertNotNull(result);
+
+ ArgumentCaptor indexCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(boundStatement, times(targetColumnNames.size())).set(indexCaptor.capture(), any(), any(Class.class));
+
+ List capturedIndices = indexCaptor.getAllValues();
+ for (int i = 0; i < capturedIndices.size(); i++) {
+ assertEquals(i, capturedIndices.get(i).intValue());
+ }
+ }
+
+ /**
+ * Constant columns are not bound: bind() sets only the non-constant columns, at sequential indices starting from
+ * zero.
+ */
+ @Test
+ public void bind_skipsConstantColumns() {
+ commonSetup(false, true, false);
+ targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
+
+ ArgumentCaptor indexCaptor = ArgumentCaptor.forClass(Integer.class);
+
+ targetInsertStatement.bind(originRow, targetRow, null, null, null, null);
+
+ // Constant columns are skipped; only the remaining columns are bound.
+ int expectedBindCount = targetColumnNames.size() - constantColumns.size();
+ verify(boundStatement, times(expectedBindCount)).set(indexCaptor.capture(), any(), any(Class.class));
+
+ List capturedIndices = indexCaptor.getAllValues();
+ for (int i = 0; i < capturedIndices.size(); i++) {
+ assertEquals(i, capturedIndices.get(i).intValue());
+ }
+ }
+
+ /**
+ * With an explode-map column present, bind() sets every target column at sequential indices starting from zero.
+ */
+ @Test
+ public void bind_bindsExplodeMapColumnsAtSequentialIndices() {
+ commonSetup(true, false, false);
+ targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
+
+ ArgumentCaptor indexCaptor = ArgumentCaptor.forClass(Integer.class);
+
+ targetInsertStatement.bind(originRow, targetRow, null, null, getSampleData(explodeMapKeyType),
+ getSampleData(explodeMapValueType));
+
+ verify(boundStatement, times(targetColumnNames.size())).set(indexCaptor.capture(), any(), any(Class.class));
+ List capturedIndices = indexCaptor.getAllValues();
+ for (int i = 0; i < capturedIndices.size(); i++) {
+ assertEquals(i, capturedIndices.get(i).intValue());
+ }
+ }
+
+ /**
+ * With both TTL and writetime enabled, bind() binds each target column at sequential indices and then appends the
+ * TTL value (as an Integer) and the writetime value (as a Long) at the two trailing indices, in that order.
+ */
+ @Test
+ public void bind_appendsTtlAndWritetimeAtTrailingIndicesWithTypes() {
+ when(writetimeTTLFeature.isEnabled()).thenReturn(true);
+ when(writetimeTTLFeature.hasTTLColumns()).thenReturn(true);
+ when(writetimeTTLFeature.hasWritetimeColumns()).thenReturn(true);
+ targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
+
+ ArgumentCaptor indexCaptor = ArgumentCaptor.forClass(Integer.class);
+ ArgumentCaptor