Skip to content

Commit 0b63a0f

Browse files
Add unit tests for TargetInsertStatement bind input validation and bind-index ordering
Cover TargetInsertStatement.bind(): rejection when TTL or writetime columns are enabled but no value is supplied, sequential bind indices for target columns, trailing TTL and writetime indices and types, constant-column skipping, explode-map binding, and exception wrapping when converting an origin value fails.
1 parent 4d9bedd commit 0b63a0f

1 file changed

Lines changed: 235 additions & 0 deletions

File tree

src/test/java/com/datastax/cdm/cql/statement/TargetInsertStatementTest.java

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222

2323
import org.junit.jupiter.api.BeforeEach;
2424
import org.junit.jupiter.api.Test;
25+
import org.mockito.ArgumentCaptor;
2526

2627
import com.datastax.cdm.cql.CommonMocks;
28+
import com.datastax.cdm.properties.KnownProperties;
2729
import com.datastax.oss.driver.api.core.cql.*;
2830
import com.datastax.oss.driver.api.core.type.DataTypes;
2931

@@ -343,4 +345,237 @@ public void bind_withNonEmptyMap_shouldCallSet() {
343345
verify(boundStatement, atLeastOnce()).set(anyInt(), any(), any(Class.class));
344346
}
345347

348+
// ---- bind(): input validation and bind-index ordering ----
349+
350+
/**
351+
* When TTL columns are enabled but no TTL value is supplied, bind() rejects the input with a RuntimeException
352+
* naming the missing TTL property, before any value is bound onto the statement.
353+
*/
354+
@Test
355+
public void bind_checkBindInputsThrowsWhenTtlMissing() {
356+
when(writetimeTTLFeature.isEnabled()).thenReturn(true);
357+
when(writetimeTTLFeature.hasTTLColumns()).thenReturn(true);
358+
targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
359+
360+
// If any value is set onto the statement, fail with a distinct exception so the assertion below
361+
// cannot pass by accident.
362+
doThrow(new IllegalStateException("TTL set called unexpectedly")).when(boundStatement).set(anyInt(), isNull(),
363+
any(Class.class));
364+
365+
RuntimeException ex = assertThrows(RuntimeException.class,
366+
() -> targetInsertStatement.bind(originRow, targetRow, null, null, null, null));
367+
assertEquals(KnownProperties.ORIGIN_TTL_NAMES + " specified, but no TTL value was provided", ex.getMessage());
368+
}
369+
370+
/**
371+
* When writetime columns are enabled but no writetime value is supplied, bind() rejects the input with a
372+
* RuntimeException naming the missing writetime property.
373+
*/
374+
@Test
375+
public void bind_checkBindInputsThrowsWhenWriteTimeMissing() {
376+
when(writetimeTTLFeature.isEnabled()).thenReturn(true);
377+
when(writetimeTTLFeature.hasWritetimeColumns()).thenReturn(true);
378+
targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
379+
380+
doThrow(new IllegalStateException("WriteTime set called unexpectedly")).when(boundStatement).set(anyInt(),
381+
isNull(), eq(Long.class));
382+
383+
RuntimeException ex = assertThrows(RuntimeException.class,
384+
() -> targetInsertStatement.bind(originRow, targetRow, null, null, null, null));
385+
assertEquals(KnownProperties.ORIGIN_WRITETIME_NAMES + " specified, but no WriteTime value was provided",
386+
ex.getMessage());
387+
}
388+
389+
/**
390+
* With both TTL and writetime enabled, bind() binds each target column at sequential indices and appends the TTL
391+
* and writetime values at the two trailing indices, each with its expected type.
392+
*/
393+
@Test
394+
public void bind_checkBindInputsValidatesAndSequentialBindsWithTtlAndWritetime() {
395+
when(writetimeTTLFeature.isEnabled()).thenReturn(true);
396+
when(writetimeTTLFeature.hasTTLColumns()).thenReturn(true);
397+
when(writetimeTTLFeature.hasWritetimeColumns()).thenReturn(true);
398+
399+
ArgumentCaptor<Integer> indexCaptor = ArgumentCaptor.forClass(Integer.class);
400+
ArgumentCaptor<Object> valueCaptor = ArgumentCaptor.forClass(Object.class);
401+
ArgumentCaptor<Class> classCaptor = ArgumentCaptor.forClass(Class.class);
402+
403+
targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
404+
405+
Integer ttl = 7200;
406+
Long writeTime = 42L;
407+
408+
BoundStatement result = targetInsertStatement.bind(originRow, targetRow, ttl, writeTime, null, null);
409+
assertNotNull(result);
410+
411+
verify(boundStatement, times(targetColumnNames.size() + 2)).set(indexCaptor.capture(), valueCaptor.capture(),
412+
classCaptor.capture());
413+
414+
List<Integer> allIndices = indexCaptor.getAllValues();
415+
List<Object> allValues = valueCaptor.getAllValues();
416+
List<Class> allClasses = classCaptor.getAllValues();
417+
418+
// Columns are bound at sequential indices.
419+
for (int i = 0; i < targetColumnNames.size(); i++) {
420+
assertEquals(i, allIndices.get(i).intValue());
421+
}
422+
423+
// TTL is bound at the index immediately after the columns, with its value and Integer type.
424+
int ttlIdx = targetColumnNames.size();
425+
assertEquals(ttlIdx, allIndices.get(ttlIdx).intValue());
426+
assertEquals(ttl, allValues.get(ttlIdx));
427+
assertEquals(Integer.class, allClasses.get(ttlIdx));
428+
429+
// WriteTime is bound at the next index, with its value and Long type.
430+
int wtIdx = targetColumnNames.size() + 1;
431+
assertEquals(wtIdx, allIndices.get(wtIdx).intValue());
432+
assertEquals(writeTime, allValues.get(wtIdx));
433+
assertEquals(Long.class, allClasses.get(wtIdx));
434+
}
435+
436+
/**
437+
* Constant columns are not bound: bind() sets only the non-constant columns, at sequential indices starting from
438+
* zero.
439+
*/
440+
@Test
441+
public void bind_withDebugDisabledConstantColumnsStillBindsCorrectly() {
442+
commonSetup(false, true, false);
443+
targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
444+
445+
ArgumentCaptor<Integer> indexCaptor = ArgumentCaptor.forClass(Integer.class);
446+
447+
targetInsertStatement.bind(originRow, targetRow, null, null, null, null);
448+
449+
// Constant columns are skipped.
450+
int expectedBindCount = targetColumnNames.size() - constantColumns.size();
451+
verify(boundStatement, times(expectedBindCount)).set(indexCaptor.capture(), any(), any(Class.class));
452+
453+
List<Integer> capturedIndices = indexCaptor.getAllValues();
454+
for (int i = 0; i < capturedIndices.size(); i++) {
455+
assertEquals(i, capturedIndices.get(i).intValue());
456+
}
457+
}
458+
459+
/**
460+
* With an explode-map column present, bind() sets every target column at sequential indices starting from zero.
461+
*/
462+
@Test
463+
public void bind_withExplodeMapSequentialIndices() {
464+
commonSetup(true, false, false);
465+
targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
466+
467+
ArgumentCaptor<Integer> indexCaptor = ArgumentCaptor.forClass(Integer.class);
468+
469+
targetInsertStatement.bind(originRow, targetRow, null, null, getSampleData(explodeMapKeyType),
470+
getSampleData(explodeMapValueType));
471+
472+
// Every column is bound at its sequential index.
473+
verify(boundStatement, times(targetColumnNames.size())).set(indexCaptor.capture(), any(), any(Class.class));
474+
List<Integer> capturedIndices = indexCaptor.getAllValues();
475+
for (int i = 0; i < capturedIndices.size(); i++) {
476+
assertEquals(i, capturedIndices.get(i).intValue());
477+
}
478+
}
479+
480+
/**
481+
* bind() sets every target column exactly once, at sequential indices 0, 1, 2, ...
482+
*/
483+
@Test
484+
public void bind_verifiesSequentialBindIndicesForAllColumns() {
485+
targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
486+
487+
BoundStatement result = targetInsertStatement.bind(originRow, targetRow, null, null, null, null);
488+
489+
assertNotNull(result);
490+
491+
ArgumentCaptor<Integer> indexCaptor = ArgumentCaptor.forClass(Integer.class);
492+
verify(boundStatement, times(targetColumnNames.size())).set(indexCaptor.capture(), any(), any(Class.class));
493+
494+
List<Integer> capturedIndices = indexCaptor.getAllValues();
495+
assertEquals(targetColumnNames.size(), capturedIndices.size());
496+
for (int i = 0; i < capturedIndices.size(); i++) {
497+
assertEquals(i, capturedIndices.get(i).intValue());
498+
}
499+
}
500+
501+
/**
502+
* The TTL value is bound at the index immediately following the last column, as an Integer.
503+
*/
504+
@Test
505+
public void bind_verifyTtlSetAtCorrectIndex() {
506+
when(writetimeTTLFeature.isEnabled()).thenReturn(true);
507+
when(writetimeTTLFeature.hasTTLColumns()).thenReturn(true);
508+
509+
ArgumentCaptor<Integer> indexCaptor = ArgumentCaptor.forClass(Integer.class);
510+
ArgumentCaptor<Object> valueCaptor = ArgumentCaptor.forClass(Object.class);
511+
ArgumentCaptor<Class> classCaptor = ArgumentCaptor.forClass(Class.class);
512+
513+
targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
514+
515+
targetInsertStatement.bind(originRow, targetRow, 3600, null, null, null);
516+
517+
verify(boundStatement, times(targetColumnNames.size() + 1)).set(indexCaptor.capture(), valueCaptor.capture(),
518+
classCaptor.capture());
519+
520+
List<Integer> indices = indexCaptor.getAllValues();
521+
List<Object> values = valueCaptor.getAllValues();
522+
List<Class> classes = classCaptor.getAllValues();
523+
524+
Integer ttlIndex = indices.get(indices.size() - 1);
525+
Object ttlValue = values.get(values.size() - 1);
526+
Class ttlClass = classes.get(classes.size() - 1);
527+
528+
assertEquals(targetColumnNames.size(), ttlIndex.intValue());
529+
assertEquals(3600, ttlValue);
530+
assertEquals(Integer.class, ttlClass);
531+
}
532+
533+
/**
534+
* The writetime value is bound at the trailing index, after all columns and the TTL, as a Long.
535+
*/
536+
@Test
537+
public void bind_verifyWritetimeSetAtCorrectIndex() {
538+
when(writetimeTTLFeature.isEnabled()).thenReturn(true);
539+
when(writetimeTTLFeature.hasTTLColumns()).thenReturn(true);
540+
when(writetimeTTLFeature.hasWritetimeColumns()).thenReturn(true);
541+
542+
ArgumentCaptor<Integer> indexCaptor = ArgumentCaptor.forClass(Integer.class);
543+
ArgumentCaptor<Object> valueCaptor = ArgumentCaptor.forClass(Object.class);
544+
ArgumentCaptor<Class> classCaptor = ArgumentCaptor.forClass(Class.class);
545+
546+
targetInsertStatement = new TargetInsertStatement(propertyHelper, targetSession);
547+
548+
targetInsertStatement.bind(originRow, targetRow, 3600, 999L, null, null);
549+
550+
verify(boundStatement, times(targetColumnNames.size() + 2)).set(indexCaptor.capture(), valueCaptor.capture(),
551+
classCaptor.capture());
552+
553+
List<Integer> indices = indexCaptor.getAllValues();
554+
List<Object> values = valueCaptor.getAllValues();
555+
List<Class> classes = classCaptor.getAllValues();
556+
557+
Integer wtIndex = indices.get(indices.size() - 1);
558+
Object wtValue = values.get(values.size() - 1);
559+
Class wtClass = classes.get(classes.size() - 1);
560+
561+
assertEquals(targetColumnNames.size() + 1, wtIndex.intValue());
562+
assertEquals(999L, wtValue);
563+
assertEquals(Long.class, wtClass);
564+
}
565+
566+
/**
567+
* When converting an origin value throws, bind() wraps the failure in a RuntimeException whose message identifies
568+
* the value being bound.
569+
*/
570+
@Test
571+
public void bind_exceptionWithNonNullOriginValue() {
572+
when(targetTable.getCorrespondingIndex(0)).thenReturn(0);
573+
when(originTable.getAndConvertData(0, originRow)).thenThrow(new RuntimeException("binding error"));
574+
575+
RuntimeException ex = assertThrows(RuntimeException.class, () -> targetInsertStatement.bind(originRow,
576+
targetRow, 3600, 123456789L, getSampleData(explodeMapKeyType), getSampleData(explodeMapValueType)));
577+
578+
assertTrue(ex.getMessage().contains("Error trying to bind value"));
579+
}
580+
346581
}

0 commit comments

Comments
 (0)