Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add data loader core key and column utils #1771

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions core/src/main/java/com/scalar/db/common/error/CoreError.java
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,24 @@ public enum CoreError implements ScalarDbError {
"Encrypted columns are not supported in the ScalarDB Community edition",
"",
""),
DATA_LOADER_INVALID_COLUMN_NON_EXISTENT(
Category.USER_ERROR,
"0144",
"Invalid key: Column %s does not exist in the table %s in namespace %s.",
"",
""),
DATA_LOADER_INVALID_BASE64_ENCODING_FOR_COLUMN_VALUE(
Category.USER_ERROR,
"0145",
"Invalid base64 encoding for blob value for column %s in table %s in namespace %s",
"",
""),
DATA_LOADER_INVALID_NUMBER_FORMAT_FOR_COLUMN_VALUE(
Category.USER_ERROR,
"0146",
"Invalid number specified for column %s in table %s in namespace %s",
"",
""),

//
// Errors for the concurrency error category
Expand Down
9 changes: 9 additions & 0 deletions data-loader/build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
subprojects {
group = "scalardb.dataloader"


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary empty line?

ext {
apacheCommonsLangVersion = '3.14.0'
apacheCommonsIoVersion = '2.16.1'
lombokVersion = '1.18.34'
}
dependencies {
// AssertJ
Expand All @@ -23,5 +25,12 @@ subprojects {
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
testImplementation "org.mockito:mockito-junit-jupiter:${mockitoVersion}"

Copy link
Collaborator

@brfrn169 brfrn169 Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto. Unnecessary empty line?


// Lombok
compileOnly "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
testCompileOnly "org.projectlombok:lombok:${lombokVersion}"
testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}"
}
}
2 changes: 1 addition & 1 deletion data-loader/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ archivesBaseName = "scalardb-data-loader-core"
dependencies {
// ScalarDB core
implementation project(':core')

// for SpotBugs
compileOnly "com.github.spotbugs:spotbugs-annotations:${spotbugsVersion}"
testCompileOnly "com.github.spotbugs:spotbugs-annotations:${spotbugsVersion}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.scalar.db.dataloader.core;

import lombok.Builder;
import lombok.Value;

/** Represents a column in a table. */
@Value
@Builder
public class ColumnInfo {
String namespace;
String tableName;
String columnName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.scalar.db.dataloader.core.exception;

/**
* An exception that is thrown when an error occurs while trying to create a ScalarDB column from a
* value
*/
public class ColumnParsingException extends Exception {

public ColumnParsingException(String message) {
super(message);
}

public ColumnParsingException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.scalar.db.dataloader.core.exception;

/**
* An exception that is thrown when an error occurs while trying to create a ScalarDB from a value
*/
public class KeyParsingException extends Exception {

public KeyParsingException(String message) {
super(message);
}

public KeyParsingException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.scalar.db.dataloader.core.util;

import com.scalar.db.common.error.CoreError;
import com.scalar.db.dataloader.core.ColumnInfo;
import com.scalar.db.dataloader.core.exception.ColumnParsingException;
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.DoubleColumn;
import com.scalar.db.io.FloatColumn;
import com.scalar.db.io.IntColumn;
import com.scalar.db.io.TextColumn;
import java.util.Base64;
import javax.annotation.Nullable;

/** Utility class for dealing and creating ScalarDB Columns */
public final class ColumnUtils {

/** Restrict instantiation via private constructor */
private ColumnUtils() {}

/**
* Create a ScalarDB column from the given data type, column name, and value. Blob source values
* need to be base64 encoded.
*
* @param dataType Data type of the specified column
* @param columnInfo ScalarDB table column information
* @param value Value for the ScalarDB column
* @return ScalarDB column
* @throws ColumnParsingException if an error occurs while creating the column and parsing the
* value
*/
public static Column<?> createColumnFromValue(
DataType dataType, ColumnInfo columnInfo, @Nullable String value)
throws ColumnParsingException {
String columnName = columnInfo.getColumnName();
try {
switch (dataType) {
case BOOLEAN:
return value != null
? BooleanColumn.of(columnName, Boolean.parseBoolean(value))
: BooleanColumn.ofNull(columnName);
case INT:
return value != null
? IntColumn.of(columnName, Integer.parseInt(value))
: IntColumn.ofNull(columnName);
case BIGINT:
return value != null
? BigIntColumn.of(columnName, Long.parseLong(value))
: BigIntColumn.ofNull(columnName);
case FLOAT:
return value != null
? FloatColumn.of(columnName, Float.parseFloat(value))
: FloatColumn.ofNull(columnName);
case DOUBLE:
return value != null
? DoubleColumn.of(columnName, Double.parseDouble(value))
: DoubleColumn.ofNull(columnName);
case TEXT:
return value != null ? TextColumn.of(columnName, value) : TextColumn.ofNull(columnName);
case BLOB:
// Source blob values need to be base64 encoded
return value != null
? BlobColumn.of(columnName, Base64.getDecoder().decode(value))
: BlobColumn.ofNull(columnName);
default:
throw new AssertionError();
}
} catch (NumberFormatException e) {
throw new ColumnParsingException(
CoreError.DATA_LOADER_INVALID_NUMBER_FORMAT_FOR_COLUMN_VALUE.buildMessage(
columnName, columnInfo.getTableName(), columnInfo.getNamespace()),
e);
} catch (IllegalArgumentException e) {
throw new ColumnParsingException(
CoreError.DATA_LOADER_INVALID_BASE64_ENCODING_FOR_COLUMN_VALUE.buildMessage(
columnName, columnInfo.getTableName(), columnInfo.getNamespace()),
e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.scalar.db.dataloader.core.util;

import com.scalar.db.api.TableMetadata;
import com.scalar.db.common.error.CoreError;
import com.scalar.db.dataloader.core.ColumnInfo;
import com.scalar.db.dataloader.core.ColumnKeyValue;
import com.scalar.db.dataloader.core.exception.ColumnParsingException;
import com.scalar.db.dataloader.core.exception.KeyParsingException;
import com.scalar.db.io.Column;
import com.scalar.db.io.DataType;
import com.scalar.db.io.Key;
import javax.annotation.Nullable;

/** Utility class for creating and dealing with ScalarDB keys. */
public final class KeyUtils {

/** Restrict instantiation via private constructor */
private KeyUtils() {}

/**
* Convert a keyValue, in the format of <key>=<value>, to a ScalarDB Key instance for a specific
* ScalarDB table.
*
* @param columnKeyValue A key value in the format of <key>=<value>
* @param namespace Name of the ScalarDB namespace
* @param tableName Name of the ScalarDB table
* @param tableMetadata Metadata for one ScalarDB table
* @return A new ScalarDB Key instance formatted by data type
* @throws KeyParsingException if there is an error parsing the key value
*/
@Nullable
public static Key parseKeyValue(
ypeckstadt marked this conversation as resolved.
Show resolved Hide resolved
@Nullable ColumnKeyValue columnKeyValue,
String namespace,
String tableName,
TableMetadata tableMetadata)
throws KeyParsingException {
if (columnKeyValue == null) {
return null;
}
String columnName = columnKeyValue.getColumnName();
DataType columnDataType = tableMetadata.getColumnDataType(columnName);
if (columnDataType == null) {
throw new KeyParsingException(
CoreError.DATA_LOADER_INVALID_COLUMN_NON_EXISTENT.buildMessage(
columnName, tableName, namespace));
}
ColumnInfo columnInfo =
ColumnInfo.builder()
.namespace(namespace)
.tableName(tableName)
.columnName(columnName)
.build();
return createKey(columnDataType, columnInfo, columnKeyValue.getColumnValue());
}

/**
* Create a ScalarDB key based on the provided data type, column name, and value.
*
* @param dataType Data type of the specified column
* @param columnInfo ScalarDB table column information
* @param value Value for ScalarDB key
* @return ScalarDB Key instance
* @throws KeyParsingException if there is an error while creating a ScalarDB key
*/
public static Key createKey(DataType dataType, ColumnInfo columnInfo, String value)
throws KeyParsingException {
try {
Column<?> keyValue = ColumnUtils.createColumnFromValue(dataType, columnInfo, value);
return Key.newBuilder().add(keyValue).build();
} catch (ColumnParsingException e) {
throw new KeyParsingException(e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.scalar.db.dataloader.core.util;

import static org.junit.jupiter.api.Assertions.*;

import com.scalar.db.common.error.CoreError;
import com.scalar.db.dataloader.core.ColumnInfo;
import com.scalar.db.dataloader.core.exception.ColumnParsingException;
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.DoubleColumn;
import com.scalar.db.io.FloatColumn;
import com.scalar.db.io.IntColumn;
import com.scalar.db.io.TextColumn;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class ColumnUtilsTest {

private static final float FLOAT_VALUE = 2.78f;

private static Stream<Arguments> provideColumnsForCreateColumnFromValue() {
return Stream.of(
Arguments.of(DataType.BOOLEAN, "boolColumn", "true", BooleanColumn.of("boolColumn", true)),
Arguments.of(DataType.BOOLEAN, "boolColumn", null, BooleanColumn.ofNull("boolColumn")),
Arguments.of(DataType.INT, "intColumn", "42", IntColumn.of("intColumn", 42)),
Arguments.of(DataType.INT, "intColumn", null, IntColumn.ofNull("intColumn")),
Arguments.of(
DataType.BIGINT,
"bigintColumn",
"123456789012",
BigIntColumn.of("bigintColumn", 123456789012L)),
Arguments.of(DataType.BIGINT, "bigintColumn", null, BigIntColumn.ofNull("bigintColumn")),
Arguments.of(
DataType.FLOAT,
"floatColumn",
Float.toString(FLOAT_VALUE),
FloatColumn.of("floatColumn", FLOAT_VALUE)),
Arguments.of(DataType.FLOAT, "floatColumn", null, FloatColumn.ofNull("floatColumn")),
Arguments.of(
DataType.DOUBLE,
"doubleColumn",
Double.toString(Math.E),
DoubleColumn.of("doubleColumn", Math.E)),
Arguments.of(DataType.DOUBLE, "doubleColumn", null, DoubleColumn.ofNull("doubleColumn")),
Arguments.of(
DataType.TEXT,
"textColumn",
"Hello, world!",
TextColumn.of("textColumn", "Hello, world!")),
Arguments.of(DataType.TEXT, "textColumn", null, TextColumn.ofNull("textColumn")),
Arguments.of(
DataType.BLOB,
"blobColumn",
Base64.getEncoder().encodeToString("binary".getBytes(StandardCharsets.UTF_8)),
BlobColumn.of("blobColumn", "binary".getBytes(StandardCharsets.UTF_8))),
Arguments.of(DataType.BLOB, "blobColumn", null, BlobColumn.ofNull("blobColumn")));
}

@ParameterizedTest
@MethodSource("provideColumnsForCreateColumnFromValue")
void createColumnFromValue_validInput_returnsColumn(
DataType dataType, String columnName, String value, Column<?> expectedColumn)
throws ColumnParsingException {
ColumnInfo columnInfo = ColumnInfo.builder().columnName(columnName).build();
Column<?> actualColumn = ColumnUtils.createColumnFromValue(dataType, columnInfo, value);
assertEquals(expectedColumn, actualColumn);
}

@Test
void createColumnFromValue_invalidNumberFormat_throwsNumberFormatException() {
String columnName = "intColumn";
String value = "not_a_number";
ColumnInfo columnInfo =
ColumnInfo.builder().namespace("ns").tableName("table").columnName(columnName).build();
ColumnParsingException exception =
assertThrows(
ColumnParsingException.class,
() -> ColumnUtils.createColumnFromValue(DataType.INT, columnInfo, value));
assertEquals(
CoreError.DATA_LOADER_INVALID_NUMBER_FORMAT_FOR_COLUMN_VALUE.buildMessage(
columnName, "table", "ns"),
exception.getMessage());
}

@Test
void createColumnFromValue_invalidBase64_throwsBase64Exception() {
String columnName = "blobColumn";
String value = "invalid_base64";
ColumnInfo columnInfo =
ColumnInfo.builder().namespace("ns").tableName("table").columnName(columnName).build();
ColumnParsingException exception =
assertThrows(
ColumnParsingException.class,
() -> ColumnUtils.createColumnFromValue(DataType.BLOB, columnInfo, value));
assertEquals(
CoreError.DATA_LOADER_INVALID_BASE64_ENCODING_FOR_COLUMN_VALUE.buildMessage(
columnName, "table", "ns"),
exception.getMessage());
}
}
Loading
Loading