Repository navigation
GH-51275: [C++][GLib] Allow the CSV Reader to accept multi-character delimiters #51286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b86c918
782a3fe
d398889
27d2edd
cd6bc4b
92d8c1b
e2b581e
004cec2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| #include <algorithm> | ||
| #include <cstdint> | ||
| #include <memory> | ||
| #include <optional> | ||
| #include <string_view> | ||
| #include <utility> | ||
|
|
||
|
|
@@ -50,6 +51,9 @@ class Lexer { | |
| : options_(options), bulk_filter_(options_) { | ||
| DCHECK_EQ(SpecializedOptions::quoting, options_.quoting); | ||
| DCHECK_EQ(SpecializedOptions::escaping, options_.escaping); | ||
| if constexpr (SpecializedOptions::multi_delimiter) { | ||
| delimiter_matcher_.emplace(options_.delimiter_string); | ||
| } | ||
| } | ||
|
|
||
| void Reset() { state_ = FIELD_START; } | ||
|
|
@@ -107,6 +111,9 @@ class Lexer { | |
| } | ||
|
|
||
| FieldStart: | ||
| if constexpr (SpecializedOptions::multi_delimiter) { | ||
| delimiter_matcher_->Reset(); | ||
| } | ||
| if constexpr (!SpecializedOptions::quoting) { | ||
| goto InField; | ||
| } else { | ||
|
|
@@ -126,13 +133,27 @@ class Lexer { | |
|
|
||
| InField: | ||
| // Inside a non-quoted part of a field | ||
| if (UseBulkFilter) { | ||
| const char* bulk_end = RunBulkFilter(data, data_end); | ||
| if (ARROW_PREDICT_FALSE(bulk_end == nullptr)) { | ||
| state_ = IN_FIELD; | ||
| goto AbortLine; | ||
| if constexpr (UseBulkFilter) { | ||
| if constexpr (SpecializedOptions::multi_delimiter) { | ||
| if (!delimiter_matcher_->has_partial_match()) { | ||
| const char* bulk_end = RunBulkFilter(data, data_end); | ||
| if (ARROW_PREDICT_FALSE(bulk_end == nullptr)) { | ||
| state_ = IN_FIELD; | ||
| goto AbortLine; | ||
| } | ||
| data = bulk_end; | ||
| } else if (ARROW_PREDICT_FALSE(data == data_end)) { | ||
| state_ = IN_FIELD; | ||
| goto AbortLine; | ||
| } | ||
| } else { | ||
| const char* bulk_end = RunBulkFilter(data, data_end); | ||
| if (ARROW_PREDICT_FALSE(bulk_end == nullptr)) { | ||
| state_ = IN_FIELD; | ||
| goto AbortLine; | ||
| } | ||
| data = bulk_end; | ||
| } | ||
| data = bulk_end; | ||
| } else { | ||
| if (ARROW_PREDICT_FALSE(data == data_end)) { | ||
| state_ = IN_FIELD; | ||
|
|
@@ -141,6 +162,9 @@ class Lexer { | |
| } | ||
| c = *data++; | ||
| if (SpecializedOptions::escaping && ARROW_PREDICT_FALSE(c == options_.escape_char)) { | ||
| if constexpr (SpecializedOptions::multi_delimiter) { | ||
| delimiter_matcher_->Reset(); | ||
| } | ||
|
Comment on lines
+165
to
+167
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Other |
||
| if (ARROW_PREDICT_FALSE(data == data_end)) { | ||
| state_ = AT_ESCAPE; | ||
| goto AbortLine; | ||
|
|
@@ -157,9 +181,14 @@ class Lexer { | |
| if (ARROW_PREDICT_FALSE(c == '\n')) { | ||
| goto LineEnd; | ||
| } | ||
| // treat delimiter as a normal token if quoting is disabled | ||
| if (ARROW_PREDICT_FALSE(SpecializedOptions::quoting && c == options_.delimiter)) { | ||
| goto FieldEnd; | ||
| if constexpr (SpecializedOptions::quoting) { | ||
| if constexpr (SpecializedOptions::multi_delimiter) { | ||
| if (ARROW_PREDICT_FALSE(delimiter_matcher_->Consume(c))) { | ||
| goto FieldStart; | ||
| } | ||
| } else if (ARROW_PREDICT_FALSE(c == options_.delimiter)) { | ||
| goto FieldStart; | ||
| } | ||
| } | ||
| goto InField; | ||
|
|
||
|
|
@@ -223,10 +252,6 @@ class Lexer { | |
| goto InField; | ||
| } | ||
|
|
||
| FieldEnd: | ||
| // At the end of a field | ||
| goto FieldStart; | ||
|
|
||
| LineEnd: | ||
| state_ = FIELD_START; | ||
| return data; | ||
|
|
@@ -262,6 +287,8 @@ class Lexer { | |
| const ParseOptions& options_; | ||
| const BulkFilterType bulk_filter_; | ||
| State state_ = FIELD_START; | ||
|
|
||
| std::optional<internal::StreamingDelimiterMatcher> delimiter_matcher_; | ||
| }; | ||
|
|
||
| // A BoundaryFinder implementation that assumes CSV cells can contain raw newlines, | ||
|
|
@@ -380,23 +407,22 @@ std::unique_ptr<Chunker> MakeChunker(const ParseOptions& options) { | |
| if (!options.newlines_in_values) { | ||
| delimiter = MakeNewlineBoundaryFinder(); | ||
| } else { | ||
| if (options.quoting) { | ||
| if (options.escaping) { | ||
| delimiter = std::make_shared< | ||
| LexingBoundaryFinder<internal::SpecializedOptions<true, true>>>(options); | ||
| } else { | ||
| delimiter = std::make_shared< | ||
| LexingBoundaryFinder<internal::SpecializedOptions<true, false>>>(options); | ||
| } | ||
| } else { | ||
| if (options.escaping) { | ||
| delimiter = std::make_shared< | ||
| LexingBoundaryFinder<internal::SpecializedOptions<false, true>>>(options); | ||
| } else { | ||
| delimiter = std::make_shared< | ||
| LexingBoundaryFinder<internal::SpecializedOptions<false, false>>>(options); | ||
| } | ||
| // Unlike the parser, the chunker only needs delimiters to identify field starts | ||
| // for quote recognition. Without quoting, delimiters cannot affect line boundaries. | ||
| auto lexing_options = options; | ||
| if (lexing_options.delimiter_string.size() == 1) { | ||
| lexing_options.delimiter = lexing_options.delimiter_string.front(); | ||
| } | ||
| const bool multi_delimiter = options.quoting && options.delimiter_string.size() > 1; | ||
| delimiter = internal::DispatchBool( | ||
| [&]<bool Quoting, bool Escaping, bool MultiDelimiter>() | ||
| -> std::shared_ptr<BoundaryFinder> { | ||
| using SpecializedOptions = | ||
| internal::SpecializedOptions<Quoting, Escaping, MultiDelimiter>; | ||
| return std::make_shared<LexingBoundaryFinder<SpecializedOptions>>( | ||
| std::move(lexing_options)); | ||
| }, | ||
| options.quoting, options.escaping, multi_delimiter); | ||
| } | ||
| return std::make_unique<Chunker>(std::move(delimiter)); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,8 +23,40 @@ namespace csv { | |
| ParseOptions ParseOptions::Defaults() { return ParseOptions(); } | ||
|
|
||
| Status ParseOptions::Validate() const { | ||
| if (ARROW_PREDICT_FALSE(delimiter == '\n' || delimiter == '\r')) { | ||
| return Status::Invalid("ParseOptions: delimiter cannot be \\r or \\n"); | ||
| // The chunker handles escapes before delimiter matching, so allowing the escape | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it cleaner to consolidate the two cases like below? std::string invalid_chars = "\r\n";
if (escaping) {
invalid_chars.push_back(escape_char);
}
const bool use_delimiter_string = !delimiter_string.empty();
const auto invalid_pos =
use_delimiter_string ? delimiter_string.find_first_of(invalid_chars)
: invalid_chars.find(delimiter);
if (invalid_pos != std::string::npos) {
return Status::Invalid("ParseOptions: delimiter contains an invalid character");
}
const char delimiter_first_byte =
use_delimiter_string ? delimiter_string.front() : delimiter;
if (quoting && delimiter_first_byte == quote_char) {
return Status::Invalid(
"ParseOptions: delimiter cannot start with the quote character");
}
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. However, by doing this, we lose the specific reason for the error. |
||
| // character in a delimiter could make it disagree with the parser. | ||
| if (escaping) { | ||
|
HuaHuaY marked this conversation as resolved.
|
||
| if (delimiter_string.empty()) { | ||
| if (delimiter == escape_char) { | ||
| return Status::Invalid( | ||
| "ParseOptions: delimiter cannot be the escape character when escaping is " | ||
| "enabled"); | ||
| } | ||
| } else if (delimiter_string.find(escape_char) != std::string::npos) { | ||
| return Status::Invalid( | ||
| "ParseOptions: delimiter_string cannot contain the escape character when " | ||
| "escaping is enabled"); | ||
| } | ||
| } | ||
| // Line endings delimit records and therefore cannot be part of a field delimiter. | ||
| if (delimiter_string.empty()) { | ||
| if (ARROW_PREDICT_FALSE(delimiter == '\n' || delimiter == '\r')) { | ||
| return Status::Invalid("ParseOptions: delimiter cannot be \\r or \\n"); | ||
| } | ||
| } else if (ARROW_PREDICT_FALSE(delimiter_string.find_first_of("\r\n") != | ||
| std::string::npos)) { | ||
| return Status::Invalid("ParseOptions: delimiter_string cannot contain \\r or \\n"); | ||
| } | ||
| if (quoting) { | ||
| // Keep structural characters out of delimiters when quoting is enabled. | ||
| const bool delimiter_contains_quote = | ||
| delimiter_string.empty() ? delimiter == quote_char | ||
| : delimiter_string.find(quote_char) != std::string::npos; | ||
| if (ARROW_PREDICT_FALSE(delimiter_contains_quote)) { | ||
| return Status::Invalid( | ||
| "ParseOptions: delimiter cannot contain the quote character when quoting is " | ||
| "enabled"); | ||
| } | ||
| } | ||
| if (ARROW_PREDICT_FALSE(quoting && (quote_char == '\n' || quote_char == '\r'))) { | ||
| return Status::Invalid("ParseOptions: quote_char cannot be \\r or \\n"); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,8 @@ struct ARROW_EXPORT ParseOptions { | |
| bool pad_short_rows = false; | ||
| /// Whether rows with more columns than expected should ignore the extra columns. | ||
| bool ignore_extra_columns = false; | ||
| /// Multi-character field delimiter. If non-empty, overrides `delimiter`. | ||
| std::string delimiter_string; | ||
|
Comment on lines
+68
to
+69
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we can just make If we keep the current two-field approach, then perhaps we can mark
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My thinking is that
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Python shouldn't be a problem as it exposes a Python However, switching to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have addressed all comments except for this. I let my LLM agent work on deprecating the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can add the deprecation in a separate PR IMHO.
HuaHuaY marked this conversation as resolved.
|
||
|
|
||
| /// Create parsing options with default values | ||
| static ParseOptions Defaults(); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.