| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, | |
| # software distributed under the License is distributed on an | |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| # KIND, either express or implied. See the License for the | |
| # specific language governing permissions and limitations | |
| # under the License. | |
| """Options for reading various file formats.""" | |
| from __future__ import annotations | |
| import warnings | |
| from typing import TYPE_CHECKING | |
| import pyarrow as pa | |
| from datafusion.expr import sort_list_to_raw_sort_list | |
| if TYPE_CHECKING: | |
| from datafusion.expr import SortExpr | |
| from ._internal import options | |
| __all__ = ["CsvReadOptions"] | |
| DEFAULT_MAX_INFER_SCHEMA = 1000 | |
| class CsvReadOptions: | |
| """Options for reading CSV files. | |
| This class provides a builder pattern for configuring CSV reading options. | |
| All methods starting with ``with_`` return ``self`` to allow method chaining. | |
| """ | |
| def __init__( | |
| self, | |
| *, | |
| has_header: bool = True, | |
| delimiter: str = ",", | |
| quote: str = '"', | |
| terminator: str | None = None, | |
| escape: str | None = None, | |
| comment: str | None = None, | |
| newlines_in_values: bool = False, | |
| schema: pa.Schema | None = None, | |
| schema_infer_max_records: int = DEFAULT_MAX_INFER_SCHEMA, | |
| file_extension: str = ".csv", | |
| table_partition_cols: list[tuple[str, pa.DataType]] | None = None, | |
| file_compression_type: str = "", | |
| file_sort_order: list[list[SortExpr]] | None = None, | |
| null_regex: str | None = None, | |
| truncated_rows: bool = False, | |
| ) -> None: | |
| """Initialize CsvReadOptions. | |
| Args: | |
| has_header: Does the CSV file have a header row? If schema inference | |
| is run on a file with no headers, default column names are created. | |
| delimiter: Column delimiter character. Must be a single ASCII character. | |
| quote: Quote character for fields containing delimiters or newlines. | |
| Must be a single ASCII character. | |
| terminator: Optional line terminator character. If ``None``, uses CRLF. | |
| Must be a single ASCII character. | |
| escape: Optional escape character for quotes. Must be a single ASCII | |
| character. | |
| comment: If specified, lines beginning with this character are ignored. | |
| Must be a single ASCII character. | |
| newlines_in_values: Whether newlines in quoted values are supported. | |
| Parsing newlines in quoted values may be affected by execution | |
| behavior such as parallel file scanning. Setting this to ``True`` | |
| ensures that newlines in values are parsed successfully, which may | |
| reduce performance. | |
| schema: Optional PyArrow schema representing the CSV files. If ``None``, | |
| the CSV reader will try to infer it based on data in the file. | |
| schema_infer_max_records: Maximum number of rows to read from CSV files | |
| for schema inference if needed. | |
| file_extension: File extension; only files with this extension are | |
| selected for data input. | |
| table_partition_cols: Partition columns as a list of tuples of | |
| (column_name, data_type). | |
| file_compression_type: File compression type. Supported values are | |
| ``"gzip"``, ``"bz2"``, ``"xz"``, ``"zstd"``, or empty string for | |
| uncompressed. | |
| file_sort_order: Optional sort order of the files as a list of sort | |
| expressions per file. | |
| null_regex: Optional regex pattern to match null values in the CSV. | |
| truncated_rows: Whether to allow truncated rows when parsing. By default | |
| this is ``False`` and will error if the CSV rows have different | |
| lengths. When set to ``True``, it will allow records with less than | |
| the expected number of columns and fill the missing columns with | |
| nulls. If the record's schema is not nullable, it will still return | |
| an error. | |
| """ | |
| validate_single_character("delimiter", delimiter) | |
| validate_single_character("quote", quote) | |
| validate_single_character("terminator", terminator) | |
| validate_single_character("escape", escape) | |
| validate_single_character("comment", comment) | |
| self.has_header = has_header | |
| self.delimiter = delimiter | |
| self.quote = quote | |
| self.terminator = terminator | |
| self.escape = escape | |
| self.comment = comment | |
| self.newlines_in_values = newlines_in_values | |
| self.schema = schema | |
| self.schema_infer_max_records = schema_infer_max_records | |
| self.file_extension = file_extension | |
| self.table_partition_cols = table_partition_cols or [] | |
| self.file_compression_type = file_compression_type | |
| self.file_sort_order = file_sort_order or [] | |
| self.null_regex = null_regex | |
| self.truncated_rows = truncated_rows | |
| def with_has_header(self, has_header: bool) -> CsvReadOptions: | |
| """Configure whether the CSV has a header row.""" | |
| self.has_header = has_header | |
| return self | |
| def with_delimiter(self, delimiter: str) -> CsvReadOptions: | |
| """Configure the column delimiter.""" | |
| self.delimiter = delimiter | |
| return self | |
| def with_quote(self, quote: str) -> CsvReadOptions: | |
| """Configure the quote character.""" | |
| self.quote = quote | |
| return self | |
| def with_terminator(self, terminator: str | None) -> CsvReadOptions: | |
| """Configure the line terminator character.""" | |
| self.terminator = terminator | |
| return self | |
| def with_escape(self, escape: str | None) -> CsvReadOptions: | |
| """Configure the escape character.""" | |
| self.escape = escape | |
| return self | |
| def with_comment(self, comment: str | None) -> CsvReadOptions: | |
| """Configure the comment character.""" | |
| self.comment = comment | |
| return self | |
| def with_newlines_in_values(self, newlines_in_values: bool) -> CsvReadOptions: | |
| """Configure whether newlines in values are supported.""" | |
| self.newlines_in_values = newlines_in_values | |
| return self | |
| def with_schema(self, schema: pa.Schema | None) -> CsvReadOptions: | |
| """Configure the schema.""" | |
| self.schema = schema | |
| return self | |
| def with_schema_infer_max_records( | |
| self, schema_infer_max_records: int | |
| ) -> CsvReadOptions: | |
| """Configure maximum records for schema inference.""" | |
| self.schema_infer_max_records = schema_infer_max_records | |
| return self | |
| def with_file_extension(self, file_extension: str) -> CsvReadOptions: | |
| """Configure the file extension filter.""" | |
| self.file_extension = file_extension | |
| return self | |
| def with_table_partition_cols( | |
| self, table_partition_cols: list[tuple[str, pa.DataType]] | |
| ) -> CsvReadOptions: | |
| """Configure table partition columns.""" | |
| self.table_partition_cols = table_partition_cols | |
| return self | |
| def with_file_compression_type(self, file_compression_type: str) -> CsvReadOptions: | |
| """Configure file compression type.""" | |
| self.file_compression_type = file_compression_type | |
| return self | |
| def with_file_sort_order( | |
| self, file_sort_order: list[list[SortExpr]] | |
| ) -> CsvReadOptions: | |
| """Configure file sort order.""" | |
| self.file_sort_order = file_sort_order | |
| return self | |
| def with_null_regex(self, null_regex: str | None) -> CsvReadOptions: | |
| """Configure null value regex pattern.""" | |
| self.null_regex = null_regex | |
| return self | |
| def with_truncated_rows(self, truncated_rows: bool) -> CsvReadOptions: | |
| """Configure whether to allow truncated rows.""" | |
| self.truncated_rows = truncated_rows | |
| return self | |
| def to_inner(self) -> options.CsvReadOptions: | |
| """Convert this object into the underlying Rust structure. | |
| This is intended for internal use only. | |
| """ | |
| file_sort_order = ( | |
| [] | |
| if self.file_sort_order is None | |
| else [ | |
| sort_list_to_raw_sort_list(sort_list) | |
| for sort_list in self.file_sort_order | |
| ] | |
| ) | |
| return options.CsvReadOptions( | |
| has_header=self.has_header, | |
| delimiter=ord(self.delimiter[0]) if self.delimiter else ord(","), | |
| quote=ord(self.quote[0]) if self.quote else ord('"'), | |
| terminator=ord(self.terminator[0]) if self.terminator else None, | |
| escape=ord(self.escape[0]) if self.escape else None, | |
| comment=ord(self.comment[0]) if self.comment else None, | |
| newlines_in_values=self.newlines_in_values, | |
| schema=self.schema, | |
| schema_infer_max_records=self.schema_infer_max_records, | |
| file_extension=self.file_extension, | |
| table_partition_cols=_convert_table_partition_cols( | |
| self.table_partition_cols | |
| ), | |
| file_compression_type=self.file_compression_type or "", | |
| file_sort_order=file_sort_order, | |
| null_regex=self.null_regex, | |
| truncated_rows=self.truncated_rows, | |
| ) | |
| def validate_single_character(name: str, value: str | None) -> None: | |
| if value is not None and len(value) != 1: | |
| message = f"{name} must be a single character" | |
| raise ValueError(message) | |
| def _convert_table_partition_cols( | |
| table_partition_cols: list[tuple[str, str | pa.DataType]], | |
| ) -> list[tuple[str, pa.DataType]]: | |
| warn = False | |
| converted_table_partition_cols = [] | |
| for col, data_type in table_partition_cols: | |
| if isinstance(data_type, str): | |
| warn = True | |
| if data_type == "string": | |
| converted_data_type = pa.string() | |
| elif data_type == "int": | |
| converted_data_type = pa.int32() | |
| else: | |
| message = ( | |
| f"Unsupported literal data type '{data_type}' for partition " | |
| "column. Supported types are 'string' and 'int'" | |
| ) | |
| raise ValueError(message) | |
| else: | |
| converted_data_type = data_type | |
| converted_table_partition_cols.append((col, converted_data_type)) | |
| if warn: | |
| message = ( | |
| "using literals for table_partition_cols data types is deprecated," | |
| "use pyarrow types instead" | |
| ) | |
| warnings.warn( | |
| message, | |
| category=DeprecationWarning, | |
| stacklevel=2, | |
| ) | |
| return converted_table_partition_cols |