From c93681e89d2a655a1eaa8a51245a49b151739306 Mon Sep 17 00:00:00 2001 From: Toshihiro Suzuki Date: Fri, 21 Nov 2025 13:52:37 +0000 Subject: [PATCH 1/2] Empty commit [skip ci] From 93e69a57e65f5febed74de45dcbc6445b1eaba95 Mon Sep 17 00:00:00 2001 From: Toshihiro Suzuki Date: Fri, 21 Nov 2025 22:52:20 +0900 Subject: [PATCH 2/2] Add isConsistentVirtualTableReadGuaranteed() to StorageInfo (#3204) --- .../db/api/DistributedStorageAdmin.java | 3 + .../java/com/scalar/db/api/StorageInfo.java | 7 ++ .../com/scalar/db/common/StorageInfoImpl.java | 21 +++- .../db/storage/cassandra/CassandraAdmin.java | 3 +- .../scalar/db/storage/cosmos/CosmosAdmin.java | 3 +- .../scalar/db/storage/dynamo/DynamoAdmin.java | 3 +- .../com/scalar/db/storage/jdbc/JdbcAdmin.java | 24 +++-- .../scalar/db/storage/jdbc/RdbEngineDb2.java | 7 ++ .../db/storage/jdbc/RdbEngineMysql.java | 6 ++ .../db/storage/jdbc/RdbEngineOracle.java | 7 ++ .../db/storage/jdbc/RdbEnginePostgresql.java | 7 ++ .../db/storage/jdbc/RdbEngineSqlServer.java | 7 ++ .../db/storage/jdbc/RdbEngineSqlite.java | 6 ++ .../db/storage/jdbc/RdbEngineStrategy.java | 13 +++ .../multistorage/MultiStorageAdmin.java | 3 +- .../objectstorage/ObjectStorageAdmin.java | 3 +- .../objectstorage/ObjectStorageWrapper.java | 6 +- .../common/checker/OperationCheckerTest.java | 8 +- .../cosmos/CosmosOperationCheckerTest.java | 3 +- .../dynamo/DynamoOperationCheckerTest.java | 2 +- .../scalar/db/storage/jdbc/JdbcAdminTest.java | 98 +++++++++++++++++++ .../multistorage/MultiStorageAdminTest.java | 32 +++--- .../ObjectStorageOperationCheckerTest.java | 2 +- .../consensuscommit/CommitHandlerTest.java | 2 +- 24 files changed, 241 insertions(+), 35 deletions(-) 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 70ba660499..c034bba5f8 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 @@ -49,7 +49,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 systemNamespace; 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 2bac099d64..99e9b29aef 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 @@ -71,7 +71,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 6c7149ecd2..3b1beb5e24 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 @@ -162,7 +162,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 ee4d9a74e3..f5d2e67f19 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; @@ -984,8 +978,22 @@ public Set getNamespaceNames() 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 8224ec6f08..2567c7c203 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 10065f0db7..d8b985e3ec 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 @@ -513,4 +513,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 06d30ca52f..8e69660f69 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 eb3ac7e94f..43d219b13b 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; @@ -404,4 +405,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 b8e8c2c8b3..cdf940d9b9 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; @@ -440,4 +441,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 11efe2c315..e27153ea4d 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 @@ -396,4 +396,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 ba3c9868c2..59dff72a25 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 @@ -329,4 +329,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 8581361c7e..aa76a7b570 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 @@ -284,7 +284,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 6e3c6289fb..584e1c8d48 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 @@ -38,7 +38,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 e57adac730..804a394426 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 @@ -33,6 +33,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; @@ -5474,6 +5475,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 7041f08ef7..4dc68deeb1 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 @@ -866,25 +866,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"); @@ -906,7 +911,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); @@ -951,9 +957,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); @@ -990,9 +998,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