Skip to content
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
12 changes: 10 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,17 @@
"object_id": "TEXT",
"version": "TEXT",
"status": "INT",
"registered_at": "TIMESTAMP"
"registered_at": "TIMESTAMP",
"column_boolean": "BOOLEAN",
"column_bigint": "BIGINT",
"column_float": "FLOAT",
"column_double": "DOUBLE",
"column_text": "TEXT",
"column_blob": "BLOB",
"column_date": "DATE",
"column_time": "TIME",
"column_timestamptz": "TIMESTAMPTZ"
},
"compaction-strategy": "LCS"
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,16 @@
import com.scalar.db.api.TransactionState;
import com.scalar.db.common.CoreError;
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.dl.client.exception.ClientException;
import com.scalar.dl.client.service.GenericContractClientService;
import com.scalar.dl.ledger.error.LedgerError;
Expand All @@ -79,7 +88,11 @@
import com.scalar.dl.ledger.service.StatusCode;
import com.scalar.dl.ledger.util.JacksonSerDe;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
Expand All @@ -99,9 +112,6 @@ public class GenericContractObjectAndCollectionEndToEndTest
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_TIMESTAMP = "TIMESTAMP";
private static final String DATA_TYPE_TEXT = "TEXT";

private static final String CONTRACT_OBJECT_GET =
com.scalar.dl.genericcontracts.object.Constants.CONTRACT_GET;
Expand All @@ -124,8 +134,29 @@ public class GenericContractObjectAndCollectionEndToEndTest
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 String SOME_COLUMN_NAME_DATE = "column_date";
private static final String SOME_COLUMN_NAME_TIME = "column_time";
private static final String SOME_COLUMN_NAME_TIMESTAMPTZ = "column_timestamptz";
private static final String SOME_DATE_TEXT = "2021-02-03";
private static final String SOME_TIME_TEXT = "05:45:00";
private static final String SOME_TIMESTAMP_TEXT = "2021-02-03 05:45:00";
private static final String SOME_TIMESTAMPTZ_TEXT = "2021-02-03 05:45:00.000 Z";
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 LocalDate SOME_DATE_VALUE = LocalDate.of(2021, 2, 3);
private static final LocalTime SOME_TIME_VALUE = LocalTime.of(5, 45);
private static final LocalDateTime SOME_TIMESTAMP_VALUE = LocalDateTime.of(2021, 2, 3, 5, 45);
private static final Instant SOME_TIMESTAMPTZ_VALUE =
SOME_TIMESTAMP_VALUE.atZone(ZoneId.of("UTC")).toInstant();
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 @@ -202,41 +233,96 @@ 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 createColumn(String columnName, DataType dataType, String value) {
return mapper
.createObjectNode()
.put(COLUMN_NAME, name)
.put(COLUMN_NAME, columnName)
.put(VALUE, value)
.put(DATA_TYPE, DATA_TYPE_INT);
.put(DATA_TYPE, dataType.name());
}

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

private JsonNode createTimestampColumn(String name, String value) {
private ArrayNode createColumns(int status) {
return mapper
.createObjectNode()
.put(COLUMN_NAME, name)
.put(VALUE, value)
.put(DATA_TYPE, DATA_TYPE_TIMESTAMP);
.createArrayNode()
.add(createColumn(IntColumn.of(SOME_COLUMN_NAME_3, status)))
.add(createColumn(SOME_COLUMN_NAME_4, DataType.TIMESTAMP, SOME_TIMESTAMP_TEXT))
.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)))
.add(createColumn(SOME_COLUMN_NAME_DATE, DataType.DATE, SOME_DATE_TEXT))
.add(createColumn(SOME_COLUMN_NAME_TIME, DataType.TIME, SOME_TIME_TEXT))
.add(
createColumn(
SOME_COLUMN_NAME_TIMESTAMPTZ, DataType.TIMESTAMPTZ, SOME_TIMESTAMPTZ_TEXT));
}

