Skip to content

Commit 41728ca

Browse files
authored
Support time-related data types in generic function (#200)
1 parent 8a3a6a0 commit 41728ca

File tree

5 files changed

+295
-13
lines changed

5 files changed

+295
-13
lines changed

common/src/main/java/com/scalar/dl/genericcontracts/object/Constants.java

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
package com.scalar.dl.genericcontracts.object;
22

3+
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
4+
import static java.time.temporal.ChronoField.HOUR_OF_DAY;
5+
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
6+
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
7+
import static java.time.temporal.ChronoField.NANO_OF_SECOND;
8+
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
9+
import static java.time.temporal.ChronoField.YEAR;
10+
11+
import java.time.ZoneOffset;
12+
import java.time.chrono.IsoChronology;
13+
import java.time.format.DateTimeFormatter;
14+
import java.time.format.DateTimeFormatterBuilder;
15+
import java.time.format.ResolverStyle;
16+
import java.time.format.SignStyle;
17+
318
public class Constants {
419

520
// Object authenticity management
@@ -49,5 +64,62 @@ public class Constants {
4964
public static final String COLLECTION_ID_IS_MISSING_OR_INVALID =
5065
"The collection ID is not specified in the arguments or is invalid.";
5166
public static final String INVALID_PUT_MUTABLE_FUNCTION_ARGUMENT_FORMAT =
52-
"The specified format of the PutMutable function argument is invalid.";
67+
"The specified format of the PutToMutableDatabase function argument is invalid.";
68+
69+
/** A formatter for a DATE literal. The format is "YYYY-MM-DD". For example, "2020-03-04". */
70+
public static final DateTimeFormatter DATE_FORMATTER =
71+
new DateTimeFormatterBuilder()
72+
.appendValue(YEAR, 4, 4, SignStyle.NEVER)
73+
.appendLiteral('-')
74+
.appendValue(MONTH_OF_YEAR, 2)
75+
.appendLiteral('-')
76+
.appendValue(DAY_OF_MONTH, 2)
77+
.toFormatter()
78+
.withResolverStyle(ResolverStyle.STRICT)
79+
.withChronology(IsoChronology.INSTANCE);
80+
/**
81+
* A formatter for a TIME literal. The format is "HH:MM:SS[.FFFFFF]". For example,
82+
* "12:34:56.123456". The fractional second is optional.
83+
*/
84+
public static final DateTimeFormatter TIME_FORMATTER =
85+
new DateTimeFormatterBuilder()
86+
.appendValue(HOUR_OF_DAY, 2)
87+
.appendLiteral(':')
88+
.appendValue(MINUTE_OF_HOUR, 2)
89+
.optionalStart()
90+
.appendLiteral(':')
91+
.appendValue(SECOND_OF_MINUTE, 2)
92+
.optionalStart()
93+
.appendFraction(NANO_OF_SECOND, 0, 6, true)
94+
.toFormatter()
95+
.withResolverStyle(ResolverStyle.STRICT)
96+
.withChronology(IsoChronology.INSTANCE);
97+
/**
98+
* A formatter for a TIMESTAMP literal. The format is "YYYY-MM-DD HH:MM:SS[.FFF]". For example,
99+
* "2020-03-04 12:34:56.123". The fractional second is optional.
100+
*/
101+
public static final DateTimeFormatter TIMESTAMP_FORMATTER =
102+
new DateTimeFormatterBuilder()
103+
.append(DATE_FORMATTER)
104+
.appendLiteral(' ')
105+
.appendValue(HOUR_OF_DAY, 2)
106+
.appendLiteral(':')
107+
.appendValue(MINUTE_OF_HOUR, 2)
108+
.optionalStart()
109+
.appendLiteral(':')
110+
.appendValue(SECOND_OF_MINUTE, 2)
111+
.optionalStart()
112+
.appendFraction(NANO_OF_SECOND, 0, 3, true)
113+
.toFormatter();
114+
/**
115+
* A formatter for a TIMESTAMPTZ literal. The format is "YYYY-MM-DD HH:MM:SS[.FFF] Z". For
116+
* example, "2020-03-04 12:34:56.123 Z". The fractional second is optional.
117+
*/
118+
public static final DateTimeFormatter TIMESTAMPTZ_FORMATTER =
119+
new DateTimeFormatterBuilder()
120+
.append(TIMESTAMP_FORMATTER)
121+
.appendLiteral(' ')
122+
.appendLiteral('Z')
123+
.toFormatter()
124+
.withZone(ZoneOffset.UTC);
53125
}

generic-contracts/scripts/objects-table-schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"object_id": "TEXT",
1212
"version": "TEXT",
1313
"status": "INT",
14-
"registered_at": "BIGINT"
14+
"registered_at": "TIMESTAMP"
1515
},
1616
"compaction-strategy": "LCS"
1717
}

generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractObjectAndCollectionEndToEndTest.java

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,14 @@
6464
import com.scalar.db.io.Key;
6565
import com.scalar.dl.client.exception.ClientException;
6666
import com.scalar.dl.client.service.GenericContractClientService;
67+
import com.scalar.dl.genericcontracts.object.Constants;
6768
import com.scalar.dl.ledger.error.LedgerError;
6869
import com.scalar.dl.ledger.model.ContractExecutionResult;
6970
import com.scalar.dl.ledger.model.LedgerValidationResult;
7071
import com.scalar.dl.ledger.service.StatusCode;
7172
import com.scalar.dl.ledger.util.JacksonSerDe;
7273
import java.io.IOException;
74+
import java.time.LocalDateTime;
7375
import java.util.HashSet;
7476
import java.util.List;
7577
import java.util.Map;
@@ -90,7 +92,7 @@ public class GenericContractObjectAndCollectionEndToEndTest
9092
private static final String ASSET_AGE = "age";
9193
private static final String ASSET_OUTPUT = "output";
9294
private static final String DATA_TYPE_INT = "INT";
93-
private static final String DATA_TYPE_BIGINT = "BIGINT";
95+
private static final String DATA_TYPE_TIMESTAMP = "TIMESTAMP";
9496
private static final String DATA_TYPE_TEXT = "TEXT";
9597

9698
private static final String PACKAGE_OBJECT = "object";
@@ -138,6 +140,8 @@ public class GenericContractObjectAndCollectionEndToEndTest
138140
private static final String SOME_COLUMN_NAME_2 = "version";
139141
private static final String SOME_COLUMN_NAME_3 = "status";
140142
private static final String SOME_COLUMN_NAME_4 = "registered_at";
143+
private static final String SOME_TIMESTAMP_TEXT = "2021-02-03 05:45:00";
144+
private static final LocalDateTime SOME_TIMESTAMP_VALUE = LocalDateTime.of(2021, 2, 3, 5, 45);
141145
private static final String SOME_COLLECTION_ID = "set";
142146
private static final ArrayNode SOME_DEFAULT_OBJECT_IDS =
143147
mapper.createArrayNode().add("object1").add("object2").add("object3").add("object4");
@@ -241,20 +245,20 @@ private JsonNode createColumn(String name, int value) {
241245
.put(DATA_TYPE, DATA_TYPE_INT);
242246
}
243247

