diff --git a/core/src/main/java/com/scalar/db/api/DistributedStorageAdmin.java b/core/src/main/java/com/scalar/db/api/DistributedStorageAdmin.java index bcc16eb30c..a45c69e317 100644 --- a/core/src/main/java/com/scalar/db/api/DistributedStorageAdmin.java +++ b/core/src/main/java/com/scalar/db/api/DistributedStorageAdmin.java @@ -47,6 +47,9 @@ public interface DistributedStorageAdmin extends Admin, AutoCloseable { /** * Returns the storage information. * + *

Note: This feature is primarily for internal use. Breaking changes can and will be + * introduced to it. Users should not depend on it. + * * @param namespace the namespace to get the storage information for * @return the storage information * @throws ExecutionException if the operation fails diff --git a/core/src/main/java/com/scalar/db/api/StorageInfo.java b/core/src/main/java/com/scalar/db/api/StorageInfo.java index 507f974318..35a40dde2b 100644 --- a/core/src/main/java/com/scalar/db/api/StorageInfo.java +++ b/core/src/main/java/com/scalar/db/api/StorageInfo.java @@ -22,6 +22,13 @@ public interface StorageInfo { */ int getMaxAtomicMutationsCount(); + /** + * Returns whether the storage guarantees consistent reads for virtual tables. + * + * @return true if the storage guarantees consistent reads for virtual tables, false otherwise + */ + boolean isConsistentVirtualTableReadGuaranteed(); + /** * The mutation atomicity unit of the storage. * diff --git a/core/src/main/java/com/scalar/db/common/StorageInfoImpl.java b/core/src/main/java/com/scalar/db/common/StorageInfoImpl.java index 2f3fc48a1e..e1f9bc2d7a 100644 --- a/core/src/main/java/com/scalar/db/common/StorageInfoImpl.java +++ b/core/src/main/java/com/scalar/db/common/StorageInfoImpl.java @@ -11,14 +11,17 @@ public class StorageInfoImpl implements StorageInfo { private final String storageName; private final MutationAtomicityUnit mutationAtomicityUnit; private final int maxAtomicMutationsCount; + private final boolean consistentVirtualTableReadGuaranteed; public StorageInfoImpl( String storageName, MutationAtomicityUnit mutationAtomicityUnit, - int maxAtomicMutationsCount) { + int maxAtomicMutationsCount, + boolean consistentVirtualTableReadGuaranteed) { this.storageName = storageName; this.mutationAtomicityUnit = mutationAtomicityUnit; this.maxAtomicMutationsCount = maxAtomicMutationsCount; + this.consistentVirtualTableReadGuaranteed = consistentVirtualTableReadGuaranteed; } @Override @@ -36,6 +39,11 @@ public int getMaxAtomicMutationsCount() { return maxAtomicMutationsCount; } + @Override + public boolean isConsistentVirtualTableReadGuaranteed() { + return consistentVirtualTableReadGuaranteed; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -47,12 +55,18 @@ public boolean equals(Object o) { StorageInfoImpl that = (StorageInfoImpl) o; return getMaxAtomicMutationsCount() == that.getMaxAtomicMutationsCount() && Objects.equals(getStorageName(), that.getStorageName()) - && getMutationAtomicityUnit() == that.getMutationAtomicityUnit(); + && getMutationAtomicityUnit() == that.getMutationAtomicityUnit() + && isConsistentVirtualTableReadGuaranteed() + == that.isConsistentVirtualTableReadGuaranteed(); } @Override public int hashCode() { - return Objects.hash(getStorageName(), getMutationAtomicityUnit(), getMaxAtomicMutationsCount()); + return Objects.hash( + getStorageName(), + getMutationAtomicityUnit(), + getMaxAtomicMutationsCount(), + isConsistentVirtualTableReadGuaranteed()); } @Override @@ -61,6 +75,7 @@ public String toString() { .add("storageName", storageName) .add("mutationAtomicityUnit", mutationAtomicityUnit) .add("maxAtomicMutationsCount", maxAtomicMutationsCount) + .add("consistentVirtualTableReadGuaranteed", consistentVirtualTableReadGuaranteed) .toString(); } } diff --git a/core/src/main/java/com/scalar/db/storage/cassandra/CassandraAdmin.java b/core/src/main/java/com/scalar/db/storage/cassandra/CassandraAdmin.java index 9a60eb15b9..b889c08a86 100644 --- a/core/src/main/java/com/scalar/db/storage/cassandra/CassandraAdmin.java +++ b/core/src/main/java/com/scalar/db/storage/cassandra/CassandraAdmin.java @@ -57,7 +57,8 @@ public class CassandraAdmin implements DistributedStorageAdmin { "cassandra", StorageInfo.MutationAtomicityUnit.PARTITION, // No limit on the number of mutations - Integer.MAX_VALUE); + Integer.MAX_VALUE, + false); private final ClusterManager clusterManager; private final String metadataKeyspace; diff --git a/core/src/main/java/com/scalar/db/storage/cosmos/CosmosAdmin.java b/core/src/main/java/com/scalar/db/storage/cosmos/CosmosAdmin.java index 3836b06443..30b14fce0d 100644 --- a/core/src/main/java/com/scalar/db/storage/cosmos/CosmosAdmin.java +++ b/core/src/main/java/com/scalar/db/storage/cosmos/CosmosAdmin.java @@ -74,7 +74,8 @@ public class CosmosAdmin implements DistributedStorageAdmin { "cosmos", StorageInfo.MutationAtomicityUnit.PARTITION, // No limit on the number of mutations - Integer.MAX_VALUE); + Integer.MAX_VALUE, + false); private final CosmosClient client; private final String metadataDatabase; diff --git a/core/src/main/java/com/scalar/db/storage/dynamo/DynamoAdmin.java b/core/src/main/java/com/scalar/db/storage/dynamo/DynamoAdmin.java index fc420242db..ceecf593fe 100644 --- a/core/src/main/java/com/scalar/db/storage/dynamo/DynamoAdmin.java +++ b/core/src/main/java/com/scalar/db/storage/dynamo/DynamoAdmin.java @@ -165,7 +165,8 @@ public class DynamoAdmin implements DistributedStorageAdmin { "dynamo", StorageInfo.MutationAtomicityUnit.STORAGE, // DynamoDB has a limit of 100 items per transactional batch write operation - 100); + 100, + false); private final DynamoDbClient client; private final ApplicationAutoScalingClient applicationAutoScalingClient; diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java index 4a1d505f4d..3085ca629c 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java @@ -53,12 +53,6 @@ public class JdbcAdmin implements DistributedStorageAdmin { @VisibleForTesting static final String JDBC_COL_DECIMAL_DIGITS = "DECIMAL_DIGITS"; private static final String INDEX_NAME_PREFIX = "index"; - private static final StorageInfo STORAGE_INFO = - new StorageInfoImpl( - "jdbc", - StorageInfo.MutationAtomicityUnit.STORAGE, - // No limit on the number of mutations - Integer.MAX_VALUE); private final RdbEngineStrategy rdbEngine; private final BasicDataSource dataSource; @@ -1011,8 +1005,22 @@ public void upgrade(Map options) throws ExecutionException { } @Override - public StorageInfo getStorageInfo(String namespace) { - return STORAGE_INFO; + public StorageInfo getStorageInfo(String namespace) throws ExecutionException { + boolean consistentVirtualTableReadGuaranteed; + try (Connection connection = dataSource.getConnection()) { + int isolationLevel = connection.getTransactionIsolation(); + consistentVirtualTableReadGuaranteed = + isolationLevel >= rdbEngine.getMinimumIsolationLevelForConsistentVirtualTableRead(); + } catch (SQLException e) { + throw new ExecutionException("Getting the transaction isolation level failed", e); + } + + return new StorageInfoImpl( + "jdbc", + StorageInfo.MutationAtomicityUnit.STORAGE, + // No limit on the number of mutations + Integer.MAX_VALUE, + consistentVirtualTableReadGuaranteed); } @Override diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineDb2.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineDb2.java index 3eb0ac10f7..cf28f98b17 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineDb2.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineDb2.java @@ -20,6 +20,7 @@ import com.scalar.db.storage.jdbc.query.SelectQuery; import com.scalar.db.storage.jdbc.query.SelectWithLimitQuery; import com.scalar.db.storage.jdbc.query.UpsertQuery; +import java.sql.Connection; import java.sql.Driver; import java.sql.JDBCType; import java.sql.ResultSet; @@ -588,4 +589,10 @@ public void throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( public String getTableNamesInNamespaceSql() { return "SELECT TABNAME FROM SYSCAT.TABLES WHERE TABSCHEMA = ? AND TYPE = 'T'"; } + + @Override + public int getMinimumIsolationLevelForConsistentVirtualTableRead() { + // In Db2, REPEATABLE READ and SERIALIZABLE isolation levels guarantee consistent reads + return Connection.TRANSACTION_REPEATABLE_READ; + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineMysql.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineMysql.java index 2e96583c1b..f3f4724138 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineMysql.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineMysql.java @@ -509,4 +509,10 @@ public void setConnectionToReadOnly(Connection connection, boolean readOnly) thr public String getTableNamesInNamespaceSql() { return "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?"; } + + @Override + public int getMinimumIsolationLevelForConsistentVirtualTableRead() { + // In MySQL, REPEATABLE READ and SERIALIZABLE isolation levels guarantee consistent reads + return Connection.TRANSACTION_REPEATABLE_READ; + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineOracle.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineOracle.java index ba8e00060c..95f45887e5 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineOracle.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineOracle.java @@ -18,6 +18,7 @@ import com.scalar.db.storage.jdbc.query.UpsertQuery; import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.sql.Connection; import java.sql.Driver; import java.sql.JDBCType; import java.sql.PreparedStatement; @@ -540,4 +541,10 @@ public void bindBlobColumnToPreparedStatement( public String getTableNamesInNamespaceSql() { return "SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER = ?"; } + + @Override + public int getMinimumIsolationLevelForConsistentVirtualTableRead() { + // In Oracle, only the SERIALIZABLE isolation level guarantees consistent reads + return Connection.TRANSACTION_SERIALIZABLE; + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEnginePostgresql.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEnginePostgresql.java index 33ba3bed7c..a7036184ce 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEnginePostgresql.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEnginePostgresql.java @@ -10,6 +10,7 @@ import com.scalar.db.storage.jdbc.query.SelectQuery; import com.scalar.db.storage.jdbc.query.SelectWithLimitQuery; import com.scalar.db.storage.jdbc.query.UpsertQuery; +import java.sql.Connection; import java.sql.Driver; import java.sql.JDBCType; import java.sql.SQLException; @@ -400,4 +401,10 @@ public String tryAddIfNotExistsToCreateIndexSql(String createIndexSql) { public String getTableNamesInNamespaceSql() { return "SELECT table_name FROM information_schema.tables WHERE table_schema = ?"; } + + @Override + public int getMinimumIsolationLevelForConsistentVirtualTableRead() { + // In PostgreSQL, REPEATABLE READ and SERIALIZABLE isolation levels guarantee consistent reads + return Connection.TRANSACTION_REPEATABLE_READ; + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlServer.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlServer.java index 4a38abeca1..1f055eeac3 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlServer.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlServer.java @@ -10,6 +10,7 @@ import com.scalar.db.storage.jdbc.query.SelectQuery; import com.scalar.db.storage.jdbc.query.SelectWithTop; import com.scalar.db.storage.jdbc.query.UpsertQuery; +import java.sql.Connection; import java.sql.Driver; import java.sql.JDBCType; import java.sql.SQLException; @@ -436,4 +437,10 @@ public Map getConnectionProperties(JdbcConfig config) { public String getTableNamesInNamespaceSql() { return "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?"; } + + @Override + public int getMinimumIsolationLevelForConsistentVirtualTableRead() { + // In SQL Server, REPEATABLE READ or higher isolation level guarantees consistent reads + return Connection.TRANSACTION_REPEATABLE_READ; + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlite.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlite.java index 8800d88aba..97008079d7 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlite.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineSqlite.java @@ -389,4 +389,10 @@ public String getTableNamesInNamespaceSql() { // Do nothing. Namespace is just a table prefix in the SQLite implementation. return null; } + + @Override + public int getMinimumIsolationLevelForConsistentVirtualTableRead() { + // In SQLite, READ COMMITTED and higher isolation levels guarantee consistent reads + return Connection.TRANSACTION_READ_COMMITTED; + } } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineStrategy.java b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineStrategy.java index 8d7992f529..b5615a4815 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineStrategy.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/RdbEngineStrategy.java @@ -325,4 +325,17 @@ default void throwIfConjunctionsColumnNotSupported( Set conjunctions, TableMetadata metadata) {} String getTableNamesInNamespaceSql(); + + /** + * Returns the minimum isolation level required to ensure consistent reads across virtual tables. + * + *

A virtual table read involves querying multiple underlying source tables. When using a lower + * isolation level, there is a risk of observing an inconsistent snapshot where data from + * different source tables reflects different points in time. This method returns the minimum + * isolation level that guarantees a consistent snapshot across all source tables involved in a + * virtual table read. + * + * @return the minimum isolation level required for consistent virtual table reads + */ + int getMinimumIsolationLevelForConsistentVirtualTableRead(); } diff --git a/core/src/main/java/com/scalar/db/storage/multistorage/MultiStorageAdmin.java b/core/src/main/java/com/scalar/db/storage/multistorage/MultiStorageAdmin.java index 0d19989f63..0a5638e233 100644 --- a/core/src/main/java/com/scalar/db/storage/multistorage/MultiStorageAdmin.java +++ b/core/src/main/java/com/scalar/db/storage/multistorage/MultiStorageAdmin.java @@ -305,7 +305,8 @@ public StorageInfo getStorageInfo(String namespace) throws ExecutionException { return new StorageInfoImpl( holder.storageName, storageInfo.getMutationAtomicityUnit(), - storageInfo.getMaxAtomicMutationsCount()); + storageInfo.getMaxAtomicMutationsCount(), + storageInfo.isConsistentVirtualTableReadGuaranteed()); } catch (RuntimeException e) { if (e.getCause() instanceof ExecutionException) { throw (ExecutionException) e.getCause(); diff --git a/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageAdmin.java b/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageAdmin.java index 681e6fcf2a..8c68e3cc44 100644 --- a/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageAdmin.java +++ b/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageAdmin.java @@ -37,7 +37,8 @@ public class ObjectStorageAdmin implements DistributedStorageAdmin { "object_storage", StorageInfo.MutationAtomicityUnit.PARTITION, // No limit on the number of mutations - Integer.MAX_VALUE); + Integer.MAX_VALUE, + false); private final ObjectStorageWrapper wrapper; private final String metadataNamespace; diff --git a/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageWrapper.java b/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageWrapper.java index 96e4117c41..8a5c2803d1 100644 --- a/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageWrapper.java +++ b/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageWrapper.java @@ -76,6 +76,10 @@ public interface ObjectStorageWrapper { */ void deleteByPrefix(String prefix) throws ObjectStorageWrapperException; - /** Close the storage wrapper. */ + /** + * Close the storage wrapper. + * + * @throws ObjectStorageWrapperException if an error occurs + */ void close() throws ObjectStorageWrapperException; } diff --git a/core/src/test/java/com/scalar/db/common/checker/OperationCheckerTest.java b/core/src/test/java/com/scalar/db/common/checker/OperationCheckerTest.java index 9e6fd87310..ec6a6833fb 100644 --- a/core/src/test/java/com/scalar/db/common/checker/OperationCheckerTest.java +++ b/core/src/test/java/com/scalar/db/common/checker/OperationCheckerTest.java @@ -53,7 +53,7 @@ public class OperationCheckerTest { private static final String COL3 = "v3"; private static final StorageInfo STORAGE_INFO = new StorageInfoImpl( - "cassandra", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE); + "cassandra", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE, false); @Mock private DatabaseConfig databaseConfig; @Mock private TableMetadataManager metadataManager; @@ -2059,9 +2059,11 @@ public void check_MutationsGiven_ForAtomicityUnit_ShouldBehaveCorrectly( .addClusteringKey(CKEY1) .build()); - StorageInfo storageInfo1 = new StorageInfoImpl("s1", mutationAtomicityUnit, Integer.MAX_VALUE); + StorageInfo storageInfo1 = + new StorageInfoImpl("s1", mutationAtomicityUnit, Integer.MAX_VALUE, false); StorageInfo storageInfo2 = - new StorageInfoImpl("s2", StorageInfo.MutationAtomicityUnit.STORAGE, Integer.MAX_VALUE); + new StorageInfoImpl( + "s2", StorageInfo.MutationAtomicityUnit.STORAGE, Integer.MAX_VALUE, false); when(storageInfoProvider.getStorageInfo("ns")).thenReturn(storageInfo1); when(storageInfoProvider.getStorageInfo("ns2")).thenReturn(storageInfo1); when(storageInfoProvider.getStorageInfo("other_ns")).thenReturn(storageInfo2); diff --git a/core/src/test/java/com/scalar/db/storage/cosmos/CosmosOperationCheckerTest.java b/core/src/test/java/com/scalar/db/storage/cosmos/CosmosOperationCheckerTest.java index b11dcfb782..7265b74424 100644 --- a/core/src/test/java/com/scalar/db/storage/cosmos/CosmosOperationCheckerTest.java +++ b/core/src/test/java/com/scalar/db/storage/cosmos/CosmosOperationCheckerTest.java @@ -41,7 +41,8 @@ public class CosmosOperationCheckerTest { private static final String COL1 = "v1"; private static final String COL2 = "v2"; private static final StorageInfo STORAGE_INFO = - new StorageInfoImpl("cosmos", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE); + new StorageInfoImpl( + "cosmos", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE, false); private static final TableMetadata TABLE_METADATA1 = TableMetadata.newBuilder() diff --git a/core/src/test/java/com/scalar/db/storage/dynamo/DynamoOperationCheckerTest.java b/core/src/test/java/com/scalar/db/storage/dynamo/DynamoOperationCheckerTest.java index 59e2fbad47..b2e91b9dfa 100644 --- a/core/src/test/java/com/scalar/db/storage/dynamo/DynamoOperationCheckerTest.java +++ b/core/src/test/java/com/scalar/db/storage/dynamo/DynamoOperationCheckerTest.java @@ -39,7 +39,7 @@ public class DynamoOperationCheckerTest { private static final String COL3 = "v3"; private static final String COL4 = "v4"; private static final StorageInfo STORAGE_INFO = - new StorageInfoImpl("dynamo", StorageInfo.MutationAtomicityUnit.STORAGE, 100); + new StorageInfoImpl("dynamo", StorageInfo.MutationAtomicityUnit.STORAGE, 100, false); @Mock private DatabaseConfig databaseConfig; @Mock private TableMetadataManager metadataManager; diff --git a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java index db08667898..99eff786c2 100644 --- a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java +++ b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java @@ -35,6 +35,7 @@ import com.google.common.collect.ImmutableSet; import com.mysql.cj.jdbc.exceptions.CommunicationsException; import com.scalar.db.api.Scan.Ordering.Order; +import com.scalar.db.api.StorageInfo; import com.scalar.db.api.TableMetadata; import com.scalar.db.api.VirtualTableInfo; import com.scalar.db.api.VirtualTableJoinType; @@ -6199,6 +6200,103 @@ public void getVirtualTableInfo_SQLExceptionThrown_ShouldThrowExecutionException .hasCause(sqlException); } + @ParameterizedTest + @EnumSource(RdbEngine.class) + void getStorageInfo_WithRepeatableReadIsolationLevel_ShouldReturnCorrectInfo(RdbEngine rdbEngine) + throws Exception { + // Arrange + int isolationLevel = Connection.TRANSACTION_REPEATABLE_READ; + JdbcAdmin admin = createJdbcAdminFor(rdbEngine); + when(dataSource.getConnection()).thenReturn(connection); + when(connection.getTransactionIsolation()).thenReturn(isolationLevel); + + // Act + StorageInfo storageInfo = admin.getStorageInfo("namespace"); + + // Assert + assertThat(storageInfo.getStorageName()).isEqualTo("jdbc"); + assertThat(storageInfo.getMutationAtomicityUnit()) + .isEqualTo(StorageInfo.MutationAtomicityUnit.STORAGE); + assertThat(storageInfo.getMaxAtomicMutationsCount()).isEqualTo(Integer.MAX_VALUE); + + // Check consistent virtual table read guarantee based on RDB engine + RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine); + boolean expectedConsistentVirtualTableReadGuaranteed = + isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableRead(); + assertThat(storageInfo.isConsistentVirtualTableReadGuaranteed()) + .isEqualTo(expectedConsistentVirtualTableReadGuaranteed); + } + + @ParameterizedTest + @EnumSource(RdbEngine.class) + void getStorageInfo_WithReadCommittedIsolationLevel_ShouldReturnCorrectInfo(RdbEngine rdbEngine) + throws Exception { + // Arrange + int isolationLevel = Connection.TRANSACTION_READ_COMMITTED; + JdbcAdmin admin = createJdbcAdminFor(rdbEngine); + when(dataSource.getConnection()).thenReturn(connection); + when(connection.getTransactionIsolation()).thenReturn(isolationLevel); + + // Act + StorageInfo storageInfo = admin.getStorageInfo("namespace"); + + // Assert + assertThat(storageInfo.getStorageName()).isEqualTo("jdbc"); + assertThat(storageInfo.getMutationAtomicityUnit()) + .isEqualTo(StorageInfo.MutationAtomicityUnit.STORAGE); + assertThat(storageInfo.getMaxAtomicMutationsCount()).isEqualTo(Integer.MAX_VALUE); + + // Check consistent virtual table read guarantee based on RDB engine + RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine); + boolean expectedConsistentVirtualTableReadGuaranteed = + isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableRead(); + assertThat(storageInfo.isConsistentVirtualTableReadGuaranteed()) + .isEqualTo(expectedConsistentVirtualTableReadGuaranteed); + } + + @ParameterizedTest + @EnumSource(RdbEngine.class) + void getStorageInfo_WithSerializableIsolationLevel_ShouldReturnCorrectInfo(RdbEngine rdbEngine) + throws Exception { + // Arrange + int isolationLevel = Connection.TRANSACTION_SERIALIZABLE; + JdbcAdmin admin = createJdbcAdminFor(rdbEngine); + when(dataSource.getConnection()).thenReturn(connection); + when(connection.getTransactionIsolation()).thenReturn(isolationLevel); + + // Act + StorageInfo storageInfo = admin.getStorageInfo("namespace"); + + // Assert + assertThat(storageInfo.getStorageName()).isEqualTo("jdbc"); + assertThat(storageInfo.getMutationAtomicityUnit()) + .isEqualTo(StorageInfo.MutationAtomicityUnit.STORAGE); + assertThat(storageInfo.getMaxAtomicMutationsCount()).isEqualTo(Integer.MAX_VALUE); + + // Check consistent virtual table read guarantee based on RDB engine + RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine); + boolean expectedConsistentVirtualTableReadGuaranteed = + isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableRead(); + assertThat(storageInfo.isConsistentVirtualTableReadGuaranteed()) + .isEqualTo(expectedConsistentVirtualTableReadGuaranteed); + } + + @ParameterizedTest + @EnumSource(RdbEngine.class) + void getStorageInfo_SQLExceptionThrown_ShouldThrowExecutionException(RdbEngine rdbEngine) + throws Exception { + // Arrange + JdbcAdmin admin = createJdbcAdminFor(rdbEngine); + when(dataSource.getConnection()).thenReturn(connection); + when(connection.getTransactionIsolation()).thenThrow(new SQLException("Connection error")); + + // Act Assert + assertThatThrownBy(() -> admin.getStorageInfo("namespace")) + .isInstanceOf(ExecutionException.class) + .hasMessageContaining("Getting the transaction isolation level failed") + .hasCauseInstanceOf(SQLException.class); + } + // Utility class used to mock ResultSet for a "select * from" query on the metadata table static class SelectAllFromMetadataTableResultSetMocker implements org.mockito.stubbing.Answer { diff --git a/core/src/test/java/com/scalar/db/storage/multistorage/MultiStorageAdminTest.java b/core/src/test/java/com/scalar/db/storage/multistorage/MultiStorageAdminTest.java index 1e5f5733d2..28a320c580 100644 --- a/core/src/test/java/com/scalar/db/storage/multistorage/MultiStorageAdminTest.java +++ b/core/src/test/java/com/scalar/db/storage/multistorage/MultiStorageAdminTest.java @@ -915,25 +915,30 @@ public void getStorageInfo_ShouldReturnProperStorageInfo() throws ExecutionExcep when(admin1.getStorageInfo(anyString())) .thenReturn( new StorageInfoImpl( - "cassandra", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE)); + "cassandra", + StorageInfo.MutationAtomicityUnit.PARTITION, + Integer.MAX_VALUE, + false)); when(admin2.getStorageInfo(anyString())) - .thenReturn(new StorageInfoImpl("dynamo", StorageInfo.MutationAtomicityUnit.STORAGE, 100)); + .thenReturn( + new StorageInfoImpl("dynamo", StorageInfo.MutationAtomicityUnit.STORAGE, 100, false)); when(admin3.getStorageInfo(anyString())) .thenReturn( new StorageInfoImpl( - "jdbc", StorageInfo.MutationAtomicityUnit.STORAGE, Integer.MAX_VALUE)); + "jdbc", StorageInfo.MutationAtomicityUnit.STORAGE, Integer.MAX_VALUE, true)); // Act Assert assertThat(multiStorageAdmin.getStorageInfo("ns1")) .isEqualTo( new StorageInfoImpl( - "s1", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE)); + "s1", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE, false)); assertThat(multiStorageAdmin.getStorageInfo("ns2")) - .isEqualTo(new StorageInfoImpl("s2", StorageInfo.MutationAtomicityUnit.STORAGE, 100)); + .isEqualTo( + new StorageInfoImpl("s2", StorageInfo.MutationAtomicityUnit.STORAGE, 100, false)); assertThat(multiStorageAdmin.getStorageInfo("ns3")) .isEqualTo( new StorageInfoImpl( - "s3", StorageInfo.MutationAtomicityUnit.STORAGE, Integer.MAX_VALUE)); + "s3", StorageInfo.MutationAtomicityUnit.STORAGE, Integer.MAX_VALUE, true)); verify(admin1).getStorageInfo("ns1"); verify(admin2).getStorageInfo("ns2"); @@ -955,7 +960,8 @@ public void createVirtualTable_ProperArgumentsGiven_ShouldCallAdminProperly() // Mock getStorageInfo to return the same storage for all namespaces StorageInfo storageInfo = - new StorageInfoImpl("s2", StorageInfo.MutationAtomicityUnit.NAMESPACE, Integer.MAX_VALUE); + new StorageInfoImpl( + "s2", StorageInfo.MutationAtomicityUnit.NAMESPACE, Integer.MAX_VALUE, true); when(admin2.getStorageInfo(namespace)).thenReturn(storageInfo); when(admin2.getStorageInfo(leftSourceNamespace)).thenReturn(storageInfo); when(admin2.getStorageInfo(rightSourceNamespace)).thenReturn(storageInfo); @@ -1000,9 +1006,11 @@ public void createVirtualTable_ProperArgumentsGiven_ShouldCallAdminProperly() // Mock getStorageInfo - virtual table in s3, both sources in s2 StorageInfo storageInfoForNamespace = - new StorageInfoImpl("s3", StorageInfo.MutationAtomicityUnit.NAMESPACE, Integer.MAX_VALUE); + new StorageInfoImpl( + "s3", StorageInfo.MutationAtomicityUnit.NAMESPACE, Integer.MAX_VALUE, true); StorageInfo storageInfoForSources = - new StorageInfoImpl("s2", StorageInfo.MutationAtomicityUnit.NAMESPACE, Integer.MAX_VALUE); + new StorageInfoImpl( + "s2", StorageInfo.MutationAtomicityUnit.NAMESPACE, Integer.MAX_VALUE, true); when(admin3.getStorageInfo(namespace)).thenReturn(storageInfoForNamespace); when(admin2.getStorageInfo(leftSourceNamespace)).thenReturn(storageInfoForSources); when(admin2.getStorageInfo(rightSourceNamespace)).thenReturn(storageInfoForSources); @@ -1039,9 +1047,11 @@ public void createVirtualTable_ProperArgumentsGiven_ShouldCallAdminProperly() // Mock getStorageInfo to return different storages for left and right sources StorageInfo storageInfo1 = - new StorageInfoImpl("s2", StorageInfo.MutationAtomicityUnit.NAMESPACE, Integer.MAX_VALUE); + new StorageInfoImpl( + "s2", StorageInfo.MutationAtomicityUnit.NAMESPACE, Integer.MAX_VALUE, true); StorageInfo storageInfo2 = - new StorageInfoImpl("s3", StorageInfo.MutationAtomicityUnit.NAMESPACE, Integer.MAX_VALUE); + new StorageInfoImpl( + "s3", StorageInfo.MutationAtomicityUnit.NAMESPACE, Integer.MAX_VALUE, true); when(admin2.getStorageInfo(namespace)).thenReturn(storageInfo1); when(admin2.getStorageInfo(leftSourceNamespace)).thenReturn(storageInfo1); when(admin3.getStorageInfo(rightSourceNamespace)).thenReturn(storageInfo2); diff --git a/core/src/test/java/com/scalar/db/storage/objectstorage/ObjectStorageOperationCheckerTest.java b/core/src/test/java/com/scalar/db/storage/objectstorage/ObjectStorageOperationCheckerTest.java index b68f4c56be..7ab328e823 100644 --- a/core/src/test/java/com/scalar/db/storage/objectstorage/ObjectStorageOperationCheckerTest.java +++ b/core/src/test/java/com/scalar/db/storage/objectstorage/ObjectStorageOperationCheckerTest.java @@ -41,7 +41,7 @@ public class ObjectStorageOperationCheckerTest { private static final String COL3 = "v3"; private static final String COL4 = "v4"; private static final StorageInfo STORAGE_INFO = - new StorageInfoImpl("ObjectStorage", StorageInfo.MutationAtomicityUnit.STORAGE, 100); + new StorageInfoImpl("ObjectStorage", StorageInfo.MutationAtomicityUnit.STORAGE, 100, false); private static final TableMetadata TABLE_METADATA1 = TableMetadata.newBuilder() diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/CommitHandlerTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/CommitHandlerTest.java index 0591d65e49..be5b7c5bd0 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/CommitHandlerTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/CommitHandlerTest.java @@ -108,7 +108,7 @@ void setUp() throws Exception { when(storageInfoProvider.getStorageInfo(ANY_NAMESPACE_NAME)) .thenReturn( new StorageInfoImpl( - "storage1", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE)); + "storage1", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE, false)); } @AfterEach