Skip to content
Merged
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
9 changes: 7 additions & 2 deletions generic-contracts/scripts/objects-table-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@
"object_id": "TEXT",
"version": "TEXT",
"status": "INT",
"registered_at": "BIGINT"
"registered_at": "BIGINT",
"column_boolean": "BOOLEAN",
"column_bigint": "BIGINT",
"column_float": "FLOAT",
"column_double": "DOUBLE",
"column_text": "TEXT",
"column_blob": "BLOB"
},
"compaction-strategy": "LCS"
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,16 @@
import com.scalar.db.common.error.CoreError;
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.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.schemaloader.SchemaLoader;
import com.scalar.db.schemaloader.SchemaLoaderException;
import com.scalar.db.service.StorageFactory;
Expand Down Expand Up @@ -115,9 +124,6 @@ public class GenericContractEndToEndTest {
private static final String ASSET_ID = "id";
private static final String ASSET_AGE = "age";
private static final String ASSET_OUTPUT = "output";
private static final String DATA_TYPE_INT = "INT";
private static final String DATA_TYPE_BIGINT = "BIGINT";
private static final String DATA_TYPE_TEXT = "TEXT";

private static final String JDBC_TRANSACTION_MANAGER = "jdbc";
private static final String PROP_STORAGE = "scalardb.storage";
Expand Down Expand Up @@ -241,6 +247,17 @@ public class GenericContractEndToEndTest {
private static final String SOME_COLUMN_NAME_2 = "version";
private static final String SOME_COLUMN_NAME_3 = "status";
private static final String SOME_COLUMN_NAME_4 = "registered_at";
private static final String SOME_COLUMN_NAME_BOOLEAN = "column_boolean";
private static final String SOME_COLUMN_NAME_BIGINT = "column_bigint";
private static final String SOME_COLUMN_NAME_FLOAT = "column_float";
private static final String SOME_COLUMN_NAME_DOUBLE = "column_double";
private static final String SOME_COLUMN_NAME_TEXT = "column_text";
private static final String SOME_COLUMN_NAME_BLOB = "column_blob";
private static final boolean SOME_BOOLEAN_VALUE = false;
private static final long SOME_BIGINT_VALUE = BigIntColumn.MAX_VALUE;
private static final float SOME_FLOAT_VALUE = Float.MAX_VALUE;
private static final double SOME_DOUBLE_VALUE = Double.MAX_VALUE;
private static final byte[] SOME_BLOB_VALUE = {1, 2, 3, 4, 5};
private static final String SOME_COLLECTION_ID = "set";
private static final ArrayNode SOME_DEFAULT_OBJECT_IDS =
mapper.createArrayNode().add("object1").add("object2").add("object3").add("object4");
Expand Down Expand Up @@ -432,41 +449,80 @@ private void prepareCollection() {
prepareCollection(clientService);
}

private JsonNode createColumn(String name, int value) {
private JsonNode createColumn(Column<?> column) {
ObjectNode jsonColumn =
mapper
.createObjectNode()
.put(COLUMN_NAME, column.getName())
.put(DATA_TYPE, column.getDataType().name());

switch (column.getDataType()) {
case BOOLEAN:
jsonColumn.put(VALUE, column.getBooleanValue());
break;
case INT:
jsonColumn.put(VALUE, column.getIntValue());
break;
case BIGINT:
jsonColumn.put(VALUE, column.getBigIntValue());
break;
case FLOAT:
jsonColumn.put(VALUE, column.getFloatValue());
break;
case DOUBLE:
jsonColumn.put(VALUE, column.getDoubleValue());
break;
case TEXT:
jsonColumn.put(VALUE, column.getTextValue());
break;
case BLOB:
jsonColumn.put(VALUE, column.getBlobValueAsBytes());
break;
default:
throw new IllegalArgumentException("Invalid data type: " + column.getDataType());
}

return jsonColumn;
}

private JsonNode createNullColumn(String columnName, DataType dataType) {
return mapper
.createObjectNode()
.put(COLUMN_NAME, name)
.put(VALUE, value)
.put(DATA_TYPE, DATA_TYPE_INT);
.put(COLUMN_NAME, columnName)
.put(DATA_TYPE, dataType.name())
.set(VALUE, null);
}

private JsonNode createColumn(String name, long value) {
private ArrayNode createColumns(int status) {
return mapper
.createObjectNode()
.put(COLUMN_NAME, name)
.put(VALUE, value)
.put(DATA_TYPE, DATA_TYPE_BIGINT);
.createArrayNode()
.add(createColumn(IntColumn.of(SOME_COLUMN_NAME_3, status)))
.add(createColumn(BigIntColumn.of(SOME_COLUMN_NAME_4, SOME_BIGINT_VALUE)))
.add(createColumn(BooleanColumn.of(SOME_COLUMN_NAME_BOOLEAN, SOME_BOOLEAN_VALUE)))
.add(createColumn(BigIntColumn.of(SOME_COLUMN_NAME_BIGINT, SOME_BIGINT_VALUE)))
.add(createColumn(FloatColumn.of(SOME_COLUMN_NAME_FLOAT, SOME_FLOAT_VALUE)))
.add(createColumn(DoubleColumn.of(SOME_COLUMN_NAME_DOUBLE, SOME_DOUBLE_VALUE)))
.add(createColumn(BlobColumn.of(SOME_COLUMN_NAME_BLOB, SOME_BLOB_VALUE)));
}

private JsonNode createColumn(String name, String value) {
private ArrayNode createNullColumns() {
return mapper
.createObjectNode()
.put(COLUMN_NAME, name)
.put(VALUE, value)
.put(DATA_TYPE, DATA_TYPE_TEXT);
.createArrayNode()
.add(createNullColumn(SOME_COLUMN_NAME_3, DataType.INT))
.add(createNullColumn(SOME_COLUMN_NAME_4, DataType.BIGINT))
.add(createNullColumn(SOME_COLUMN_NAME_BOOLEAN, DataType.BOOLEAN))
.add(createNullColumn(SOME_COLUMN_NAME_BIGINT, DataType.BIGINT))
.add(createNullColumn(SOME_COLUMN_NAME_FLOAT, DataType.FLOAT))
.add(createNullColumn(SOME_COLUMN_NAME_DOUBLE, DataType.DOUBLE))
.add(createNullColumn(SOME_COLUMN_NAME_TEXT, DataType.TEXT))
.add(createNullColumn(SOME_COLUMN_NAME_BLOB, DataType.BLOB));
}

private JsonNode createFunctionArguments(
String objectId, String version, int status, long registeredAt) {
private ObjectNode createFunctionArguments(String objectId, String version, ArrayNode columns) {
ArrayNode partitionKey =
mapper.createArrayNode().add(createColumn(SOME_COLUMN_NAME_1, objectId));
mapper.createArrayNode().add(createColumn(TextColumn.of(SOME_COLUMN_NAME_1, objectId)));
ArrayNode clusteringKey =
mapper.createArrayNode().add(createColumn(SOME_COLUMN_NAME_2, version));
ArrayNode columns =
mapper
.createArrayNode()
.add(createColumn(SOME_COLUMN_NAME_3, status))
.add(createColumn(SOME_COLUMN_NAME_4, registeredAt));
mapper.createArrayNode().add(createColumn(TextColumn.of(SOME_COLUMN_NAME_2, version)));

ObjectNode arguments = mapper.createObjectNode();
arguments.put(NAMESPACE, SOME_FUNCTION_NAMESPACE);
Expand All @@ -478,6 +534,14 @@ private JsonNode createFunctionArguments(
return arguments;
}

private ObjectNode createFunctionArguments(String objectId, String version, int status) {
return createFunctionArguments(objectId, version, createColumns(status));
}

private ObjectNode createFunctionArgumentsWithNullColumns(String objectId, String version) {
return createFunctionArguments(objectId, version, createNullColumns());
}

private void addObjectsToCollection(
GenericContractClientService clientService, String collectionId, ArrayNode objectIds) {
JsonNode arguments =
Expand Down Expand Up @@ -749,9 +813,8 @@ public void putObject_FunctionArgumentsGiven_ShouldPutRecordToFunctionTable()
.put(OBJECT_ID, SOME_OBJECT_ID)
.put(HASH_VALUE, SOME_HASH_VALUE_1)
.set(METADATA, SOME_METADATA_1);
JsonNode functionArguments0 = createFunctionArguments(SOME_OBJECT_ID, SOME_VERSION_ID_0, 0, 1L);
JsonNode functionArguments1 =
createFunctionArguments(SOME_OBJECT_ID, SOME_VERSION_ID_1, 1, 1234567890123L);
JsonNode functionArguments0 = createFunctionArguments(SOME_OBJECT_ID, SOME_VERSION_ID_0, 0);
JsonNode functionArguments1 = createFunctionArguments(SOME_OBJECT_ID, SOME_VERSION_ID_1, 1);
Scan scan =
Scan.newBuilder()
.namespace(SOME_FUNCTION_NAMESPACE)
Expand All @@ -773,11 +836,63 @@ public void putObject_FunctionArgumentsGiven_ShouldPutRecordToFunctionTable()
assertThat(results.get(0).getText(SOME_COLUMN_NAME_1)).isEqualTo(SOME_OBJECT_ID);
assertThat(results.get(0).getText(SOME_COLUMN_NAME_2)).isEqualTo(SOME_VERSION_ID_0);
assertThat(results.get(0).getInt(SOME_COLUMN_NAME_3)).isEqualTo(0);
assertThat(results.get(0).getBigInt(SOME_COLUMN_NAME_4)).isEqualTo(1L);
assertThat(results.get(0).getBigInt(SOME_COLUMN_NAME_4)).isEqualTo(SOME_BIGINT_VALUE);
assertThat(results.get(0).getBoolean(SOME_COLUMN_NAME_BOOLEAN)).isEqualTo(SOME_BOOLEAN_VALUE);
assertThat(results.get(0).getBigInt(SOME_COLUMN_NAME_BIGINT)).isEqualTo(SOME_BIGINT_VALUE);
assertThat(results.get(0).getFloat(SOME_COLUMN_NAME_FLOAT)).isEqualTo(SOME_FLOAT_VALUE);
assertThat(results.get(0).getDouble(SOME_COLUMN_NAME_DOUBLE)).isEqualTo(SOME_DOUBLE_VALUE);
assertThat(results.get(0).getBlobAsBytes(SOME_COLUMN_NAME_BLOB)).isEqualTo(SOME_BLOB_VALUE);
assertThat(results.get(1).getText(SOME_COLUMN_NAME_1)).isEqualTo(SOME_OBJECT_ID);
assertThat(results.get(1).getText(SOME_COLUMN_NAME_2)).isEqualTo(SOME_VERSION_ID_1);
assertThat(results.get(1).getInt(SOME_COLUMN_NAME_3)).isEqualTo(1);
assertThat(results.get(1).getBigInt(SOME_COLUMN_NAME_4)).isEqualTo(1234567890123L);
assertThat(results.get(1).getBigInt(SOME_COLUMN_NAME_4)).isEqualTo(SOME_BIGINT_VALUE);
assertThat(results.get(1).getBoolean(SOME_COLUMN_NAME_BOOLEAN)).isEqualTo(SOME_BOOLEAN_VALUE);
assertThat(results.get(1).getBigInt(SOME_COLUMN_NAME_BIGINT)).isEqualTo(SOME_BIGINT_VALUE);
assertThat(results.get(1).getFloat(SOME_COLUMN_NAME_FLOAT)).isEqualTo(SOME_FLOAT_VALUE);
assertThat(results.get(1).getDouble(SOME_COLUMN_NAME_DOUBLE)).isEqualTo(SOME_DOUBLE_VALUE);
assertThat(results.get(1).getBlobAsBytes(SOME_COLUMN_NAME_BLOB)).isEqualTo(SOME_BLOB_VALUE);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Test
public void putObject_FunctionArgumentsWithNullColumnsGiven_ShouldPutRecordToFunctionTable()
throws ExecutionException {
// Arrange
JsonNode contractArguments =
mapper
.createObjectNode()
.put(OBJECT_ID, SOME_OBJECT_ID)
.put(HASH_VALUE, SOME_HASH_VALUE_0)
.set(METADATA, SOME_METADATA_0);
JsonNode functionArguments =
createFunctionArgumentsWithNullColumns(SOME_OBJECT_ID, SOME_VERSION_ID_0);
Scan scan =
Scan.newBuilder()
.namespace(SOME_FUNCTION_NAMESPACE)
.table(SOME_FUNCTION_TABLE)
.partitionKey(Key.ofText(SOME_COLUMN_NAME_1, SOME_OBJECT_ID))
.build();

// Act
clientService.executeContract(
ID_OBJECT_PUT, contractArguments, ID_OBJECT_PUT_MUTABLE, functionArguments);

// Assert
try (Scanner scanner = storage.scan(scan)) {
List<Result> results = scanner.all();
assertThat(results).hasSize(1);
assertThat(results.get(0).getText(SOME_COLUMN_NAME_1)).isEqualTo(SOME_OBJECT_ID);
assertThat(results.get(0).getText(SOME_COLUMN_NAME_2)).isEqualTo(SOME_VERSION_ID_0);
assertThat(results.get(0).isNull(SOME_COLUMN_NAME_3)).isTrue();
assertThat(results.get(0).isNull(SOME_COLUMN_NAME_4)).isTrue();
assertThat(results.get(0).isNull(SOME_COLUMN_NAME_BOOLEAN)).isTrue();
assertThat(results.get(0).isNull(SOME_COLUMN_NAME_BIGINT)).isTrue();
assertThat(results.get(0).isNull(SOME_COLUMN_NAME_FLOAT)).isTrue();
assertThat(results.get(0).isNull(SOME_COLUMN_NAME_DOUBLE)).isTrue();
assertThat(results.get(0).isNull(SOME_COLUMN_NAME_TEXT)).isTrue();
assertThat(results.get(0).isNull(SOME_COLUMN_NAME_BLOB)).isTrue();
} catch (IOException e) {
throw new RuntimeException(e);
}
Expand All @@ -800,7 +915,9 @@ public void putObject_FunctionArgumentsGiven_ShouldPutRecordToFunctionTable()
.put(TABLE, "foo")
.set(
PARTITION_KEY,
mapper.createArrayNode().add(createColumn(SOME_COLUMN_NAME_1, SOME_OBJECT_ID)));
mapper
.createArrayNode()
.add(createColumn(TextColumn.of(SOME_COLUMN_NAME_1, SOME_OBJECT_ID))));

// Act Assert
assertThatThrownBy(
Expand Down Expand Up @@ -832,8 +949,8 @@ public void putObject_FunctionArgumentsGiven_ShouldPutRecordToFunctionTable()
.put(OBJECT_ID, SOME_OBJECT_ID)
.put(HASH_VALUE, SOME_HASH_VALUE_1)
.set(METADATA, SOME_METADATA_1);
JsonNode functionArguments0 = createFunctionArguments(SOME_OBJECT_ID, SOME_VERSION_ID_0, 0, 1L);
JsonNode functionArguments1 = createFunctionArguments(SOME_OBJECT_ID, SOME_VERSION_ID_0, 1, 1L);
JsonNode functionArguments0 = createFunctionArguments(SOME_OBJECT_ID, SOME_VERSION_ID_0, 0);
JsonNode functionArguments1 = createFunctionArguments(SOME_OBJECT_ID, SOME_VERSION_ID_0, 1);
Put put =
Put.newBuilder()
.namespace(SOME_FUNCTION_NAMESPACE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,10 @@ private Column<?> getColumn(JsonNode jsonColumn) {
}

if (dataType.equals(DataType.FLOAT)) {
if (!value.isFloat()) {
// The JSON deserializer does not distinguish between float and double values; all JSON
// numbers with a decimal point are deserialized as double. Therefore, we check for isDouble()
// here even for FLOAT columns.
if (!value.isDouble()) {
throw new ContractContextException(Constants.INVALID_PUT_MUTABLE_FUNCTION_ARGUMENT_FORMAT);
}
return FloatColumn.of(columnName, value.floatValue());
Expand All @@ -183,7 +186,9 @@ private Column<?> getColumn(JsonNode jsonColumn) {
}

if (dataType.equals(DataType.BLOB)) {
if (!value.isBinary()) {
// BLOB data is expected as a Base64-encoded string due to JSON limitations. JSON cannot
// represent binary data directly, so BLOBs must be provided as Base64-encoded strings.
if (!value.isTextual()) {
throw new ContractContextException(Constants.INVALID_PUT_MUTABLE_FUNCTION_ARGUMENT_FORMAT);
}
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ public class PutToMutableDatabaseTest {
private static final String SOME_DOUBLE_COLUMN_NAME = "double_column";
private static final double SOME_DOUBLE_COLUMN_VALUE = 1.23;
private static final String SOME_BLOB_COLUMN_NAME = "blob_column";
private static final byte[] SOME_BLOB_COLUMN_VALUE = {10, 20, 30};
private static final byte[] SOME_BLOB_COLUMN_VALUE = {1, 2, 3, 4, 5};
private static final String SOME_BLOB_COLUMN_TEXT = "AQIDBAU="; // Base64 of {1, 2, 3, 4, 5}
private static final JsonNode SOME_TEXT_COLUMN1 =
mapper
.createObjectNode()
Expand Down Expand Up @@ -81,7 +82,10 @@ public class PutToMutableDatabaseTest {
mapper
.createObjectNode()
.put(Constants.COLUMN_NAME, SOME_FLOAT_COLUMN_NAME)
.put(Constants.VALUE, SOME_FLOAT_COLUMN_VALUE)
.put(
Constants.VALUE,
Double.valueOf(
SOME_FLOAT_COLUMN_VALUE)) // float is always converted to double in function args
.put(Constants.DATA_TYPE, "FLOAT");
private static final JsonNode SOME_DOUBLE_COLUMN =
mapper
Expand All @@ -93,7 +97,7 @@ public class PutToMutableDatabaseTest {
mapper
.createObjectNode()
.put(Constants.COLUMN_NAME, SOME_BLOB_COLUMN_NAME)
.put(Constants.VALUE, SOME_BLOB_COLUMN_VALUE)
.put(Constants.VALUE, SOME_BLOB_COLUMN_TEXT)
.put(Constants.DATA_TYPE, "BLOB");
private static final JsonNode SOME_NULL_COLUMN =
mapper
Expand Down Expand Up @@ -481,9 +485,9 @@ public void invoke_ColumnsWithUnmatchedTypeGiven_ShouldThrowContractContextExcep
mapper
.createObjectNode()
.put(Constants.COLUMN_NAME, SOME_FLOAT_COLUMN_NAME)
.put(Constants.VALUE, SOME_DOUBLE_COLUMN_VALUE)
.put(Constants.VALUE, SOME_INT_COLUMN_VALUE)
.put(Constants.DATA_TYPE, "FLOAT"),
"DOUBLE value with FLOAT data type")
"INT value with FLOAT data type")
.put(
mapper
.createObjectNode()
Expand All @@ -495,16 +499,16 @@ public void invoke_ColumnsWithUnmatchedTypeGiven_ShouldThrowContractContextExcep
mapper
.createObjectNode()
.put(Constants.COLUMN_NAME, SOME_TEXT_COLUMN1_NAME)
.put(Constants.VALUE, SOME_BLOB_COLUMN_VALUE)
.put(Constants.VALUE, SOME_INT_COLUMN_VALUE)
.put(Constants.DATA_TYPE, "TEXT"),
"BLOB value with TEXT data type")
"INT value with TEXT data type")
.put(
mapper
.createObjectNode()
.put(Constants.COLUMN_NAME, SOME_BLOB_COLUMN_NAME)
.put(Constants.VALUE, SOME_TEXT_COLUMN1_VALUE)
.put(Constants.VALUE, SOME_BLOB_COLUMN_VALUE)
.put(Constants.DATA_TYPE, "BLOB"),
"TEXT value with BLOB data type");
"BLOB value with BLOB data type");

// Act Assert
builder
Expand Down