Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better YDB Mapping Defaults (opt-in) #29

Merged
merged 5 commits into from
Feb 21, 2024
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
24 changes: 24 additions & 0 deletions databind/src/main/java/tech/ydb/yoj/ExperimentalApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package tech.ydb.yoj;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.SOURCE;

/**
* Annotates <em>experimental features</em>. These features are not part of the stable YOJ API: they can change incompatibly,
* or even disappear entirely <em>in any release</em>.
*/
@Target({TYPE, FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(SOURCE)
public @interface ExperimentalApi {
/**
* @return URL of the GitHub issue tracking the experimental API
*/
String issue();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
format = JSON,
virtualTimestamps = true,
retentionPeriod = "PT1H",
initialScan = true
initialScan = false /* otherwise YDB is "overloaded" during YdbRepositoryIntegrationTest */
)
@Changefeed(name = "test-changefeed2")
public class ChangefeedEntity implements Entity<ChangefeedEntity> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ private Set<String> collectKeyFields(Collection<? extends Entity.Id<T>> ids) {
@Override
protected String declarations() {
var valuesDeclaration = values.keySet().stream()
.map(e -> getDeclaration("$" + e.getPath(), YqlType.of(e.getType()).getYqlTypeName()))
.map(e -> getDeclaration("$" + e.getPath(), YqlType.of(e).getYqlTypeName()))
.collect(joining());

var keysDeclaration = getKeyParams().stream()
Expand Down Expand Up @@ -106,7 +106,7 @@ public List<YqlStatementParam> getParams() {

private List<YqlStatementParam> getValuesParams() {
return this.values.keySet().stream()
.map(x -> new YqlStatementParam(YqlType.of(x.getType()), x.getPath(), false))
.map(x -> new YqlStatementParam(YqlType.of(x), x.getPath(), false))
.collect(Collectors.toList());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -321,17 +331,79 @@ 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 &harr; 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 <strong>STRONGLY</strong> 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 &#64;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 &harr; YDB column type mapping for the specified field value type(s).
* <p>
* In new projects, we <strong>STRONGLY</strong> 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: looks little bit strange: useLegacyMappingFor is deprecated, but useRecommendedMappingFor is experimental. Where is a correct way to set mappings?

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);
}
}
}

/**
* @deprecated Nothing in YOJ calls {@code YqlPrimitiveType.of(Type)} any more.
* <p>Please use {@link #of(JavaField) YqlPrimitiveType.of(JavaField)} because it correcly
* respects the customizations specified in the {@link Column &#64;Column} annotation.
*/
@NonNull
@Deprecated(forRemoval = true)
public static YqlPrimitiveType of(Type javaType) {
return resolveYqlType(javaType, FieldValueType.forJavaType(javaType), null, null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
public interface YqlType {
ValueProtos.Type.Builder getYqlTypeBuilder();

/**
* @deprecated Nothing in YOJ calls {@code YqlType.of(Type)} any more.
* <p>Please use {@link #of(JavaField) YqlType.of(JavaField)} because it correcly
* respects the customizations specified in the {@link Column &#64;Column} annotation.
*/
@NonNull
@Deprecated(forRemoval = true)
static YqlPrimitiveType of(Type javaType) {
return YqlPrimitiveType.of(javaType);
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -36,29 +36,47 @@ public class YqlTypeTest {

@Test
public void fromYqlMustThrowConversionExceptionIfValueIsNonDeserializable() {
record WithNonDeserializableObject(NonDeserializableObject object) {
}

Schema<WithNonDeserializableObject> schema = new Schema<>(WithNonDeserializableObject.class) {
};

assertThatExceptionOfType(ConversionException.class)
.isThrownBy(() ->
YqlType.of(NonDeserializableObject.class).fromYql(ValueProtos.Value.newBuilder()
YqlType.of(schema.getField("object")).fromYql(ValueProtos.Value.newBuilder()
.setTextValue("{}")
.build())
);
}

@Test
public void toYqlMustThrowConversionExceptionIfValueIsNonSerializable() {
record WithNonSerializableObject(NonSerializableObject object) {
}

Schema<WithNonSerializableObject> schema = new Schema<>(WithNonSerializableObject.class) {
};

assertThatExceptionOfType(ConversionException.class)
.isThrownBy(() -> YqlType.of(NonSerializableObject.class).toYql(new NonSerializableObject()));
.isThrownBy(() -> YqlType.of(schema.getField("object")).toYql(new NonSerializableObject()));
}

@Test
public void unknownEnumValuesAreDeserializedAsNull() {
assertThat(YqlType.of(EmptyEnum.class).fromYql(ValueProtos.Value.newBuilder().setTextValue("UZHOS").build()))
record WithEmptyEnum(EmptyEnum emptyEnum) {
}

Schema<WithEmptyEnum> schema = new Schema<>(WithEmptyEnum.class) {
};

assertThat(YqlType.of(schema.getField("emptyEnum")).fromYql(ValueProtos.Value.newBuilder().setTextValue("UZHOS").build()))
.isNull();
}

@Test
public void testSimpleListResponse() {
Schema schema = new Schema(TestResponse.class) {
Schema<TestResponse> schema = new Schema<>(TestResponse.class) {
};
var actual = YqlType.of(schema.getField("simpleListValue")).fromYql(
ValueProtos.Value.newBuilder()
Expand Down
Loading
Loading