Skip to content

Commit 125edde

Browse files
committed
noticket: Allow to use any wrapper (and not only top-level wrapper/raw unwrapped value) in FieldValue.ofObj() for flat fields
1 parent 17ef7db commit 125edde

File tree

8 files changed

+293
-50
lines changed

8 files changed

+293
-50
lines changed

databind/src/main/java/tech/ydb/yoj/databind/expression/values/FieldValue.java

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import tech.ydb.yoj.databind.CustomValueTypes;
77
import tech.ydb.yoj.databind.FieldValueType;
88
import tech.ydb.yoj.databind.expression.IllegalExpressionException;
9-
import tech.ydb.yoj.databind.schema.ObjectSchema;
9+
import tech.ydb.yoj.databind.schema.Schema;
1010
import tech.ydb.yoj.databind.schema.Schema.JavaField;
11+
import tech.ydb.yoj.databind.schema.naming.NamingStrategy;
1112

1213
import javax.annotation.Nullable;
1314
import java.lang.reflect.Type;
@@ -33,6 +34,8 @@ public sealed interface FieldValue extends tech.ydb.yoj.databind.expression.Fiel
3334

3435
@Override
3536
default Object getRaw(@NonNull JavaField field) {
37+
field = field.isFlat() ? field.toFlatField() : field;
38+
3639
Comparable<?> cmp = getComparable(field);
3740
return CustomValueTypes.postconvert(field, cmp);
3841
}
@@ -49,7 +52,8 @@ default Comparable<?> getComparable(@NonNull JavaField field) {
4952
}
5053

