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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-6805-634e657b1b76201dc230811b73c8eba18a891fae
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 4038bb1ae79daaf4963dab3bbf3ac1ef68a49dff
Author: Kary Zheng <[email protected]>
AuthorDate: Wed Jul 22 18:22:10 2026 -0700

    fix(MachineLearningScorer): splice metric_list verbatim instead of 
double-encoding (#6805)
    
    ### What changes were proposed in this PR?
    
    Fixes a double-encoding bug in `MachineLearningScorerOpDesc` where the
    selected metrics collapse into a single malformed `metric_list` element.
    
    `getSelectedMetrics()` returns the chosen metric names as one
    comma-separated, already-quoted fragment (e.g. `'Accuracy','F1 Score'`)
    and is spliced into the generated Python as a list:
    
    ```
    metric_list = [${getSelectedMetrics()}]
    ```
    
    Its return type was **`EncodableString`**, so the Python template
    builder **re-encoded the whole fragment as one Python string value**
    instead of splicing it verbatim. The emitted code became:
    
    ```python
    metric_list = [self.decode_python_template('J0FjY3VyYWN5JywnRjEgU2NvcmUn')]
    ```
    
    where the base64 `J0FjY3VyYWN5JywnRjEgU2NvcmUn` decodes to
    `'Accuracy','F1 Score'` — i.e. the entire quoted list collapses into
    **one** decoded string element:
    
    ```python
    metric_list = ["'Accuracy','F1 Score'"]   # ONE element, incl. inner quotes
    ```
    
    instead of the intended:
    
    ```python
    metric_list = ['Accuracy', 'F1 Score']
    ```
    
    So every downstream `if 'X' in metric_list` and `for metric in
    metric_list` / `metrics_func[metric]` operates on that single malformed
    element — the selected metrics are never matched, and the classification
    path hits a `KeyError` on the bogus key. The Scorer produces wrong/empty
    output for both the classification and regression paths.
    
    **Fix:** change `getSelectedMetrics()`'s return type from
    `EncodableString` to plain `String`, so the builder splices it verbatim
    into `metric_list = ['Accuracy','F1 Score']`. The method body is
    unchanged.
    
    ```diff
    -  private def getSelectedMetrics(): EncodableString = {
    +  private def getSelectedMetrics(): String = {
         val metric = if (isRegression) regressionMetrics else 
classificationMetrics
         metric.map(metric => getMetricName(metric)).mkString("'", "','", "'")
       }
    ```
    
    ### Any related issues, documentation, discussions?
    
    Closes #6790
    
    ### How was this PR tested?
    
    Added a regression test to `MachineLearningScorerOpDescSpec` that
    constructs the descriptor with `classificationMetrics = List(accuracy,
    f1Score)`, calls `generatePythonCode()`, and asserts:
    - the emitted code contains `metric_list = ['Accuracy','F1 Score']`
    (verbatim), and
    - the metric names are **not** base64-re-encoded through the template
    builder.
    
    Both assertions fail on `main` (the line is emitted as `metric_list =
    [self.decode_python_template('J0FjY3VyYWN5JywnRjEgU2NvcmUn')]`) and pass
    with this change.
    
    Ran the full suite locally:
    
    ```
    sbt "WorkflowOperator/testOnly 
org.apache.texera.amber.operator.machineLearning.Scorer.MachineLearningScorerOpDescSpec"
    ...
    Tests: succeeded 7, failed 0, canceled 0, ignored 0, pending 0
    All tests passed.
    ```
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Opus 4.8)
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../Scorer/MachineLearningScorerOpDesc.scala             |  6 ++++--
 .../Scorer/MachineLearningScorerOpDescSpec.scala         | 16 ++++++++++++++++
 2 files changed, 20 insertions(+), 2 deletions(-)

diff --git 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/Scorer/MachineLearningScorerOpDesc.scala
 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/Scorer/MachineLearningScorerOpDesc.scala
index a2f72a513e..e43c3f3947 100644
--- 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/Scorer/MachineLearningScorerOpDesc.scala
+++ 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/machineLearning/Scorer/MachineLearningScorerOpDesc.scala
@@ -122,8 +122,10 @@ class MachineLearningScorerOpDesc extends 
PythonOperatorDescriptor {
       case _                           => throw new 
IllegalArgumentException("Unknown metric type")
     }
 
-  private def getSelectedMetrics(): EncodableString = {
-    // Return a string of metrics using the getEachScorerName() method
+  // Must be a plain String, not EncodableString: this is a raw Python fragment
+  // spliced verbatim into `metric_list = [...]`. An EncodableString would be
+  // re-encoded as one quoted value, collapsing the list into a single element.
+  private def getSelectedMetrics(): String = {
     val metric = if (isRegression) regressionMetrics else classificationMetrics
     metric.map(metric => getMetricName(metric)).mkString("'", "','", "'")
   }
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/machineLearning/Scorer/MachineLearningScorerOpDescSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/machineLearning/Scorer/MachineLearningScorerOpDescSpec.scala
index 14909b5c9f..07e728596b 100644
--- 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/machineLearning/Scorer/MachineLearningScorerOpDescSpec.scala
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/machineLearning/Scorer/MachineLearningScorerOpDescSpec.scala
@@ -80,6 +80,22 @@ class MachineLearningScorerOpDescSpec extends AnyFlatSpec 
with Matchers {
     code should 
include(Base64.getEncoder.encodeToString("yhat".getBytes(StandardCharsets.UTF_8)))
   }
 
+  it should "splice the selected metrics verbatim into a proper metric_list" 
in {
+    // The metric fragment must be spliced verbatim, not re-encoded as one 
quoted
+    // value (which would collapse the whole list into a single malformed 
element).
+    val d = new MachineLearningScorerOpDesc
+    d.actualValueColumn = "y"
+    d.predictValueColumn = "yhat"
+    d.classificationMetrics =
+      List(classificationMetricsFnc.accuracy, classificationMetricsFnc.f1Score)
+    val code = d.generatePythonCode()
+    code should include("metric_list = ['Accuracy','F1 Score']")
+    // The metric names must NOT be base64-re-encoded through the template 
builder.
+    val encoded =
+      Base64.getEncoder.encodeToString("'Accuracy','F1 
Score'".getBytes(StandardCharsets.UTF_8))
+    code should not include encoded
+  }
+
   "MachineLearningScorerOpDesc" should "round-trip its config fields through 
the polymorphic base" in {
     val d = new MachineLearningScorerOpDesc
     d.isRegression = true

Reply via email to