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 0e35f95dc8 [GLUTEN-12597][CORE] Migrate WindowRel to 
ConsistentPartitionWindowRel + BoundsType (Substrait 0.98) (#12727)
0e35f95dc8 is described below

commit 0e35f95dc839aa2cbcd48788d830577bde8a8dbd
Author: Niels Pardon <[email protected]>
AuthorDate: Wed Aug 19 15:27:34 2026 +0200

    [GLUTEN-12597][CORE] Migrate WindowRel to ConsistentPartitionWindowRel + 
BoundsType (Substrait 0.98) (#12727)
---
 .../substrait-plans/tpcds-q47-wholestage-9.json    | 84 +++++++++----------
 .../local-engine/Parser/AggregateFunctionParser.h  | 13 +--
 .../Parser/RelParsers/WindowRelParser.cpp          | 54 ++++++------
 .../Parser/RelParsers/WindowRelParser.h            | 10 +--
 cpp/velox/substrait/SubstraitToVeloxPlan.cc        | 41 ++++-----
 cpp/velox/substrait/SubstraitToVeloxPlan.h         |  6 +-
 .../substrait/SubstraitToVeloxPlanValidator.cc     | 28 +++----
 .../substrait/SubstraitToVeloxPlanValidator.h      |  2 +-
 .../substrait/expression/WindowFunctionNode.java   | 30 ++++---
 .../apache/gluten/substrait/rel/WindowRelNode.java |  8 +-
 .../substrait/proto/substrait/algebra.proto        | 69 ++++++++++++----
 .../apache/gluten/utils/WindowRelProtoSuite.scala  | 96 ++++++++++++++++++++++
 12 files changed, 282 insertions(+), 159 deletions(-)

diff --git 
a/backends-clickhouse/src/test/resources/substrait-plans/tpcds-q47-wholestage-9.json
 
b/backends-clickhouse/src/test/resources/substrait-plans/tpcds-q47-wholestage-9.json
index 946cb1e476..cc68e2e45b 100644
--- 
a/backends-clickhouse/src/test/resources/substrait-plans/tpcds-q47-wholestage-9.json
+++ 
b/backends-clickhouse/src/test/resources/substrait-plans/tpcds-q47-wholestage-9.json
@@ -207,23 +207,22 @@
                               }]
                             }
                           },
