Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
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.assertj.core.api.Assertions.catchThrowable;

import com.scalar.db.api.DistributedStorage;
import com.scalar.db.api.DistributedStorageAdminIntegrationTestBase;
import com.scalar.db.api.Put;
import com.scalar.db.api.PutBuilder;
import com.scalar.db.api.Result;
import com.scalar.db.api.Scan;
import com.scalar.db.api.Scanner;
import com.scalar.db.api.TableMetadata;
import com.scalar.db.config.DatabaseConfig;
import com.scalar.db.exception.storage.ExecutionException;
import com.scalar.db.io.DataType;
import com.scalar.db.io.Key;
import com.scalar.db.util.AdminTestUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -37,11 +56,28 @@ protected boolean isCreateIndexOnTextColumnEnabled() {
return !JdbcTestUtils.isDb2(rdbEngine);
}

@SuppressWarnings("unused")
private boolean isOracle() {
return JdbcEnv.isOracle();
}

@SuppressWarnings("unused")
private boolean isDb2() {
return JdbcEnv.isDb2();
}

@SuppressWarnings("unused")
private boolean isSqlite() {
return JdbcEnv.isSqlite();
}

@SuppressWarnings("unused")
private boolean isColumnTypeConversionToTextNotFullySupported() {
return JdbcTestUtils.isDb2(rdbEngine)
|| JdbcTestUtils.isOracle(rdbEngine)
|| JdbcTestUtils.isSqlite(rdbEngine);
}

@Test
@Override
@DisabledIf("isDb2")
Expand Down Expand Up @@ -97,6 +133,228 @@ public void renameColumn_Db2_ForPrimaryOrIndexKeyColumn_ShouldThrowUnsupportedOp
}
}

@Test
@Override
@DisabledIf("isColumnTypeConversionToTextNotFullySupported")
public void
alterColumnType_AlterColumnTypeFromEachExistingDataTypeToText_ShouldAlterColumnTypesCorrectly()
throws ExecutionException, IOException {
super
.alterColumnType_AlterColumnTypeFromEachExistingDataTypeToText_ShouldAlterColumnTypesCorrectly();
}

@Test
@EnabledIf("isOracle")
public void
alterColumnType_Oracle_AlterColumnTypeFromEachExistingDataTypeToText_ShouldThrowUnsupportedOperationException()
throws ExecutionException {
try (DistributedStorage storage = storageFactory.getStorage()) {
// Arrange
Map<String, String> options = getCreationOptions();
TableMetadata.Builder currentTableMetadataBuilder =
TableMetadata.newBuilder()
.addColumn(getColumnName1(), DataType.INT)
.addColumn(getColumnName2(), DataType.INT)
.addColumn(getColumnName3(), DataType.INT)
.addColumn(getColumnName4(), DataType.BIGINT)
.addColumn(getColumnName5(), DataType.FLOAT)
.addColumn(getColumnName6(), DataType.DOUBLE)
.addColumn(getColumnName7(), DataType.TEXT)
.addColumn(getColumnName8(), DataType.BLOB)
.addColumn(getColumnName9(), DataType.DATE)
.addColumn(getColumnName10(), DataType.TIME)
.addPartitionKey(getColumnName1())
.addClusteringKey(getColumnName2(), Scan.Ordering.Order.ASC);
if (isTimestampTypeSupported()) {
currentTableMetadataBuilder
.addColumn(getColumnName11(), DataType.TIMESTAMP)
.addColumn(getColumnName12(), DataType.TIMESTAMPTZ);
}
TableMetadata currentTableMetadata = currentTableMetadataBuilder.build();
admin.createTable(getNamespace1(), getTable4(), currentTableMetadata, options);
PutBuilder.Buildable put =
Put.newBuilder()
.namespace(getNamespace1())
.table(getTable4())
.partitionKey(Key.ofInt(getColumnName1(), 1))
.clusteringKey(Key.ofInt(getColumnName2(), 2))
.intValue(getColumnName3(), 1)
.bigIntValue(getColumnName4(), 2L)
.floatValue(getColumnName5(), 3.0f)
.doubleValue(getColumnName6(), 4.0d)
.textValue(getColumnName7(), "5")
.blobValue(getColumnName8(), "6".getBytes(StandardCharsets.UTF_8))
.dateValue(getColumnName9(), LocalDate.now(ZoneId.of("UTC")))
.timeValue(getColumnName10(), LocalTime.now(ZoneId.of("UTC")));
if (isTimestampTypeSupported()) {
put.timestampValue(getColumnName11(), LocalDateTime.now(ZoneOffset.UTC));
put.timestampTZValue(getColumnName12(), Instant.now());
}
storage.put(put.build());
storage.close();

// Act Assert
assertThatThrownBy(
() ->
admin.alterColumnType(
getNamespace1(), getTable4(), getColumnName3(), DataType.TEXT))
.isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(
() ->
admin.alterColumnType(
getNamespace1(), getTable4(), getColumnName4(), DataType.TEXT))
.isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(
() ->
admin.alterColumnType(
getNamespace1(), getTable4(), getColumnName5(), DataType.TEXT))
.isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(
() ->
admin.alterColumnType(
getNamespace1(), getTable4(), getColumnName6(), DataType.TEXT))
.isInstanceOf(UnsupportedOperationException.class);
admin.alterColumnType(getNamespace1(), getTable4(), getColumnName7(), DataType.TEXT);
assertThatThrownBy(
() ->
admin.alterColumnType(
getNamespace1(), getTable4(), getColumnName8(), DataType.TEXT))
.isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(
() ->
admin.alterColumnType(
getNamespace1(), getTable4(), getColumnName9(), DataType.TEXT))
.isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(
() ->
admin.alterColumnType(
getNamespace1(), getTable4(), getColumnName10(), DataType.TEXT))
.isInstanceOf(UnsupportedOperationException.class);
if (isTimestampTypeSupported()) {
assertThatThrownBy(
() ->
admin.alterColumnType(
getNamespace1(), getTable4(), getColumnName11(), DataType.TEXT))
.isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(
() ->
admin.alterColumnType(
getNamespace1(), getTable4(), getColumnName12(), DataType.TEXT))
.isInstanceOf(UnsupportedOperationException.class);
}
} finally {
admin.dropTable(getNamespace1(), getTable4(), true);
}
}