5154
static FieldValue ofObj(@NonNull Object obj, @NonNull JavaField schemaField) {
52-
FieldValueType fvt = FieldValueType.forJavaType(obj.getClass(), schemaField.getField());
55+
Class<?> objRawType = obj.getClass();
56+
FieldValueType fvt = FieldValueType.forJavaType(objRawType, schemaField.getField());
5357
obj = CustomValueTypes.preconvert(schemaField, obj);
5458

5559
return switch (fvt) {
@@ -62,19 +66,37 @@ static FieldValue ofObj(@NonNull Object obj, @NonNull JavaField schemaField) {
6266
case TIMESTAMP -> new TimestampFieldValue((Instant) obj);
6367
case UUID -> new UuidFieldValue((UUID) obj);
6468
case COMPOSITE -> {
65-
ObjectSchema<?> schema = ObjectSchema.of(obj.getClass());
69+
JavaField innerField;
70+
if (schemaField.isFlat()) {
71+
// For flat fields, walk the chain of wrappers to find a wrapper which matches the obj's raw type exactly
72+
innerField = schemaField.getFlatRoot().findFlatChild(child -> child.getRawType().equals(objRawType));
73+
} else {
74+
// For non-flat fields, we just require an exact match to obj's raw type (or else Schema.flatten() will fail!)
75+
innerField = schemaField;
76+
}
77+
Preconditions.checkArgument(innerField != null && innerField.getRawType().equals(objRawType),
78+
"Composite schema field %s is not compatible with value of type %s", schemaField, objRawType);
79+
80+
class InnerSchema<T> extends Schema<T> {
81+
protected InnerSchema(JavaField subSchemaField) {
82+
super(subSchemaField, (NamingStrategy) null);
83+
}
84+
}
85+
86+
Schema<?> schema = new InnerSchema<>(innerField);
6687
List<JavaField> flatFields = schema.flattenFields();
6788

6889
@SuppressWarnings({"rawtypes", "unchecked"})
69-
Map<String, Object> flattenedObj = ((ObjectSchema) schema).flatten(obj);
90+
Map<String, Object> flattenedObj = ((Schema) schema).flatten(obj);
7091

7192
List<Tuple.FieldAndValue> allFieldValues = tupleValues(flatFields, flattenedObj);
7293
if (allFieldValues.size() == 1) {
7394
FieldValue singleValue = allFieldValues.iterator().next().value();
7495
Preconditions.checkArgument(singleValue != null, "Wrappers must have a non-null value inside them");
7596
yield singleValue;
97+
} else {
98+
yield new TupleFieldValue(new Tuple(obj, allFieldValues));
7699
}
77-
yield new TupleFieldValue(new Tuple(obj, allFieldValues));
78100
}
79101
default -> throw new UnsupportedOperationException("Unsupported value type: not a string, integer, timestamp, UUID, enum, "
80102
+ "floating-point number, byte array, tuple or wrapper of the above");

databind/src/main/java/tech/ydb/yoj/databind/schema/Schema.java

Lines changed: 106 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.ArrayList;
2828
import java.util.Arrays;
2929
import java.util.Collection;
30+
import java.util.HashMap;
3031
import java.util.HashSet;
3132
import java.util.LinkedHashMap;
3233
import java.util.List;
@@ -35,7 +36,6 @@
3536
import java.util.Objects;
3637
import java.util.Optional;
3738
import java.util.Set;
38-
import java.util.function.Function;
3939
import java.util.function.Predicate;
4040
import java.util.function.UnaryOperator;
4141
import java.util.regex.Pattern;
@@ -45,15 +45,10 @@
4545
import static java.lang.String.format;
4646
import static java.util.Arrays.asList;
4747
import static java.util.stream.Collectors.toList;
48-
import static java.util.stream.Collectors.toMap;
49-
import static lombok.AccessLevel.PROTECTED;
5048

5149
public abstract class Schema<T> {
5250
public static final String PATH_DELIMITER = ".";
5351

54-
@Getter(PROTECTED)
55-
private final SchemaKey<T> schemaKey;
56-
5752
@Getter
5853
private final List<JavaField> fields;
5954

@@ -69,6 +64,10 @@ public abstract class Schema<T> {
6964

7065
protected final ReflectType<T> reflectType;
7166

67+
private final Class<T> type;
68+
private final NamingStrategy namingStrategy;
69+
70+
@Deprecated
7271
private final String staticName;
7372

7473
protected Schema(@NonNull Class<T> type) {
@@ -88,12 +87,11 @@ protected Schema(@NonNull Class<T> type, @NonNull NamingStrategy namingStrategy,
8887
}
8988

9089
protected Schema(@NonNull SchemaKey<T> key, @NonNull Reflector reflector) {
91-
Class<T> type = key.clazz();
92-
NamingStrategy namingStrategy = key.namingStrategy();
90+
this.type = key.clazz();
91+
this.namingStrategy = key.namingStrategy();
9392

9493
this.reflectType = reflector.reflectRootType(type);
9594

96-
this.schemaKey = key;
9795
this.staticName = namingStrategy.getNameForClass(type);
9896

9997
this.fields = reflectType.getFields().stream().map(this::newRootJavaField).toList();
@@ -106,17 +104,24 @@ protected Schema(@NonNull SchemaKey<T> key, @NonNull Reflector reflector) {
106104
this.ttlModifier = prepareTtlModifier(extractTtlModifier(type));
107105
this.changefeeds = prepareChangefeeds(collectChangefeeds(type));
108106
}
109-
107+
110108
protected Schema(Schema<?> schema, String subSchemaFieldPath) {
111-
JavaField subSchemaField = schema.getField(subSchemaFieldPath);
109+
this(schema.getField(subSchemaFieldPath), schema.getNamingStrategy());
110+
}
112111

112+
protected Schema(JavaField subSchemaField, @Nullable NamingStrategy parentNamingStrategy) {
113113
@SuppressWarnings("unchecked") ReflectType<T> rt = (ReflectType<T>) subSchemaField.field.getReflectType();
114-
this.reflectType = rt;
115114

116-
this.schemaKey = schema.schemaKey.withClazz(reflectType.getRawType());
115+
this.reflectType = rt;
116+
this.type = rt.getRawType();
117+
this.namingStrategy = parentNamingStrategy == null ? SUBFIELD_SCHEMA_NAMING_STRATEGY : parentNamingStrategy;
117118

118-
this.staticName = schema.staticName;
119-
this.globalIndexes = schema.globalIndexes;
119+
// This is a subfield, *NOT* an Entity, so it has no table name, no TTL, no indexes and no changefeeds
120+
// (And also, no useful naming strategy, because all field names have already been assigned by the moment you construct a subfield Schema!)
121+
this.staticName = "";
122+
this.ttlModifier = null;
123+
this.globalIndexes = List.of();
124+
this.changefeeds = List.of();
120125

121126
if (subSchemaField.fields != null) {
122127
this.fields = subSchemaField.fields.stream().map(this::newRootJavaField).toList();
@@ -129,19 +134,22 @@ protected Schema(Schema<?> schema, String subSchemaFieldPath) {
129134
this.fields = List.of();
130135
}
131136
}
132-
this.ttlModifier = schema.ttlModifier;
133-
this.changefeeds = schema.changefeeds;
134137
}
135138

136-
public String getTypeName() {
137-
return getType().getSimpleName();
139+
public final String getTypeName() {
140+
return type.getSimpleName();
138141
}
139142

140143
private void validateFieldNames() {
141-
flattenFields().stream().collect(toMap(JavaField::getName, Function.identity(), ((x, y) -> {
142-
throw new IllegalArgumentException("fields with same name `%s` detected: `{%s}` and `{%s}`"
143-
.formatted(x.getName(), x.getField(), y.getField()));
144-
})));
144+
Map<String, JavaField> fieldNames = new HashMap<>();
145+
for (JavaField field : flattenFields()) {
146+
String fieldName = field.getName();
147+
JavaField existingField = fieldNames.putIfAbsent(fieldName, field);
148+
if (existingField != null) {
149+
throw new IllegalArgumentException("fields with same name \"%s\" detected: {%s} and {%s}"
150+
.formatted(fieldName, field.getField(), existingField.getField()));
151+
}
152+
}
145153
}
146154

147155
private List<Index> prepareIndexes(List<GlobalIndex> indexes) {
@@ -276,23 +284,29 @@ protected boolean isFlattenable(ReflectField field) {
276284
}
277285

278286
public final Class<T> getType() {
279-
return schemaKey.clazz();
287+
return type;
280288
}
281289

282290
public final NamingStrategy getNamingStrategy() {
283-
return schemaKey.namingStrategy();
291+
return namingStrategy;
284292
}
285293

286294
/**
287-
* DEPRECATED: old method, use correct instance of {@link TableDescriptor}
288-
* Returns the name of the table for data binding.
289-
* <p>
290-
* If the {@link Table} annotation is present, the field {@code name} should be used to
291-
* specify the table name.
295+
* @deprecated This method will be pulled down to {@code EntitySchema} in YOJ 3.0.0 or even earlier; and it might be removed in YOJ 3.x.
296+
* <br>YOJ end-users <strong>should never</strong> use this method themselves. To customize table name, just add the {@link Table} annotation to
297+
* an {@code Entity} and specify the desired name in the annotation's {@code name} field. To dynamically choose table name, use
298+
* the {@code BaseDb.table(TableDescriptor)} method inside your transaction.
299+
* <br>
300+
* This method always had somewhat unclear semantics (it was never specified what it returns for anything that's not an {@code EntitySchema})
301+
* and unnecessarily coupled the data-binding model ({@code Schema}s) to database concepts (tables, which have names, and implementation-defined
302+
* syntax for names and paths).
292303
*
293-
* @return the table name for data binding
304+
* @return this {@code Schema}'s "name", as determined by {@code NamingStrategy}. For {@code EntitySchema}, this will be the <em>table name</em>
305+
* that's used if you don't obtain the table with an explicit {@code TableDescriptor}. Other instances of {@code Schema} are not
306+
* guaranteed to return anything meaningful and/or useful from this method, and might return an empty {@code String}
307+
* (but <em>not</em> {@code null}.)
294308
*/
295-
@Deprecated
309+
@Deprecated(forRemoval = true)
296310
public final String getName() {
297311
return staticName;
298312
}
@@ -375,20 +389,15 @@ private Optional<JavaField> findField(String... pathComponents) {
375389

376390
@Override
377391
public final int hashCode() {
378-
return Objects.hashCode(staticName);
392+
return Objects.hash(getClass(), getType(), getNamingStrategy());
379393
}
380394

381395
@Override
382396
public final boolean equals(Object o) {
383-
if (this == o) {
384-
return true;
385-
}
386-
if (o == null || getClass() != o.getClass()) {
387-
return false;
388-
}
389-
390-
Schema<?> other = (Schema<?>) o;
391-
return Objects.equals(staticName, other.staticName);
397+
return o instanceof Schema<?> otherSchema
398+
&& otherSchema.getClass().equals(this.getClass())
399+
&& otherSchema.getType().equals(this.getType())
400+
&& otherSchema.getNamingStrategy().equals(this.getNamingStrategy());
392401
}
393402

394403
@Override
@@ -398,7 +407,9 @@ public final String toString() {
398407
schemaName = getClass().getName();
399408
}
400409

401-
return schemaName + " \"" + staticName + "\" [type=" + getType().getName() + "]";
410+
String staticTableName = staticName.isEmpty() ? "" : " \"" + staticName + "\"";
411+
412+
return schemaName + staticTableName + " [type=" + getTypeName() + "]";
402413
}
403414

404415
private static final class DummyCustomValueSubField implements ReflectField {
@@ -626,6 +637,41 @@ public boolean isFlat() {
626637
return getSimpleFieldCardinality(this) == 1;
627638
}
628639

640+
/**
641+
* @return the uppermost field that contains this flat field and is still {@link #isFlat() flat}; {@code this} if no such flat field exists
642+
* @throws IllegalStateException if this field is not {@link #isFlat() flat}
643+
*/
644+
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/pull/130")
645+
public JavaField getFlatRoot() {
646+
Preconditions.checkState(isFlat(), "Cannot get flat parent for a non-flat field");
647+
648+
JavaField flatRoot = this;
649+
while (flatRoot.parent != null && flatRoot.parent.getChildren().size() == 1) {
650+
flatRoot = flatRoot.parent;
651+
}
652+
return flatRoot;
653+
}
654+
655+
/**
656+
* @param condition the condition for matching the fields
657+
* @return the outermost flat child field that {@code this} field contains (including {@code this} itself!) that matches the {@code condition}
658+
* @throws IllegalStateException if this field is not {@link #isFlat() flat}
659+
*/
660+
@Nullable
661+
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/pull/130")
662+
public JavaField findFlatChild(@NonNull Predicate<JavaField> condition) {
663+
Preconditions.checkState(isFlat(), "Cannot get flat child for a non-flat field");
664+
665+
JavaField current = this;
666+
while (current != null) {
667+
if (condition.test(current)) {
668+
return current;
669+
}
670+
current = current.getChildren().isEmpty() ? null : current.getChildren().get(0);
671+
}
672+
return null;
673+
}
674+
629675
/**
630676
* Determining that a java field is mapped in more than one database field.
631677
*
@@ -843,4 +889,21 @@ public static class Consumer {
843889
boolean important;
844890
}
845891
}
892+
893+
private static final NamingStrategy SUBFIELD_SCHEMA_NAMING_STRATEGY = new NamingStrategy() {
894+
@Override
895+
public String getNameForClass(@NonNull Class<?> entityClass) {
896+
throw new UnsupportedOperationException("Schema.SUBFIELD_SCHEMA_NAMING_STRATEGY.getNameForClass() must never be called");
897+
}
898+
899+
@Override
900+
public void assignFieldName(@NonNull JavaField javaField) {
901+
throw new UnsupportedOperationException("Schema.SUBFIELD_SCHEMA_NAMING_STRATEGY.assignFieldName() must never be called");
902+
}
903+
904+
@Override
905+
public String toString() {
906+
return "Schema.SUBFIELD_SCHEMA_NAMING_STRATEGY";
907+
}
908+
};
846909
}

repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import tech.ydb.yoj.repository.test.sample.model.IndexedEntity;
1919
import tech.ydb.yoj.repository.test.sample.model.LogEntry;
2020
import tech.ydb.yoj.repository.test.sample.model.MultiWrappedEntity;
21+
import tech.ydb.yoj.repository.test.sample.model.MultiWrappedEntity2;
2122
import tech.ydb.yoj.repository.test.sample.model.NetworkAppliance;
2223
import tech.ydb.yoj.repository.test.sample.model.Primitive;
2324
import tech.ydb.yoj.repository.test.sample.model.Project;
@@ -136,6 +137,11 @@ public Table<DetachedEntity> detachedEntities() {
136137
public Table<MultiWrappedEntity> multiWrappedIdEntities() {
137138
return table(MultiWrappedEntity.class);
138139
}
140+
141+
@Override
142+
public Table<MultiWrappedEntity2> multiWrappedEntities2() {
143+
return table(MultiWrappedEntity2.class);
144+
}
139145
}
140146

141147
private static class Supabubble2InMemoryTable extends InMemoryTable<Supabubble2> implements TestEntityOperations.Supabubble2Table {

0 commit comments

Comments
 (0)