private JsonNode createFunctionArguments(
String objectId, String version, int status, long registeredAt) {
private ArrayNode createNullColumns() {
return mapper
.createArrayNode()
.add(createNullColumn(SOME_COLUMN_NAME_3, DataType.INT))
.add(createNullColumn(SOME_COLUMN_NAME_4, DataType.TIMESTAMP))
.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))
.add(createNullColumn(SOME_COLUMN_NAME_DATE, DataType.DATE))
.add(createNullColumn(SOME_COLUMN_NAME_TIME, DataType.TIME))
.add(createNullColumn(SOME_COLUMN_NAME_TIMESTAMPTZ, DataType.TIMESTAMPTZ));
}

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(createTimestampColumn(SOME_COLUMN_NAME_4, SOME_TIMESTAMP_TEXT));
mapper.createArrayNode().add(createColumn(TextColumn.of(SOME_COLUMN_NAME_2, version)));

ObjectNode arguments = mapper.createObjectNode();
arguments.put(NAMESPACE, getFunctionNamespace());
Expand All @@ -248,6 +334,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 @@ -519,9 +613,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(getFunctionNamespace())
Expand All @@ -544,10 +637,72 @@ public void putObject_FunctionArgumentsGiven_ShouldPutRecordToFunctionTable()
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).getTimestamp(SOME_COLUMN_NAME_4)).isEqualTo(SOME_TIMESTAMP_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(0).getDate(SOME_COLUMN_NAME_DATE)).isEqualTo(SOME_DATE_VALUE);
assertThat(results.get(0).getTime(SOME_COLUMN_NAME_TIME)).isEqualTo(SOME_TIME_VALUE);
assertThat(results.get(0).getTimestampTZ(SOME_COLUMN_NAME_TIMESTAMPTZ))
.isEqualTo(SOME_TIMESTAMPTZ_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).getTimestamp(SOME_COLUMN_NAME_4)).isEqualTo(SOME_TIMESTAMP_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);
assertThat(results.get(1).getDate(SOME_COLUMN_NAME_DATE)).isEqualTo(SOME_DATE_VALUE);
assertThat(results.get(1).getTime(SOME_COLUMN_NAME_TIME)).isEqualTo(SOME_TIME_VALUE);
assertThat(results.get(1).getTimestampTZ(SOME_COLUMN_NAME_TIMESTAMPTZ))
.isEqualTo(SOME_TIMESTAMPTZ_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(getFunctionNamespace())
.table(getFunctionTable())
.partitionKey(Key.ofText(SOME_COLUMN_NAME_1, SOME_OBJECT_ID))
.build();

// Act
clientService.executeContract(CONTRACT_PUT, contractArguments, FUNCTION_PUT, 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();
assertThat(results.get(0).isNull(SOME_COLUMN_NAME_DATE)).isTrue();
assertThat(results.get(0).isNull(SOME_COLUMN_NAME_TIME)).isTrue();
assertThat(results.get(0).isNull(SOME_COLUMN_NAME_TIMESTAMPTZ)).isTrue();
} catch (IOException e) {
throw new RuntimeException(e);
}
Expand All @@ -570,7 +725,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 @@ -602,8 +759,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(getFunctionNamespace())
Expand Down Expand Up @@ -648,16 +805,20 @@ public void putObject_FunctionArgumentsGiven_ShouldPutRecordToFunctionTable()
.put(TABLE, getFunctionTable());
functionArguments.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))));
functionArguments.set(
CLUSTERING_KEY,
mapper.createArrayNode().add(createColumn(SOME_COLUMN_NAME_2, SOME_VERSION_ID_0)));
mapper
.createArrayNode()
.add(createColumn(TextColumn.of(SOME_COLUMN_NAME_2, SOME_VERSION_ID_0))));
functionArguments.set(
COLUMNS,
mapper
.createArrayNode()
.add(createColumn(SOME_COLUMN_NAME_3, 0))
.add(createTimestampColumn(SOME_COLUMN_NAME_4, "2024-05-19")));
.add(createColumn(IntColumn.of(SOME_COLUMN_NAME_3, 0)))
.add(createColumn(SOME_COLUMN_NAME_4, DataType.TIMESTAMP, "2024-05-19")));

// Act Assert
assertThatThrownBy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,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 @@ -193,7 +196,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
Loading