@Test
@Override
@DisabledIf("isOracle")
public void alterColumnType_WideningConversion_ShouldAlterColumnTypesCorrectly()
throws ExecutionException, IOException {
super.alterColumnType_WideningConversion_ShouldAlterColumnTypesCorrectly();
}

@Test
@EnabledIf("isOracle")
public void alterColumnType_Oracle_WideningConversion_ShouldAlterColumnTypesCorrectly()
throws ExecutionException, IOException {
try {
// Arrange
Map<String, String> options = getCreationOptions();
TableMetadata.Builder currentTableMetadataBuilder =
TableMetadata.newBuilder()
.addColumn(getColumnName1(), DataType.INT)
.addColumn(getColumnName2(), DataType.INT)
.addColumn(getColumnName3(), DataType.INT)
.addColumn(getColumnName4(), DataType.FLOAT)
.addPartitionKey(getColumnName1())
.addClusteringKey(getColumnName2(), Scan.Ordering.Order.ASC);
TableMetadata currentTableMetadata = currentTableMetadataBuilder.build();
admin.createTable(getNamespace1(), getTable4(), currentTableMetadata, options);
DistributedStorage storage = storageFactory.getStorage();
int expectedColumn3Value = 1;
float expectedColumn4Value = 4.0f;

PutBuilder.Buildable put =
Put.newBuilder()
.namespace(getNamespace1())
.table(getTable4())
.partitionKey(Key.ofInt(getColumnName1(), 1))
.clusteringKey(Key.ofInt(getColumnName2(), 2))
.intValue(getColumnName3(), expectedColumn3Value)
.floatValue(getColumnName4(), expectedColumn4Value);
storage.put(put.build());
storage.close();

// Act
admin.alterColumnType(getNamespace1(), getTable4(), getColumnName3(), DataType.BIGINT);
Throwable exception =
catchThrowable(
() ->
admin.alterColumnType(
getNamespace1(), getTable4(), getColumnName4(), DataType.DOUBLE));

// Assert
assertThat(exception).isInstanceOf(UnsupportedOperationException.class);
TableMetadata.Builder expectedTableMetadataBuilder =
TableMetadata.newBuilder()
.addColumn(getColumnName1(), DataType.INT)
.addColumn(getColumnName2(), DataType.INT)
.addColumn(getColumnName3(), DataType.BIGINT)
.addColumn(getColumnName4(), DataType.FLOAT)
.addPartitionKey(getColumnName1())
.addClusteringKey(getColumnName2(), Scan.Ordering.Order.ASC);
TableMetadata expectedTableMetadata = expectedTableMetadataBuilder.build();
assertThat(admin.getTableMetadata(getNamespace1(), getTable4()))
.isEqualTo(expectedTableMetadata);
storage = storageFactory.getStorage();
Scan scan =
Scan.newBuilder()
.namespace(getNamespace1())
.table(getTable4())
.partitionKey(Key.ofInt(getColumnName1(), 1))
.build();
try (Scanner scanner = storage.scan(scan)) {
List<Result> results = scanner.all();
assertThat(results).hasSize(1);
Result result = results.get(0);
assertThat(result.getBigInt(getColumnName3())).isEqualTo(expectedColumn3Value);
}
storage.close();
} finally {
admin.dropTable(getNamespace1(), getTable4(), true);
}
}

