diff --git a/generic-contracts/scripts/objects-table-schema.json b/generic-contracts/scripts/objects-table-schema.json index 3150440a..a421cdb9 100644 --- a/generic-contracts/scripts/objects-table-schema.json +++ b/generic-contracts/scripts/objects-table-schema.json @@ -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" } } - diff --git a/generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractEndToEndTest.java b/generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractEndToEndTest.java index 67bfdb5b..bf888acd 100644 --- a/generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractEndToEndTest.java +++ b/generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractEndToEndTest.java @@ -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; @@ -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"; @@ -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"); @@ -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); @@ -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 = @@ -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) @@ -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 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); } @@ -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( @@ -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) diff --git a/generic-contracts/src/main/java/com/scalar/dl/genericcontracts/object/PutToMutableDatabase.java b/generic-contracts/src/main/java/com/scalar/dl/genericcontracts/object/PutToMutableDatabase.java index 91b5b85d..a57f5889 100644 --- a/generic-contracts/src/main/java/com/scalar/dl/genericcontracts/object/PutToMutableDatabase.java +++ b/generic-contracts/src/main/java/com/scalar/dl/genericcontracts/object/PutToMutableDatabase.java @@ -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()); @@ -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 { diff --git a/generic-contracts/src/test/java/com/scalar/dl/genericcontracts/object/PutToMutableDatabaseTest.java b/generic-contracts/src/test/java/com/scalar/dl/genericcontracts/object/PutToMutableDatabaseTest.java index b187decf..d4259743 100644 --- a/generic-contracts/src/test/java/com/scalar/dl/genericcontracts/object/PutToMutableDatabaseTest.java +++ b/generic-contracts/src/test/java/com/scalar/dl/genericcontracts/object/PutToMutableDatabaseTest.java @@ -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() @@ -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 @@ -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 @@ -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() @@ -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