This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 1bebd7c6e7 [vortex] Free the intermediate native expressions built for 
a scan (#9841)
1bebd7c6e7 is described below

commit 1bebd7c6e7dd6501fdceca060aa425f4112518ef
Author: jackylee <[email protected]>
AuthorDate: Tue Sep 22 11:40:18 2026 +0800

    [vortex] Free the intermediate native expressions built for a scan (#9841)
---
 .../paimon/format/vortex/VortexReaderFactory.java  | 20 +++++---
 .../paimon/format/vortex/VortexRecordsReader.java  | 25 +++++----
 .../src/main/java/dev/vortex/api/Expression.java   | 10 +++-
 .../dev/vortex/api/ExpressionLifetimeTest.java     | 59 ++++++++++++++++++++++
 4 files changed, 95 insertions(+), 19 deletions(-)

diff --git 
a/paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexReaderFactory.java
 
b/paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexReaderFactory.java
index 6974ac489d..6ffc3bef81 100644
--- 
a/paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexReaderFactory.java
+++ 
b/paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexReaderFactory.java
@@ -56,16 +56,20 @@ public class VortexReaderFactory implements 
FormatReaderFactory {
     @Override
     public FileRecordReader<InternalRow> createReader(Context context) {
         long[] rowIndices = toRowIndices(context.selection());
-        Expression predicate = 
VortexPredicateConverter.toVortexExpression(predicates);
         Pair<Path, Map<String, String>> vortexSpecified =
                 toVortexSpecifiedForReader(context.fileIO(), 
context.filePath());
-        return new VortexRecordsReader(
-                vortexSpecified.getLeft(),
-                dataSchemaRowType,
-                projectedRowType,
-                rowIndices,
-                predicate,
-                vortexSpecified.getRight());
+        // The scan clones the filter it is given, the same way it clones the 
projection, so this
+        // tree is ours to release once the reader's constructor has built the 
scan. One tree is
+        // built per data file, and nothing downstream reaches it again.
+        try (Expression predicate = 
VortexPredicateConverter.toVortexExpression(predicates)) {
+            return new VortexRecordsReader(
+                    vortexSpecified.getLeft(),
+                    dataSchemaRowType,
+                    projectedRowType,
+                    rowIndices,
+                    predicate,
+                    vortexSpecified.getRight());
+        }
     }
 
     @Nullable
diff --git 
a/paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexRecordsReader.java
 
b/paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexRecordsReader.java
index c41ac27e42..8a194f5e18 100644
--- 
a/paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexRecordsReader.java
+++ 
b/paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexRecordsReader.java
@@ -91,17 +91,22 @@ public class VortexRecordsReader implements 
FileRecordReader<InternalRow> {
                             ImmutableScanOptions.builder().ordered(true);
 
                     java.util.List<String> columns = 
physicalReadRowType.getFieldNames();
-                    scanBuilder.projection(
-                            Expression.select(columns.toArray(new String[0]), 
Expression.root()));
-
-                    if (rowIndices != null) {
-                        scanBuilder.selectionIndices(rowIndices);
-                        
scanBuilder.selectionMode(ScanOptions.SelectionMode.INCLUDE);
-                    }
-                    if (predicate != null) {
-                        scanBuilder.filter(predicate);
+                    // The scan clones the projection it is given, so both the 
root and the select
+                    // built here are ours to release once dataSource.scan has 
returned.
+                    try (Expression root = Expression.root();
+                            Expression projection =
+                                    Expression.select(columns.toArray(new 
String[0]), root)) {
+                        scanBuilder.projection(projection);
+
+                        if (rowIndices != null) {
+                            scanBuilder.selectionIndices(rowIndices);
+                            
scanBuilder.selectionMode(ScanOptions.SelectionMode.INCLUDE);
+                        }
+                        if (predicate != null) {
+                            scanBuilder.filter(predicate);
+                        }
+                        this.scan = dataSource.scan(scanBuilder.build());
                     }
-                    this.scan = dataSource.scan(scanBuilder.build());
                 } catch (Exception e) {
                     dataSource.close();
                     throw e;
diff --git 
a/paimon-vortex/paimon-vortex-jni/src/main/java/dev/vortex/api/Expression.java 
b/paimon-vortex/paimon-vortex-jni/src/main/java/dev/vortex/api/Expression.java
index ccff4e4aa2..ceb2a2a41f 100644
--- 
a/paimon-vortex/paimon-vortex-jni/src/main/java/dev/vortex/api/Expression.java
+++ 
b/paimon-vortex/paimon-vortex-jni/src/main/java/dev/vortex/api/Expression.java
@@ -51,7 +51,15 @@ public final class Expression implements AutoCloseable {
 
     public static Expression column(String name) {
         long rootPtr = NativeExpression.root();
-        return new Expression(NativeExpression.getItem(name, rootPtr));
+        try {
+            return new Expression(NativeExpression.getItem(name, rootPtr));
+        } finally {
+            // Vortex builders do not take ownership of their inputs -- see 
the module doc of
+            // vortex-jni/src/expression.rs at 0.73.0 -- so releasing this 
intermediate is our
+            // responsibility, and getItem has already cloned it into the 
expression we return.
+            // It is never wrapped in an Expression, so without this no caller 
can reach it.
+            NativeExpression.free(rootPtr);
+        }
     }
 
     public static Expression select(String[] columns, Expression parent) {
diff --git 
a/paimon-vortex/paimon-vortex-jni/src/test/java/dev/vortex/api/ExpressionLifetimeTest.java
 
b/paimon-vortex/paimon-vortex-jni/src/test/java/dev/vortex/api/ExpressionLifetimeTest.java
new file mode 100644
index 0000000000..af63ceffda
--- /dev/null
+++ 
b/paimon-vortex/paimon-vortex-jni/src/test/java/dev/vortex/api/ExpressionLifetimeTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dev.vortex.api;
+
+import dev.vortex.jni.NativeLoader;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * {@link Expression#column} releases the intermediate root it builds, so that 
pointer is freed
+ * outside any {@link Expression} wrapper. Closing the returned expression 
must therefore free its
+ * own pointer once and never the root's again: a second free of either would 
abort the JVM inside
+ * this test rather than fail it.
+ *
+ * <p>That the child stays valid after the root is freed is covered end to end 
by the round-trip
+ * tests in {@code VortexPredicateConverterTest}, which read rows through a 
scan built from
+ * expressions this reader has already released.
+ */
+class ExpressionLifetimeTest {
+
+    @BeforeAll
+    static void loadNativeLibrary() {
+        // Report a missing library as a test failure rather than an 
ExceptionInInitializerError
+        // from the static initializer of NativeExpression.
+        NativeLoader.loadJni();
+    }
+
+    @Test
+    void closingAColumnFreesItsPointerExactlyOnce() {
+        Expression column = Expression.column("col_a");
+        assertThat(column.nativePointer()).isNotZero();
+
+        column.close();
+        assertThat(column.nativePointer()).isZero();
+
+        // Idempotent: the second close must not reach the native side.
+        column.close();
+        assertThat(column.nativePointer()).isZero();
+    }
+}

Reply via email to