244-
private JsonNode createColumn(String name, long value) {
248+
private JsonNode createColumn(String name, String value) {
245249
return mapper
246250
.createObjectNode()
247251
.put(COLUMN_NAME, name)
248252
.put(VALUE, value)
249-
.put(DATA_TYPE, DATA_TYPE_BIGINT);
253+
.put(DATA_TYPE, DATA_TYPE_TEXT);
250254
}
251255

252-
private JsonNode createColumn(String name, String value) {
256+
private JsonNode createTimestampColumn(String name, String value) {
253257
return mapper
254258
.createObjectNode()
255259
.put(COLUMN_NAME, name)
256260
.put(VALUE, value)
257-
.put(DATA_TYPE, DATA_TYPE_TEXT);
261+
.put(DATA_TYPE, DATA_TYPE_TIMESTAMP);
258262
}
259263

260264
private JsonNode createFunctionArguments(
@@ -267,7 +271,7 @@ private JsonNode createFunctionArguments(
267271
mapper
268272
.createArrayNode()
269273
.add(createColumn(SOME_COLUMN_NAME_3, status))
270-
.add(createColumn(SOME_COLUMN_NAME_4, registeredAt));
274+
.add(createTimestampColumn(SOME_COLUMN_NAME_4, SOME_TIMESTAMP_TEXT));
271275

272276
ObjectNode arguments = mapper.createObjectNode();
273277
arguments.put(NAMESPACE, getFunctionNamespace());
@@ -574,11 +578,11 @@ public void putObject_FunctionArgumentsGiven_ShouldPutRecordToFunctionTable()
574578
assertThat(results.get(0).getText(SOME_COLUMN_NAME_1)).isEqualTo(SOME_OBJECT_ID);
575579
assertThat(results.get(0).getText(SOME_COLUMN_NAME_2)).isEqualTo(SOME_VERSION_ID_0);
576580
assertThat(results.get(0).getInt(SOME_COLUMN_NAME_3)).isEqualTo(0);
577-
assertThat(results.get(0).getBigInt(SOME_COLUMN_NAME_4)).isEqualTo(1L);
581+
assertThat(results.get(0).getTimestamp(SOME_COLUMN_NAME_4)).isEqualTo(SOME_TIMESTAMP_VALUE);
578582
assertThat(results.get(1).getText(SOME_COLUMN_NAME_1)).isEqualTo(SOME_OBJECT_ID);
579583
assertThat(results.get(1).getText(SOME_COLUMN_NAME_2)).isEqualTo(SOME_VERSION_ID_1);
580584
assertThat(results.get(1).getInt(SOME_COLUMN_NAME_3)).isEqualTo(1);
581-
assertThat(results.get(1).getBigInt(SOME_COLUMN_NAME_4)).isEqualTo(1234567890123L);
585+
assertThat(results.get(1).getTimestamp(SOME_COLUMN_NAME_4)).isEqualTo(SOME_TIMESTAMP_VALUE);
582586
} catch (IOException e) {
583587
throw new RuntimeException(e);
584588
}
@@ -662,6 +666,45 @@ public void putObject_FunctionArgumentsGiven_ShouldPutRecordToFunctionTable()
662666
.isEqualTo(StatusCode.CONFLICT);
663667
}
664668

669+
@Test
670+
public void
671+
putObject_FunctionArgumentsWithInvalidTimeRelatedTypeFormatGiven_ShouldThrowClientException() {
672+
// Arrange
673+
JsonNode contractArguments =
674+
mapper
675+
.createObjectNode()
676+
.put(OBJECT_ID, SOME_OBJECT_ID)
677+
.put(HASH_VALUE, SOME_HASH_VALUE_0)
678+
.set(METADATA, SOME_METADATA_0);
679+
ObjectNode functionArguments =
680+
mapper
681+
.createObjectNode()
682+
.put(NAMESPACE, getFunctionNamespace())
683+
.put(TABLE, getFunctionTable());
684+
functionArguments.set(
685+
PARTITION_KEY,
686+
mapper.createArrayNode().add(createColumn(SOME_COLUMN_NAME_1, SOME_OBJECT_ID)));
687+
functionArguments.set(
688+
CLUSTERING_KEY,
689+
mapper.createArrayNode().add(createColumn(SOME_COLUMN_NAME_2, SOME_VERSION_ID_0)));
690+
functionArguments.set(
691+
COLUMNS,
692+
mapper
693+
.createArrayNode()
694+
.add(createColumn(SOME_COLUMN_NAME_3, 0))
695+
.add(createTimestampColumn(SOME_COLUMN_NAME_4, "2024-05-19")));
696+
697+
// Act Assert
698+
assertThatThrownBy(
699+
() ->
700+
clientService.executeContract(
701+
ID_OBJECT_PUT, contractArguments, ID_OBJECT_PUT_MUTABLE, functionArguments))
702+
.isExactlyInstanceOf(ClientException.class)
703+
.hasMessage(Constants.INVALID_PUT_MUTABLE_FUNCTION_ARGUMENT_FORMAT)
704+
.extracting("code")
705+
.isEqualTo(StatusCode.CONTRACT_CONTEXTUAL_ERROR);
706+
}
707+
665708
@Test
666709
public void putObject_InvalidMetadataGiven_ShouldThrowClientException() {
667710
// Arrange

generic-contracts/src/main/java/com/scalar/dl/genericcontracts/object/PutToMutableDatabase.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,24 @@
1313
import com.scalar.db.io.BooleanColumn;
1414
import com.scalar.db.io.Column;
1515
import com.scalar.db.io.DataType;
16+
import com.scalar.db.io.DateColumn;
1617
import com.scalar.db.io.DoubleColumn;
1718
import com.scalar.db.io.FloatColumn;
1819
import com.scalar.db.io.IntColumn;
1920
import com.scalar.db.io.Key;
2021
import com.scalar.db.io.TextColumn;
22+
import com.scalar.db.io.TimeColumn;
23+
import com.scalar.db.io.TimestampColumn;
24+
import com.scalar.db.io.TimestampTZColumn;
2125
import com.scalar.dl.ledger.database.Database;
2226
import com.scalar.dl.ledger.exception.ContractContextException;
2327
import com.scalar.dl.ledger.function.JacksonBasedFunction;
2428
import java.io.IOException;
29+
import java.time.Instant;
30+
import java.time.LocalDate;
31+
import java.time.LocalDateTime;
32+
import java.time.LocalTime;
33+
import java.time.format.DateTimeParseException;
2534
import java.util.ArrayList;
2635
import java.util.List;
2736
import javax.annotation.Nullable;
@@ -193,6 +202,42 @@ private Column<?> getColumn(JsonNode jsonColumn) {
193202
}
194203
}
195204

205+
if (dataType.equals(DataType.DATE)
206+
|| dataType.equals(DataType.TIME)
207+
|| dataType.equals(DataType.TIMESTAMP)
208+
|| dataType.equals(DataType.TIMESTAMPTZ)) {
209+
if (!value.isTextual()) {
210+
throw new ContractContextException(Constants.INVALID_PUT_MUTABLE_FUNCTION_ARGUMENT_FORMAT);
211+
}
212+
return getTimeRelatedColumn(columnName, value.textValue(), dataType);
213+
}
214+
215+
throw new ContractContextException(Constants.INVALID_PUT_MUTABLE_FUNCTION_ARGUMENT_FORMAT);
216+
}
217+
218+
private Column<?> getTimeRelatedColumn(String columnName, String value, DataType dataType) {
219+
try {
220+
if (dataType.equals(DataType.DATE)) {
221+
return DateColumn.of(columnName, LocalDate.parse(value, Constants.DATE_FORMATTER));
222+
}
223+
224+
if (dataType.equals(DataType.TIME)) {
225+
return TimeColumn.of(columnName, LocalTime.parse(value, Constants.TIME_FORMATTER));
226+
}
227+
228+
if (dataType.equals(DataType.TIMESTAMP)) {
229+
return TimestampColumn.of(
230+
columnName, LocalDateTime.parse(value, Constants.TIMESTAMP_FORMATTER));
231+
}
232+
233+
if (dataType.equals(DataType.TIMESTAMPTZ)) {
234+
return TimestampTZColumn.of(
235+
columnName, Constants.TIMESTAMPTZ_FORMATTER.parse(value, Instant::from));
236+
}
237+
} catch (DateTimeParseException e) {
238+
throw new ContractContextException(Constants.INVALID_PUT_MUTABLE_FUNCTION_ARGUMENT_FORMAT);
239+
}
240+
196241
throw new ContractContextException(Constants.INVALID_PUT_MUTABLE_FUNCTION_ARGUMENT_FORMAT);
197242
}
198243

@@ -227,6 +272,22 @@ private Column<?> createNullColumn(String columnName, DataType dataType) {
227272
return BlobColumn.ofNull(columnName);
228273
}
229274

275+
if (dataType.equals(DataType.DATE)) {
276+
return DateColumn.ofNull(columnName);
277+
}
278+
279+
if (dataType.equals(DataType.TIME)) {
280+
return TimeColumn.ofNull(columnName);
281+
}
282+
283+
if (dataType.equals(DataType.TIMESTAMP)) {
284+
return TimestampColumn.ofNull(columnName);
285+
}
286+
287+
if (dataType.equals(DataType.TIMESTAMPTZ)) {
288+
return TimestampTZColumn.ofNull(columnName);
289+
}
290+
230291
throw new ContractContextException(Constants.INVALID_PUT_MUTABLE_FUNCTION_ARGUMENT_FORMAT);
231292
}
232293

@@ -246,6 +307,14 @@ private DataType getDataType(String dataType) {
246307
return DataType.TEXT;
247308
case "BLOB":
248309
return DataType.BLOB;
310+
case "DATE":
311+
return DataType.DATE;
312+
case "TIME":
313+
return DataType.TIME;
314+
case "TIMESTAMP":
315+
return DataType.TIMESTAMP;
316+
case "TIMESTAMPTZ":
317+
return DataType.TIMESTAMPTZ;
249318
default:
250319
throw new ContractContextException(Constants.INVALID_PUT_MUTABLE_FUNCTION_ARGUMENT_FORMAT);
251320
}

0 commit comments

Comments
 (0)