diff --git a/data-loader/core/build.gradle b/data-loader/core/build.gradle index 28a2dba4de..5239fd42ef 100644 --- a/data-loader/core/build.gradle +++ b/data-loader/core/build.gradle @@ -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}" diff --git a/data-loader/core/src/main/java/com/scalar/db/dataloader/core/ColumnInfo.java b/data-loader/core/src/main/java/com/scalar/db/dataloader/core/ColumnInfo.java new file mode 100644 index 0000000000..685f58a833 --- /dev/null +++ b/data-loader/core/src/main/java/com/scalar/db/dataloader/core/ColumnInfo.java @@ -0,0 +1,24 @@ +package com.scalar.db.dataloader.core; + +import lombok.Builder; +import lombok.Value; + +/** + * Represents a column in a database table. + * + *
This class holds the metadata for a column, including the namespace (schema), table name, and + * the column name within the table. + */ +@Value +@Builder +public class ColumnInfo { + + /** The namespace (schema) where the table is located. */ + String namespace; + + /** The name of the table where the column resides. */ + String tableName; + + /** The name of the column in the table. */ + String columnName; +} diff --git a/data-loader/core/src/main/java/com/scalar/db/dataloader/core/ErrorMessage.java b/data-loader/core/src/main/java/com/scalar/db/dataloader/core/ErrorMessage.java new file mode 100644 index 0000000000..1b1986288b --- /dev/null +++ b/data-loader/core/src/main/java/com/scalar/db/dataloader/core/ErrorMessage.java @@ -0,0 +1,10 @@ +package com.scalar.db.dataloader.core; + +public class ErrorMessage { + public static final String INVALID_NUMBER_FORMAT_FOR_COLUMN_VALUE = + "Invalid number specified for column %s in table %s in namespace %s"; + public static final String INVALID_BASE64_ENCODING_FOR_COLUMN_VALUE = + "Invalid base64 encoding for blob value for column %s in table %s in namespace %s"; + public static final String INVALID_COLUMN_NON_EXISTENT = + "Invalid key: Column %s does not exist in the table %s in namespace %s."; +} diff --git a/data-loader/core/src/main/java/com/scalar/db/dataloader/core/exception/ColumnParsingException.java b/data-loader/core/src/main/java/com/scalar/db/dataloader/core/exception/ColumnParsingException.java new file mode 100644 index 0000000000..58880d70bb --- /dev/null +++ b/data-loader/core/src/main/java/com/scalar/db/dataloader/core/exception/ColumnParsingException.java @@ -0,0 +1,30 @@ +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. + * + *
This exception is typically used to indicate a problem with parsing or converting data into a + * format that can be used to create a column in ScalarDB. + */ +public class ColumnParsingException extends Exception { + + /** + * Constructs a new {@code ColumnParsingException} with the specified detail message. + * + * @param message the detail message explaining the cause of the exception + */ + public ColumnParsingException(String message) { + super(message); + } + + /** + * Constructs a new {@code ColumnParsingException} with the specified detail message and cause. + * + * @param message the detail message explaining the cause of the exception + * @param cause the cause of the exception (can be {@code null}) + */ + public ColumnParsingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/data-loader/core/src/main/java/com/scalar/db/dataloader/core/exception/KeyParsingException.java b/data-loader/core/src/main/java/com/scalar/db/dataloader/core/exception/KeyParsingException.java new file mode 100644 index 0000000000..b0efa86345 --- /dev/null +++ b/data-loader/core/src/main/java/com/scalar/db/dataloader/core/exception/KeyParsingException.java @@ -0,0 +1,30 @@ +package com.scalar.db.dataloader.core.exception; + +/** + * An exception that is thrown when an error occurs while trying to create a ScalarDB key from a + * value. + * + *
This exception is typically used to indicate a problem with parsing or converting data into a + * format that can be used to create a key in ScalarDB. + */ +public class KeyParsingException extends Exception { + + /** + * Constructs a new {@code KeyParsingException} with the specified detail message. + * + * @param message the detail message explaining the cause of the exception + */ + public KeyParsingException(String message) { + super(message); + } + + /** + * Constructs a new {@code KeyParsingException} with the specified detail message and cause. + * + * @param message the detail message explaining the cause of the exception + * @param cause the cause of the exception (can be {@code null}) + */ + public KeyParsingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/data-loader/core/src/main/java/com/scalar/db/dataloader/core/util/ColumnUtils.java b/data-loader/core/src/main/java/com/scalar/db/dataloader/core/util/ColumnUtils.java new file mode 100644 index 0000000000..1a846d40c2 --- /dev/null +++ b/data-loader/core/src/main/java/com/scalar/db/dataloader/core/util/ColumnUtils.java @@ -0,0 +1,99 @@ +package com.scalar.db.dataloader.core.util; + +import static com.scalar.db.dataloader.core.ErrorMessage.INVALID_BASE64_ENCODING_FOR_COLUMN_VALUE; +import static com.scalar.db.dataloader.core.ErrorMessage.INVALID_NUMBER_FORMAT_FOR_COLUMN_VALUE; + +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 creating and managing ScalarDB columns. + * + *
This class provides methods for creating ScalarDB columns based on the given data type, column + * information, and value. It includes handling for various data types and special cases like base64 + * encoding for BLOB data. + */ +public final class ColumnUtils { + + /** Restrict instantiation via private constructor */ + private ColumnUtils() {} + + /** + * Creates a ScalarDB column from the given data type, column information, and value. + * + *
Blob source values need to be base64 encoded before passing them as a value. If the value is + * {@code null}, the corresponding column is created as a {@code null} column. + * + * @param dataType the data type of the specified column + * @param columnInfo the ScalarDB table column information + * @param value the value for the ScalarDB column (may be {@code null}) + * @return the ScalarDB column created from the specified data + * @throws ColumnParsingException if an error occurs while creating the column or 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( + String.format( + INVALID_NUMBER_FORMAT_FOR_COLUMN_VALUE, + columnName, + columnInfo.getTableName(), + columnInfo.getNamespace()), + e); + } catch (IllegalArgumentException e) { + throw new ColumnParsingException( + String.format( + INVALID_BASE64_ENCODING_FOR_COLUMN_VALUE, + columnName, + columnInfo.getTableName(), + columnInfo.getNamespace()), + e); + } + } +} diff --git a/data-loader/core/src/main/java/com/scalar/db/dataloader/core/util/KeyUtils.java b/data-loader/core/src/main/java/com/scalar/db/dataloader/core/util/KeyUtils.java new file mode 100644 index 0000000000..e5ecc5edd8 --- /dev/null +++ b/data-loader/core/src/main/java/com/scalar/db/dataloader/core/util/KeyUtils.java @@ -0,0 +1,88 @@ +package com.scalar.db.dataloader.core.util; + +import static com.scalar.db.dataloader.core.ErrorMessage.INVALID_COLUMN_NON_EXISTENT; + +import com.scalar.db.api.TableMetadata; +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 managing ScalarDB keys. + * + *
This class provides methods to parse key-value pairs and create ScalarDB key instances. It
+ * also includes utility methods for handling data types, columns, and potential parsing exceptions.
+ */
+public final class KeyUtils {
+
+ /** Restrict instantiation via private constructor */
+ private KeyUtils() {}
+
+ /**
+ * Converts a key-value pair, in the format of This method uses the provided table metadata to determine the data type for the key and
+ * creates a corresponding ScalarDB Key. If the key does not match any column in the table
+ * metadata, a {@link KeyParsingException} is thrown.
+ *
+ * @param columnKeyValue a key-value pair in the format of This method creates a ScalarDB Key instance by converting the column value to the
+ * appropriate data type and constructing the key using that value.
+ *
+ * @param dataType the data type of the specified column
+ * @param columnInfo the ScalarDB table column information
+ * @param value the value for the ScalarDB key
+ * @return a ScalarDB Key instance
+ * @throws KeyParsingException if there is an error while creating the 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);
+ }
+ }
+}
diff --git a/data-loader/core/src/test/java/com/scalar/db/dataloader/core/util/ColumnUtilsTest.java b/data-loader/core/src/test/java/com/scalar/db/dataloader/core/util/ColumnUtilsTest.java
new file mode 100644
index 0000000000..3892deea69
--- /dev/null
+++ b/data-loader/core/src/test/java/com/scalar/db/dataloader/core/util/ColumnUtilsTest.java
@@ -0,0 +1,107 @@
+package com.scalar.db.dataloader.core.util;
+
+import static com.scalar.db.dataloader.core.ErrorMessage.INVALID_BASE64_ENCODING_FOR_COLUMN_VALUE;
+import static com.scalar.db.dataloader.core.ErrorMessage.INVALID_NUMBER_FORMAT_FOR_COLUMN_VALUE;
+import static org.junit.jupiter.api.Assertions.*;
+
+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