This is an automated email from the ASF dual-hosted git repository.
zhouyuan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git
The following commit(s) were added to refs/heads/main by this push:
new d93ac7418f [GLUTEN-12597][CORE] Migrate TopNRel to Expression count +
FetchMode (Substrait 0.98) (#12728)
d93ac7418f is described below
commit d93ac7418feee88c82354e07bd0961de6b1445cb
Author: Niels Pardon <[email protected]>
AuthorDate: Wed Aug 19 15:27:54 2026 +0200
[GLUTEN-12597][CORE] Migrate TopNRel to Expression count + FetchMode
(Substrait 0.98) (#12728)
---
cpp/velox/substrait/SubstraitParser.cc | 12 ++++
cpp/velox/substrait/SubstraitParser.h | 8 +++
cpp/velox/substrait/SubstraitToVeloxPlan.cc | 13 +++-
.../substrait/SubstraitToVeloxPlanValidator.cc | 23 ++++++-
.../apache/gluten/substrait/rel/RelBuilder.java | 8 +--
.../org/apache/gluten/substrait/rel/TopNNode.java | 10 +++-
.../substrait/proto/substrait/algebra.proto | 32 ++++++++--
.../gluten/substrait/rel/TopNRelProtoSuite.scala | 70 ++++++++++++++++++++++
8 files changed, 164 insertions(+), 12 deletions(-)
diff --git a/cpp/velox/substrait/SubstraitParser.cc
b/cpp/velox/substrait/SubstraitParser.cc
index a57b3f69cc..e31ea8fa4e 100644
--- a/cpp/velox/substrait/SubstraitParser.cc
+++ b/cpp/velox/substrait/SubstraitParser.cc
@@ -16,6 +16,7 @@
*/
#include "SubstraitParser.h"
+#include <limits>
#include "TypeUtils.h"
#include "VeloxSubstraitSignature.h"
#include "velox/common/base/Exceptions.h"
@@ -316,6 +317,17 @@ std::vector<TypePtr> SubstraitParser::sigToTypes(const
std::string& signature) {
return types;
}
+std::optional<int32_t> SubstraitParser::getRowCount(const
::substrait::Expression& expression) {
+ if (!expression.has_literal() || !expression.literal().has_i64()) {
+ return std::nullopt;
+ }
+ const int64_t count = expression.literal().i64();
+ if (count <= 0 || count > std::numeric_limits<int32_t>::max()) {
+ return std::nullopt;
+ }
+ return static_cast<int32_t>(count);
+}
+
template <typename T>
T SubstraitParser::getLiteralValue(const ::substrait::Expression::Literal& /*
literal */) {
VELOX_NYI();
diff --git a/cpp/velox/substrait/SubstraitParser.h
b/cpp/velox/substrait/SubstraitParser.h
index 1122f3dc9b..5097783f9b 100644
--- a/cpp/velox/substrait/SubstraitParser.h
+++ b/cpp/velox/substrait/SubstraitParser.h
@@ -24,6 +24,8 @@
#include <google/protobuf/wrappers.pb.h>
+#include <optional>
+
#include "velox/connectors/hive/TableHandle.h"
#include "velox/type/Type.h"
@@ -103,6 +105,12 @@ class SubstraitParser {
template <typename T>
static T getLiteralValue(const ::substrait::Expression::Literal& /* literal
*/);
+ /// Extract a row count that Substrait models as an Expression, e.g.
TopNRel::count. Velox row
+ /// counts are positive int32 values, so std::nullopt is returned when the
expression is unset,
+ /// null, not an i64 literal, or out of the int32 range. Callers are
expected to reject the plan
+ /// in that case rather than silently substituting a count.
+ static std::optional<int32_t> getRowCount(const ::substrait::Expression&
expression);
+
private:
/// A map used for mapping Substrait function keywords into Velox functions'
/// keywords. Key: the Substrait function keyword, Value: the Velox function
diff --git a/cpp/velox/substrait/SubstraitToVeloxPlan.cc
b/cpp/velox/substrait/SubstraitToVeloxPlan.cc
index e648be8003..2cff687dd2 100644
--- a/cpp/velox/substrait/SubstraitToVeloxPlan.cc
+++ b/cpp/velox/substrait/SubstraitToVeloxPlan.cc
@@ -1401,10 +1401,21 @@ core::PlanNodePtr
SubstraitToVeloxPlanConverter::toVeloxPlan(const ::substrait::
}
core::PlanNodePtr SubstraitToVeloxPlanConverter::toVeloxPlan(const
::substrait::TopNRel& topNRel) {
+ // validate(TopNRel) rejects everything checked below, but native validation
can be turned off
+ // (spark.gluten.sql.enable.native.validation), and this converter is also
reachable from the
+ // JSON-plan test and benchmark paths. Fail loudly rather than executing a
plan whose OFFSET or
+ // WITH TIES semantics Velox's TopN cannot express and would silently drop.
+ VELOX_USER_CHECK(topNRel.mode() == ::substrait::FETCH_MODE_ROWS_ONLY,
"TopNRel only supports FETCH_MODE_ROWS_ONLY.");
+ VELOX_USER_CHECK(!topNRel.has_offset(), "TopNRel does not support an
offset.");
+ // TopNRel models the row limit as an `Expression count`; Gluten's producer
always emits a
+ // positive i64 literal.
+ const auto count = SubstraitParser::getRowCount(topNRel.count());
+ VELOX_USER_CHECK(count.has_value(), "TopNRel count must be an i64 literal in
the range [1, INT32_MAX].");
+
auto childNode = convertSingleInput<::substrait::TopNRel>(topNRel);
auto [sortingKeys, sortingOrders] = processSortField(topNRel.sorts(),
childNode->outputType());
return std::make_shared<core::TopNNode>(
- nextPlanNodeId(), sortingKeys, sortingOrders,
static_cast<int32_t>(topNRel.n()), false /*isPartial*/, childNode);
+ nextPlanNodeId(), sortingKeys, sortingOrders, count.value(), false
/*isPartial*/, childNode);
}
core::PlanNodePtr SubstraitToVeloxPlanConverter::constructValueStreamNode(
diff --git a/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
b/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
index 638fcb4d8d..f1bd84b671 100644
--- a/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
+++ b/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
@@ -515,8 +515,27 @@ bool SubstraitToVeloxPlanValidator::validate(const
::substrait::TopNRel& topNRel
rowType = std::make_shared<RowType>(std::move(names), std::move(types));
}
- if (topNRel.n() < 0) {
- LOG_VALIDATION_MSG("N should be valid in TopNRel.");
+ // Velox's TopN supports neither an OFFSET nor WITH TIES, and its row count
is a positive int32.
+ // Reject anything else here so the query falls back instead of throwing
during plan conversion.
+ // The mode is checked against an allow list: FETCH_MODE_UNSPECIFIED is the
proto3 default and is
+ // not a valid producer choice, and a future mode must not be silently
treated as ROWS_ONLY.
+ if (topNRel.mode() != ::substrait::FETCH_MODE_ROWS_ONLY) {
+ LOG_VALIDATION_MSG("Only FETCH_MODE_ROWS_ONLY is supported in TopNRel.");
+ return false;
+ }
+ if (topNRel.has_offset()) {
+ LOG_VALIDATION_MSG("Offset is not supported in TopNRel.");
+ return false;
+ }
+ if (!SubstraitParser::getRowCount(topNRel.count()).has_value()) {
+ LOG_VALIDATION_MSG("Count should be an i64 literal in the range [1,
INT32_MAX] in TopNRel.");
+ return false;
+ }
+ // Substrait requires at least one sort field, and core::TopNNode asserts on
an empty key list.
+ // The duplicate-key loop below is a no-op for an empty list, so reject here
to fall back instead
+ // of throwing during plan conversion.
+ if (topNRel.sorts_size() == 0) {
+ LOG_VALIDATION_MSG("At least one sort field is required in TopNRel.");
return false;
}
diff --git
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/RelBuilder.java
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/RelBuilder.java
index e7d0640b7d..46dfbb0e28 100644
---
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/RelBuilder.java
+++
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/RelBuilder.java
@@ -285,20 +285,20 @@ public class RelBuilder {
}
public static RelNode makeTopNRel(
- RelNode input, Long n, List<SortField> sorts, SubstraitContext context,
Long operatorId) {
+ RelNode input, Long count, List<SortField> sorts, SubstraitContext
context, Long operatorId) {
context.registerRelToOperator(operatorId);
- return new TopNNode(input, n, sorts);
+ return new TopNNode(input, count, sorts);
}
public static RelNode makeTopNRel(
RelNode input,
- Long n,
+ Long count,
List<SortField> sorts,
AdvancedExtensionNode extensionNode,
SubstraitContext context,
Long operatorId) {
context.registerRelToOperator(operatorId);
- return new TopNNode(input, n, sorts, extensionNode);
+ return new TopNNode(input, count, sorts, extensionNode);
}
public static RelNode makeWindowRel(
diff --git
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/TopNNode.java
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/TopNNode.java
index dbc325674f..c23748b12a 100644
---
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/TopNNode.java
+++
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/TopNNode.java
@@ -16,8 +16,10 @@
*/
package org.apache.gluten.substrait.rel;
+import org.apache.gluten.substrait.expression.ExpressionBuilder;
import org.apache.gluten.substrait.extensions.AdvancedExtensionNode;
+import io.substrait.proto.FetchMode;
import io.substrait.proto.Rel;
import io.substrait.proto.RelCommon;
import io.substrait.proto.SortField;
@@ -55,12 +57,18 @@ public class TopNNode implements RelNode, Serializable {
relCommonBuilder.setDirect(RelCommon.Direct.newBuilder());
TopNRel.Builder topNBuilder = TopNRel.newBuilder();
+ topNBuilder.setCommon(relCommonBuilder.build());
if (input != null) {
topNBuilder.setInput(input.toProtobuf());
}
- topNBuilder.setN(count);
+ // Substrait's TopNRel expresses the row limit as an `Expression count`;
the only Gluten
+ // producer (TakeOrderedAndProject) supplies a literal Long, so wrap it as
an i64 literal.
+ // FetchMode has no meaningful proto3 default (FETCH_MODE_UNSPECIFIED is
not a valid producer
+ // choice), so set it explicitly; Gluten never emits WITH TIES.
+
topNBuilder.setCount(ExpressionBuilder.makeLongLiteral(count).toProtobuf());
+ topNBuilder.setMode(FetchMode.FETCH_MODE_ROWS_ONLY);
for (int i = 0; i < sorts.size(); i++) {
topNBuilder.addSorts(i, sorts.get(i));
diff --git
a/gluten-substrait/src/main/resources/substrait/proto/substrait/algebra.proto
b/gluten-substrait/src/main/resources/substrait/proto/substrait/algebra.proto
index 0015182f29..3d906a4e24 100644
---
a/gluten-substrait/src/main/resources/substrait/proto/substrait/algebra.proto
+++
b/gluten-substrait/src/main/resources/substrait/proto/substrait/algebra.proto
@@ -351,13 +351,37 @@ message FetchRel {
substrait.extensions.AdvancedExtension advanced_extension = 10;
}
-// The relational operator representing TOP N calculation
+// The top-N relational operator. A combination of a sort and fetch that only
+// maintains the number of records required to ensure a limited output.
message TopNRel {
RelCommon common = 1;
Rel input = 2;
- int64 n = 3;
- repeated SortField sorts = 4;
- substrait.extensions.AdvancedExtension advanced_extension = 10;
+
+ // List of one or more fields to sort by. At least one is required.
+ repeated SortField sorts = 3;
+
+ // Expression evaluating to a non-negative integer specifying the number of
+ // records to skip. Null is treated as 0. Recommended type is int64.
+ Expression offset = 4;
+
+ // Expression evaluating to a non-negative integer specifying the number of
+ // records to return. Null signals ALL. Recommended type is int64.
+ Expression count = 5;
+
+ // Determines how to handle rows tied with the last returned row.
+ FetchMode mode = 6;
+
+ substrait.extensions.AdvancedExtension advanced_extension = 7;
+}
+
+// Determines how a fetch operation handles rows tied with the last returned
row.
+enum FetchMode {
+ // Unspecified. Producers must set one of the defined modes.
+ FETCH_MODE_UNSPECIFIED = 0;
+ // Only return the requested number of rows.
+ FETCH_MODE_ROWS_ONLY = 1;
+ // Include additional rows tied with the last row (per the sort fields).
+ FETCH_MODE_WITH_TIES = 2;
}
// The relational operator representing a GROUP BY Aggregate
diff --git
a/gluten-substrait/src/test/scala/org/apache/gluten/substrait/rel/TopNRelProtoSuite.scala
b/gluten-substrait/src/test/scala/org/apache/gluten/substrait/rel/TopNRelProtoSuite.scala
new file mode 100644
index 0000000000..448364b85e
--- /dev/null
+++
b/gluten-substrait/src/test/scala/org/apache/gluten/substrait/rel/TopNRelProtoSuite.scala
@@ -0,0 +1,70 @@
+/*
+ * 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 org.apache.gluten.substrait.rel
+
+import org.apache.gluten.substrait.SubstraitContext
+
+import io.substrait.proto.{FetchMode, SortField, TopNRel}
+import org.scalatest.funsuite.AnyFunSuite
+
+import java.util.Collections
+
+/**
+ * Locks the TopNRel producer contract after adopting upstream Substrait's
`TopNRel`. Gluten's
+ * vendored copy previously carried a local `int64 n = 3` field with `sorts =
4`. Upstream instead
+ * models the limit as `Expression count = 5`, alongside `Expression offset =
4` and
+ * `FetchMode mode = 6`, and puts `sorts` on tag 3. Gluten's only TopNRel
producer (Spark
+ * `TakeOrderedAndProject`) supplies a literal `Long` limit with no offset and
no ties, so the
+ * producer wraps the limit into an i64-literal `Expression`, sets
`FETCH_MODE_ROWS_ONLY` explicitly
+ * (`FETCH_MODE_UNSPECIFIED` is merely the proto3 default and not a valid
producer choice), and
+ * leaves `offset` unset. These assertions pin that contract, none of which a
plain compile catches.
+ */
+class TopNRelProtoSuite extends AnyFunSuite {
+
+ test("makeTopNRel emits count as an i64 literal expression with ROWS_ONLY
mode and no offset") {
+ val context = new SubstraitContext
+ val sorts = Collections.singletonList(SortField.getDefaultInstance)
+ val rel = RelBuilder.makeTopNRel(null, 10L, sorts, context, 0L)
+ val topNRel = rel.toProtobuf.getTopN
+
+ // The limit travels as an i64-literal `Expression`, not a scalar field.
+ assert(topNRel.hasCount)
+ assert(topNRel.getCount.getLiteral.getI64 === 10L)
+ // Fetch mode must be set explicitly rather than left at the proto3
default.
+ assert(topNRel.getMode === FetchMode.FETCH_MODE_ROWS_ONLY)
+ // Gluten never emits an offset (an unset offset is treated as 0).
+ assert(!topNRel.hasOffset)
+ assert(topNRel.getSortsCount === 1)
+ // RelCommon carries the direct output mapping, as it does for every other
rel node.
+ assert(topNRel.hasCommon)
+ assert(topNRel.getCommon.hasDirect)
+ }
+
+ test("TopNRel field numbers match upstream Substrait") {
+ // A round trip through the generated class cannot detect a renumber,
because producer and
+ // consumer share one schema. Assert on the descriptor so the wire tags
are actually pinned:
+ // the native backend and any other Substrait consumer decode by field
number, not by name.
+ val descriptor = TopNRel.getDescriptor
+ assert(descriptor.findFieldByName("sorts").getNumber === 3)
+ assert(descriptor.findFieldByName("offset").getNumber === 4)
+ assert(descriptor.findFieldByName("count").getNumber === 5)
+ assert(descriptor.findFieldByName("mode").getNumber === 6)
+ assert(descriptor.findFieldByName("advanced_extension").getNumber === 7)
+ // The Gluten-local `int64 n` field is gone; tag 3 now carries `sorts`.
+ assert(descriptor.findFieldByName("n") === null)
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]