From 58f82af2037dbaabb582d1bf6b96b463983122f1 Mon Sep 17 00:00:00 2001 From: Vincent Guilpain Date: Wed, 24 Sep 2025 15:14:08 +0900 Subject: [PATCH 1/7] For Oracle, change the data type for BLOB column to support storing up to 2GB --- core/build.gradle | 1 + ...tAdminIntegrationTestWithJdbcDatabase.java | 2 +- ...bcAdminCaseSensitivityIntegrationTest.java | 2 +- .../jdbc/JdbcAdminImportTestUtils.java | 2 +- .../jdbc/JdbcAdminIntegrationTest.java | 2 +- ...dbcDatabaseColumnValueIntegrationTest.java | 206 ++++++++++++++++++ ...aseConditionalMutationIntegrationTest.java | 5 + ...baseCrossPartitionScanIntegrationTest.java | 7 +- ...tipleClusteringKeyScanIntegrationTest.java | 2 +- ...seMultiplePartitionKeyIntegrationTest.java | 2 +- ...DatabaseSecondaryIndexIntegrationTest.java | 6 +- ...ingleClusteringKeyScanIntegrationTest.java | 2 +- ...baseSinglePartitionKeyIntegrationTest.java | 2 +- ...nAdminIntegrationTestWithJdbcDatabase.java | 2 +- .../JdbcTransactionAdminIntegrationTest.java | 2 +- .../java/com/scalar/db/common/CoreError.java | 18 ++ .../db/common/checker/OperationChecker.java | 2 +- .../db/storage/jdbc/JdbcOperationChecker.java | 10 + .../db/storage/jdbc/RdbEngineOracle.java | 86 +++++++- .../db/storage/jdbc/RdbEngineStrategy.java | 18 ++ .../jdbc/query/PreparedStatementBinder.java | 3 +- .../common/checker/OperationCheckerTest.java | 7 +- .../scalar/db/storage/jdbc/JdbcAdminTest.java | 75 ++++++- .../jdbc/JdbcOperationCheckerTest.java | 46 +++- .../db/storage/jdbc/RdbEngineOracleTest.java | 163 ++++++++++++++ ...StorageColumnValueIntegrationTestBase.java | 36 +-- ...onditionalMutationIntegrationTestBase.java | 198 ++++++++++------- ...CrossPartitionScanIntegrationTestBase.java | 38 ++-- 28 files changed, 806 insertions(+), 139 deletions(-) diff --git a/core/build.gradle b/core/build.gradle index 63292e79cd..811bd26d19 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -251,6 +251,7 @@ task integrationTestJdbc(type: Test) { options { systemProperties(System.getProperties().findAll{it.key.toString().startsWith("scalardb")}) } + maxHeapSize = "4g" } task integrationTestMultiStorage(type: Test) { diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/ConsensusCommitAdminIntegrationTestWithJdbcDatabase.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/ConsensusCommitAdminIntegrationTestWithJdbcDatabase.java index 37c0ef9de2..352efe8243 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/ConsensusCommitAdminIntegrationTestWithJdbcDatabase.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/ConsensusCommitAdminIntegrationTestWithJdbcDatabase.java @@ -418,6 +418,6 @@ public void alterColumnType_Sqlite_AlterColumnType_ShouldThrowUnsupportedOperati @Override protected boolean isIndexOnBlobColumnSupported() { - return !JdbcTestUtils.isDb2(rdbEngine); + return !(JdbcTestUtils.isDb2(rdbEngine) || JdbcTestUtils.isOracle(rdbEngine)); } } diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminCaseSensitivityIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminCaseSensitivityIntegrationTest.java index 71dad0d7a7..b7c086002c 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminCaseSensitivityIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminCaseSensitivityIntegrationTest.java @@ -498,6 +498,6 @@ public void alterColumnType_Sqlite_AlterColumnType_ShouldThrowUnsupportedOperati @Override protected boolean isIndexOnBlobColumnSupported() { - return !JdbcTestUtils.isDb2(rdbEngine); + return !(JdbcTestUtils.isDb2(rdbEngine) || JdbcTestUtils.isOracle(rdbEngine)); } } diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminImportTestUtils.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminImportTestUtils.java index 333185686a..238d36561a 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminImportTestUtils.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminImportTestUtils.java @@ -482,7 +482,7 @@ private LinkedHashMap prepareColumnsForDb2() { columns.put("col17", "NCLOB(512)"); columns.put("col18", "BINARY(5)"); columns.put("col19", "VARBINARY(512)"); - columns.put("col20", "BLOB(1024)"); + columns.put("col20", "BLOB(2G)"); columns.put("col21", "CHAR(5) FOR BIT DATA"); columns.put("col22", "VARCHAR(512) FOR BIT DATA"); columns.put("col23", "DATE"); diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminIntegrationTest.java index 7bddc4a058..41c2e20a31 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcAdminIntegrationTest.java @@ -498,6 +498,6 @@ public void alterColumnType_Sqlite_AlterColumnType_ShouldThrowUnsupportedOperati @Override protected boolean isIndexOnBlobColumnSupported() { - return !JdbcTestUtils.isDb2(rdbEngine); + return !(JdbcTestUtils.isDb2(rdbEngine) || JdbcTestUtils.isOracle(rdbEngine)); } } diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java index 970aea8dc7..cae40f1189 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java @@ -1,12 +1,41 @@ package com.scalar.db.storage.jdbc; +import static org.assertj.core.api.Assertions.assertThat; + import com.scalar.db.api.DistributedStorageColumnValueIntegrationTestBase; +import com.scalar.db.api.Get; +import com.scalar.db.api.Put; +import com.scalar.db.api.PutBuilder; +import com.scalar.db.api.Result; +import com.scalar.db.api.TableMetadata; import com.scalar.db.config.DatabaseConfig; +import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.io.BigIntColumn; +import com.scalar.db.io.BlobColumn; +import com.scalar.db.io.BooleanColumn; import com.scalar.db.io.Column; import com.scalar.db.io.DataType; +import com.scalar.db.io.DateColumn; +import com.scalar.db.io.DoubleColumn; +import com.scalar.db.io.FloatColumn; +import com.scalar.db.io.IntColumn; +import com.scalar.db.io.Key; +import com.scalar.db.io.TextColumn; +import com.scalar.db.io.TimeColumn; +import com.scalar.db.io.TimestampColumn; +import com.scalar.db.io.TimestampTZColumn; import com.scalar.db.util.TestUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import java.util.Properties; import java.util.Random; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class JdbcDatabaseColumnValueIntegrationTest extends DistributedStorageColumnValueIntegrationTestBase { @@ -68,4 +97,181 @@ protected Column getColumnWithMaxValue(String columnName, DataType dataType) } return super.getColumnWithMaxValue(columnName, dataType); } + // TODO Test this for all storages + @EnabledIf("isDb2OrOracle") + @ParameterizedTest() + @MethodSource("provideBlobSizes") + public void put_largeBlobData_ShouldWorkCorrectly(int blobSize, String humanReadableBlobSize) + throws ExecutionException { + String tableName = TABLE + "_large_single_blob"; + try { + // Arrange + TableMetadata.Builder metadata = + TableMetadata.newBuilder() + .addColumn(COL_NAME1, DataType.INT) + .addColumn(COL_NAME2, DataType.BLOB) + .addPartitionKey(COL_NAME1); + + admin.createTable(namespace, tableName, metadata.build(), true, getCreationOptions()); + byte[] blobData = createLargeBlob(blobSize); + Put put = + Put.newBuilder() + .namespace(namespace) + .table(tableName) + .partitionKey(Key.ofInt(COL_NAME1, 1)) + .blobValue(COL_NAME2, blobData) + .build(); + + // Act + storage.put(put); + + // Assert + Optional optionalResult = + storage.get( + Get.newBuilder() + .namespace(namespace) + .table(tableName) + .partitionKey(Key.ofInt(COL_NAME1, 1)) + .build()); + assertThat(optionalResult).isPresent(); + Result result = optionalResult.get(); + assertThat(result.getColumns().get(COL_NAME2).getBlobValueAsBytes()).isEqualTo(blobData); + } finally { + admin.dropTable(namespace, tableName, true); + } + } + + Stream provideBlobSizes() { + List args = new ArrayList<>(); + if (isOracle()) { + // As explained in + // `com.scalar.db.storage.jdbc.RdbEngineOracle.bindBlobColumnToPreparedStatement()`, + // handing a BLOB size bigger than 32,767 bytes requires a workaround so we particularly test + // values around it. + args.add(Arguments.of(32_766, "32.766 KB")); + args.add(Arguments.of(32_767, "32.767 KB")); + } + args.add(Arguments.of(100_000_000, "100 MB")); + return args.stream(); + } + + @EnabledIf("isOracle") + @Test + public void put_largeBlobData_WithMultipleBlobColumnsShouldWorkCorrectly() + throws ExecutionException { + String tableName = TABLE + "_large_multiples_blob"; + try { + // Arrange + TableMetadata.Builder metadata = + TableMetadata.newBuilder() + .addColumn(COL_NAME1, DataType.INT) + .addColumn(COL_NAME2, DataType.BLOB) + .addColumn(COL_NAME3, DataType.BLOB) + .addPartitionKey(COL_NAME1); + + admin.createTable(namespace, tableName, metadata.build(), true, getCreationOptions()); + byte[] blobDataCol2 = createLargeBlob(32_766); + byte[] blobDataCol3 = createLargeBlob(5000); + Put put = + Put.newBuilder() + .namespace(namespace) + .table(tableName) + .partitionKey(Key.ofInt(COL_NAME1, 1)) + .blobValue(COL_NAME2, blobDataCol2) + .blobValue(COL_NAME3, blobDataCol3) + .build(); + + // Act + storage.put(put); + + // Assert + Optional optionalResult = + storage.get( + Get.newBuilder() + .namespace(namespace) + .table(tableName) + .partitionKey(Key.ofInt(COL_NAME1, 1)) + .build()); + assertThat(optionalResult).isPresent(); + Result result = optionalResult.get(); + assertThat(result.getColumns().get(COL_NAME2).getBlobValueAsBytes()).isEqualTo(blobDataCol2); + assertThat(result.getColumns().get(COL_NAME3).getBlobValueAsBytes()).isEqualTo(blobDataCol3); + } finally { + admin.dropTable(namespace, tableName, true); + } + } + + @EnabledIf("isOracle") + @Test + public void put_largeBlobData_WithAllColumnsTypesShouldWorkCorrectly() throws ExecutionException { + // Arrange + IntColumn partitionKeyValue = (IntColumn) getColumnWithMaxValue(PARTITION_KEY, DataType.INT); + BooleanColumn col1Value = (BooleanColumn) getColumnWithMaxValue(COL_NAME1, DataType.BOOLEAN); + IntColumn col2Value = (IntColumn) getColumnWithMaxValue(COL_NAME2, DataType.INT); + BigIntColumn col3Value = (BigIntColumn) getColumnWithMaxValue(COL_NAME3, DataType.BIGINT); + FloatColumn col4Value = (FloatColumn) getColumnWithMaxValue(COL_NAME4, DataType.FLOAT); + DoubleColumn col5Value = (DoubleColumn) getColumnWithMaxValue(COL_NAME5, DataType.DOUBLE); + TextColumn col6Value = (TextColumn) getColumnWithMaxValue(COL_NAME6, DataType.TEXT); + BlobColumn col7Value = BlobColumn.of(COL_NAME7, createLargeBlob(32_766)); + DateColumn col8Value = (DateColumn) getColumnWithMaxValue(COL_NAME8, DataType.DATE); + TimeColumn col9Value = (TimeColumn) getColumnWithMaxValue(COL_NAME9, DataType.TIME); + TimestampTZColumn col10Value = + (TimestampTZColumn) getColumnWithMaxValue(COL_NAME10, DataType.TIMESTAMPTZ); + TimestampColumn column11Value = null; + if (isTimestampTypeSupported()) { + column11Value = (TimestampColumn) getColumnWithMaxValue(COL_NAME11, DataType.TIMESTAMP); + } + + PutBuilder.Buildable put = + Put.newBuilder() + .namespace(namespace) + .table(TABLE) + .partitionKey(Key.newBuilder().add(partitionKeyValue).build()) + .value(col1Value) + .value(col2Value) + .value(col3Value) + .value(col4Value) + .value(col5Value) + .value(col6Value) + .value(col7Value) + .value(col8Value) + .value(col9Value) + .value(col10Value); + if (isTimestampTypeSupported()) { + put.value(column11Value); + } + // Act + storage.put(put.build()); + + // Assert + assertResult( + partitionKeyValue, + col1Value, + col2Value, + col3Value, + col4Value, + col5Value, + col6Value, + col7Value, + col8Value, + col9Value, + col10Value, + column11Value); + } + + private byte[] createLargeBlob(int size) { + byte[] blob = new byte[size]; + random.nextBytes(blob); + return blob; + } + + @SuppressWarnings("unused") + private boolean isDb2OrOracle() { + return JdbcEnv.isOracle() || JdbcEnv.isDb2(); + } + + @SuppressWarnings("unused") + private boolean isOracle() { + return JdbcEnv.isOracle(); + } } diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseConditionalMutationIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseConditionalMutationIntegrationTest.java index ecf45dd959..5faca2a730 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseConditionalMutationIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseConditionalMutationIntegrationTest.java @@ -42,4 +42,9 @@ protected Column getColumnWithRandomValue( } return super.getColumnWithRandomValue(random, columnName, dataType); } + + @Override + protected boolean isConditionOnBlobColumnSupported() { + return !JdbcTestUtils.isOracle(rdbEngine); + } } diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseCrossPartitionScanIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseCrossPartitionScanIntegrationTest.java index 1e85cd44d5..0375c00cfc 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseCrossPartitionScanIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseCrossPartitionScanIntegrationTest.java @@ -83,6 +83,11 @@ protected Stream provideColumnsForCNFConditionsTest() { @Override protected boolean isOrderingOnBlobColumnSupported() { - return !JdbcTestUtils.isDb2(rdbEngine); + return !(JdbcTestUtils.isDb2(rdbEngine) || JdbcTestUtils.isOracle(rdbEngine)); + } + + @Override + protected boolean isConditionOnBlobColumnSupported() { + return !JdbcTestUtils.isOracle(rdbEngine); } } diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseMultipleClusteringKeyScanIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseMultipleClusteringKeyScanIntegrationTest.java index 2247c67e0b..9c9f85739e 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseMultipleClusteringKeyScanIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseMultipleClusteringKeyScanIntegrationTest.java @@ -100,7 +100,7 @@ protected List getDataTypes() { rdbEngine, ImmutableMap.of( RdbEngineOracle.class, - ImmutableList.of(DataType.TIMESTAMPTZ), + ImmutableList.of(DataType.TIMESTAMPTZ, DataType.BLOB), RdbEngineDb2.class, ImmutableList.of(DataType.BLOB))); } diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseMultiplePartitionKeyIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseMultiplePartitionKeyIntegrationTest.java index 82919a2f72..6f7c6bd4bd 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseMultiplePartitionKeyIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseMultiplePartitionKeyIntegrationTest.java @@ -94,7 +94,7 @@ protected List getDataTypes() { rdbEngine, ImmutableMap.of( RdbEngineOracle.class, - ImmutableList.of(DataType.TIMESTAMPTZ), + ImmutableList.of(DataType.TIMESTAMPTZ, DataType.BLOB), RdbEngineYugabyte.class, ImmutableList.of(DataType.FLOAT, DataType.DOUBLE), RdbEngineDb2.class, diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseSecondaryIndexIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseSecondaryIndexIntegrationTest.java index 16fc9182c3..984a603423 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseSecondaryIndexIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseSecondaryIndexIntegrationTest.java @@ -81,6 +81,10 @@ protected Set getSecondaryIndexTypes() { JdbcTestUtils.filterDataTypes( Arrays.asList(DataType.values()), rdbEngine, - ImmutableMap.of(RdbEngineDb2.class, ImmutableList.of(DataType.BLOB)))); + ImmutableMap.of( + RdbEngineDb2.class, + ImmutableList.of(DataType.BLOB), + RdbEngineOracle.class, + ImmutableList.of(DataType.BLOB)))); } } diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseSingleClusteringKeyScanIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseSingleClusteringKeyScanIntegrationTest.java index 4f77dafed1..2e2ad7c67c 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseSingleClusteringKeyScanIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseSingleClusteringKeyScanIntegrationTest.java @@ -76,7 +76,7 @@ protected List getClusteringKeyTypes() { rdbEngine, ImmutableMap.of( RdbEngineOracle.class, - ImmutableList.of(DataType.TIMESTAMPTZ), + ImmutableList.of(DataType.TIMESTAMPTZ, DataType.BLOB), RdbEngineDb2.class, ImmutableList.of(DataType.BLOB))); } diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseSinglePartitionKeyIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseSinglePartitionKeyIntegrationTest.java index 480d384ec8..6319de3cf4 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseSinglePartitionKeyIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseSinglePartitionKeyIntegrationTest.java @@ -77,7 +77,7 @@ protected List getPartitionKeyTypes() { rdbEngine, ImmutableMap.of( RdbEngineOracle.class, - ImmutableList.of(DataType.TIMESTAMPTZ), + ImmutableList.of(DataType.TIMESTAMPTZ, DataType.BLOB), RdbEngineYugabyte.class, ImmutableList.of(DataType.FLOAT, DataType.DOUBLE), RdbEngineDb2.class, diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/SingleCrudOperationTransactionAdminIntegrationTestWithJdbcDatabase.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/SingleCrudOperationTransactionAdminIntegrationTestWithJdbcDatabase.java index 859fe2a57b..ec632157dd 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/SingleCrudOperationTransactionAdminIntegrationTestWithJdbcDatabase.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/SingleCrudOperationTransactionAdminIntegrationTestWithJdbcDatabase.java @@ -420,6 +420,6 @@ public void alterColumnType_Sqlite_AlterColumnType_ShouldThrowUnsupportedOperati @Override protected boolean isIndexOnBlobColumnSupported() { - return !JdbcTestUtils.isDb2(rdbEngine); + return !(JdbcTestUtils.isDb2(rdbEngine) || JdbcTestUtils.isOracle(rdbEngine)); } } diff --git a/core/src/integration-test/java/com/scalar/db/transaction/jdbc/JdbcTransactionAdminIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/transaction/jdbc/JdbcTransactionAdminIntegrationTest.java index 7ed7fe9e1f..a94e7b50a8 100644 --- a/core/src/integration-test/java/com/scalar/db/transaction/jdbc/JdbcTransactionAdminIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/transaction/jdbc/JdbcTransactionAdminIntegrationTest.java @@ -492,7 +492,7 @@ public void alterColumnType_Sqlite_AlterColumnType_ShouldThrowUnsupportedOperati @Override protected boolean isIndexOnBlobColumnSupported() { - return !JdbcTestUtils.isDb2(rdbEngine); + return !(JdbcTestUtils.isDb2(rdbEngine) || JdbcTestUtils.isOracle(rdbEngine)); } @Override diff --git a/core/src/main/java/com/scalar/db/common/CoreError.java b/core/src/main/java/com/scalar/db/common/CoreError.java index 259b029be9..de3224fb57 100644 --- a/core/src/main/java/com/scalar/db/common/CoreError.java +++ b/core/src/main/java/com/scalar/db/common/CoreError.java @@ -808,6 +808,24 @@ public enum CoreError implements ScalarDbError { "Db2 does not support column type conversion from %s to %s", "", ""), + JDBC_ORACLE_INDEX_OR_KEY_ON_BLOB_COLUMN_NOT_SUPPORTED( + Category.USER_ERROR, + "0241", + "With Oracle, using a BLOB column as partition key, clustering key or secondary index is not supported.", + "", + ""), + JDBC_ORACLE_CROSS_PARTITION_SCAN_ORDERING_ON_BLOB_COLUMN_NOT_SUPPORTED( + Category.USER_ERROR, + "0242", + "With Oracle, setting an ordering on a BLOB column when using a cross partition scan operation is not supported. Ordering: %s", + "", + ""), + JDBC_ORACLE_CROSS_PARTITION_SCAN_CONDITION_ON_BLOB_COLUMN_NOT_SUPPORTED( + Category.USER_ERROR, + "0243", + "With Oracle, setting a condition on a BLOB column when using a cross partition scan operation is not supported. Condition: %s", + "", + ""), // // Errors for the concurrency error category diff --git a/core/src/main/java/com/scalar/db/common/checker/OperationChecker.java b/core/src/main/java/com/scalar/db/common/checker/OperationChecker.java index 8a977be66f..0cc122656d 100644 --- a/core/src/main/java/com/scalar/db/common/checker/OperationChecker.java +++ b/core/src/main/java/com/scalar/db/common/checker/OperationChecker.java @@ -268,7 +268,7 @@ protected void checkOrderingsForScanAll(ScanAll scanAll, TableMetadata metadata) } } - private void checkConjunctions(Selection selection, TableMetadata metadata) { + protected void checkConjunctions(Selection selection, TableMetadata metadata) { for (Conjunction conjunction : selection.getConjunctions()) { for (ConditionalExpression condition : conjunction.getConditions()) { boolean isValid; diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcOperationChecker.java b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcOperationChecker.java index 8cf440d4a7..7644a8546d 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcOperationChecker.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcOperationChecker.java @@ -1,6 +1,7 @@ package com.scalar.db.storage.jdbc; import com.scalar.db.api.ScanAll; +import com.scalar.db.api.Selection; import com.scalar.db.api.TableMetadata; import com.scalar.db.common.StorageInfoProvider; import com.scalar.db.common.TableMetadataManager; @@ -26,4 +27,13 @@ protected void checkOrderingsForScanAll(ScanAll scanAll, TableMetadata metadata) super.checkOrderingsForScanAll(scanAll, metadata); rdbEngine.throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported(scanAll, metadata); } + + @Override + protected void checkConjunctions(Selection selection, TableMetadata metadata) { + super.checkConjunctions(selection, metadata); + if (selection instanceof ScanAll) { + rdbEngine.throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( + (ScanAll) selection, metadata); + } + } } 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 4bf9bcb5a6..86192353d2 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 @@ -3,7 +3,10 @@ import static com.scalar.db.util.ScalarDbUtils.getFullTableName; import com.google.common.annotations.VisibleForTesting; +import com.scalar.db.api.ConditionalExpression; import com.scalar.db.api.LikeExpression; +import com.scalar.db.api.Scan; +import com.scalar.db.api.ScanAll; import com.scalar.db.api.TableMetadata; import com.scalar.db.common.CoreError; import com.scalar.db.exception.storage.ExecutionException; @@ -12,14 +15,18 @@ import com.scalar.db.storage.jdbc.query.SelectQuery; import com.scalar.db.storage.jdbc.query.SelectWithFetchFirstNRowsOnly; import com.scalar.db.storage.jdbc.query.UpsertQuery; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.sql.Driver; import java.sql.JDBCType; +import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Types; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -243,7 +250,7 @@ public String getDataTypeForEngine(DataType scalarDbDataType) { case BIGINT: return "NUMBER(16)"; case BLOB: - return "RAW(2000)"; + return "BLOB"; case BOOLEAN: return "NUMBER(1)"; case DOUBLE: @@ -274,7 +281,8 @@ public String getDataTypeForKey(DataType dataType) { case TEXT: return "VARCHAR2(" + keyColumnSize + ")"; case BLOB: - return "RAW(" + keyColumnSize + ")"; + throw new UnsupportedOperationException( + CoreError.JDBC_ORACLE_INDEX_OR_KEY_ON_BLOB_COLUMN_NOT_SUPPORTED.buildMessage()); default: return null; } @@ -437,6 +445,49 @@ public String tryAddIfNotExistsToCreateIndexSql(String createIndexSql) { return createIndexSql; } + @Override + @Nullable + public String getDataTypeForSecondaryIndex(DataType dataType) { + if (dataType == DataType.BLOB) { + throw new UnsupportedOperationException( + CoreError.JDBC_ORACLE_INDEX_OR_KEY_ON_BLOB_COLUMN_NOT_SUPPORTED.buildMessage()); + } else { + return super.getDataTypeForSecondaryIndex(dataType); + } + } + + @Override + public void throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( + ScanAll scanAll, TableMetadata metadata) { + Optional orderingOnBlobColumn = + scanAll.getOrderings().stream() + .filter( + ordering -> metadata.getColumnDataType(ordering.getColumnName()) == DataType.BLOB) + .findFirst(); + if (orderingOnBlobColumn.isPresent()) { + throw new UnsupportedOperationException( + CoreError.JDBC_ORACLE_CROSS_PARTITION_SCAN_ORDERING_ON_BLOB_COLUMN_NOT_SUPPORTED + .buildMessage(orderingOnBlobColumn.get())); + } + } + + @Override + public void throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( + ScanAll scanAll, TableMetadata metadata) { + Optional conditionalExpression = + scanAll.getConjunctions().stream() + .flatMap(conjunction -> conjunction.getConditions().stream()) + .filter( + condition -> + metadata.getColumnDataType(condition.getColumn().getName()) == DataType.BLOB) + .findFirst(); + if (conditionalExpression.isPresent()) { + throw new UnsupportedOperationException( + CoreError.JDBC_ORACLE_CROSS_PARTITION_SCAN_CONDITION_ON_BLOB_COLUMN_NOT_SUPPORTED + .buildMessage(conditionalExpression.get())); + } + } + @Override public RdbEngineTimeTypeStrategy getTimeTypeStrategy() { @@ -451,4 +502,35 @@ public void throwIfAlterColumnTypeNotSupported(DataType from, DataType to) { from.toString(), to.toString())); } } + + @Override + public void bindBlobColumnToPreparedStatement( + PreparedStatement preparedStatement, int index, byte[] bytes) throws SQLException { + // When writing to the BLOB data type with a BLOB size greater than 32766 using a MERGE INTO + // statement, an internal error ORA-03137 on the server side occurs so we needed to use a + // workaround. This has been confirmed to be a limitation by AWS support. + // Below is a detailed explanation of the workaround. + // + // Depending on the byte array size, the JDBC driver automatically choose one the following mode + // to transfer the BLOB data to the server: + // - DIRECT: the most efficient mode. It's used when the byte array length is less than 32767. + // - STREAM: this mode is less efficient. It's used when the byte array length is greater than + // 32766. + // - LOB BINDING: this mode is the least efficient. It's used when an input stream without + // specifying the length is specified. + // + // When the driver selects the STREAM mode, the error + // ORA-03137 occurs. So, we work around the issue by making sure to use the driver in a way so + // that it should never selects the STREAM mode. + // For more details about the modes, see the following documentation: + // https://docs.oracle.com/en/database/oracle/oracle-database/23/jjdbc/LOBs-and-BFiles.html#GUID-8FD40D53-8D64-4187-9F6F-FF78242188AD + if (bytes.length <= 32766) { + // the DIRECT mode is used to send BLOB data of small size + preparedStatement.setBytes(index, bytes); + } else { + // the LOB BINDING mode is used to send BLOB data of large size + InputStream inputStream = new ByteArrayInputStream(bytes); + preparedStatement.setBinaryStream(index, inputStream); + } + } } 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 55ebc1937b..431900e15d 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 @@ -14,6 +14,7 @@ import java.sql.Connection; import java.sql.Driver; import java.sql.JDBCType; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLWarning; @@ -219,6 +220,11 @@ default OffsetDateTime encode(TimestampTZColumn column) { return column.getTimestampTZValue().atOffset(ZoneOffset.UTC); } + default void bindBlobColumnToPreparedStatement( + PreparedStatement preparedStatement, int index, byte[] bytes) throws SQLException { + preparedStatement.setBytes(index, bytes); + } + default DateColumn parseDateColumn(ResultSet resultSet, String columnName) throws SQLException { return DateColumn.of(columnName, resultSet.getObject(columnName, LocalDate.class)); } @@ -305,4 +311,16 @@ default void setConnectionToReadOnly(Connection connection, boolean readOnly) */ default void throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( ScanAll scanAll, TableMetadata metadata) {} + + /** + * Throws an exception if a cross-partition scan operation with a condition on a blob column is + * specified and is not supported in the underlying storage. + * + * @param scanAll the ScanAll operation + * @param metadata the table metadata + * @throws UnsupportedOperationException if the ScanAll operation contains a condition on a blob + * column, and it is not supported in the underlying storage + */ + default void throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( + ScanAll scanAll, TableMetadata metadata) {} } diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/query/PreparedStatementBinder.java b/core/src/main/java/com/scalar/db/storage/jdbc/query/PreparedStatementBinder.java index 906446f512..e1368b7257 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/query/PreparedStatementBinder.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/query/PreparedStatementBinder.java @@ -130,7 +130,8 @@ public void visit(BlobColumn column) { if (column.hasNullValue()) { preparedStatement.setNull(index++, getSqlType(column.getName())); } else { - preparedStatement.setBytes(index++, column.getBlobValueAsBytes()); + rdbEngine.bindBlobColumnToPreparedStatement( + preparedStatement, index++, column.getBlobValueAsBytes()); } } catch (SQLException e) { sqlException = e; 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 e734368dca..9e6fd87310 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 @@ -418,8 +418,9 @@ public void whenCheckingScanOperationWithEmptyOrdering_shouldNotThrowAnyExceptio } @Test - public void whenCheckingScanAllOperationWithCrossPartitionScanEnabledWithOrdering_shouldNotThrow() - throws ExecutionException { + public void + whenCheckingScanAllOperationWithCrossPartitionScanEnabledWithConditionAndOrdering_shouldNotThrow() + throws ExecutionException { // Arrange TableMetadata metadata = TableMetadata.newBuilder() @@ -434,11 +435,13 @@ public void whenCheckingScanAllOperationWithCrossPartitionScanEnabledWithOrderin .namespace(NAMESPACE) .table(TABLE_NAME) .all() + .where(ConditionBuilder.column(COL1).isEqualToInt(10)) .ordering(Scan.Ordering.asc(COL1)) .ordering(Scan.Ordering.desc(COL2)) .build(); when(databaseConfig.isCrossPartitionScanEnabled()).thenReturn(true); when(databaseConfig.isCrossPartitionScanOrderingEnabled()).thenReturn(true); + when(databaseConfig.isCrossPartitionScanFilteringEnabled()).thenReturn(true); operationChecker = new OperationChecker(databaseConfig, metadataManager, storageInfoProvider); 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 c8636162e2..82a089eb90 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 @@ -563,7 +563,7 @@ public void createTableInternal_ForSqlServer_ShouldCreateTableAndIndexes() throw public void createTableInternal_ForOracle_ShouldCreateTableAndIndexes() throws SQLException { createTableInternal_ForX_CreateTableAndIndexes( RdbEngine.ORACLE, - "CREATE TABLE \"my_ns\".\"foo_table\"(\"c3\" NUMBER(1),\"c1\" VARCHAR2(128),\"c5\" NUMBER(10),\"c2\" NUMBER(16),\"c4\" RAW(2000),\"c6\" BINARY_DOUBLE,\"c7\" BINARY_FLOAT,\"c8\" DATE,\"c9\" TIMESTAMP(6),\"c10\" TIMESTAMP(3),\"c11\" TIMESTAMP(3) WITH TIME ZONE, PRIMARY KEY (\"c3\",\"c1\",\"c5\")) ROWDEPENDENCIES", + "CREATE TABLE \"my_ns\".\"foo_table\"(\"c3\" NUMBER(1),\"c1\" VARCHAR2(128),\"c5\" NUMBER(10),\"c2\" NUMBER(16),\"c4\" BLOB,\"c6\" BINARY_DOUBLE,\"c7\" BINARY_FLOAT,\"c8\" DATE,\"c9\" TIMESTAMP(6),\"c10\" TIMESTAMP(3),\"c11\" TIMESTAMP(3) WITH TIME ZONE, PRIMARY KEY (\"c3\",\"c1\",\"c5\")) ROWDEPENDENCIES", "ALTER TABLE \"my_ns\".\"foo_table\" INITRANS 3 MAXTRANS 255", "CREATE UNIQUE INDEX \"my_ns.foo_table_clustering_order_idx\" ON \"my_ns\".\"foo_table\" (\"c3\" ASC,\"c1\" DESC,\"c5\" ASC)", "CREATE INDEX \"my_ns\".\"index_my_ns_foo_table_c5\" ON \"my_ns\".\"foo_table\" (\"c5\")", @@ -577,7 +577,7 @@ public void createTableInternal_ForOracle_ShouldCreateTableAndIndexes() throws S when(config.getOracleVariableKeyColumnSize()).thenReturn(64); createTableInternal_ForX_CreateTableAndIndexes( new RdbEngineOracle(config), - "CREATE TABLE \"my_ns\".\"foo_table\"(\"c3\" NUMBER(1),\"c1\" VARCHAR2(64),\"c5\" NUMBER(10),\"c2\" NUMBER(16),\"c4\" RAW(2000),\"c6\" BINARY_DOUBLE,\"c7\" BINARY_FLOAT,\"c8\" DATE,\"c9\" TIMESTAMP(6),\"c10\" TIMESTAMP(3),\"c11\" TIMESTAMP(3) WITH TIME ZONE, PRIMARY KEY (\"c3\",\"c1\",\"c5\")) ROWDEPENDENCIES", + "CREATE TABLE \"my_ns\".\"foo_table\"(\"c3\" NUMBER(1),\"c1\" VARCHAR2(64),\"c5\" NUMBER(10),\"c2\" NUMBER(16),\"c4\" BLOB,\"c6\" BINARY_DOUBLE,\"c7\" BINARY_FLOAT,\"c8\" DATE,\"c9\" TIMESTAMP(6),\"c10\" TIMESTAMP(3),\"c11\" TIMESTAMP(3) WITH TIME ZONE, PRIMARY KEY (\"c3\",\"c1\",\"c5\")) ROWDEPENDENCIES", "ALTER TABLE \"my_ns\".\"foo_table\" INITRANS 3 MAXTRANS 255", "CREATE UNIQUE INDEX \"my_ns.foo_table_clustering_order_idx\" ON \"my_ns\".\"foo_table\" (\"c3\" ASC,\"c1\" DESC,\"c5\" ASC)", "CREATE INDEX \"my_ns\".\"index_my_ns_foo_table_c5\" ON \"my_ns\".\"foo_table\" (\"c5\")", @@ -704,7 +704,7 @@ public void createTableInternal_IfNotExistsForOracle_ShouldCreateTableAndIndexes throws SQLException { createTableInternal_IfNotExistsForX_createTableAndIndexesIfNotExists( RdbEngine.ORACLE, - "CREATE TABLE \"my_ns\".\"foo_table\"(\"c3\" NUMBER(1),\"c1\" VARCHAR2(128),\"c5\" NUMBER(10),\"c2\" NUMBER(16),\"c4\" RAW(2000),\"c6\" BINARY_DOUBLE,\"c7\" BINARY_FLOAT,\"c8\" DATE,\"c9\" TIMESTAMP(6),\"c10\" TIMESTAMP(3),\"c11\" TIMESTAMP(3) WITH TIME ZONE, PRIMARY KEY (\"c3\",\"c1\",\"c5\")) ROWDEPENDENCIES", + "CREATE TABLE \"my_ns\".\"foo_table\"(\"c3\" NUMBER(1),\"c1\" VARCHAR2(128),\"c5\" NUMBER(10),\"c2\" NUMBER(16),\"c4\" BLOB,\"c6\" BINARY_DOUBLE,\"c7\" BINARY_FLOAT,\"c8\" DATE,\"c9\" TIMESTAMP(6),\"c10\" TIMESTAMP(3),\"c11\" TIMESTAMP(3) WITH TIME ZONE, PRIMARY KEY (\"c3\",\"c1\",\"c5\")) ROWDEPENDENCIES", "ALTER TABLE \"my_ns\".\"foo_table\" INITRANS 3 MAXTRANS 255", "CREATE UNIQUE INDEX \"my_ns.foo_table_clustering_order_idx\" ON \"my_ns\".\"foo_table\" (\"c3\" ASC,\"c1\" DESC,\"c5\" ASC)", "CREATE INDEX \"my_ns\".\"index_my_ns_foo_table_c5\" ON \"my_ns\".\"foo_table\" (\"c5\")", @@ -4722,7 +4722,8 @@ void hasDifferentClusteringOrders_GivenBothAscAndDescOrders_ShouldReturnTrue() { } @Test - void createTableInternal_WithBlobColumnAsKeyOrIndex_ShouldThrowUnsupportedOperationException() { + void + createTableInternal_Db2_WithBlobColumnAsKeyOrIndex_ShouldThrowUnsupportedOperationException() { // Arrange TableMetadata metadata1 = TableMetadata.newBuilder().addPartitionKey("pk").addColumn("pk", DataType.BLOB).build(); @@ -4755,7 +4756,7 @@ void createTableInternal_WithBlobColumnAsKeyOrIndex_ShouldThrowUnsupportedOperat } @Test - void createIndex_WithBlobColumnAsKeyOrIndex_ShouldThrowUnsupportedOperationException() + void createIndex_Db2_WithBlobColumnAsKeyOrIndex_ShouldThrowUnsupportedOperationException() throws SQLException { // Arrange String namespace = "my_ns"; @@ -4784,6 +4785,70 @@ void createIndex_WithBlobColumnAsKeyOrIndex_ShouldThrowUnsupportedOperationExcep .hasMessageContainingAll("BLOB", "index"); } + @Test + void + createTableInternal_Oracle_WithBlobColumnAsKeyOrIndex_ShouldThrowUnsupportedOperationException() { + // Arrange + TableMetadata metadata1 = + TableMetadata.newBuilder().addPartitionKey("pk").addColumn("pk", DataType.BLOB).build(); + TableMetadata metadata2 = + TableMetadata.newBuilder() + .addPartitionKey("pk") + .addClusteringKey("ck") + .addColumn("pk", DataType.INT) + .addColumn("ck", DataType.BLOB) + .build(); + TableMetadata metadata3 = + TableMetadata.newBuilder() + .addPartitionKey("pk") + .addColumn("pk", DataType.INT) + .addColumn("col", DataType.BLOB) + .addSecondaryIndex("col") + .build(); + JdbcAdmin admin = createJdbcAdminFor(RdbEngine.ORACLE); + + // Act Assert + assertThatThrownBy(() -> admin.createTableInternal(connection, "ns", "tbl", metadata1, false)) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContainingAll("BLOB", "key"); + assertThatThrownBy(() -> admin.createTableInternal(connection, "ns", "tbl", metadata2, false)) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContainingAll("BLOB", "key"); + assertThatThrownBy(() -> admin.createTableInternal(connection, "ns", "tbl", metadata3, false)) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContainingAll("BLOB", "index"); + } + + @Test + void createIndex_Oracle_WithBlobColumnAsKeyOrIndex_ShouldThrowUnsupportedOperationException() + throws SQLException { + // Arrange + String namespace = "my_ns"; + String table = "my_tbl"; + String indexColumn = "index_col"; + JdbcAdmin admin = createJdbcAdminFor(RdbEngine.ORACLE); + + PreparedStatement selectStatement = mock(PreparedStatement.class); + ResultSet resultSet = + mockResultSet( + new SelectAllFromMetadataTableResultSetMocker.Row( + "pk", DataType.BOOLEAN.toString(), "PARTITION", null, false), + new SelectAllFromMetadataTableResultSetMocker.Row( + indexColumn, DataType.BLOB.toString(), null, null, false)); + when(selectStatement.executeQuery()).thenReturn(resultSet); + when(connection.prepareStatement(any())).thenReturn(selectStatement); + Statement statement = mock(Statement.class); + + when(dataSource.getConnection()).thenReturn(connection); + when(connection.createStatement()).thenReturn(statement); + + // Act Assert + assertThatThrownBy( + () -> admin.createIndex(namespace, table, indexColumn, Collections.emptyMap())) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContainingAll("BLOB", "index"); + } + // 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/jdbc/JdbcOperationCheckerTest.java b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcOperationCheckerTest.java index 0a0f8cdf6e..4c28f6e9de 100644 --- a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcOperationCheckerTest.java +++ b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcOperationCheckerTest.java @@ -2,8 +2,10 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import com.scalar.db.api.Scan; import com.scalar.db.api.ScanAll; import com.scalar.db.api.TableMetadata; import com.scalar.db.common.StorageInfoProvider; @@ -22,6 +24,7 @@ public class JdbcOperationCheckerTest { @Mock private StorageInfoProvider storageInfoProvider; @Mock private RdbEngineStrategy rdbEngine; @Mock private ScanAll scanAll; + @Mock private Scan scan; @Mock private TableMetadata tableMetadata; private JdbcOperationChecker operationChecker; @@ -51,7 +54,8 @@ public void checkOrderingsForScanAll_WhenAdditionalCheckThrows_ShouldPropagateEx Exception exception = new RuntimeException(); doThrow(exception) .when(rdbEngine) - .throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported(any(), any()); + .throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( + any(ScanAll.class), any(TableMetadata.class)); // Act Assertions.assertThatThrownBy( @@ -62,4 +66,44 @@ public void checkOrderingsForScanAll_WhenAdditionalCheckThrows_ShouldPropagateEx verify(rdbEngine) .throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported(scanAll, tableMetadata); } + + @Test + public void checkConjunctions_WithScanAll_ShouldInvokeAdditionalCheckOnRdbEngine() { + // Arrange + // Act + operationChecker.checkConjunctions(scanAll, tableMetadata); + + // Assert + verify(rdbEngine) + .throwIfCrossPartitionScanConditionOnBlobColumnNotSupported(scanAll, tableMetadata); + } + + @Test + public void checkConjunctions_WithScan_ShouldNotInvokeAdditionalCheckOnRdbEngine() { + // Arrange + // Act + operationChecker.checkConjunctions(scan, tableMetadata); + + // Assert + verify(rdbEngine, never()) + .throwIfCrossPartitionScanConditionOnBlobColumnNotSupported(any(), any()); + } + + @Test + public void checkConjunctions_WithScanAll_WhenAdditionalCheckThrows_ShouldPropagateException() { + // Arrange + Exception exception = new RuntimeException(); + doThrow(exception) + .when(rdbEngine) + .throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( + any(ScanAll.class), any(TableMetadata.class)); + + // Act + Assertions.assertThatThrownBy(() -> operationChecker.checkConjunctions(scanAll, tableMetadata)) + .isEqualTo(exception); + + // Assert + verify(rdbEngine) + .throwIfCrossPartitionScanConditionOnBlobColumnNotSupported(scanAll, tableMetadata); + } } diff --git a/core/src/test/java/com/scalar/db/storage/jdbc/RdbEngineOracleTest.java b/core/src/test/java/com/scalar/db/storage/jdbc/RdbEngineOracleTest.java index 88e64cb8f9..b50dfd9d9c 100644 --- a/core/src/test/java/com/scalar/db/storage/jdbc/RdbEngineOracleTest.java +++ b/core/src/test/java/com/scalar/db/storage/jdbc/RdbEngineOracleTest.java @@ -1,13 +1,33 @@ package com.scalar.db.storage.jdbc; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import com.scalar.db.api.ConditionBuilder; +import com.scalar.db.api.Scan; import com.scalar.db.api.Scan.Ordering.Order; +import com.scalar.db.api.ScanAll; import com.scalar.db.api.TableMetadata; import com.scalar.db.io.DataType; +import java.util.Arrays; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; class RdbEngineOracleTest { + @Mock private ScanAll scanAll; + @Mock private TableMetadata metadata; + private RdbEngineOracle rdbEngineOracle; + + @BeforeEach + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this).close(); + rdbEngineOracle = new RdbEngineOracle(); + } @Test void createTableInternalSqlsAfterCreateTable_GivenSameClusteringOrders_ShouldNotCreateIndex() { @@ -57,4 +77,147 @@ void createTableInternalSqlsAfterCreateTable_GivenDifferentClusteringOrders_Shou assertThat(sqls[0]).startsWith("ALTER TABLE "); assertThat(sqls[1]).startsWith("CREATE UNIQUE INDEX "); } + + @Test + public void + throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported_WithBlobOrdering_ShouldThrowException() { + // Arrange + Scan.Ordering blobOrdering = Scan.Ordering.asc("blob_column"); + Scan.Ordering intOrdering = Scan.Ordering.desc("int_column"); + + when(scanAll.getOrderings()).thenReturn(Arrays.asList(intOrdering, blobOrdering)); + when(metadata.getColumnDataType("blob_column")).thenReturn(DataType.BLOB); + when(metadata.getColumnDataType("int_column")).thenReturn(DataType.INT); + + // Act & Assert + assertThatThrownBy( + () -> + rdbEngineOracle.throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( + scanAll, metadata)) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("blob_column"); + } + + @Test + public void + throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported_WithoutBlobOrdering_ShouldNotThrowException() { + // Arrange + Scan.Ordering intOrdering = Scan.Ordering.asc("int_column"); + Scan.Ordering textOrdering = Scan.Ordering.desc("text_column"); + + when(scanAll.getOrderings()).thenReturn(Arrays.asList(intOrdering, textOrdering)); + when(metadata.getColumnDataType("int_column")).thenReturn(DataType.INT); + when(metadata.getColumnDataType("text_column")).thenReturn(DataType.TEXT); + + // Act & Assert + assertThatCode( + () -> + rdbEngineOracle.throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( + scanAll, metadata)) + .doesNotThrowAnyException(); + } + + @Test + public void + throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported_WithNoOrderings_ShouldNotThrowException() { + // Arrange + when(scanAll.getOrderings()).thenReturn(Arrays.asList()); + + // Act & Assert + assertThatCode( + () -> + rdbEngineOracle.throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( + scanAll, metadata)) + .doesNotThrowAnyException(); + } + + @Test + public void + throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported_WithMultipleBlobOrderings_ShouldThrowForFirst() { + // Arrange + Scan.Ordering blobOrdering1 = Scan.Ordering.asc("blob_column1"); + Scan.Ordering blobOrdering2 = Scan.Ordering.desc("blob_column2"); + + when(scanAll.getOrderings()).thenReturn(Arrays.asList(blobOrdering1, blobOrdering2)); + when(metadata.getColumnDataType("blob_column1")).thenReturn(DataType.BLOB); + when(metadata.getColumnDataType("blob_column2")).thenReturn(DataType.BLOB); + + // Act & Assert + assertThatThrownBy( + () -> + rdbEngineOracle.throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( + scanAll, metadata)) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("blob_column1"); + } + + @Test + public void + throwIfCrossPartitionScanConditionOnBlobColumnNotSupported_WithoutBlobCondition_ShouldNotThrowException() { + // Arrange + TableMetadata metadata = mock(TableMetadata.class); + ScanAll scanAll = + (ScanAll) + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .all() + .where(ConditionBuilder.column("int_column").isEqualToInt(10)) + .and(ConditionBuilder.column("text_column").isEqualToText("value")) + .build(); + + when(metadata.getColumnDataType("int_column")).thenReturn(DataType.INT); + when(metadata.getColumnDataType("text_column")).thenReturn(DataType.TEXT); + + // Act & Assert + assertThatCode( + () -> + rdbEngineOracle.throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( + scanAll, metadata)) + .doesNotThrowAnyException(); + } + + @Test + public void + throwIfCrossPartitionScanConditionOnBlobColumnNotSupported_WithNoConditions_ShouldNotThrowException() { + // Arrange + TableMetadata metadata = mock(TableMetadata.class); + ScanAll scanAll = (ScanAll) Scan.newBuilder().namespace("ns").table("tbl").all().build(); + + // Act & Assert + assertThatCode( + () -> + rdbEngineOracle.throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( + scanAll, metadata)) + .doesNotThrowAnyException(); + } + + @Test + public void + throwIfCrossPartitionScanConditionOnBlobColumnNotSupported_WithMixedConditions_ShouldThrowWhenBlobPresent() { + // Arrange + TableMetadata metadata = mock(TableMetadata.class); + ScanAll scanAll = + (ScanAll) + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .all() + .where(ConditionBuilder.column("int_column").isGreaterThanInt(100)) + .and(ConditionBuilder.column("blob_column").isNotEqualToBlob(new byte[] {5, 6})) + .and(ConditionBuilder.column("text_column").isEqualToText("test")) + .build(); + + when(metadata.getColumnDataType("int_column")).thenReturn(DataType.INT); + when(metadata.getColumnDataType("blob_column")).thenReturn(DataType.BLOB); + when(metadata.getColumnDataType("text_column")).thenReturn(DataType.TEXT); + + // Act & Assert + assertThatThrownBy( + () -> + rdbEngineOracle.throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( + scanAll, metadata)) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("blob_column"); + } } diff --git a/integration-test/src/main/java/com/scalar/db/api/DistributedStorageColumnValueIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/api/DistributedStorageColumnValueIntegrationTestBase.java index 9f8ab2a11f..6dd3c5b69b 100644 --- a/integration-test/src/main/java/com/scalar/db/api/DistributedStorageColumnValueIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/api/DistributedStorageColumnValueIntegrationTestBase.java @@ -49,26 +49,26 @@ public abstract class DistributedStorageColumnValueIntegrationTestBase { private static final String TEST_NAME = "storage_col_val"; private static final String NAMESPACE = "int_test_" + TEST_NAME; - private static final String TABLE = "test_table"; - private static final String PARTITION_KEY = "pkey"; - private static final String COL_NAME1 = "c1"; - private static final String COL_NAME2 = "c2"; - private static final String COL_NAME3 = "c3"; - private static final String COL_NAME4 = "c4"; - private static final String COL_NAME5 = "c5"; - private static final String COL_NAME6 = "c6"; - private static final String COL_NAME7 = "c7"; - private static final String COL_NAME8 = "c8"; - private static final String COL_NAME9 = "c9"; - private static final String COL_NAME10 = "c10"; - private static final String COL_NAME11 = "c11"; + protected static final String TABLE = "test_table"; + protected static final String PARTITION_KEY = "pkey"; + protected static final String COL_NAME1 = "c1"; + protected static final String COL_NAME2 = "c2"; + protected static final String COL_NAME3 = "c3"; + protected static final String COL_NAME4 = "c4"; + protected static final String COL_NAME5 = "c5"; + protected static final String COL_NAME6 = "c6"; + protected static final String COL_NAME7 = "c7"; + protected static final String COL_NAME8 = "c8"; + protected static final String COL_NAME9 = "c9"; + protected static final String COL_NAME10 = "c10"; + protected static final String COL_NAME11 = "c11"; private static final int ATTEMPT_COUNT = 50; - private static final Random random = new Random(); + protected static final Random random = new Random(); - private DistributedStorageAdmin admin; - private DistributedStorage storage; - private String namespace; + protected DistributedStorageAdmin admin; + protected DistributedStorage storage; + protected String namespace; private long seed; @@ -685,7 +685,7 @@ public void put_forTimeRelatedTypesWithVariousJvmTimezone_ShouldPutCorrectly( } } - private void assertResult( + protected void assertResult( IntColumn partitionKeyValue, BooleanColumn col1Value, IntColumn col2Value, diff --git a/integration-test/src/main/java/com/scalar/db/api/DistributedStorageConditionalMutationIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/api/DistributedStorageConditionalMutationIntegrationTestBase.java index e3bddc9cc6..3a485dc176 100644 --- a/integration-test/src/main/java/com/scalar/db/api/DistributedStorageConditionalMutationIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/api/DistributedStorageConditionalMutationIntegrationTestBase.java @@ -121,13 +121,15 @@ private void createTable() throws ExecutionException { .addColumn(COL_NAME4, DataType.FLOAT) .addColumn(COL_NAME5, DataType.DOUBLE) .addColumn(COL_NAME6, DataType.TEXT) - .addColumn(COL_NAME7, DataType.BLOB) - .addColumn(COL_NAME8, DataType.DATE) - .addColumn(COL_NAME9, DataType.TIME) - .addColumn(COL_NAME10, DataType.TIMESTAMPTZ) + .addColumn(COL_NAME7, DataType.DATE) + .addColumn(COL_NAME8, DataType.TIME) + .addColumn(COL_NAME9, DataType.TIMESTAMPTZ) .addPartitionKey(PARTITION_KEY); if (isTimestampTypeSupported()) { - tableMetadata.addColumn(COL_NAME11, DataType.TIMESTAMP); + tableMetadata.addColumn(COL_NAME10, DataType.TIMESTAMP); + } + if (isConditionOnBlobColumnSupported()) { + tableMetadata.addColumn(COL_NAME11, DataType.BLOB); } Map options = getCreationOptions(); @@ -179,6 +181,9 @@ protected List getOperatorAndDataTypeListForTest() { if (!isTimestampTypeSupported()) { dataTypes.remove(DataType.TIMESTAMP); } + if (!isConditionOnBlobColumnSupported()) { + dataTypes.remove(DataType.BLOB); + } List ret = new ArrayList<>(); for (Operator operator : Operator.values()) { @@ -546,9 +551,11 @@ private void put_withPutIf_shouldPutProperly( COL_NAME6, COL_NAME7, COL_NAME8, - COL_NAME9, - COL_NAME10); + COL_NAME9); if (isTimestampTypeSupported()) { + columnNames.add(COL_NAME10); + } + if (isConditionOnBlobColumnSupported()) { columnNames.add(COL_NAME11); } assertThat(result.get().getContainedColumnNames()) @@ -589,22 +596,24 @@ private void put_withPutIf_shouldPutProperly( assertThat(result.get().getText(COL_NAME6)) .describedAs(description) .isEqualTo(expected.get(COL_NAME6).getTextValue()); - assertThat(result.get().getBlob(COL_NAME7)) + assertThat(result.get().getDate(COL_NAME7)) .describedAs(description) - .isEqualTo(expected.get(COL_NAME7).getBlobValue()); - assertThat(result.get().getDate(COL_NAME8)) + .isEqualTo(expected.get(COL_NAME7).getDateValue()); + assertThat(result.get().getTime(COL_NAME8)) .describedAs(description) - .isEqualTo(expected.get(COL_NAME8).getDateValue()); - assertThat(result.get().getTime(COL_NAME9)) + .isEqualTo(expected.get(COL_NAME8).getTimeValue()); + assertThat(result.get().getTimestampTZ(COL_NAME9)) .describedAs(description) - .isEqualTo(expected.get(COL_NAME9).getTimeValue()); - assertThat(result.get().getTimestampTZ(COL_NAME10)) - .describedAs(description) - .isEqualTo(expected.get(COL_NAME10).getTimestampTZValue()); + .isEqualTo(expected.get(COL_NAME9).getTimestampTZValue()); if (isTimestampTypeSupported()) { - assertThat(result.get().getTimestamp(COL_NAME11)) + assertThat(result.get().getTimestamp(COL_NAME10)) .describedAs(description) - .isEqualTo(expected.get(COL_NAME11).getTimestampValue()); + .isEqualTo(expected.get(COL_NAME10).getTimestampValue()); + } + if (isConditionOnBlobColumnSupported()) { + assertThat(result.get().getBlob(COL_NAME11)) + .describedAs(description) + .isEqualTo(expected.get(COL_NAME11).getBlobValue()); } } @@ -637,9 +646,11 @@ public void put_withPutIfExistsWhenRecordExists_shouldPutProperly() throws Execu COL_NAME6, COL_NAME7, COL_NAME8, - COL_NAME9, - COL_NAME10); + COL_NAME9); if (isTimestampTypeSupported()) { + columnNames.add(COL_NAME10); + } + if (isConditionOnBlobColumnSupported()) { columnNames.add(COL_NAME11); } assertThat(result.get().getContainedColumnNames()).isEqualTo(columnNames); @@ -649,14 +660,16 @@ public void put_withPutIfExistsWhenRecordExists_shouldPutProperly() throws Execu assertThat(result.get().getFloat(COL_NAME4)).isEqualTo(put.getFloatValue(COL_NAME4)); assertThat(result.get().getDouble(COL_NAME5)).isEqualTo(put.getDoubleValue(COL_NAME5)); assertThat(result.get().getText(COL_NAME6)).isEqualTo(put.getTextValue(COL_NAME6)); - assertThat(result.get().getBlob(COL_NAME7)).isEqualTo(put.getBlobValue(COL_NAME7)); - assertThat(result.get().getDate(COL_NAME8)).isEqualTo(put.getDateValue(COL_NAME8)); - assertThat(result.get().getTime(COL_NAME9)).isEqualTo(put.getTimeValue(COL_NAME9)); - assertThat(result.get().getTimestampTZ(COL_NAME10)) - .isEqualTo(put.getTimestampTZValue(COL_NAME10)); + assertThat(result.get().getDate(COL_NAME7)).isEqualTo(put.getDateValue(COL_NAME7)); + assertThat(result.get().getTime(COL_NAME8)).isEqualTo(put.getTimeValue(COL_NAME8)); + assertThat(result.get().getTimestampTZ(COL_NAME9)) + .isEqualTo(put.getTimestampTZValue(COL_NAME9)); if (isTimestampTypeSupported()) { - assertThat(result.get().getTimestamp(COL_NAME11)) - .isEqualTo(put.getTimestampValue(COL_NAME11)); + assertThat(result.get().getTimestamp(COL_NAME10)) + .isEqualTo(put.getTimestampValue(COL_NAME10)); + } + if (isConditionOnBlobColumnSupported()) { + assertThat(result.get().getBlob(COL_NAME11)).isEqualTo(put.getBlobValue(COL_NAME11)); } } @@ -706,9 +719,11 @@ public void put_withPutIfNotExistsWhenRecordDoesNotExist_shouldPutProperly() COL_NAME6, COL_NAME7, COL_NAME8, - COL_NAME9, - COL_NAME10); + COL_NAME9); if (isTimestampTypeSupported()) { + columnNames.add(COL_NAME10); + } + if (isConditionOnBlobColumnSupported()) { columnNames.add(COL_NAME11); } assertThat(result.get().getContainedColumnNames()).isEqualTo(columnNames); @@ -718,14 +733,16 @@ public void put_withPutIfNotExistsWhenRecordDoesNotExist_shouldPutProperly() assertThat(result.get().getFloat(COL_NAME4)).isEqualTo(put.getFloatValue(COL_NAME4)); assertThat(result.get().getDouble(COL_NAME5)).isEqualTo(put.getDoubleValue(COL_NAME5)); assertThat(result.get().getText(COL_NAME6)).isEqualTo(put.getTextValue(COL_NAME6)); - assertThat(result.get().getBlob(COL_NAME7)).isEqualTo(put.getBlobValue(COL_NAME7)); - assertThat(result.get().getDate(COL_NAME8)).isEqualTo(put.getDateValue(COL_NAME8)); - assertThat(result.get().getTime(COL_NAME9)).isEqualTo(put.getTimeValue(COL_NAME9)); - assertThat(result.get().getTimestampTZ(COL_NAME10)) - .isEqualTo(put.getTimestampTZValue(COL_NAME10)); + assertThat(result.get().getDate(COL_NAME7)).isEqualTo(put.getDateValue(COL_NAME7)); + assertThat(result.get().getTime(COL_NAME8)).isEqualTo(put.getTimeValue(COL_NAME8)); + assertThat(result.get().getTimestampTZ(COL_NAME9)) + .isEqualTo(put.getTimestampTZValue(COL_NAME9)); if (isTimestampTypeSupported()) { - assertThat(result.get().getTimestamp(COL_NAME11)) - .isEqualTo(put.getTimestampValue(COL_NAME11)); + assertThat(result.get().getTimestamp(COL_NAME10)) + .isEqualTo(put.getTimestampValue(COL_NAME10)); + } + if (isConditionOnBlobColumnSupported()) { + assertThat(result.get().getBlob(COL_NAME11)).isEqualTo(put.getBlobValue(COL_NAME11)); } } @@ -758,9 +775,11 @@ public void put_withPutIfNotExistsWhenRecordExists_shouldThrowNoMutationExceptio COL_NAME6, COL_NAME7, COL_NAME8, - COL_NAME9, - COL_NAME10); + COL_NAME9); if (isTimestampTypeSupported()) { + columnNames.add(COL_NAME10); + } + if (isConditionOnBlobColumnSupported()) { columnNames.add(COL_NAME11); } assertThat(result.get().getContainedColumnNames()).isEqualTo(columnNames); @@ -775,17 +794,19 @@ public void put_withPutIfNotExistsWhenRecordExists_shouldThrowNoMutationExceptio .isEqualTo(initialData.get(COL_NAME5).getDoubleValue()); assertThat(result.get().getText(COL_NAME6)) .isEqualTo(initialData.get(COL_NAME6).getTextValue()); - assertThat(result.get().getBlob(COL_NAME7)) - .isEqualTo(initialData.get(COL_NAME7).getBlobValue()); - assertThat(result.get().getDate(COL_NAME8)) - .isEqualTo(initialData.get(COL_NAME8).getDateValue()); - assertThat(result.get().getTime(COL_NAME9)) - .isEqualTo(initialData.get(COL_NAME9).getTimeValue()); - assertThat(result.get().getTimestampTZ(COL_NAME10)) - .isEqualTo(initialData.get(COL_NAME10).getTimestampTZValue()); + assertThat(result.get().getDate(COL_NAME7)) + .isEqualTo(initialData.get(COL_NAME7).getDateValue()); + assertThat(result.get().getTime(COL_NAME8)) + .isEqualTo(initialData.get(COL_NAME8).getTimeValue()); + assertThat(result.get().getTimestampTZ(COL_NAME9)) + .isEqualTo(initialData.get(COL_NAME9).getTimestampTZValue()); if (isTimestampTypeSupported()) { - assertThat(result.get().getTimestamp(COL_NAME11)) - .isEqualTo(initialData.get(COL_NAME11).getTimestampValue()); + assertThat(result.get().getTimestamp(COL_NAME10)) + .isEqualTo(initialData.get(COL_NAME10).getTimestampValue()); + } + if (isConditionOnBlobColumnSupported()) { + assertThat(result.get().getBlob(COL_NAME11)) + .isEqualTo(initialData.get(COL_NAME11).getBlobValue()); } } @@ -1147,9 +1168,11 @@ private void delete_withDeleteIf_shouldPutProperly( COL_NAME6, COL_NAME7, COL_NAME8, - COL_NAME9, - COL_NAME10); + COL_NAME9); if (isTimestampTypeSupported()) { + columnNames.add(COL_NAME10); + } + if (isConditionOnBlobColumnSupported()) { columnNames.add(COL_NAME11); } assertThat(result.get().getContainedColumnNames()) @@ -1189,22 +1212,24 @@ private void delete_withDeleteIf_shouldPutProperly( assertThat(result.get().getText(COL_NAME6)) .describedAs(description) .isEqualTo(initialData.get(COL_NAME6).getTextValue()); - assertThat(result.get().getBlob(COL_NAME7)) + assertThat(result.get().getDate(COL_NAME7)) .describedAs(description) - .isEqualTo(initialData.get(COL_NAME7).getBlobValue()); - assertThat(result.get().getDate(COL_NAME8)) + .isEqualTo(initialData.get(COL_NAME7).getDateValue()); + assertThat(result.get().getTime(COL_NAME8)) .describedAs(description) - .isEqualTo(initialData.get(COL_NAME8).getDateValue()); - assertThat(result.get().getTime(COL_NAME9)) + .isEqualTo(initialData.get(COL_NAME8).getTimeValue()); + assertThat(result.get().getTimestampTZ(COL_NAME9)) .describedAs(description) - .isEqualTo(initialData.get(COL_NAME9).getTimeValue()); - assertThat(result.get().getTimestampTZ(COL_NAME10)) - .describedAs(description) - .isEqualTo(initialData.get(COL_NAME10).getTimestampTZValue()); + .isEqualTo(initialData.get(COL_NAME9).getTimestampTZValue()); if (isTimestampTypeSupported()) { - assertThat(result.get().getTimestamp(COL_NAME11)) + assertThat(result.get().getTimestamp(COL_NAME10)) + .describedAs(description) + .isEqualTo(initialData.get(COL_NAME10).getTimestampValue()); + } + if (isConditionOnBlobColumnSupported()) { + assertThat(result.get().getBlob(COL_NAME11)) .describedAs(description) - .isEqualTo(initialData.get(COL_NAME11).getTimestampValue()); + .isEqualTo(initialData.get(COL_NAME11).getBlobValue()); } } } @@ -1296,15 +1321,16 @@ private Put preparePutWithRandomValues( .value(getColumnWithRandomValue(random.get(), COL_NAME4, DataType.FLOAT)) .value(getColumnWithRandomValue(random.get(), COL_NAME5, DataType.DOUBLE)) .value(getColumnWithRandomValue(random.get(), COL_NAME6, DataType.TEXT)) - .value(getColumnWithRandomValue(random.get(), COL_NAME7, DataType.BLOB)) - .value(getColumnWithRandomValue(random.get(), COL_NAME8, DataType.DATE)) - .value(getColumnWithRandomValue(random.get(), COL_NAME9, DataType.TIME)) - .value(getColumnWithRandomValue(random.get(), COL_NAME10, DataType.TIMESTAMPTZ)) + .value(getColumnWithRandomValue(random.get(), COL_NAME7, DataType.DATE)) + .value(getColumnWithRandomValue(random.get(), COL_NAME8, DataType.TIME)) + .value(getColumnWithRandomValue(random.get(), COL_NAME9, DataType.TIMESTAMPTZ)) .consistency(Consistency.LINEARIZABLE); if (isTimestampTypeSupported()) { - put.value(getColumnWithRandomValue(random.get(), COL_NAME11, DataType.TIMESTAMP)); + put.value(getColumnWithRandomValue(random.get(), COL_NAME10, DataType.TIMESTAMP)); + } + if (isConditionOnBlobColumnSupported()) { + put.value(getColumnWithRandomValue(random.get(), COL_NAME11, DataType.BLOB)); } - return put.build(); } @@ -1418,14 +1444,16 @@ private Put preparePutWithNullValues( .floatValue(COL_NAME4, null) .doubleValue(COL_NAME5, null) .textValue(COL_NAME6, null) - .blobValue(COL_NAME7, (ByteBuffer) null) - .dateValue(COL_NAME8, null) - .timeValue(COL_NAME9, null) - .timestampTZValue(COL_NAME10, null) + .dateValue(COL_NAME7, null) + .timeValue(COL_NAME8, null) + .timestampTZValue(COL_NAME9, null) .consistency(Consistency.LINEARIZABLE); if (isTimestampTypeSupported()) { - put.timestampValue(COL_NAME11, null); + put.timestampValue(COL_NAME10, null); + } + if (isConditionOnBlobColumnSupported()) { + put.blobValue(COL_NAME11, (ByteBuffer) null); } return put.build(); @@ -1473,12 +1501,14 @@ private Map> putInitialDataWithoutValues( .put(COL_NAME4, FloatColumn.ofNull(COL_NAME4)) .put(COL_NAME5, DoubleColumn.ofNull(COL_NAME5)) .put(COL_NAME6, TextColumn.ofNull(COL_NAME6)) - .put(COL_NAME7, BlobColumn.ofNull(COL_NAME7)) - .put(COL_NAME8, DateColumn.ofNull(COL_NAME8)) - .put(COL_NAME9, TimeColumn.ofNull(COL_NAME9)) - .put(COL_NAME10, TimestampTZColumn.ofNull(COL_NAME10)); + .put(COL_NAME7, DateColumn.ofNull(COL_NAME7)) + .put(COL_NAME8, TimeColumn.ofNull(COL_NAME8)) + .put(COL_NAME9, TimestampTZColumn.ofNull(COL_NAME9)); if (isTimestampTypeSupported()) { - columns.put(COL_NAME11, TimestampColumn.ofNull(COL_NAME11)); + columns.put(COL_NAME10, TimestampColumn.ofNull(COL_NAME10)); + } + if (isConditionOnBlobColumnSupported()) { + columns.put(COL_NAME11, BlobColumn.ofNull(COL_NAME11)); } return columns.build(); @@ -1526,15 +1556,15 @@ private String getColumnName(DataType dataType) { return COL_NAME5; case TEXT: return COL_NAME6; - case BLOB: - return COL_NAME7; case DATE: - return COL_NAME8; + return COL_NAME7; case TIME: - return COL_NAME9; + return COL_NAME8; case TIMESTAMPTZ: - return COL_NAME10; + return COL_NAME9; case TIMESTAMP: + return COL_NAME10; + case BLOB: return COL_NAME11; default: throw new AssertionError(); @@ -1728,4 +1758,8 @@ public DataType getDataType() { protected boolean isTimestampTypeSupported() { return true; } + + protected boolean isConditionOnBlobColumnSupported() { + return true; + } } diff --git a/integration-test/src/main/java/com/scalar/db/api/DistributedStorageCrossPartitionScanIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/api/DistributedStorageCrossPartitionScanIntegrationTestBase.java index ab9787512c..d331877127 100644 --- a/integration-test/src/main/java/com/scalar/db/api/DistributedStorageCrossPartitionScanIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/api/DistributedStorageCrossPartitionScanIntegrationTestBase.java @@ -155,14 +155,14 @@ private void createTableForConditionTests() throws ExecutionException { .addColumn(COL_NAME4, DataType.DOUBLE) .addColumn(COL_NAME5, DataType.TEXT) .addColumn(COL_NAME6, DataType.BOOLEAN) - .addColumn(COL_NAME7, DataType.BLOB) - .addColumn(COL_NAME8, DataType.DATE) - .addColumn(COL_NAME9, DataType.TIME) - .addColumn(COL_NAME10, DataType.TIMESTAMPTZ) + .addColumn(COL_NAME7, DataType.DATE) + .addColumn(COL_NAME8, DataType.TIME) + .addColumn(COL_NAME9, DataType.TIMESTAMPTZ) .addPartitionKey(PARTITION_KEY_NAME); if (isTimestampTypeSupported()) { - tableMetadata.addColumn(COL_NAME11, DataType.TIMESTAMP); + tableMetadata.addColumn(COL_NAME10, DataType.TIMESTAMP); } + tableMetadata.addColumn(COL_NAME11, DataType.BLOB); Map options = getCreationOptions(); admin.createNamespace(getNamespaceName(), true, options); @@ -375,12 +375,14 @@ protected List> prepareNonKeyColumns(int i) { columns.add(DoubleColumn.of(COL_NAME4, i)); columns.add(TextColumn.of(COL_NAME5, String.valueOf(i))); columns.add(BooleanColumn.of(COL_NAME6, i % 2 == 0)); - columns.add(BlobColumn.of(COL_NAME7, String.valueOf(i).getBytes(StandardCharsets.UTF_8))); - columns.add(DateColumn.of(COL_NAME8, DateColumn.MIN_VALUE.plusDays(i))); - columns.add(TimeColumn.of(COL_NAME9, TimeColumn.MIN_VALUE.plusSeconds(i))); - columns.add(TimestampTZColumn.of(COL_NAME10, TimestampTZColumn.MIN_VALUE.plusSeconds(i))); + columns.add(DateColumn.of(COL_NAME7, DateColumn.MIN_VALUE.plusDays(i))); + columns.add(TimeColumn.of(COL_NAME8, TimeColumn.MIN_VALUE.plusSeconds(i))); + columns.add(TimestampTZColumn.of(COL_NAME9, TimestampTZColumn.MIN_VALUE.plusSeconds(i))); if (isTimestampTypeSupported()) { - columns.add(TimestampColumn.of(COL_NAME11, TimestampColumn.MIN_VALUE.plusSeconds(i))); + columns.add(TimestampColumn.of(COL_NAME10, TimestampColumn.MIN_VALUE.plusSeconds(i))); + } + if (isConditionOnBlobColumnSupported()) { + columns.add(BlobColumn.of(COL_NAME11, String.valueOf(i).getBytes(StandardCharsets.UTF_8))); } return columns; } @@ -393,12 +395,14 @@ private List> prepareNullColumns() { columns.add(DoubleColumn.ofNull(COL_NAME4)); columns.add(TextColumn.ofNull(COL_NAME5)); columns.add(BooleanColumn.ofNull(COL_NAME6)); - columns.add(BlobColumn.ofNull(COL_NAME7)); - columns.add(DateColumn.ofNull(COL_NAME8)); - columns.add(TimeColumn.ofNull(COL_NAME9)); - columns.add(TimestampTZColumn.ofNull(COL_NAME10)); + columns.add(DateColumn.ofNull(COL_NAME7)); + columns.add(TimeColumn.ofNull(COL_NAME8)); + columns.add(TimestampTZColumn.ofNull(COL_NAME9)); if (isTimestampTypeSupported()) { - columns.add(TimestampColumn.ofNull(COL_NAME11)); + columns.add(TimestampColumn.ofNull(COL_NAME10)); + } + if (isConditionOnBlobColumnSupported()) { + columns.add(BlobColumn.ofNull(COL_NAME11)); } return columns; } @@ -1262,4 +1266,8 @@ protected boolean isTimestampTypeSupported() { protected boolean isOrderingOnBlobColumnSupported() { return true; } + + protected boolean isConditionOnBlobColumnSupported() { + return true; + } } From 7ac621c0ce2b0e63ffd6d9b919077196b40eb90d Mon Sep 17 00:00:00 2001 From: Vincent Guilpain Date: Mon, 20 Oct 2025 09:27:41 +0900 Subject: [PATCH 2/7] Update comment [skip ci] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java index cae40f1189..ce5c3e055b 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java @@ -146,7 +146,7 @@ Stream provideBlobSizes() { if (isOracle()) { // As explained in // `com.scalar.db.storage.jdbc.RdbEngineOracle.bindBlobColumnToPreparedStatement()`, - // handing a BLOB size bigger than 32,767 bytes requires a workaround so we particularly test + // handing a BLOB size bigger than 32,766 bytes requires a workaround so we particularly test // values around it. args.add(Arguments.of(32_766, "32.766 KB")); args.add(Arguments.of(32_767, "32.767 KB")); From 538eb223b1d9e8a533eb777af8348069e0ca8a56 Mon Sep 17 00:00:00 2001 From: Vincent Guilpain Date: Mon, 20 Oct 2025 09:30:21 +0900 Subject: [PATCH 3/7] Apply suggestion from @Copilot [skip ci] Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../jdbc/JdbcDatabaseColumnValueIntegrationTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java index ce5c3e055b..9a803763df 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/JdbcDatabaseColumnValueIntegrationTest.java @@ -97,7 +97,12 @@ protected Column getColumnWithMaxValue(String columnName, DataType dataType) } return super.getColumnWithMaxValue(columnName, dataType); } - // TODO Test this for all storages + // TODO: Expand this test to cover all supported storages, not just Oracle/DB2. + // This test verifies that large BLOB data can be inserted and retrieved correctly. + // Currently, it is limited to Oracle and DB2 due to known differences in BLOB handling, + // potential resource constraints, or lack of support for large BLOBs in other engines. + // Before enabling for other storages, investigate their BLOB size limits and behavior, + // and ensure the test does not cause failures or excessive resource usage. @EnabledIf("isDb2OrOracle") @ParameterizedTest() @MethodSource("provideBlobSizes") From f293d5ce14742f91df42b3fe758d2380e7859374 Mon Sep 17 00:00:00 2001 From: Vincent Guilpain Date: Mon, 20 Oct 2025 09:30:41 +0900 Subject: [PATCH 4/7] Apply suggestion from @Copilot [skip ci] Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../main/java/com/scalar/db/storage/jdbc/RdbEngineOracle.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 86192353d2..479e4b9aa4 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 @@ -511,7 +511,7 @@ public void bindBlobColumnToPreparedStatement( // workaround. This has been confirmed to be a limitation by AWS support. // Below is a detailed explanation of the workaround. // - // Depending on the byte array size, the JDBC driver automatically choose one the following mode + // Depending on the byte array size, the JDBC driver automatically chooses one the following mode // to transfer the BLOB data to the server: // - DIRECT: the most efficient mode. It's used when the byte array length is less than 32767. // - STREAM: this mode is less efficient. It's used when the byte array length is greater than From 23db59153559dc8da03d0450494a65f7e1d2259d Mon Sep 17 00:00:00 2001 From: Vincent Guilpain Date: Mon, 20 Oct 2025 09:30:51 +0900 Subject: [PATCH 5/7] Apply suggestion from @Copilot [skip ci] Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../main/java/com/scalar/db/storage/jdbc/RdbEngineOracle.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 479e4b9aa4..cc5f7bf257 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 @@ -521,7 +521,7 @@ public void bindBlobColumnToPreparedStatement( // // When the driver selects the STREAM mode, the error // ORA-03137 occurs. So, we work around the issue by making sure to use the driver in a way so - // that it should never selects the STREAM mode. + // that it should never select the STREAM mode. // For more details about the modes, see the following documentation: // https://docs.oracle.com/en/database/oracle/oracle-database/23/jjdbc/LOBs-and-BFiles.html#GUID-8FD40D53-8D64-4187-9F6F-FF78242188AD if (bytes.length <= 32766) { From 96c2073e861779035165c1f68358d44595a5858e Mon Sep 17 00:00:00 2001 From: Vincent Guilpain Date: Tue, 28 Oct 2025 17:38:34 +0900 Subject: [PATCH 6/7] Check condition on BLOB for selection operation --- .../java/com/scalar/db/common/CoreError.java | 4 +- .../db/storage/jdbc/JdbcOperationChecker.java | 5 +- .../db/storage/jdbc/RdbEngineOracle.java | 12 +++-- .../db/storage/jdbc/RdbEngineStrategy.java | 16 ++++--- .../jdbc/JdbcOperationCheckerTest.java | 44 ++++++++--------- .../db/storage/jdbc/RdbEngineOracleTest.java | 48 ++++++++----------- 6 files changed, 60 insertions(+), 69 deletions(-) diff --git a/core/src/main/java/com/scalar/db/common/CoreError.java b/core/src/main/java/com/scalar/db/common/CoreError.java index de3224fb57..e0a5445d31 100644 --- a/core/src/main/java/com/scalar/db/common/CoreError.java +++ b/core/src/main/java/com/scalar/db/common/CoreError.java @@ -820,10 +820,10 @@ public enum CoreError implements ScalarDbError { "With Oracle, setting an ordering on a BLOB column when using a cross partition scan operation is not supported. Ordering: %s", "", ""), - JDBC_ORACLE_CROSS_PARTITION_SCAN_CONDITION_ON_BLOB_COLUMN_NOT_SUPPORTED( + JDBC_ORACLE_SELECTION_CONDITION_ON_BLOB_COLUMN_NOT_SUPPORTED( Category.USER_ERROR, "0243", - "With Oracle, setting a condition on a BLOB column when using a cross partition scan operation is not supported. Condition: %s", + "With Oracle, setting a condition on a BLOB column when using a selection operation is not supported. Condition: %s", "", ""), diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcOperationChecker.java b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcOperationChecker.java index 7644a8546d..5869412b73 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcOperationChecker.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcOperationChecker.java @@ -31,9 +31,6 @@ protected void checkOrderingsForScanAll(ScanAll scanAll, TableMetadata metadata) @Override protected void checkConjunctions(Selection selection, TableMetadata metadata) { super.checkConjunctions(selection, metadata); - if (selection instanceof ScanAll) { - rdbEngine.throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( - (ScanAll) selection, metadata); - } + rdbEngine.throwIfConjunctionsOnBlobColumnNotSupported(selection.getConjunctions(), metadata); } } 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 cc5f7bf257..ef750d89cc 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 @@ -7,6 +7,7 @@ import com.scalar.db.api.LikeExpression; import com.scalar.db.api.Scan; import com.scalar.db.api.ScanAll; +import com.scalar.db.api.Selection.Conjunction; import com.scalar.db.api.TableMetadata; import com.scalar.db.common.CoreError; import com.scalar.db.exception.storage.ExecutionException; @@ -27,6 +28,7 @@ import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -472,10 +474,10 @@ public void throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( } @Override - public void throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( - ScanAll scanAll, TableMetadata metadata) { + public void throwIfConjunctionsOnBlobColumnNotSupported( + Set conjunctions, TableMetadata metadata) { Optional conditionalExpression = - scanAll.getConjunctions().stream() + conjunctions.stream() .flatMap(conjunction -> conjunction.getConditions().stream()) .filter( condition -> @@ -483,8 +485,8 @@ public void throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( .findFirst(); if (conditionalExpression.isPresent()) { throw new UnsupportedOperationException( - CoreError.JDBC_ORACLE_CROSS_PARTITION_SCAN_CONDITION_ON_BLOB_COLUMN_NOT_SUPPORTED - .buildMessage(conditionalExpression.get())); + CoreError.JDBC_ORACLE_SELECTION_CONDITION_ON_BLOB_COLUMN_NOT_SUPPORTED.buildMessage( + conditionalExpression.get())); } } 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 431900e15d..e7bb1a9cb7 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 @@ -2,6 +2,7 @@ import com.scalar.db.api.LikeExpression; import com.scalar.db.api.ScanAll; +import com.scalar.db.api.Selection.Conjunction; import com.scalar.db.api.TableMetadata; import com.scalar.db.exception.storage.ExecutionException; import com.scalar.db.io.DataType; @@ -26,6 +27,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -313,14 +315,14 @@ default void throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( ScanAll scanAll, TableMetadata metadata) {} /** - * Throws an exception if a cross-partition scan operation with a condition on a blob column is - * specified and is not supported in the underlying storage. + * Throws an exception if one of the conjunction targets a blob column and is not supported in the + * underlying storage. * - * @param scanAll the ScanAll operation + * @param conjunctions a set of conjunction * @param metadata the table metadata - * @throws UnsupportedOperationException if the ScanAll operation contains a condition on a blob - * column, and it is not supported in the underlying storage + * @throws UnsupportedOperationException if one of the conjunction targets a blob column, and it + * is not supported in the underlying storage */ - default void throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( - ScanAll scanAll, TableMetadata metadata) {} + default void throwIfConjunctionsOnBlobColumnNotSupported( + Set conjunctions, TableMetadata metadata) {} } diff --git a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcOperationCheckerTest.java b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcOperationCheckerTest.java index 4c28f6e9de..f9afee0551 100644 --- a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcOperationCheckerTest.java +++ b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcOperationCheckerTest.java @@ -2,15 +2,21 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import com.google.common.collect.Sets; import com.scalar.db.api.Scan; import com.scalar.db.api.ScanAll; +import com.scalar.db.api.Selection; +import com.scalar.db.api.Selection.Conjunction; import com.scalar.db.api.TableMetadata; import com.scalar.db.common.StorageInfoProvider; import com.scalar.db.common.TableMetadataManager; import com.scalar.db.config.DatabaseConfig; +import java.util.Collections; +import java.util.Set; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -25,6 +31,7 @@ public class JdbcOperationCheckerTest { @Mock private RdbEngineStrategy rdbEngine; @Mock private ScanAll scanAll; @Mock private Scan scan; + @Mock private Selection selection; @Mock private TableMetadata tableMetadata; private JdbcOperationChecker operationChecker; @@ -68,42 +75,35 @@ public void checkOrderingsForScanAll_WhenAdditionalCheckThrows_ShouldPropagateEx } @Test - public void checkConjunctions_WithScanAll_ShouldInvokeAdditionalCheckOnRdbEngine() { + public void checkConjunctions_ShouldInvokeAdditionalCheckOnRdbEngine() { // Arrange // Act - operationChecker.checkConjunctions(scanAll, tableMetadata); + Conjunction conjunction1 = mock(Conjunction.class); + Conjunction conjunction2 = mock(Conjunction.class); + Set conjunctions = Sets.newHashSet(conjunction1, conjunction2); + when(selection.getConjunctions()).thenReturn(conjunctions); + operationChecker.checkConjunctions(selection, tableMetadata); // Assert - verify(rdbEngine) - .throwIfCrossPartitionScanConditionOnBlobColumnNotSupported(scanAll, tableMetadata); + verify(rdbEngine).throwIfConjunctionsOnBlobColumnNotSupported(conjunctions, tableMetadata); } @Test - public void checkConjunctions_WithScan_ShouldNotInvokeAdditionalCheckOnRdbEngine() { - // Arrange - // Act - operationChecker.checkConjunctions(scan, tableMetadata); - - // Assert - verify(rdbEngine, never()) - .throwIfCrossPartitionScanConditionOnBlobColumnNotSupported(any(), any()); - } - - @Test - public void checkConjunctions_WithScanAll_WhenAdditionalCheckThrows_ShouldPropagateException() { + public void checkConjunctions_WhenAdditionalCheckThrows_ShouldPropagateException() { // Arrange Exception exception = new RuntimeException(); doThrow(exception) .when(rdbEngine) - .throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( - any(ScanAll.class), any(TableMetadata.class)); + .throwIfConjunctionsOnBlobColumnNotSupported(any(), any(TableMetadata.class)); + Set conjunctions = Collections.emptySet(); + when(selection.getConjunctions()).thenReturn(conjunctions); // Act - Assertions.assertThatThrownBy(() -> operationChecker.checkConjunctions(scanAll, tableMetadata)) + Assertions.assertThatThrownBy( + () -> operationChecker.checkConjunctions(selection, tableMetadata)) .isEqualTo(exception); // Assert - verify(rdbEngine) - .throwIfCrossPartitionScanConditionOnBlobColumnNotSupported(scanAll, tableMetadata); + verify(rdbEngine).throwIfConjunctionsOnBlobColumnNotSupported(conjunctions, tableMetadata); } } diff --git a/core/src/test/java/com/scalar/db/storage/jdbc/RdbEngineOracleTest.java b/core/src/test/java/com/scalar/db/storage/jdbc/RdbEngineOracleTest.java index b50dfd9d9c..1cc7f9228a 100644 --- a/core/src/test/java/com/scalar/db/storage/jdbc/RdbEngineOracleTest.java +++ b/core/src/test/java/com/scalar/db/storage/jdbc/RdbEngineOracleTest.java @@ -10,6 +10,7 @@ import com.scalar.db.api.Scan; import com.scalar.db.api.Scan.Ordering.Order; import com.scalar.db.api.ScanAll; +import com.scalar.db.api.Selection.Conjunction; import com.scalar.db.api.TableMetadata; import com.scalar.db.io.DataType; import java.util.Arrays; @@ -153,18 +154,14 @@ void createTableInternalSqlsAfterCreateTable_GivenDifferentClusteringOrders_Shou @Test public void - throwIfCrossPartitionScanConditionOnBlobColumnNotSupported_WithoutBlobCondition_ShouldNotThrowException() { + throwIfConjunctionsOnBlobColumnNotSupported_WithoutBlobCondition_ShouldNotThrowException() { // Arrange TableMetadata metadata = mock(TableMetadata.class); - ScanAll scanAll = - (ScanAll) - Scan.newBuilder() - .namespace("ns") - .table("tbl") - .all() - .where(ConditionBuilder.column("int_column").isEqualToInt(10)) - .and(ConditionBuilder.column("text_column").isEqualToText("value")) - .build(); + java.util.Set conjunctions = + java.util.Collections.singleton( + Conjunction.of( + ConditionBuilder.column("int_column").isEqualToInt(10), + ConditionBuilder.column("text_column").isEqualToText("value"))); when(metadata.getColumnDataType("int_column")).thenReturn(DataType.INT); when(metadata.getColumnDataType("text_column")).thenReturn(DataType.TEXT); @@ -172,41 +169,35 @@ void createTableInternalSqlsAfterCreateTable_GivenDifferentClusteringOrders_Shou // Act & Assert assertThatCode( () -> - rdbEngineOracle.throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( - scanAll, metadata)) + rdbEngineOracle.throwIfConjunctionsOnBlobColumnNotSupported(conjunctions, metadata)) .doesNotThrowAnyException(); } @Test public void - throwIfCrossPartitionScanConditionOnBlobColumnNotSupported_WithNoConditions_ShouldNotThrowException() { + throwIfConjunctionsOnBlobColumnNotSupported_WithNoConditions_ShouldNotThrowException() { // Arrange TableMetadata metadata = mock(TableMetadata.class); - ScanAll scanAll = (ScanAll) Scan.newBuilder().namespace("ns").table("tbl").all().build(); // Act & Assert assertThatCode( () -> - rdbEngineOracle.throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( - scanAll, metadata)) + rdbEngineOracle.throwIfConjunctionsOnBlobColumnNotSupported( + java.util.Collections.emptySet(), metadata)) .doesNotThrowAnyException(); } @Test public void - throwIfCrossPartitionScanConditionOnBlobColumnNotSupported_WithMixedConditions_ShouldThrowWhenBlobPresent() { + throwIfConjunctionsOnBlobColumnNotSupported_WithMixedConditions_ShouldThrowWhenBlobPresent() { // Arrange TableMetadata metadata = mock(TableMetadata.class); - ScanAll scanAll = - (ScanAll) - Scan.newBuilder() - .namespace("ns") - .table("tbl") - .all() - .where(ConditionBuilder.column("int_column").isGreaterThanInt(100)) - .and(ConditionBuilder.column("blob_column").isNotEqualToBlob(new byte[] {5, 6})) - .and(ConditionBuilder.column("text_column").isEqualToText("test")) - .build(); + java.util.Set conjunctions = + java.util.Collections.singleton( + Conjunction.of( + ConditionBuilder.column("int_column").isGreaterThanInt(100), + ConditionBuilder.column("blob_column").isNotEqualToBlob(new byte[] {5, 6}), + ConditionBuilder.column("text_column").isEqualToText("test"))); when(metadata.getColumnDataType("int_column")).thenReturn(DataType.INT); when(metadata.getColumnDataType("blob_column")).thenReturn(DataType.BLOB); @@ -215,8 +206,7 @@ void createTableInternalSqlsAfterCreateTable_GivenDifferentClusteringOrders_Shou // Act & Assert assertThatThrownBy( () -> - rdbEngineOracle.throwIfCrossPartitionScanConditionOnBlobColumnNotSupported( - scanAll, metadata)) + rdbEngineOracle.throwIfConjunctionsOnBlobColumnNotSupported(conjunctions, metadata)) .isInstanceOf(UnsupportedOperationException.class) .hasMessageContaining("blob_column"); } From 7293681919a8f0da38bc619c97ac19f75154f86d Mon Sep 17 00:00:00 2001 From: Vincent Guilpain Date: Wed, 29 Oct 2025 14:47:44 +0900 Subject: [PATCH 7/7] Rename conjuctions check method --- .../db/storage/jdbc/JdbcOperationChecker.java | 2 +- .../scalar/db/storage/jdbc/RdbEngineOracle.java | 2 +- .../db/storage/jdbc/RdbEngineStrategy.java | 10 +++++----- .../storage/jdbc/JdbcOperationCheckerTest.java | 6 +++--- .../db/storage/jdbc/RdbEngineOracleTest.java | 16 ++++++---------- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcOperationChecker.java b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcOperationChecker.java index 5869412b73..4db66d6107 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcOperationChecker.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcOperationChecker.java @@ -31,6 +31,6 @@ protected void checkOrderingsForScanAll(ScanAll scanAll, TableMetadata metadata) @Override protected void checkConjunctions(Selection selection, TableMetadata metadata) { super.checkConjunctions(selection, metadata); - rdbEngine.throwIfConjunctionsOnBlobColumnNotSupported(selection.getConjunctions(), metadata); + rdbEngine.throwIfConjunctionsColumnNotSupported(selection.getConjunctions(), metadata); } } 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 630c830a88..da6d1f973d 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 @@ -474,7 +474,7 @@ public void throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( } @Override - public void throwIfConjunctionsOnBlobColumnNotSupported( + public void throwIfConjunctionsColumnNotSupported( Set conjunctions, TableMetadata metadata) { Optional conditionalExpression = conjunctions.stream() 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 e7bb1a9cb7..48de3ff4d9 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 @@ -315,14 +315,14 @@ default void throwIfCrossPartitionScanOrderingOnBlobColumnNotSupported( ScanAll scanAll, TableMetadata metadata) {} /** - * Throws an exception if one of the conjunction targets a blob column and is not supported in the - * underlying storage. + * Throws an exception if one of the conjunctions column is not supported in the underlying + * storage. * * @param conjunctions a set of conjunction * @param metadata the table metadata - * @throws UnsupportedOperationException if one of the conjunction targets a blob column, and it - * is not supported in the underlying storage + * @throws UnsupportedOperationException if one of the conjunctions column is not supported in the + * underlying storage */ - default void throwIfConjunctionsOnBlobColumnNotSupported( + default void throwIfConjunctionsColumnNotSupported( Set conjunctions, TableMetadata metadata) {} } diff --git a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcOperationCheckerTest.java b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcOperationCheckerTest.java index f9afee0551..716014ddae 100644 --- a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcOperationCheckerTest.java +++ b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcOperationCheckerTest.java @@ -85,7 +85,7 @@ public void checkConjunctions_ShouldInvokeAdditionalCheckOnRdbEngine() { operationChecker.checkConjunctions(selection, tableMetadata); // Assert - verify(rdbEngine).throwIfConjunctionsOnBlobColumnNotSupported(conjunctions, tableMetadata); + verify(rdbEngine).throwIfConjunctionsColumnNotSupported(conjunctions, tableMetadata); } @Test @@ -94,7 +94,7 @@ public void checkConjunctions_WhenAdditionalCheckThrows_ShouldPropagateException Exception exception = new RuntimeException(); doThrow(exception) .when(rdbEngine) - .throwIfConjunctionsOnBlobColumnNotSupported(any(), any(TableMetadata.class)); + .throwIfConjunctionsColumnNotSupported(any(), any(TableMetadata.class)); Set conjunctions = Collections.emptySet(); when(selection.getConjunctions()).thenReturn(conjunctions); @@ -104,6 +104,6 @@ public void checkConjunctions_WhenAdditionalCheckThrows_ShouldPropagateException .isEqualTo(exception); // Assert - verify(rdbEngine).throwIfConjunctionsOnBlobColumnNotSupported(conjunctions, tableMetadata); + verify(rdbEngine).throwIfConjunctionsColumnNotSupported(conjunctions, tableMetadata); } } diff --git a/core/src/test/java/com/scalar/db/storage/jdbc/RdbEngineOracleTest.java b/core/src/test/java/com/scalar/db/storage/jdbc/RdbEngineOracleTest.java index 1cc7f9228a..1bbd0e55db 100644 --- a/core/src/test/java/com/scalar/db/storage/jdbc/RdbEngineOracleTest.java +++ b/core/src/test/java/com/scalar/db/storage/jdbc/RdbEngineOracleTest.java @@ -153,8 +153,7 @@ void createTableInternalSqlsAfterCreateTable_GivenDifferentClusteringOrders_Shou } @Test - public void - throwIfConjunctionsOnBlobColumnNotSupported_WithoutBlobCondition_ShouldNotThrowException() { + public void throwIfConjunctionsColumnNotSupported_WithoutBlobCondition_ShouldNotThrowException() { // Arrange TableMetadata metadata = mock(TableMetadata.class); java.util.Set conjunctions = @@ -168,28 +167,26 @@ void createTableInternalSqlsAfterCreateTable_GivenDifferentClusteringOrders_Shou // Act & Assert assertThatCode( - () -> - rdbEngineOracle.throwIfConjunctionsOnBlobColumnNotSupported(conjunctions, metadata)) + () -> rdbEngineOracle.throwIfConjunctionsColumnNotSupported(conjunctions, metadata)) .doesNotThrowAnyException(); } @Test - public void - throwIfConjunctionsOnBlobColumnNotSupported_WithNoConditions_ShouldNotThrowException() { + public void throwIfConjunctionsColumnNotSupported_WithNoConditions_ShouldNotThrowException() { // Arrange TableMetadata metadata = mock(TableMetadata.class); // Act & Assert assertThatCode( () -> - rdbEngineOracle.throwIfConjunctionsOnBlobColumnNotSupported( + rdbEngineOracle.throwIfConjunctionsColumnNotSupported( java.util.Collections.emptySet(), metadata)) .doesNotThrowAnyException(); } @Test public void - throwIfConjunctionsOnBlobColumnNotSupported_WithMixedConditions_ShouldThrowWhenBlobPresent() { + throwIfConjunctionsColumnNotSupported_WithMixedConditions_ShouldThrowWhenBlobPresent() { // Arrange TableMetadata metadata = mock(TableMetadata.class); java.util.Set conjunctions = @@ -205,8 +202,7 @@ void createTableInternalSqlsAfterCreateTable_GivenDifferentClusteringOrders_Shou // Act & Assert assertThatThrownBy( - () -> - rdbEngineOracle.throwIfConjunctionsOnBlobColumnNotSupported(conjunctions, metadata)) + () -> rdbEngineOracle.throwIfConjunctionsColumnNotSupported(conjunctions, metadata)) .isInstanceOf(UnsupportedOperationException.class) .hasMessageContaining("blob_column"); }