diff --git a/core/src/main/java/com/scalar/db/api/CrudOperable.java b/core/src/main/java/com/scalar/db/api/CrudOperable.java index ecbf67dd28..50f70a7c1f 100644 --- a/core/src/main/java/com/scalar/db/api/CrudOperable.java +++ b/core/src/main/java/com/scalar/db/api/CrudOperable.java @@ -156,6 +156,17 @@ public interface CrudOperable { */ void mutate(List mutations) throws E; + /** + * Executes multiple operations in a batch through a transaction with the specified list of {@link + * Operation} commands and returns a list of {@link BatchResult} that contains results of the + * operations. Note that the order of the results corresponds to the order of the operations. + * + * @param operations a list of {@code Operation} commands + * @return a list of {@code BatchResult} that contains results of the operations + * @throws E if any of the transaction CRUD operations fails + */ + List batch(List operations) throws E; + /** A scanner abstraction for iterating results. */ interface Scanner extends AutoCloseable, Iterable { /** @@ -183,4 +194,38 @@ interface Scanner extends AutoCloseable, Iterabl @Override void close() throws E; } + + /** A batch operation result returned by {@link CrudOperable#batch(List)}. */ + interface BatchResult { + /** + * Returns the type of the operation. + * + * @return the operation type + */ + Type getType(); + + /** + * Returns a result of a get operation. + * + * @return an {@code Optional} with the returned result + */ + Optional getGetResult(); + + /** + * Returns a list of results of a scan operation. + * + * @return a list of {@link Result} + */ + List getScanResult(); + + enum Type { + GET, + SCAN, + PUT, + INSERT, + UPSERT, + UPDATE, + DELETE + } + } } diff --git a/core/src/main/java/com/scalar/db/api/TransactionCrudOperable.java b/core/src/main/java/com/scalar/db/api/TransactionCrudOperable.java index f30127ea8a..b9b12ec48f 100644 --- a/core/src/main/java/com/scalar/db/api/TransactionCrudOperable.java +++ b/core/src/main/java/com/scalar/db/api/TransactionCrudOperable.java @@ -167,6 +167,22 @@ void delete(List deletes) void mutate(List mutations) throws CrudConflictException, CrudException, UnsatisfiedConditionException; + /** + * {@inheritDoc} + * + * @throws CrudConflictException if the transaction CRUD operation fails due to transient faults + * (e.g., a conflict error). You can retry the transaction from the beginning + * @throws CrudException if the transaction CRUD operation fails due to transient or nontransient + * faults. You can try retrying the transaction from the beginning, but the transaction may + * still fail if the cause is nontransient + * @throws UnsatisfiedConditionException if a condition is specified in a {@link Put}, {@link + * Delete}, or {@link Update} command, and if the condition is not satisfied or the entry does + * not exist + */ + @Override + List batch(List operations) + throws CrudConflictException, CrudException, UnsatisfiedConditionException; + interface Scanner extends CrudOperable.Scanner { /** * {@inheritDoc} diff --git a/core/src/main/java/com/scalar/db/api/TransactionManagerCrudOperable.java b/core/src/main/java/com/scalar/db/api/TransactionManagerCrudOperable.java index c03eef4034..bb714b93de 100644 --- a/core/src/main/java/com/scalar/db/api/TransactionManagerCrudOperable.java +++ b/core/src/main/java/com/scalar/db/api/TransactionManagerCrudOperable.java @@ -190,6 +190,24 @@ void mutate(List mutations) throws CrudConflictException, CrudException, UnsatisfiedConditionException, UnknownTransactionStatusException; + /** + * {@inheritDoc} + * + * @throws CrudConflictException if the transaction CRUD operation fails due to transient faults + * (e.g., a conflict error). You can retry the transaction from the beginning + * @throws CrudException if the transaction CRUD operation fails due to transient or nontransient + * faults. You can try retrying the transaction from the beginning, but the transaction may + * still fail if the cause is nontransient + * @throws UnsatisfiedConditionException if a condition is specified in a {@link Put}, {@link + * Delete}, or {@link Update} command, and if the condition is not satisfied or the entry does + * not exist + * @throws UnknownTransactionStatusException if the status of the commit is unknown + */ + @Override + List batch(List operations) + throws CrudConflictException, CrudException, UnsatisfiedConditionException, + UnknownTransactionStatusException; + interface Scanner extends CrudOperable.Scanner { /** * {@inheritDoc} diff --git a/core/src/main/java/com/scalar/db/common/AbstractDistributedTransaction.java b/core/src/main/java/com/scalar/db/common/AbstractDistributedTransaction.java index 88376ca461..df970f3500 100644 --- a/core/src/main/java/com/scalar/db/common/AbstractDistributedTransaction.java +++ b/core/src/main/java/com/scalar/db/common/AbstractDistributedTransaction.java @@ -1,15 +1,21 @@ package com.scalar.db.common; +import static com.google.common.base.Preconditions.checkArgument; + import com.scalar.db.api.Delete; import com.scalar.db.api.DistributedTransaction; import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; +import com.scalar.db.api.Result; import com.scalar.db.api.Scan; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; +import com.scalar.db.exception.transaction.CrudException; import com.scalar.db.util.ScalarDbUtils; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -59,8 +65,60 @@ public Optional getTable() { return tableName; } - protected List copyAndSetTargetToIfNot(List mutations) { - return ScalarDbUtils.copyAndSetTargetToIfNot(mutations, namespace, tableName); + @Override + public void mutate(List mutations) throws CrudException { + checkArgument(!mutations.isEmpty(), CoreError.EMPTY_MUTATIONS_SPECIFIED.buildMessage()); + for (Mutation mutation : mutations) { + if (mutation instanceof Put) { + put((Put) mutation); + } else if (mutation instanceof Delete) { + delete((Delete) mutation); + } else if (mutation instanceof Insert) { + insert((Insert) mutation); + } else if (mutation instanceof Upsert) { + upsert((Upsert) mutation); + } else { + assert mutation instanceof Update; + update((Update) mutation); + } + } + } + + @Override + public List batch(List operations) throws CrudException { + checkArgument(!operations.isEmpty(), CoreError.EMPTY_OPERATIONS_SPECIFIED.buildMessage()); + List ret = new ArrayList<>(); + for (Operation operation : operations) { + if (operation instanceof Get) { + Optional result = get((Get) operation); + ret.add(new BatchResultImpl(result)); + } else if (operation instanceof Scan) { + List results = scan((Scan) operation); + ret.add(new BatchResultImpl(results)); + } else if (operation instanceof Put) { + put((Put) operation); + ret.add(BatchResultImpl.PUT_BATCH_RESULT); + } else if (operation instanceof Insert) { + insert((Insert) operation); + ret.add(BatchResultImpl.INSERT_BATCH_RESULT); + } else if (operation instanceof Upsert) { + upsert((Upsert) operation); + ret.add(BatchResultImpl.UPSERT_BATCH_RESULT); + } else if (operation instanceof Update) { + update((Update) operation); + ret.add(BatchResultImpl.UPDATE_BATCH_RESULT); + } else if (operation instanceof Delete) { + delete((Delete) operation); + ret.add(BatchResultImpl.DELETE_BATCH_RESULT); + } else { + throw new AssertionError("Unknown operation: " + operation); + } + } + return ret; + } + + protected List copyAndSetTargetToIfNot(List operations) { + return ScalarDbUtils.copyAndSetTargetToIfNot(operations, namespace, tableName); } protected Get copyAndSetTargetToIfNot(Get get) { diff --git a/core/src/main/java/com/scalar/db/common/AbstractDistributedTransactionManager.java b/core/src/main/java/com/scalar/db/common/AbstractDistributedTransactionManager.java index 85a3a476a0..6592a763e8 100644 --- a/core/src/main/java/com/scalar/db/common/AbstractDistributedTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/AbstractDistributedTransactionManager.java @@ -5,7 +5,7 @@ import com.scalar.db.api.DistributedTransactionManager; import com.scalar.db.api.Get; import com.scalar.db.api.Insert; -import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Scan; import com.scalar.db.api.Update; @@ -73,8 +73,8 @@ public DistributedTransaction resume(String txId) throws TransactionNotFoundExce throw new UnsupportedOperationException("resume is not supported in this implementation"); } - protected List copyAndSetTargetToIfNot(List mutations) { - return ScalarDbUtils.copyAndSetTargetToIfNot(mutations, namespace, tableName); + protected List copyAndSetTargetToIfNot(List operations) { + return ScalarDbUtils.copyAndSetTargetToIfNot(operations, namespace, tableName); } protected Get copyAndSetTargetToIfNot(Get get) { diff --git a/core/src/main/java/com/scalar/db/common/AbstractTwoPhaseCommitTransaction.java b/core/src/main/java/com/scalar/db/common/AbstractTwoPhaseCommitTransaction.java index 2616702461..125fa4ef72 100644 --- a/core/src/main/java/com/scalar/db/common/AbstractTwoPhaseCommitTransaction.java +++ b/core/src/main/java/com/scalar/db/common/AbstractTwoPhaseCommitTransaction.java @@ -1,15 +1,21 @@ package com.scalar.db.common; +import static com.google.common.base.Preconditions.checkArgument; + import com.scalar.db.api.Delete; import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; +import com.scalar.db.api.Result; import com.scalar.db.api.Scan; import com.scalar.db.api.TwoPhaseCommitTransaction; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; +import com.scalar.db.exception.transaction.CrudException; import com.scalar.db.util.ScalarDbUtils; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -59,8 +65,60 @@ public Optional getTable() { return tableName; } - protected List copyAndSetTargetToIfNot(List mutations) { - return ScalarDbUtils.copyAndSetTargetToIfNot(mutations, namespace, tableName); + @Override + public void mutate(List mutations) throws CrudException { + checkArgument(!mutations.isEmpty(), CoreError.EMPTY_MUTATIONS_SPECIFIED.buildMessage()); + for (Mutation mutation : mutations) { + if (mutation instanceof Put) { + put((Put) mutation); + } else if (mutation instanceof Delete) { + delete((Delete) mutation); + } else if (mutation instanceof Insert) { + insert((Insert) mutation); + } else if (mutation instanceof Upsert) { + upsert((Upsert) mutation); + } else { + assert mutation instanceof Update; + update((Update) mutation); + } + } + } + + @Override + public List batch(List operations) throws CrudException { + checkArgument(!operations.isEmpty(), CoreError.EMPTY_OPERATIONS_SPECIFIED.buildMessage()); + List ret = new ArrayList<>(); + for (Operation operation : operations) { + if (operation instanceof Get) { + Optional result = get((Get) operation); + ret.add(new BatchResultImpl(result)); + } else if (operation instanceof Scan) { + List results = scan((Scan) operation); + ret.add(new BatchResultImpl(results)); + } else if (operation instanceof Put) { + put((Put) operation); + ret.add(BatchResultImpl.PUT_BATCH_RESULT); + } else if (operation instanceof Insert) { + insert((Insert) operation); + ret.add(BatchResultImpl.INSERT_BATCH_RESULT); + } else if (operation instanceof Upsert) { + upsert((Upsert) operation); + ret.add(BatchResultImpl.UPSERT_BATCH_RESULT); + } else if (operation instanceof Update) { + update((Update) operation); + ret.add(BatchResultImpl.UPDATE_BATCH_RESULT); + } else if (operation instanceof Delete) { + delete((Delete) operation); + ret.add(BatchResultImpl.DELETE_BATCH_RESULT); + } else { + throw new AssertionError("Unknown operation: " + operation); + } + } + return ret; + } + + protected List copyAndSetTargetToIfNot(List operations) { + return ScalarDbUtils.copyAndSetTargetToIfNot(operations, namespace, tableName); } protected Get copyAndSetTargetToIfNot(Get get) { diff --git a/core/src/main/java/com/scalar/db/common/AbstractTwoPhaseCommitTransactionManager.java b/core/src/main/java/com/scalar/db/common/AbstractTwoPhaseCommitTransactionManager.java index 631e009449..7c214ced90 100644 --- a/core/src/main/java/com/scalar/db/common/AbstractTwoPhaseCommitTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/AbstractTwoPhaseCommitTransactionManager.java @@ -3,7 +3,7 @@ import com.scalar.db.api.Delete; import com.scalar.db.api.Get; import com.scalar.db.api.Insert; -import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Scan; import com.scalar.db.api.TwoPhaseCommitTransaction; @@ -68,8 +68,8 @@ public TwoPhaseCommitTransaction resume(String txId) throws TransactionNotFoundE throw new UnsupportedOperationException("resume is not supported in this implementation"); } - protected List copyAndSetTargetToIfNot(List mutations) { - return ScalarDbUtils.copyAndSetTargetToIfNot(mutations, namespace, tableName); + protected List copyAndSetTargetToIfNot(List operations) { + return ScalarDbUtils.copyAndSetTargetToIfNot(operations, namespace, tableName); } protected Get copyAndSetTargetToIfNot(Get get) { diff --git a/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedDistributedTransactionManager.java b/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedDistributedTransactionManager.java index f5cdb0bcf9..f319a3317b 100644 --- a/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedDistributedTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedDistributedTransactionManager.java @@ -7,6 +7,7 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -21,10 +22,12 @@ import com.scalar.db.exception.transaction.UnknownTransactionStatusException; import com.scalar.db.util.ActiveExpiringMap; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; +import javax.annotation.Nonnull; import javax.annotation.concurrent.ThreadSafe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -103,6 +106,11 @@ public DistributedTransaction resume(String txId) throws TransactionNotFoundExce CoreError.TRANSACTION_NOT_FOUND.buildMessage(), txId)); } + /** + * The methods of this class are synchronized to be thread-safe because the rollback() method may + * be called from the expiration handler in a different thread while other methods are being + * executed. + */ @VisibleForTesting class ActiveTransaction extends DecoratedDistributedTransaction { @@ -124,7 +132,37 @@ public synchronized List scan(Scan scan) throws CrudException { @Override public synchronized Scanner getScanner(Scan scan) throws CrudException { - return super.getScanner(scan); + Scanner scanner = super.getScanner(scan); + return new Scanner() { + @Override + public Optional one() throws CrudException { + synchronized (ActiveTransaction.this) { + return scanner.one(); + } + } + + @Override + public List all() throws CrudException { + synchronized (ActiveTransaction.this) { + return scanner.all(); + } + } + + @Override + public void close() throws CrudException { + synchronized (ActiveTransaction.this) { + scanner.close(); + } + } + + @Nonnull + @Override + public Iterator iterator() { + synchronized (ActiveTransaction.this) { + return scanner.iterator(); + } + } + }; } /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @@ -173,6 +211,12 @@ public synchronized void mutate(List mutations) throws CrudE super.mutate(mutations); } + @Override + public synchronized List batch(List operations) + throws CrudException { + return super.batch(operations); + } + @Override public synchronized void commit() throws CommitException, UnknownTransactionStatusException { super.commit(); diff --git a/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedTwoPhaseCommitTransactionManager.java b/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedTwoPhaseCommitTransactionManager.java index af5d630ef2..eba73dabc4 100644 --- a/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedTwoPhaseCommitTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedTwoPhaseCommitTransactionManager.java @@ -5,6 +5,7 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -23,10 +24,12 @@ import com.scalar.db.exception.transaction.ValidationException; import com.scalar.db.util.ActiveExpiringMap; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; +import javax.annotation.Nonnull; import javax.annotation.concurrent.ThreadSafe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -109,6 +112,11 @@ public TwoPhaseCommitTransaction resume(String txId) throws TransactionNotFoundE CoreError.TRANSACTION_NOT_FOUND.buildMessage(), txId)); } + /** + * The methods of this class are synchronized to be thread-safe because the rollback() method may + * be called from the expiration handler in a different thread while other methods are being + * executed. + */ @VisibleForTesting class ActiveTransaction extends DecoratedTwoPhaseCommitTransaction { @@ -130,7 +138,37 @@ public synchronized List scan(Scan scan) throws CrudException { @Override public synchronized Scanner getScanner(Scan scan) throws CrudException { - return super.getScanner(scan); + Scanner scanner = super.getScanner(scan); + return new Scanner() { + @Override + public Optional one() throws CrudException { + synchronized (ActiveTransaction.this) { + return scanner.one(); + } + } + + @Override + public List all() throws CrudException { + synchronized (ActiveTransaction.this) { + return scanner.all(); + } + } + + @Override + public void close() throws CrudException { + synchronized (ActiveTransaction.this) { + scanner.close(); + } + } + + @Nonnull + @Override + public Iterator iterator() { + synchronized (ActiveTransaction.this) { + return scanner.iterator(); + } + } + }; } /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @@ -179,6 +217,12 @@ public synchronized void mutate(List mutations) throws CrudE super.mutate(mutations); } + @Override + public synchronized List batch(List operations) + throws CrudException { + return super.batch(operations); + } + @Override public synchronized void prepare() throws PreparationException { super.prepare(); diff --git a/core/src/main/java/com/scalar/db/common/BatchResultImpl.java b/core/src/main/java/com/scalar/db/common/BatchResultImpl.java new file mode 100644 index 0000000000..335c491072 --- /dev/null +++ b/core/src/main/java/com/scalar/db/common/BatchResultImpl.java @@ -0,0 +1,67 @@ +package com.scalar.db.common; + +import com.scalar.db.api.CrudOperable; +import com.scalar.db.api.Result; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import javax.annotation.Nullable; + +public class BatchResultImpl implements CrudOperable.BatchResult { + + public static final BatchResultImpl PUT_BATCH_RESULT = new BatchResultImpl(Type.PUT); + public static final BatchResultImpl INSERT_BATCH_RESULT = new BatchResultImpl(Type.INSERT); + public static final BatchResultImpl UPDATE_BATCH_RESULT = new BatchResultImpl(Type.UPDATE); + public static final BatchResultImpl UPSERT_BATCH_RESULT = new BatchResultImpl(Type.UPSERT); + public static final BatchResultImpl DELETE_BATCH_RESULT = new BatchResultImpl(Type.DELETE); + + private final Type type; + + @Nullable private final Optional getResult; + + @Nullable private final List scanResult; + + private BatchResultImpl(Type type) { + this.type = type; + this.getResult = null; + this.scanResult = null; + } + + public BatchResultImpl(Optional getResult) { + this.type = Type.GET; + this.getResult = Objects.requireNonNull(getResult); + this.scanResult = null; + } + + @SuppressFBWarnings("EI_EXPOSE_REP2") + public BatchResultImpl(List scanResult) { + this.type = Type.SCAN; + this.getResult = null; + this.scanResult = Objects.requireNonNull(scanResult); + } + + @Override + public Type getType() { + return type; + } + + @Override + public Optional getGetResult() { + if (getResult == null) { + throw new IllegalStateException( + CoreError.BATCH_RESULT_DOES_NOT_HAVE_GET_RESULT.buildMessage()); + } + return getResult; + } + + @SuppressFBWarnings("EI_EXPOSE_REP") + @Override + public List getScanResult() { + if (scanResult == null) { + throw new IllegalStateException( + CoreError.BATCH_RESULT_DOES_NOT_HAVE_SCAN_RESULT.buildMessage()); + } + return scanResult; + } +} diff --git a/core/src/main/java/com/scalar/db/common/CoreError.java b/core/src/main/java/com/scalar/db/common/CoreError.java index 259b029be9..5b3c48a5a2 100644 --- a/core/src/main/java/com/scalar/db/common/CoreError.java +++ b/core/src/main/java/com/scalar/db/common/CoreError.java @@ -808,6 +808,17 @@ public enum CoreError implements ScalarDbError { "Db2 does not support column type conversion from %s to %s", "", ""), + EMPTY_OPERATIONS_SPECIFIED(Category.USER_ERROR, "0241", "The operations are empty", "", ""), + SINGLE_CRUD_OPERATION_TRANSACTION_MULTIPLE_OPERATIONS_NOT_SUPPORTED( + Category.USER_ERROR, + "0242", + "Multiple operations are not supported in single CRUD operation transactions", + "", + ""), + BATCH_RESULT_DOES_NOT_HAVE_GET_RESULT( + Category.USER_ERROR, "0243", "This batch result doesn't have a get result", "", ""), + BATCH_RESULT_DOES_NOT_HAVE_SCAN_RESULT( + Category.USER_ERROR, "0244", "This batch result doesn't have a scan result", "", ""), // // Errors for the concurrency error category diff --git a/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransaction.java b/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransaction.java index ca997988c6..087624f748 100644 --- a/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransaction.java +++ b/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransaction.java @@ -5,6 +5,7 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -129,6 +130,11 @@ public void mutate(List mutations) throws CrudException { transaction.mutate(mutations); } + @Override + public List batch(List operations) throws CrudException { + return transaction.batch(operations); + } + @Override public void commit() throws CommitException, UnknownTransactionStatusException { transaction.commit(); diff --git a/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransactionManager.java b/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransactionManager.java index 9ade75d900..f8e426abbf 100644 --- a/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransactionManager.java @@ -7,6 +7,7 @@ import com.scalar.db.api.Insert; import com.scalar.db.api.Isolation; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -229,6 +230,12 @@ public void mutate(List mutations) transactionManager.mutate(mutations); } + @Override + public List batch(List operations) + throws CrudException, UnknownTransactionStatusException { + return transactionManager.batch(operations); + } + @Override public TransactionState getState(String txId) throws TransactionException { return transactionManager.getState(txId); diff --git a/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransaction.java b/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransaction.java index 04a48f4314..6f5f1fdcc6 100644 --- a/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransaction.java +++ b/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransaction.java @@ -4,6 +4,7 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -131,6 +132,11 @@ public void mutate(List mutations) throws CrudException { transaction.mutate(mutations); } + @Override + public List batch(List operations) throws CrudException { + return transaction.batch(operations); + } + @Override public void prepare() throws PreparationException { transaction.prepare(); diff --git a/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransactionManager.java b/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransactionManager.java index ce479795f1..cde37decdb 100644 --- a/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransactionManager.java @@ -4,6 +4,7 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -163,6 +164,12 @@ public void mutate(List mutations) transactionManager.mutate(mutations); } + @Override + public List batch(List operations) + throws CrudException, UnknownTransactionStatusException { + return transactionManager.batch(operations); + } + @Override public TransactionState getState(String txId) throws TransactionException { return transactionManager.getState(txId); diff --git a/core/src/main/java/com/scalar/db/common/ReadOnlyDistributedTransaction.java b/core/src/main/java/com/scalar/db/common/ReadOnlyDistributedTransaction.java index 5275c5e084..29047937fb 100644 --- a/core/src/main/java/com/scalar/db/common/ReadOnlyDistributedTransaction.java +++ b/core/src/main/java/com/scalar/db/common/ReadOnlyDistributedTransaction.java @@ -4,6 +4,7 @@ import com.scalar.db.api.DistributedTransaction; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; @@ -58,6 +59,17 @@ public void delete(Delete delete) throws CrudException { CoreError.MUTATION_NOT_ALLOWED_IN_READ_ONLY_TRANSACTION.buildMessage(getId())); } + @Override + public List batch(List operations) throws CrudException { + for (Operation operation : operations) { + if (operation instanceof Mutation) { + throw new IllegalStateException( + CoreError.MUTATION_NOT_ALLOWED_IN_READ_ONLY_TRANSACTION.buildMessage(getId())); + } + } + return super.batch(operations); + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override diff --git a/core/src/main/java/com/scalar/db/common/StateManagedDistributedTransactionManager.java b/core/src/main/java/com/scalar/db/common/StateManagedDistributedTransactionManager.java index 498b1e2e6b..19af37f613 100644 --- a/core/src/main/java/com/scalar/db/common/StateManagedDistributedTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/StateManagedDistributedTransactionManager.java @@ -7,6 +7,7 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -131,6 +132,12 @@ public void mutate(List mutations) throws CrudException { super.mutate(mutations); } + @Override + public List batch(List operations) throws CrudException { + checkIfActive(); + return super.batch(operations); + } + @Override public void commit() throws CommitException, UnknownTransactionStatusException { checkIfActive(); diff --git a/core/src/main/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManager.java b/core/src/main/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManager.java index 3935b0df32..e48a8fbdfa 100644 --- a/core/src/main/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManager.java @@ -5,6 +5,7 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -137,6 +138,12 @@ public void mutate(List mutations) throws CrudException { super.mutate(mutations); } + @Override + public List batch(List operations) throws CrudException { + checkIfActive(); + return super.batch(operations); + } + @Override public void prepare() throws PreparationException { checkIfActive(); diff --git a/core/src/main/java/com/scalar/db/exception/transaction/CommitException.java b/core/src/main/java/com/scalar/db/exception/transaction/CommitException.java index 2bb1312eb7..11281924ee 100644 --- a/core/src/main/java/com/scalar/db/exception/transaction/CommitException.java +++ b/core/src/main/java/com/scalar/db/exception/transaction/CommitException.java @@ -1,5 +1,6 @@ package com.scalar.db.exception.transaction; +import com.scalar.db.api.AuthAdmin; import javax.annotation.Nullable; /** @@ -29,4 +30,31 @@ public CommitException( boolean authenticationError) { super(message, cause, transactionId, authenticationError, false, false, null); } + + public CommitException( + String message, + @Nullable String transactionId, + boolean authenticationError, + boolean authorizationError, + @Nullable AuthAdmin.Privilege requiredPrivileges) { + super( + message, transactionId, authenticationError, authorizationError, false, requiredPrivileges); + } + + public CommitException( + String message, + Throwable cause, + @Nullable String transactionId, + boolean authenticationError, + boolean authorizationError, + @Nullable AuthAdmin.Privilege requiredPrivileges) { + super( + message, + cause, + transactionId, + authenticationError, + authorizationError, + false, + requiredPrivileges); + } } diff --git a/core/src/main/java/com/scalar/db/service/TransactionService.java b/core/src/main/java/com/scalar/db/service/TransactionService.java index 492dc6e9b5..6d07779729 100644 --- a/core/src/main/java/com/scalar/db/service/TransactionService.java +++ b/core/src/main/java/com/scalar/db/service/TransactionService.java @@ -8,6 +8,7 @@ import com.scalar.db.api.Insert; import com.scalar.db.api.Isolation; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -239,6 +240,12 @@ public void mutate(List mutations) manager.mutate(mutations); } + @Override + public List batch(List operations) + throws CrudException, UnknownTransactionStatusException { + return manager.batch(operations); + } + @Override public void close() { manager.close(); diff --git a/core/src/main/java/com/scalar/db/service/TwoPhaseCommitTransactionService.java b/core/src/main/java/com/scalar/db/service/TwoPhaseCommitTransactionService.java index 6cf6f68a98..1791062ef1 100644 --- a/core/src/main/java/com/scalar/db/service/TwoPhaseCommitTransactionService.java +++ b/core/src/main/java/com/scalar/db/service/TwoPhaseCommitTransactionService.java @@ -5,6 +5,7 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -176,6 +177,12 @@ public void mutate(List mutations) manager.mutate(mutations); } + @Override + public List batch(List operations) + throws CrudException, UnknownTransactionStatusException { + return manager.batch(operations); + } + @Override public void close() { manager.close(); diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommit.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommit.java index a9578c619f..3c8f96cabc 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommit.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommit.java @@ -161,25 +161,6 @@ public void update(Update update) throws CrudException { } } - @Override - public void mutate(List mutations) throws CrudException { - checkArgument(!mutations.isEmpty(), CoreError.EMPTY_MUTATIONS_SPECIFIED.buildMessage()); - for (Mutation m : mutations) { - if (m instanceof Put) { - put((Put) m); - } else if (m instanceof Delete) { - delete((Delete) m); - } else if (m instanceof Insert) { - insert((Insert) m); - } else if (m instanceof Upsert) { - upsert((Upsert) m); - } else { - assert m instanceof Update; - update((Update) m); - } - } - } - @Override public void commit() throws CommitException, UnknownTransactionStatusException { if (!context.areAllScannersClosed()) { diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManager.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManager.java index 3a244994cd..731afcabe8 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManager.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManager.java @@ -13,9 +13,11 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; +import com.scalar.db.api.Selection; import com.scalar.db.api.TransactionCrudOperable; import com.scalar.db.api.TransactionState; import com.scalar.db.api.Update; @@ -451,6 +453,13 @@ public void mutate(List mutations) false); } + @Override + public List batch(List operations) + throws CrudException, UnknownTransactionStatusException { + boolean readOnly = operations.stream().allMatch(o -> o instanceof Selection); + return executeTransaction(t -> t.batch(operations), readOnly); + } + private R executeTransaction( ThrowableFunction throwableFunction, boolean readOnly) diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommit.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommit.java index 84d7fb2e6b..f1a6b10625 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommit.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommit.java @@ -156,25 +156,6 @@ public void update(Update update) throws CrudException { } } - @Override - public void mutate(List mutations) throws CrudException { - checkArgument(!mutations.isEmpty()); - for (Mutation m : mutations) { - if (m instanceof Put) { - put((Put) m); - } else if (m instanceof Delete) { - delete((Delete) m); - } else if (m instanceof Insert) { - insert((Insert) m); - } else if (m instanceof Upsert) { - upsert((Upsert) m); - } else { - assert m instanceof Update; - update((Update) m); - } - } - } - @Override public void prepare() throws PreparationException { if (!context.areAllScannersClosed()) { diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManager.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManager.java index e42aa19f55..e3969083a0 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManager.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManager.java @@ -10,6 +10,7 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -397,6 +398,13 @@ public void mutate(List mutations) false); } + @Override + public List batch(List operations) + throws CrudException, UnknownTransactionStatusException { + boolean readOnly = operations.stream().allMatch(o -> o instanceof Get || o instanceof Scan); + return executeTransaction(t -> t.batch(operations), readOnly); + } + private R executeTransaction( ThrowableFunction throwableFunction, boolean readOnly) diff --git a/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransaction.java b/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransaction.java index b202d1f75b..91c0e62638 100644 --- a/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransaction.java +++ b/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransaction.java @@ -1,7 +1,5 @@ package com.scalar.db.transaction.jdbc; -import static com.google.common.base.Preconditions.checkArgument; - import com.scalar.db.api.ConditionBuilder; import com.scalar.db.api.ConditionalExpression; import com.scalar.db.api.Delete; @@ -281,25 +279,6 @@ public void update(Update update) throws CrudException { } } - @Override - public void mutate(List mutations) throws CrudException { - checkArgument(!mutations.isEmpty(), CoreError.EMPTY_MUTATIONS_SPECIFIED.buildMessage()); - for (Mutation mutation : mutations) { - if (mutation instanceof Put) { - put((Put) mutation); - } else if (mutation instanceof Delete) { - delete((Delete) mutation); - } else if (mutation instanceof Insert) { - insert((Insert) mutation); - } else if (mutation instanceof Upsert) { - upsert((Upsert) mutation); - } else { - assert mutation instanceof Update; - update((Update) mutation); - } - } - } - @Override public void commit() throws CommitException, UnknownTransactionStatusException { try { diff --git a/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransactionManager.java b/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransactionManager.java index 4031568a83..d59be41248 100644 --- a/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransactionManager.java +++ b/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransactionManager.java @@ -8,6 +8,7 @@ import com.scalar.db.api.Insert; import com.scalar.db.api.Isolation; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -377,6 +378,13 @@ public void mutate(List mutations) false); } + @Override + public List batch(List operations) + throws CrudException, UnknownTransactionStatusException { + boolean readOnly = operations.stream().allMatch(o -> o instanceof Get || o instanceof Scan); + return executeTransaction(t -> t.batch(operations), readOnly); + } + private R executeTransaction( ThrowableFunction throwableFunction, boolean readOnly) diff --git a/core/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManager.java b/core/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManager.java index 7d55093ab1..4ffe715c2b 100644 --- a/core/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManager.java +++ b/core/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManager.java @@ -15,6 +15,7 @@ import com.scalar.db.api.Isolation; import com.scalar.db.api.Mutation; import com.scalar.db.api.MutationCondition; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.PutBuilder; import com.scalar.db.api.PutIf; @@ -28,6 +29,7 @@ import com.scalar.db.api.Upsert; import com.scalar.db.common.AbstractDistributedTransactionManager; import com.scalar.db.common.AbstractTransactionManagerCrudOperableScanner; +import com.scalar.db.common.BatchResultImpl; import com.scalar.db.common.CoreError; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.exception.storage.ExecutionException; @@ -36,10 +38,12 @@ import com.scalar.db.exception.transaction.CrudException; import com.scalar.db.exception.transaction.TransactionException; import com.scalar.db.exception.transaction.TransactionNotFoundException; +import com.scalar.db.exception.transaction.UnknownTransactionStatusException; import com.scalar.db.exception.transaction.UnsatisfiedConditionException; import com.scalar.db.service.StorageFactory; import com.scalar.db.util.ScalarDbUtils; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -356,19 +360,55 @@ public void mutate(List mutations) throws CrudException { .buildMessage()); } - for (Mutation mutation : mutations) { - if (mutation instanceof Put) { - put((Put) mutation); - } else if (mutation instanceof Delete) { - delete((Delete) mutation); - } else if (mutation instanceof Insert) { - insert((Insert) mutation); - } else if (mutation instanceof Upsert) { - upsert((Upsert) mutation); - } else { - assert mutation instanceof Update; - update((Update) mutation); - } + Mutation mutation = mutations.get(0); + if (mutation instanceof Put) { + put((Put) mutation); + } else if (mutation instanceof Delete) { + delete((Delete) mutation); + } else if (mutation instanceof Insert) { + insert((Insert) mutation); + } else if (mutation instanceof Upsert) { + upsert((Upsert) mutation); + } else { + assert mutation instanceof Update; + update((Update) mutation); + } + } + + @Override + public List batch(List operations) + throws CrudException, UnknownTransactionStatusException { + checkArgument(!operations.isEmpty(), CoreError.EMPTY_OPERATIONS_SPECIFIED.buildMessage()); + if (operations.size() > 1) { + throw new UnsupportedOperationException( + CoreError.SINGLE_CRUD_OPERATION_TRANSACTION_MULTIPLE_OPERATIONS_NOT_SUPPORTED + .buildMessage()); + } + + Operation operation = operations.get(0); + if (operation instanceof Get) { + Optional result = get((Get) operation); + return Collections.singletonList(new BatchResultImpl(result)); + } else if (operation instanceof Scan) { + List results = scan((Scan) operation); + return Collections.singletonList(new BatchResultImpl(results)); + } else if (operation instanceof Put) { + put((Put) operation); + return Collections.singletonList(BatchResultImpl.PUT_BATCH_RESULT); + } else if (operation instanceof Insert) { + insert((Insert) operation); + return Collections.singletonList(BatchResultImpl.INSERT_BATCH_RESULT); + } else if (operation instanceof Upsert) { + upsert((Upsert) operation); + return Collections.singletonList(BatchResultImpl.UPSERT_BATCH_RESULT); + } else if (operation instanceof Update) { + update((Update) operation); + return Collections.singletonList(BatchResultImpl.UPDATE_BATCH_RESULT); + } else if (operation instanceof Delete) { + delete((Delete) operation); + return Collections.singletonList(BatchResultImpl.DELETE_BATCH_RESULT); + } else { + throw new AssertionError("Unknown operation: " + operation); } } diff --git a/core/src/main/java/com/scalar/db/util/ScalarDbUtils.java b/core/src/main/java/com/scalar/db/util/ScalarDbUtils.java index 70d5aa1303..5f70da4f50 100644 --- a/core/src/main/java/com/scalar/db/util/ScalarDbUtils.java +++ b/core/src/main/java/com/scalar/db/util/ScalarDbUtils.java @@ -11,7 +11,6 @@ import com.scalar.db.api.Insert; import com.scalar.db.api.InsertBuilder; import com.scalar.db.api.LikeExpression; -import com.scalar.db.api.Mutation; import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.PutBuilder; @@ -63,13 +62,33 @@ public final class ScalarDbUtils { private ScalarDbUtils() {} @SuppressWarnings("unchecked") - public static List copyAndSetTargetToIfNot( - List mutations, Optional namespace, Optional tableName) { - return mutations.stream() - .map(m -> (T) copyAndSetTargetToIfNot(m, namespace, tableName)) + public static List copyAndSetTargetToIfNot( + List operations, Optional namespace, Optional tableName) { + return operations.stream() + .map(o -> (T) copyAndSetTargetToIfNot(o, namespace, tableName)) .collect(Collectors.toList()); } + public static Operation copyAndSetTargetToIfNot( + Operation operation, Optional namespace, Optional tableName) { + if (operation instanceof Get) { + return copyAndSetTargetToIfNot((Get) operation, namespace, tableName); + } else if (operation instanceof Scan) { + return copyAndSetTargetToIfNot((Scan) operation, namespace, tableName); + } else if (operation instanceof Put) { + return copyAndSetTargetToIfNot((Put) operation, namespace, tableName); + } else if (operation instanceof Delete) { + return copyAndSetTargetToIfNot((Delete) operation, namespace, tableName); + } else if (operation instanceof Insert) { + return copyAndSetTargetToIfNot((Insert) operation, namespace, tableName); + } else if (operation instanceof Upsert) { + return copyAndSetTargetToIfNot((Upsert) operation, namespace, tableName); + } else { + assert operation instanceof Update; + return copyAndSetTargetToIfNot((Update) operation, namespace, tableName); + } + } + public static Get copyAndSetTargetToIfNot( Get get, Optional namespace, Optional tableName) { GetBuilder.BuildableGetOrGetWithIndexFromExisting builder = Get.newBuilder(get); // copy @@ -98,22 +117,6 @@ public static Scan copyAndSetTargetToIfNot( return ret; } - public static Mutation copyAndSetTargetToIfNot( - Mutation mutation, Optional namespace, Optional tableName) { - if (mutation instanceof Put) { - return copyAndSetTargetToIfNot((Put) mutation, namespace, tableName); - } else if (mutation instanceof Delete) { - return copyAndSetTargetToIfNot((Delete) mutation, namespace, tableName); - } else if (mutation instanceof Insert) { - return copyAndSetTargetToIfNot((Insert) mutation, namespace, tableName); - } else if (mutation instanceof Upsert) { - return copyAndSetTargetToIfNot((Upsert) mutation, namespace, tableName); - } else { - assert mutation instanceof Update; - return copyAndSetTargetToIfNot((Update) mutation, namespace, tableName); - } - } - public static Put copyAndSetTargetToIfNot( Put put, Optional namespace, Optional tableName) { PutBuilder.BuildableFromExisting builder = Put.newBuilder(put); // copy diff --git a/core/src/test/java/com/scalar/db/common/ReadOnlyDistributedTransactionTest.java b/core/src/test/java/com/scalar/db/common/ReadOnlyDistributedTransactionTest.java new file mode 100644 index 0000000000..8f0f9766f0 --- /dev/null +++ b/core/src/test/java/com/scalar/db/common/ReadOnlyDistributedTransactionTest.java @@ -0,0 +1,338 @@ +package com.scalar.db.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.scalar.db.api.CrudOperable.BatchResult; +import com.scalar.db.api.Delete; +import com.scalar.db.api.DistributedTransaction; +import com.scalar.db.api.Get; +import com.scalar.db.api.Insert; +import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; +import com.scalar.db.api.Put; +import com.scalar.db.api.Result; +import com.scalar.db.api.Scan; +import com.scalar.db.api.TransactionCrudOperable.Scanner; +import com.scalar.db.api.Update; +import com.scalar.db.api.Upsert; +import com.scalar.db.exception.transaction.CrudException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class ReadOnlyDistributedTransactionTest { + + private static final String TRANSACTION_ID = "test-tx-id"; + + @Mock private DistributedTransaction transaction; + + private ReadOnlyDistributedTransaction readOnlyTransaction; + + @BeforeEach + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this).close(); + + when(transaction.getId()).thenReturn(TRANSACTION_ID); + readOnlyTransaction = new ReadOnlyDistributedTransaction(transaction); + } + + @Test + public void get_ShouldDelegateToUnderlyingTransaction() throws CrudException { + // Arrange + Get get = mock(Get.class); + Optional expected = Optional.of(mock(Result.class)); + when(transaction.get(get)).thenReturn(expected); + + // Act + Optional actual = readOnlyTransaction.get(get); + + // Assert + assertThat(actual).isEqualTo(expected); + verify(transaction).get(get); + } + + @Test + public void scan_ShouldDelegateToUnderlyingTransaction() throws CrudException { + // Arrange + Scan scan = mock(Scan.class); + List expected = Arrays.asList(mock(Result.class), mock(Result.class)); + when(transaction.scan(scan)).thenReturn(expected); + + // Act + List actual = readOnlyTransaction.scan(scan); + + // Assert + assertThat(actual).isEqualTo(expected); + verify(transaction).scan(scan); + } + + @Test + public void getScanner_ShouldDelegateToUnderlyingTransaction() throws CrudException { + // Arrange + Scan scan = mock(Scan.class); + Scanner expected = mock(Scanner.class); + when(transaction.getScanner(scan)).thenReturn(expected); + + // Act + Scanner actual = readOnlyTransaction.getScanner(scan); + + // Assert + assertThat(actual).isEqualTo(expected); + verify(transaction).getScanner(scan); + } + + @Test + public void put_ShouldThrowIllegalStateException() { + // Arrange + Put put = mock(Put.class); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.put(put)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void put_WithList_ShouldThrowIllegalStateException() { + // Arrange + @SuppressWarnings("unchecked") + List puts = (List) mock(List.class); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.put(puts)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void insert_ShouldThrowIllegalStateException() { + // Arrange + Insert insert = mock(Insert.class); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.insert(insert)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void upsert_ShouldThrowIllegalStateException() { + // Arrange + Upsert upsert = mock(Upsert.class); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.upsert(upsert)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void update_ShouldThrowIllegalStateException() { + // Arrange + Update update = mock(Update.class); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.update(update)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void delete_ShouldThrowIllegalStateException() { + // Arrange + Delete delete = mock(Delete.class); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.delete(delete)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void delete_WithList_ShouldThrowIllegalStateException() { + // Arrange + @SuppressWarnings("unchecked") + List deletes = (List) mock(List.class); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.delete(deletes)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void mutate_ShouldThrowIllegalStateException() { + // Arrange + @SuppressWarnings("unchecked") + List mutations = (List) mock(List.class); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.mutate(mutations)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void batch_WithPut_ShouldThrowIllegalStateException() { + // Arrange + Put put = mock(Put.class); + List operations = Collections.singletonList(put); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.batch(operations)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void batch_WithInsert_ShouldThrowIllegalStateException() { + // Arrange + Insert insert = mock(Insert.class); + List operations = Collections.singletonList(insert); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.batch(operations)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void batch_WithUpsert_ShouldThrowIllegalStateException() { + // Arrange + Upsert upsert = mock(Upsert.class); + List operations = Collections.singletonList(upsert); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.batch(operations)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void batch_WithUpdate_ShouldThrowIllegalStateException() { + // Arrange + Update update = mock(Update.class); + List operations = Collections.singletonList(update); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.batch(operations)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void batch_WithDelete_ShouldThrowIllegalStateException() { + // Arrange + Delete delete = mock(Delete.class); + List operations = Collections.singletonList(delete); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.batch(operations)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void batch_WithMixedOperationsIncludingMutation_ShouldThrowIllegalStateException() { + // Arrange + Get get = mock(Get.class); + Put put = mock(Put.class); + List operations = Arrays.asList(get, put); + + // Act Assert + assertThatThrownBy(() -> readOnlyTransaction.batch(operations)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mutations are not allowed in read-only transactions") + .hasMessageContaining(TRANSACTION_ID); + } + + @Test + public void batch_WithOnlyReadOperations_ShouldDelegateToUnderlyingTransaction() + throws CrudException { + // Arrange + Get get = mock(Get.class); + Scan scan = mock(Scan.class); + List operations = Arrays.asList(get, scan); + @SuppressWarnings("unchecked") + List expected = (List) mock(List.class); + when(transaction.batch(any())).thenReturn(expected); + + // Act + List actual = readOnlyTransaction.batch(operations); + + // Assert + assertThat(actual).isEqualTo(expected); + verify(transaction).batch(operations); + } + + @Test + public void batch_WithEmptyOperations_ShouldDelegateToUnderlyingTransaction() + throws CrudException { + // Arrange + List operations = Collections.emptyList(); + @SuppressWarnings("unchecked") + List expected = (List) mock(List.class); + when(transaction.batch(any())).thenReturn(expected); + + // Act + List actual = readOnlyTransaction.batch(operations); + + // Assert + assertThat(actual).isEqualTo(expected); + verify(transaction).batch(operations); + } + + @Test + public void commit_ShouldDelegateToUnderlyingTransaction() { + // Act Assert + assertThatCode(() -> readOnlyTransaction.commit()).doesNotThrowAnyException(); + assertThatCode(() -> verify(transaction).commit()).doesNotThrowAnyException(); + } + + @Test + public void rollback_ShouldDelegateToUnderlyingTransaction() { + // Act Assert + assertThatCode(() -> readOnlyTransaction.rollback()).doesNotThrowAnyException(); + assertThatCode(() -> verify(transaction).rollback()).doesNotThrowAnyException(); + } + + @Test + public void abort_ShouldDelegateToUnderlyingTransaction() { + // Act Assert + assertThatCode(() -> readOnlyTransaction.abort()).doesNotThrowAnyException(); + assertThatCode(() -> verify(transaction).abort()).doesNotThrowAnyException(); + } + + @Test + public void getId_ShouldReturnTransactionId() { + // Act + String actual = readOnlyTransaction.getId(); + + // Assert + assertThat(actual).isEqualTo(TRANSACTION_ID); + } +} diff --git a/core/src/test/java/com/scalar/db/common/StateManagedDistributedTransactionManagerTest.java b/core/src/test/java/com/scalar/db/common/StateManagedDistributedTransactionManagerTest.java index 6f1efc08e7..6171701d3f 100644 --- a/core/src/test/java/com/scalar/db/common/StateManagedDistributedTransactionManagerTest.java +++ b/core/src/test/java/com/scalar/db/common/StateManagedDistributedTransactionManagerTest.java @@ -18,6 +18,7 @@ import com.scalar.db.api.Insert; import com.scalar.db.api.Isolation; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Scan; import com.scalar.db.api.SerializableStrategy; @@ -163,10 +164,13 @@ public void crud_ShouldNotThrowAnyException() { Update update = mock(Update.class); @SuppressWarnings("unchecked") List mutations = (List) mock(List.class); + @SuppressWarnings("unchecked") + List operations = (List) mock(List.class); // Act Assert assertThatCode(() -> transaction.get(get)).doesNotThrowAnyException(); assertThatCode(() -> transaction.scan(scan)).doesNotThrowAnyException(); + assertThatCode(() -> transaction.getScanner(scan)).doesNotThrowAnyException(); assertThatCode(() -> transaction.put(put)).doesNotThrowAnyException(); assertThatCode(() -> transaction.put(puts)).doesNotThrowAnyException(); assertThatCode(() -> transaction.delete(delete)).doesNotThrowAnyException(); @@ -175,6 +179,7 @@ public void crud_ShouldNotThrowAnyException() { assertThatCode(() -> transaction.upsert(upsert)).doesNotThrowAnyException(); assertThatCode(() -> transaction.update(update)).doesNotThrowAnyException(); assertThatCode(() -> transaction.mutate(mutations)).doesNotThrowAnyException(); + assertThatCode(() -> transaction.batch(operations)).doesNotThrowAnyException(); } @Test @@ -194,12 +199,16 @@ public void crud_AfterCommit_ShouldThrowIllegalStateException() Update update = mock(Update.class); @SuppressWarnings("unchecked") List mutations = (List) mock(List.class); + @SuppressWarnings("unchecked") + List operations = (List) mock(List.class); transaction.commit(); // Act Assert assertThatThrownBy(() -> transaction.get(get)).isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.scan(scan)).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> transaction.getScanner(scan)) + .isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.put(put)).isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.put(puts)).isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.delete(delete)) @@ -214,6 +223,8 @@ public void crud_AfterCommit_ShouldThrowIllegalStateException() .isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.mutate(mutations)) .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> transaction.batch(operations)) + .isInstanceOf(IllegalStateException.class); } @Test @@ -232,12 +243,16 @@ public void crud_AfterRollback_ShouldThrowIllegalStateException() throws Rollbac Update update = mock(Update.class); @SuppressWarnings("unchecked") List mutations = (List) mock(List.class); + @SuppressWarnings("unchecked") + List operations = (List) mock(List.class); transaction.rollback(); // Act Assert assertThatThrownBy(() -> transaction.get(get)).isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.scan(scan)).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> transaction.getScanner(scan)) + .isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.put(put)).isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.put(puts)).isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.delete(delete)) @@ -252,6 +267,8 @@ public void crud_AfterRollback_ShouldThrowIllegalStateException() throws Rollbac .isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.mutate(mutations)) .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> transaction.batch(operations)) + .isInstanceOf(IllegalStateException.class); } @Test diff --git a/core/src/test/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManagerTest.java b/core/src/test/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManagerTest.java index e1e01722a9..517c1fbb0d 100644 --- a/core/src/test/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManagerTest.java +++ b/core/src/test/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManagerTest.java @@ -14,6 +14,7 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Scan; import com.scalar.db.api.TwoPhaseCommitTransaction; @@ -121,10 +122,13 @@ public void crud_ShouldNotThrowAnyException() { Update update = mock(Update.class); @SuppressWarnings("unchecked") List mutations = (List) mock(List.class); + @SuppressWarnings("unchecked") + List operations = (List) mock(List.class); // Act Assert assertThatCode(() -> transaction.get(get)).doesNotThrowAnyException(); assertThatCode(() -> transaction.scan(scan)).doesNotThrowAnyException(); + assertThatCode(() -> transaction.getScanner(scan)).doesNotThrowAnyException(); assertThatCode(() -> transaction.put(put)).doesNotThrowAnyException(); assertThatCode(() -> transaction.put(puts)).doesNotThrowAnyException(); assertThatCode(() -> transaction.delete(delete)).doesNotThrowAnyException(); @@ -133,6 +137,7 @@ public void crud_ShouldNotThrowAnyException() { assertThatCode(() -> transaction.upsert(upsert)).doesNotThrowAnyException(); assertThatCode(() -> transaction.update(update)).doesNotThrowAnyException(); assertThatCode(() -> transaction.mutate(mutations)).doesNotThrowAnyException(); + assertThatCode(() -> transaction.batch(operations)).doesNotThrowAnyException(); } @Test @@ -151,12 +156,16 @@ public void crud_AfterPrepare_ShouldThrowIllegalStateException() throws Preparat Update update = mock(Update.class); @SuppressWarnings("unchecked") List mutations = (List) mock(List.class); + @SuppressWarnings("unchecked") + List operations = (List) mock(List.class); transaction.prepare(); // Act Assert assertThatThrownBy(() -> transaction.get(get)).isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.scan(scan)).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> transaction.getScanner(scan)) + .isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.put(put)).isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.put(puts)).isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.delete(delete)) @@ -171,6 +180,8 @@ public void crud_AfterPrepare_ShouldThrowIllegalStateException() throws Preparat .isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.mutate(mutations)) .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> transaction.batch(operations)) + .isInstanceOf(IllegalStateException.class); } @Test @@ -189,12 +200,16 @@ public void crud_AfterRollback_ShouldThrowIllegalStateException() throws Rollbac Update update = mock(Update.class); @SuppressWarnings("unchecked") List mutations = (List) mock(List.class); + @SuppressWarnings("unchecked") + List operations = (List) mock(List.class); transaction.rollback(); // Act Assert assertThatThrownBy(() -> transaction.get(get)).isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.scan(scan)).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> transaction.getScanner(scan)) + .isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.put(put)).isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.put(puts)).isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.delete(delete)) @@ -209,6 +224,8 @@ public void crud_AfterRollback_ShouldThrowIllegalStateException() throws Rollbac .isInstanceOf(IllegalStateException.class); assertThatThrownBy(() -> transaction.mutate(mutations)) .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> transaction.batch(operations)) + .isInstanceOf(IllegalStateException.class); } @Test diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManagerTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManagerTest.java index ab4840a739..5690ca5add 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManagerTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManagerTest.java @@ -13,6 +13,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.scalar.db.api.CrudOperable; import com.scalar.db.api.Delete; import com.scalar.db.api.DistributedStorage; import com.scalar.db.api.DistributedStorageAdmin; @@ -21,6 +22,7 @@ import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -1394,4 +1396,32 @@ public void mutate_ShouldMutate() throws TransactionException { verify(transaction).mutate(mutations); verify(transaction).commit(); } + + @Test + public void batch_ShouldBatch() throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + ConsensusCommitManager spied = spy(manager); + doReturn(transaction) + .when(spied) + .begin(anyString(), eq(Isolation.SNAPSHOT), eq(true), eq(true)); + + @SuppressWarnings("unchecked") + List operations = mock(List.class); + + @SuppressWarnings("unchecked") + List batchResults = mock(List.class); + + when(transaction.batch(operations)).thenReturn(batchResults); + + // Act + List actual = spied.batch(operations); + + // Assert + verify(spied).begin(anyString(), eq(Isolation.SNAPSHOT), eq(true), eq(true)); + verify(transaction).batch(operations); + verify(transaction).commit(); + assertThat(actual).isEqualTo(batchResults); + } } diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitTest.java index 656864f0f4..3370d54429 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitTest.java @@ -15,6 +15,7 @@ import static org.mockito.Mockito.when; import com.scalar.db.api.ConditionBuilder; +import com.scalar.db.api.CrudOperable; import com.scalar.db.api.Delete; import com.scalar.db.api.Get; import com.scalar.db.api.Insert; @@ -484,22 +485,205 @@ public void update_UpdateWithConditionGiven_ShouldCallCrudHandlerPut() } @Test - public void mutate_PutAndDeleteGiven_ShouldCallCrudHandlerPutAndDelete() + public void mutate_MutationsGiven_ShouldCallCrudHandlerPutAndDelete() throws CrudException, ExecutionException { // Arrange - Put put = preparePut(); - Delete delete = prepareDelete(); - doNothing().when(crud).put(put, context); - doNothing().when(crud).delete(delete, context); + Put put = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Insert insert = + Insert.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Upsert upsert = + Upsert.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_3)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Update update = + Update.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_4)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Delete delete = + Delete.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .build(); + + // Act + consensus.mutate(Arrays.asList(put, insert, upsert, update, delete)); + + // Assert + Put expectedPutFromInsert = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .enableInsertMode() + .build(); + Put expectedPutFromUpsert = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_3)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .enableImplicitPreRead() + .build(); + Put expectedPutFromUpdate = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_4)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .condition(ConditionBuilder.putIfExists()) + .enableImplicitPreRead() + .build(); + + verify(crud).put(put, context); + verify(crud).put(expectedPutFromInsert, context); + verify(crud).put(expectedPutFromUpsert, context); + verify(crud).put(expectedPutFromUpdate, context); + verify(crud).delete(delete, context); + verify(mutationOperationChecker).check(put); + verify(mutationOperationChecker).check(expectedPutFromInsert); + verify(mutationOperationChecker).check(expectedPutFromUpsert); + verify(mutationOperationChecker).check(expectedPutFromUpdate); + verify(mutationOperationChecker).check(delete); + } + + @Test + public void mutate_EmptyMutationsGiven_ShouldThrowIllegalArgumentException() { + // Arrange // Act Assert - consensus.mutate(Arrays.asList(put, delete)); + assertThatThrownBy(() -> consensus.mutate(Collections.emptyList())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void batch_OperationsGiven_ShouldCallCrudHandlerProperly() + throws CrudException, ExecutionException { + // Arrange + Get get = prepareGet(); + Scan scan = prepareScan(); + Put put = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Insert insert = + Insert.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Upsert upsert = + Upsert.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_3)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Update update = + Update.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_4)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Delete delete = + Delete.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .build(); + + TransactionResult result1 = mock(TransactionResult.class); + TransactionResult result2 = mock(TransactionResult.class); + TransactionResult result3 = mock(TransactionResult.class); + + when(crud.get(get, context)).thenReturn(Optional.of(result1)); + when(crud.scan(scan, context)).thenReturn(Arrays.asList(result2, result3)); + + // Act + List results = + consensus.batch(Arrays.asList(get, scan, put, insert, upsert, update, delete)); // Assert + Put expectedPutFromInsert = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .enableInsertMode() + .build(); + Put expectedPutFromUpsert = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_3)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .enableImplicitPreRead() + .build(); + Put expectedPutFromUpdate = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_4)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .condition(ConditionBuilder.putIfExists()) + .enableImplicitPreRead() + .build(); + + verify(crud).get(get, context); + verify(crud).scan(scan, context); verify(crud).put(put, context); + verify(crud).put(expectedPutFromInsert, context); + verify(crud).put(expectedPutFromUpsert, context); + verify(crud).put(expectedPutFromUpdate, context); verify(crud).delete(delete, context); verify(mutationOperationChecker).check(put); + verify(mutationOperationChecker).check(expectedPutFromInsert); + verify(mutationOperationChecker).check(expectedPutFromUpsert); + verify(mutationOperationChecker).check(expectedPutFromUpdate); verify(mutationOperationChecker).check(delete); + assertThat(results).hasSize(7); + assertThat(results.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.GET); + assertThat(results.get(0).getGetResult()).hasValue(result1); + assertThat(results.get(1).getType()).isEqualTo(CrudOperable.BatchResult.Type.SCAN); + assertThat(results.get(1).getScanResult()).containsExactly(result2, result3); + assertThat(results.get(2).getType()).isEqualTo(CrudOperable.BatchResult.Type.PUT); + assertThat(results.get(3).getType()).isEqualTo(CrudOperable.BatchResult.Type.INSERT); + assertThat(results.get(4).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPSERT); + assertThat(results.get(5).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPDATE); + assertThat(results.get(6).getType()).isEqualTo(CrudOperable.BatchResult.Type.DELETE); + } + + @Test + public void batch_EmptyOperationsGiven_ShouldThrowIllegalArgumentException() { + // Arrange + + // Act Assert + assertThatThrownBy(() -> consensus.batch(Collections.emptyList())) + .isInstanceOf(IllegalArgumentException.class); } @Test diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/SnapshotTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/SnapshotTest.java index ba894ebf4e..9d8b2dfa85 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/SnapshotTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/SnapshotTest.java @@ -258,18 +258,6 @@ private Put preparePut(String partitionKeyColumnValue, String clusteringKeyColum .build(); } - private Put prepareAnotherPut() { - Key partitionKey = Key.ofText(ANY_NAME_5, ANY_TEXT_5); - Key clusteringKey = Key.ofText(ANY_NAME_6, ANY_TEXT_6); - return Put.newBuilder() - .namespace(ANY_NAMESPACE_NAME) - .table(ANY_TABLE_NAME) - .partitionKey(partitionKey) - .clusteringKey(clusteringKey) - .consistency(Consistency.LINEARIZABLE) - .build(); - } - private Put preparePutWithPartitionKeyOnly() { Key partitionKey = Key.ofText(ANY_NAME_1, ANY_TEXT_1); return Put.newBuilder() diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManagerTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManagerTest.java index 19e878e747..efc9215af0 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManagerTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManagerTest.java @@ -12,12 +12,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.scalar.db.api.CrudOperable; import com.scalar.db.api.Delete; import com.scalar.db.api.DistributedStorage; import com.scalar.db.api.DistributedStorageAdmin; import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -1421,4 +1423,34 @@ public void mutate_ShouldMutate() throws TransactionException { verify(transaction).validate(); verify(transaction).commit(); } + + @Test + public void batch_ShouldBatch() throws TransactionException { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction) + .when(spied) + .begin(anyString(), eq(Isolation.SNAPSHOT), eq(true), eq(true)); + + @SuppressWarnings("unchecked") + List operations = mock(List.class); + + @SuppressWarnings("unchecked") + List batchResults = mock(List.class); + + when(transaction.batch(operations)).thenReturn(batchResults); + + // Act + List actual = spied.batch(operations); + + // Assert + verify(spied).begin(anyString(), eq(Isolation.SNAPSHOT), eq(true), eq(true)); + verify(transaction).batch(operations); + verify(transaction).prepare(); + verify(transaction).validate(); + verify(transaction).commit(); + assertThat(actual).isEqualTo(batchResults); + } } diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitTest.java index ab0d6be6d7..369c62812a 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitTest.java @@ -12,6 +12,7 @@ import static org.mockito.Mockito.when; import com.scalar.db.api.ConditionBuilder; +import com.scalar.db.api.CrudOperable; import com.scalar.db.api.Delete; import com.scalar.db.api.Get; import com.scalar.db.api.Insert; @@ -484,22 +485,207 @@ public void update_UpdateWithConditionGiven_ShouldCallCrudHandlerPut() } @Test - public void mutate_PutAndDeleteGiven_ShouldCallCrudHandlerPutAndDelete() + public void mutate_MutationsGiven_ShouldCallCrudHandlerPutAndDelete() throws CrudException, ExecutionException { // Arrange - Put put = preparePut(); - Delete delete = prepareDelete(); + Put put = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Insert insert = + Insert.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Upsert upsert = + Upsert.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_3)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Update update = + Update.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_4)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Delete delete = + Delete.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .build(); // Act - transaction.mutate(Arrays.asList(put, delete)); + transaction.mutate(Arrays.asList(put, insert, upsert, update, delete)); // Assert + Put expectedPutFromInsert = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .enableInsertMode() + .build(); + Put expectedPutFromUpsert = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_3)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .enableImplicitPreRead() + .build(); + Put expectedPutFromUpdate = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_4)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .condition(ConditionBuilder.putIfExists()) + .enableImplicitPreRead() + .build(); + verify(crud).put(put, context); + verify(crud).put(expectedPutFromInsert, context); + verify(crud).put(expectedPutFromUpsert, context); + verify(crud).put(expectedPutFromUpdate, context); verify(crud).delete(delete, context); verify(mutationOperationChecker).check(put); + verify(mutationOperationChecker).check(expectedPutFromInsert); + verify(mutationOperationChecker).check(expectedPutFromUpsert); + verify(mutationOperationChecker).check(expectedPutFromUpdate); verify(mutationOperationChecker).check(delete); } + @Test + public void mutate_EmptyMutationsGiven_ShouldThrowIllegalArgumentException() { + // Arrange + + // Act Assert + assertThatThrownBy(() -> transaction.mutate(Collections.emptyList())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void batch_OperationsGiven_ShouldCallCrudHandlerProperly() + throws CrudException, ExecutionException { + // Arrange + Get get = prepareGet(); + Scan scan = prepareScan(); + Put put = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Insert insert = + Insert.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Upsert upsert = + Upsert.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_3)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Update update = + Update.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_4)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Delete delete = + Delete.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .build(); + + TransactionResult result1 = mock(TransactionResult.class); + TransactionResult result2 = mock(TransactionResult.class); + TransactionResult result3 = mock(TransactionResult.class); + + when(crud.get(get, context)).thenReturn(Optional.of(result1)); + when(crud.scan(scan, context)).thenReturn(Arrays.asList(result2, result3)); + + // Act + List results = + transaction.batch(Arrays.asList(get, scan, put, insert, upsert, update, delete)); + + // Assert + Put expectedPutFromInsert = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .enableInsertMode() + .build(); + Put expectedPutFromUpsert = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_3)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .enableImplicitPreRead() + .build(); + Put expectedPutFromUpdate = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_4)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .condition(ConditionBuilder.putIfExists()) + .enableImplicitPreRead() + .build(); + + verify(crud).get(get, context); + verify(crud).scan(scan, context); + verify(crud).put(put, context); + verify(crud).put(expectedPutFromInsert, context); + verify(crud).put(expectedPutFromUpsert, context); + verify(crud).put(expectedPutFromUpdate, context); + verify(crud).delete(delete, context); + verify(mutationOperationChecker).check(put); + verify(mutationOperationChecker).check(expectedPutFromInsert); + verify(mutationOperationChecker).check(expectedPutFromUpsert); + verify(mutationOperationChecker).check(expectedPutFromUpdate); + verify(mutationOperationChecker).check(delete); + assertThat(results).hasSize(7); + assertThat(results.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.GET); + assertThat(results.get(0).getGetResult()).hasValue(result1); + assertThat(results.get(1).getType()).isEqualTo(CrudOperable.BatchResult.Type.SCAN); + assertThat(results.get(1).getScanResult()).containsExactly(result2, result3); + assertThat(results.get(2).getType()).isEqualTo(CrudOperable.BatchResult.Type.PUT); + assertThat(results.get(3).getType()).isEqualTo(CrudOperable.BatchResult.Type.INSERT); + assertThat(results.get(4).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPSERT); + assertThat(results.get(5).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPDATE); + assertThat(results.get(6).getType()).isEqualTo(CrudOperable.BatchResult.Type.DELETE); + } + + @Test + public void batch_EmptyOperationsGiven_ShouldThrowIllegalArgumentException() { + // Arrange + + // Act Assert + assertThatThrownBy(() -> transaction.batch(Collections.emptyList())) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void prepare_ProcessedCrudGiven_ShouldPrepareRecordsWithSnapshot() throws PreparationException, CrudException { diff --git a/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionManagerTest.java b/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionManagerTest.java index a4d13422df..98b19cfc9f 100644 --- a/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionManagerTest.java +++ b/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionManagerTest.java @@ -11,12 +11,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.scalar.db.api.CrudOperable; import com.scalar.db.api.Delete; import com.scalar.db.api.DistributedTransaction; import com.scalar.db.api.DistributedTransactionManager; import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Mutation; +import com.scalar.db.api.Operation; import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; @@ -1558,4 +1560,30 @@ public void mutate_ShouldMutate() throws TransactionException { verify(transaction).mutate(mutations); verify(transaction).commit(); } + + @Test + public void batch_ShouldBatch() throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + JdbcTransactionManager spied = spy(manager); + doReturn(transaction).when(spied).beginReadOnly(); + + @SuppressWarnings("unchecked") + List operations = mock(List.class); + + @SuppressWarnings("unchecked") + List batchResults = mock(List.class); + + when(transaction.batch(operations)).thenReturn(batchResults); + + // Act + List actual = spied.batch(operations); + + // Assert + verify(spied).beginReadOnly(); + verify(transaction).batch(operations); + verify(transaction).commit(); + assertThat(actual).isEqualTo(batchResults); + } } diff --git a/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionTest.java b/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionTest.java index 2423457186..15681ec0c6 100644 --- a/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionTest.java +++ b/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionTest.java @@ -10,7 +10,9 @@ import static org.mockito.Mockito.when; import com.scalar.db.api.ConditionBuilder; +import com.scalar.db.api.CrudOperable; import com.scalar.db.api.Delete; +import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.MutationCondition; import com.scalar.db.api.Put; @@ -31,7 +33,9 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; +import java.util.List; import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; @@ -880,4 +884,213 @@ public void update_UpdateGiven_WhenSQLExceptionThrownByJdbcService_ShouldThrowCr // Act Assert assertThatThrownBy(() -> transaction.update(update)).isInstanceOf(CrudException.class); } + + @Test + public void mutate_MutationsGiven_ShouldCallJdbcServiceProperly() + throws CrudException, ExecutionException, SQLException { + // Arrange + Put put = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Insert insert = + Insert.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Upsert upsert = + Upsert.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_3)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Update update = + Update.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_4)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Delete delete = + Delete.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .build(); + + Put expectedPutFromInsert = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .condition(ConditionBuilder.putIfNotExists()) + .build(); + Put expectedPutFromUpsert = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_3)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Put expectedPutFromUpdate = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_4)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .condition(ConditionBuilder.putIfExists()) + .build(); + + when(jdbcService.put(put, connection)).thenReturn(true); + when(jdbcService.put(expectedPutFromInsert, connection)).thenReturn(true); + when(jdbcService.put(expectedPutFromUpsert, connection)).thenReturn(true); + when(jdbcService.put(expectedPutFromUpdate, connection)).thenReturn(true); + when(jdbcService.delete(delete, connection)).thenReturn(true); + + // Act + transaction.mutate(Arrays.asList(put, insert, upsert, update, delete)); + + // Assert + verify(jdbcService).put(put, connection); + verify(jdbcService).put(expectedPutFromInsert, connection); + verify(jdbcService).put(expectedPutFromUpsert, connection); + verify(jdbcService).put(expectedPutFromUpdate, connection); + verify(jdbcService).delete(delete, connection); + } + + @Test + public void mutate_EmptyMutationsGiven_ShouldThrowIllegalArgumentException() { + // Arrange + + // Act Assert + assertThatThrownBy(() -> transaction.mutate(Collections.emptyList())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void batch_OperationsGiven_ShouldCallJdbcServiceProperly() + throws CrudException, ExecutionException, SQLException { + // Arrange + Get get = + Get.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .build(); + Scan scan = + Scan.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .build(); + Put put = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Insert insert = + Insert.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Upsert upsert = + Upsert.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_3)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Update update = + Update.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_4)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Delete delete = + Delete.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_1)) + .build(); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + Put expectedPutFromInsert = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_2)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .condition(ConditionBuilder.putIfNotExists()) + .build(); + Put expectedPutFromUpsert = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_3)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .build(); + Put expectedPutFromUpdate = + Put.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText(ANY_NAME_1, ANY_TEXT_4)) + .textValue(ANY_NAME_3, ANY_TEXT_3) + .condition(ConditionBuilder.putIfExists()) + .build(); + + when(jdbcService.get(get, connection)).thenReturn(Optional.of(result1)); + when(jdbcService.scan(scan, connection)).thenReturn(Arrays.asList(result2, result3)); + when(jdbcService.put(put, connection)).thenReturn(true); + when(jdbcService.put(expectedPutFromInsert, connection)).thenReturn(true); + when(jdbcService.put(expectedPutFromUpsert, connection)).thenReturn(true); + when(jdbcService.put(expectedPutFromUpdate, connection)).thenReturn(true); + when(jdbcService.delete(delete, connection)).thenReturn(true); + + // Act + List results = + transaction.batch(Arrays.asList(get, scan, put, insert, upsert, update, delete)); + + // Assert + verify(jdbcService).get(get, connection); + verify(jdbcService).scan(scan, connection); + verify(jdbcService).put(put, connection); + verify(jdbcService).put(expectedPutFromInsert, connection); + verify(jdbcService).put(expectedPutFromUpsert, connection); + verify(jdbcService).put(expectedPutFromUpdate, connection); + verify(jdbcService).delete(delete, connection); + assertThat(results).hasSize(7); + assertThat(results.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.GET); + assertThat(results.get(0).getGetResult()).hasValue(result1); + assertThat(results.get(1).getType()).isEqualTo(CrudOperable.BatchResult.Type.SCAN); + assertThat(results.get(1).getScanResult()).containsExactly(result2, result3); + assertThat(results.get(2).getType()).isEqualTo(CrudOperable.BatchResult.Type.PUT); + assertThat(results.get(3).getType()).isEqualTo(CrudOperable.BatchResult.Type.INSERT); + assertThat(results.get(4).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPSERT); + assertThat(results.get(5).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPDATE); + assertThat(results.get(6).getType()).isEqualTo(CrudOperable.BatchResult.Type.DELETE); + } + + @Test + public void batch_EmptyOperationsGiven_ShouldThrowIllegalArgumentException() { + // Arrange + + // Act Assert + assertThatThrownBy(() -> transaction.batch(Collections.emptyList())) + .isInstanceOf(IllegalArgumentException.class); + } } diff --git a/core/src/test/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManagerTest.java b/core/src/test/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManagerTest.java index 42ef3d1c96..02fc22a191 100644 --- a/core/src/test/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManagerTest.java +++ b/core/src/test/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManagerTest.java @@ -10,6 +10,7 @@ import com.scalar.db.api.ConditionBuilder; import com.scalar.db.api.Consistency; +import com.scalar.db.api.CrudOperable; import com.scalar.db.api.Delete; import com.scalar.db.api.DistributedStorage; import com.scalar.db.api.Get; @@ -31,6 +32,7 @@ import com.scalar.db.io.Key; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -745,4 +747,370 @@ public void delete_ExecutionExceptionThrownByStorage_ShouldThrowCrudException() assertThatThrownBy(() -> transactionManager.delete(delete)) .isInstanceOf(UnsatisfiedConditionException.class); } + + @Test + public void mutate_WithPut_ShouldCallStorageProperly() throws Exception { + // Arrange + Put put = + Put.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .build(); + Put putWithLinearizableConsistency = + Put.newBuilder(put).consistency(Consistency.LINEARIZABLE).build(); + + // Act + transactionManager.mutate(Collections.singletonList(put)); + + // Assert + verify(storage).put(putWithLinearizableConsistency); + } + + @Test + public void mutate_WithInsert_ShouldCallStorageProperly() throws Exception { + // Arrange + Insert insert = + Insert.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .build(); + + // Act + transactionManager.mutate(Collections.singletonList(insert)); + + // Assert + verify(storage) + .put( + Put.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .condition(ConditionBuilder.putIfNotExists()) + .consistency(Consistency.LINEARIZABLE) + .build()); + } + + @Test + public void mutate_WithUpsert_ShouldCallStorageProperly() throws Exception { + // Arrange + Upsert upsert = + Upsert.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .build(); + + // Act + transactionManager.mutate(Collections.singletonList(upsert)); + + // Assert + verify(storage) + .put( + Put.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .consistency(Consistency.LINEARIZABLE) + .build()); + } + + @Test + public void mutate_WithUpdate_ShouldCallStorageProperly() throws Exception { + // Arrange + Update update = + Update.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .build(); + + // Act + transactionManager.mutate(Collections.singletonList(update)); + + // Assert + verify(storage) + .put( + Put.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .condition(ConditionBuilder.putIfExists()) + .consistency(Consistency.LINEARIZABLE) + .build()); + } + + @Test + public void mutate_WithDelete_ShouldCallStorageProperly() throws Exception { + // Arrange + Delete delete = + Delete.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build(); + Delete deleteWithLinearizableConsistency = + Delete.newBuilder(delete).consistency(Consistency.LINEARIZABLE).build(); + + // Act + transactionManager.mutate(Collections.singletonList(delete)); + + // Assert + verify(storage).delete(deleteWithLinearizableConsistency); + } + + @Test + public void mutate_EmptyMutations_ShouldThrowIllegalArgumentException() { + // Arrange + + // Act Assert + assertThatThrownBy(() -> transactionManager.mutate(Collections.emptyList())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void mutate_MultipleMutations_ShouldThrowUnsupportedOperationException() { + // Arrange + Put put1 = + Put.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .build(); + Put put2 = + Put.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 1)) + .intValue("col", 1) + .build(); + + // Act Assert + assertThatThrownBy(() -> transactionManager.mutate(Arrays.asList(put1, put2))) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + public void batch_WithGetOperation_ShouldReturnBatchResult() throws Exception { + // Arrange + Get get = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build(); + Get getWithLinearizableConsistency = + Get.newBuilder(get).consistency(Consistency.LINEARIZABLE).build(); + + Result result = mock(Result.class); + when(storage.get(getWithLinearizableConsistency)).thenReturn(Optional.of(result)); + + // Act + List batchResults = + transactionManager.batch(Collections.singletonList(get)); + + // Assert + verify(storage).get(getWithLinearizableConsistency); + assertThat(batchResults).hasSize(1); + assertThat(batchResults.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.GET); + assertThat(batchResults.get(0).getGetResult()).isEqualTo(Optional.of(result)); + } + + @Test + public void batch_WithScanOperation_ShouldReturnBatchResult() throws Exception { + // Arrange + Scan scan = + Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build(); + Scan scanWithLinearizableConsistency = + Scan.newBuilder(scan).consistency(Consistency.LINEARIZABLE).build(); + + List results = Arrays.asList(mock(Result.class), mock(Result.class)); + Scanner scanner = mock(Scanner.class); + when(scanner.all()).thenReturn(results); + when(storage.scan(scanWithLinearizableConsistency)).thenReturn(scanner); + + // Act + List batchResults = + transactionManager.batch(Collections.singletonList(scan)); + + // Assert + verify(storage).scan(scanWithLinearizableConsistency); + assertThat(batchResults).hasSize(1); + assertThat(batchResults.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.SCAN); + assertThat(batchResults.get(0).getScanResult()).isEqualTo(results); + } + + @Test + public void batch_WithPutOperation_ShouldReturnEmptyBatchResult() throws Exception { + // Arrange + Put put = + Put.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .build(); + Put putWithLinearizableConsistency = + Put.newBuilder(put).consistency(Consistency.LINEARIZABLE).build(); + + // Act + List batchResults = + transactionManager.batch(Collections.singletonList(put)); + + // Assert + verify(storage).put(putWithLinearizableConsistency); + assertThat(batchResults).hasSize(1); + assertThat(batchResults.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.PUT); + // Empty batch result should throw IllegalStateException when accessing results + assertThatThrownBy(() -> batchResults.get(0).getGetResult()) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> batchResults.get(0).getScanResult()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void batch_WithInsertOperation_ShouldReturnEmptyBatchResult() throws Exception { + // Arrange + Insert insert = + Insert.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .build(); + + // Act + List batchResults = + transactionManager.batch(Collections.singletonList(insert)); + + // Assert + verify(storage) + .put( + Put.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .condition(ConditionBuilder.putIfNotExists()) + .consistency(Consistency.LINEARIZABLE) + .build()); + assertThat(batchResults).hasSize(1); + assertThat(batchResults.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.INSERT); + assertThatThrownBy(() -> batchResults.get(0).getGetResult()) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> batchResults.get(0).getScanResult()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void batch_WithUpsertOperation_ShouldReturnEmptyBatchResult() throws Exception { + // Arrange + Upsert upsert = + Upsert.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .build(); + + // Act + List batchResults = + transactionManager.batch(Collections.singletonList(upsert)); + + // Assert + verify(storage) + .put( + Put.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .consistency(Consistency.LINEARIZABLE) + .build()); + assertThat(batchResults).hasSize(1); + assertThat(batchResults.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPSERT); + assertThatThrownBy(() -> batchResults.get(0).getGetResult()) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> batchResults.get(0).getScanResult()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void batch_WithUpdateOperation_ShouldReturnEmptyBatchResult() throws Exception { + // Arrange + Update update = + Update.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .build(); + + // Act + List batchResults = + transactionManager.batch(Collections.singletonList(update)); + + // Assert + verify(storage) + .put( + Put.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofInt("id", 0)) + .intValue("col", 0) + .condition(ConditionBuilder.putIfExists()) + .consistency(Consistency.LINEARIZABLE) + .build()); + assertThat(batchResults).hasSize(1); + assertThat(batchResults.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPDATE); + assertThatThrownBy(() -> batchResults.get(0).getGetResult()) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> batchResults.get(0).getScanResult()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void batch_WithDeleteOperation_ShouldReturnEmptyBatchResult() throws Exception { + // Arrange + Delete delete = + Delete.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build(); + Delete deleteWithLinearizableConsistency = + Delete.newBuilder(delete).consistency(Consistency.LINEARIZABLE).build(); + + // Act + List batchResults = + transactionManager.batch(Collections.singletonList(delete)); + + // Assert + verify(storage).delete(deleteWithLinearizableConsistency); + assertThat(batchResults).hasSize(1); + assertThat(batchResults.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.DELETE); + assertThatThrownBy(() -> batchResults.get(0).getGetResult()) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> batchResults.get(0).getScanResult()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void batch_EmptyOperations_ShouldThrowIllegalArgumentException() { + // Arrange + + // Act Assert + assertThatThrownBy(() -> transactionManager.batch(Collections.emptyList())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void batch_MultipleOperations_ShouldThrowUnsupportedOperationException() { + // Arrange + Get get1 = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build(); + Get get2 = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 1)).build(); + + // Act Assert + assertThatThrownBy(() -> transactionManager.batch(Arrays.asList(get1, get2))) + .isInstanceOf(UnsupportedOperationException.class); + } } diff --git a/integration-test/src/main/java/com/scalar/db/api/DistributedTransactionIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/api/DistributedTransactionIntegrationTestBase.java index d60e6c4d4e..4b497b9944 100644 --- a/integration-test/src/main/java/com/scalar/db/api/DistributedTransactionIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/api/DistributedTransactionIntegrationTestBase.java @@ -506,7 +506,6 @@ public void get_GetGivenForIndexColumn_ShouldReturnRecords() throws TransactionE .namespace(namespace) .table(TABLE) .indexKey(Key.ofInt(SOME_COLUMN, 2)) - .consistency(Consistency.LINEARIZABLE) .build(); Get getBuiltByBuilder = @@ -544,7 +543,6 @@ public void scanOrGetScanner_ScanGivenForIndexColumn_ShouldReturnRecords(ScanTyp .namespace(namespace) .table(TABLE) .indexKey(Key.ofInt(SOME_COLUMN, 2)) - .consistency(Consistency.LINEARIZABLE) .build(); Scan scanBuiltByBuilder = @@ -1018,6 +1016,108 @@ public void mutateAndCommit_ShouldMutateRecordsProperly() throws TransactionExce assertThat(result2.isPresent()).isFalse(); } + @Test + public void batch_ShouldBatchProperly() throws TransactionException { + // Arrange + populateRecords(); + Get get = prepareGet(0, 0); + Scan scan = + Scan.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .build(); + Put put = Put.newBuilder(preparePut(0, 0)).intValue(BALANCE, 200).build(); + Insert insert = + Insert.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 4)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 300) + .build(); + Upsert upsert = + Upsert.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 2)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, 250) + .build(); + Update update = + Update.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 150) + .build(); + Delete delete = prepareDelete(0, 1); + + // Act + DistributedTransaction tx = manager.start(); + List results = + tx.batch(Arrays.asList(get, scan, put, insert, upsert, update, delete)); + tx.commit(); + + // Assert + assertThat(results).hasSize(7); + + // Verify get + assertThat(results.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.GET); + Optional getResult = results.get(0).getGetResult(); + assertThat(getResult).isPresent(); + assertThat(getResult.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(getResult.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(getBalance(getResult.get())).isEqualTo(INITIAL_BALANCE); + + // Verify scan + assertThat(results.get(1).getType()).isEqualTo(CrudOperable.BatchResult.Type.SCAN); + List scanResult = results.get(1).getScanResult(); + assertThat(scanResult).hasSize(NUM_TYPES); + assertThat(scanResult.get(0).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(scanResult.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(1).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(2).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(scanResult.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(3).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(scanResult.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + // Verify put + assertThat(results.get(2).getType()).isEqualTo(CrudOperable.BatchResult.Type.PUT); + Optional putResult = get(prepareGet(0, 0)); + assertThat(putResult).isPresent(); + assertThat(getBalance(putResult.get())).isEqualTo(200); + + // Verify insert + assertThat(results.get(3).getType()).isEqualTo(CrudOperable.BatchResult.Type.INSERT); + Optional insertResult = get(prepareGet(4, 0)); + assertThat(insertResult).isPresent(); + assertThat(getBalance(insertResult.get())).isEqualTo(300); + + // Verify upsert + assertThat(results.get(4).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPSERT); + Optional upsertResult = get(prepareGet(2, 1)); + assertThat(upsertResult).isPresent(); + assertThat(getBalance(upsertResult.get())).isEqualTo(250); + + // Verify update + assertThat(results.get(5).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPDATE); + Optional updateResult = get(prepareGet(1, 0)); + assertThat(updateResult).isPresent(); + assertThat(getBalance(updateResult.get())).isEqualTo(150); + + // Verify delete + assertThat(results.get(6).getType()).isEqualTo(CrudOperable.BatchResult.Type.DELETE); + Optional deleteResult = get(prepareGet(0, 1)); + assertThat(deleteResult).isEmpty(); + } + @Test public void getState_forSuccessfulTransaction_ShouldReturnCommittedState() throws TransactionException { @@ -1468,6 +1568,121 @@ public void mutate_DefaultNamespaceGiven_ShouldWorkProperly() throws Transaction } } + @Test + public void batch_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { + Properties properties = getProperties(getTestName()); + properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace); + try (DistributedTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTransactionManager()) { + // Arrange + populateRecords(); + Get get = + Get.newBuilder() + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .build(); + Scan scan = Scan.newBuilder().table(TABLE).partitionKey(Key.ofInt(ACCOUNT_ID, 1)).build(); + Put put = + Put.newBuilder() + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 200) + .build(); + Insert insert = + Insert.newBuilder() + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 4)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 300) + .build(); + Upsert upsert = + Upsert.newBuilder() + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 2)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, 250) + .build(); + Update update = + Update.newBuilder() + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 150) + .build(); + Delete delete = + Delete.newBuilder() + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .build(); + + // Act + DistributedTransaction tx = managerWithDefaultNamespace.start(); + List results = + tx.batch(Arrays.asList(get, scan, put, insert, upsert, update, delete)); + tx.commit(); + + // Assert + assertThat(results).hasSize(7); + + // Verify get + assertThat(results.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.GET); + Optional getResult = results.get(0).getGetResult(); + assertThat(getResult).isPresent(); + assertThat(getResult.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(getResult.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(getBalance(getResult.get())).isEqualTo(INITIAL_BALANCE); + + // Verify scan + assertThat(results.get(1).getType()).isEqualTo(CrudOperable.BatchResult.Type.SCAN); + List scanResult = results.get(1).getScanResult(); + assertThat(scanResult).hasSize(NUM_TYPES); + assertThat(scanResult.get(0).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(scanResult.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(1).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(2).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(scanResult.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(3).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(scanResult.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + // Verify put + assertThat(results.get(2).getType()).isEqualTo(CrudOperable.BatchResult.Type.PUT); + Optional putResult = get(prepareGet(0, 0)); + assertThat(putResult).isPresent(); + assertThat(getBalance(putResult.get())).isEqualTo(200); + + // Verify insert + assertThat(results.get(3).getType()).isEqualTo(CrudOperable.BatchResult.Type.INSERT); + Optional insertResult = get(prepareGet(4, 0)); + assertThat(insertResult).isPresent(); + assertThat(getBalance(insertResult.get())).isEqualTo(300); + + // Verify upsert + assertThat(results.get(4).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPSERT); + Optional upsertResult = get(prepareGet(2, 1)); + assertThat(upsertResult).isPresent(); + assertThat(getBalance(upsertResult.get())).isEqualTo(250); + + // Verify update + assertThat(results.get(5).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPDATE); + Optional updateResult = get(prepareGet(1, 0)); + assertThat(updateResult).isPresent(); + assertThat(getBalance(updateResult.get())).isEqualTo(150); + + // Verify delete + assertThat(results.get(6).getType()).isEqualTo(CrudOperable.BatchResult.Type.DELETE); + Optional deleteResult = get(prepareGet(0, 1)); + assertThat(deleteResult).isEmpty(); + } + } + @Test public void put_withPutIfWithVerifiedCondition_shouldPutProperly() throws TransactionException { // Arrange @@ -2070,6 +2285,114 @@ public void update_withUpdateIfWithNonVerifiedCondition_shouldThrowUnsatisfiedCo assertThat(result.isNull(SOME_COLUMN)).isTrue(); } + @Test + public void getAndUpdate_ShouldGetAndUpdateCorrectly() throws TransactionException { + // Arrange + populateRecords(); + int amount = 100; + + // Act Assert + DistributedTransaction transaction = manager.begin(); + + Get get1 = + Get.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .build(); + Get get2 = + Get.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .build(); + + List batchResults = transaction.batch(Arrays.asList(get1, get2)); + assertThat(batchResults).hasSize(2); + Optional result1 = batchResults.get(0).getGetResult(); + assertThat(result1).isPresent(); + Optional result2 = batchResults.get(1).getGetResult(); + assertThat(result2).isPresent(); + + Update update1 = + Update.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, getBalance(result1.get()) - amount) + .build(); + Update update2 = + Update.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, getBalance(result2.get()) + amount) + .build(); + + transaction.mutate(Arrays.asList(update1, update2)); + + Get get3 = + Get.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 2)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .build(); + Get get4 = + Get.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 3)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .build(); + + batchResults = transaction.batch(Arrays.asList(get3, get4)); + assertThat(batchResults).hasSize(2); + Optional result3 = batchResults.get(0).getGetResult(); + assertThat(result3).isPresent(); + Optional result4 = batchResults.get(1).getGetResult(); + assertThat(result4).isPresent(); + + Update update3 = + Update.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 2)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, getBalance(result3.get()) - amount) + .build(); + Update update4 = + Update.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 3)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, getBalance(result4.get()) + amount) + .build(); + + transaction.mutate(Arrays.asList(update3, update4)); + + transaction.commit(); + + // Assert + Optional finalResult1 = manager.get(get1); + assertThat(finalResult1).isPresent(); + assertThat(getBalance(finalResult1.get())).isEqualTo(INITIAL_BALANCE - amount); + Optional finalResult2 = manager.get(get2); + assertThat(finalResult2).isPresent(); + assertThat(getBalance(finalResult2.get())).isEqualTo(INITIAL_BALANCE + amount); + Optional finalResult3 = manager.get(get3); + assertThat(finalResult3).isPresent(); + assertThat(getBalance(finalResult3.get())).isEqualTo(INITIAL_BALANCE - amount); + Optional finalResult4 = manager.get(get4); + assertThat(finalResult4).isPresent(); + assertThat(getBalance(finalResult4.get())).isEqualTo(INITIAL_BALANCE + amount); + } + @Test public void manager_get_GetGivenForCommittedRecord_ShouldReturnRecord() throws TransactionException { @@ -2402,6 +2725,106 @@ public void manager_mutate_ShouldMutateRecords() throws TransactionException { assertThat(result2.isPresent()).isFalse(); } + @Test + public void manager_batch_ShouldBatchProperly() throws TransactionException { + // Arrange + populateRecords(); + Get get = prepareGet(0, 0); + Scan scan = + Scan.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .build(); + Put put = Put.newBuilder(preparePut(0, 0)).intValue(BALANCE, 200).build(); + Insert insert = + Insert.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 4)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 300) + .build(); + Upsert upsert = + Upsert.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 2)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, 250) + .build(); + Update update = + Update.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 150) + .build(); + Delete delete = prepareDelete(0, 1); + + // Act + List results = + manager.batch(Arrays.asList(get, scan, put, insert, upsert, update, delete)); + + // Assert + assertThat(results).hasSize(7); + + // Verify get + assertThat(results.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.GET); + Optional getResult = results.get(0).getGetResult(); + assertThat(getResult).isPresent(); + assertThat(getResult.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(getResult.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(getBalance(getResult.get())).isEqualTo(INITIAL_BALANCE); + + // Verify scan + assertThat(results.get(1).getType()).isEqualTo(CrudOperable.BatchResult.Type.SCAN); + List scanResult = results.get(1).getScanResult(); + assertThat(scanResult).hasSize(NUM_TYPES); + assertThat(scanResult.get(0).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(scanResult.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(1).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(2).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(scanResult.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(3).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(scanResult.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + // Verify put + assertThat(results.get(2).getType()).isEqualTo(CrudOperable.BatchResult.Type.PUT); + Optional putResult = get(prepareGet(0, 0)); + assertThat(putResult).isPresent(); + assertThat(getBalance(putResult.get())).isEqualTo(200); + + // Verify insert + assertThat(results.get(3).getType()).isEqualTo(CrudOperable.BatchResult.Type.INSERT); + Optional insertResult = get(prepareGet(4, 0)); + assertThat(insertResult).isPresent(); + assertThat(getBalance(insertResult.get())).isEqualTo(300); + + // Verify upsert + assertThat(results.get(4).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPSERT); + Optional upsertResult = get(prepareGet(2, 1)); + assertThat(upsertResult).isPresent(); + assertThat(getBalance(upsertResult.get())).isEqualTo(250); + + // Verify update + assertThat(results.get(5).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPDATE); + Optional updateResult = get(prepareGet(1, 0)); + assertThat(updateResult).isPresent(); + assertThat(getBalance(updateResult.get())).isEqualTo(150); + + // Verify delete + assertThat(results.get(6).getType()).isEqualTo(CrudOperable.BatchResult.Type.DELETE); + Optional deleteResult = get(prepareGet(0, 1)); + assertThat(deleteResult).isEmpty(); + } + @Test public void manager_get_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties(getTestName()); @@ -2610,6 +3033,120 @@ public void manager_mutate_DefaultNamespaceGiven_ShouldWorkProperly() } } + @Test + public void manager_batch_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { + Properties properties = getProperties(getTestName()); + properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace); + try (DistributedTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTransactionManager()) { + // Arrange + populateRecords(); + Get get = + Get.newBuilder() + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .build(); + Scan scan = Scan.newBuilder().table(TABLE).partitionKey(Key.ofInt(ACCOUNT_ID, 1)).build(); + Put put = + Put.newBuilder() + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 200) + .build(); + Insert insert = + Insert.newBuilder() + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 4)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 300) + .build(); + Upsert upsert = + Upsert.newBuilder() + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 2)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, 250) + .build(); + Update update = + Update.newBuilder() + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 150) + .build(); + Delete delete = + Delete.newBuilder() + .table(TABLE) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .build(); + + // Act + List results = + managerWithDefaultNamespace.batch( + Arrays.asList(get, scan, put, insert, upsert, update, delete)); + + // Assert + assertThat(results).hasSize(7); + + // Verify get + assertThat(results.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.GET); + Optional getResult = results.get(0).getGetResult(); + assertThat(getResult).isPresent(); + assertThat(getResult.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(getResult.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(getBalance(getResult.get())).isEqualTo(INITIAL_BALANCE); + + // Verify scan + assertThat(results.get(1).getType()).isEqualTo(CrudOperable.BatchResult.Type.SCAN); + List scanResult = results.get(1).getScanResult(); + assertThat(scanResult).hasSize(NUM_TYPES); + assertThat(scanResult.get(0).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(scanResult.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(1).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(2).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(scanResult.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(3).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(scanResult.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + // Verify put + assertThat(results.get(2).getType()).isEqualTo(CrudOperable.BatchResult.Type.PUT); + Optional putResult = get(prepareGet(0, 0)); + assertThat(putResult).isPresent(); + assertThat(getBalance(putResult.get())).isEqualTo(200); + + // Verify insert + assertThat(results.get(3).getType()).isEqualTo(CrudOperable.BatchResult.Type.INSERT); + Optional insertResult = get(prepareGet(4, 0)); + assertThat(insertResult).isPresent(); + assertThat(getBalance(insertResult.get())).isEqualTo(300); + + // Verify upsert + assertThat(results.get(4).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPSERT); + Optional upsertResult = get(prepareGet(2, 1)); + assertThat(upsertResult).isPresent(); + assertThat(getBalance(upsertResult.get())).isEqualTo(250); + + // Verify update + assertThat(results.get(5).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPDATE); + Optional updateResult = get(prepareGet(1, 0)); + assertThat(updateResult).isPresent(); + assertThat(getBalance(updateResult.get())).isEqualTo(150); + + // Verify delete + assertThat(results.get(6).getType()).isEqualTo(CrudOperable.BatchResult.Type.DELETE); + Optional deleteResult = get(prepareGet(0, 1)); + assertThat(deleteResult).isEmpty(); + } + } + protected Optional get(Get get) throws TransactionException { DistributedTransaction tx = manager.start(); try { @@ -2702,7 +3239,6 @@ protected Get prepareGet(int id, int type) { .table(TABLE) .partitionKey(partitionKey) .clusteringKey(clusteringKey) - .consistency(Consistency.LINEARIZABLE) .build(); } @@ -2719,19 +3255,13 @@ protected Scan prepareScan(int id, int fromType, int toType) { .namespace(namespace) .table(TABLE) .partitionKey(partitionKey) - .consistency(Consistency.LINEARIZABLE) .start(Key.ofInt(ACCOUNT_TYPE, fromType)) .end(Key.ofInt(ACCOUNT_TYPE, toType)) .build(); } protected Scan prepareScanAll() { - return Scan.newBuilder() - .namespace(namespace) - .table(TABLE) - .all() - .consistency(Consistency.LINEARIZABLE) - .build(); + return Scan.newBuilder().namespace(namespace).table(TABLE).all().build(); } protected Put preparePut(int id, int type) { @@ -2742,7 +3272,6 @@ protected Put preparePut(int id, int type) { .table(TABLE) .partitionKey(partitionKey) .clusteringKey(clusteringKey) - .consistency(Consistency.LINEARIZABLE) .build(); } @@ -2776,7 +3305,6 @@ protected Delete prepareDelete(int id, int type) { .table(TABLE) .partitionKey(partitionKey) .clusteringKey(clusteringKey) - .consistency(Consistency.LINEARIZABLE) .build(); } diff --git a/integration-test/src/main/java/com/scalar/db/api/TwoPhaseCommitTransactionIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/api/TwoPhaseCommitTransactionIntegrationTestBase.java index bc1c5b7e73..d19d4596b9 100644 --- a/integration-test/src/main/java/com/scalar/db/api/TwoPhaseCommitTransactionIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/api/TwoPhaseCommitTransactionIntegrationTestBase.java @@ -528,7 +528,6 @@ public void get_GetGivenForIndexColumn_ShouldReturnRecords() throws TransactionE .namespace(namespace1) .table(TABLE_1) .indexKey(Key.ofInt(SOME_COLUMN, 2)) - .consistency(Consistency.LINEARIZABLE) .build(); Get getBuiltByBuilder = @@ -568,7 +567,6 @@ public void scanOrGetScanner_ScanGivenForIndexColumn_ShouldReturnRecords(ScanTyp .namespace(namespace1) .table(TABLE_1) .indexKey(Key.ofInt(SOME_COLUMN, 2)) - .consistency(Consistency.LINEARIZABLE) .build(); Scan scanBuiltByBuilder = @@ -1066,6 +1064,110 @@ public void mutateAndRollback_WithMultipleSubTransactions_ShouldRollbackRecordsP assertThat(result2.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); } + @Test + public void batch_ShouldBatchProperly() throws TransactionException { + // Arrange + populateRecords(manager1, namespace1, TABLE_1); + Get get = prepareGet(0, 0, namespace1, TABLE_1); + Scan scan = + Scan.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .build(); + Put put = Put.newBuilder(preparePut(0, 0, namespace1, TABLE_1)).intValue(BALANCE, 200).build(); + Insert insert = + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 4)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 300) + .build(); + Upsert upsert = + Upsert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 2)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, 250) + .build(); + Update update = + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 150) + .build(); + Delete delete = prepareDelete(0, 1, namespace1, TABLE_1); + + // Act + TwoPhaseCommitTransaction tx = manager1.start(); + List results = + tx.batch(Arrays.asList(get, scan, put, insert, upsert, update, delete)); + tx.prepare(); + tx.validate(); + tx.commit(); + + // Assert + assertThat(results).hasSize(7); + + // Verify get + assertThat(results.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.GET); + Optional getResult = results.get(0).getGetResult(); + assertThat(getResult).isPresent(); + assertThat(getResult.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(getResult.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(getBalance(getResult.get())).isEqualTo(INITIAL_BALANCE); + + // Verify scan + assertThat(results.get(1).getType()).isEqualTo(CrudOperable.BatchResult.Type.SCAN); + List scanResult = results.get(1).getScanResult(); + assertThat(scanResult).hasSize(NUM_TYPES); + assertThat(scanResult.get(0).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(scanResult.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(1).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(2).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(scanResult.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(3).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(scanResult.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + // Verify put + assertThat(results.get(2).getType()).isEqualTo(CrudOperable.BatchResult.Type.PUT); + Optional putResult = get(prepareGet(0, 0, namespace1, TABLE_1)); + assertThat(putResult).isPresent(); + assertThat(getBalance(putResult.get())).isEqualTo(200); + + // Verify insert + assertThat(results.get(3).getType()).isEqualTo(CrudOperable.BatchResult.Type.INSERT); + Optional insertResult = get(prepareGet(4, 0, namespace1, TABLE_1)); + assertThat(insertResult).isPresent(); + assertThat(getBalance(insertResult.get())).isEqualTo(300); + + // Verify upsert + assertThat(results.get(4).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPSERT); + Optional upsertResult = get(prepareGet(2, 1, namespace1, TABLE_1)); + assertThat(upsertResult).isPresent(); + assertThat(getBalance(upsertResult.get())).isEqualTo(250); + + // Verify update + assertThat(results.get(5).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPDATE); + Optional updateResult = get(prepareGet(1, 0, namespace1, TABLE_1)); + assertThat(updateResult).isPresent(); + assertThat(getBalance(updateResult.get())).isEqualTo(150); + + // Verify delete + assertThat(results.get(6).getType()).isEqualTo(CrudOperable.BatchResult.Type.DELETE); + Optional deleteResult = get(prepareGet(0, 1, namespace1, TABLE_1)); + assertThat(deleteResult).isEmpty(); + } + @Test public void getState_forSuccessfulTransaction_ShouldReturnCommittedState() throws TransactionException { @@ -1710,6 +1812,123 @@ public void mutate_DefaultNamespaceGiven_ShouldWorkProperly() throws Transaction } } + @Test + public void batch_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { + Properties properties = getProperties1(getTestName()); + properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { + // Arrange + populateRecords(manager1, namespace1, TABLE_1); + Get get = + Get.newBuilder() + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .build(); + Scan scan = Scan.newBuilder().table(TABLE_1).partitionKey(Key.ofInt(ACCOUNT_ID, 1)).build(); + Put put = + Put.newBuilder() + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 200) + .build(); + Insert insert = + Insert.newBuilder() + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 4)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 300) + .build(); + Upsert upsert = + Upsert.newBuilder() + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 2)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, 250) + .build(); + Update update = + Update.newBuilder() + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 150) + .build(); + Delete delete = + Delete.newBuilder() + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .build(); + + // Act + TwoPhaseCommitTransaction tx = managerWithDefaultNamespace.start(); + List results = + tx.batch(Arrays.asList(get, scan, put, insert, upsert, update, delete)); + tx.prepare(); + tx.validate(); + tx.commit(); + + // Assert + assertThat(results).hasSize(7); + + // Verify get + assertThat(results.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.GET); + Optional getResult = results.get(0).getGetResult(); + assertThat(getResult).isPresent(); + assertThat(getResult.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(getResult.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(getBalance(getResult.get())).isEqualTo(INITIAL_BALANCE); + + // Verify scan + assertThat(results.get(1).getType()).isEqualTo(CrudOperable.BatchResult.Type.SCAN); + List scanResult = results.get(1).getScanResult(); + assertThat(scanResult).hasSize(NUM_TYPES); + assertThat(scanResult.get(0).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(scanResult.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(1).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(2).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(scanResult.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(3).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(scanResult.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + // Verify put + assertThat(results.get(2).getType()).isEqualTo(CrudOperable.BatchResult.Type.PUT); + Optional putResult = get(prepareGet(0, 0, namespace1, TABLE_1)); + assertThat(putResult).isPresent(); + assertThat(getBalance(putResult.get())).isEqualTo(200); + + // Verify insert + assertThat(results.get(3).getType()).isEqualTo(CrudOperable.BatchResult.Type.INSERT); + Optional insertResult = get(prepareGet(4, 0, namespace1, TABLE_1)); + assertThat(insertResult).isPresent(); + assertThat(getBalance(insertResult.get())).isEqualTo(300); + + // Verify upsert + assertThat(results.get(4).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPSERT); + Optional upsertResult = get(prepareGet(2, 1, namespace1, TABLE_1)); + assertThat(upsertResult).isPresent(); + assertThat(getBalance(upsertResult.get())).isEqualTo(250); + + // Verify update + assertThat(results.get(5).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPDATE); + Optional updateResult = get(prepareGet(1, 0, namespace1, TABLE_1)); + assertThat(updateResult).isPresent(); + assertThat(getBalance(updateResult.get())).isEqualTo(150); + + // Verify delete + assertThat(results.get(6).getType()).isEqualTo(CrudOperable.BatchResult.Type.DELETE); + Optional deleteResult = get(prepareGet(0, 1, namespace1, TABLE_1)); + assertThat(deleteResult).isEmpty(); + } + } + @Test public void put_withPutIfWithVerifiedCondition_shouldPutProperly() throws TransactionException { // Arrange @@ -2682,6 +2901,106 @@ public void manager_mutate_ShouldMutateRecords() throws TransactionException { assertThat(result2.isPresent()).isFalse(); } + @Test + public void manager_batch_ShouldBatchProperly() throws TransactionException { + // Arrange + populateRecords(manager1, namespace1, TABLE_1); + Get get = prepareGet(0, 0, namespace1, TABLE_1); + Scan scan = + Scan.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .build(); + Put put = Put.newBuilder(preparePut(0, 0, namespace1, TABLE_1)).intValue(BALANCE, 200).build(); + Insert insert = + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 4)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 300) + .build(); + Upsert upsert = + Upsert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 2)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, 250) + .build(); + Update update = + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 150) + .build(); + Delete delete = prepareDelete(0, 1, namespace1, TABLE_1); + + // Act + List results = + manager1.batch(Arrays.asList(get, scan, put, insert, upsert, update, delete)); + + // Assert + assertThat(results).hasSize(7); + + // Verify get + assertThat(results.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.GET); + Optional getResult = results.get(0).getGetResult(); + assertThat(getResult).isPresent(); + assertThat(getResult.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(getResult.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(getBalance(getResult.get())).isEqualTo(INITIAL_BALANCE); + + // Verify scan + assertThat(results.get(1).getType()).isEqualTo(CrudOperable.BatchResult.Type.SCAN); + List scanResult = results.get(1).getScanResult(); + assertThat(scanResult).hasSize(NUM_TYPES); + assertThat(scanResult.get(0).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(scanResult.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(1).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(2).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(scanResult.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(3).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(scanResult.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + // Verify put + assertThat(results.get(2).getType()).isEqualTo(CrudOperable.BatchResult.Type.PUT); + Optional putResult = get(prepareGet(0, 0, namespace1, TABLE_1)); + assertThat(putResult).isPresent(); + assertThat(getBalance(putResult.get())).isEqualTo(200); + + // Verify insert + assertThat(results.get(3).getType()).isEqualTo(CrudOperable.BatchResult.Type.INSERT); + Optional insertResult = get(prepareGet(4, 0, namespace1, TABLE_1)); + assertThat(insertResult).isPresent(); + assertThat(getBalance(insertResult.get())).isEqualTo(300); + + // Verify upsert + assertThat(results.get(4).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPSERT); + Optional upsertResult = get(prepareGet(2, 1, namespace1, TABLE_1)); + assertThat(upsertResult).isPresent(); + assertThat(getBalance(upsertResult.get())).isEqualTo(250); + + // Verify update + assertThat(results.get(5).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPDATE); + Optional updateResult = get(prepareGet(1, 0, namespace1, TABLE_1)); + assertThat(updateResult).isPresent(); + assertThat(getBalance(updateResult.get())).isEqualTo(150); + + // Verify delete + assertThat(results.get(6).getType()).isEqualTo(CrudOperable.BatchResult.Type.DELETE); + Optional deleteResult = get(prepareGet(0, 1, namespace1, TABLE_1)); + assertThat(deleteResult).isEmpty(); + } + @Test public void manager_get_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); @@ -2889,6 +3208,120 @@ public void manager_mutate_DefaultNamespaceGiven_ShouldWorkProperly() } } + @Test + public void manager_batch_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { + Properties properties = getProperties1(getTestName()); + properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { + // Arrange + populateRecords(manager1, namespace1, TABLE_1); + Get get = + Get.newBuilder() + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .build(); + Scan scan = Scan.newBuilder().table(TABLE_1).partitionKey(Key.ofInt(ACCOUNT_ID, 1)).build(); + Put put = + Put.newBuilder() + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 200) + .build(); + Insert insert = + Insert.newBuilder() + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 4)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 300) + .build(); + Upsert upsert = + Upsert.newBuilder() + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 2)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, 250) + .build(); + Update update = + Update.newBuilder() + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 150) + .build(); + Delete delete = + Delete.newBuilder() + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .build(); + + // Act + List results = + managerWithDefaultNamespace.batch( + Arrays.asList(get, scan, put, insert, upsert, update, delete)); + + // Assert + assertThat(results).hasSize(7); + + // Verify get + assertThat(results.get(0).getType()).isEqualTo(CrudOperable.BatchResult.Type.GET); + Optional getResult = results.get(0).getGetResult(); + assertThat(getResult).isPresent(); + assertThat(getResult.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(getResult.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(getBalance(getResult.get())).isEqualTo(INITIAL_BALANCE); + + // Verify scan + assertThat(results.get(1).getType()).isEqualTo(CrudOperable.BatchResult.Type.SCAN); + List scanResult = results.get(1).getScanResult(); + assertThat(scanResult).hasSize(NUM_TYPES); + assertThat(scanResult.get(0).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(scanResult.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(1).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(scanResult.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(2).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(scanResult.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(scanResult.get(3).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(scanResult.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(scanResult.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + // Verify put + assertThat(results.get(2).getType()).isEqualTo(CrudOperable.BatchResult.Type.PUT); + Optional putResult = get(prepareGet(0, 0, namespace1, TABLE_1)); + assertThat(putResult).isPresent(); + assertThat(getBalance(putResult.get())).isEqualTo(200); + + // Verify insert + assertThat(results.get(3).getType()).isEqualTo(CrudOperable.BatchResult.Type.INSERT); + Optional insertResult = get(prepareGet(4, 0, namespace1, TABLE_1)); + assertThat(insertResult).isPresent(); + assertThat(getBalance(insertResult.get())).isEqualTo(300); + + // Verify upsert + assertThat(results.get(4).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPSERT); + Optional upsertResult = get(prepareGet(2, 1, namespace1, TABLE_1)); + assertThat(upsertResult).isPresent(); + assertThat(getBalance(upsertResult.get())).isEqualTo(250); + + // Verify update + assertThat(results.get(5).getType()).isEqualTo(CrudOperable.BatchResult.Type.UPDATE); + Optional updateResult = get(prepareGet(1, 0, namespace1, TABLE_1)); + assertThat(updateResult).isPresent(); + assertThat(getBalance(updateResult.get())).isEqualTo(150); + + // Verify delete + assertThat(results.get(6).getType()).isEqualTo(CrudOperable.BatchResult.Type.DELETE); + Optional deleteResult = get(prepareGet(0, 1, namespace1, TABLE_1)); + assertThat(deleteResult).isEmpty(); + } + } + private Optional get(Get get) throws TransactionException { TwoPhaseCommitTransaction tx = manager1.start(); try { @@ -3000,7 +3433,6 @@ protected Get prepareGet(int id, int type, String namespaceName, String tableNam .table(tableName) .partitionKey(partitionKey) .clusteringKey(clusteringKey) - .consistency(Consistency.LINEARIZABLE) .build(); } @@ -3021,19 +3453,13 @@ protected Scan prepareScan( .namespace(namespaceName) .table(tableName) .partitionKey(partitionKey) - .consistency(Consistency.LINEARIZABLE) .start(Key.ofInt(ACCOUNT_TYPE, fromType)) .end(Key.ofInt(ACCOUNT_TYPE, toType)) .build(); } protected Scan prepareScanAll(String namespaceName, String tableName) { - return Scan.newBuilder() - .namespace(namespaceName) - .table(tableName) - .all() - .consistency(Consistency.LINEARIZABLE) - .build(); + return Scan.newBuilder().namespace(namespaceName).table(tableName).all().build(); } protected Put preparePut(int id, int type, String namespaceName, String tableName) { @@ -3044,7 +3470,6 @@ protected Put preparePut(int id, int type, String namespaceName, String tableNam .table(tableName) .partitionKey(partitionKey) .clusteringKey(clusteringKey) - .consistency(Consistency.LINEARIZABLE) .build(); } @@ -3081,7 +3506,6 @@ protected Delete prepareDelete(int id, int type, String namespaceName, String ta .table(tableName) .partitionKey(partitionKey) .clusteringKey(clusteringKey) - .consistency(Consistency.LINEARIZABLE) .build(); } diff --git a/integration-test/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionIntegrationTestBase.java index 0b712f994d..81fab2ded7 100644 --- a/integration-test/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionIntegrationTestBase.java @@ -223,6 +223,11 @@ public void mutateAndCommit_AfterRead_ShouldMutateRecordsProperly() {} @Test public void mutateAndCommit_ShouldMutateRecordsProperly() {} + @Disabled("Single CRUD operation transactions don't support beginning a transaction") + @Override + @Test + public void batch_ShouldBatchProperly() {} + @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override @Test @@ -312,6 +317,11 @@ public void delete_DefaultNamespaceGiven_ShouldWorkProperly() {} @Test public void mutate_DefaultNamespaceGiven_ShouldWorkProperly() {} + @Disabled("Single CRUD operation transactions don't support beginning a transaction") + @Override + @Test + public void batch_DefaultNamespaceGiven_ShouldWorkProperly() {} + @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override @Test @@ -449,18 +459,35 @@ public void abort_forOngoingTransaction_ShouldAbortCorrectly() {} @Test public void rollback_forOngoingTransaction_ShouldRollbackCorrectly() {} + @Disabled("Single CRUD operation transactions don't support beginning a transaction") + @Override + @Test + public void getAndUpdate_ShouldGetAndUpdateCorrectly() {} + @Disabled( "Single CRUD operation transactions don't support executing multiple mutations in a transaction") @Override @Test public void manager_mutate_ShouldMutateRecords() {} + @Disabled( + "Single CRUD operation transactions don't support executing multiple mutations in a transaction") + @Override + @Test + public void manager_batch_ShouldBatchProperly() {} + @Disabled( "Single CRUD operation transactions don't support executing multiple mutations in a transaction") @Override @Test public void manager_mutate_DefaultNamespaceGiven_ShouldWorkProperly() {} + @Disabled( + "Single CRUD operation transactions don't support executing multiple mutations in a transaction") + @Override + @Test + public void manager_batch_DefaultNamespaceGiven_ShouldWorkProperly() {} + @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override @ParameterizedTest