mbutrovich commented on code in PR #5032:
URL: https://github.com/apache/datafusion-comet/pull/5032#discussion_r3658190951


##########
spark/src/main/scala/org/apache/comet/serde/strings.scala:
##########
@@ -109,15 +109,38 @@ object CometOctetLength extends 
CometScalarFunction[OctetLength]("octet_length")
   }
 }
 
-object CometStringTranslate extends 
CometScalarFunction[StringTranslate]("translate") {
+object CometStringTranslate
+    extends CometExpressionSerde[StringTranslate]
+    with NativeOptInAvailable {
   private val incompatReason =
     "DataFusion's translate iterates over Unicode graphemes (Spark uses code 
points) and" +
       " substitutes U+0000 instead of treating it as a deletion sentinel"
 
   override def getIncompatibleReasons(): Seq[String] = Seq(incompatReason)
 
-  override def getSupportLevel(expr: StringTranslate): SupportLevel = 
Incompatible(
-    Some(incompatReason))
+  override def getSupportLevel(expr: StringTranslate): SupportLevel =
+    if (!CometConf.isExprAllowIncompat(getExprConfigName(expr))) {
+      Compatible(nativeOptIn =
+        
Some(NativeOptIn(CometConf.getExprAllowIncompatConfigKey(getExprConfigName(expr)))))
+    } else {
+      Compatible()
+    }
+
+  override def convert(
+      expr: StringTranslate,
+      inputs: Seq[Attribute],
+      binding: Boolean): Option[Expr] = {
+    if (CometConf.isExprAllowIncompat(getExprConfigName(expr))) {
+      val childExprs = expr.children.map(exprToProtoInternal(_, inputs, 
binding))
+      val optExpr = scalarFunctionExprToProto("translate", childExprs: _*)
+      optExprWithFallbackReason(optExpr, expr, expr.children: _*)

Review Comment:
   First, lines 134-136 are a verbatim copy of `CometScalarFunction.convert` 
(`spark/src/main/scala/org/apache/comet/serde/CometScalarFunction.scala:29-33`),
 so the function name `"translate"` now lives in two places. Dropping the 
`CometScalarFunction[StringTranslate]("translate")` base to hand-inline its 
body is also a step away from the established idiom for this exact case, which 
keeps the base and overrides `convert` to choose a branch: `CometInitCap` 
(`strings.scala:162-174`), `CometStringReplace` (`:192-199`), 
`CometCaseConversionBase` (`:67-78`).
   
   Second, and more to the point: the whole `getSupportLevel` plus `convert` 
branching structure is what `CodegenDispatchFallback` exists to express 
declaratively. `QueryPlanSerde.exprToProtoInternal` already routes a 
non-opted-in `Incompatible` result from such a serde through 
`emitJvmCodegenDispatch` and falls back to Spark only when the dispatcher 
refuses (`QueryPlanSerde.scala:838-870`, `dispatchIfFallback` at `:1109-1117`), 
and `CodegenDispatchFallback extends NativeOptInAvailable` so `GenerateDocs` 
renders the same compatible-by-default row (`GenerateDocs.scala:193`). So the 
entire diff to this object should be one mixin on the original:
   
   ```scala
   object CometStringTranslate
       extends CometScalarFunction[StringTranslate]("translate")
       with CodegenDispatchFallback {
     private val incompatReason = ...
     override def getIncompatibleReasons(): Seq[String] = Seq(incompatReason)
     override def getSupportLevel(expr: StringTranslate): SupportLevel = 
Incompatible(Some(incompatReason))
   }
   ```
   
   That keeps the existing `Incompatible` declaration honest, keeps the 
function name in one place, and gets the `logWarning` on the opted-in path 
(`QueryPlanSerde.scala:841-846`) that the hand-rolled version drops.



##########
spark/src/main/scala/org/apache/comet/serde/structs.scala:
##########
@@ -259,50 +259,50 @@ object CometJsonToStructs extends 
CometCodegenDispatch[JsonToStructs] with Nativ
   }
 }
 
-object CometStructsToCsv extends CometExpressionSerde[StructsToCsv] {
+object CometStructsToCsv extends CometCodegenDispatch[StructsToCsv] with 
NativeOptInAvailable {
 
   private val incompatibleDataTypes = Seq(DateType, TimestampType, 
TimestampNTZType, BinaryType)
 
   override def getIncompatibleReasons(): Seq[String] = Seq(
     "Date, Timestamp, TimestampNTZ, and Binary data types may produce 
different results" +
       " (https://github.com/apache/datafusion-comet/issues/3232)")
 
-  override def getUnsupportedReasons(): Seq[String] = Seq(
-    "Complex types (arrays, maps, structs) in the schema are not supported")
-
-  override def getSupportLevel(expr: StructsToCsv): SupportLevel = {
+  // The native ToCsv path only supports non-complex, compatible field types. 
Everything else
+  // (and the default, unless opted in) runs through the codegen dispatcher, 
which is bit-exact.
+  private def nativeSupported(expr: StructsToCsv): Boolean = {
     val dataTypes = expr.inputSchema.fields.map(_.dataType)
-    val containsComplexType = dataTypes.exists(DataTypeSupport.isComplexType)
-    if (containsComplexType) {
-      return Unsupported(
-        Some(
-          s"The schema ${expr.inputSchema} is not supported because it 
includes a complex type"))
-    }
-    val containsIncompatibleDataTypes = 
dataTypes.exists(incompatibleDataTypes.contains)
-    if (containsIncompatibleDataTypes) {
-      return Incompatible(
-        Some(
-          s"The schema ${expr.inputSchema} is not supported because " +
-            s"it includes a incompatible data types: $incompatibleDataTypes"))
-    }
-    // https://github.com/apache/datafusion-comet/issues/3232
-    Incompatible()
+    !dataTypes.exists(DataTypeSupport.isComplexType) &&
+    !dataTypes.exists(incompatibleDataTypes.contains)
   }
 
+  override def getSupportLevel(expr: StructsToCsv): SupportLevel =
+    if (!CometConf.isExprAllowIncompat(getExprConfigName(expr)) && 
nativeSupported(expr)) {
+      Compatible(nativeOptIn =
+        
Some(NativeOptIn(CometConf.getExprAllowIncompatConfigKey(getExprConfigName(expr)))))
+    } else {
+      Compatible()
+    }
+
   override def convert(
       expr: StructsToCsv,
       inputs: Seq[Attribute],
       binding: Boolean): Option[ExprOuterClass.Expr] = {
-    for {
-      childProto <- exprToProtoInternal(expr.child, inputs, binding)
-    } yield {
-      val optionsProto = options2Proto(expr.options, expr.timeZoneId)
-      val toCsv = ExprOuterClass.ToCsv
-        .newBuilder()
-        .setChild(childProto)
-        .setOptions(optionsProto)
-        .build()
-      ExprOuterClass.Expr.newBuilder().setToCsv(toCsv).build()
+    if (CometConf.isExprAllowIncompat(getExprConfigName(expr)) && 
nativeSupported(expr)) {

Review Comment:
   When `isExprAllowIncompat` is true and `nativeSupported` is false, the user 
asked for the native path and silently gets the dispatcher with nothing 
recorded. `CometStructsToJson` has the same hole so this is not new, but it 
deserves a `logDebug` naming the disqualifying field type. Using 
`CodegenDispatchFallback` gets this for free through the `Unsupported` arm.



##########
spark/src/main/scala/org/apache/comet/serde/structs.scala:
##########
@@ -259,50 +259,50 @@ object CometJsonToStructs extends 
CometCodegenDispatch[JsonToStructs] with Nativ
   }
 }
 
-object CometStructsToCsv extends CometExpressionSerde[StructsToCsv] {
+object CometStructsToCsv extends CometCodegenDispatch[StructsToCsv] with 
NativeOptInAvailable {
 
   private val incompatibleDataTypes = Seq(DateType, TimestampType, 
TimestampNTZType, BinaryType)
 
   override def getIncompatibleReasons(): Seq[String] = Seq(
     "Date, Timestamp, TimestampNTZ, and Binary data types may produce 
different results" +
       " (https://github.com/apache/datafusion-comet/issues/3232)")
 
-  override def getUnsupportedReasons(): Seq[String] = Seq(
-    "Complex types (arrays, maps, structs) in the schema are not supported")
-
-  override def getSupportLevel(expr: StructsToCsv): SupportLevel = {
+  // The native ToCsv path only supports non-complex, compatible field types. 
Everything else
+  // (and the default, unless opted in) runs through the codegen dispatcher, 
which is bit-exact.
+  private def nativeSupported(expr: StructsToCsv): Boolean = {
     val dataTypes = expr.inputSchema.fields.map(_.dataType)
-    val containsComplexType = dataTypes.exists(DataTypeSupport.isComplexType)
-    if (containsComplexType) {
-      return Unsupported(
-        Some(
-          s"The schema ${expr.inputSchema} is not supported because it 
includes a complex type"))
-    }
-    val containsIncompatibleDataTypes = 
dataTypes.exists(incompatibleDataTypes.contains)
-    if (containsIncompatibleDataTypes) {
-      return Incompatible(
-        Some(
-          s"The schema ${expr.inputSchema} is not supported because " +
-            s"it includes a incompatible data types: $incompatibleDataTypes"))
-    }
-    // https://github.com/apache/datafusion-comet/issues/3232
-    Incompatible()
+    !dataTypes.exists(DataTypeSupport.isComplexType) &&
+    !dataTypes.exists(incompatibleDataTypes.contains)
   }
 
+  override def getSupportLevel(expr: StructsToCsv): SupportLevel =
+    if (!CometConf.isExprAllowIncompat(getExprConfigName(expr)) && 
nativeSupported(expr)) {
+      Compatible(nativeOptIn =
+        
Some(NativeOptIn(CometConf.getExprAllowIncompatConfigKey(getExprConfigName(expr)))))
+    } else {
+      Compatible()
+    }
+
   override def convert(
       expr: StructsToCsv,
       inputs: Seq[Attribute],
       binding: Boolean): Option[ExprOuterClass.Expr] = {
-    for {
-      childProto <- exprToProtoInternal(expr.child, inputs, binding)
-    } yield {
-      val optionsProto = options2Proto(expr.options, expr.timeZoneId)
-      val toCsv = ExprOuterClass.ToCsv
-        .newBuilder()
-        .setChild(childProto)
-        .setOptions(optionsProto)
-        .build()
-      ExprOuterClass.Expr.newBuilder().setToCsv(toCsv).build()
+    if (CometConf.isExprAllowIncompat(getExprConfigName(expr)) && 
nativeSupported(expr)) {
+      for {
+        childProto <- exprToProtoInternal(expr.child, inputs, binding)
+      } yield {
+        val optionsProto = options2Proto(expr.options, expr.timeZoneId)
+        val toCsv = ExprOuterClass.ToCsv
+          .newBuilder()
+          .setChild(childProto)
+          .setOptions(optionsProto)
+          .build()
+        ExprOuterClass.Expr.newBuilder().setToCsv(toCsv).build()
+      }
+    } else {
+      // Default: route through the codegen dispatcher so Spark's own 
doGenCode runs inside the

Review Comment:
   The same argument applies here, and more strongly because the pre-existing 
`getSupportLevel` already returned exactly the right classifications: 
`Unsupported` for complex field types, `Incompatible` for the #3232 types, 
`Incompatible()` otherwise. With `with CodegenDispatchFallback` on line 262 
those three outcomes route as intended with no other change: `Unsupported` to 
the dispatcher plus a `logDebug` (`QueryPlanSerde.scala:827-833`), non-opted-in 
`Incompatible` to the dispatcher plus the `[COMET-INFO]` opt-in hint 
(`:854-860`), opted-in `Incompatible` to the native proto (`:847`). That turns 
a +30/-30 rewrite into a one-line change and removes the duplicated 
`nativeSupported(expr)` test that currently has to be kept in sync between 
`:279` and `:290`.
   
   Deleting `getUnsupportedReasons` (was `structs.scala:270-271`) also loses 
real information. The native path still cannot do complex types, and under 
`NativeOptInAvailable` the documented surface for native-path limitations is 
`getIncompatibleReasons` 
(`spark/src/main/scala/org/apache/comet/serde/CometExpressionSerde.scala:52-63`).
 Keeping `CodegenDispatchFallback` preserves both accessors as-is; if you keep 
the rewrite instead, the complex-type limitation needs folding into 
`getIncompatibleReasons` at `:266-268`, otherwise a user who sets 
`allowIncompatible` has no way to find out why they did not get the native path.



##########
spark/src/test/resources/sql-tests/expressions/string/string_translate.sql:
##########
@@ -15,29 +15,29 @@
 -- specific language governing permissions and limitations
 -- under the License.
 
--- translate is gated as Incompatible by default. DataFusion's translate 
iterates over Unicode
--- graphemes (Spark uses code points) and substitutes U+0000 instead of 
treating it as a deletion
--- sentinel, so the native path silently diverges from Spark for 
combining-mark inputs and for
--- to=NUL. These default-config tests assert that the expression falls back 
cleanly to Spark.
--- See string_translate_enabled.sql for the opt-in native path.
+-- translate runs through the codegen dispatcher by default so results match 
Spark exactly. The
+-- native path diverges from Spark (DataFusion iterates over Unicode graphemes 
where Spark uses code
+-- points, and substitutes U+0000 instead of treating it as a deletion 
sentinel), so it is opt-in
+-- via spark.comet.expression.StringTranslate.allowIncompatible. See 
string_translate_enabled.sql
+-- for the opt-in native path.
 
 statement
 CREATE TABLE test_translate(s string, from_str string, to_str string) USING 
parquet
 
 statement
 INSERT INTO test_translate VALUES ('hello', 'el', 'ip'), ('hello', 'aeiou', 
'12345'), ('', 'a', 'b'), (NULL, 'a', 'b'), ('hello', '', ''), ('abc', 'abc', 
'x')

Review Comment:
   The four `query` blocks do assert native execution, so flipping them off 
`expect_fallback` is real new coverage of the routing. What is missing is the 
payload: the header comment at `:18-22` names two specific ways the native path 
diverges, and the six rows on line 28 are ASCII-only and hit neither. The file 
is now identical to `string_translate_enabled.sql` in CREATE TABLE, INSERT rows 
and queries, differing only in the config header, so nothing distinguishes the 
dispatcher from the native path here. Please add the two rows that are only 
assertable now that the default is bit-exact: a combining-mark input for the 
grapheme versus code-point difference, and a `to` argument containing U+0000 
for the deletion-sentinel difference. To keep the fixture ASCII, build them 
with `decode(X'65CC81', 'UTF-8')` and `decode(X'00', 'UTF-8')` rather than 
embedding the characters. Those two rows are the regression test for this PR.



##########
spark/src/main/scala/org/apache/comet/serde/structs.scala:
##########
@@ -259,50 +259,50 @@ object CometJsonToStructs extends 
CometCodegenDispatch[JsonToStructs] with Nativ
   }
 }
 
-object CometStructsToCsv extends CometExpressionSerde[StructsToCsv] {
+object CometStructsToCsv extends CometCodegenDispatch[StructsToCsv] with 
NativeOptInAvailable {
 
   private val incompatibleDataTypes = Seq(DateType, TimestampType, 
TimestampNTZType, BinaryType)
 
   override def getIncompatibleReasons(): Seq[String] = Seq(
     "Date, Timestamp, TimestampNTZ, and Binary data types may produce 
different results" +
       " (https://github.com/apache/datafusion-comet/issues/3232)")
 
-  override def getUnsupportedReasons(): Seq[String] = Seq(
-    "Complex types (arrays, maps, structs) in the schema are not supported")
-
-  override def getSupportLevel(expr: StructsToCsv): SupportLevel = {
+  // The native ToCsv path only supports non-complex, compatible field types. 
Everything else
+  // (and the default, unless opted in) runs through the codegen dispatcher, 
which is bit-exact.
+  private def nativeSupported(expr: StructsToCsv): Boolean = {
     val dataTypes = expr.inputSchema.fields.map(_.dataType)
-    val containsComplexType = dataTypes.exists(DataTypeSupport.isComplexType)
-    if (containsComplexType) {
-      return Unsupported(
-        Some(
-          s"The schema ${expr.inputSchema} is not supported because it 
includes a complex type"))
-    }
-    val containsIncompatibleDataTypes = 
dataTypes.exists(incompatibleDataTypes.contains)
-    if (containsIncompatibleDataTypes) {
-      return Incompatible(
-        Some(
-          s"The schema ${expr.inputSchema} is not supported because " +
-            s"it includes a incompatible data types: $incompatibleDataTypes"))
-    }
-    // https://github.com/apache/datafusion-comet/issues/3232
-    Incompatible()
+    !dataTypes.exists(DataTypeSupport.isComplexType) &&
+    !dataTypes.exists(incompatibleDataTypes.contains)
   }

Review Comment:
   `nativeSupported` is the new branch predicate driving both `getSupportLevel` 
(`:279`) and `convert` (`:290`), and no test takes it false. The two cases it 
excludes are the reason the change exists:
   
   Date, Timestamp, TimestampNTZ and Binary fields were `Incompatible` before 
this PR and fell the operator back to Spark. That is #3232.
   
   Complex field types were `Unsupported` before and also fell back. They are 
legal `to_csv` input: `StructsToCsv.checkInputDataTypes` accepts arrays, maps 
and nested structs and rejects only Variant (Spark 
`csvExpressions.scala:240-261` on master and branch-4.0; branch-3.5 has no gate 
at all). Nothing tests that the kernel can hand `converter()` a nested 
`InternalRow`, which is the new and untried part of this path.
   
   The cheapest place to cover the first group is the existing suite rather 
than a new fixture. `CometCsvExpressionSuite`'s test named "to_csv - default 
options" 
(`spark/src/test/scala/org/apache/comet/CometCsvExpressionSuite.scala:36`) is 
actually run under `allowIncompatible=true` (`:50`), and it selects c0-c5, c7, 
c8, c9 and c12 out of the 14-column fuzz schema, skipping exactly c6 Double, 
c10 Timestamp, c11 TimestampNTZ and c13 Binary 
(`FuzzDataGenerator.scala:303-317`). Those are the columns the native path 
cannot match Spark on. After this PR the dispatcher matches them by 
construction, so that test can drop the `allowIncompatible` wrapper and add 
those four columns, which is what would actually justify the name it already 
has.



##########
spark/src/test/resources/sql-tests/expressions/csv/to_csv.sql:
##########
@@ -0,0 +1,41 @@
+-- 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.
+
+-- to_csv runs through the codegen dispatcher by default so results match 
Spark exactly, including
+-- quoting and escaping. The native path is opt-in via
+-- spark.comet.expression.StructsToCsv.allowIncompatible.
+
+statement
+CREATE TABLE test_to_csv(a int, b string, c double) USING parquet
+
+statement
+INSERT INTO test_to_csv VALUES
+  (1, 'x', 2.5),
+  (-3, 'hello,world', 0.0),
+  (0, 'has "quote"', -1.5),
+  (NULL, NULL, NULL),
+  (7, '', 3.0)
+
+-- column struct: values with delimiters and quotes exercise Spark's CSV 
quoting rules
+query
+SELECT to_csv(named_struct('a', a, 'b', b, 'c', c)) FROM test_to_csv

Review Comment:
   `options` is the main axis of behavior difference for this expression and 
nothing in the new fixture varies it. `CometCsvExpressionSuite:71-161` covers 
delimiter, escape, quoteAll and nullValue but only under 
`allowIncompatible=true`, so the dispatcher path has no options coverage 
anywhere. `to_csv(s, map('sep', ';'))` plus a `timestampFormat` over a 
timestamp field would cover both this and the #3232 types in one query.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to