From ab7e018c0e07640247c9313885f7431ffa673611 Mon Sep 17 00:00:00 2001 From: brfrn169 Date: Fri, 21 Nov 2025 13:18:50 +0900 Subject: [PATCH 1/4] Add isConsistentVirtualTableRead() to StorageInfo --- .../db/api/DistributedStorageAdmin.java | 3 + .../java/com/scalar/db/api/StorageInfo.java | 7 ++ .../com/scalar/db/common/StorageInfoImpl.java | 10 +- .../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 | 2 + .../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, 221 insertions(+), 33 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..fafeb79f82 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 isConsistentVirtualTableRead(); + /** * 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..e0f7e3eb09 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 consistentVirtualTableRead; public StorageInfoImpl( String storageName, MutationAtomicityUnit mutationAtomicityUnit, - int maxAtomicMutationsCount) { + int maxAtomicMutationsCount, + boolean consistentVirtualTableRead) { this.storageName = storageName; this.mutationAtomicityUnit = mutationAtomicityUnit; this.maxAtomicMutationsCount = maxAtomicMutationsCount; + this.consistentVirtualTableRead = consistentVirtualTableRead; } @Override @@ -36,6 +39,11 @@ public int getMaxAtomicMutationsCount() { return maxAtomicMutationsCount; } + @Override + public boolean isConsistentVirtualTableRead() { + return consistentVirtualTableRead; + } + @Override public boolean equals(Object o) { if (this == o) { 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..353b92ccaa 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 consistentVirtualTableRead; + try (Connection connection = dataSource.getConnection()) { + int isolationLevel = connection.getTransactionIsolation(); + consistentVirtualTableRead = + isolationLevel >= rdbEngine.getMinimumIsolationLevelForConsistencyReads(); + } 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, + consistentVirtualTableRead); } @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..5eecca527c 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 getMinimumIsolationLevelForConsistencyReads() { + // 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..b0ba414dfa 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 getMinimumIsolationLevelForConsistencyReads() { + // 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..e4d27965b7 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 getMinimumIsolationLevelForConsistencyReads() { + // 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..15ebb99781 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 getMinimumIsolationLevelForConsistencyReads() { + // 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..848bc00d85 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 getMinimumIsolationLevelForConsistencyReads() { + // 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..526d488f69 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 getMinimumIsolationLevelForConsistencyReads() { + // 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..2cd3ec13a2 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,6 @@ default void throwIfConjunctionsColumnNotSupported( Set conjunctions, TableMetadata metadata) {} String getTableNamesInNamespaceSql(); + + int getMinimumIsolationLevelForConsistencyReads(); } 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..bec8c7851f 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.isConsistentVirtualTableRead()); } 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..857070e267 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 read guarantee based on RDB engine + RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine); + boolean expectedConsistentVirtualTableRead = + isolationLevel >= strategy.getMinimumIsolationLevelForConsistencyReads(); + assertThat(storageInfo.isConsistentVirtualTableRead()) + .isEqualTo(expectedConsistentVirtualTableRead); + } + + @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 read guarantee based on RDB engine + RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine); + boolean expectedConsistentVirtualTableRead = + isolationLevel >= strategy.getMinimumIsolationLevelForConsistencyReads(); + assertThat(storageInfo.isConsistentVirtualTableRead()) + .isEqualTo(expectedConsistentVirtualTableRead); + } + + @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 read guarantee based on RDB engine + RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine); + boolean expectedConsistentVirtualTableRead = + isolationLevel >= strategy.getMinimumIsolationLevelForConsistencyReads(); + assertThat(storageInfo.isConsistentVirtualTableRead()) + .isEqualTo(expectedConsistentVirtualTableRead); + } + + @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 From 18121d2202adf46e6ae532c3f6fad68918c11319 Mon Sep 17 00:00:00 2001 From: brfrn169 Date: Fri, 21 Nov 2025 13:32:45 +0900 Subject: [PATCH 2/4] Fix based on feedback --- .../java/com/scalar/db/common/StorageInfoImpl.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 e0f7e3eb09..74b4e81317 100644 --- a/core/src/main/java/com/scalar/db/common/StorageInfoImpl.java +++ b/core/src/main/java/com/scalar/db/common/StorageInfoImpl.java @@ -55,12 +55,17 @@ public boolean equals(Object o) { StorageInfoImpl that = (StorageInfoImpl) o; return getMaxAtomicMutationsCount() == that.getMaxAtomicMutationsCount() && Objects.equals(getStorageName(), that.getStorageName()) - && getMutationAtomicityUnit() == that.getMutationAtomicityUnit(); + && getMutationAtomicityUnit() == that.getMutationAtomicityUnit() + && isConsistentVirtualTableRead() == that.isConsistentVirtualTableRead(); } @Override public int hashCode() { - return Objects.hash(getStorageName(), getMutationAtomicityUnit(), getMaxAtomicMutationsCount()); + return Objects.hash( + getStorageName(), + getMutationAtomicityUnit(), + getMaxAtomicMutationsCount(), + isConsistentVirtualTableRead()); } @Override @@ -69,6 +74,7 @@ public String toString() { .add("storageName", storageName) .add("mutationAtomicityUnit", mutationAtomicityUnit) .add("maxAtomicMutationsCount", maxAtomicMutationsCount) + .add("consistentVirtualTableRead", consistentVirtualTableRead) .toString(); } } From 92e5729d112ae559b0a8d7230e74aa78f7cf00e3 Mon Sep 17 00:00:00 2001 From: brfrn169 Date: Fri, 21 Nov 2025 16:04:03 +0900 Subject: [PATCH 3/4] Rename isConsistentVirtualTableRead() to isConsistentVirtualTableReadGuaranteed() --- .../java/com/scalar/db/api/StorageInfo.java | 2 +- .../com/scalar/db/common/StorageInfoImpl.java | 17 ++++++----- .../com/scalar/db/storage/jdbc/JdbcAdmin.java | 8 ++--- .../scalar/db/storage/jdbc/RdbEngineDb2.java | 2 +- .../db/storage/jdbc/RdbEngineMysql.java | 2 +- .../db/storage/jdbc/RdbEngineOracle.java | 2 +- .../db/storage/jdbc/RdbEnginePostgresql.java | 2 +- .../db/storage/jdbc/RdbEngineSqlServer.java | 2 +- .../db/storage/jdbc/RdbEngineSqlite.java | 2 +- .../db/storage/jdbc/RdbEngineStrategy.java | 2 +- .../multistorage/MultiStorageAdmin.java | 2 +- .../scalar/db/storage/jdbc/JdbcAdminTest.java | 30 +++++++++---------- 12 files changed, 37 insertions(+), 36 deletions(-) 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 fafeb79f82..35a40dde2b 100644 --- a/core/src/main/java/com/scalar/db/api/StorageInfo.java +++ b/core/src/main/java/com/scalar/db/api/StorageInfo.java @@ -27,7 +27,7 @@ public interface StorageInfo { * * @return true if the storage guarantees consistent reads for virtual tables, false otherwise */ - boolean isConsistentVirtualTableRead(); + 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 74b4e81317..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,17 +11,17 @@ public class StorageInfoImpl implements StorageInfo { private final String storageName; private final MutationAtomicityUnit mutationAtomicityUnit; private final int maxAtomicMutationsCount; - private final boolean consistentVirtualTableRead; + private final boolean consistentVirtualTableReadGuaranteed; public StorageInfoImpl( String storageName, MutationAtomicityUnit mutationAtomicityUnit, int maxAtomicMutationsCount, - boolean consistentVirtualTableRead) { + boolean consistentVirtualTableReadGuaranteed) { this.storageName = storageName; this.mutationAtomicityUnit = mutationAtomicityUnit; this.maxAtomicMutationsCount = maxAtomicMutationsCount; - this.consistentVirtualTableRead = consistentVirtualTableRead; + this.consistentVirtualTableReadGuaranteed = consistentVirtualTableReadGuaranteed; } @Override @@ -40,8 +40,8 @@ public int getMaxAtomicMutationsCount() { } @Override - public boolean isConsistentVirtualTableRead() { - return consistentVirtualTableRead; + public boolean isConsistentVirtualTableReadGuaranteed() { + return consistentVirtualTableReadGuaranteed; } @Override @@ -56,7 +56,8 @@ public boolean equals(Object o) { return getMaxAtomicMutationsCount() == that.getMaxAtomicMutationsCount() && Objects.equals(getStorageName(), that.getStorageName()) && getMutationAtomicityUnit() == that.getMutationAtomicityUnit() - && isConsistentVirtualTableRead() == that.isConsistentVirtualTableRead(); + && isConsistentVirtualTableReadGuaranteed() + == that.isConsistentVirtualTableReadGuaranteed(); } @Override @@ -65,7 +66,7 @@ public int hashCode() { getStorageName(), getMutationAtomicityUnit(), getMaxAtomicMutationsCount(), - isConsistentVirtualTableRead()); + isConsistentVirtualTableReadGuaranteed()); } @Override @@ -74,7 +75,7 @@ public String toString() { .add("storageName", storageName) .add("mutationAtomicityUnit", mutationAtomicityUnit) .add("maxAtomicMutationsCount", maxAtomicMutationsCount) - .add("consistentVirtualTableRead", consistentVirtualTableRead) + .add("consistentVirtualTableReadGuaranteed", consistentVirtualTableReadGuaranteed) .toString(); } } 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 353b92ccaa..c4ea815917 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 @@ -1006,11 +1006,11 @@ public void upgrade(Map options) throws ExecutionException { @Override public StorageInfo getStorageInfo(String namespace) throws ExecutionException { - boolean consistentVirtualTableRead; + boolean consistentVirtualTableReadGuaranteed; try (Connection connection = dataSource.getConnection()) { int isolationLevel = connection.getTransactionIsolation(); - consistentVirtualTableRead = - isolationLevel >= rdbEngine.getMinimumIsolationLevelForConsistencyReads(); + consistentVirtualTableReadGuaranteed = + isolationLevel >= rdbEngine.getMinimumIsolationLevelForConsistentVirtualTableReads(); } catch (SQLException e) { throw new ExecutionException("Getting the transaction isolation level failed", e); } @@ -1020,7 +1020,7 @@ public StorageInfo getStorageInfo(String namespace) throws ExecutionException { StorageInfo.MutationAtomicityUnit.STORAGE, // No limit on the number of mutations Integer.MAX_VALUE, - consistentVirtualTableRead); + 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 5eecca527c..bf7cf62839 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 @@ -591,7 +591,7 @@ public String getTableNamesInNamespaceSql() { } @Override - public int getMinimumIsolationLevelForConsistencyReads() { + public int getMinimumIsolationLevelForConsistentVirtualTableReads() { // 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 b0ba414dfa..cb89cfd531 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 @@ -511,7 +511,7 @@ public String getTableNamesInNamespaceSql() { } @Override - public int getMinimumIsolationLevelForConsistencyReads() { + public int getMinimumIsolationLevelForConsistentVirtualTableReads() { // 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 e4d27965b7..c1270da16f 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 @@ -543,7 +543,7 @@ public String getTableNamesInNamespaceSql() { } @Override - public int getMinimumIsolationLevelForConsistencyReads() { + public int getMinimumIsolationLevelForConsistentVirtualTableReads() { // 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 15ebb99781..8d1d8c971c 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 @@ -403,7 +403,7 @@ public String getTableNamesInNamespaceSql() { } @Override - public int getMinimumIsolationLevelForConsistencyReads() { + public int getMinimumIsolationLevelForConsistentVirtualTableReads() { // 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 848bc00d85..39f16e9f53 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 @@ -439,7 +439,7 @@ public String getTableNamesInNamespaceSql() { } @Override - public int getMinimumIsolationLevelForConsistencyReads() { + public int getMinimumIsolationLevelForConsistentVirtualTableReads() { // 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 526d488f69..e23ef4d05e 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 @@ -391,7 +391,7 @@ public String getTableNamesInNamespaceSql() { } @Override - public int getMinimumIsolationLevelForConsistencyReads() { + public int getMinimumIsolationLevelForConsistentVirtualTableReads() { // 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 2cd3ec13a2..442a29771d 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 @@ -326,5 +326,5 @@ default void throwIfConjunctionsColumnNotSupported( String getTableNamesInNamespaceSql(); - int getMinimumIsolationLevelForConsistencyReads(); + int getMinimumIsolationLevelForConsistentVirtualTableReads(); } 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 bec8c7851f..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 @@ -306,7 +306,7 @@ public StorageInfo getStorageInfo(String namespace) throws ExecutionException { holder.storageName, storageInfo.getMutationAtomicityUnit(), storageInfo.getMaxAtomicMutationsCount(), - storageInfo.isConsistentVirtualTableRead()); + storageInfo.isConsistentVirtualTableReadGuaranteed()); } catch (RuntimeException e) { if (e.getCause() instanceof ExecutionException) { throw (ExecutionException) e.getCause(); 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 857070e267..215788c4c9 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 @@ -6219,12 +6219,12 @@ void getStorageInfo_WithRepeatableReadIsolationLevel_ShouldReturnCorrectInfo(Rdb .isEqualTo(StorageInfo.MutationAtomicityUnit.STORAGE); assertThat(storageInfo.getMaxAtomicMutationsCount()).isEqualTo(Integer.MAX_VALUE); - // Check consistent read guarantee based on RDB engine + // Check consistent virtual table read guarantee based on RDB engine RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine); - boolean expectedConsistentVirtualTableRead = - isolationLevel >= strategy.getMinimumIsolationLevelForConsistencyReads(); - assertThat(storageInfo.isConsistentVirtualTableRead()) - .isEqualTo(expectedConsistentVirtualTableRead); + boolean expectedConsistentVirtualTableReadGuaranteed = + isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableReads(); + assertThat(storageInfo.isConsistentVirtualTableReadGuaranteed()) + .isEqualTo(expectedConsistentVirtualTableReadGuaranteed); } @ParameterizedTest @@ -6246,12 +6246,12 @@ void getStorageInfo_WithReadCommittedIsolationLevel_ShouldReturnCorrectInfo(RdbE .isEqualTo(StorageInfo.MutationAtomicityUnit.STORAGE); assertThat(storageInfo.getMaxAtomicMutationsCount()).isEqualTo(Integer.MAX_VALUE); - // Check consistent read guarantee based on RDB engine + // Check consistent virtual table read guarantee based on RDB engine RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine); - boolean expectedConsistentVirtualTableRead = - isolationLevel >= strategy.getMinimumIsolationLevelForConsistencyReads(); - assertThat(storageInfo.isConsistentVirtualTableRead()) - .isEqualTo(expectedConsistentVirtualTableRead); + boolean expectedConsistentVirtualTableReadGuaranteed = + isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableReads(); + assertThat(storageInfo.isConsistentVirtualTableReadGuaranteed()) + .isEqualTo(expectedConsistentVirtualTableReadGuaranteed); } @ParameterizedTest @@ -6273,12 +6273,12 @@ void getStorageInfo_WithSerializableIsolationLevel_ShouldReturnCorrectInfo(RdbEn .isEqualTo(StorageInfo.MutationAtomicityUnit.STORAGE); assertThat(storageInfo.getMaxAtomicMutationsCount()).isEqualTo(Integer.MAX_VALUE); - // Check consistent read guarantee based on RDB engine + // Check consistent virtual table read guarantee based on RDB engine RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine); - boolean expectedConsistentVirtualTableRead = - isolationLevel >= strategy.getMinimumIsolationLevelForConsistencyReads(); - assertThat(storageInfo.isConsistentVirtualTableRead()) - .isEqualTo(expectedConsistentVirtualTableRead); + boolean expectedConsistentVirtualTableReadGuaranteed = + isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableReads(); + assertThat(storageInfo.isConsistentVirtualTableReadGuaranteed()) + .isEqualTo(expectedConsistentVirtualTableReadGuaranteed); } @ParameterizedTest From 2747303c5be4e008a84327bcda5d0137437ee5f7 Mon Sep 17 00:00:00 2001 From: brfrn169 Date: Fri, 21 Nov 2025 18:55:33 +0900 Subject: [PATCH 4/4] Rename and add javadoc --- .../java/com/scalar/db/storage/jdbc/JdbcAdmin.java | 2 +- .../com/scalar/db/storage/jdbc/RdbEngineDb2.java | 2 +- .../com/scalar/db/storage/jdbc/RdbEngineMysql.java | 2 +- .../com/scalar/db/storage/jdbc/RdbEngineOracle.java | 2 +- .../scalar/db/storage/jdbc/RdbEnginePostgresql.java | 2 +- .../scalar/db/storage/jdbc/RdbEngineSqlServer.java | 2 +- .../com/scalar/db/storage/jdbc/RdbEngineSqlite.java | 2 +- .../scalar/db/storage/jdbc/RdbEngineStrategy.java | 13 ++++++++++++- .../com/scalar/db/storage/jdbc/JdbcAdminTest.java | 6 +++--- 9 files changed, 22 insertions(+), 11 deletions(-) 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 c4ea815917..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 @@ -1010,7 +1010,7 @@ public StorageInfo getStorageInfo(String namespace) throws ExecutionException { try (Connection connection = dataSource.getConnection()) { int isolationLevel = connection.getTransactionIsolation(); consistentVirtualTableReadGuaranteed = - isolationLevel >= rdbEngine.getMinimumIsolationLevelForConsistentVirtualTableReads(); + isolationLevel >= rdbEngine.getMinimumIsolationLevelForConsistentVirtualTableRead(); } catch (SQLException e) { throw new ExecutionException("Getting the transaction isolation level failed", e); } 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 bf7cf62839..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 @@ -591,7 +591,7 @@ public String getTableNamesInNamespaceSql() { } @Override - public int getMinimumIsolationLevelForConsistentVirtualTableReads() { + 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 cb89cfd531..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 @@ -511,7 +511,7 @@ public String getTableNamesInNamespaceSql() { } @Override - public int getMinimumIsolationLevelForConsistentVirtualTableReads() { + 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 c1270da16f..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 @@ -543,7 +543,7 @@ public String getTableNamesInNamespaceSql() { } @Override - public int getMinimumIsolationLevelForConsistentVirtualTableReads() { + 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 8d1d8c971c..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 @@ -403,7 +403,7 @@ public String getTableNamesInNamespaceSql() { } @Override - public int getMinimumIsolationLevelForConsistentVirtualTableReads() { + 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 39f16e9f53..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 @@ -439,7 +439,7 @@ public String getTableNamesInNamespaceSql() { } @Override - public int getMinimumIsolationLevelForConsistentVirtualTableReads() { + 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 e23ef4d05e..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 @@ -391,7 +391,7 @@ public String getTableNamesInNamespaceSql() { } @Override - public int getMinimumIsolationLevelForConsistentVirtualTableReads() { + 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 442a29771d..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 @@ -326,5 +326,16 @@ default void throwIfConjunctionsColumnNotSupported( String getTableNamesInNamespaceSql(); - int getMinimumIsolationLevelForConsistentVirtualTableReads(); + /** + * 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/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java index 215788c4c9..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 @@ -6222,7 +6222,7 @@ void getStorageInfo_WithRepeatableReadIsolationLevel_ShouldReturnCorrectInfo(Rdb // Check consistent virtual table read guarantee based on RDB engine RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine); boolean expectedConsistentVirtualTableReadGuaranteed = - isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableReads(); + isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableRead(); assertThat(storageInfo.isConsistentVirtualTableReadGuaranteed()) .isEqualTo(expectedConsistentVirtualTableReadGuaranteed); } @@ -6249,7 +6249,7 @@ void getStorageInfo_WithReadCommittedIsolationLevel_ShouldReturnCorrectInfo(RdbE // Check consistent virtual table read guarantee based on RDB engine RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine); boolean expectedConsistentVirtualTableReadGuaranteed = - isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableReads(); + isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableRead(); assertThat(storageInfo.isConsistentVirtualTableReadGuaranteed()) .isEqualTo(expectedConsistentVirtualTableReadGuaranteed); } @@ -6276,7 +6276,7 @@ void getStorageInfo_WithSerializableIsolationLevel_ShouldReturnCorrectInfo(RdbEn // Check consistent virtual table read guarantee based on RDB engine RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine); boolean expectedConsistentVirtualTableReadGuaranteed = - isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableReads(); + isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableRead(); assertThat(storageInfo.isConsistentVirtualTableReadGuaranteed()) .isEqualTo(expectedConsistentVirtualTableReadGuaranteed); }