zhuxiangyi commented on code in PR #8334:
URL: https://github.com/apache/paimon/pull/8334#discussion_r3795044524


##########
paimon-api/src/main/java/org/apache/paimon/types/RowType.java:
##########
@@ -333,6 +334,166 @@ public RowType project(String... names) {
         return project(Arrays.asList(names));
     }
 
+    /**
+     * Project this row type by a list of (possibly nested) dotted paths, e.g. 
{@code ["f0",
+     * "nest.a"]}. A path without a dot selects the whole top-level field 
(same as {@link
+     * #project(List)}); a dotted path selects only the addressed sub-field of 
a nested {@link
+     * RowType}, preserving field ids and nullability of every level. Fields 
are emitted in the
+     * order the paths are given (exactly like {@link #project(List)}), not in 
schema declaration
+     * order. This is used by data evolution to reconstruct the partial nested 
schema of a
+     * column-group file from its {@code writeCols}.
+     */
+    public RowType projectByPaths(List<String> paths) {
+        return projectTypeByPaths(this, paths);
+    }
+
+    private static RowType projectTypeByPaths(RowType type, List<String> 
paths) {
+        // group paths by their immediate child name, keeping the order in 
which the paths are
+        // given; a child appearing without a tail (or also with a tail) is 
selected as a whole
+        // field
+        Map<String, List<String>> childToSubPaths = new LinkedHashMap<>();
+        Set<String> wholeChildren = new HashSet<>();
+        Map<String, DataField> fieldByName = new LinkedHashMap<>();
+        for (DataField field : type.getFields()) {
+            fieldByName.put(field.name(), field);
+        }
+        for (String path : paths) {
+            int dot = path.indexOf('.');
+            // Prefer an exact field-name match so a column whose name itself 
contains a dot (and
+            // any
+            // plain top-level name) is selected whole; only split into 
head.tail for genuine nested
+            // sub-field paths that do not name a field directly. This keeps 
backward compatibility
+            // with the legacy exact-name project(List).
+            if (dot < 0 || fieldByName.containsKey(path)) {
+                childToSubPaths.computeIfAbsent(path, k -> new ArrayList<>());
+                wholeChildren.add(path);
+            } else {
+                String head = path.substring(0, dot);
+                String tail = path.substring(dot + 1);
+                childToSubPaths.computeIfAbsent(head, k -> new 
ArrayList<>()).add(tail);
+            }
+        }
+
+        // Emit fields in the order the paths were given, exactly like 
project(List). Callers such
+        // as TableSchema.project(writeCols) rebuild the physical layout of a 
data file from its
+        // writeCols, and that order is not necessarily the schema declaration 
order; reordering
+        // here would silently describe the file with the columns permuted.
+        List<DataField> result = new ArrayList<>();
+        for (Map.Entry<String, List<String>> entry : 
childToSubPaths.entrySet()) {
+            String name = entry.getKey();
+            DataField field = fieldByName.get(name);
+            if (field == null) {
+                throw new IllegalArgumentException(
+                        "Cannot project by paths, unknown field '" + name + "' 
in " + type);
+            }
+            List<String> subPaths = entry.getValue();
+            if (wholeChildren.contains(name) || subPaths.isEmpty()) {
+                result.add(field);
+            } else if (field.type() instanceof RowType) {
+                RowType prunedChild =
+                        projectTypeByPaths((RowType) field.type(), subPaths)
+                                .copy(field.type().isNullable());
+                result.add(field.newType(prunedChild));
+            } else {
+                // a dotted path addresses a sub-field, but this field is not 
a ROW; reject rather
+                // than silently selecting the whole field, so invalid dotted 
paths surface early
+                throw new IllegalArgumentException(
+                        "Cannot project sub-field(s) "
+                                + subPaths
+                                + " of non-ROW field '"
+                                + name
+                                + "' in "
+                                + type);
+            }
+        }
+        return new RowType(type.isNullable(), result);
+    }
+
+    /**
+     * Compute the dotted paths describing this (possibly partially nested) 
write type relative to a
+     * full row type. A top-level field, or a nested field whose structure 
fully covers the
+     * corresponding field in {@code fullType}, is emitted by its name; a 
nested field that only
+     * covers some sub-fields is expanded into dotted leaf paths. This is the 
inverse of {@link
+     * #projectByPaths(List)} and is used to derive {@code writeCols}.
+     */
+    public List<String> leafPaths(RowType fullType) {
+        List<String> result = new ArrayList<>();
+        collectLeafPaths(getFields(), fullType, "", result);
+        return result;
+    }
+
+    private static void collectLeafPaths(
+            List<DataField> writeFields, RowType fullType, String prefix, 
List<String> out) {
+        for (DataField writeField : writeFields) {
+            String path = prefix.isEmpty() ? writeField.name() : prefix + "." 
+ writeField.name();
+            // A field absent from the reference type (e.g. the _ROW_ID / 
_SEQUENCE_NUMBER special
+            // fields added by row tracking, which are not part of the table's 
logical row type) has
+            // no sub-field split: emit it whole by name, matching the legacy 
getFieldNames()
+            // output.
+            if (!fullType.containsField(writeField.id())) {
+                out.add(path);
+                continue;
+            }
+            DataField fullField = fullType.getField(writeField.id());
+            boolean willExpand =
+                    writeField.type() instanceof RowType
+                            && fullField.type() instanceof RowType
+                            && !coversFully(
+                                    (RowType) writeField.type(), (RowType) 
fullField.type());
+            // A dotted path is only unambiguous if no name segment contains a 
literal '.'. A name
+            // with a dot is fine when emitted whole at top level 
(projectByPaths matches it
+            // exactly),
+            // but not when it participates in a multi-segment nested path.
+            if (writeField.name().indexOf('.') >= 0 && (!prefix.isEmpty() || 
willExpand)) {
+                throw new UnsupportedOperationException(
+                        "Sub-field-level data evolution does not support a 
nested field whose name "
+                                + "contains '.': "
+                                + path);
+            }
+            if (willExpand) {
+                // A partial struct nested inside another partial struct (a 
path deeper than one
+                // level, e.g. nest.sub.x) cannot be composed back on read — 
the data-evolution read
+                // path only assembles one nested level. Reject it here so 
such a file is never
+                // written/committed and later breaks full-table reads.
+                if (!prefix.isEmpty()) {
+                    throw new UnsupportedOperationException(
+                            "Sub-field-level data evolution supports only one 
level of partial "
+                                    + "nesting; the nested sub-field '"
+                                    + path
+                                    + "' cannot be partially written. Write 
the whole '"
+                                    + path
+                                    + "' sub-field instead.");
+                }
+                collectLeafPaths(
+                        ((RowType) writeField.type()).getFields(),
+                        (RowType) fullField.type(),
+                        path,
+                        out);
+            } else {
+                out.add(path);
+            }
+        }
+    }
+
+    /** Whether {@code part} contains every (recursively nested) field of 
{@code full}. */
+    private static boolean coversFully(RowType part, RowType full) {

Review Comment:
   You're right, thanks. coversFully now compares field ids positionally and 
recurses, so a cct keeps its ordered leaves instead of collapsing to the bare 
column name.
   Note DataTypesTest.testProjectByPathsMatchesLegacyProjectIncludingOrder had 
asserted the p assertion is updated. I also added a reversed-nested-projection 
read test, sinceDataEvolutionSplitRead calls leafPaths for its format cache key.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to