ARROW-6231: [C++] Allow generating CSV column names

Add an option that, if enabled, will autogenerate column names
for the target table rather than read them from the CSV file.
This commit is contained in:
Antoine Pitrou committed 2019-08-27 15:41:23 +02:00
1 parent cef82b41f5
commit cc7fdc6997
5 files changed
+97 -18

No files matched your search

+6 -1
View File
@@ -100,8 +100,13 @@ struct ARROW_EXPORT ReadOptions {
// Number of header rows to skip (not including the row of column names, if any)
int32_t skip_rows = 0;
// Column names (if empty, will be read from first row after `skip_rows`)
// Column names for the target table.
// If empty, fall back on autogenerate_column_names.
std::vector<std::string> column_names;
// Whether to autogenerate column names if `column_names` is empty.
// If true, column names will be of the form "f0", "f1"...
// If false, column names will be read from the first CSV row after `skip_rows`.
bool autogenerate_column_names = false;
static ReadOptions Defaults();
};
+28 -12
View File
@@ -162,29 +162,34 @@ class BaseTableReader : public csv::TableReader {
}
if (read_options_.column_names.empty()) {
// Read one row with column names
// Parse one row (either to read column names or to know the number of columns)
BlockParser parser(pool_, parse_options_, num_csv_cols_, 1);
uint32_t parsed_size = 0;
RETURN_NOT_OK(parser.Parse(reinterpret_cast<const char*>(cur_data_),
static_cast<uint32_t>(cur_size_), &parsed_size));
if (parser.num_rows() != 1) {
return Status::Invalid(
"Could not read column names from CSV file, either "
"Could not read first row from CSV file, either "
"file is too short or header is larger than block size");
}
if (parser.num_cols() == 0) {
return Status::Invalid("No columns in CSV file");
}
// Read column names from last header row
auto visit = [&](const uint8_t* data, uint32_t size, bool quoted) -> Status {
column_names_.emplace_back(reinterpret_cast<const char*>(data), size);
return Status::OK();
};
RETURN_NOT_OK(parser.VisitLastRow(visit));
DCHECK_EQ(static_cast<size_t>(parser.num_cols()), column_names_.size());
// Skip parsed header row
cur_data_ += parsed_size;
cur_size_ -= parsed_size;
if (read_options_.autogenerate_column_names) {
column_names_ = GenerateColumnNames(parser.num_cols());
} else {
// Read column names from header row
auto visit = [&](const uint8_t* data, uint32_t size, bool quoted) -> Status {
column_names_.emplace_back(reinterpret_cast<const char*>(data), size);
return Status::OK();
};
RETURN_NOT_OK(parser.VisitLastRow(visit));
DCHECK_EQ(static_cast<size_t>(parser.num_cols()), column_names_.size());
// Skip parsed header row
cur_data_ += parsed_size;
cur_size_ -= parsed_size;
}
} else {
column_names_ = read_options_.column_names;
}
@@ -271,6 +276,17 @@ class BaseTableReader : public csv::TableReader {
return ColumnBuilder::MakeNull(pool_, type, task_group_, out);
}
std::vector<std::string> GenerateColumnNames(int32_t num_cols) {
std::vector<std::string> res;
res.reserve(num_cols);
for (int32_t i = 0; i < num_cols; ++i) {
std::stringstream ss;
ss << "f" << i;
res.push_back(ss.str());
}
return res;
}
// Trigger conversion of parsed block data
Status ProcessData(const std::shared_ptr<BlockParser>& parser, int64_t block_index) {
for (auto& builder : column_builders_) {
+26 -5
View File
@@ -55,8 +55,13 @@ cdef class ReadOptions:
The number of rows to skip at the start of the CSV data, not
including the row of column names (if any).
column_names: list, optional
The column names in the CSV file. If empty, column names will be
read from the first row after `skip_rows`.
The column names of the target table. If empty, fall back on
`autogenerate_column_names`.
autogenerate_column_names: bool, optional (default False)
Whether to autogenerate column names if `column_names` is empty.
If true, column names will be of the form "f0", "f1"...
If false, column names will be read from the first CSV row
after `skip_rows`.
"""
cdef:
CCSVReadOptions options
@@ -65,7 +70,7 @@ cdef class ReadOptions:
__slots__ = ()
def __init__(self, use_threads=None, block_size=None, skip_rows=None,
column_names=None):
column_names=None, autogenerate_column_names=None):
self.options = CCSVReadOptions.Defaults()
if use_threads is not None:
self.use_threads = use_threads
@@ -75,6 +80,8 @@ cdef class ReadOptions:
self.skip_rows = skip_rows
if column_names is not None:
self.column_names = column_names
if autogenerate_column_names is not None:
self.autogenerate_column_names= autogenerate_column_names
@property
def use_threads(self):
@@ -115,8 +122,8 @@ cdef class ReadOptions:
@property
def column_names(self):
"""
The column names in the CSV file. If empty, column names will be
read from the first row after `skip_rows`.
The column names of the target table. If empty, fall back on
`autogenerate_column_names`.
"""
return [frombytes(s) for s in self.options.column_names]
@@ -126,6 +133,20 @@ cdef class ReadOptions:
for item in value:
self.options.column_names.push_back(tobytes(item))
@property
def autogenerate_column_names(self):
"""
Whether to autogenerate column names if `column_names` is empty.
If true, column names will be of the form "f0", "f1"...
If false, column names will be read from the first CSV row
after `skip_rows`.
"""
return self.options.autogenerate_column_names
@autogenerate_column_names.setter
def autogenerate_column_names(self, value):
self.options.autogenerate_column_names = value
cdef class ParseOptions:
"""
+1
View File
@@ -1145,6 +1145,7 @@ cdef extern from "arrow/csv/api.h" namespace "arrow::csv" nogil:
int32_t block_size
int32_t skip_rows
vector[c_string] column_names
c_bool autogenerate_column_names
@staticmethod
CCSVReadOptions Defaults()
+36
View File
@@ -80,12 +80,23 @@ def test_read_options():
opts.column_names = ["ab", "cd"]
assert opts.column_names == ["ab", "cd"]
assert opts.autogenerate_column_names is False
opts.autogenerate_column_names = True
assert opts.autogenerate_column_names is True
opts = cls(block_size=1234, use_threads=False, skip_rows=42,
column_names=["a", "b", "c"])
assert opts.block_size == 1234
assert opts.use_threads is False
assert opts.skip_rows == 42
assert opts.column_names == ["a", "b", "c"]
assert opts.autogenerate_column_names is False
opts = cls(autogenerate_column_names=True)
assert opts.use_threads is True
assert opts.skip_rows == 0
assert opts.column_names == []
assert opts.autogenerate_column_names is True
def test_parse_options():
@@ -318,6 +329,31 @@ class BaseTestCSVRead:
"y": ["kl", "op"],
}
def test_header_autogenerate_column_names(self):
rows = b"ab,cd\nef,gh\nij,kl\nmn,op\n"
opts = ReadOptions()
opts.autogenerate_column_names = True
table = self.read_bytes(rows, read_options=opts)
self.check_names(table, ["f0", "f1"])
assert table.to_pydict() == {
"f0": ["ab", "ef", "ij", "mn"],
"f1": ["cd", "gh", "kl", "op"],
}
opts.skip_rows = 3
table = self.read_bytes(rows, read_options=opts)
self.check_names(table, ["f0", "f1"])
assert table.to_pydict() == {
"f0": ["mn"],
"f1": ["op"],
}
# Not enough rows, impossible to infer number of columns
opts.skip_rows = 4
with pytest.raises(pa.ArrowInvalid):
table = self.read_bytes(rows, read_options=opts)
def test_include_columns(self):
rows = b"ab,cd\nef,gh\nij,kl\nmn,op\n"