2727import java .util .ArrayList ;
2828import java .util .Arrays ;
2929import java .util .Collection ;
30+ import java .util .HashMap ;
3031import java .util .HashSet ;
3132import java .util .LinkedHashMap ;
3233import java .util .List ;
3536import java .util .Objects ;
3637import java .util .Optional ;
3738import java .util .Set ;
38- import java .util .function .Function ;
3939import java .util .function .Predicate ;
4040import java .util .function .UnaryOperator ;
4141import java .util .regex .Pattern ;
4545import static java .lang .String .format ;
4646import static java .util .Arrays .asList ;
4747import static java .util .stream .Collectors .toList ;
48- import static java .util .stream .Collectors .toMap ;
49- import static lombok .AccessLevel .PROTECTED ;
5048
5149public 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}
0 commit comments