-                          "measures": [{
-                            "measure": {
-                              "upperBound": {
-                                "currentRow": {
-                                }
-                              },
-                              "lowerBound": {
-                                "unboundedPreceding": {
-                                }
-                              },
-                              "outputType": {
-                                "i32": {
-                                  "nullability": "NULLABILITY_REQUIRED"
-                                }
-                              },
-                              "columnName": "rn_510"
-                            }
+                          "windowFunctions": [{
+                            "upperBound": {
+                              "currentRow": {
+                              }
+                            },
+                            "lowerBound": {
+                              "unbounded": {
+                              }
+                            },
+                            "outputType": {
+                              "i32": {
+                                "nullability": "NULLABILITY_REQUIRED"
+                              }
+                            },
+                            "boundsType": "BOUNDS_TYPE_ROWS",
+                            "columnName": "rn_510"
                           }],
                           "partitionExpressions": [{
                             "selection": {
@@ -345,35 +344,34 @@
                       }
                     }
                   },
-                  "measures": [{
-                    "measure": {
-                      "functionReference": 4,
-                      "upperBound": {
-                        "unboundedFollowing": {
-                        }
-                      },
-                      "lowerBound": {
-                        "unboundedPreceding": {
-                        }
-                      },
-                      "outputType": {
-                        "fp64": {
-                          "nullability": "NULLABILITY_NULLABLE"
-                        }
-                      },
-                      "arguments": [{
-                        "value": {
-                          "selection": {
-                            "directReference": {
-                              "structField": {
-                                "field": 7
-                              }
+                  "windowFunctions": [{
+                    "functionReference": 4,
+                    "upperBound": {
+                      "unbounded": {
+                      }
+                    },
+                    "lowerBound": {
+                      "unbounded": {
+                      }
+                    },
+                    "outputType": {
+                      "fp64": {
+                        "nullability": "NULLABILITY_NULLABLE"
+                      }
+                    },
+                    "arguments": [{
+                      "value": {
+                        "selection": {
+                          "directReference": {
+                            "structField": {
+                              "field": 7
                             }
                           }
                         }
-                      }],
-                      "columnName": "avg_monthly_sales_509"
-                    }
+                      }
+                    }],
+                    "boundsType": "BOUNDS_TYPE_ROWS",
+                    "columnName": "avg_monthly_sales_509"
                   }],
                   "partitionExpressions": [{
                     "selection": {
diff --git a/cpp-ch/local-engine/Parser/AggregateFunctionParser.h 
b/cpp-ch/local-engine/Parser/AggregateFunctionParser.h
index 3c2463bf92..5c8fe59dcf 100644
--- a/cpp-ch/local-engine/Parser/AggregateFunctionParser.h
+++ b/cpp-ch/local-engine/Parser/AggregateFunctionParser.h
@@ -53,13 +53,14 @@ public:
 
         CommonFunctionInfo() { function_ref = -1; }
 
-        CommonFunctionInfo(const substrait::WindowRel::Measure & win_measure)
-            : function_ref(win_measure.measure().function_reference())
-            , arguments(win_measure.measure().arguments())
-            , output_type(win_measure.measure().output_type())
-            , phase(win_measure.measure().phase())
-            , sort_fields(win_measure.measure().sorts())
+        CommonFunctionInfo(const 
substrait::ConsistentPartitionWindowRel::WindowRelFunction & win_function)
+            : function_ref(win_function.function_reference())
+            , arguments(win_function.arguments())
+            , output_type(win_function.output_type())
+            , phase(win_function.phase())
         {
+            // Substrait 0.98's WindowRelFunction has no per-function sorts 
(they are hoisted to the rel);
+            // Gluten always left them empty, so sort_fields stays 
default-constructed.
             is_in_window = true;
             is_aggregate_function = true;
         }
diff --git a/cpp-ch/local-engine/Parser/RelParsers/WindowRelParser.cpp 
b/cpp-ch/local-engine/Parser/RelParsers/WindowRelParser.cpp
index 4fb9feafff..e8baaa7ea5 100644
--- a/cpp-ch/local-engine/Parser/RelParsers/WindowRelParser.cpp
+++ b/cpp-ch/local-engine/Parser/RelParsers/WindowRelParser.cpp
@@ -64,9 +64,8 @@ WindowRelParser::parse(DB::QueryPlanPtr current_plan_, const 
substrait::Rel & re
 
     // The output header is : original columns ++ window columns
     output_header = *current_plan->getCurrentHeader();
-    for (const auto & measure : win_rel_pb.measures())
+    for (const auto & win_function : win_rel_pb.window_functions())
     {
-        const auto & win_function = measure.measure();
         ColumnWithTypeAndName named_col;
         named_col.name = win_function.column_name();
         named_col.type = TypeParser::parseType(win_function.output_type());
@@ -120,8 +119,7 @@ std::unordered_map<String, WindowDescription> 
WindowRelParser::parseWindowDescri
     for (size_t i = 0; i < win_infos.size(); ++i)
     {
         auto & win_info = win_infos[i];
-        const auto & measure = *win_info.measure;
-        const auto & win_function = measure.measure();
+        const auto & win_function = *win_info.window_function;
         auto win_description = parseWindowDescription(win_info);
 
         /// Check whether there is already a window description with the same 
name
@@ -148,7 +146,7 @@ DB::WindowFrame WindowRelParser::parseWindowFrame(const 
WindowInfo & win_info)
 {
     DB::WindowFrame win_frame;
     const auto & signature_function_name = win_info.signature_function_name;
-    const auto & window_function = win_info.measure->measure();
+    const auto & window_function = *win_info.window_function;
     win_frame.type = parseWindowFrameType(signature_function_name, 
window_function);
     parseBoundType(window_function.lower_bound(), true, win_frame.begin_type, 
win_frame.begin_offset, win_frame.begin_preceding);
     parseBoundType(window_function.upper_bound(), false, win_frame.end_type, 
win_frame.end_offset, win_frame.end_preceding);
@@ -162,25 +160,27 @@ DB::WindowFrame WindowRelParser::parseWindowFrame(const 
WindowInfo & win_info)
     return win_frame;
 }
 
-DB::WindowFrame::FrameType
-WindowRelParser::parseWindowFrameType(const std::string & function_name, const 
substrait::Expression::WindowFunction & window_function)
+DB::WindowFrame::FrameType WindowRelParser::parseWindowFrameType(
+    const std::string & function_name, const 
substrait::ConsistentPartitionWindowRel::WindowRelFunction & window_function)
 {
     // It's weird! The frame type only could be rows in spark for rank(). But 
in clickhouse
     // it's should be range. If run rank() over rows frame, the result is 
different. The rank number
     // is different for the same values.
-    static const std::unordered_map<std::string, substrait::WindowType> 
special_function_frame_type
-        = {{"rank", substrait::RANGE}, {"dense_rank", substrait::RANGE}, 
{"percent_rank", substrait::RANGE}};
+    static const std::unordered_map<std::string, 
substrait::Expression_WindowFunction_BoundsType> special_function_frame_type
+        = {{"rank", 
substrait::Expression_WindowFunction_BoundsType_BOUNDS_TYPE_RANGE},
+           {"dense_rank", 
substrait::Expression_WindowFunction_BoundsType_BOUNDS_TYPE_RANGE},
+           {"percent_rank", 
substrait::Expression_WindowFunction_BoundsType_BOUNDS_TYPE_RANGE}};
 
-    substrait::WindowType frame_type;
+    substrait::Expression_WindowFunction_BoundsType frame_type;
     auto iter = special_function_frame_type.find(function_name);
     if (iter != special_function_frame_type.end())
         frame_type = iter->second;
     else
-        frame_type = window_function.window_type();
+        frame_type = window_function.bounds_type();
 
-    if (frame_type == substrait::ROWS)
+    if (frame_type == 
substrait::Expression_WindowFunction_BoundsType_BOUNDS_TYPE_ROWS)
         return DB::WindowFrame::FrameType::ROWS;
-    else if (frame_type == substrait::RANGE)
+    else if (frame_type == 
substrait::Expression_WindowFunction_BoundsType_BOUNDS_TYPE_RANGE)
         return DB::WindowFrame::FrameType::RANGE;
     else
         throw DB::Exception(DB::ErrorCodes::UNKNOWN_TYPE, "Unknow window frame 
type:{}", frame_type);
@@ -222,17 +222,13 @@ void WindowRelParser::parseBoundType(
         offset = 0;
         preceding_direction = is_begin_or_end;
     }
-    else if (bound.has_unbounded_preceding())
+    else if (bound.has_unbounded())
     {
+        // Substrait 0.98 collapses unbounded_preceding/unbounded_following 
into a single `unbounded`;
+        // the direction is inferred from position (begin bound = preceding, 
end bound = following).
         bound_type = DB::WindowFrame::BoundaryType::Unbounded;
         offset = 0;
-        preceding_direction = true;
-    }
-    else if (bound.has_unbounded_following())
-    {
-        bound_type = DB::WindowFrame::BoundaryType::Unbounded;
-        offset = 0;
-        preceding_direction = false;
+        preceding_direction = is_begin_or_end;
     }
     else
     {
@@ -242,7 +238,7 @@ void WindowRelParser::parseBoundType(
 
 WindowFunctionDescription WindowRelParser::parseWindowFunctionDescription(
     const String & ch_function_name,
-    const substrait::Expression::WindowFunction & window_function,
+    const substrait::ConsistentPartitionWindowRel::WindowRelFunction & 
window_function,
     const DB::Names & arg_names,
     const DB::DataTypes & arg_types,
     const DB::Array & params)
@@ -259,16 +255,16 @@ WindowFunctionDescription 
WindowRelParser::parseWindowFunctionDescription(
     return description;
 }
 
-void WindowRelParser::initWindowsInfos(const substrait::WindowRel & win_rel)
+void WindowRelParser::initWindowsInfos(const 
substrait::ConsistentPartitionWindowRel & win_rel)
 {
-    win_infos.reserve(win_rel.measures_size());
-    for (const auto & measure : win_rel.measures())
+    win_infos.reserve(win_rel.window_functions_size());
+    for (const auto & win_function : win_rel.window_functions())
     {
         WindowInfo win_info;
-        win_info.result_column_name = measure.measure().column_name();
-        win_info.measure = &measure;
-        win_info.signature_function_name = 
*parseSignatureFunctionName(measure.measure().function_reference());
-        win_info.parser_func_info = 
AggregateFunctionParser::CommonFunctionInfo(measure);
+        win_info.result_column_name = win_function.column_name();
+        win_info.window_function = &win_function;
+        win_info.signature_function_name = 
*parseSignatureFunctionName(win_function.function_reference());
+        win_info.parser_func_info = 
AggregateFunctionParser::CommonFunctionInfo(win_function);
         win_info.function_parser = 
AggregateFunctionParserFactory::instance().get(win_info.signature_function_name,
 parser_context);
         win_info.function_name = 
win_info.function_parser->getCHFunctionName(win_info.parser_func_info);
         win_info.partition_exprs = win_rel.partition_expressions();
diff --git a/cpp-ch/local-engine/Parser/RelParsers/WindowRelParser.h 
b/cpp-ch/local-engine/Parser/RelParsers/WindowRelParser.h
index 0c3b76947e..31eee0fc36 100644
--- a/cpp-ch/local-engine/Parser/RelParsers/WindowRelParser.h
+++ b/cpp-ch/local-engine/Parser/RelParsers/WindowRelParser.h
@@ -40,7 +40,7 @@ public:
 private:
     struct WindowInfo
     {
-        const substrait::WindowRel::Measure * measure = nullptr;
+        const substrait::ConsistentPartitionWindowRel::WindowRelFunction * 
window_function = nullptr;
         String result_column_name;
         DB::Strings arg_column_names;
         DB::DataTypes arg_column_types;
@@ -69,8 +69,8 @@ private:
     // function may have different window frame in CH and spark.
     DB::WindowDescription parseWindowDescription(const WindowInfo & win_info);
     DB::WindowFrame parseWindowFrame(const WindowInfo & win_info);
-    DB::WindowFrame::FrameType
-    parseWindowFrameType(const std::string & function_name, const 
substrait::Expression::WindowFunction & window_function);
+    DB::WindowFrame::FrameType parseWindowFrameType(
+        const std::string & function_name, const 
substrait::ConsistentPartitionWindowRel::WindowRelFunction & window_function);
     static void parseBoundType(
         const substrait::Expression::WindowFunction::Bound & bound,
         bool is_begin_or_end,
@@ -79,12 +79,12 @@ private:
         bool & preceding);
     DB::WindowFunctionDescription parseWindowFunctionDescription(
         const String & ch_function_name,
-        const substrait::Expression::WindowFunction & window_function,
+        const substrait::ConsistentPartitionWindowRel::WindowRelFunction & 
window_function,
         const DB::Names & arg_names,
         const DB::DataTypes & arg_types,
         const DB::Array & params);
 
-    void initWindowsInfos(const substrait::WindowRel & win_rel);
+    void initWindowsInfos(const substrait::ConsistentPartitionWindowRel & 
win_rel);
     void tryAddProjectionBeforeWindow();
     void tryAddProjectionAfterWindow();
 };
diff --git a/cpp/velox/substrait/SubstraitToVeloxPlan.cc 
b/cpp/velox/substrait/SubstraitToVeloxPlan.cc
index 888ab8d6ce..e648be8003 100644
--- a/cpp/velox/substrait/SubstraitToVeloxPlan.cc
+++ b/cpp/velox/substrait/SubstraitToVeloxPlan.cc
@@ -1097,14 +1097,14 @@ core::PlanNodePtr 
SubstraitToVeloxPlanConverter::toVeloxPlan(const ::substrait::
 const core::WindowNode::Frame SubstraitToVeloxPlanConverter::createWindowFrame(
     const ::substrait::Expression_WindowFunction_Bound& lower_bound,
     const ::substrait::Expression_WindowFunction_Bound& upper_bound,
-    const ::substrait::WindowType& type,
+    const ::substrait::Expression_WindowFunction_BoundsType& type,
     const RowTypePtr& inputType) {
   core::WindowNode::Frame frame;
   switch (type) {
-    case ::substrait::WindowType::ROWS:
+    case ::substrait::Expression_WindowFunction_BoundsType_BOUNDS_TYPE_ROWS:
       frame.type = core::WindowNode::WindowType::kRows;
       break;
-    case ::substrait::WindowType::RANGE:
+    case ::substrait::Expression_WindowFunction_BoundsType_BOUNDS_TYPE_RANGE:
       frame.type = core::WindowNode::WindowType::kRange;
       break;
     default:
@@ -1125,14 +1125,17 @@ const core::WindowNode::Frame 
SubstraitToVeloxPlanConverter::createWindowFrame(
     }
   };
 
-  auto boundTypeConversion = [&](::substrait::Expression_WindowFunction_Bound 
boundType)
-      -> std::tuple<core::WindowNode::BoundType, core::TypedExprPtr> {
+  auto boundTypeConversion = [&](::substrait::Expression_WindowFunction_Bound 
boundType,
+                                 bool isLowerBound) -> 
std::tuple<core::WindowNode::BoundType, core::TypedExprPtr> {
     if (boundType.has_current_row()) {
       return std::make_tuple(core::WindowNode::BoundType::kCurrentRow, 
nullptr);
-    } else if (boundType.has_unbounded_following()) {
-      return std::make_tuple(core::WindowNode::BoundType::kUnboundedFollowing, 
nullptr);
-    } else if (boundType.has_unbounded_preceding()) {
-      return std::make_tuple(core::WindowNode::BoundType::kUnboundedPreceding, 
nullptr);
+    } else if (boundType.has_unbounded()) {
+      // Substrait 0.98 uses a single `unbounded` bound; the direction is 
inferred from position:
+      // an unbounded lower bound is the start of the partition, an unbounded 
upper bound is the end.
+      return std::make_tuple(
+          isLowerBound ? core::WindowNode::BoundType::kUnboundedPreceding
+                       : core::WindowNode::BoundType::kUnboundedFollowing,
+          nullptr);
     } else if (boundType.has_following()) {
       auto following = boundType.following();
       return std::make_tuple(
@@ -1147,29 +1150,29 @@ const core::WindowNode::Frame 
SubstraitToVeloxPlanConverter::createWindowFrame(
       VELOX_FAIL("The BoundType is not supported.");
     }
   };
-  std::tie(frame.startType, frame.startValue) = 
boundTypeConversion(lower_bound);
-  std::tie(frame.endType, frame.endValue) = boundTypeConversion(upper_bound);
+  std::tie(frame.startType, frame.startValue) = 
boundTypeConversion(lower_bound, /*isLowerBound=*/true);
+  std::tie(frame.endType, frame.endValue) = boundTypeConversion(upper_bound, 
/*isLowerBound=*/false);
   return frame;
 }
 
-core::PlanNodePtr SubstraitToVeloxPlanConverter::toVeloxPlan(const 
::substrait::WindowRel& windowRel) {
+core::PlanNodePtr SubstraitToVeloxPlanConverter::toVeloxPlan(
+    const ::substrait::ConsistentPartitionWindowRel& windowRel) {
   core::PlanNodePtr childNode;
   if (windowRel.has_input()) {
     childNode = toVeloxPlan(windowRel.input());
   } else {
-    VELOX_FAIL("Child Rel is expected in WindowRel.");
+    VELOX_FAIL("Child Rel is expected in ConsistentPartitionWindowRel.");
   }
 
   const auto& inputType = childNode->outputType();
 
-  // Parse measures and get the window expressions.
-  // Each measure represents one window expression.
+  // Parse window functions and get the window expressions.
+  // Each window function represents one window expression.
   std::vector<core::WindowNode::Function> windowNodeFunctions;
   std::vector<std::string> windowColumnNames;
 
-  windowNodeFunctions.reserve(windowRel.measures().size());
-  for (const auto& smea : windowRel.measures()) {
-    const auto& windowFunction = smea.measure();
+  windowNodeFunctions.reserve(windowRel.window_functions().size());
+  for (const auto& windowFunction : windowRel.window_functions()) {
     std::string funcName = SubstraitParser::findVeloxFunction(functionMap_, 
windowFunction.function_reference());
     std::vector<core::TypedExprPtr> windowParams;
     auto& argumentList = windowFunction.arguments();
@@ -1189,7 +1192,7 @@ core::PlanNodePtr 
SubstraitToVeloxPlanConverter::toVeloxPlan(const ::substrait::
         windowVeloxType, std::move(windowParams), 
exec::sanitizeName(funcName));
     auto upperBound = windowFunction.upper_bound();
     auto lowerBound = windowFunction.lower_bound();
-    auto type = windowFunction.window_type();
+    auto type = windowFunction.bounds_type();
 
     windowColumnNames.push_back(windowFunction.column_name());
 
diff --git a/cpp/velox/substrait/SubstraitToVeloxPlan.h 
b/cpp/velox/substrait/SubstraitToVeloxPlan.h
index 5ced0b01d2..b0cf76fff3 100644
--- a/cpp/velox/substrait/SubstraitToVeloxPlan.h
+++ b/cpp/velox/substrait/SubstraitToVeloxPlan.h
@@ -104,8 +104,8 @@ class SubstraitToVeloxPlanConverter {
   /// Used to convert Substrait GenerateRel into Velox PlanNode.
   core::PlanNodePtr toVeloxPlan(const ::substrait::GenerateRel& generateRel);
 
-  /// Used to convert Substrait WindowRel into Velox PlanNode.
-  core::PlanNodePtr toVeloxPlan(const ::substrait::WindowRel& windowRel);
+  /// Used to convert Substrait ConsistentPartitionWindowRel into Velox 
PlanNode.
+  core::PlanNodePtr toVeloxPlan(const 
::substrait::ConsistentPartitionWindowRel& windowRel);
 
   /// Used to convert Substrait WindowGroupLimitRel into Velox PlanNode.
   core::PlanNodePtr toVeloxPlan(const ::substrait::WindowGroupLimitRel& 
windowGroupLimitRel);
@@ -277,7 +277,7 @@ class SubstraitToVeloxPlanConverter {
   const core::WindowNode::Frame createWindowFrame(
       const ::substrait::Expression_WindowFunction_Bound& lower_bound,
       const ::substrait::Expression_WindowFunction_Bound& upper_bound,
-      const ::substrait::WindowType& type,
+      const ::substrait::Expression_WindowFunction_BoundsType& type,
       const RowTypePtr& inputType);
 
   /// The unique identification for each PlanNode.
diff --git a/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc 
b/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
index c041e580db..638fcb4d8d 100644
--- a/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
+++ b/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
@@ -637,8 +637,7 @@ bool SubstraitToVeloxPlanValidator::validate(const 
::substrait::ExpandRel& expan
 
 bool validateBoundType(::substrait::Expression_WindowFunction_Bound boundType) 
{
   switch (boundType.kind_case()) {
-    case ::substrait::Expression_WindowFunction_Bound::kUnboundedFollowing:
-    case ::substrait::Expression_WindowFunction_Bound::kUnboundedPreceding:
+    case ::substrait::Expression_WindowFunction_Bound::kUnbounded:
     case ::substrait::Expression_WindowFunction_Bound::kCurrentRow:
     case ::substrait::Expression_WindowFunction_Bound::kFollowing:
     case ::substrait::Expression_WindowFunction_Bound::kPreceding:
@@ -649,28 +648,28 @@ bool 
validateBoundType(::substrait::Expression_WindowFunction_Bound boundType) {
   return true;
 }
 
-bool SubstraitToVeloxPlanValidator::validate(const ::substrait::WindowRel& 
windowRel) {
+bool SubstraitToVeloxPlanValidator::validate(const 
::substrait::ConsistentPartitionWindowRel& windowRel) {
   if (windowRel.has_input() && !validate(windowRel.input())) {
-    LOG_VALIDATION_MSG("WindowRel input fails to validate.");
+    LOG_VALIDATION_MSG("ConsistentPartitionWindowRel input fails to 
validate.");
     return false;
   }
 
   // Get and validate the input types from extension.
   if (!windowRel.has_advanced_extension()) {
-    LOG_VALIDATION_MSG("Input types are expected in WindowRel.");
+    LOG_VALIDATION_MSG("Input types are expected in 
ConsistentPartitionWindowRel.");
     return false;
   }
   const auto& extension = windowRel.advanced_extension();
   TypePtr inputRowType;
   std::vector<TypePtr> types;
   if (!parseVeloxType(extension, inputRowType) || 
!flattenSingleLevel(inputRowType, types)) {
-    LOG_VALIDATION_MSG("Validation failed for input types in WindowRel.");
+    LOG_VALIDATION_MSG("Validation failed for input types in 
ConsistentPartitionWindowRel.");
     return false;
   }
 
   if (types.empty()) {
     // See: https://github.com/apache/gluten/issues/7600.
-    LOG_VALIDATION_MSG("Validation failed for empty input schema in 
WindowRel.");
+    LOG_VALIDATION_MSG("Validation failed for empty input schema in 
ConsistentPartitionWindowRel.");
     return false;
   }
 
@@ -684,9 +683,8 @@ bool SubstraitToVeloxPlanValidator::validate(const 
::substrait::WindowRel& windo
 
   // Validate WindowFunction
   std::vector<std::string> funcSpecs;
-  funcSpecs.reserve(windowRel.measures().size());
-  for (const auto& smea : windowRel.measures()) {
-    const auto& windowFunction = smea.measure();
+  funcSpecs.reserve(windowRel.window_functions().size());
+  for (const auto& windowFunction : windowRel.window_functions()) {
     
funcSpecs.emplace_back(planConverter_->findFuncSpec(windowFunction.function_reference()));
     SubstraitParser::parseType(windowFunction.output_type());
     for (const auto& arg : windowFunction.arguments()) {
@@ -701,14 +699,14 @@ bool SubstraitToVeloxPlanValidator::validate(const 
::substrait::WindowRel& windo
       }
     }
     // Validate BoundType and Frame Type
-    switch (windowFunction.window_type()) {
-      case ::substrait::WindowType::ROWS:
-      case ::substrait::WindowType::RANGE:
+    switch (windowFunction.bounds_type()) {
+      case ::substrait::Expression_WindowFunction_BoundsType_BOUNDS_TYPE_ROWS:
+      case ::substrait::Expression_WindowFunction_BoundsType_BOUNDS_TYPE_RANGE:
         break;
       default:
         LOG_VALIDATION_MSG(
-            "the window type only support ROWS and RANGE, and the input type 
is " +
-            std::to_string(windowFunction.window_type()));
+            "the bounds type only support ROWS and RANGE, and the input type 
is " +
+            std::to_string(windowFunction.bounds_type()));
         return false;
     }
 
diff --git a/cpp/velox/substrait/SubstraitToVeloxPlanValidator.h 
b/cpp/velox/substrait/SubstraitToVeloxPlanValidator.h
index 51c58b6afb..3f83c4c32c 100644
--- a/cpp/velox/substrait/SubstraitToVeloxPlanValidator.h
+++ b/cpp/velox/substrait/SubstraitToVeloxPlanValidator.h
@@ -87,7 +87,7 @@ class SubstraitToVeloxPlanValidator {
   bool validate(const ::substrait::SortRel& sortRel);
 
   /// Used to validate whether the computing of this Window is supported.
-  bool validate(const ::substrait::WindowRel& windowRel);
+  bool validate(const ::substrait::ConsistentPartitionWindowRel& windowRel);
 
   /// Used to validate whether the computing of this WindowGroupLimit is 
supported.
   bool validate(const ::substrait::WindowGroupLimitRel& windowGroupLimitRel);
diff --git 
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/WindowFunctionNode.java
 
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/WindowFunctionNode.java
index a114c6050a..d4be6c6ba2 100644
--- 
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/WindowFunctionNode.java
+++ 
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/WindowFunctionNode.java
@@ -21,10 +21,10 @@ import org.apache.gluten.expression.ExpressionConverter;
 import org.apache.gluten.substrait.SubstraitContext;
 import org.apache.gluten.substrait.type.TypeNode;
 
+import io.substrait.proto.ConsistentPartitionWindowRel;
 import io.substrait.proto.Expression;
 import io.substrait.proto.FunctionArgument;
 import io.substrait.proto.FunctionOption;
-import io.substrait.proto.WindowType;
 import org.apache.spark.sql.catalyst.expressions.Attribute;
 import org.apache.spark.sql.catalyst.expressions.PreComputeRangeFrameBound;
 
@@ -82,14 +82,11 @@ public class WindowFunctionNode implements Serializable {
         builder.setCurrentRow(currentRowBuilder.build());
         break;
       case ("UNBOUNDED PRECEDING"):
-        Expression.WindowFunction.Bound.Unbounded_Preceding.Builder 
precedingBuilder =
-            Expression.WindowFunction.Bound.Unbounded_Preceding.newBuilder();
-        builder.setUnboundedPreceding(precedingBuilder.build());
-        break;
       case ("UNBOUNDED FOLLOWING"):
-        Expression.WindowFunction.Bound.Unbounded_Following.Builder 
followingBuilder =
-            Expression.WindowFunction.Bound.Unbounded_Following.newBuilder();
-        builder.setUnboundedFollowing(followingBuilder.build());
+        // Substrait 0.98 collapses the two unbounded bounds into a single 
`unbounded`; the
+        // direction is inferred from position (lower bound = start of the 
partition, upper
+        // bound = end of the partition).
+        
builder.setUnbounded(Expression.WindowFunction.Bound.Unbounded.newBuilder().build());
         break;
       default:
         if (boundType instanceof PreComputeRangeFrameBound) {
@@ -141,23 +138,24 @@ public class WindowFunctionNode implements Serializable {
     return builder;
   }
 
-  private WindowType getWindowType(String type) {
-    WindowType windowType;
+  private Expression.WindowFunction.BoundsType getBoundsType(String type) {
+    Expression.WindowFunction.BoundsType boundsType;
     switch (type) {
       case ("ROWS"):
-        windowType = WindowType.forNumber(0);
+        boundsType = Expression.WindowFunction.BoundsType.BOUNDS_TYPE_ROWS;
         break;
       case ("RANGE"):
-        windowType = WindowType.forNumber(1);
+        boundsType = Expression.WindowFunction.BoundsType.BOUNDS_TYPE_RANGE;
         break;
       default:
         throw new UnsupportedOperationException("Only support ROWS and RANGE 
Frame type.");
     }
-    return windowType;
+    return boundsType;
   }
 
-  public Expression.WindowFunction toProtobuf() {
-    Expression.WindowFunction.Builder windowBuilder = 
Expression.WindowFunction.newBuilder();
+  public ConsistentPartitionWindowRel.WindowRelFunction toProtobuf() {
+    ConsistentPartitionWindowRel.WindowRelFunction.Builder windowBuilder =
+        ConsistentPartitionWindowRel.WindowRelFunction.newBuilder();
     windowBuilder.setFunctionReference(functionId);
     if (ignoreNulls) {
       FunctionOption option = 
FunctionOption.newBuilder().setName("ignoreNulls").build();
@@ -179,7 +177,7 @@ public class WindowFunctionNode implements Serializable {
         Expression.WindowFunction.Bound.newBuilder();
     windowBuilder.setLowerBound(setBound(lowerBoundBuilder, 
lowerBound).build());
     windowBuilder.setUpperBound(setBound(upperBoundBuilder, 
upperBound).build());
-    windowBuilder.setWindowType(getWindowType(frameType));
+    windowBuilder.setBoundsType(getBoundsType(frameType));
     return windowBuilder.build();
   }
 }
diff --git 
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/WindowRelNode.java
 
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/WindowRelNode.java
index 5e619cc647..3a47da8b16 100644
--- 
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/WindowRelNode.java
+++ 
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/rel/WindowRelNode.java
@@ -20,10 +20,10 @@ import 
org.apache.gluten.substrait.expression.ExpressionNode;
 import org.apache.gluten.substrait.expression.WindowFunctionNode;
 import org.apache.gluten.substrait.extensions.AdvancedExtensionNode;
 
+import io.substrait.proto.ConsistentPartitionWindowRel;
 import io.substrait.proto.Rel;
 import io.substrait.proto.RelCommon;
 import io.substrait.proto.SortField;
-import io.substrait.proto.WindowRel;
 
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -67,16 +67,14 @@ public class WindowRelNode implements RelNode, Serializable 
{
     RelCommon.Builder relCommonBuilder = RelCommon.newBuilder();
     relCommonBuilder.setDirect(RelCommon.Direct.newBuilder());
 
-    WindowRel.Builder windowBuilder = WindowRel.newBuilder();
+    ConsistentPartitionWindowRel.Builder windowBuilder = 
ConsistentPartitionWindowRel.newBuilder();
     windowBuilder.setCommon(relCommonBuilder.build());
     if (input != null) {
       windowBuilder.setInput(input.toProtobuf());
     }
 
     for (WindowFunctionNode windowFunctionNode : windowFunctionNodes) {
-      WindowRel.Measure.Builder measureBuilder = 
WindowRel.Measure.newBuilder();
-      measureBuilder.setMeasure(windowFunctionNode.toProtobuf());
-      windowBuilder.addMeasures(measureBuilder.build());
+      windowBuilder.addWindowFunctions(windowFunctionNode.toProtobuf());
     }
 
     for (int i = 0; i < partitionExpressions.size(); 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 6647fe38e8..0015182f29 100644
--- 
a/gluten-substrait/src/main/resources/substrait/proto/substrait/algebra.proto
+++ 
b/gluten-substrait/src/main/resources/substrait/proto/substrait/algebra.proto
@@ -411,16 +411,41 @@ message SortRel {
   substrait.extensions.AdvancedExtension advanced_extension = 10;
 }
 
-message WindowRel {
+message ConsistentPartitionWindowRel {
   RelCommon common = 1;
   Rel input = 2;
-  repeated Measure measures = 3;
+  repeated WindowRelFunction window_functions = 3;
   repeated Expression partition_expressions = 4;
   repeated SortField sorts = 5;
+
   substrait.extensions.AdvancedExtension advanced_extension = 10;
 
-  message Measure {
-    Expression.WindowFunction measure = 1;
+  // This message mirrors the `WindowFunction` message but removes the fields 
defining the partition,
+  // sorts, and bounds, since those must be consistent across the various 
functions in this rel.  Refer
+  // to the `WindowFunction` message for a description of these fields.
+  message WindowRelFunction {
+    // References a function_anchor defined in this plan.
+    uint32 function_reference = 1;
+
+    repeated FunctionArgument arguments = 9;
+
+    repeated FunctionOption options = 11;
+
+    Type output_type = 7;
+
+    AggregationPhase phase = 6;
+
+    AggregateFunction.AggregationInvocation invocation = 10;
+
+    Expression.WindowFunction.Bound lower_bound = 5;
+
+    Expression.WindowFunction.Bound upper_bound = 4;
+
+    Expression.WindowFunction.BoundsType bounds_type = 12;
+
+    // Gluten fork: output column name for this window function, consumed by 
the
+    // native backends to name the resulting window output column.
+    string column_name = 13;
   }
 }
 
@@ -596,7 +621,7 @@ message Rel {
     HashJoinRel hash_join = 13;
     MergeJoinRel merge_join = 14;
     ExpandRel expand = 15;
-    WindowRel window = 16;
+    ConsistentPartitionWindowRel window = 16;
     GenerateRel generate = 17;
     WriteRel write = 19;
     TopNRel top_n = 23;
@@ -1068,8 +1093,8 @@ message Expression {
     // Optional; defaults to the start of the partition.
     Bound lower_bound = 5;
 
-    string column_name = 12;
-    WindowType window_type = 13;
+    // Defines the bounds type: ROWS, RANGE
+    BoundsType bounds_type = 12;
 
     // Defines the record relative to the current record up to which the window
     // extends. The bound is inclusive. If the upper bound indexes a record
@@ -1081,6 +1106,18 @@ message Expression {
     // Deprecated; use arguments instead.
     repeated Expression args = 8 [deprecated = true];
 
+    enum BoundsType {
+      BOUNDS_TYPE_UNSPECIFIED = 0;
+      // The lower and upper bound specify how many rows before and after the 
current row
+      // the window should extend.
+      BOUNDS_TYPE_ROWS = 1;
+      // The lower and upper bound describe a range of values.  The window 
should include all rows
+      // where the value of the ordering column is greater than or equal to 
(current_value - lower bound)
+      // and less than or equal to (current_value + upper bound).  This bounds 
type is only valid if there
+      // is a single ordering column.
+      BOUNDS_TYPE_RANGE = 2;
+    }
+
     // Defines one of the two boundaries for the window of a window function.
     message Bound {
       // Defines that the bound extends this far back from the current record.
@@ -1112,9 +1149,10 @@ message Expression {
       // Defines that the bound extends to or from the current record.
       message CurrentRow {}
 
-      message Unbounded_Preceding {}
-
-      message Unbounded_Following {}
+      // Defines an "unbounded bound": for lower bounds this means the start
+      // of the partition, and for upper bounds this means the end of the
+      // partition.
+      message Unbounded {}
 
       oneof kind {
         // The bound extends some number of records behind the current record.
@@ -1127,8 +1165,10 @@ message Expression {
         // The bound extends to the current record.
         CurrentRow current_row = 3;
 
-        Unbounded_Preceding unbounded_preceding = 4;
-        Unbounded_Following unbounded_following = 5;
+        // The bound extends to the start of the partition or the end of the
+        // partition, depending on whether this represents the upper or lower
+        // bound.
+        Unbounded unbounded = 4;
       }
     }
   }
@@ -1477,11 +1517,6 @@ enum AggregationPhase {
   AGGREGATION_PHASE_INTERMEDIATE_TO_RESULT = 4;
 }
 
-enum WindowType {
-  ROWS = 0;
-  RANGE = 1;
-}
-
 // An aggregate function.
 message AggregateFunction {
   // Points to a function_anchor defined in this plan, which must refer
diff --git 
a/gluten-substrait/src/test/scala/org/apache/gluten/utils/WindowRelProtoSuite.scala
 
b/gluten-substrait/src/test/scala/org/apache/gluten/utils/WindowRelProtoSuite.scala
new file mode 100644
index 0000000000..33e94382d4
--- /dev/null
+++ 
b/gluten-substrait/src/test/scala/org/apache/gluten/utils/WindowRelProtoSuite.scala
@@ -0,0 +1,96 @@
+/*
+ * 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.utils
+
+import org.apache.gluten.substrait.`type`.TypeBuilder
+import org.apache.gluten.substrait.SubstraitContext
+import org.apache.gluten.substrait.expression.{ExpressionBuilder, 
ExpressionNode}
+import org.apache.gluten.substrait.rel.RelBuilder
+
+import org.apache.spark.sql.catalyst.expressions.{Attribute, CurrentRow, 
UnboundedFollowing, UnboundedPreceding}
+
+import io.substrait.proto.{Expression, SortField}
+import org.scalatest.funsuite.AnyFunSuite
+
+import java.util.{Arrays, Collections}
+
+/**
+ * Round-trip coverage for the Substrait 0.98 windowing migration (`WindowRel` 
->
+ * `ConsistentPartitionWindowRel`, `WindowType` -> `BoundsType`, and the
+ * `unbounded_preceding`/`unbounded_following` -> single `unbounded` 
collapse). These assertions pin
+ * the silent risks of the migration: the frame-type enum value remap and the 
unbounded-bound
+ * collapse, neither of which a plain compile would catch.
+ */
+class WindowRelProtoSuite extends AnyFunSuite {
+
+  private def windowFunctionProto(
+      upperBound: org.apache.spark.sql.catalyst.expressions.Expression,
+      lowerBound: org.apache.spark.sql.catalyst.expressions.Expression,
+      frameType: String,
+      columnName: String)
+      : io.substrait.proto.ConsistentPartitionWindowRel.WindowRelFunction = {
+    val windowFunction = ExpressionBuilder.makeWindowFunction(
+      Integer.valueOf(0),
+      Collections.emptyList[ExpressionNode](),
+      columnName,
+      TypeBuilder.makeI32(false),
+      upperBound,
+      lowerBound,
+      frameType,
+      Collections.emptyList[Attribute]()
+    )
+    val rel = RelBuilder.makeWindowRel(
+      null,
+      Arrays.asList(windowFunction),
+      Collections.emptyList[ExpressionNode](),
+      Collections.emptyList[SortField](),
+      new SubstraitContext(),
+      0L)
+    val proto = rel.toProtobuf
+    assert(proto.hasWindow, "Rel oneof should carry a 
ConsistentPartitionWindowRel")
+    val window = proto.getWindow
+    assert(window.getWindowFunctionsCount === 1)
+    window.getWindowFunctions(0)
+  }
+
+  test("ROWS frame maps to BOUNDS_TYPE_ROWS (value remap) and keeps the 
column_name graft") {
+    val fn = windowFunctionProto(CurrentRow, UnboundedPreceding, "ROWS", 
"rows_col")
+    // Fork WindowType.ROWS was 0; 0.98 BoundsType.BOUNDS_TYPE_ROWS is 1 
(UNSPECIFIED took slot 0).
+    assert(fn.getBoundsType === 
Expression.WindowFunction.BoundsType.BOUNDS_TYPE_ROWS)
+    assert(fn.getBoundsType.getNumber === 1)
+    // column_name is a Gluten graft on WindowRelFunction (0.98 has none).
+    assert(fn.getColumnName === "rows_col")
+    // UNBOUNDED PRECEDING collapses to the single `unbounded` bound; CURRENT 
ROW is unchanged.
+    assert(fn.getLowerBound.hasUnbounded)
+    assert(fn.getUpperBound.hasCurrentRow)
+  }
+
+  test("RANGE frame maps to BOUNDS_TYPE_RANGE (value remap)") {
+    val fn = windowFunctionProto(CurrentRow, UnboundedPreceding, "RANGE", 
"range_col")
+    // Fork WindowType.RANGE was 1; 0.98 BoundsType.BOUNDS_TYPE_RANGE is 2.
+    assert(fn.getBoundsType === 
Expression.WindowFunction.BoundsType.BOUNDS_TYPE_RANGE)
+    assert(fn.getBoundsType.getNumber === 2)
+  }
+
+  test("UNBOUNDED FOLLOWING collapses to the single unbounded bound") {
+    val fn = windowFunctionProto(UnboundedFollowing, UnboundedPreceding, 
"ROWS", "both_unbounded")
+    // Both the lower (UNBOUNDED PRECEDING) and upper (UNBOUNDED FOLLOWING) 
bounds collapse to
+    // `unbounded`; direction is inferred from position by the consumers.
+    assert(fn.getLowerBound.hasUnbounded)
+    assert(fn.getUpperBound.hasUnbounded)
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to