GH-43660: [C++] Add a CastingGenerator to Parquet Reader that applies required casts before slicing#43661
GH-43660: [C++] Add a CastingGenerator to Parquet Reader that applies required casts before slicing#43661sahil1105 wants to merge 21 commits into
CastingGenerator to Parquet Reader that applies required casts before slicing#43661Conversation
|
|
CastingGenerator to Parquet Reader that applies required casts before slicingCastingGenerator to Parquet Reader that applies required casts before slicing
226d2d1 to
f1c0d6e
Compare
| @@ -555,6 +562,57 @@ Future<std::shared_ptr<parquet::arrow::FileReader>> ParquetFileFormat::GetReader | |||
| }); | |||
| } | |||
|
|
|||
| struct CastingGenerator { | |||
There was a problem hiding this comment.
I'm a bit curious why casting generator is required, since during reading parquet to arrow, Parquet reader already applies a round of casting.
There was a problem hiding this comment.
You're probably right and it likely does perform some casts during the read. However, it doesn't seem to be doing it for certain cases like String to LargeString.
Could you point me to where the Parquet Reader should be performing this cast? I'm happy to add it there.
There was a problem hiding this comment.
There was a problem hiding this comment.
Thank you!
Based on what I see, that is only responsible for casting the data to the logical type specified in the parquet metadata and not the Arrow type we want to convert to (the one in the dataset_schema). For strings, that seems to always map to a String type (based on FromByteArray which is called by GetArrowType which is called by GetTypeForNode which is called by NodeToSchemaField which is called in SchemaManifest::Make during the creation of the LeafReader). Am I missing something?
There was a problem hiding this comment.
Based on what I see, that is only responsible for casting the data to the logical type specified in the parquet metadata and not the Arrow type we want to convert to (the one in the dataset_schema)
Parquet logical type doesn't have an arrow schema, isn't it? Binary reader reads from ::arrow::BinaryBuilder, and casting it to user-specified binary type.
For strings, that seems to always map to a String type (based on FromByteArray which is called by GetArrowType which is called by GetTypeForNode which is called by NodeToSchemaField which is called in SchemaManifest::Make during the creation of the LeafReader).
Yeah, you're right, the read "cast" with file-schema rather than an expected schema. I think a native cast is better here but this doesn't solve your problem, perhaps I can trying to add a naive SchemaManifest with hint solving here, but it would spend some time.
::arrow::Result<std::shared_ptr<ArrowType>> GetTypeForNode(
int column_index, const schema::PrimitiveNode& primitive_node,
SchemaTreeContext* ctx)
Maybe we should rethink the GetTypeForNode handling for string/large_string/stringView, or using some handle written type hint here. A casting generator is also good for me when the reader cannot provide the right casting
There was a problem hiding this comment.
Provide hint would looks like: apache/arrow-rs#5939
Maybe I can add separate issue for that
There was a problem hiding this comment.
Parquet logical type doesn't have an arrow schema, isn't it?
As far as I understand, the parquet metadata may or may not have the arrow schema. I believe it depends on the writer. It looks like it tries to get that using GetOriginSchema in SchemaManifest::Make. However, the schema at write time might not be the same as the schema the reader expects.
Binary reader reads from ::arrow::BinaryBuilder, and casting it to user-specified binary type.
Sorry, I didn't quite follow. Are you saying that we should use this to do the cast at read time somehow?
I think a native cast is better here but this doesn't solve your problem, perhaps I can trying to add a naive SchemaManifest with hint solving here, but it would spend some time.
Maybe we should rethink the GetTypeForNode handling for string/large_string/stringView, or using some handle written type hint here.
That makes sense to me.
Maybe I can add separate issue for that
That would be great, thanks!
There was a problem hiding this comment.
I just check this. We can first add a CastingGenerator, because sometimes we would have schema evolution here, and need cast to a "final type". The Parquet reader code can be regard as an optimization
There was a problem hiding this comment.
Sounds good, thanks!
bdac54a to
e832a0d
Compare
|
I'm not sure why the "Java JNI / AMD64 manylinux2014 Java JNI (pull_request)" and "continuous-integration/appveyor/pr" are failing. The latter in particular passed on the previous commit which is the same as this commit (my last commit was an empty commit to trigger the CI again). Any insights would be appreciated. I would also appreciate feedback on if and what unit tests need to be added for this change. It seems like the existing unit tests provide good coverage of this code (they helped me find some of the bugs), but I'm happy to add more if it is deemed useful. |
|
The interface general LGTM. I'm a little busy on working days, and will take a careful around in weekend. You also can mark it as ready for review here |
Thanks, I appreciate it! |
|
@mapleFU If you get a chance to review this PR this weekend, that'd be great. Thanks! |
|
Sorry for delaying I'll take a pass today. I'm out today so I didn't finish review, will continue after I wake up |
mapleFU
left a comment
There was a problem hiding this comment.
The idea looks great to me, but I don't know would setting memory to maximum matters
| if (this->cols_to_skip_.count(field->name())) { | ||
| // Maintain the original input type. | ||
| out_schema_fields.emplace_back(field->WithType(column->type())); | ||
| out_cols.emplace_back(std::move(column)); |
There was a problem hiding this comment.
So cols_to_skip_ is just not "Project" this field, rather than not need this field?
There was a problem hiding this comment.
Yes, to skip the cast and leave them as they are.
| @@ -617,6 +684,9 @@ Result<RecordBatchGenerator> ParquetFileFormat::ScanBatchesAsync( | |||
| [this, options, parquet_fragment, pre_filtered, | |||
| row_groups](const std::shared_ptr<parquet::arrow::FileReader>& reader) mutable | |||
| -> Result<RecordBatchGenerator> { | |||
| // Since we already do the batching through the SlicingGenerator, we don't need the | |||
| // reader to batch its output. | |||
| reader->set_batch_size(std::numeric_limits<int64_t>::max()); | |||
| // Ensure that parquet_fragment has FileMetaData | |||
There was a problem hiding this comment.
Assuming a large file, would memory usage grows high in this case?
There was a problem hiding this comment.
I think we have that problem regardless of the batch size of the reader.
We pass this reader to reader->GetRecordBatchGenerator (a few lines down). This eventually creates a RowGroupGenerator (
arrow/cpp/src/parquet/arrow/reader.cc
Line 1204 in 6a2e19a
FetchNext for RowGroupGenerator, it essentially calls ReadOneRowGroup which reads the entire row group into a table and then creates an output stream using TableBatchReader (arrow/cpp/src/parquet/arrow/reader.cc
Line 1170 in 6a2e19a
batch_size just creates a reader on top of it which generates zero-copy slices. This is the same as what the SlicingGenerator does and is redundant. In my opinion, it's better to set the batch size to INT_MAX so that it returns one table per row group, and then we can perform the batching through the SlicingGenerator.
| // We need to skip casting the dictionary columns since the dataset_schema doesn't | ||
| // have the dictionary-encoding information. Parquet reader will return them with the | ||
| // dictionary type, which is what we eventually want. |
There was a problem hiding this comment.
I've forget something here, would Dict(String) and Cast(xxx -> LargeString) matters?
There was a problem hiding this comment.
I wasn't sure, so I left that case untouched. There are more casts done further up the chain (e.g. in MakeExecBatch potentially) that seem to handle those cases. I couldn't figure out how the dictionary case gets handled either, so I left it as is. I'm happy to implement it here if you could point me in the right direction.
…s before slices are created
912db12 to
f1a4955
Compare
|
Also cc @bkietz as the expert here |
|
cc @srilman |
|
Hello! I was interested in picking up this PR from Sahil since the use case still seems relevant to us. I tested locally with the latest changes on main and it looked like this PR can improve performance of the Scanner quite a bit, although there were some concerns about the robustness of setting the batch size to INT_MAX in Was wondering if I could get some a review/feedback on whether this is headed in the right direction. Thanks! |
|
|
||
| BENCHMARK(ParquetScanToTableCastStrings); | ||
|
|
||
| } // namespace dataset |
There was a problem hiding this comment.
Local results (comparing to main):
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Non-regressions: (20)
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
benchmark baseline contender change % counters
ParquetScanToTableCastStrings/num_batches:1000/batch_size:1000 1.068M items/sec 10.574M items/sec 890.391 {'family_index': 0, 'per_family_instance_index': 2, 'run_name': 'ParquetScanToTableCastStrings/num_batches:1000/batch_size:1000', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 1}
ParquetScanToTableCastStrings/num_batches:1000/batch_size:100 761.915K items/sec 1.065M items/sec 39.760 {'family_index': 0, 'per_family_instance_index': 1, 'run_name': 'ParquetScanToTableCastStrings/num_batches:1000/batch_size:100', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 4}
ParquetScanToTableCastStrings/num_batches:1000/batch_size:10 86.631K items/sec 107.530K items/sec 24.124 {'family_index': 0, 'per_family_instance_index': 0, 'run_name': 'ParquetScanToTableCastStrings/num_batches:1000/batch_size:10', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 5}
ScanOnlyBench/num_batches:1000/batch_size:1000/scan_alg:0/real_time 41.041 MiB/sec 49.586 MiB/sec 20.820 {'family_index': 1, 'per_family_instance_index': 4, 'run_name': 'ScanOnlyBench/num_batches:1000/batch_size:1000/scan_alg:0/real_time', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 6}
ScanOnlyBench/num_batches:1000/batch_size:10/scan_alg:0/real_time 457.346 KiB/sec 518.384 KiB/sec 13.346 {'family_index': 1, 'per_family_instance_index': 0, 'run_name': 'ScanOnlyBench/num_batches:1000/batch_size:10/scan_alg:0/real_time', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 6}
ScanOnlyBench/num_batches:1000/batch_size:100/scan_alg:0/real_time 4.421 MiB/sec 5.011 MiB/sec 13.339 {'family_index': 1, 'per_family_instance_index': 2, 'run_name': 'ScanOnlyBench/num_batches:1000/batch_size:100/scan_alg:0/real_time', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 6}
ScanOnlyBench/num_batches:1000/batch_size:10/scan_alg:1/real_time 634.336 KiB/sec 718.653 KiB/sec 13.292 {'family_index': 1, 'per_family_instance_index': 1, 'run_name': 'ScanOnlyBench/num_batches:1000/batch_size:10/scan_alg:1/real_time', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 9}
MinimalEndToEndBench/num_batches:1000/batch_size:100/scan_alg:0/real_time 4.563 MiB/sec 5.089 MiB/sec 11.535 {'family_index': 0, 'per_family_instance_index': 2, 'run_name': 'MinimalEndToEndBench/num_batches:1000/batch_size:100/scan_alg:0/real_time', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 7}
MinimalEndToEndBench/num_batches:1000/batch_size:10/scan_alg:1/real_time 624.277 KiB/sec 680.752 KiB/sec 9.047 {'family_index': 0, 'per_family_instance_index': 1, 'run_name': 'MinimalEndToEndBench/num_batches:1000/batch_size:10/scan_alg:1/real_time', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 9}
MinimalEndToEndBench/num_batches:1000/batch_size:10/scan_alg:0/real_time 483.179 KiB/sec 522.102 KiB/sec 8.055 {'family_index': 0, 'per_family_instance_index': 0, 'run_name': 'MinimalEndToEndBench/num_batches:1000/batch_size:10/scan_alg:0/real_time', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 7}
ScanOnlyBench/num_batches:1000/batch_size:100/scan_alg:1/real_time 6.578 MiB/sec 7.080 MiB/sec 7.645 {'family_index': 1, 'per_family_instance_index': 3, 'run_name': 'ScanOnlyBench/num_batches:1000/batch_size:100/scan_alg:1/real_time', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 9}
MinimalEndToEndBench/num_batches:1000/batch_size:1000/scan_alg:0/real_time 46.819 MiB/sec 49.794 MiB/sec 6.353 {'family_index': 0, 'per_family_instance_index': 4, 'run_name': 'MinimalEndToEndBench/num_batches:1000/batch_size:1000/scan_alg:0/real_time', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 7}
ScanOnlyBench/num_batches:1000/batch_size:1000/scan_alg:1/real_time 67.173 MiB/sec 70.629 MiB/sec 5.145 {'family_index': 1, 'per_family_instance_index': 5, 'run_name': 'ScanOnlyBench/num_batches:1000/batch_size:1000/scan_alg:1/real_time', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 9}
GetFilteredFragments/single_file 1.501M items/sec 1.557M items/sec 3.709 {'family_index': 3, 'per_family_instance_index': 0, 'run_name': 'GetFilteredFragments/single_file', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 105, 'num_filtered_fragments': 1.0, 'num_fragments': 10000.0}
MinimalEndToEndBench/num_batches:1000/batch_size:100/scan_alg:1/real_time 6.327 MiB/sec 6.481 MiB/sec 2.430 {'family_index': 0, 'per_family_instance_index': 3, 'run_name': 'MinimalEndToEndBench/num_batches:1000/batch_size:100/scan_alg:1/real_time', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 9}
GetFilteredFragments/range 229.364K items/sec 232.693K items/sec 1.451 {'family_index': 4, 'per_family_instance_index': 0, 'run_name': 'GetFilteredFragments/range', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 16, 'num_filtered_fragments': 9800.0, 'num_fragments': 10000.0}
MinimalEndToEndBench/num_batches:1000/batch_size:1000/scan_alg:1/real_time 61.721 MiB/sec 62.435 MiB/sec 1.156 {'family_index': 0, 'per_family_instance_index': 5, 'run_name': 'MinimalEndToEndBench/num_batches:1000/batch_size:1000/scan_alg:1/real_time', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 9}
GetFilteredFragments/single_dir 3.419M items/sec 3.444M items/sec 0.733 {'family_index': 1, 'per_family_instance_index': 0, 'run_name': 'GetFilteredFragments/single_dir', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 236, 'num_filtered_fragments': 100.0, 'num_fragments': 10000.0}
GetFilteredFragments/multi_dir 40.060K items/sec 40.215K items/sec 0.386 {'family_index': 2, 'per_family_instance_index': 0, 'run_name': 'GetFilteredFragments/multi_dir', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 3, 'num_filtered_fragments': 100.0, 'num_fragments': 10000.0}
GetAllFragments 35.682M items/sec 35.433M items/sec -0.698 {'family_index': 0, 'per_family_instance_index': 0, 'run_name': 'GetAllFragments', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 2282, 'num_fragments': 10000.0}
| -> Result<RecordBatchGenerator> { | ||
| // Since we already do the batching through the SlicingGenerator, we don't need the | ||
| // reader to batch its output. | ||
| reader->set_batch_size(std::numeric_limits<int64_t>::max()); |
There was a problem hiding this comment.
Well, if you have a very large row group (say 1 GB or more), what this will do is read the entire file at once, then slice it, instead of generating batches progressively.
So this sounds like a bad idea to me. The entire purpose of ScanBatchesAsync is to generate batches progressively so that the consumer can process them without waiting for the entire file to be read. This allows covering IO latencies and also decreasing memory footprint (say you're computing the average of a column: it would be wasteful to load the entire column at once instead of having a running average computation over smaller batches).
There was a problem hiding this comment.
Based on my understanding of this example, wouldn't calling ScanBatchesAsync create a RowGroupGenerator, which would then call ReadOneRowGroup and read the entire row group into an in-memory table anyways? The batch size here seems to just control the chunk size of that table.
The goal of this PR was to reduce the overhead of casting batches of string offset buffers (which would require O(N^2) memory/compute where N is the length of a row group) by performing the cast on the entire table/row-group at once, which we will have at some point because of ReadOneRowGroup, so I don't think this represents a regression.
However, I understand wanting to keep the intended semantics of ScanBatchesAsync the same. Maybe instead we can perform the cast at a different layer of the stack e.g. at the reader level?
|
|
||
| static void ParquetScanBenchmark_Customize(benchmark::internal::Benchmark* b) { | ||
| for (const int32_t num_batches : {1000}) { | ||
| for (const int batch_size : {10, 100, 1000}) { |
There was a problem hiding this comment.
Those batch sizes are much too small for good performance, so I'm not sure why they're being benchmarked.
You could instead try {1000, 10000, 100000}.
There was a problem hiding this comment.
Thanks for the suggestion, I updated the benchmark with those batch sizes:
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Non-regressions: (3)
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
benchmark baseline contender change % counters
ParquetScanToTableCastStrings/num_batches:1000/batch_size:1000 186.666K items/sec 11.289M items/sec 5947.814 {'family_index': 0, 'per_family_instance_index': 0, 'run_name': 'ParquetScanToTableCastStrings/num_batches:1000/batch_size:1000', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 1}
ParquetScanToTableCastStrings/num_batches:10/batch_size:100000 403.236M items/sec 797.722M items/sec 97.830 {'family_index': 0, 'per_family_instance_index': 2, 'run_name': 'ParquetScanToTableCastStrings/num_batches:10/batch_size:100000', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 100}
ParquetScanToTableCastStrings/num_batches:100/batch_size:10000 57.155M items/sec 110.185M items/sec 92.784 {'family_index': 0, 'per_family_instance_index': 1, 'run_name': 'ParquetScanToTableCastStrings/num_batches:100/batch_size:10000', 'repetitions': 1, 'repetition_index': 0, 'threads': 1, 'iterations': 39}
| // Casting before slicing is more efficient. Casts on slices might require wasteful | ||
| // allocations and computation. |
There was a problem hiding this comment.
Can you explain which kind of wasteful allocations and computations would be required when casting a slice?
There was a problem hiding this comment.
When casting the string offsets buffer, we allocate offset + length + 1 elements, memset the first offset elements to zero, and perform the cast on length many elements. So assuming batch_size = 1000, a row group of 10,000 elements would require allocating buffers of lengths 1001, 2001, ... 10,001 to perform the casts on all batches.
|
Ok, I think this is approaching the problem from the wrong end.
|
I would be happy to open a separate issue to look at For the second point, the cast here was not a workaround for reading LargeString data, but rather, we were working with datasets that have a single, unified schema and individual files that potentially have different schemas (e.g. string vs large_string ), so the casting would still be necessary at some point to reconcile the differences in schema. This cast is currently handled in MakeExecBatch, but there is comment that this should be done by the reader, so I believe this PR would be an incremental step in achieving this goal, but was wondering what your thoughts would be? |
No API needs to be changed here. The output of casting is by definition the same logical length as the input, but it does not need to have the same physical allocation shape (i.e. offset). The current code reuses the input offset (through |
There was a problem hiding this comment.
Sorry for chiming in late. As I’m going through the code, I had two quick questions:
1. When is the cast applied before this change?
2. Could you clarify how the O(N^2) complexity arises? I read the issue description but couldn’t quite follow it - probably because I’m not very familiar with the dataset code - so a short walkthrough would be really helpful.
Thanks.
Updated: just saw the other comment about the cast:
This cast is currently handled in MakeExecBatch, but there is comment that this should be done by the reader, so I believe this PR would be an incremental step in achieving this goal, but was wondering what your thoughts would be?
@zanmato1984 of course! It's not so much an issue with datasets as it is the actual casting kernel in compute as @pitrou pointed out. When casting small slices of size L of a larger array of length N, to cast the first slice, we have to allocate a new offsets buffer for the up/downcasted int offsets that has size L+1. The second slice will have offset L, and because of the way casting is implemented, we will actually need to allocate a buffer of size 2L+1 and zero out the first L elements since the output of the cast inherits the offset from the input slice. Continuing this pattern, the amount of work required is roughly, L + 2L + 3L + ... N ~ N^2/2L or O(N^2) if L is constant. Where this relates to datasets is when reading a parquet file, ScanAsyncBatches essentially is reading one row group at a time, and then produces slices on top that row group, which then need to be casted in MakeExecBatch, following the pattern described above. |
|
Thanks for the explanation @scott-routledge2 ! I now see the problem and sorry for you to have to illustrate that again. IIUC there are two aspects in this particular use case:
I think they are separated. And for 1, I agree with @pitrou that it is an arguable implementation choice we made for
It doesn't necessarily have to be the case. We are well free to output an intact, zero-offset array, which is logically equal to the current output with non-zero offset. This should require no API changes. For 2, I think the requirement of outputting batches of unified schema (applying implicit casts when necessary) makes a lot of sense. However I'm wondering if we can do it in an less-intrusive way, for example, applying the casts explicitly by leveraging existing mechanisms like arrow/cpp/src/arrow/dataset/scanner.h Line 62 in 5a48044 Thanks. |
|
@pitrou @zanmato1984 Thanks for the explanations! I opened a new PR to address the casting issue as discussed. As for 2, I'm also happy to keep this PR open and investigate a cleaner way of ensuring output batches have the same schema. Although I was wondering if there should be a separate issue? |
I think so. Moving on to a separate issue allow us to preserve the original purpose along with all the discussion history in the current issue/pr, which would make sense to the future readers. |
…ffset -> Binary offset types (#48171) ### Rationale for this change Casting Binary offset -> Binary offset types relies on ZeroCopyCastExec, which propagates the offset of the input to the output. This can lead to larger allocations than necessary when casting arrays with offsets. See #43660 and #43661 for more context. ### What changes are included in this PR? Ensure output array has a small offset (it can still be non-zero since reusing the null bitmap requires in_offset % 8 == out_offset % 8) ### Are these changes tested? Ran unit tests and benchmarked locally. ### Are there any user-facing changes? No * GitHub Issue: #43660 Authored-by: Scott Routledge <scott@bodo.ai> Signed-off-by: Rossi Sun <zanmato1984@gmail.com>
|
Issue resolved by #48171, so closing this for now. Feel free to file a new issue/PR for the schema aligning issue, if it still is. |
…nary offset -> Binary offset types (apache#48171) ### Rationale for this change Casting Binary offset -> Binary offset types relies on ZeroCopyCastExec, which propagates the offset of the input to the output. This can lead to larger allocations than necessary when casting arrays with offsets. See apache#43660 and apache#43661 for more context. ### What changes are included in this PR? Ensure output array has a small offset (it can still be non-zero since reusing the null bitmap requires in_offset % 8 == out_offset % 8) ### Are these changes tested? Ran unit tests and benchmarked locally. ### Are there any user-facing changes? No * GitHub Issue: apache#43660 Authored-by: Scott Routledge <scott@bodo.ai> Signed-off-by: Rossi Sun <zanmato1984@gmail.com>
Rationale for this change
(See #43660)
What changes are included in this PR?
ParquetFileFormat::ScanBatchesAsync, set thebatch_sizefor thereaderto INT64_MAX. Since we already have aSlicingGeneratorthat is responsible for converting the reader's output intobatch_sizesized batches, we don't need the reader to batch its output.CastingGeneratorthat applies any required casts to the output of the reader before the output is passed toSlicingGenerator. The logic for the cast closely follows the logic inMakeExecBatch(arrow/cpp/src/arrow/compute/expression.cc
Line 644 in 6a2e19a
arrow/cpp/src/arrow/compute/expression.cc
Line 672 in 6a2e19a
Are these changes tested?
I would like some feedback on the best way to test this. The changes passed our internal test cases, however, it may not account for all possible use cases.
Are there any user-facing changes?
No.