diff --git a/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlPrimitiveType.java b/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlPrimitiveType.java index 9e6aad2f..7a60fbcc 100644 --- a/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlPrimitiveType.java +++ b/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlPrimitiveType.java @@ -12,6 +12,7 @@ import tech.ydb.proto.ValueProtos.Type.PrimitiveTypeId; import tech.ydb.proto.ValueProtos.Value.ValueCase; import tech.ydb.table.values.proto.ProtoValue; +import tech.ydb.yoj.ExperimentalApi; import tech.ydb.yoj.databind.ByteArray; import tech.ydb.yoj.databind.DbType; import tech.ydb.yoj.databind.FieldValueType; @@ -87,6 +88,9 @@ public class YqlPrimitiveType implements YqlType { private static final Setter INSTANT_SECOND_SETTER = (b, v) -> b.setInt64Value(((Instant) v).getEpochSecond()); private static final Setter INSTANT_UINT_SECOND_SETTER = (b, v) -> b.setUint64Value(((Instant) v).getEpochSecond()); private static final Setter TIMESTAMP_SETTER = (b, v) -> b.setUint64Value(ProtoValue.fromTimestamp((Instant) v).getUint64Value()); + private static final Setter TIMESTAMP_SECONDS_SETTER = (b, v) -> b.setUint64Value(ProtoValue.fromTimestamp(((Instant) v).truncatedTo(ChronoUnit.SECONDS)).getUint64Value()); + private static final Setter TIMESTAMP_MILLI_SETTER = (b, v) -> b.setUint64Value(ProtoValue.fromTimestamp(((Instant) v).truncatedTo(ChronoUnit.MILLIS)).getUint64Value()); + private static final Setter DURATION_SETTER = (b, v) -> b.setInt64Value(ProtoValue.fromInterval((Duration) v).getInt64Value()); private static final Setter DURATION_UINT_SETTER = (b, v) -> b.setUint64Value(ProtoValue.fromInterval((Duration) v).getInt64Value()); private static final Setter DURATION_MILLI_SETTER = (b, v) -> b.setInt64Value(((Duration) v).toMillis()); @@ -125,6 +129,8 @@ public class YqlPrimitiveType implements YqlType { private static final Getter INSTANT_SECOND_GETTER = v -> Instant.ofEpochSecond(v.getInt64Value()); private static final Getter INSTANT_UINT_SECOND_GETTER = v -> Instant.ofEpochSecond(v.getUint64Value()); private static final Getter TIMESTAMP_GETTER = ProtoValue::toTimestamp; + private static final Getter TIMESTAMP_SECONDS_GETTER = v -> ProtoValue.toTimestamp(v).truncatedTo(ChronoUnit.SECONDS); + private static final Getter TIMESTAMP_MILLI_GETTER = v -> ProtoValue.toTimestamp(v).truncatedTo(ChronoUnit.MILLIS); private static final Getter DURATION_GETTER = ProtoValue::toInterval; private static final Getter DURATION_UINT_GETTER = v -> Duration.of(v.getUint64Value(), ChronoUnit.MICROS); private static final Getter DURATION_MILLI_GETTER = v -> Duration.ofMillis(v.getInt64Value()); @@ -163,17 +169,21 @@ public class YqlPrimitiveType implements YqlType { registerYqlType(Long.class, PrimitiveTypeId.UINT64, null, false, ULONG_SETTER, ULONG_GETTER); registerYqlType(Float.class, PrimitiveTypeId.FLOAT, null, true, FLOAT_SETTER, FLOAT_GETTER); registerYqlType(Double.class, PrimitiveTypeId.DOUBLE, null, true, DOUBLE_SETTER, DOUBLE_GETTER); + registerYqlType(byte[].class, PrimitiveTypeId.STRING, null, true, BYTES_SETTER, BYTES_GETTER); registerYqlType(ByteArray.class, PrimitiveTypeId.STRING, null, true, BYTE_ARRAY_SETTER, BYTE_ARRAY_GETTER); - registerYqlType(Instant.class, PrimitiveTypeId.INT64, DbTypeQualifier.MILLISECONDS, true, INSTANT_SETTER, INSTANT_GETTER); + + registerYqlType(Instant.class, PrimitiveTypeId.INT64, null, true, INSTANT_SETTER, INSTANT_GETTER); // defaults to millis + registerYqlType(Instant.class, PrimitiveTypeId.INT64, DbTypeQualifier.MILLISECONDS, false, INSTANT_SETTER, INSTANT_GETTER); + registerYqlType(Instant.class, PrimitiveTypeId.UINT64, null, false, INSTANT_UINT_SETTER, INSTANT_UINT_GETTER); // defaults to millis registerYqlType(Instant.class, PrimitiveTypeId.UINT64, DbTypeQualifier.MILLISECONDS, false, INSTANT_UINT_SETTER, INSTANT_UINT_GETTER); registerYqlType(Instant.class, PrimitiveTypeId.INT64, DbTypeQualifier.SECONDS, false, INSTANT_SECOND_SETTER, INSTANT_SECOND_GETTER); registerYqlType(Instant.class, PrimitiveTypeId.UINT64, DbTypeQualifier.SECONDS, false, INSTANT_UINT_SECOND_SETTER, INSTANT_UINT_SECOND_GETTER); registerYqlType(Instant.class, PrimitiveTypeId.TIMESTAMP, null, false, TIMESTAMP_SETTER, TIMESTAMP_GETTER); - // XXX Temporarily require an explicit specification - // of the database type for duration fields in order to find possible places of the old use - // of duration in the DB model - registerYqlType(Duration.class, PrimitiveTypeId.INTERVAL, null, false /* XXX true */, DURATION_SETTER, DURATION_GETTER); + registerYqlType(Instant.class, PrimitiveTypeId.TIMESTAMP, DbTypeQualifier.SECONDS, false, TIMESTAMP_SECONDS_SETTER, TIMESTAMP_SECONDS_GETTER); + registerYqlType(Instant.class, PrimitiveTypeId.TIMESTAMP, DbTypeQualifier.MILLISECONDS, false, TIMESTAMP_MILLI_SETTER, TIMESTAMP_MILLI_GETTER); + + registerYqlType(Duration.class, PrimitiveTypeId.INTERVAL, null, true, DURATION_SETTER, DURATION_GETTER); registerYqlType(Duration.class, PrimitiveTypeId.INT64, null, false, DURATION_SETTER, DURATION_GETTER); registerYqlType(Duration.class, PrimitiveTypeId.UINT64, null, false, DURATION_UINT_SETTER, DURATION_UINT_GETTER); registerYqlType(Duration.class, PrimitiveTypeId.INT64, DbTypeQualifier.MILLISECONDS, false, DURATION_MILLI_SETTER, DURATION_MILLI_GETTER); @@ -321,14 +331,70 @@ private static void registerYqlType( } } + /** + * @deprecated Call {@link #useRecommendedMappingFor(FieldValueType[]) useNewMappingFor(STRING, ENUM)} instead. + */ + @Deprecated(forRemoval = true) public static void changeStringDefaultTypeToUtf8() { - VALUE_DEFAULT_YQL_TYPES.put(FieldValueType.STRING, new ValueYqlTypeSelector(FieldValueType.STRING, PrimitiveTypeId.UTF8, null)); - VALUE_DEFAULT_YQL_TYPES.put(FieldValueType.ENUM, new ValueYqlTypeSelector(FieldValueType.ENUM, PrimitiveTypeId.UTF8, null)); + useRecommendedMappingFor(FieldValueType.STRING, FieldValueType.ENUM); } + /** + * @deprecated This method has a misleading name. Call {@link #useLegacyMappingFor(FieldValueType[]) useLegacyMappingFor(STRING, ENUM)} instead. + */ + @Deprecated(forRemoval = true) public static void resetStringDefaultTypeToDefaults() { - VALUE_DEFAULT_YQL_TYPES.put(FieldValueType.STRING, new ValueYqlTypeSelector(FieldValueType.STRING, PrimitiveTypeId.STRING, null)); - VALUE_DEFAULT_YQL_TYPES.put(FieldValueType.ENUM, new ValueYqlTypeSelector(FieldValueType.ENUM, PrimitiveTypeId.STRING, null)); + useLegacyMappingFor(FieldValueType.STRING, FieldValueType.ENUM); + } + + /** + * Uses the legacy (YOJ 1.0.x) field value type ↔ YDB column type mapping for the specified field value type(s). + * If you need to support legacy applications, call {@code useLegacyMappingFor(STRING, ENUM, TIMESTAMP)} before using + * any YOJ features. + * + * @deprecated We STRONGLY advise against using the legacy mapping in new projects. + * Please call {@link #useRecommendedMappingFor(FieldValueType...) useNewMappingFor(STRING, ENUM, TIMESTAMP)} instead, + * and annotate custom-mapped columns with {@link Column @Column} where a different mapping is desired. + * + * @param fieldValueTypes field value types to use legacy mapping for + */ + @Deprecated + public static void useLegacyMappingFor(FieldValueType... fieldValueTypes) { + for (var fvt : fieldValueTypes) { + switch (fvt) { + case STRING, ENUM -> VALUE_DEFAULT_YQL_TYPES.put(fvt, new ValueYqlTypeSelector(fvt, PrimitiveTypeId.STRING, null)); + case TIMESTAMP -> { + var selector = new YqlTypeSelector(Instant.class, PrimitiveTypeId.INT64, null); + JAVA_DEFAULT_YQL_TYPES.put(Instant.class, selector); + YQL_TYPES.put(selector, new YqlPrimitiveType(Instant.class, PrimitiveTypeId.INT64, INSTANT_SETTER, INSTANT_GETTER)); + } + default -> throw new IllegalArgumentException("There is no legacy mapping for field value type: " + fvt); + } + } + } + + /** + * Uses the recommended field value type ↔ YDB column type mapping for the specified field value type(s). + *

+ * In new projects, we STRONGLY advise that you call {@code useNewMappingFor(STRING, ENUM, TIMESTAMP)} + * before using any YOJ features. This will eventually become the default mapping, and the call will become a no-op and + * mighe even be removed. + * + * @param fieldValueTypes field value types to use the new mapping for + */ + @ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/20") + public static void useRecommendedMappingFor(FieldValueType... fieldValueTypes) { + for (var fvt : fieldValueTypes) { + switch (fvt) { + case STRING, ENUM -> VALUE_DEFAULT_YQL_TYPES.put(fvt, new ValueYqlTypeSelector(fvt, PrimitiveTypeId.UTF8, null)); + case TIMESTAMP -> { + var selector = new YqlTypeSelector(Instant.class, PrimitiveTypeId.TIMESTAMP, null); + JAVA_DEFAULT_YQL_TYPES.put(Instant.class, selector); + YQL_TYPES.put(selector, new YqlPrimitiveType(Instant.class, PrimitiveTypeId.TIMESTAMP, TIMESTAMP_SETTER, TIMESTAMP_GETTER)); + } + default -> throw new IllegalArgumentException("There is no new mapping for field value type: " + fvt); + } + } } /** diff --git a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesTest.java b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesLegacyMappingTest.java similarity index 70% rename from repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesTest.java rename to repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesLegacyMappingTest.java index e32c41fa..c560f516 100644 --- a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesTest.java +++ b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesLegacyMappingTest.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.AllArgsConstructor; -import lombok.SneakyThrows; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -18,7 +18,6 @@ import tech.ydb.yoj.repository.db.common.CommonConverters; import tech.ydb.yoj.repository.db.json.JacksonJsonConverter; -import java.lang.reflect.Type; import java.time.Duration; import java.time.Instant; import java.util.Collection; @@ -27,10 +26,12 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assume.assumeTrue; +import static tech.ydb.yoj.databind.FieldValueType.ENUM; +import static tech.ydb.yoj.databind.FieldValueType.STRING; +import static tech.ydb.yoj.databind.FieldValueType.TIMESTAMP; @RunWith(Parameterized.class) -public class YqlTypeAllTypesTest { +public class YqlTypeAllTypesLegacyMappingTest { static { FieldValueType.registerStringValueType(UUID.class); CommonConverters.defineJsonConverter(JacksonJsonConverter.getDefault()); @@ -39,6 +40,11 @@ public class YqlTypeAllTypesTest { private static final Map OBJECT_VALUE = Map.of("string", "Unnamed", "number", 11, "boolean", true); private static final TestSchema SCHEMA = new TestSchema(TestFields.class); + @BeforeClass + public static void setUp() { + YqlPrimitiveType.useLegacyMappingFor(STRING, ENUM, TIMESTAMP); + } + @Parameters public static Collection data() { ObjectNode objectNode = JsonNodeFactory.instance.objectNode(); @@ -50,91 +56,89 @@ public static Collection data() { UUID uuid = UUID.fromString("faefa9bc-eabc-4b90-a973-f25e6cd91e63"); return List.of(new Object[][]{ - {"fieldBoolean", "Bool", true, true}, - {"fieldBooleanBool", "Bool", true, false}, - {"fieldPrimitiveBoolean", "Bool", true, false}, - {"fieldPrimitiveBooleanBool", "Bool", true, false}, - {"fieldByte", "Int32", (byte) 17, true}, - {"fieldByteInt32", "Int32", (byte) 17, false}, - {"fieldByteUint8", "Uint8", (byte) 17, false}, - {"fieldPrimitiveByte", "Int32", (byte) 17, true}, - {"fieldPrimitiveByteInt32", "Int32", (byte) 17, false}, - {"fieldPrimitiveByteUint8", "Uint8", (byte) 17, false}, - {"fieldShort", "Int32", (short) 1723, true}, - {"fieldShortInt32", "Int32", (short) 1723, false}, - {"fieldPrimitiveShort", "Int32", (short) 1723, true}, - {"fieldPrimitiveShortInt32", "Int32", (short) 1723, false}, - {"fieldInteger", "Int32", 42, true}, - {"fieldIntegerInt32", "Int32", 42, false}, - {"fieldIntegerUint32", "Uint32", 42, false}, - {"fieldIntegerUint8", "Uint8", 42, false}, - {"fieldPrimitiveInteger", "Int32", 42, true}, - {"fieldPrimitiveIntegerInt32", "Int32", 42, false}, - {"fieldPrimitiveIntegerUint32", "Uint32", 42, false}, - {"fieldPrimitiveIntegerUint8", "Uint8", 42, false}, - {"fieldLong", "Int64", 100500L, true}, - {"fieldLongInt64", "Int64", 100500L, false}, - {"fieldLongUint64", "Uint64", 100500L, false}, - {"fieldPrimitiveLong", "Int64", 100500L, true}, - {"fieldPrimitiveLongInt64", "Int64", 100500L, false}, - {"fieldPrimitiveLongUint64", "Uint64", 100500L, false}, - {"fieldFloat", "Float", 17.42f, true}, - {"fieldFloatFloat", "Float", 17.42f, false}, - {"fieldPrimitiveFloat", "Float", 17.42f, true}, - {"fieldPrimitiveFloatFloat", "Float", 17.42f, false}, - {"fieldDouble", "Double", 100500.23, true}, - {"fieldDoubleDouble", "Double", 100500.23, false}, - {"fieldPrimitiveDouble", "Double", 100500.23, true}, - {"fieldPrimitiveDoubleDouble", "Double", 100500.23, false}, - {"fieldString", "String", "There is nothing here.", true}, - {"fieldStringString", "String", "There is nothing here.", false}, - {"fieldStringUtf8", "Utf8", "There is nothing here.", false}, - {"fieldBytes", "String", new byte[]{17, 42, -23}, true}, - {"fieldBytesString", "String", new byte[]{17, 42, -23}, false}, - {"fieldInstant", "Int64", Instant.parse("2020-02-20T11:07:17.00Z"), true}, - {"fieldInstantInt64", "Int64", Instant.parse("2020-02-20T11:07:17.00Z"), false}, - {"fieldInstantUint64", "Uint64", Instant.parse("2020-02-20T11:07:17.00Z"), false}, - {"fieldInstantSeconds", "Int64", Instant.parse("2020-02-20T11:07:17.00Z"), false}, - {"fieldInstantInt64Seconds", "Int64", Instant.parse("2020-02-20T11:07:17.00Z"), false}, - {"fieldInstantUint64Seconds", "Uint64", Instant.parse("2020-02-20T11:07:17.00Z"), false}, - {"fieldInstantMilliseconds", "Int64", Instant.parse("2020-02-20T11:07:17.00Z"), false}, - {"fieldInstantInt64Milliseconds", "Int64", Instant.parse("2020-02-20T11:07:17.00Z"), false}, - {"fieldInstantUint64Milliseconds", "Uint64", Instant.parse("2020-02-20T11:07:17.00Z"), false}, - {"fieldInstantTimestamp", "Timestamp", Instant.parse("2020-02-20T11:07:17.516000Z"), false}, - // XXX Temporarily require an explicit specification - // of the database type for duration fields in order to find possible places of the old use - // of duration in the DB model - {"fieldDurationInterval", "Interval", Duration.parse("P1DT30M0.000001S"), false}, - {"fieldDurationInt64", "Int64", Duration.parse("-P1DT30M0.000001S"), false}, - {"fieldDurationUint64", "Uint64", Duration.parse("P1DT30M0.000001S"), false}, - {"fieldDurationInt64Milliseconds", "Int64", Duration.parse("-P1DT30M0.001S"), false}, - {"fieldDurationUint64Milliseconds", "Uint64", Duration.parse("P1DT30M0.001S"), false}, - {"fieldDurationInt32", "Int32", Duration.parse("-P1DT30M7S"), false}, - {"fieldDurationUint32", "Uint32", Duration.parse("P1DT30M7S"), false}, - {"fieldDurationUtf8", "Utf8", Duration.parse("-P1DT17H30M7.123456S"), false}, - {"fieldEnum", "String", TestEnum.BLUE, true}, - {"fieldEnumString", "String", TestEnum.BLUE, false}, - {"fieldEnumUtf8", "Utf8", TestEnum.BLUE, false}, - {"fieldEnumWithNameQualifier", "String", TestEnum.BLUE, true}, - {"fieldEnumDbTypeStringWithNameQualifier", "String", TestEnum.BLUE, false}, - {"fieldEnumDbTypeUtf8WithNameQualifier", "Utf8", TestEnum.BLUE, false}, - {"fieldEnumWithEnumToStringAndNameQualifier", "String", TestToStringValueEnum.BLUE, true}, - {"fieldEnumDbTypeStringWithEnumToStringAndNameQualifier", "String", TestToStringValueEnum.BLUE, false}, - {"fieldEnumDbTypeUtf8WithEnumToStringAndNameQualifier", "Utf8", TestToStringValueEnum.BLUE, false}, - {"fieldEnumWithToStringQualifier", "String", TestToStringValueEnum.BLUE, true}, - {"fieldEnumDbTypeStringWithToStringQualifier", "String", TestToStringValueEnum.BLUE, false}, - {"fieldEnumDbTypeUtf8WithToStringQualifier", "Utf8", TestToStringValueEnum.BLUE, false}, - {"fieldObject", "Json", OBJECT_VALUE, true}, - {"fieldObjectUtf8", "Utf8", OBJECT_VALUE, false}, - {"fieldObjectString", "String", OBJECT_VALUE, false}, - {"fieldObjectJson", "Json", OBJECT_VALUE, false}, - {"fieldJsonNode", "Json", objectNode, true}, - {"fieldJsonNodeUtf8", "Utf8", objectNode, false}, - {"fieldJsonNodeString", "String", objectNode, false}, - {"fieldJsonNodeJson", "Json", objectNode, false}, - {"fieldUUID", "String", uuid, false}, - {"fieldUUIDUtf8", "Utf8", uuid, false}, - {"fieldUUIDString", "String", uuid, false}, + {"fieldBoolean", "Bool", true}, + {"fieldBooleanBool", "Bool", true}, + {"fieldPrimitiveBoolean", "Bool", true}, + {"fieldPrimitiveBooleanBool", "Bool", true}, + {"fieldByte", "Int32", (byte) 17}, + {"fieldByteInt32", "Int32", (byte) 17}, + {"fieldByteUint8", "Uint8", (byte) 17}, + {"fieldPrimitiveByte", "Int32", (byte) 17}, + {"fieldPrimitiveByteInt32", "Int32", (byte) 17}, + {"fieldPrimitiveByteUint8", "Uint8", (byte) 17}, + {"fieldShort", "Int32", (short) 1723}, + {"fieldShortInt32", "Int32", (short) 1723}, + {"fieldPrimitiveShort", "Int32", (short) 1723}, + {"fieldPrimitiveShortInt32", "Int32", (short) 1723}, + {"fieldInteger", "Int32", 42}, + {"fieldIntegerInt32", "Int32", 42}, + {"fieldIntegerUint32", "Uint32", 42}, + {"fieldIntegerUint8", "Uint8", 42}, + {"fieldPrimitiveInteger", "Int32", 42}, + {"fieldPrimitiveIntegerInt32", "Int32", 42}, + {"fieldPrimitiveIntegerUint32", "Uint32", 42}, + {"fieldPrimitiveIntegerUint8", "Uint8", 42}, + {"fieldLong", "Int64", 100500L}, + {"fieldLongInt64", "Int64", 100500L}, + {"fieldLongUint64", "Uint64", 100500L}, + {"fieldPrimitiveLong", "Int64", 100500L}, + {"fieldPrimitiveLongInt64", "Int64", 100500L}, + {"fieldPrimitiveLongUint64", "Uint64", 100500L}, + {"fieldFloat", "Float", 17.42f}, + {"fieldFloatFloat", "Float", 17.42f}, + {"fieldPrimitiveFloat", "Float", 17.42f}, + {"fieldPrimitiveFloatFloat", "Float", 17.42f}, + {"fieldDouble", "Double", 100500.23}, + {"fieldDoubleDouble", "Double", 100500.23}, + {"fieldPrimitiveDouble", "Double", 100500.23}, + {"fieldPrimitiveDoubleDouble", "Double", 100500.23}, + {"fieldString", "String", "There is nothing here."}, + {"fieldStringString", "String", "There is nothing here."}, + {"fieldStringUtf8", "Utf8", "There is nothing here."}, + {"fieldBytes", "String", new byte[]{17, 42, -23}}, + {"fieldBytesString", "String", new byte[]{17, 42, -23}}, + {"fieldInstant", "Int64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantInt64", "Int64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantUint64", "Uint64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantSeconds", "Int64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantInt64Seconds", "Int64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantUint64Seconds", "Uint64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantMilliseconds", "Int64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantInt64Milliseconds", "Int64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantUint64Milliseconds", "Uint64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantTimestamp", "Timestamp", Instant.parse("2020-02-20T11:07:17.516000Z")}, + {"fieldDuration", "Interval", Duration.parse("P1DT30M0.000001S")}, + {"fieldDurationInterval", "Interval", Duration.parse("P1DT30M0.000001S")}, + {"fieldDurationInt64", "Int64", Duration.parse("-P1DT30M0.000001S")}, + {"fieldDurationUint64", "Uint64", Duration.parse("P1DT30M0.000001S")}, + {"fieldDurationInt64Milliseconds", "Int64", Duration.parse("-P1DT30M0.001S")}, + {"fieldDurationUint64Milliseconds", "Uint64", Duration.parse("P1DT30M0.001S")}, + {"fieldDurationInt32", "Int32", Duration.parse("-P1DT30M7S")}, + {"fieldDurationUint32", "Uint32", Duration.parse("P1DT30M7S")}, + {"fieldDurationUtf8", "Utf8", Duration.parse("-P1DT17H30M7.123456S")}, + {"fieldEnum", "String", TestEnum.BLUE}, + {"fieldEnumString", "String", TestEnum.BLUE}, + {"fieldEnumUtf8", "Utf8", TestEnum.BLUE}, + {"fieldEnumWithNameQualifier", "String", TestEnum.BLUE}, + {"fieldEnumDbTypeStringWithNameQualifier", "String", TestEnum.BLUE}, + {"fieldEnumDbTypeUtf8WithNameQualifier", "Utf8", TestEnum.BLUE}, + {"fieldEnumWithEnumToStringAndNameQualifier", "String", TestToStringValueEnum.BLUE}, + {"fieldEnumDbTypeStringWithEnumToStringAndNameQualifier", "String", TestToStringValueEnum.BLUE}, + {"fieldEnumDbTypeUtf8WithEnumToStringAndNameQualifier", "Utf8", TestToStringValueEnum.BLUE}, + {"fieldEnumWithToStringQualifier", "String", TestToStringValueEnum.BLUE}, + {"fieldEnumDbTypeStringWithToStringQualifier", "String", TestToStringValueEnum.BLUE}, + {"fieldEnumDbTypeUtf8WithToStringQualifier", "Utf8", TestToStringValueEnum.BLUE}, + {"fieldObject", "Json", OBJECT_VALUE}, + {"fieldObjectUtf8", "Utf8", OBJECT_VALUE}, + {"fieldObjectString", "String", OBJECT_VALUE}, + {"fieldObjectJson", "Json", OBJECT_VALUE}, + {"fieldJsonNode", "Json", objectNode}, + {"fieldJsonNodeUtf8", "Utf8", objectNode}, + {"fieldJsonNodeString", "String", objectNode}, + {"fieldJsonNodeJson", "Json", objectNode}, + {"fieldUUID", "String", uuid}, + {"fieldUUIDUtf8", "Utf8", uuid}, + {"fieldUUIDString", "String", uuid}, }); } @@ -144,25 +148,13 @@ public static Collection data() { public String dbType; @Parameter(2) public Object value; - @Parameter(3) - public boolean testOfTypeMethod; @Test - public void testOfJavaFiled() { - var yqlType = YqlType.of(SCHEMA.getField(fieldName)); + public void testOfJavaField() { + var javaField = SCHEMA.getField(fieldName); + var yqlType = YqlType.of(javaField); - assertThat(yqlType.getJavaType()).isEqualTo(getTestFieldType(fieldName)); - assertThat(yqlType.getYqlTypeName()).isEqualTo(dbType); - } - - @Test - public void testOfType() { - assumeTrue(testOfTypeMethod); - - Type type = getTestFieldType(fieldName); - var yqlType = YqlType.of(type); - - assertThat(yqlType.getJavaType()).isEqualTo(type); + assertThat(yqlType.getJavaType()).isEqualTo(javaField.getType()); assertThat(yqlType.getYqlTypeName()).isEqualTo(dbType); } @@ -175,11 +167,6 @@ public void testToFrom() { assertThat(actual).isEqualTo(value); } - @SneakyThrows(NoSuchFieldException.class) - private static Type getTestFieldType(String fieldName) { - return TestFields.class.getDeclaredField(fieldName).getGenericType(); - } - @AllArgsConstructor public static class TestFields { @Column diff --git a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesRecommendedMappingTest.java b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesRecommendedMappingTest.java new file mode 100644 index 00000000..3d0821d3 --- /dev/null +++ b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesRecommendedMappingTest.java @@ -0,0 +1,395 @@ +package tech.ydb.yoj.repository.ydb.yql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.AllArgsConstructor; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; +import tech.ydb.yoj.databind.DbType; +import tech.ydb.yoj.databind.FieldValueType; +import tech.ydb.yoj.databind.schema.Column; +import tech.ydb.yoj.databind.schema.Schema; +import tech.ydb.yoj.repository.DbTypeQualifier; +import tech.ydb.yoj.repository.db.common.CommonConverters; +import tech.ydb.yoj.repository.db.json.JacksonJsonConverter; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static tech.ydb.yoj.databind.FieldValueType.ENUM; +import static tech.ydb.yoj.databind.FieldValueType.STRING; +import static tech.ydb.yoj.databind.FieldValueType.TIMESTAMP; + +@RunWith(Parameterized.class) +public class YqlTypeAllTypesRecommendedMappingTest { + static { + FieldValueType.registerStringValueType(UUID.class); + CommonConverters.defineJsonConverter(JacksonJsonConverter.getDefault()); + } + + private static final Map OBJECT_VALUE = Map.of("string", "Unnamed", "number", 11, "boolean", true); + private static final TestSchema SCHEMA = new TestSchema(TestFields.class); + + @BeforeClass + public static void setUp() { + YqlPrimitiveType.useRecommendedMappingFor(STRING, ENUM, TIMESTAMP); + } + + @AfterClass + public static void tearDown() { + YqlPrimitiveType.useLegacyMappingFor(STRING, ENUM, TIMESTAMP); + } + + @Parameters + public static Collection data() { + ObjectNode objectNode = JsonNodeFactory.instance.objectNode(); + + objectNode.put("integer", 17).put("boolean", true).put("string", "Nothing").putNull("null"); + objectNode.putArray("array").add(17).add(true).add("Not empty string.").addNull(); + objectNode.putObject("object").put("integer", 17).put("boolean", true).put("string", "Nothing").putNull("null"); + + UUID uuid = UUID.fromString("faefa9bc-eabc-4b90-a973-f25e6cd91e63"); + + return List.of(new Object[][]{ + {"fieldBoolean", "Bool", true}, + {"fieldBooleanBool", "Bool", true}, + {"fieldPrimitiveBoolean", "Bool", true}, + {"fieldPrimitiveBooleanBool", "Bool", true}, + {"fieldByte", "Int32", (byte) 17}, + {"fieldByteInt32", "Int32", (byte) 17}, + {"fieldByteUint8", "Uint8", (byte) 17}, + {"fieldPrimitiveByte", "Int32", (byte) 17}, + {"fieldPrimitiveByteInt32", "Int32", (byte) 17}, + {"fieldPrimitiveByteUint8", "Uint8", (byte) 17}, + {"fieldShort", "Int32", (short) 1723}, + {"fieldShortInt32", "Int32", (short) 1723}, + {"fieldPrimitiveShort", "Int32", (short) 1723}, + {"fieldPrimitiveShortInt32", "Int32", (short) 1723}, + {"fieldInteger", "Int32", 42}, + {"fieldIntegerInt32", "Int32", 42}, + {"fieldIntegerUint32", "Uint32", 42}, + {"fieldIntegerUint8", "Uint8", 42}, + {"fieldPrimitiveInteger", "Int32", 42}, + {"fieldPrimitiveIntegerInt32", "Int32", 42}, + {"fieldPrimitiveIntegerUint32", "Uint32", 42}, + {"fieldPrimitiveIntegerUint8", "Uint8", 42}, + {"fieldLong", "Int64", 100500L}, + {"fieldLongInt64", "Int64", 100500L}, + {"fieldLongUint64", "Uint64", 100500L}, + {"fieldPrimitiveLong", "Int64", 100500L}, + {"fieldPrimitiveLongInt64", "Int64", 100500L}, + {"fieldPrimitiveLongUint64", "Uint64", 100500L}, + {"fieldFloat", "Float", 17.42f}, + {"fieldFloatFloat", "Float", 17.42f}, + {"fieldPrimitiveFloat", "Float", 17.42f}, + {"fieldPrimitiveFloatFloat", "Float", 17.42f}, + {"fieldDouble", "Double", 100500.23}, + {"fieldDoubleDouble", "Double", 100500.23}, + {"fieldPrimitiveDouble", "Double", 100500.23}, + {"fieldPrimitiveDoubleDouble", "Double", 100500.23}, + {"fieldString", "Utf8", "There is nothing here."}, + {"fieldStringString", "String", "There is nothing here."}, + {"fieldStringUtf8", "Utf8", "There is nothing here."}, + {"fieldBytes", "String", new byte[]{17, 42, -23}}, + {"fieldBytesString", "String", new byte[]{17, 42, -23}}, + {"fieldInstant", "Timestamp", Instant.parse("2024-02-19T18:22:18.459200Z")}, // microsecond accuracy + {"fieldInstantInt64", "Int64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantUint64", "Uint64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantSeconds", "Timestamp", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantInt64Seconds", "Int64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantUint64Seconds", "Uint64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantMilliseconds", "Timestamp", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantInt64Milliseconds", "Int64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantUint64Milliseconds", "Uint64", Instant.parse("2020-02-20T11:07:17.00Z")}, + {"fieldInstantTimestamp", "Timestamp", Instant.parse("2020-02-20T11:07:17.516000Z")}, + {"fieldDuration", "Interval", Duration.parse("P1DT30M0.000001S")}, + {"fieldDurationInterval", "Interval", Duration.parse("P1DT30M0.000001S")}, + {"fieldDurationInt64", "Int64", Duration.parse("-P1DT30M0.000001S")}, + {"fieldDurationUint64", "Uint64", Duration.parse("P1DT30M0.000001S")}, + {"fieldDurationInt64Milliseconds", "Int64", Duration.parse("-P1DT30M0.001S")}, + {"fieldDurationUint64Milliseconds", "Uint64", Duration.parse("P1DT30M0.001S")}, + {"fieldDurationInt32", "Int32", Duration.parse("-P1DT30M7S")}, + {"fieldDurationUint32", "Uint32", Duration.parse("P1DT30M7S")}, + {"fieldDurationUtf8", "Utf8", Duration.parse("-P1DT17H30M7.123456S")}, + {"fieldEnum", "Utf8", TestEnum.BLUE}, + {"fieldEnumString", "String", TestEnum.BLUE}, + {"fieldEnumUtf8", "Utf8", TestEnum.BLUE}, + {"fieldEnumWithNameQualifier", "Utf8", TestEnum.BLUE}, + {"fieldEnumDbTypeStringWithNameQualifier", "String", TestEnum.BLUE}, + {"fieldEnumDbTypeUtf8WithNameQualifier", "Utf8", TestEnum.BLUE}, + {"fieldEnumWithEnumToStringAndNameQualifier", "Utf8", TestToStringValueEnum.BLUE}, + {"fieldEnumDbTypeStringWithEnumToStringAndNameQualifier", "String", TestToStringValueEnum.BLUE}, + {"fieldEnumDbTypeUtf8WithEnumToStringAndNameQualifier", "Utf8", TestToStringValueEnum.BLUE}, + {"fieldEnumWithToStringQualifier", "Utf8", TestToStringValueEnum.BLUE}, + {"fieldEnumDbTypeStringWithToStringQualifier", "String", TestToStringValueEnum.BLUE}, + {"fieldEnumDbTypeUtf8WithToStringQualifier", "Utf8", TestToStringValueEnum.BLUE}, + {"fieldObject", "Json", OBJECT_VALUE}, + {"fieldObjectUtf8", "Utf8", OBJECT_VALUE}, + {"fieldObjectString", "String", OBJECT_VALUE}, + {"fieldObjectJson", "Json", OBJECT_VALUE}, + {"fieldJsonNode", "Json", objectNode}, + {"fieldJsonNodeUtf8", "Utf8", objectNode}, + {"fieldJsonNodeString", "String", objectNode}, + {"fieldJsonNodeJson", "Json", objectNode}, + {"fieldUUID", "Utf8", uuid}, + {"fieldUUIDUtf8", "Utf8", uuid}, + {"fieldUUIDString", "String", uuid}, + }); + } + + @Parameter(0) + public String fieldName; + @Parameter(1) + public String dbType; + @Parameter(2) + public Object value; + + @Test + public void testOfJavaField() { + var javaField = SCHEMA.getField(fieldName); + var yqlType = YqlType.of(javaField); + + assertThat(yqlType.getJavaType()).isEqualTo(javaField.getType()); + assertThat(yqlType.getYqlTypeName()).isEqualTo(dbType); + } + + @Test + public void testToFrom() { + var yqlType = YqlType.of(SCHEMA.getField(fieldName)); + + var actual = yqlType.fromYql(yqlType.toYql(value).build()); + + assertThat(actual).isEqualTo(value); + } + + @AllArgsConstructor + public static class TestFields { + @Column + private Boolean fieldBoolean; + @Column(dbType = DbType.BOOL) + private Boolean fieldBooleanBool; + + @Column + private boolean fieldPrimitiveBoolean; + @Column(dbType = DbType.BOOL) + private boolean fieldPrimitiveBooleanBool; + + @Column + private Byte fieldByte; + @Column(dbType = DbType.INT32) + private Byte fieldByteInt32; + @Column(dbType = DbType.UINT8) + private Byte fieldByteUint8; + + @Column + private byte fieldPrimitiveByte; + @Column(dbType = DbType.INT32) + private byte fieldPrimitiveByteInt32; + @Column(dbType = DbType.UINT8) + private byte fieldPrimitiveByteUint8; + + @Column + private Short fieldShort; + @Column(dbType = DbType.INT32) + private Short fieldShortInt32; + + @Column + private short fieldPrimitiveShort; + @Column(dbType = DbType.INT32) + private short fieldPrimitiveShortInt32; + + @Column + private Integer fieldInteger; + @Column(dbType = DbType.INT32) + private Integer fieldIntegerInt32; + @Column(dbType = DbType.UINT32) + private Integer fieldIntegerUint32; + @Column(dbType = DbType.UINT8) + private Integer fieldIntegerUint8; + + @Column + private int fieldPrimitiveInteger; + @Column(dbType = DbType.INT32) + private int fieldPrimitiveIntegerInt32; + @Column(dbType = DbType.UINT32) + private int fieldPrimitiveIntegerUint32; + @Column(dbType = DbType.UINT8) + private int fieldPrimitiveIntegerUint8; + + @Column + private Long fieldLong; + @Column(dbType = DbType.INT64) + private Long fieldLongInt64; + @Column(dbType = DbType.UINT64) + private Long fieldLongUint64; + + @Column + private long fieldPrimitiveLong; + @Column(dbType = DbType.INT64) + private long fieldPrimitiveLongInt64; + @Column(dbType = DbType.UINT64) + private long fieldPrimitiveLongUint64; + + @Column + private Float fieldFloat; + @Column(dbType = DbType.FLOAT) + private Float fieldFloatFloat; + + @Column + private float fieldPrimitiveFloat; + @Column(dbType = DbType.FLOAT) + private float fieldPrimitiveFloatFloat; + + @Column + private Double fieldDouble; + @Column(dbType = DbType.DOUBLE) + private Double fieldDoubleDouble; + + @Column + private double fieldPrimitiveDouble; + @Column(dbType = DbType.DOUBLE) + private double fieldPrimitiveDoubleDouble; + + @Column + private String fieldString; + @Column(dbType = DbType.STRING) + private String fieldStringString; + @Column(dbType = DbType.UTF8) + private String fieldStringUtf8; + + @Column + private byte[] fieldBytes; + @Column(dbType = DbType.STRING) + private byte[] fieldBytesString; + + @Column + private Instant fieldInstant; + @Column(dbType = DbType.INT64) + private Instant fieldInstantInt64; + @Column(dbType = DbType.UINT64) + private Instant fieldInstantUint64; + @Column(dbTypeQualifier = DbTypeQualifier.SECONDS) + private Instant fieldInstantSeconds; + @Column(dbType = DbType.INT64, dbTypeQualifier = DbTypeQualifier.SECONDS) + private Instant fieldInstantInt64Seconds; + @Column(dbType = DbType.UINT64, dbTypeQualifier = DbTypeQualifier.SECONDS) + private Instant fieldInstantUint64Seconds; + @Column(dbTypeQualifier = DbTypeQualifier.MILLISECONDS) + private Instant fieldInstantMilliseconds; + @Column(dbType = DbType.INT64, dbTypeQualifier = DbTypeQualifier.MILLISECONDS) + private Instant fieldInstantInt64Milliseconds; + @Column(dbType = DbType.UINT64, dbTypeQualifier = DbTypeQualifier.MILLISECONDS) + private Instant fieldInstantUint64Milliseconds; + @Column(dbType = DbType.TIMESTAMP) + private Instant fieldInstantTimestamp; + + private Duration fieldDuration; + @Column(dbType = DbType.INTERVAL) + private Duration fieldDurationInterval; + @Column(dbType = DbType.INT64) + private Duration fieldDurationInt64; + @Column(dbType = DbType.UINT64) + private Duration fieldDurationUint64; + @Column(dbType = DbType.INT64, dbTypeQualifier = DbTypeQualifier.MILLISECONDS) + private Duration fieldDurationInt64Milliseconds; + @Column(dbType = DbType.UINT64, dbTypeQualifier = DbTypeQualifier.MILLISECONDS) + private Duration fieldDurationUint64Milliseconds; + @Column(dbType = DbType.INT32) + private Duration fieldDurationInt32; + @Column(dbType = DbType.UINT32) + private Duration fieldDurationUint32; + @Column(dbType = DbType.UTF8) + private Duration fieldDurationUtf8; + + @Column + private TestEnum fieldEnum; + @Column(dbType = DbType.STRING) + private TestEnum fieldEnumString; + @Column(dbType = DbType.UTF8) + private TestEnum fieldEnumUtf8; + + @Column(dbTypeQualifier = DbTypeQualifier.ENUM_NAME) + private TestEnum fieldEnumWithNameQualifier; + @Column(dbType = DbType.STRING, dbTypeQualifier = DbTypeQualifier.ENUM_NAME) + private TestEnum fieldEnumDbTypeStringWithNameQualifier; + @Column(dbType = DbType.UTF8, dbTypeQualifier = DbTypeQualifier.ENUM_NAME) + private TestEnum fieldEnumDbTypeUtf8WithNameQualifier; + + @Column(dbTypeQualifier = DbTypeQualifier.ENUM_NAME) + private TestToStringValueEnum fieldEnumWithEnumToStringAndNameQualifier; + @Column(dbType = DbType.STRING, dbTypeQualifier = DbTypeQualifier.ENUM_NAME) + private TestToStringValueEnum fieldEnumDbTypeStringWithEnumToStringAndNameQualifier; + @Column(dbType = DbType.UTF8, dbTypeQualifier = DbTypeQualifier.ENUM_NAME) + private TestToStringValueEnum fieldEnumDbTypeUtf8WithEnumToStringAndNameQualifier; + + @Column(dbTypeQualifier = DbTypeQualifier.ENUM_TO_STRING) + private TestToStringValueEnum fieldEnumWithToStringQualifier; + @Column(dbType = DbType.STRING, dbTypeQualifier = DbTypeQualifier.ENUM_TO_STRING) + private TestToStringValueEnum fieldEnumDbTypeStringWithToStringQualifier; + @Column(dbType = DbType.UTF8, dbTypeQualifier = DbTypeQualifier.ENUM_TO_STRING) + private TestToStringValueEnum fieldEnumDbTypeUtf8WithToStringQualifier; + + @Column + private Map fieldObject; + @Column(dbType = DbType.UTF8) + private Map fieldObjectUtf8; + @Column(dbType = DbType.STRING) + private Map fieldObjectString; + @Column(dbType = DbType.JSON) + private Map fieldObjectJson; + + @Column + private JsonNode fieldJsonNode; + @Column(dbType = DbType.UTF8) + private JsonNode fieldJsonNodeUtf8; + @Column(dbType = DbType.STRING) + private JsonNode fieldJsonNodeString; + @Column(dbType = DbType.JSON) + private JsonNode fieldJsonNodeJson; + + @Column + private UUID fieldUUID; + @Column(dbType = DbType.UTF8) + private UUID fieldUUIDUtf8; + @Column(dbType = DbType.STRING) + private UUID fieldUUIDString; + } + + public static class TestSchema extends Schema { + protected TestSchema(Class type) { + super(type); + } + } + + public enum TestEnum { + RED, + BLUE + } + + public enum TestToStringValueEnum { + RED("redValue"), + BLUE("blueValue"); + + private final String value; + + TestToStringValueEnum(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + } +}