Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.scalar.db.api.UpdateIfExists;
import com.scalar.db.api.Upsert;
import com.scalar.db.common.AbstractDistributedTransactionManager;
import com.scalar.db.common.AbstractTransactionManagerCrudOperableScanner;
import com.scalar.db.common.error.CoreError;
import com.scalar.db.config.DatabaseConfig;
import com.scalar.db.exception.storage.ExecutionException;
Expand Down Expand Up @@ -165,7 +166,43 @@ public List<Result> scan(Scan scan) throws CrudException {

@Override
public Scanner getScanner(Scan scan) throws CrudException {
throw new UnsupportedOperationException("Implement later");
scan = copyAndSetTargetToIfNot(scan);

com.scalar.db.api.Scanner scanner;
try {
scanner = storage.scan(scan);
} catch (ExecutionException e) {
throw new CrudException(e.getMessage(), e, null);
}

return new AbstractTransactionManagerCrudOperableScanner() {
@Override
public Optional<Result> one() throws CrudException {
Copy link

Copilot AI May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The try–catch logic for wrapping ExecutionException into CrudException is duplicated in one() and all(). Consider extracting a helper method (e.g. wrapCrud(Supplier<T>)) to reduce code duplication and improve readability.

Copilot uses AI. Check for mistakes.
try {
return scanner.one();
} catch (ExecutionException e) {
throw new CrudException(e.getMessage(), e, null);
}
}

@Override
public List<Result> all() throws CrudException {
try {
return scanner.all();
} catch (ExecutionException e) {
throw new CrudException(e.getMessage(), e, null);
}
}

@Override
public void close() throws CrudException {
try {
scanner.close();
} catch (IOException e) {
throw new CrudException(e.getMessage(), e, null);
}
}
};
}

/** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.scalar.db.api.Result;
import com.scalar.db.api.Scan;
import com.scalar.db.api.Scanner;
import com.scalar.db.api.TransactionManagerCrudOperable;
import com.scalar.db.api.Update;
import com.scalar.db.api.Upsert;
import com.scalar.db.config.DatabaseConfig;
Expand All @@ -28,7 +29,9 @@
import com.scalar.db.exception.transaction.TransactionException;
import com.scalar.db.exception.transaction.UnsatisfiedConditionException;
import com.scalar.db.io.Key;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -151,6 +154,178 @@ public void scan_ExecutionExceptionThrownByStorage_ShouldThrowCrudException()
.hasCause(exception);
}

@Test
public void getScannerAndScannerOne_ShouldReturnScannerAndShouldReturnProperResult()
throws ExecutionException, TransactionException, IOException {
// Arrange
Scan scan =
Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build();

Result result1 = mock(Result.class);
Result result2 = mock(Result.class);
Result result3 = mock(Result.class);

Scanner scanner = mock(Scanner.class);
when(scanner.one())
.thenReturn(Optional.of(result1))
.thenReturn(Optional.of(result2))
.thenReturn(Optional.of(result3))
.thenReturn(Optional.empty());

when(storage.scan(scan)).thenReturn(scanner);

// Act Assert
TransactionManagerCrudOperable.Scanner actual = transactionManager.getScanner(scan);
assertThat(actual.one()).hasValue(result1);
assertThat(actual.one()).hasValue(result2);
assertThat(actual.one()).hasValue(result3);
assertThat(actual.one()).isEmpty();
actual.close();

verify(storage).scan(scan);
verify(scanner).close();
}

@Test
public void getScannerAndScannerAll_ShouldReturnScannerAndShouldReturnProperResults()
throws ExecutionException, TransactionException, IOException {
// Arrange
Scan scan =
Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build();

Result result1 = mock(Result.class);
Result result2 = mock(Result.class);
Result result3 = mock(Result.class);

Scanner scanner = mock(Scanner.class);
when(scanner.all()).thenReturn(Arrays.asList(result1, result2, result3));

when(storage.scan(scan)).thenReturn(scanner);

// Act Assert
TransactionManagerCrudOperable.Scanner actual = transactionManager.getScanner(scan);
assertThat(actual.all()).containsExactly(result1, result2, result3);
actual.close();

verify(storage).scan(scan);
verify(scanner).close();
}

@Test
public void getScannerAndScannerIterator_ShouldReturnScannerAndShouldReturnProperResults()
throws ExecutionException, TransactionException, IOException {
// Arrange
Scan scan =
Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build();

Result result1 = mock(Result.class);
Result result2 = mock(Result.class);
Result result3 = mock(Result.class);

Scanner scanner = mock(Scanner.class);
when(scanner.one())
.thenReturn(Optional.of(result1))
.thenReturn(Optional.of(result2))
.thenReturn(Optional.of(result3))
.thenReturn(Optional.empty());

when(storage.scan(scan)).thenReturn(scanner);

// Act Assert
TransactionManagerCrudOperable.Scanner actual = transactionManager.getScanner(scan);

Iterator<Result> iterator = actual.iterator();
assertThat(iterator.hasNext()).isTrue();
assertThat(iterator.next()).isEqualTo(result1);
assertThat(iterator.hasNext()).isTrue();
assertThat(iterator.next()).isEqualTo(result2);
assertThat(iterator.hasNext()).isTrue();
assertThat(iterator.next()).isEqualTo(result3);
assertThat(iterator.hasNext()).isFalse();
actual.close();

verify(storage).scan(scan);
verify(scanner).close();
}

@Test
public void getScanner_WhenExecutionExceptionThrownByJdbcService_ShouldThrowCrudException()
throws ExecutionException {
// Arrange
Scan scan =
Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build();

ExecutionException executionException = mock(ExecutionException.class);
when(executionException.getMessage()).thenReturn("error");
when(storage.scan(scan)).thenThrow(executionException);

// Act Assert
assertThatThrownBy(() -> transactionManager.getScanner(scan)).isInstanceOf(CrudException.class);
}

@Test
public void
getScannerAndScannerOne_WhenExecutionExceptionThrownByScannerOne_ShouldThrowCrudException()
throws ExecutionException, CrudException {
// Arrange
Scan scan =
Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build();

Scanner scanner = mock(Scanner.class);

ExecutionException executionException = mock(ExecutionException.class);
when(executionException.getMessage()).thenReturn("error");
when(scanner.one()).thenThrow(executionException);

when(storage.scan(scan)).thenReturn(scanner);

// Act Assert
TransactionManagerCrudOperable.Scanner actual = transactionManager.getScanner(scan);
assertThatThrownBy(actual::one).isInstanceOf(CrudException.class);
}

@Test
public void
getScannerAndScannerAll_WhenExecutionExceptionThrownByScannerAll_ShouldThrowCrudException()
throws ExecutionException, CrudException {
// Arrange
Scan scan =
Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build();

Scanner scanner = mock(Scanner.class);

ExecutionException executionException = mock(ExecutionException.class);
when(executionException.getMessage()).thenReturn("error");
when(scanner.all()).thenThrow(executionException);

when(storage.scan(scan)).thenReturn(scanner);

// Act Assert
TransactionManagerCrudOperable.Scanner actual = transactionManager.getScanner(scan);
assertThatThrownBy(actual::all).isInstanceOf(CrudException.class);
}

@Test
public void
getScannerAndScannerClose_WhenIOExceptionThrownByScannerClose_ShouldThrowCrudException()
throws ExecutionException, CrudException, IOException {
// Arrange
Scan scan =
Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build();

Scanner scanner = mock(Scanner.class);

IOException ioException = mock(IOException.class);
when(ioException.getMessage()).thenReturn("error");
doThrow(ioException).when(scanner).close();

when(storage.scan(scan)).thenReturn(scanner);

// Act Assert
TransactionManagerCrudOperable.Scanner actual = transactionManager.getScanner(scan);
assertThatThrownBy(actual::close).isInstanceOf(CrudException.class);
}

@Test
public void put_ShouldCallStorageProperly() throws ExecutionException, TransactionException {
// Arrange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,11 +410,6 @@ public void abort_forOngoingTransaction_ShouldAbortCorrectly() {}
@Test
public void rollback_forOngoingTransaction_ShouldRollbackCorrectly() {}

@Disabled("Implement later")
@Override
@Test
public void manager_getScanner_ScanGivenForCommittedRecord_ShouldReturnRecords() {}

@Disabled(
"Single CRUD operation transactions don't support executing multiple mutations in a transaction")
@Override
Expand Down