@Test
@EnabledIf("isSqlite")
public void alterColumnType_Sqlite_AlterColumnType_ShouldThrowUnsupportedOperationException()
throws ExecutionException {
try {
// Arrange
Map<String, String> options = getCreationOptions();
TableMetadata.Builder currentTableMetadataBuilder =
TableMetadata.newBuilder()
.addColumn(getColumnName1(), DataType.INT)
.addColumn(getColumnName2(), DataType.INT)
.addColumn(getColumnName3(), DataType.INT)
.addPartitionKey(getColumnName1())
.addClusteringKey(getColumnName2(), Scan.Ordering.Order.ASC);
TableMetadata currentTableMetadata = currentTableMetadataBuilder.build();
admin.createTable(getNamespace1(), getTable4(), currentTableMetadata, options);

// Act Assert
assertThatThrownBy(
() ->
admin.alterColumnType(
getNamespace1(), getTable4(), getColumnName3(), DataType.TEXT))
.isInstanceOf(UnsupportedOperationException.class);
} finally {
admin.dropTable(getNamespace1(), getTable4(), true);
}
}

@Override
protected boolean isIndexOnBlobColumnSupported() {
return !JdbcTestUtils.isDb2(rdbEngine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public static Properties getPropertiesForNormalUser(String testName) {
return properties;
}

public static boolean isOracle() {
return System.getProperty(PROP_JDBC_URL, DEFAULT_JDBC_URL).startsWith("jdbc:oracle:");
}

public static boolean isSqlite() {
return System.getProperty(PROP_JDBC_URL, DEFAULT_JDBC_URL).startsWith("jdbc:sqlite:");
}
Expand Down
13 changes: 13 additions & 0 deletions core/src/main/java/com/scalar/db/api/Admin.java
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,19 @@ default void dropColumnFromTable(
void renameColumn(String namespace, String table, String oldColumnName, String newColumnName)
throws ExecutionException;

/**
* Alters the data type of existing column of an existing table.
*
* @param namespace the table namespace
* @param table the table name
* @param columnName the name of the column to alter
* @param newColumnType the new data type of the column
* @throws IllegalArgumentException if the table or the column does not exist
* @throws ExecutionException if the operation fails
*/
void alterColumnType(String namespace, String table, String columnName, DataType newColumnType)
throws ExecutionException;

/**
* Renames an existing table.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,42 @@ public void renameColumn(
}
}

@Override
public void alterColumnType(
String namespace, String table, String columnName, DataType newColumnType)
throws ExecutionException {
TableMetadata tableMetadata = getTableMetadata(namespace, table);
if (tableMetadata == null) {
throw new IllegalArgumentException(
CoreError.TABLE_NOT_FOUND.buildMessage(ScalarDbUtils.getFullTableName(namespace, table)));
}

if (!tableMetadata.getColumnNames().contains(columnName)) {
throw new IllegalArgumentException(
CoreError.COLUMN_NOT_FOUND2.buildMessage(
ScalarDbUtils.getFullTableName(namespace, table), columnName));
}

DataType currentColumnType = tableMetadata.getColumnDataType(columnName);
if (currentColumnType == newColumnType) {
return;
}
if (!isTypeConversionSupported(currentColumnType, newColumnType)) {
throw new IllegalArgumentException(
CoreError.INVALID_COLUMN_TYPE_CONVERSION.buildMessage(
currentColumnType, newColumnType, columnName));
}

try {
admin.alterColumnType(namespace, table, columnName, newColumnType);
} catch (ExecutionException e) {
throw new ExecutionException(
CoreError.ALTERING_COLUMN_TYPE_FAILED.buildMessage(
ScalarDbUtils.getFullTableName(namespace, table), columnName, newColumnType),
e);
}
}

@Override
public void renameTable(String namespace, String oldTableName, String newTableName)
throws ExecutionException {
Expand Down Expand Up @@ -486,4 +522,29 @@ public StorageInfo getStorageInfo(String namespace) throws ExecutionException {
public void close() {
admin.close();
}

private boolean isTypeConversionSupported(DataType from, DataType to) {
if (from == to) {
return true;
}
switch (from) {
case BOOLEAN:
case BIGINT:
case DOUBLE:
case BLOB:
case DATE:
case TIME:
case TIMESTAMP:
case TIMESTAMPTZ:
return to == DataType.TEXT;
case INT:
return to == DataType.BIGINT || to == DataType.TEXT;
case FLOAT:
return to == DataType.DOUBLE || to == DataType.TEXT;
case TEXT:
return false;
default:
throw new AssertionError("Unknown data type: " + from);
}
}
}
Loading
Loading