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
