ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner - #13709

Closed
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000
Closed

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner#13709
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000

Conversation

@joosthooz

Copy link
Copy Markdown
Contributor

WIP Adding an optional function that wraps all input streams with a user-supplied transcoding function.

Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadcpp/src/arrow/python/io.h Outdated
@github-actions

Copy link
Copy Markdown

Thanks for opening a pull request!

If this is not a minor PR. Could you open an issue for this pull request on JIRA? https://issues.apache.org/jira/browse/ARROW

Opening JIRAs ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename pull request title in the following format?

ARROW-${JIRA_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

Comment threadpython/pyarrow/_dataset.pyx Outdated


# from io.pxi
class Transcoder:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do we really want to jump back to python here instead of using C++ utilities for decoding? (I'm not sure if there are any good standard utilities so maybe the answer is yes).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We mostly discussed this in the JIRA - you'd have to pull in a library like icu if you want to do it on the C++ side, and also Python (at least) has 'special' encodings like 'unicodereplace' that users may or may not expect to be able to use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope to add that as a possibility in the future, but for now I wanted to mimic the behavior of read_csv as much as possible. We'll have to see how bad of a bottleneck this will create. But for scanning a single file it shouldn't matter, and that is good enough for my use case because I just want to be able to deal with files that are larger than memory (which pyarrow.dataset will allow me to do and read_csv will not)

@joosthoozjoosthooz changed the title Arrow-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerJul 28, 2022
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

The current state is that it works, but it relies on the workaround of adding an encoding parameter to pyarrow.dataset().
That needs to be dealt with before proceeding.

@westonpacewestonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add the encoding option in CsvFileFormat? I think that is the entry point to "fragment scan options" for pyarrow datasets and it appears to be a thin wrapper around CsvFileFormatOptions:

l1_csv_format = ds.CsvFileFormat(read_options=..., parse_options=..., convert_options=..., encoding='latin-1')
my_dataset = ds.dataset([my_files], format=l1_csv_format)

Comment threadpython/pyarrow/io.pxi Outdated
Parameters
----------
src_encoding : str
The codec to use when reading data data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The codec to use when reading data data.
The codec to use when reading data.

Comment threadpython/pyarrow/io.pxi Outdated
Create a function that will add a transcoding transformation to a stream.
Data from that stream will be decoded according to ``src_encoding`` and
then re-encoded according to ``dest_encoding``.
The created function can be used to wrap streams once they are created.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The created function can be used to wrap streams once they are created.
The created function can be used to wrap streams.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thank you for the comments and suggestions.
I implemented Weston's suggestion and added an encoding field to CsvFileFormat instead of the added parameter to dataset(). This works, but I dislike it a lot. The field is a duplicate to the same one in ReadOptions. So when using read_csv, users need to use the field in read_options, but when using dataset, they need to use the field in format. They can also use both of them, 1 is silently discarded. Here's what this looks like:

>>> fo = ds.CsvFileFormat(default_fragment_scan_options=ds.CsvFragmentScanOptions(read_options=csv.ReadOptions(encoding='iso-8259')), encoding='cp1252')
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
>>> fo.encoding
'cp1252'

Instead of duplicating the encoding field in the CsvFileFormat, we store the encoding in a private field in the CsvFragmentScanOptions.
In that class, the read_options.encoding field gets lost when initializing it by using the C struct (which doesn't have the encoding field).
So when the read_options are read, we restore it again.
@joosthooz

joosthooz commented Jul 29, 2022

Copy link
Copy Markdown
ContributorAuthor

I pushed an alternative way of passing the encoding in 22eff73. For the user it works the same way as in read_csv: it is a field in read_options. I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

Edit: Hm, it is not working the way I want it yet. The value still gets lost when creating a CsvFileFormat.
Is it an option to add encoding as a field to the C struct ReadOptions?

@westonpace

Copy link
Copy Markdown
Member

I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

I like this approach if you can get it working. Can you add this to the CsvFileFormat constructor?

 else :
# default_fragment_scan_options is needed to add a transcoder
self.default_fragment_scan_options = CsvFragmentScanOptions()
if read_options is not None:
self.default_fragment_scan_options.encoding = read_options.encoding

Is it an option to add encoding as a field to the C struct ReadOptions?

That seems undesirable. The C++ csv reader doesn't have the field because it has no ability to handle encodings. So I'm not sure we want to add a field that is completely ignored.

It needs to be stored in both CsvFileFormat and CsvFragmentScanOptions because if the user has a reference to these separate objects, they would otherwise become inconsistent.
1 would report the default 'utf8' (forgetting the user's encoding choice), while the other would still properly report the requested encoding.
To the user it would be unclear which of these values would be eventually used by the transcoding.
@joosthooz

Copy link
Copy Markdown
ContributorAuthor

(4d819aa should be Removed encoding from CsvFragmentScanOptions.equals())

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Ok I pushed something completely different. I added encoding as a field in the C struct and some wrapper code that tries to dlopen the libiconv library. I haven't really tested it beyond seeing that it doesn't crash when I read some data from a dataset. Now the question is how do we let the user specify what they want to do? As in choose between a Python transcoder or a library on his system. And how do we show what libraries we have available? Should we create an example about how peopple can add their own wrappers?

@lidavidm

Copy link
Copy Markdown
Member

I think we're getting a bit far afield…Dynamic linking needs platform-specific code and usually we configure optional dependencies with build flags.

What if we add the C++-side field, have it error in C++ if not set to the default, and in python, we can reset the value to the default and configure the transcoder? That leaves us the path to upgrade and should avoid excessive python-side hacks. If we decide it's valuable to have built-in C++ side transcoding, then we have the option there already.

An alternative would be to have the Python wrappers for these structs no longer actually wrap the C++ structs, so that we aren't limited to the C++ fields. But that would lead to some code duplication/messiness as well.

I'm not sure we can avoid some messiness: the fundamental issue is that we have a Python-only field but are trying to directly wrap the C++ structs. That extra field needs to be mirrored somewhere. Either we do work to pass it around on the Python side or we give in and add it in C++.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

That sums it up very nicely. Both alternatives are fine with me. I just pushed an update that aims to do what you suggest, which is adding an encoding field to the C++ struct. The CSV reader returns an Invalid error when the user has specified an encoding other than UTF-8 but the stream_transform_func is empty.
Is that the right error type or would an IOError be more suitable?
How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it? Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

Comment threadcpp/src/arrow/csv/options.h Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadpython/pyarrow/dataset.py Outdated
Comment threadpython/pyarrow/io.pxi
@lidavidm

Copy link
Copy Markdown
Member

How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it?

It feels like it shouldn't be publicly accessible? Or else it should mirror the C++ side option name 1:1

Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

I guess we can only get pointer equality, but yes

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Somewhere in the CSV reader itself we should also validate the option

I tried this, but it doesn't work, because in that case we would need to re-set the field back to utf8 when adding a transcoder in python. Otherwise, the error is triggered even though we are transcoding to utf8. But then, we will again run into the issue where the ReadOptions object that the user created is changed:

>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> dataset = ds.dataset("file.csv", format=fo)
>>> ro.encoding
'utf8'

This would be really strange if you ask me. And if we accept this strange behavior, we didn't need to add the encoding field in the first place.
So now, the field is basically ignored in the CSV reader, there only is the check in the dataset CSV reader that there must be a wrapping function set if the encoding is not utf8.

@lidavidm

Copy link
Copy Markdown
Member

Ah, thanks for explaining.

Wonder if we should/could pass a copy of the ReadOptions then?

@lidavidm

Copy link
Copy Markdown
Member

But it's not a big deal, I think so long as the field is clearly documented

@pitrou

Copy link
Copy Markdown
Member

@joosthooz Do you want reviewing at this point or are you looking to polish this PR first?

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thanks for checking in, @pitrou ! Most important is to choose which approach to take, to make that easier I opened #13820 to compare against.
After that I need to check why some of the tests are failing (they seem unrelated) and maybe polish a bit and then I'll move it out of the draft state.

@pitrou

Copy link
Copy Markdown
Member

I would favor #13820, which pushes complexity into Python, over this one, which introduces a dummy option in C++ that has no effect.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Continuing here: #13820

lidavidm pushed a commit that referenced this pull request Sep 6, 2022
…ding transcoding function option to CSV scanner (#13820)
This is an alternative version of #13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
zagto pushed a commit to zagto/arrow that referenced this pull request Oct 7, 2022
…ding transcoding function option to CSV scanner (apache#13820)
This is an alternative version of apache#13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@joosthooz@westonpace@lidavidm@pitrou
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner - #13709

Closed
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000
Closed

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner#13709
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000

Conversation

@joosthooz

Copy link
Copy Markdown
Contributor

WIP Adding an optional function that wraps all input streams with a user-supplied transcoding function.

Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadcpp/src/arrow/python/io.h Outdated
@github-actions

Copy link
Copy Markdown

Thanks for opening a pull request!

If this is not a minor PR. Could you open an issue for this pull request on JIRA? https://issues.apache.org/jira/browse/ARROW

Opening JIRAs ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename pull request title in the following format?

ARROW-${JIRA_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

Comment threadpython/pyarrow/_dataset.pyx Outdated


# from io.pxi
class Transcoder:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do we really want to jump back to python here instead of using C++ utilities for decoding? (I'm not sure if there are any good standard utilities so maybe the answer is yes).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We mostly discussed this in the JIRA - you'd have to pull in a library like icu if you want to do it on the C++ side, and also Python (at least) has 'special' encodings like 'unicodereplace' that users may or may not expect to be able to use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope to add that as a possibility in the future, but for now I wanted to mimic the behavior of read_csv as much as possible. We'll have to see how bad of a bottleneck this will create. But for scanning a single file it shouldn't matter, and that is good enough for my use case because I just want to be able to deal with files that are larger than memory (which pyarrow.dataset will allow me to do and read_csv will not)

@joosthoozjoosthooz changed the title Arrow-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerJul 28, 2022
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

The current state is that it works, but it relies on the workaround of adding an encoding parameter to pyarrow.dataset().
That needs to be dealt with before proceeding.

@westonpacewestonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add the encoding option in CsvFileFormat? I think that is the entry point to "fragment scan options" for pyarrow datasets and it appears to be a thin wrapper around CsvFileFormatOptions:

l1_csv_format = ds.CsvFileFormat(read_options=..., parse_options=..., convert_options=..., encoding='latin-1')
my_dataset = ds.dataset([my_files], format=l1_csv_format)

Comment threadpython/pyarrow/io.pxi Outdated
Parameters
----------
src_encoding : str
The codec to use when reading data data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The codec to use when reading data data.
The codec to use when reading data.

Comment threadpython/pyarrow/io.pxi Outdated
Create a function that will add a transcoding transformation to a stream.
Data from that stream will be decoded according to ``src_encoding`` and
then re-encoded according to ``dest_encoding``.
The created function can be used to wrap streams once they are created.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The created function can be used to wrap streams once they are created.
The created function can be used to wrap streams.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thank you for the comments and suggestions.
I implemented Weston's suggestion and added an encoding field to CsvFileFormat instead of the added parameter to dataset(). This works, but I dislike it a lot. The field is a duplicate to the same one in ReadOptions. So when using read_csv, users need to use the field in read_options, but when using dataset, they need to use the field in format. They can also use both of them, 1 is silently discarded. Here's what this looks like:

>>> fo = ds.CsvFileFormat(default_fragment_scan_options=ds.CsvFragmentScanOptions(read_options=csv.ReadOptions(encoding='iso-8259')), encoding='cp1252')
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
>>> fo.encoding
'cp1252'

Instead of duplicating the encoding field in the CsvFileFormat, we store the encoding in a private field in the CsvFragmentScanOptions.
In that class, the read_options.encoding field gets lost when initializing it by using the C struct (which doesn't have the encoding field).
So when the read_options are read, we restore it again.
@joosthooz

joosthooz commented Jul 29, 2022

Copy link
Copy Markdown
ContributorAuthor

I pushed an alternative way of passing the encoding in 22eff73. For the user it works the same way as in read_csv: it is a field in read_options. I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

Edit: Hm, it is not working the way I want it yet. The value still gets lost when creating a CsvFileFormat.
Is it an option to add encoding as a field to the C struct ReadOptions?

@westonpace

Copy link
Copy Markdown
Member

I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

I like this approach if you can get it working. Can you add this to the CsvFileFormat constructor?

 else :
# default_fragment_scan_options is needed to add a transcoder
self.default_fragment_scan_options = CsvFragmentScanOptions()
if read_options is not None:
self.default_fragment_scan_options.encoding = read_options.encoding

Is it an option to add encoding as a field to the C struct ReadOptions?

That seems undesirable. The C++ csv reader doesn't have the field because it has no ability to handle encodings. So I'm not sure we want to add a field that is completely ignored.

It needs to be stored in both CsvFileFormat and CsvFragmentScanOptions because if the user has a reference to these separate objects, they would otherwise become inconsistent.
1 would report the default 'utf8' (forgetting the user's encoding choice), while the other would still properly report the requested encoding.
To the user it would be unclear which of these values would be eventually used by the transcoding.
@joosthooz

Copy link
Copy Markdown
ContributorAuthor

(4d819aa should be Removed encoding from CsvFragmentScanOptions.equals())

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Ok I pushed something completely different. I added encoding as a field in the C struct and some wrapper code that tries to dlopen the libiconv library. I haven't really tested it beyond seeing that it doesn't crash when I read some data from a dataset. Now the question is how do we let the user specify what they want to do? As in choose between a Python transcoder or a library on his system. And how do we show what libraries we have available? Should we create an example about how peopple can add their own wrappers?

@lidavidm

Copy link
Copy Markdown
Member

I think we're getting a bit far afield…Dynamic linking needs platform-specific code and usually we configure optional dependencies with build flags.

What if we add the C++-side field, have it error in C++ if not set to the default, and in python, we can reset the value to the default and configure the transcoder? That leaves us the path to upgrade and should avoid excessive python-side hacks. If we decide it's valuable to have built-in C++ side transcoding, then we have the option there already.

An alternative would be to have the Python wrappers for these structs no longer actually wrap the C++ structs, so that we aren't limited to the C++ fields. But that would lead to some code duplication/messiness as well.

I'm not sure we can avoid some messiness: the fundamental issue is that we have a Python-only field but are trying to directly wrap the C++ structs. That extra field needs to be mirrored somewhere. Either we do work to pass it around on the Python side or we give in and add it in C++.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

That sums it up very nicely. Both alternatives are fine with me. I just pushed an update that aims to do what you suggest, which is adding an encoding field to the C++ struct. The CSV reader returns an Invalid error when the user has specified an encoding other than UTF-8 but the stream_transform_func is empty.
Is that the right error type or would an IOError be more suitable?
How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it? Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

Comment threadcpp/src/arrow/csv/options.h Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadpython/pyarrow/dataset.py Outdated
Comment threadpython/pyarrow/io.pxi
@lidavidm

Copy link
Copy Markdown
Member

How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it?

It feels like it shouldn't be publicly accessible? Or else it should mirror the C++ side option name 1:1

Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

I guess we can only get pointer equality, but yes

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Somewhere in the CSV reader itself we should also validate the option

I tried this, but it doesn't work, because in that case we would need to re-set the field back to utf8 when adding a transcoder in python. Otherwise, the error is triggered even though we are transcoding to utf8. But then, we will again run into the issue where the ReadOptions object that the user created is changed:

>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> dataset = ds.dataset("file.csv", format=fo)
>>> ro.encoding
'utf8'

This would be really strange if you ask me. And if we accept this strange behavior, we didn't need to add the encoding field in the first place.
So now, the field is basically ignored in the CSV reader, there only is the check in the dataset CSV reader that there must be a wrapping function set if the encoding is not utf8.

@lidavidm

Copy link
Copy Markdown
Member

Ah, thanks for explaining.

Wonder if we should/could pass a copy of the ReadOptions then?

@lidavidm

Copy link
Copy Markdown
Member

But it's not a big deal, I think so long as the field is clearly documented

@pitrou

Copy link
Copy Markdown
Member

@joosthooz Do you want reviewing at this point or are you looking to polish this PR first?

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thanks for checking in, @pitrou ! Most important is to choose which approach to take, to make that easier I opened #13820 to compare against.
After that I need to check why some of the tests are failing (they seem unrelated) and maybe polish a bit and then I'll move it out of the draft state.

@pitrou

Copy link
Copy Markdown
Member

I would favor #13820, which pushes complexity into Python, over this one, which introduces a dummy option in C++ that has no effect.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Continuing here: #13820

lidavidm pushed a commit that referenced this pull request Sep 6, 2022
…ding transcoding function option to CSV scanner (#13820)
This is an alternative version of #13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
zagto pushed a commit to zagto/arrow that referenced this pull request Oct 7, 2022
…ding transcoding function option to CSV scanner (apache#13820)
This is an alternative version of apache#13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@joosthooz@westonpace@lidavidm@pitrou
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner - #13709

Closed
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000
Closed

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner#13709
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000

Conversation

@joosthooz

Copy link
Copy Markdown
Contributor

WIP Adding an optional function that wraps all input streams with a user-supplied transcoding function.

Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadcpp/src/arrow/python/io.h Outdated
@github-actions

Copy link
Copy Markdown

Thanks for opening a pull request!

If this is not a minor PR. Could you open an issue for this pull request on JIRA? https://issues.apache.org/jira/browse/ARROW

Opening JIRAs ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename pull request title in the following format?

ARROW-${JIRA_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

Comment threadpython/pyarrow/_dataset.pyx Outdated


# from io.pxi
class Transcoder:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do we really want to jump back to python here instead of using C++ utilities for decoding? (I'm not sure if there are any good standard utilities so maybe the answer is yes).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We mostly discussed this in the JIRA - you'd have to pull in a library like icu if you want to do it on the C++ side, and also Python (at least) has 'special' encodings like 'unicodereplace' that users may or may not expect to be able to use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope to add that as a possibility in the future, but for now I wanted to mimic the behavior of read_csv as much as possible. We'll have to see how bad of a bottleneck this will create. But for scanning a single file it shouldn't matter, and that is good enough for my use case because I just want to be able to deal with files that are larger than memory (which pyarrow.dataset will allow me to do and read_csv will not)

@joosthoozjoosthooz changed the title Arrow-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerJul 28, 2022
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

The current state is that it works, but it relies on the workaround of adding an encoding parameter to pyarrow.dataset().
That needs to be dealt with before proceeding.

@westonpacewestonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add the encoding option in CsvFileFormat? I think that is the entry point to "fragment scan options" for pyarrow datasets and it appears to be a thin wrapper around CsvFileFormatOptions:

l1_csv_format = ds.CsvFileFormat(read_options=..., parse_options=..., convert_options=..., encoding='latin-1')
my_dataset = ds.dataset([my_files], format=l1_csv_format)

Comment threadpython/pyarrow/io.pxi Outdated
Parameters
----------
src_encoding : str
The codec to use when reading data data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The codec to use when reading data data.
The codec to use when reading data.

Comment threadpython/pyarrow/io.pxi Outdated
Create a function that will add a transcoding transformation to a stream.
Data from that stream will be decoded according to ``src_encoding`` and
then re-encoded according to ``dest_encoding``.
The created function can be used to wrap streams once they are created.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The created function can be used to wrap streams once they are created.
The created function can be used to wrap streams.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thank you for the comments and suggestions.
I implemented Weston's suggestion and added an encoding field to CsvFileFormat instead of the added parameter to dataset(). This works, but I dislike it a lot. The field is a duplicate to the same one in ReadOptions. So when using read_csv, users need to use the field in read_options, but when using dataset, they need to use the field in format. They can also use both of them, 1 is silently discarded. Here's what this looks like:

>>> fo = ds.CsvFileFormat(default_fragment_scan_options=ds.CsvFragmentScanOptions(read_options=csv.ReadOptions(encoding='iso-8259')), encoding='cp1252')
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
>>> fo.encoding
'cp1252'

Instead of duplicating the encoding field in the CsvFileFormat, we store the encoding in a private field in the CsvFragmentScanOptions.
In that class, the read_options.encoding field gets lost when initializing it by using the C struct (which doesn't have the encoding field).
So when the read_options are read, we restore it again.
@joosthooz

joosthooz commented Jul 29, 2022

Copy link
Copy Markdown
ContributorAuthor

I pushed an alternative way of passing the encoding in 22eff73. For the user it works the same way as in read_csv: it is a field in read_options. I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

Edit: Hm, it is not working the way I want it yet. The value still gets lost when creating a CsvFileFormat.
Is it an option to add encoding as a field to the C struct ReadOptions?

@westonpace

Copy link
Copy Markdown
Member

I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

I like this approach if you can get it working. Can you add this to the CsvFileFormat constructor?

 else :
# default_fragment_scan_options is needed to add a transcoder
self.default_fragment_scan_options = CsvFragmentScanOptions()
if read_options is not None:
self.default_fragment_scan_options.encoding = read_options.encoding

Is it an option to add encoding as a field to the C struct ReadOptions?

That seems undesirable. The C++ csv reader doesn't have the field because it has no ability to handle encodings. So I'm not sure we want to add a field that is completely ignored.

It needs to be stored in both CsvFileFormat and CsvFragmentScanOptions because if the user has a reference to these separate objects, they would otherwise become inconsistent.
1 would report the default 'utf8' (forgetting the user's encoding choice), while the other would still properly report the requested encoding.
To the user it would be unclear which of these values would be eventually used by the transcoding.
@joosthooz

Copy link
Copy Markdown
ContributorAuthor

(4d819aa should be Removed encoding from CsvFragmentScanOptions.equals())

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Ok I pushed something completely different. I added encoding as a field in the C struct and some wrapper code that tries to dlopen the libiconv library. I haven't really tested it beyond seeing that it doesn't crash when I read some data from a dataset. Now the question is how do we let the user specify what they want to do? As in choose between a Python transcoder or a library on his system. And how do we show what libraries we have available? Should we create an example about how peopple can add their own wrappers?

@lidavidm

Copy link
Copy Markdown
Member

I think we're getting a bit far afield…Dynamic linking needs platform-specific code and usually we configure optional dependencies with build flags.

What if we add the C++-side field, have it error in C++ if not set to the default, and in python, we can reset the value to the default and configure the transcoder? That leaves us the path to upgrade and should avoid excessive python-side hacks. If we decide it's valuable to have built-in C++ side transcoding, then we have the option there already.

An alternative would be to have the Python wrappers for these structs no longer actually wrap the C++ structs, so that we aren't limited to the C++ fields. But that would lead to some code duplication/messiness as well.

I'm not sure we can avoid some messiness: the fundamental issue is that we have a Python-only field but are trying to directly wrap the C++ structs. That extra field needs to be mirrored somewhere. Either we do work to pass it around on the Python side or we give in and add it in C++.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

That sums it up very nicely. Both alternatives are fine with me. I just pushed an update that aims to do what you suggest, which is adding an encoding field to the C++ struct. The CSV reader returns an Invalid error when the user has specified an encoding other than UTF-8 but the stream_transform_func is empty.
Is that the right error type or would an IOError be more suitable?
How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it? Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

Comment threadcpp/src/arrow/csv/options.h Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadpython/pyarrow/dataset.py Outdated
Comment threadpython/pyarrow/io.pxi
@lidavidm

Copy link
Copy Markdown
Member

How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it?

It feels like it shouldn't be publicly accessible? Or else it should mirror the C++ side option name 1:1

Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

I guess we can only get pointer equality, but yes

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Somewhere in the CSV reader itself we should also validate the option

I tried this, but it doesn't work, because in that case we would need to re-set the field back to utf8 when adding a transcoder in python. Otherwise, the error is triggered even though we are transcoding to utf8. But then, we will again run into the issue where the ReadOptions object that the user created is changed:

>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> dataset = ds.dataset("file.csv", format=fo)
>>> ro.encoding
'utf8'

This would be really strange if you ask me. And if we accept this strange behavior, we didn't need to add the encoding field in the first place.
So now, the field is basically ignored in the CSV reader, there only is the check in the dataset CSV reader that there must be a wrapping function set if the encoding is not utf8.

@lidavidm

Copy link
Copy Markdown
Member

Ah, thanks for explaining.

Wonder if we should/could pass a copy of the ReadOptions then?

@lidavidm

Copy link
Copy Markdown
Member

But it's not a big deal, I think so long as the field is clearly documented

@pitrou

Copy link
Copy Markdown
Member

@joosthooz Do you want reviewing at this point or are you looking to polish this PR first?

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thanks for checking in, @pitrou ! Most important is to choose which approach to take, to make that easier I opened #13820 to compare against.
After that I need to check why some of the tests are failing (they seem unrelated) and maybe polish a bit and then I'll move it out of the draft state.

@pitrou

Copy link
Copy Markdown
Member

I would favor #13820, which pushes complexity into Python, over this one, which introduces a dummy option in C++ that has no effect.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Continuing here: #13820

lidavidm pushed a commit that referenced this pull request Sep 6, 2022
…ding transcoding function option to CSV scanner (#13820)
This is an alternative version of #13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
zagto pushed a commit to zagto/arrow that referenced this pull request Oct 7, 2022
…ding transcoding function option to CSV scanner (apache#13820)
This is an alternative version of apache#13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@joosthooz@westonpace@lidavidm@pitrou
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner - #13709

Closed
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000
Closed

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner#13709
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000

Conversation

@joosthooz

Copy link
Copy Markdown
Contributor

WIP Adding an optional function that wraps all input streams with a user-supplied transcoding function.

Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadcpp/src/arrow/python/io.h Outdated
@github-actions

Copy link
Copy Markdown

Thanks for opening a pull request!

If this is not a minor PR. Could you open an issue for this pull request on JIRA? https://issues.apache.org/jira/browse/ARROW

Opening JIRAs ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename pull request title in the following format?

ARROW-${JIRA_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

Comment threadpython/pyarrow/_dataset.pyx Outdated


# from io.pxi
class Transcoder:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do we really want to jump back to python here instead of using C++ utilities for decoding? (I'm not sure if there are any good standard utilities so maybe the answer is yes).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We mostly discussed this in the JIRA - you'd have to pull in a library like icu if you want to do it on the C++ side, and also Python (at least) has 'special' encodings like 'unicodereplace' that users may or may not expect to be able to use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope to add that as a possibility in the future, but for now I wanted to mimic the behavior of read_csv as much as possible. We'll have to see how bad of a bottleneck this will create. But for scanning a single file it shouldn't matter, and that is good enough for my use case because I just want to be able to deal with files that are larger than memory (which pyarrow.dataset will allow me to do and read_csv will not)

@joosthoozjoosthooz changed the title Arrow-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerJul 28, 2022
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

The current state is that it works, but it relies on the workaround of adding an encoding parameter to pyarrow.dataset().
That needs to be dealt with before proceeding.

@westonpacewestonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add the encoding option in CsvFileFormat? I think that is the entry point to "fragment scan options" for pyarrow datasets and it appears to be a thin wrapper around CsvFileFormatOptions:

l1_csv_format = ds.CsvFileFormat(read_options=..., parse_options=..., convert_options=..., encoding='latin-1')
my_dataset = ds.dataset([my_files], format=l1_csv_format)

Comment threadpython/pyarrow/io.pxi Outdated
Parameters
----------
src_encoding : str
The codec to use when reading data data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The codec to use when reading data data.
The codec to use when reading data.

Comment threadpython/pyarrow/io.pxi Outdated
Create a function that will add a transcoding transformation to a stream.
Data from that stream will be decoded according to ``src_encoding`` and
then re-encoded according to ``dest_encoding``.
The created function can be used to wrap streams once they are created.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The created function can be used to wrap streams once they are created.
The created function can be used to wrap streams.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thank you for the comments and suggestions.
I implemented Weston's suggestion and added an encoding field to CsvFileFormat instead of the added parameter to dataset(). This works, but I dislike it a lot. The field is a duplicate to the same one in ReadOptions. So when using read_csv, users need to use the field in read_options, but when using dataset, they need to use the field in format. They can also use both of them, 1 is silently discarded. Here's what this looks like:

>>> fo = ds.CsvFileFormat(default_fragment_scan_options=ds.CsvFragmentScanOptions(read_options=csv.ReadOptions(encoding='iso-8259')), encoding='cp1252')
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
>>> fo.encoding
'cp1252'

Instead of duplicating the encoding field in the CsvFileFormat, we store the encoding in a private field in the CsvFragmentScanOptions.
In that class, the read_options.encoding field gets lost when initializing it by using the C struct (which doesn't have the encoding field).
So when the read_options are read, we restore it again.
@joosthooz

joosthooz commented Jul 29, 2022

Copy link
Copy Markdown
ContributorAuthor

I pushed an alternative way of passing the encoding in 22eff73. For the user it works the same way as in read_csv: it is a field in read_options. I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

Edit: Hm, it is not working the way I want it yet. The value still gets lost when creating a CsvFileFormat.
Is it an option to add encoding as a field to the C struct ReadOptions?

@westonpace

Copy link
Copy Markdown
Member

I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

I like this approach if you can get it working. Can you add this to the CsvFileFormat constructor?

 else :
# default_fragment_scan_options is needed to add a transcoder
self.default_fragment_scan_options = CsvFragmentScanOptions()
if read_options is not None:
self.default_fragment_scan_options.encoding = read_options.encoding

Is it an option to add encoding as a field to the C struct ReadOptions?

That seems undesirable. The C++ csv reader doesn't have the field because it has no ability to handle encodings. So I'm not sure we want to add a field that is completely ignored.

It needs to be stored in both CsvFileFormat and CsvFragmentScanOptions because if the user has a reference to these separate objects, they would otherwise become inconsistent.
1 would report the default 'utf8' (forgetting the user's encoding choice), while the other would still properly report the requested encoding.
To the user it would be unclear which of these values would be eventually used by the transcoding.
@joosthooz

Copy link
Copy Markdown
ContributorAuthor

(4d819aa should be Removed encoding from CsvFragmentScanOptions.equals())

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Ok I pushed something completely different. I added encoding as a field in the C struct and some wrapper code that tries to dlopen the libiconv library. I haven't really tested it beyond seeing that it doesn't crash when I read some data from a dataset. Now the question is how do we let the user specify what they want to do? As in choose between a Python transcoder or a library on his system. And how do we show what libraries we have available? Should we create an example about how peopple can add their own wrappers?

@lidavidm

Copy link
Copy Markdown
Member

I think we're getting a bit far afield…Dynamic linking needs platform-specific code and usually we configure optional dependencies with build flags.

What if we add the C++-side field, have it error in C++ if not set to the default, and in python, we can reset the value to the default and configure the transcoder? That leaves us the path to upgrade and should avoid excessive python-side hacks. If we decide it's valuable to have built-in C++ side transcoding, then we have the option there already.

An alternative would be to have the Python wrappers for these structs no longer actually wrap the C++ structs, so that we aren't limited to the C++ fields. But that would lead to some code duplication/messiness as well.

I'm not sure we can avoid some messiness: the fundamental issue is that we have a Python-only field but are trying to directly wrap the C++ structs. That extra field needs to be mirrored somewhere. Either we do work to pass it around on the Python side or we give in and add it in C++.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

That sums it up very nicely. Both alternatives are fine with me. I just pushed an update that aims to do what you suggest, which is adding an encoding field to the C++ struct. The CSV reader returns an Invalid error when the user has specified an encoding other than UTF-8 but the stream_transform_func is empty.
Is that the right error type or would an IOError be more suitable?
How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it? Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

Comment threadcpp/src/arrow/csv/options.h Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadpython/pyarrow/dataset.py Outdated
Comment threadpython/pyarrow/io.pxi
@lidavidm

Copy link
Copy Markdown
Member

How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it?

It feels like it shouldn't be publicly accessible? Or else it should mirror the C++ side option name 1:1

Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

I guess we can only get pointer equality, but yes

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Somewhere in the CSV reader itself we should also validate the option

I tried this, but it doesn't work, because in that case we would need to re-set the field back to utf8 when adding a transcoder in python. Otherwise, the error is triggered even though we are transcoding to utf8. But then, we will again run into the issue where the ReadOptions object that the user created is changed:

>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> dataset = ds.dataset("file.csv", format=fo)
>>> ro.encoding
'utf8'

This would be really strange if you ask me. And if we accept this strange behavior, we didn't need to add the encoding field in the first place.
So now, the field is basically ignored in the CSV reader, there only is the check in the dataset CSV reader that there must be a wrapping function set if the encoding is not utf8.

@lidavidm

Copy link
Copy Markdown
Member

Ah, thanks for explaining.

Wonder if we should/could pass a copy of the ReadOptions then?

@lidavidm

Copy link
Copy Markdown
Member

But it's not a big deal, I think so long as the field is clearly documented

@pitrou

Copy link
Copy Markdown
Member

@joosthooz Do you want reviewing at this point or are you looking to polish this PR first?

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thanks for checking in, @pitrou ! Most important is to choose which approach to take, to make that easier I opened #13820 to compare against.
After that I need to check why some of the tests are failing (they seem unrelated) and maybe polish a bit and then I'll move it out of the draft state.

@pitrou

Copy link
Copy Markdown
Member

I would favor #13820, which pushes complexity into Python, over this one, which introduces a dummy option in C++ that has no effect.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Continuing here: #13820

lidavidm pushed a commit that referenced this pull request Sep 6, 2022
…ding transcoding function option to CSV scanner (#13820)
This is an alternative version of #13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
zagto pushed a commit to zagto/arrow that referenced this pull request Oct 7, 2022
…ding transcoding function option to CSV scanner (apache#13820)
This is an alternative version of apache#13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@joosthooz@westonpace@lidavidm@pitrou
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner - #13709

Closed
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000
Closed

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner#13709
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000

Conversation

@joosthooz

Copy link
Copy Markdown
Contributor

WIP Adding an optional function that wraps all input streams with a user-supplied transcoding function.

Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadcpp/src/arrow/python/io.h Outdated
@github-actions

Copy link
Copy Markdown

Thanks for opening a pull request!

If this is not a minor PR. Could you open an issue for this pull request on JIRA? https://issues.apache.org/jira/browse/ARROW

Opening JIRAs ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename pull request title in the following format?

ARROW-${JIRA_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

Comment threadpython/pyarrow/_dataset.pyx Outdated


# from io.pxi
class Transcoder:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do we really want to jump back to python here instead of using C++ utilities for decoding? (I'm not sure if there are any good standard utilities so maybe the answer is yes).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We mostly discussed this in the JIRA - you'd have to pull in a library like icu if you want to do it on the C++ side, and also Python (at least) has 'special' encodings like 'unicodereplace' that users may or may not expect to be able to use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope to add that as a possibility in the future, but for now I wanted to mimic the behavior of read_csv as much as possible. We'll have to see how bad of a bottleneck this will create. But for scanning a single file it shouldn't matter, and that is good enough for my use case because I just want to be able to deal with files that are larger than memory (which pyarrow.dataset will allow me to do and read_csv will not)

@joosthoozjoosthooz changed the title Arrow-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerJul 28, 2022
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

The current state is that it works, but it relies on the workaround of adding an encoding parameter to pyarrow.dataset().
That needs to be dealt with before proceeding.

@westonpacewestonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add the encoding option in CsvFileFormat? I think that is the entry point to "fragment scan options" for pyarrow datasets and it appears to be a thin wrapper around CsvFileFormatOptions:

l1_csv_format = ds.CsvFileFormat(read_options=..., parse_options=..., convert_options=..., encoding='latin-1')
my_dataset = ds.dataset([my_files], format=l1_csv_format)

Comment threadpython/pyarrow/io.pxi Outdated
Parameters
----------
src_encoding : str
The codec to use when reading data data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The codec to use when reading data data.
The codec to use when reading data.

Comment threadpython/pyarrow/io.pxi Outdated
Create a function that will add a transcoding transformation to a stream.
Data from that stream will be decoded according to ``src_encoding`` and
then re-encoded according to ``dest_encoding``.
The created function can be used to wrap streams once they are created.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The created function can be used to wrap streams once they are created.
The created function can be used to wrap streams.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thank you for the comments and suggestions.
I implemented Weston's suggestion and added an encoding field to CsvFileFormat instead of the added parameter to dataset(). This works, but I dislike it a lot. The field is a duplicate to the same one in ReadOptions. So when using read_csv, users need to use the field in read_options, but when using dataset, they need to use the field in format. They can also use both of them, 1 is silently discarded. Here's what this looks like:

>>> fo = ds.CsvFileFormat(default_fragment_scan_options=ds.CsvFragmentScanOptions(read_options=csv.ReadOptions(encoding='iso-8259')), encoding='cp1252')
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
>>> fo.encoding
'cp1252'

Instead of duplicating the encoding field in the CsvFileFormat, we store the encoding in a private field in the CsvFragmentScanOptions.
In that class, the read_options.encoding field gets lost when initializing it by using the C struct (which doesn't have the encoding field).
So when the read_options are read, we restore it again.
@joosthooz

joosthooz commented Jul 29, 2022

Copy link
Copy Markdown
ContributorAuthor

I pushed an alternative way of passing the encoding in 22eff73. For the user it works the same way as in read_csv: it is a field in read_options. I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

Edit: Hm, it is not working the way I want it yet. The value still gets lost when creating a CsvFileFormat.
Is it an option to add encoding as a field to the C struct ReadOptions?

@westonpace

Copy link
Copy Markdown
Member

I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

I like this approach if you can get it working. Can you add this to the CsvFileFormat constructor?

 else :
# default_fragment_scan_options is needed to add a transcoder
self.default_fragment_scan_options = CsvFragmentScanOptions()
if read_options is not None:
self.default_fragment_scan_options.encoding = read_options.encoding

Is it an option to add encoding as a field to the C struct ReadOptions?

That seems undesirable. The C++ csv reader doesn't have the field because it has no ability to handle encodings. So I'm not sure we want to add a field that is completely ignored.

It needs to be stored in both CsvFileFormat and CsvFragmentScanOptions because if the user has a reference to these separate objects, they would otherwise become inconsistent.
1 would report the default 'utf8' (forgetting the user's encoding choice), while the other would still properly report the requested encoding.
To the user it would be unclear which of these values would be eventually used by the transcoding.
@joosthooz

Copy link
Copy Markdown
ContributorAuthor

(4d819aa should be Removed encoding from CsvFragmentScanOptions.equals())

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Ok I pushed something completely different. I added encoding as a field in the C struct and some wrapper code that tries to dlopen the libiconv library. I haven't really tested it beyond seeing that it doesn't crash when I read some data from a dataset. Now the question is how do we let the user specify what they want to do? As in choose between a Python transcoder or a library on his system. And how do we show what libraries we have available? Should we create an example about how peopple can add their own wrappers?

@lidavidm

Copy link
Copy Markdown
Member

I think we're getting a bit far afield…Dynamic linking needs platform-specific code and usually we configure optional dependencies with build flags.

What if we add the C++-side field, have it error in C++ if not set to the default, and in python, we can reset the value to the default and configure the transcoder? That leaves us the path to upgrade and should avoid excessive python-side hacks. If we decide it's valuable to have built-in C++ side transcoding, then we have the option there already.

An alternative would be to have the Python wrappers for these structs no longer actually wrap the C++ structs, so that we aren't limited to the C++ fields. But that would lead to some code duplication/messiness as well.

I'm not sure we can avoid some messiness: the fundamental issue is that we have a Python-only field but are trying to directly wrap the C++ structs. That extra field needs to be mirrored somewhere. Either we do work to pass it around on the Python side or we give in and add it in C++.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

That sums it up very nicely. Both alternatives are fine with me. I just pushed an update that aims to do what you suggest, which is adding an encoding field to the C++ struct. The CSV reader returns an Invalid error when the user has specified an encoding other than UTF-8 but the stream_transform_func is empty.
Is that the right error type or would an IOError be more suitable?
How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it? Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

Comment threadcpp/src/arrow/csv/options.h Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadpython/pyarrow/dataset.py Outdated
Comment threadpython/pyarrow/io.pxi
@lidavidm

Copy link
Copy Markdown
Member

How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it?

It feels like it shouldn't be publicly accessible? Or else it should mirror the C++ side option name 1:1

Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

I guess we can only get pointer equality, but yes

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Somewhere in the CSV reader itself we should also validate the option

I tried this, but it doesn't work, because in that case we would need to re-set the field back to utf8 when adding a transcoder in python. Otherwise, the error is triggered even though we are transcoding to utf8. But then, we will again run into the issue where the ReadOptions object that the user created is changed:

>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> dataset = ds.dataset("file.csv", format=fo)
>>> ro.encoding
'utf8'

This would be really strange if you ask me. And if we accept this strange behavior, we didn't need to add the encoding field in the first place.
So now, the field is basically ignored in the CSV reader, there only is the check in the dataset CSV reader that there must be a wrapping function set if the encoding is not utf8.

@lidavidm

Copy link
Copy Markdown
Member

Ah, thanks for explaining.

Wonder if we should/could pass a copy of the ReadOptions then?

@lidavidm

Copy link
Copy Markdown
Member

But it's not a big deal, I think so long as the field is clearly documented

@pitrou

Copy link
Copy Markdown
Member

@joosthooz Do you want reviewing at this point or are you looking to polish this PR first?

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thanks for checking in, @pitrou ! Most important is to choose which approach to take, to make that easier I opened #13820 to compare against.
After that I need to check why some of the tests are failing (they seem unrelated) and maybe polish a bit and then I'll move it out of the draft state.

@pitrou

Copy link
Copy Markdown
Member

I would favor #13820, which pushes complexity into Python, over this one, which introduces a dummy option in C++ that has no effect.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Continuing here: #13820

lidavidm pushed a commit that referenced this pull request Sep 6, 2022
…ding transcoding function option to CSV scanner (#13820)
This is an alternative version of #13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
zagto pushed a commit to zagto/arrow that referenced this pull request Oct 7, 2022
…ding transcoding function option to CSV scanner (apache#13820)
This is an alternative version of apache#13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@joosthooz@westonpace@lidavidm@pitrou
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner - #13709

Closed
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000
Closed

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner#13709
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000

Conversation

@joosthooz

Copy link
Copy Markdown
Contributor

WIP Adding an optional function that wraps all input streams with a user-supplied transcoding function.

Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadcpp/src/arrow/python/io.h Outdated
@github-actions

Copy link
Copy Markdown

Thanks for opening a pull request!

If this is not a minor PR. Could you open an issue for this pull request on JIRA? https://issues.apache.org/jira/browse/ARROW

Opening JIRAs ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename pull request title in the following format?

ARROW-${JIRA_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

Comment threadpython/pyarrow/_dataset.pyx Outdated


# from io.pxi
class Transcoder:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do we really want to jump back to python here instead of using C++ utilities for decoding? (I'm not sure if there are any good standard utilities so maybe the answer is yes).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We mostly discussed this in the JIRA - you'd have to pull in a library like icu if you want to do it on the C++ side, and also Python (at least) has 'special' encodings like 'unicodereplace' that users may or may not expect to be able to use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope to add that as a possibility in the future, but for now I wanted to mimic the behavior of read_csv as much as possible. We'll have to see how bad of a bottleneck this will create. But for scanning a single file it shouldn't matter, and that is good enough for my use case because I just want to be able to deal with files that are larger than memory (which pyarrow.dataset will allow me to do and read_csv will not)

@joosthoozjoosthooz changed the title Arrow-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerJul 28, 2022
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

The current state is that it works, but it relies on the workaround of adding an encoding parameter to pyarrow.dataset().
That needs to be dealt with before proceeding.

@westonpacewestonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add the encoding option in CsvFileFormat? I think that is the entry point to "fragment scan options" for pyarrow datasets and it appears to be a thin wrapper around CsvFileFormatOptions:

l1_csv_format = ds.CsvFileFormat(read_options=..., parse_options=..., convert_options=..., encoding='latin-1')
my_dataset = ds.dataset([my_files], format=l1_csv_format)

Comment threadpython/pyarrow/io.pxi Outdated
Parameters
----------
src_encoding : str
The codec to use when reading data data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The codec to use when reading data data.
The codec to use when reading data.

Comment threadpython/pyarrow/io.pxi Outdated
Create a function that will add a transcoding transformation to a stream.
Data from that stream will be decoded according to ``src_encoding`` and
then re-encoded according to ``dest_encoding``.
The created function can be used to wrap streams once they are created.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The created function can be used to wrap streams once they are created.
The created function can be used to wrap streams.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thank you for the comments and suggestions.
I implemented Weston's suggestion and added an encoding field to CsvFileFormat instead of the added parameter to dataset(). This works, but I dislike it a lot. The field is a duplicate to the same one in ReadOptions. So when using read_csv, users need to use the field in read_options, but when using dataset, they need to use the field in format. They can also use both of them, 1 is silently discarded. Here's what this looks like:

>>> fo = ds.CsvFileFormat(default_fragment_scan_options=ds.CsvFragmentScanOptions(read_options=csv.ReadOptions(encoding='iso-8259')), encoding='cp1252')
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
>>> fo.encoding
'cp1252'

Instead of duplicating the encoding field in the CsvFileFormat, we store the encoding in a private field in the CsvFragmentScanOptions.
In that class, the read_options.encoding field gets lost when initializing it by using the C struct (which doesn't have the encoding field).
So when the read_options are read, we restore it again.
@joosthooz

joosthooz commented Jul 29, 2022

Copy link
Copy Markdown
ContributorAuthor

I pushed an alternative way of passing the encoding in 22eff73. For the user it works the same way as in read_csv: it is a field in read_options. I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

Edit: Hm, it is not working the way I want it yet. The value still gets lost when creating a CsvFileFormat.
Is it an option to add encoding as a field to the C struct ReadOptions?

@westonpace

Copy link
Copy Markdown
Member

I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

I like this approach if you can get it working. Can you add this to the CsvFileFormat constructor?

 else :
# default_fragment_scan_options is needed to add a transcoder
self.default_fragment_scan_options = CsvFragmentScanOptions()
if read_options is not None:
self.default_fragment_scan_options.encoding = read_options.encoding

Is it an option to add encoding as a field to the C struct ReadOptions?

That seems undesirable. The C++ csv reader doesn't have the field because it has no ability to handle encodings. So I'm not sure we want to add a field that is completely ignored.

It needs to be stored in both CsvFileFormat and CsvFragmentScanOptions because if the user has a reference to these separate objects, they would otherwise become inconsistent.
1 would report the default 'utf8' (forgetting the user's encoding choice), while the other would still properly report the requested encoding.
To the user it would be unclear which of these values would be eventually used by the transcoding.
@joosthooz

Copy link
Copy Markdown
ContributorAuthor

(4d819aa should be Removed encoding from CsvFragmentScanOptions.equals())

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Ok I pushed something completely different. I added encoding as a field in the C struct and some wrapper code that tries to dlopen the libiconv library. I haven't really tested it beyond seeing that it doesn't crash when I read some data from a dataset. Now the question is how do we let the user specify what they want to do? As in choose between a Python transcoder or a library on his system. And how do we show what libraries we have available? Should we create an example about how peopple can add their own wrappers?

@lidavidm

Copy link
Copy Markdown
Member

I think we're getting a bit far afield…Dynamic linking needs platform-specific code and usually we configure optional dependencies with build flags.

What if we add the C++-side field, have it error in C++ if not set to the default, and in python, we can reset the value to the default and configure the transcoder? That leaves us the path to upgrade and should avoid excessive python-side hacks. If we decide it's valuable to have built-in C++ side transcoding, then we have the option there already.

An alternative would be to have the Python wrappers for these structs no longer actually wrap the C++ structs, so that we aren't limited to the C++ fields. But that would lead to some code duplication/messiness as well.

I'm not sure we can avoid some messiness: the fundamental issue is that we have a Python-only field but are trying to directly wrap the C++ structs. That extra field needs to be mirrored somewhere. Either we do work to pass it around on the Python side or we give in and add it in C++.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

That sums it up very nicely. Both alternatives are fine with me. I just pushed an update that aims to do what you suggest, which is adding an encoding field to the C++ struct. The CSV reader returns an Invalid error when the user has specified an encoding other than UTF-8 but the stream_transform_func is empty.
Is that the right error type or would an IOError be more suitable?
How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it? Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

Comment threadcpp/src/arrow/csv/options.h Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadpython/pyarrow/dataset.py Outdated
Comment threadpython/pyarrow/io.pxi
@lidavidm

Copy link
Copy Markdown
Member

How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it?

It feels like it shouldn't be publicly accessible? Or else it should mirror the C++ side option name 1:1

Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

I guess we can only get pointer equality, but yes

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Somewhere in the CSV reader itself we should also validate the option

I tried this, but it doesn't work, because in that case we would need to re-set the field back to utf8 when adding a transcoder in python. Otherwise, the error is triggered even though we are transcoding to utf8. But then, we will again run into the issue where the ReadOptions object that the user created is changed:

>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> dataset = ds.dataset("file.csv", format=fo)
>>> ro.encoding
'utf8'

This would be really strange if you ask me. And if we accept this strange behavior, we didn't need to add the encoding field in the first place.
So now, the field is basically ignored in the CSV reader, there only is the check in the dataset CSV reader that there must be a wrapping function set if the encoding is not utf8.

@lidavidm

Copy link
Copy Markdown
Member

Ah, thanks for explaining.

Wonder if we should/could pass a copy of the ReadOptions then?

@lidavidm

Copy link
Copy Markdown
Member

But it's not a big deal, I think so long as the field is clearly documented

@pitrou

Copy link
Copy Markdown
Member

@joosthooz Do you want reviewing at this point or are you looking to polish this PR first?

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thanks for checking in, @pitrou ! Most important is to choose which approach to take, to make that easier I opened #13820 to compare against.
After that I need to check why some of the tests are failing (they seem unrelated) and maybe polish a bit and then I'll move it out of the draft state.

@pitrou

Copy link
Copy Markdown
Member

I would favor #13820, which pushes complexity into Python, over this one, which introduces a dummy option in C++ that has no effect.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Continuing here: #13820

lidavidm pushed a commit that referenced this pull request Sep 6, 2022
…ding transcoding function option to CSV scanner (#13820)
This is an alternative version of #13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
zagto pushed a commit to zagto/arrow that referenced this pull request Oct 7, 2022
…ding transcoding function option to CSV scanner (apache#13820)
This is an alternative version of apache#13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@joosthooz@westonpace@lidavidm@pitrou
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner - #13709

Closed
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000
Closed

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner#13709
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000

Conversation

@joosthooz

Copy link
Copy Markdown
Contributor

WIP Adding an optional function that wraps all input streams with a user-supplied transcoding function.

Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadcpp/src/arrow/python/io.h Outdated
@github-actions

Copy link
Copy Markdown

Thanks for opening a pull request!

If this is not a minor PR. Could you open an issue for this pull request on JIRA? https://issues.apache.org/jira/browse/ARROW

Opening JIRAs ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename pull request title in the following format?

ARROW-${JIRA_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

Comment threadpython/pyarrow/_dataset.pyx Outdated


# from io.pxi
class Transcoder:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do we really want to jump back to python here instead of using C++ utilities for decoding? (I'm not sure if there are any good standard utilities so maybe the answer is yes).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We mostly discussed this in the JIRA - you'd have to pull in a library like icu if you want to do it on the C++ side, and also Python (at least) has 'special' encodings like 'unicodereplace' that users may or may not expect to be able to use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope to add that as a possibility in the future, but for now I wanted to mimic the behavior of read_csv as much as possible. We'll have to see how bad of a bottleneck this will create. But for scanning a single file it shouldn't matter, and that is good enough for my use case because I just want to be able to deal with files that are larger than memory (which pyarrow.dataset will allow me to do and read_csv will not)

@joosthoozjoosthooz changed the title Arrow-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerJul 28, 2022
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

The current state is that it works, but it relies on the workaround of adding an encoding parameter to pyarrow.dataset().
That needs to be dealt with before proceeding.

@westonpacewestonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add the encoding option in CsvFileFormat? I think that is the entry point to "fragment scan options" for pyarrow datasets and it appears to be a thin wrapper around CsvFileFormatOptions:

l1_csv_format = ds.CsvFileFormat(read_options=..., parse_options=..., convert_options=..., encoding='latin-1')
my_dataset = ds.dataset([my_files], format=l1_csv_format)

Comment threadpython/pyarrow/io.pxi Outdated
Parameters
----------
src_encoding : str
The codec to use when reading data data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The codec to use when reading data data.
The codec to use when reading data.

Comment threadpython/pyarrow/io.pxi Outdated
Create a function that will add a transcoding transformation to a stream.
Data from that stream will be decoded according to ``src_encoding`` and
then re-encoded according to ``dest_encoding``.
The created function can be used to wrap streams once they are created.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The created function can be used to wrap streams once they are created.
The created function can be used to wrap streams.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thank you for the comments and suggestions.
I implemented Weston's suggestion and added an encoding field to CsvFileFormat instead of the added parameter to dataset(). This works, but I dislike it a lot. The field is a duplicate to the same one in ReadOptions. So when using read_csv, users need to use the field in read_options, but when using dataset, they need to use the field in format. They can also use both of them, 1 is silently discarded. Here's what this looks like:

>>> fo = ds.CsvFileFormat(default_fragment_scan_options=ds.CsvFragmentScanOptions(read_options=csv.ReadOptions(encoding='iso-8259')), encoding='cp1252')
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
>>> fo.encoding
'cp1252'

Instead of duplicating the encoding field in the CsvFileFormat, we store the encoding in a private field in the CsvFragmentScanOptions.
In that class, the read_options.encoding field gets lost when initializing it by using the C struct (which doesn't have the encoding field).
So when the read_options are read, we restore it again.
@joosthooz

joosthooz commented Jul 29, 2022

Copy link
Copy Markdown
ContributorAuthor

I pushed an alternative way of passing the encoding in 22eff73. For the user it works the same way as in read_csv: it is a field in read_options. I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

Edit: Hm, it is not working the way I want it yet. The value still gets lost when creating a CsvFileFormat.
Is it an option to add encoding as a field to the C struct ReadOptions?

@westonpace

Copy link
Copy Markdown
Member

I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

I like this approach if you can get it working. Can you add this to the CsvFileFormat constructor?

 else :
# default_fragment_scan_options is needed to add a transcoder
self.default_fragment_scan_options = CsvFragmentScanOptions()
if read_options is not None:
self.default_fragment_scan_options.encoding = read_options.encoding

Is it an option to add encoding as a field to the C struct ReadOptions?

That seems undesirable. The C++ csv reader doesn't have the field because it has no ability to handle encodings. So I'm not sure we want to add a field that is completely ignored.

It needs to be stored in both CsvFileFormat and CsvFragmentScanOptions because if the user has a reference to these separate objects, they would otherwise become inconsistent.
1 would report the default 'utf8' (forgetting the user's encoding choice), while the other would still properly report the requested encoding.
To the user it would be unclear which of these values would be eventually used by the transcoding.
@joosthooz

Copy link
Copy Markdown
ContributorAuthor

(4d819aa should be Removed encoding from CsvFragmentScanOptions.equals())

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Ok I pushed something completely different. I added encoding as a field in the C struct and some wrapper code that tries to dlopen the libiconv library. I haven't really tested it beyond seeing that it doesn't crash when I read some data from a dataset. Now the question is how do we let the user specify what they want to do? As in choose between a Python transcoder or a library on his system. And how do we show what libraries we have available? Should we create an example about how peopple can add their own wrappers?

@lidavidm

Copy link
Copy Markdown
Member

I think we're getting a bit far afield…Dynamic linking needs platform-specific code and usually we configure optional dependencies with build flags.

What if we add the C++-side field, have it error in C++ if not set to the default, and in python, we can reset the value to the default and configure the transcoder? That leaves us the path to upgrade and should avoid excessive python-side hacks. If we decide it's valuable to have built-in C++ side transcoding, then we have the option there already.

An alternative would be to have the Python wrappers for these structs no longer actually wrap the C++ structs, so that we aren't limited to the C++ fields. But that would lead to some code duplication/messiness as well.

I'm not sure we can avoid some messiness: the fundamental issue is that we have a Python-only field but are trying to directly wrap the C++ structs. That extra field needs to be mirrored somewhere. Either we do work to pass it around on the Python side or we give in and add it in C++.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

That sums it up very nicely. Both alternatives are fine with me. I just pushed an update that aims to do what you suggest, which is adding an encoding field to the C++ struct. The CSV reader returns an Invalid error when the user has specified an encoding other than UTF-8 but the stream_transform_func is empty.
Is that the right error type or would an IOError be more suitable?
How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it? Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

Comment threadcpp/src/arrow/csv/options.h Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadpython/pyarrow/dataset.py Outdated
Comment threadpython/pyarrow/io.pxi
@lidavidm

Copy link
Copy Markdown
Member

How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it?

It feels like it shouldn't be publicly accessible? Or else it should mirror the C++ side option name 1:1

Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

I guess we can only get pointer equality, but yes

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Somewhere in the CSV reader itself we should also validate the option

I tried this, but it doesn't work, because in that case we would need to re-set the field back to utf8 when adding a transcoder in python. Otherwise, the error is triggered even though we are transcoding to utf8. But then, we will again run into the issue where the ReadOptions object that the user created is changed:

>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> dataset = ds.dataset("file.csv", format=fo)
>>> ro.encoding
'utf8'

This would be really strange if you ask me. And if we accept this strange behavior, we didn't need to add the encoding field in the first place.
So now, the field is basically ignored in the CSV reader, there only is the check in the dataset CSV reader that there must be a wrapping function set if the encoding is not utf8.

@lidavidm

Copy link
Copy Markdown
Member

Ah, thanks for explaining.

Wonder if we should/could pass a copy of the ReadOptions then?

@lidavidm

Copy link
Copy Markdown
Member

But it's not a big deal, I think so long as the field is clearly documented

@pitrou

Copy link
Copy Markdown
Member

@joosthooz Do you want reviewing at this point or are you looking to polish this PR first?

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thanks for checking in, @pitrou ! Most important is to choose which approach to take, to make that easier I opened #13820 to compare against.
After that I need to check why some of the tests are failing (they seem unrelated) and maybe polish a bit and then I'll move it out of the draft state.

@pitrou

Copy link
Copy Markdown
Member

I would favor #13820, which pushes complexity into Python, over this one, which introduces a dummy option in C++ that has no effect.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Continuing here: #13820

lidavidm pushed a commit that referenced this pull request Sep 6, 2022
…ding transcoding function option to CSV scanner (#13820)
This is an alternative version of #13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
zagto pushed a commit to zagto/arrow that referenced this pull request Oct 7, 2022
…ding transcoding function option to CSV scanner (apache#13820)
This is an alternative version of apache#13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@joosthooz@westonpace@lidavidm@pitrou
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner - #13709

Closed
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000
Closed

ARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scanner#13709
joosthooz wants to merge 27 commits into
apache:masterfrom
joosthooz:ARROW-16000

Conversation

@joosthooz

Copy link
Copy Markdown
Contributor

WIP Adding an optional function that wraps all input streams with a user-supplied transcoding function.

Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadpython/pyarrow/_dataset.pyx Outdated
Comment threadcpp/src/arrow/python/io.h Outdated
@github-actions

Copy link
Copy Markdown

Thanks for opening a pull request!

If this is not a minor PR. Could you open an issue for this pull request on JIRA? https://issues.apache.org/jira/browse/ARROW

Opening JIRAs ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename pull request title in the following format?

ARROW-${JIRA_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

Comment threadpython/pyarrow/_dataset.pyx Outdated


# from io.pxi
class Transcoder:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do we really want to jump back to python here instead of using C++ utilities for decoding? (I'm not sure if there are any good standard utilities so maybe the answer is yes).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We mostly discussed this in the JIRA - you'd have to pull in a library like icu if you want to do it on the C++ side, and also Python (at least) has 'special' encodings like 'unicodereplace' that users may or may not expect to be able to use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope to add that as a possibility in the future, but for now I wanted to mimic the behavior of read_csv as much as possible. We'll have to see how bad of a bottleneck this will create. But for scanning a single file it shouldn't matter, and that is good enough for my use case because I just want to be able to deal with files that are larger than memory (which pyarrow.dataset will allow me to do and read_csv will not)

@joosthoozjoosthooz changed the title Arrow-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerARROW-16000: [C++][Python] Dataset: Added transcoding function option to CSV scannerJul 28, 2022
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

The current state is that it works, but it relies on the workaround of adding an encoding parameter to pyarrow.dataset().
That needs to be dealt with before proceeding.

@westonpacewestonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add the encoding option in CsvFileFormat? I think that is the entry point to "fragment scan options" for pyarrow datasets and it appears to be a thin wrapper around CsvFileFormatOptions:

l1_csv_format = ds.CsvFileFormat(read_options=..., parse_options=..., convert_options=..., encoding='latin-1')
my_dataset = ds.dataset([my_files], format=l1_csv_format)

Comment threadpython/pyarrow/io.pxi Outdated
Parameters
----------
src_encoding : str
The codec to use when reading data data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The codec to use when reading data data.
The codec to use when reading data.

Comment threadpython/pyarrow/io.pxi Outdated
Create a function that will add a transcoding transformation to a stream.
Data from that stream will be decoded according to ``src_encoding`` and
then re-encoded according to ``dest_encoding``.
The created function can be used to wrap streams once they are created.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The created function can be used to wrap streams once they are created.
The created function can be used to wrap streams.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thank you for the comments and suggestions.
I implemented Weston's suggestion and added an encoding field to CsvFileFormat instead of the added parameter to dataset(). This works, but I dislike it a lot. The field is a duplicate to the same one in ReadOptions. So when using read_csv, users need to use the field in read_options, but when using dataset, they need to use the field in format. They can also use both of them, 1 is silently discarded. Here's what this looks like:

>>> fo = ds.CsvFileFormat(default_fragment_scan_options=ds.CsvFragmentScanOptions(read_options=csv.ReadOptions(encoding='iso-8259')), encoding='cp1252')
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
>>> fo.encoding
'cp1252'

Instead of duplicating the encoding field in the CsvFileFormat, we store the encoding in a private field in the CsvFragmentScanOptions.
In that class, the read_options.encoding field gets lost when initializing it by using the C struct (which doesn't have the encoding field).
So when the read_options are read, we restore it again.
@joosthooz

joosthooz commented Jul 29, 2022

Copy link
Copy Markdown
ContributorAuthor

I pushed an alternative way of passing the encoding in 22eff73. For the user it works the same way as in read_csv: it is a field in read_options. I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

Edit: Hm, it is not working the way I want it yet. The value still gets lost when creating a CsvFileFormat.
Is it an option to add encoding as a field to the C struct ReadOptions?

@westonpace

Copy link
Copy Markdown
Member

I store the value in CsvFragmentScanOptions, and then restore it.
How do you feel about this?

I like this approach if you can get it working. Can you add this to the CsvFileFormat constructor?

 else :
# default_fragment_scan_options is needed to add a transcoder
self.default_fragment_scan_options = CsvFragmentScanOptions()
if read_options is not None:
self.default_fragment_scan_options.encoding = read_options.encoding

Is it an option to add encoding as a field to the C struct ReadOptions?

That seems undesirable. The C++ csv reader doesn't have the field because it has no ability to handle encodings. So I'm not sure we want to add a field that is completely ignored.

It needs to be stored in both CsvFileFormat and CsvFragmentScanOptions because if the user has a reference to these separate objects, they would otherwise become inconsistent.
1 would report the default 'utf8' (forgetting the user's encoding choice), while the other would still properly report the requested encoding.
To the user it would be unclear which of these values would be eventually used by the transcoding.
@joosthooz

Copy link
Copy Markdown
ContributorAuthor

(4d819aa should be Removed encoding from CsvFragmentScanOptions.equals())

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Ok I pushed something completely different. I added encoding as a field in the C struct and some wrapper code that tries to dlopen the libiconv library. I haven't really tested it beyond seeing that it doesn't crash when I read some data from a dataset. Now the question is how do we let the user specify what they want to do? As in choose between a Python transcoder or a library on his system. And how do we show what libraries we have available? Should we create an example about how peopple can add their own wrappers?

@lidavidm

Copy link
Copy Markdown
Member

I think we're getting a bit far afield…Dynamic linking needs platform-specific code and usually we configure optional dependencies with build flags.

What if we add the C++-side field, have it error in C++ if not set to the default, and in python, we can reset the value to the default and configure the transcoder? That leaves us the path to upgrade and should avoid excessive python-side hacks. If we decide it's valuable to have built-in C++ side transcoding, then we have the option there already.

An alternative would be to have the Python wrappers for these structs no longer actually wrap the C++ structs, so that we aren't limited to the C++ fields. But that would lead to some code duplication/messiness as well.

I'm not sure we can avoid some messiness: the fundamental issue is that we have a Python-only field but are trying to directly wrap the C++ structs. That extra field needs to be mirrored somewhere. Either we do work to pass it around on the Python side or we give in and add it in C++.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

That sums it up very nicely. Both alternatives are fine with me. I just pushed an update that aims to do what you suggest, which is adding an encoding field to the C++ struct. The CSV reader returns an Invalid error when the user has specified an encoding other than UTF-8 but the stream_transform_func is empty.
Is that the right error type or would an IOError be more suitable?
How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it? Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

Comment threadcpp/src/arrow/csv/options.h Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadcpp/src/arrow/dataset/file_csv.cc Outdated
Comment threadpython/pyarrow/dataset.py Outdated
Comment threadpython/pyarrow/io.pxi
@lidavidm

Copy link
Copy Markdown
Member

How do you feel about the name of the set_transcoder function in CsvFragmentScanOptions? Should I add a docstring to it?

It feels like it shouldn't be publicly accessible? Or else it should mirror the C++ side option name 1:1

Should the stream_transform_func be added to the equals() function? (in that case I think I need to add a getter/setting for it too)

I guess we can only get pointer equality, but yes

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Somewhere in the CSV reader itself we should also validate the option

I tried this, but it doesn't work, because in that case we would need to re-set the field back to utf8 when adding a transcoder in python. Otherwise, the error is triggered even though we are transcoding to utf8. But then, we will again run into the issue where the ReadOptions object that the user created is changed:

>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> dataset = ds.dataset("file.csv", format=fo)
>>> ro.encoding
'utf8'

This would be really strange if you ask me. And if we accept this strange behavior, we didn't need to add the encoding field in the first place.
So now, the field is basically ignored in the CSV reader, there only is the check in the dataset CSV reader that there must be a wrapping function set if the encoding is not utf8.

@lidavidm

Copy link
Copy Markdown
Member

Ah, thanks for explaining.

Wonder if we should/could pass a copy of the ReadOptions then?

@lidavidm

Copy link
Copy Markdown
Member

But it's not a big deal, I think so long as the field is clearly documented

@pitrou

Copy link
Copy Markdown
Member

@joosthooz Do you want reviewing at this point or are you looking to polish this PR first?

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Thanks for checking in, @pitrou ! Most important is to choose which approach to take, to make that easier I opened #13820 to compare against.
After that I need to check why some of the tests are failing (they seem unrelated) and maybe polish a bit and then I'll move it out of the draft state.

@pitrou

Copy link
Copy Markdown
Member

I would favor #13820, which pushes complexity into Python, over this one, which introduces a dummy option in C++ that has no effect.

@joosthooz

Copy link
Copy Markdown
ContributorAuthor

Continuing here: #13820

lidavidm pushed a commit that referenced this pull request Sep 6, 2022
…ding transcoding function option to CSV scanner (#13820)
This is an alternative version of #13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
zagto pushed a commit to zagto/arrow that referenced this pull request Oct 7, 2022
…ding transcoding function option to CSV scanner (apache#13820)
This is an alternative version of apache#13709, to compare what the best approach is.
Instead of extending the C++ ReadOptions struct with an `encoding` field, this implementations adds a python version of the ReadOptions object to both `CsvFileFormat` and `CsvFragmentScanOptions`. The reason it is needed in both places, is to prevent these kinds of inconsistencies:
```
>>> import pyarrow.dataset as ds
>>> import pyarrow.csv as csv
>>> ro =csv.ReadOptions(encoding='iso8859')
>>> fo = ds.CsvFileFormat(read_options=ro)
>>> fo.default_fragment_scan_options.read_options.encoding
'utf8'
```
Authored-by: Joost Hoozemans <joosthooz@msn.com>
Signed-off-by: David Li <li.davidm96@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@joosthooz@westonpace@lidavidm@pitrou