-
Notifications
You must be signed in to change notification settings - Fork 344
feat: route translate and to_csv through codegen dispatch by default #5032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| -- 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in 26994c9. |
||
|
|
||
| -- literal struct (constant folding is disabled by the test suite) | ||
| query | ||
| SELECT | ||
| to_csv(named_struct('a', 1, 'b', 'x', 'c', 2.5)), | ||
| to_csv(named_struct('s', 'a,b', 'n', CAST(NULL AS INT))) | ||
|
|
||
| -- Options are the main axis of behavior difference for this expression, and the dispatcher path | ||
| -- had no options coverage anywhere (CometCsvExpressionSuite only varies them under | ||
| -- allowIncompatible=true). `sep` also changes which values need quoting. | ||
| query | ||
| SELECT to_csv(named_struct('a', a, 'b', b, 'c', c), map('sep', ';')) FROM test_to_csv | ||
|
|
||
| -- timestampFormat over date and timestamp fields: these are the #3232 types that were | ||
| -- Incompatible before this change and now route through the dispatcher. | ||
| -- BinaryType is the other #3232 type but is deliberately absent: on Spark 3.4 / 3.5 the CSV | ||
| -- converter has no binary branch and renders it with Java's default Object.toString(), e.g. | ||
| -- "[B@10bc15e4", an identity hash that differs between any two evaluations, so the value is not | ||
| -- assertable by any engine including Spark itself. (Spark 4.0 added a real binary formatter, but | ||
| -- this fixture runs on every supported version.) | ||
| statement | ||
| CREATE TABLE test_to_csv_temporal(d date, t timestamp) USING parquet | ||
|
|
||
| statement | ||
| INSERT INTO test_to_csv_temporal VALUES | ||
| (DATE '2024-01-31', TIMESTAMP '2024-01-31 12:34:56.789'), | ||
| (DATE '1970-01-01', TIMESTAMP '1970-01-01 00:00:00'), | ||
| (NULL, NULL) | ||
|
|
||
| query | ||
| SELECT to_csv(named_struct('d', d, 't', t)) FROM test_to_csv_temporal | ||
|
|
||
| query | ||
| SELECT to_csv(named_struct('d', d, 't', t), map('timestampFormat', 'yyyy/MM/dd HH:mm', 'dateFormat', 'dd-MM-yyyy')) FROM test_to_csv_temporal | ||
|
|
||
| -- Complex field types (arrays, maps, nested structs). `StructsToCsv.checkInputDataTypes` accepts | ||
| -- them and `CometBatchKernelCodegen.isSupportedDataType` recurses into them, so they pass the | ||
| -- plan-time gate and reach the runtime dispatch. That is the accepted-at-plan-time / | ||
| -- rejected-at-runtime shape #5219 found for TIME types: `canHandle` greenlights the expression | ||
| -- before the plan commits, so a runtime gap is an execute-time failure with no fallback left. | ||
| -- | ||
| -- Only non-nullness is asserted here, because the rendered value is not comparable on every | ||
| -- supported Spark version. Spark 3.4 / 3.5's `UnivocityGenerator.makeConverter` has no branch for | ||
| -- complex types and lands on `getter.get(ordinal, dataType).toString`, an identity string such as | ||
| -- "org.apache.spark.sql.vectorized.ColumnarArray@1ada50f0" whose hash differs between any two | ||
| -- evaluations and whose class differs between Spark's converter input (`ColumnarArray` / | ||
| -- `UnsafeArrayData`) and the kernel's (`InputArray_*`). That generic `get` is exactly what | ||
| -- `CometSpecializedGettersDispatch` implements for `CometInternalRow` / `CometArrayData`, so on | ||
| -- those versions these queries are the coverage for it. Spark 4.0 added real array/map/struct | ||
| -- converters, which render deterministically; `to_csv_nested.sql` asserts those values in full. | ||
| statement | ||
| CREATE TABLE test_to_csv_nested(s struct<i: int, arr: array<int>, m: map<string, int>, n: struct<x: int, y: string>>) USING parquet | ||
|
|
||
| statement | ||
| INSERT INTO test_to_csv_nested VALUES | ||
| (named_struct('i', 1, 'arr', array(1, 2, 3), 'm', map('k', 10), 'n', named_struct('x', 5, 'y', 'z'))), | ||
| (named_struct('i', NULL, 'arr', CAST(NULL AS array<int>), 'm', CAST(NULL AS map<string, int>), 'n', CAST(NULL AS struct<x: int, y: string>))), | ||
| (CAST(NULL AS struct<i: int, arr: array<int>, m: map<string, int>, n: struct<x: int, y: string>>)) | ||
|
|
||
| -- Struct column straight from the scan: the kernel reads it with getStruct, so the converter's | ||
| -- per-field reads of the array / map / struct fields all land on `CometInternalRow`. The all-null | ||
| -- and null-struct rows keep the assertion from collapsing to a constant true. | ||
| query | ||
| SELECT to_csv(s) IS NOT NULL FROM test_to_csv_nested | ||
|
|
||
| -- named_struct over the complex fields: the kernel's array / map / struct readers produce the | ||
| -- values that CreateNamedStruct stores into the row the converter then reads. | ||
| query | ||
| SELECT to_csv(named_struct('arr', s.arr, 'm', s.m, 'n', s.n)) IS NOT NULL FROM test_to_csv_nested | ||
|
|
||
| -- Complex types nested inside complex types, so the converter recurses through the kernel's | ||
| -- element getters rather than stopping at the first level. | ||
| query | ||
| SELECT to_csv(named_struct('aa', array(array(1, 2), array(3)), 'ms', map('k', named_struct('x', 1)))) IS NOT NULL FROM test_to_csv_nested | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| -- 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. | ||
|
|
||
| -- ANSI mode with a raising subtree inside the struct: `to_csv` routes through the codegen | ||
| -- dispatcher by default, and the whole tree (StructsToCsv over CreateNamedStruct over Cast) is | ||
| -- bound into one kernel, so the Cast's ANSI throw has to survive the dispatcher rather than being | ||
| -- swallowed into a NULL. That is the failure mode #5219 fixed for `CometBatchKernelCodegen`'s null | ||
| -- short-circuit, which skipped a subtree Spark would have evaluated when every input ordinal was | ||
| -- tested up front. | ||
| -- | ||
| -- `to_csv` does not reach the short-circuit today (CreateNamedStruct is not NullIntolerant, so | ||
| -- `allNullIntolerant` already fails), but the shape is one wire-up change away from mattering and | ||
| -- was untested for this expression, so these queries pin it. The second query is the exact #5219 | ||
| -- shape: two input ordinals where the non-cast one is NULL on the row whose cast raises. | ||
| -- Config: spark.sql.ansi.enabled=true | ||
| -- Config: spark.comet.exec.scalaUDF.codegen.enabled=true | ||
|
|
||
| statement | ||
| CREATE TABLE test_to_csv_ansi(s string, i int) USING parquet | ||
|
|
||
| statement | ||
| INSERT INTO test_to_csv_ansi VALUES | ||
| ('1', 10), | ||
| ('notanint', NULL) | ||
|
|
||
| query expect_error(CAST_INVALID_INPUT) | ||
| SELECT to_csv(named_struct('a', CAST(s AS INT))) FROM test_to_csv_ansi | ||
|
|
||
| query expect_error(CAST_INVALID_INPUT) | ||
| SELECT to_csv(named_struct('a', CAST(s AS INT), 'b', i)) FROM test_to_csv_ansi | ||
|
|
||
| -- The struct field's cast is the only raising node, so restricting to the well-formed row makes | ||
| -- the expression succeed. Doubles as the sentinel required by `ExpectError` fixtures: it uses | ||
| -- checkSparkAnswerAndOperator, so a silent dispatcher rejection (which would make the queries | ||
| -- above pass vacuously via a Spark fallback that raises the same error) fails here instead. | ||
| query | ||
| SELECT to_csv(named_struct('a', CAST(s AS INT), 'b', i)) FROM test_to_csv_ansi WHERE i IS NOT NULL | ||
|
|
||
| -- try_cast does not raise under ANSI, so the surrounding to_csv must render the NULL field with | ||
| -- the CSV nullValue rather than propagating an error. | ||
| query | ||
| SELECT to_csv(named_struct('a', TRY_CAST(s AS INT), 'b', i)) FROM test_to_csv_ansi |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| -- 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. | ||
|
|
||
| -- Full-value coverage of `to_csv` over complex field types, which `to_csv.sql` can only assert for | ||
| -- non-nullness because Spark 3.4 / 3.5 render a complex field as a Java identity string. Spark 4.0 | ||
| -- gave `UnivocityGenerator.makeConverter` real array / map / struct branches ("[1, 2, 3]", | ||
| -- "{k -> 10}", "{5, z}"), built from the typed `getArray` / `getMap` / `getStruct` getters, so from | ||
| -- 4.0 on the value is deterministic and Comet's kernel-side `CometInternalRow` / `CometArrayData` / | ||
| -- `CometMapData` getters have to agree with Spark's row-side ones element for element. | ||
| -- MinSparkVersion: 4.0 | ||
|
|
||
| statement | ||
| CREATE TABLE test_to_csv_nested4(s struct<i: int, arr: array<int>, m: map<string, int>, n: struct<x: int, y: string>>) USING parquet | ||
|
|
||
| statement | ||
| INSERT INTO test_to_csv_nested4 VALUES | ||
| (named_struct('i', 1, 'arr', array(1, 2, 3), 'm', map('k', 10), 'n', named_struct('x', 5, 'y', 'z'))), | ||
| (named_struct('i', 2, 'arr', array(), 'm', map(), 'n', named_struct('x', NULL, 'y', NULL))), | ||
| (named_struct('i', 3, 'arr', array(1, CAST(NULL AS int)), 'm', map('k', CAST(NULL AS int)), 'n', named_struct('x', 7, 'y', 'a,b'))), | ||
| (named_struct('i', NULL, 'arr', CAST(NULL AS array<int>), 'm', CAST(NULL AS map<string, int>), 'n', CAST(NULL AS struct<x: int, y: string>))), | ||
| (CAST(NULL AS struct<i: int, arr: array<int>, m: map<string, int>, n: struct<x: int, y: string>>)) | ||
|
|
||
| -- Struct column straight from the scan: the converter reads the array / map / struct fields off | ||
| -- the kernel's CometInternalRow. Empty collections, null elements and a value needing CSV quoting | ||
| -- are all in the data above. | ||
| query | ||
| SELECT to_csv(s) FROM test_to_csv_nested4 | ||
|
|
||
| -- nullValue changes how a null *element* inside a complex field renders (`appendNull` only emits | ||
| -- when the option was set explicitly), which is a separate code path from a null top-level field. | ||
| query | ||
| SELECT to_csv(s, map('nullValue', 'NIL')) FROM test_to_csv_nested4 | ||
|
|
||
| -- named_struct over the complex fields, so the values pass through CreateNamedStruct's row before | ||
| -- the converter reads them back. | ||
| query | ||
| SELECT to_csv(named_struct('arr', s.arr, 'm', s.m, 'n', s.n)) FROM test_to_csv_nested4 | ||
|
|
||
| -- Complex inside complex: the converter recurses, so an array-of-array read goes through the | ||
| -- kernel's element getters rather than stopping at the first level. | ||
| query | ||
| SELECT to_csv(named_struct('aa', array(array(1, 2), array(3)), 'ms', map('k', named_struct('x', 1)))) FROM test_to_csv_nested4 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that
to_csvroutes through the dispatcher by default for a struct built fromnamed_struct, would it be worth adding a case where one of the struct's fields is itself a cast that can raise under ANSI, for exampleto_csv(named_struct('a', CAST(bad_str AS INT)))? #5219 found and fixed a bug in the shared dispatcher for exactly this shape, and it merged after this branch's last commit, so there is not yet a test here that pins the interaction forto_csv.Would it also be worth adding a query that runs
to_csvover a struct with an array, map, or nested-struct field, even without asserting the exact output value? The comment block above explains well why Spark's own output for complex types is not a stable value to assert, but that is a different question from whether Comet's dispatcher path can hand a nestedInternalRow/ArrayData/MapDatathroughCometSpecializedGettersDispatchwithout hitting an unhandled exception of its own. That path is not exercised by any test in this PR, and it is the same class of gap #5219 found for TIME types.