From f72dc249da163e9a0a1345efba4d8f70cf086196 Mon Sep 17 00:00:00 2001 From: Vasiliy Mikhailov Date: Sun, 12 Jul 2026 04:04:04 +0000 Subject: [PATCH 1/2] Add unit tests for TargetInsertStatement bind input validation and bind-index ordering --- .../statement/TargetInsertStatementTest.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) 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 valueCaptor = ArgumentCaptor.forClass(Object.class); + ArgumentCaptor classCaptor = ArgumentCaptor.forClass(Class.class); + + Integer ttl = 7200; + Long writeTime = 42L; + + BoundStatement result = targetInsertStatement.bind(originRow, targetRow, ttl, writeTime, null, null); + assertNotNull(result); + + verify(boundStatement, times(targetColumnNames.size() + 2)).set(indexCaptor.capture(), valueCaptor.capture(), + classCaptor.capture()); + + List allIndices = indexCaptor.getAllValues(); + List allValues = valueCaptor.getAllValues(); + List allClasses = classCaptor.getAllValues(); + + // Columns are bound at sequential indices. + for (int i = 0; i < targetColumnNames.size(); i++) { + assertEquals(i, allIndices.get(i).intValue()); + } + + // TTL is bound immediately after the columns, with its value and Integer type. + int ttlIdx = targetColumnNames.size(); + assertEquals(ttlIdx, allIndices.get(ttlIdx).intValue()); + assertEquals(ttl, allValues.get(ttlIdx)); + assertEquals(Integer.class, allClasses.get(ttlIdx)); + + // WriteTime is bound at the next index, with its value and Long type. + int wtIdx = targetColumnNames.size() + 1; + assertEquals(wtIdx, allIndices.get(wtIdx).intValue()); + assertEquals(writeTime, allValues.get(wtIdx)); + assertEquals(Long.class, allClasses.get(wtIdx)); + } + + /** + * When converting an origin value throws, bind() wraps the failure in a RuntimeException whose message identifies + * the value being bound. + */ + @Test + public void bind_exceptionWithNonNullOriginValue() { + when(targetTable.getCorrespondingIndex(0)).thenReturn(0); + when(originTable.getAndConvertData(0, originRow)).thenThrow(new RuntimeException("binding error")); + + RuntimeException ex = assertThrows(RuntimeException.class, () -> targetInsertStatement.bind(originRow, + targetRow, 3600, 123456789L, getSampleData(explodeMapKeyType), getSampleData(explodeMapValueType))); + + assertTrue(ex.getMessage().contains("Error trying to bind value")); + } + } From c97ecf4e25bdec893178262fe0f2adb8a1a4d6f1 Mon Sep 17 00:00:00 2001 From: Vasiliy Mikhailov Date: Sun, 12 Jul 2026 22:18:28 +0300 Subject: [PATCH 2/2] Raise JaCoCo coverage thresholds to lock in the added test coverage With the new tests the build now reaches INSTRUCTION 54.1%, COMPLEXITY 43.7% and 1475 missed lines, so the coverage check is tightened accordingly (COMPLEXITY 0.43, INSTRUCTION 53.5%, LINE MISSEDCOUNT 1480), keeping a small margin so routine changes do not trip it. --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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