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 4fbd758dee [GLUTEN-9003][VL] Add native counter functions for Delta's 
IncrementMetric (#13357)
4fbd758dee is described below

commit 4fbd758dee9424ffd1c1c1c53b3eb00679d044bd
Author: Mohammad Linjawi <[email protected]>
AuthorDate: Fri Nov 6 15:36:55 2026 +0300

    [GLUTEN-9003][VL] Add native counter functions for Delta's IncrementMetric 
(#13357)
    
    * [GLUTEN-9003][VL] Add native counter functions for Delta's IncrementMetric
---
 cpp/velox/CMakeLists.txt                           |   1 +
 cpp/velox/compute/WholeStageResultIterator.cc      |  27 +++-
 cpp/velox/compute/WholeStageResultIterator.h       |   3 +
 .../operators/functions/IncrementMetricFunction.cc | 153 +++++++++++++++++++
 .../operators/functions/IncrementMetricFunction.h  |  75 +++++++++
 .../functions/RegistrationAllFunctions.cc          |   3 +
 cpp/velox/substrait/SubstraitToVeloxPlan.cc        |   8 +-
 cpp/velox/tests/CMakeLists.txt                     |   2 +-
 cpp/velox/tests/IncrementMetricFunctionTest.cc     | 170 +++++++++++++++++++++
 9 files changed, 436 insertions(+), 6 deletions(-)

diff --git a/cpp/velox/CMakeLists.txt b/cpp/velox/CMakeLists.txt
index 23dee4ac72..6500d5c446 100644
--- a/cpp/velox/CMakeLists.txt
+++ b/cpp/velox/CMakeLists.txt
@@ -177,6 +177,7 @@ set(VELOX_SRCS
     operators/functions/delta/DeltaBitmapAggregator.cc
     operators/functions/overlay/GetMapValue.cc
     operators/functions/overlay/RegisterFunctionOverlay.cc
+    operators/functions/IncrementMetricFunction.cc
     operators/functions/RowConstructorWithNull.cc
     operators/functions/SparkExprToSubfieldFilterParser.cc
     operators/plannodes/RowVectorStream.cc
diff --git a/cpp/velox/compute/WholeStageResultIterator.cc 
b/cpp/velox/compute/WholeStageResultIterator.cc
index 71e0d20a94..0fae61ec00 100644
--- a/cpp/velox/compute/WholeStageResultIterator.cc
+++ b/cpp/velox/compute/WholeStageResultIterator.cc
@@ -27,6 +27,7 @@
 #include "compute/delta/DeltaSplit.h"
 #include "compute/delta/DeltaSplitInfo.h"
 #include "config/VeloxConfig.h"
+#include "operators/functions/IncrementMetricFunction.h"
 #include "utils/ConfigExtractor.h"
 #include "velox/connectors/hive/HiveConfig.h"
 #include "velox/connectors/hive/HiveConnectorSplit.h"
@@ -106,6 +107,7 @@ WholeStageResultIterator::WholeStageResultIterator(
       taskInfo_(taskInfo),
       executor_(executor),
       veloxPlan_(planNode),
+      usesIncrementMetricFunctions_(usesIncrementMetricFunctions(*planNode)),
       spillExecutor_(spillExecutor),
       connectorIds_(std::move(connectorIds)),
       scanNodeIds_(scanNodeIds),
@@ -511,8 +513,7 @@ void WholeStageResultIterator::collectMetrics() {
         customStats[customMetric.first] = folly::dynamic::object("sum", 
customMetric.second.sum)(
             "count", customMetric.second.count)("min", 
customMetric.second.min)("max", customMetric.second.max);
       }
-
-      operatorStats.push_back(folly::dynamic::object("inputRows", 
opStats->inputRows)(
+      folly::dynamic operatorJson = folly::dynamic::object("inputRows", 
opStats->inputRows)(
           "inputVectors", opStats->inputVectors)("inputBytes", 
opStats->inputBytes)(
           "rawInputRows", opStats->rawInputRows)("rawInputBytes", 
opStats->rawInputBytes)(
           "outputRows", opStats->outputRows)("outputVectors", 
opStats->outputVectors)(
@@ -521,7 +522,19 @@ void WholeStageResultIterator::collectMetrics() {
           "numMemoryAllocations", 
opStats->numMemoryAllocations)("spilledInputBytes", opStats->spilledInputBytes)(
           "spilledBytes", opStats->spilledBytes)("spilledRows", 
opStats->spilledRows)(
           "spilledPartitions", opStats->spilledPartitions)("spilledFiles", 
opStats->spilledFiles)(
-          "physicalWrittenBytes", 
opStats->physicalWrittenBytes)("customStats", customStats));
+          "physicalWrittenBytes", 
opStats->physicalWrittenBytes)("customStats", customStats);
+      // Rows processed per Delta counter function, keyed by its slot name. 
Only FilterProject
+      // operators export these, and only when the plan calls a counter, so 
other operators get no key.
+      folly::dynamic expressionStats = folly::dynamic::object();
+      for (const auto& exprStats : opStats->expressionStats) {
+        if (isIncrementMetricFunction(exprStats.first)) {
+          expressionStats[exprStats.first] = exprStats.second.numProcessedRows;
+        }
+      }
+      if (!expressionStats.empty()) {
+        operatorJson["expressionStats"] = std::move(expressionStats);
+      }
+      operatorStats.push_back(std::move(operatorJson));
     }
 
     statsNum += static_cast<unsigned int>(operatorStats.size());
@@ -711,6 +724,14 @@ std::unordered_map<std::string, std::string> 
WholeStageResultIterator::getQueryC
     setIfExists(kOpTraceDirectoryCreateConfig, 
velox::core::QueryConfig::kOpTraceDirectoryCreateConfig);
 
     overwriteVeloxConf(veloxCfg_.get(), configs, kDynamicBackendConfPrefix);
+
+    // Only a plan that calls a Delta counter reads its counts from the 
per-expression processed-row
+    // stats (see IncrementMetricFunction.h). Set after the user overrides: a 
query that disabled
+    // the stats, or switched to simplified evaluation, which does not count, 
would report zero
+    // modified rows and could suppress valid change-data-feed rows.
+    if (usesIncrementMetricFunctions_) {
+      setIncrementMetricQueryConfigs(configs);
+    }
   } catch (const std::invalid_argument& err) {
     std::string errDetails = err.what();
     throw std::runtime_error("Invalid conf arg: " + errDetails);
diff --git a/cpp/velox/compute/WholeStageResultIterator.h 
b/cpp/velox/compute/WholeStageResultIterator.h
index e7febfdadf..d1633bbb0b 100644
--- a/cpp/velox/compute/WholeStageResultIterator.h
+++ b/cpp/velox/compute/WholeStageResultIterator.h
@@ -133,6 +133,9 @@ class WholeStageResultIterator : public 
SplitAwareColumnarBatchIterator {
   std::unique_ptr<facebook::velox::exec::TaskCursor> cursor_;
   facebook::velox::exec::Task* task_ = nullptr;
   std::shared_ptr<const facebook::velox::core::PlanNode> veloxPlan_;
+  /// Whether the plan calls a Delta counter and so needs the query configs 
set by
+  /// setIncrementMetricQueryConfigs().
+  const bool usesIncrementMetricFunctions_;
 
   /// Spill.
   std::string spillStrategy_;
diff --git a/cpp/velox/operators/functions/IncrementMetricFunction.cc 
b/cpp/velox/operators/functions/IncrementMetricFunction.cc
new file mode 100644
index 0000000000..509ac0b5a3
--- /dev/null
+++ b/cpp/velox/operators/functions/IncrementMetricFunction.cc
@@ -0,0 +1,153 @@
+/*
+ * 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.
+ */
+
+#include "operators/functions/IncrementMetricFunction.h"
+
+#include <charconv>
+#include <memory>
+#include <optional>
+#include <string_view>
+#include <system_error>
+#include <unordered_set>
+#include <vector>
+
+#include "velox/core/Expressions.h"
+#include "velox/core/QueryConfig.h"
+#include "velox/expression/EvalCtx.h"
+#include "velox/expression/FunctionSignature.h"
+#include "velox/expression/VectorFunction.h"
+
+namespace gluten {
+namespace {
+
+using namespace facebook::velox;
+
+/// Returns its only argument. See the header for why the name, not the body, 
carries the meaning.
+class IncrementMetricFunction : public exec::VectorFunction {
+ public:
+  void apply(
+      const SelectivityVector& rows,
+      std::vector<VectorPtr>& args,
+      const TypePtr& /*outputType*/,
+      exec::EvalCtx& context,
+      VectorPtr& result) const override {
+    VELOX_CHECK_EQ(args.size(), 1);
+    context.moveOrCopyResult(args[0], rows, result);
+  }
+};
+
+void collectIncrementMetricCalls(const core::ITypedExpr& expr, 
std::unordered_set<std::string>& names) {
+  if (const auto* call = dynamic_cast<const core::CallTypedExpr*>(&expr)) {
+    if (isIncrementMetricFunction(call->name())) {
+      names.insert(call->name());
+    }
+  }
+  if (const auto* lambda = dynamic_cast<const core::LambdaTypedExpr*>(&expr)) {
+    if (lambda->body() != nullptr) {
+      collectIncrementMetricCalls(*lambda->body(), names);
+    }
+    return;
+  }
+  for (const auto& input : expr.inputs()) {
+    if (input != nullptr) {
+      collectIncrementMetricCalls(*input, names);
+    }
+  }
+}
+
+void collectIncrementMetricCallsInProjects(const core::PlanNode& plan, 
std::unordered_set<std::string>& names) {
+  if (const auto* project = dynamic_cast<const core::ProjectNode*>(&plan)) {
+    for (const auto& projection : project->projections()) {
+      if (projection != nullptr) {
+        collectIncrementMetricCalls(*projection, names);
+      }
+    }
+  }
+  for (const auto& source : plan.sources()) {
+    if (source != nullptr) {
+      collectIncrementMetricCallsInProjects(*source, names);
+    }
+  }
+}
+
+} // namespace
+
+std::string incrementMetricFunctionName(int32_t slot) {
+  return std::string(kIncrementMetricFunctionPrefix) + std::to_string(slot);
+}
+
+bool isIncrementMetricFunction(const std::string& functionName) {
+  const std::string_view prefix{kIncrementMetricFunctionPrefix};
+  if (functionName.size() <= prefix.size() || functionName.compare(0, 
prefix.size(), prefix) != 0) {
+    return false;
+  }
+  int32_t slot = -1;
+  const char* const end = functionName.data() + functionName.size();
+  const auto [parsedEnd, error] = std::from_chars(functionName.data() + 
prefix.size(), end, slot);
+  // The last check rejects spellings that parse to a slot but are not its 
name, such as a leading
+  // zero.
+  return error == std::errc() && parsedEnd == end && slot >= 0 && slot < 
kIncrementMetricFunctionSlots &&
+      functionName == incrementMetricFunctionName(slot);
+}
+
+void registerIncrementMetricFunctions() {
+  // Not deterministic: keeps Velox from constant-folding or 
dictionary-peeling the call, either of
+  // which would change how many rows it is evaluated on. No default null 
behavior: a null input
+  // still counts, as it does on Spark.
+  const auto metadata = 
exec::VectorFunctionMetadataBuilder().deterministic(false).defaultNullBehavior(false).build();
+  for (int32_t slot = 0; slot < kIncrementMetricFunctionSlots; ++slot) {
+    exec::registerVectorFunction(
+        incrementMetricFunctionName(slot),
+        
{exec::FunctionSignatureBuilder().typeVariable("T").returnType("T").argumentType("T").build()},
+        std::make_unique<IncrementMetricFunction>(),
+        metadata);
+  }
+}
+
+bool usesIncrementMetricFunctions(const core::PlanNode& plan) {
+  std::unordered_set<std::string> names;
+  collectIncrementMetricCallsInProjects(plan, names);
+  return !names.empty();
+}
+
+void checkIncrementMetricFunctionsInProjects(
+    const std::unordered_map<uint64_t, std::string>& functionMap,
+    const core::PlanNode& plan) {
+  std::optional<std::unordered_set<std::string>> calledInProjects;
+  for (const auto& entry : functionMap) {
+    // Declarations are spelled `<name>:<argument types>`.
+    const auto name = entry.second.substr(0, entry.second.find(':'));
+    if (!isIncrementMetricFunction(name)) {
+      continue;
+    }
+    if (!calledInProjects.has_value()) {
+      calledInProjects.emplace();
+      collectIncrementMetricCallsInProjects(plan, *calledInProjects);
+    }
+    VELOX_CHECK(
+        calledInProjects->count(name) > 0,
+        "Delta counter {} is declared but no projection calls it, and the JVM 
credits counters only from projections",
+        name);
+  }
+}
+
+void setIncrementMetricQueryConfigs(std::unordered_map<std::string, 
std::string>& configs) {
+  configs[core::QueryConfig::kOperatorTrackExpressionStats] = "true";
+  configs[core::QueryConfig::kExprEvalSimplified] = "false";
+}
+
+} // namespace gluten
diff --git a/cpp/velox/operators/functions/IncrementMetricFunction.h 
b/cpp/velox/operators/functions/IncrementMetricFunction.h
new file mode 100644
index 0000000000..618f2d2a27
--- /dev/null
+++ b/cpp/velox/operators/functions/IncrementMetricFunction.h
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <unordered_map>
+
+#include "velox/core/PlanNode.h"
+
+namespace gluten {
+
+/// Delta's `IncrementMetric` counts how many rows an expression is evaluated 
on. Velox already
+/// keeps that count per expression (`ExprStats::numProcessedRows`) on its 
regular evaluation path
+/// and the FilterProject operator exports it per function name when asked to, 
so a counter is a
+/// pass-through function whose name is the key the JVM credits the metric 
from. Inside a CASE WHEN
+/// branch that is the number of rows that took the branch, at the top of a 
projection it is every
+/// row, which is exactly how Spark evaluates `IncrementMetric`.
+///
+/// Counters only appear in projections: the JVM emits them from Delta's 
project transformer alone
+/// (the filter transformer strips its counters and credits the filter's 
output rows), and a
+/// Substrait ProjectRel always becomes a ProjectNode. 
checkIncrementMetricFunctionsInProjects()
+/// enforces this.
+///
+/// The names are fixed slots, `increment_metric_<slot>`, registered once at 
startup. The JVM
+/// assigns one slot per distinct metric of a projection, so a metric needs no 
name of its own and
+/// nothing is registered while queries run.
+constexpr const char* kIncrementMetricFunctionPrefix = "increment_metric_";
+
+/// Number of counter slots registered. Mirrored by 
`IncrementMetricCall.maxCounters` on the JVM.
+constexpr int32_t kIncrementMetricFunctionSlots = 32;
+
+/// Name of the counter function for `slot`.
+std::string incrementMetricFunctionName(int32_t slot);
+
+/// True if `functionName` is one of the registered slots, 
`increment_metric_0` to
+/// `increment_metric_<kIncrementMetricFunctionSlots - 1>`.
+bool isIncrementMetricFunction(const std::string& functionName);
+
+/// Registers every counter slot. Called from registerAllFunctions().
+void registerIncrementMetricFunctions();
+
+/// True if a projection in `plan` calls a counter, which is when the query 
needs
+/// setIncrementMetricQueryConfigs().
+bool usesIncrementMetricFunctions(const facebook::velox::core::PlanNode& plan);
+
+/// Throws unless every counter declared in `functionMap`, the Substrait 
plan's function
+/// declarations, is called by a projection of `plan`. The JVM credits 
counters from projections
+/// only, so a counter anywhere else would leave its metric wrong.
+void checkIncrementMetricFunctionsInProjects(
+    const std::unordered_map<uint64_t, std::string>& functionMap,
+    const facebook::velox::core::PlanNode& plan);
+
+/// Sets the query configs a plan that calls a counter depends on, over any 
user override:
+/// FilterProject operators export their expression statistics, and 
expressions use the regular
+/// evaluation path, since simplified evaluation 
(`expression.eval_simplified`) applies functions
+/// without counting their rows.
+void setIncrementMetricQueryConfigs(std::unordered_map<std::string, 
std::string>& configs);
+
+} // namespace gluten
diff --git a/cpp/velox/operators/functions/RegistrationAllFunctions.cc 
b/cpp/velox/operators/functions/RegistrationAllFunctions.cc
index 25ae12c19b..1d91e42263 100644
--- a/cpp/velox/operators/functions/RegistrationAllFunctions.cc
+++ b/cpp/velox/operators/functions/RegistrationAllFunctions.cc
@@ -16,6 +16,7 @@
  */
 #include "operators/functions/RegistrationAllFunctions.h"
 
+#include "operators/functions/IncrementMetricFunction.h"
 #include "operators/functions/RowConstructorWithNull.h"
 #include "operators/functions/RowFunctionWithNull.h"
 #include "operators/functions/delta/DeltaBitmapAggregator.h"
@@ -86,6 +87,8 @@ void registerAllFunctions() {
 
   velox::functions::iceberg::registerFunctions();
   registerDeltaBitmapAggregator();
+  // Fixed counter slots for Delta's IncrementMetric; see 
IncrementMetricFunction.h.
+  registerIncrementMetricFunctions();
 
   // Gluten-managed function implementations. Registered last so they take
   // precedence over same-name, same-signature Velox functions.
diff --git a/cpp/velox/substrait/SubstraitToVeloxPlan.cc 
b/cpp/velox/substrait/SubstraitToVeloxPlan.cc
index df076d0f6f..bc52611bb9 100644
--- a/cpp/velox/substrait/SubstraitToVeloxPlan.cc
+++ b/cpp/velox/substrait/SubstraitToVeloxPlan.cc
@@ -23,6 +23,7 @@
 #include "compute/delta/DeltaSplitInfo.h"
 #include "compute/iceberg/IcebergPlanConverter.h"
 #include "jni/JniHashTable.h"
+#include "operators/functions/IncrementMetricFunction.h"
 #include "operators/hashjoin/HashTableBuilder.h"
 #include "operators/plannodes/RowVectorStream.h"
 #ifdef ENABLE_KAFKA
@@ -1951,13 +1952,16 @@ core::PlanNodePtr 
SubstraitToVeloxPlanConverter::toVeloxPlan(const ::substrait::
   // In fact, only one RelRoot or Rel is expected here.
   VELOX_CHECK_EQ(substraitPlan.relations_size(), 1);
   const auto& rel = substraitPlan.relations(0);
+  core::PlanNodePtr plan;
   if (rel.has_root()) {
-    return toVeloxPlan(rel.root());
+    plan = toVeloxPlan(rel.root());
   } else if (rel.has_rel()) {
-    return toVeloxPlan(rel.rel());
+    plan = toVeloxPlan(rel.rel());
   } else {
     VELOX_FAIL("RelRoot or Rel is expected in Plan.");
   }
+  checkIncrementMetricFunctionsInProjects(functionMap_, *plan);
+  return plan;
 }
 
 std::string SubstraitToVeloxPlanConverter::nextPlanNodeId() {
diff --git a/cpp/velox/tests/CMakeLists.txt b/cpp/velox/tests/CMakeLists.txt
index 82af2bacbc..a45420d395 100644
--- a/cpp/velox/tests/CMakeLists.txt
+++ b/cpp/velox/tests/CMakeLists.txt
@@ -154,7 +154,7 @@ add_velox_test(
   VeloxSubstraitSignatureTest.cc
   VeloxToSubstraitTypeTest.cc)
 add_velox_test(spark_functions_test SOURCES SparkFunctionTest.cc
-               FunctionTest.cc)
+               FunctionTest.cc IncrementMetricFunctionTest.cc)
 add_velox_test(runtime_test SOURCES RuntimeTest.cc)
 add_velox_test(velox_backend_test SOURCES VeloxBackendTest.cc)
 add_velox_test(velox_memory_test SOURCES MemoryManagerTest.cc)
diff --git a/cpp/velox/tests/IncrementMetricFunctionTest.cc 
b/cpp/velox/tests/IncrementMetricFunctionTest.cc
new file mode 100644
index 0000000000..e68cdfa769
--- /dev/null
+++ b/cpp/velox/tests/IncrementMetricFunctionTest.cc
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+
+#include <optional>
+#include <unordered_map>
+
+#include "operators/functions/IncrementMetricFunction.h"
+#include "operators/functions/RegistrationAllFunctions.h"
+#include "velox/common/base/tests/GTestUtils.h"
+#include "velox/core/QueryConfig.h"
+#include "velox/exec/tests/utils/PlanBuilder.h"
+#include "velox/functions/sparksql/tests/SparkFunctionBaseTest.h"
+
+using namespace facebook::velox;
+using namespace facebook::velox::functions::sparksql::test;
+
+namespace gluten {
+
+class IncrementMetricFunctionTest : public SparkFunctionBaseTest {
+ public:
+  IncrementMetricFunctionTest() {
+    registerAllFunctions();
+  }
+};
+
+TEST_F(IncrementMetricFunctionTest, slotNames) {
+  ASSERT_EQ(incrementMetricFunctionName(0), "increment_metric_0");
+  ASSERT_EQ(incrementMetricFunctionName(31), "increment_metric_31");
+  ASSERT_TRUE(isIncrementMetricFunction("increment_metric_7"));
+  ASSERT_FALSE(isIncrementMetricFunction("plus"));
+  ASSERT_FALSE(isIncrementMetricFunction("my_increment_metric_7"));
+  ASSERT_TRUE(isIncrementMetricFunction("increment_metric_0"));
+  
ASSERT_TRUE(isIncrementMetricFunction(incrementMetricFunctionName(kIncrementMetricFunctionSlots
 - 1)));
+  // Only the registered slots count.
+  ASSERT_FALSE(isIncrementMetricFunction("increment_metric_"));
+  
ASSERT_FALSE(isIncrementMetricFunction(incrementMetricFunctionName(kIncrementMetricFunctionSlots)));
+  ASSERT_FALSE(isIncrementMetricFunction("increment_metric_-1"));
+  ASSERT_FALSE(isIncrementMetricFunction("increment_metric_07"));
+  ASSERT_FALSE(isIncrementMetricFunction("increment_metric_7x"));
+  ASSERT_FALSE(isIncrementMetricFunction("increment_metric_foo"));
+}
+
+TEST_F(IncrementMetricFunctionTest, passesInputThroughIncludingNulls) {
+  auto input = makeNullableFlatVector<int64_t>({1, std::nullopt, 3});
+  auto result = evaluate("increment_metric_0(c0)", makeRowVector({input}));
+  facebook::velox::test::assertEqualVectors(input, result);
+
+  auto last = incrementMetricFunctionName(kIncrementMetricFunctionSlots - 1);
+  auto strings = makeFlatVector<StringView>({"a", "bb", "ccc"});
+  auto stringResult = evaluate(last + "(c0)", makeRowVector({strings}));
+  facebook::velox::test::assertEqualVectors(strings, stringResult);
+}
+
+TEST_F(IncrementMetricFunctionTest, countsOnlyRowsThatReachTheCall) {
+  // The shape of a MERGE clause: rows 2, 3 and 4 take the branch, row 1 does 
not, so each counter
+  // sees the rows of its own branch, exactly as Spark evaluates 
IncrementMetric. The literals are
+  // what Delta wraps; they must not be folded into constants, which would 
skip the count.
+  // Spark function names: the parser's `>` maps to a Presto name that Gluten 
does not register.
+  auto data = makeRowVector({makeFlatVector<int32_t>({1, 2, 3, 4})});
+  auto exprSet = compileExpression(
+      "if(greaterthan(c0, 1), increment_metric_1(true), 
increment_metric_2(false))", asRowType(data->type()));
+  auto result = evaluate(*exprSet, data);
+  facebook::velox::test::assertEqualVectors(makeFlatVector<bool>({false, true, 
true, true}), result);
+  auto stats = exprSet->stats();
+  ASSERT_EQ(stats.at("increment_metric_1").numProcessedRows, 3);
+  ASSERT_EQ(stats.at("increment_metric_2").numProcessedRows, 1);
+
+  // A counter over a literal at the root of a projection counts every row.
+  exprSet = compileExpression("increment_metric_3(true)", 
asRowType(data->type()));
+  evaluate(*exprSet, data);
+  ASSERT_EQ(exprSet->stats().at("increment_metric_3").numProcessedRows, 4);
+}
+
+TEST_F(IncrementMetricFunctionTest, countsEveryRowOfAnEncodedInput) {
+  // Peeling the dictionary would evaluate the counter on its 2 distinct 
values instead of 6 rows.
+  auto base = makeFlatVector<int32_t>({10, 20});
+  auto input = wrapInDictionary(makeIndices({0, 1, 0, 1, 0, 1}), 6, base);
+  auto data = makeRowVector({input});
+  auto exprSet = compileExpression("increment_metric_5(c0)", 
asRowType(data->type()));
+  auto result = evaluate(*exprSet, data);
+  facebook::velox::test::assertEqualVectors(input, result);
+  ASSERT_EQ(exprSet->stats().at("increment_metric_5").numProcessedRows, 6);
+}
+
+TEST_F(IncrementMetricFunctionTest, nullInputStillCounts) {
+  auto data = makeRowVector({makeNullableFlatVector<int64_t>({1, std::nullopt, 
std::nullopt})});
+  auto exprSet = compileExpression("increment_metric_4(c0)", 
asRowType(data->type()));
+  evaluate(*exprSet, data);
+  ASSERT_EQ(exprSet->stats().at("increment_metric_4").numProcessedRows, 3);
+}
+
+TEST_F(IncrementMetricFunctionTest, detectsCountersInAPlan) {
+  auto data = makeRowVector({makeFlatVector<int32_t>({1, 2})});
+  auto withCounter = 
exec::test::PlanBuilder().values({data}).project({"increment_metric_5(c0) AS 
c0"}).planNode();
+  ASSERT_TRUE(usesIncrementMetricFunctions(*withCounter));
+
+  auto nestedCounter = exec::test::PlanBuilder()
+                           .values({data})
+                           .project({"if(greaterthan(c0, 1), 
increment_metric_6(c0), c0) AS c0"})
+                           .filter("greaterthan(c0, 0)")
+                           .planNode();
+  ASSERT_TRUE(usesIncrementMetricFunctions(*nestedCounter));
+
+  auto plain =
+      exec::test::PlanBuilder().values({data}).project({"add(c0, 1) AS 
c0"}).filter("greaterthan(c0, 0)").planNode();
+  ASSERT_FALSE(usesIncrementMetricFunctions(*plain));
+}
+
+TEST_F(IncrementMetricFunctionTest, rejectsCountersOutsideProjections) {
+  auto data = makeRowVector({makeFlatVector<int32_t>({1, 2})});
+  // Substrait declarations, `<name>:<argument types>`, as the plan converter 
receives them.
+  const std::unordered_map<uint64_t, std::string> functionMap{
+      {0, "increment_metric_5:i32"}, {1, "greaterthan:i32_i32"}};
+
+  auto inProject = exec::test::PlanBuilder()
+                       .values({data})
+                       .project({"increment_metric_5(c0) AS c0"})
+                       .filter("greaterthan(c0, 0)")
+                       .planNode();
+  checkIncrementMetricFunctionsInProjects(functionMap, *inProject);
+
+  // The JVM credits counters from projections only, so a counter in a filter 
is rejected.
+  auto inFilter = 
exec::test::PlanBuilder().values({data}).filter("greaterthan(increment_metric_5(c0),
 0)").planNode();
+  ASSERT_FALSE(usesIncrementMetricFunctions(*inFilter));
+  VELOX_ASSERT_THROW(
+      checkIncrementMetricFunctionsInProjects(functionMap, *inFilter),
+      "Delta counter increment_metric_5 is declared but no projection calls 
it");
+
+  // Plans without counters are not inspected.
+  checkIncrementMetricFunctionsInProjects({{1, "greaterthan:i32_i32"}}, 
*inFilter);
+}
+
+TEST_F(IncrementMetricFunctionTest, 
queryConfigsKeepCountingUnderUserOverrides) {
+  // A counter at the root of a projection, like the single clause of an 
insert-only MERGE.
+  auto data = makeRowVector({makeFlatVector<int32_t>({1, 2, 3, 4})});
+  // Overrides a user can pass through 
spark.gluten.sql.columnar.backend.velox.*: simplified
+  // evaluation applies the counter without counting the rows it processes, 
and without the export
+  // FilterProject operators report no counts.
+  std::unordered_map<std::string, std::string> configs{
+      {core::QueryConfig::kExprEvalSimplified, "true"}, 
{core::QueryConfig::kOperatorTrackExpressionStats, "false"}};
+  setIncrementMetricQueryConfigs(configs);
+  queryCtx_->testingOverrideConfigUnsafe(std::move(configs));
+  ASSERT_FALSE(queryCtx_->queryConfig().exprEvalSimplified());
+  ASSERT_TRUE(queryCtx_->queryConfig().operatorTrackExpressionStats());
+
+  // FilterProject operators pick their evaluator from the query config the 
same way.
+  auto exprSet =
+      exec::makeExprSetFromFlag({makeTypedExpr("increment_metric_0(c0)", 
asRowType(data->type()))}, &execCtx_);
+  auto result = evaluate(*exprSet, data);
+  facebook::velox::test::assertEqualVectors(data->childAt(0), result);
+  const auto stats = exprSet->stats();
+  ASSERT_EQ(stats.count("increment_metric_0"), 1);
+  ASSERT_EQ(stats.at("increment_metric_0").numProcessedRows, 4);
+}
+
+} // namespace gluten


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

Reply via email to