From 774f0a95014c4a0dc1b1f46ccb178c9d3665bd82 Mon Sep 17 00:00:00 2001 From: AlenkaF Date: Tue, 23 Jun 2026 10:09:48 +0200 Subject: [PATCH 1/8] Split the current pyarrow page, no content changes --- docs/source/python/index.rst | 10 +- docs/source/python/parquet.rst | 987 ------------------ docs/source/python/parquet/index.rst | 89 ++ docs/source/python/parquet/parquet.rst | 458 ++++++++ .../python/parquet/parquet_datasets.rst | 229 ++++ .../python/parquet/parquet_encryption.rst | 233 +++++ .../python/parquet/parquet_type_handling.rst | 95 ++ 7 files changed, 1109 insertions(+), 992 deletions(-) delete mode 100644 docs/source/python/parquet.rst create mode 100644 docs/source/python/parquet/index.rst create mode 100644 docs/source/python/parquet/parquet.rst create mode 100644 docs/source/python/parquet/parquet_datasets.rst create mode 100644 docs/source/python/parquet/parquet_encryption.rst create mode 100644 docs/source/python/parquet/parquet_type_handling.rst diff --git a/docs/source/python/index.rst b/docs/source/python/index.rst index cef8998ed4cd..e6e8a62cee95 100644 --- a/docs/source/python/index.rst +++ b/docs/source/python/index.rst @@ -52,17 +52,17 @@ files into Arrow structures. filesystems numpy pandas - interchange_protocol - dlpack timestamps - orc + parquet/index + dataset csv + orc feather json - parquet - dataset flight extending_types + interchange_protocol + dlpack integration env_vars api diff --git a/docs/source/python/parquet.rst b/docs/source/python/parquet.rst deleted file mode 100644 index 4fc1368fb270..000000000000 --- a/docs/source/python/parquet.rst +++ /dev/null @@ -1,987 +0,0 @@ -.. 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. - -.. currentmodule:: pyarrow -.. _parquet: - -Reading and Writing the Apache Parquet Format -============================================= - -The `Apache Parquet `_ project provides a -standardized open-source columnar storage format for use in data analysis -systems. It was created originally for use in `Apache Hadoop -`_ with systems like `Apache Drill -`_, `Apache Hive `_, `Apache -Impala `_, and `Apache Spark -`_ adopting it as a shared standard for high -performance data IO. - -Apache Arrow is an ideal in-memory transport layer for data that is being read -or written with Parquet files. We have been concurrently developing the `C++ -implementation of -Apache Parquet `_, -which includes a native, multithreaded C++ adapter to and from in-memory Arrow -data. PyArrow includes Python bindings to this code, which thus enables reading -and writing Parquet files with pandas as well. - -Obtaining pyarrow with Parquet Support --------------------------------------- - -If you installed ``pyarrow`` with pip or conda, it should be built with Parquet -support bundled: - -.. code-block:: python - - >>> import pyarrow.parquet as pq - -If you are building ``pyarrow`` from source, you must use ``-DARROW_PARQUET=ON`` -when compiling the C++ libraries and enable the Parquet extensions when -building ``pyarrow``. If you want to use Parquet Encryption, then you must -use ``-DPARQUET_REQUIRE_ENCRYPTION=ON`` too when compiling the C++ libraries. -See the :ref:`Python Development ` page for more details. - -Reading and Writing Single Files --------------------------------- - -The functions :func:`~.parquet.read_table` and :func:`~.parquet.write_table` -read and write the :ref:`pyarrow.Table ` object, respectively. - -Let's look at a simple table: - -.. code-block:: python - - >>> import numpy as np - >>> import pandas as pd - >>> import pyarrow as pa - >>> df = pd.DataFrame({'one': [-1, np.nan, 2.5], - ... 'two': ['foo', 'bar', 'baz'], - ... 'three': [True, False, True]}, - ... index=list('abc')) - >>> table = pa.Table.from_pandas(df) - -We write this to Parquet format with ``write_table``: - -.. code-block:: python - - >>> import pyarrow.parquet as pq - >>> pq.write_table(table, 'example.parquet') - -This creates a single Parquet file. In practice, a Parquet dataset may consist -of many files in many directories. We can read a single file back with -``read_table``: - -.. code-block:: python - - >>> table2 = pq.read_table('example.parquet') - >>> table2.to_pandas() - one two three - a -1.0 foo True - b NaN bar False - c 2.5 baz True - -You can pass a subset of columns to read, which can be much faster than reading -the whole file (due to the columnar layout): - -.. code-block:: python - - >>> pq.read_table('example.parquet', columns=['one', 'three']) - pyarrow.Table - one: double - three: bool - ---- - one: [[-1,null,2.5]] - three: [[true,false,true]] - -When reading a subset of columns from a file that used a Pandas dataframe as the -source, we use ``read_pandas`` to maintain any additional index column data: - -.. code-block:: python - - >>> pq.read_pandas('example.parquet', columns=['two']).to_pandas() - two - a foo - b bar - c baz - -We do not need to use a string to specify the origin of the file. It can be any of: - -* A file path as a string -* A :ref:`NativeFile ` from PyArrow -* A Python file object - -In general, a Python file object will have the worst read performance, while a -string file path or an instance of :class:`~.NativeFile` (especially memory -maps) will perform the best. - -.. _parquet_mmap: - -Reading Parquet and Memory Mapping -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Because Parquet data needs to be decoded from the Parquet format -and compression, it can't be directly mapped from disk. -Thus the ``memory_map`` option might perform better on some systems -but won't help much with resident memory consumption. - -.. code-block:: python - - >>> pq_array = pa.parquet.read_table(path, memory_map=True) # doctest: +SKIP - >>> print("RSS: {}MB".format(pa.total_allocated_bytes() >> 20)) # doctest: +SKIP - RSS: 4299MB - - >>> pq_array = pa.parquet.read_table(path, memory_map=False) # doctest: +SKIP - >>> print("RSS: {}MB".format(pa.total_allocated_bytes() >> 20)) # doctest: +SKIP - RSS: 4299MB - -If you need to deal with Parquet data bigger than memory, -the :ref:`dataset` and partitioning is probably what you are looking for. - -Parquet file writing options -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:func:`~pyarrow.parquet.write_table()` has a number of options to -control various settings when writing a Parquet file. - -* ``version``, the Parquet format version to use. ``'1.0'`` ensures - compatibility with older readers, while ``'2.4'`` and greater values - enable more Parquet types and encodings. -* ``data_page_size``, to control the approximate size of encoded data - pages within a column chunk. This currently defaults to 1MB. -* ``flavor``, to set compatibility options particular to a Parquet - consumer like ``'spark'`` for Apache Spark. - -See the :func:`~pyarrow.parquet.write_table()` docstring for more details. - -There are some additional data type handling-specific options -described below. - -Omitting the DataFrame index -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When using ``pa.Table.from_pandas`` to convert to an Arrow table, by default -one or more special columns are added to keep track of the index (row -labels). Storing the index takes extra space, so if your index is not valuable, -you may choose to omit it by passing ``preserve_index=False`` - -.. code-block:: python - - >>> df = pd.DataFrame({'one': [-1, np.nan, 2.5], - ... 'two': ['foo', 'bar', 'baz'], - ... 'three': [True, False, True]}, - ... index=list('abc')) - >>> table = pa.Table.from_pandas(df, preserve_index=False) - -Then we have: - -.. code-block:: python - - >>> pq.write_table(table, 'example_noindex.parquet') - >>> t = pq.read_table('example_noindex.parquet') - >>> t.to_pandas() - one two three - 0 -1.0 foo True - 1 NaN bar False - 2 2.5 baz True - -Here you see the index did not survive the round trip. - -Finer-grained Reading and Writing ---------------------------------- - -``read_table`` uses the :class:`~.ParquetFile` class, which has other features: - -.. code-block:: python - - >>> parquet_file = pq.ParquetFile('example.parquet') - >>> parquet_file.metadata - - created_by: parquet-cpp-arrow version ... - num_columns: 4 - num_rows: 3 - num_row_groups: 1 - format_version: 2.6 - serialized_size: ... - >>> parquet_file.schema - - required group field_id=-1 schema { - optional double field_id=-1 one; - optional binary field_id=-1 two (String); - optional boolean field_id=-1 three; - optional binary field_id=-1 __index_level_0__ (String); - } - - -As you can learn more in the `Apache Parquet format -`_, a Parquet file consists of -multiple row groups. ``read_table`` will read all of the row groups and -concatenate them into a single table. You can read individual row groups with -``read_row_group``: - -.. code-block:: python - - >>> parquet_file.num_row_groups - 1 - >>> parquet_file.read_row_group(0) - pyarrow.Table - one: double - two: large_string - three: bool - __index_level_0__: large_string - ---- - one: [[-1,null,2.5]] - two: [["foo","bar","baz"]] - three: [[true,false,true]] - __index_level_0__: [["a","b","c"]] - -We can similarly write a Parquet file with multiple row groups by using -``ParquetWriter``: - -.. code-block:: python - - >>> with pq.ParquetWriter('example2.parquet', table.schema) as writer: - ... for i in range(3): - ... writer.write_table(table) - >>> pf2 = pq.ParquetFile('example2.parquet') - >>> pf2.num_row_groups - 3 - -Inspecting the Parquet File Metadata ------------------------------------- - -The ``FileMetaData`` of a Parquet file can be accessed through -:class:`~.ParquetFile` as shown above: - -.. code-block:: python - - >>> parquet_file = pq.ParquetFile('example.parquet') - >>> metadata = parquet_file.metadata - >>> metadata - - created_by: parquet-cpp-arrow version ... - num_columns: 4 - num_rows: 3 - num_row_groups: 1 - format_version: 2.6 - serialized_size: ... - -or can also be read directly using :func:`~parquet.read_metadata`: - -.. code-block:: python - - >>> metadata = pq.read_metadata('example.parquet') - >>> metadata - - created_by: parquet-cpp-arrow version ... - num_columns: 4 - num_rows: 3 - num_row_groups: 1 - format_version: 2.6 - serialized_size: ... - -The returned ``FileMetaData`` object allows to inspect the -`Parquet file metadata `__, -such as the row groups and column chunk metadata and statistics: - -.. code-block:: python - - >>> metadata.row_group(0) - - num_columns: 4 - num_rows: 3 - total_byte_size: 290 - sorting_columns: () - >>> metadata.row_group(0).column(0) - - file_offset: 0 - file_path:... - physical_type: DOUBLE - num_values: 3 - path_in_schema: one - is_stats_set: True - statistics: - - has_min_max: True - min: -1.0 - max: 2.5 - null_count: 1 - distinct_count: None - num_values: 2 - physical_type: DOUBLE - logical_type: None - converted_type (legacy): NONE - geo_statistics: - None - compression: SNAPPY - encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY') - has_dictionary_page: True - dictionary_page_offset: 4 - data_page_offset: 36 - total_compressed_size: 106 - total_uncompressed_size: 102 - bloom_filter_offset: None - bloom_filter_length: None - -Data Type Handling ------------------- - -Reading types as DictionaryArray -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``read_dictionary`` option in ``read_table`` and ``ParquetDataset`` will -cause columns to be read as ``DictionaryArray``, which will become -``pandas.Categorical`` when converted to pandas. This option is only valid for -string and binary column types, and it can yield significantly lower memory use -and improved performance for columns with many repeated string values. - -.. code-block:: python - - >>> pq.read_table('example.parquet', read_dictionary=['two']) - pyarrow.Table - one: double - two: dictionary - three: bool - __index_level_0__: large_string - ---- - one: [[-1,null,2.5]] - two: [ -- dictionary: - ["foo","bar","baz"] -- indices: - [0,1,2]] - three: [[true,false,true]] - __index_level_0__: [["a","b","c"]] - -Storing timestamps -~~~~~~~~~~~~~~~~~~ - -Some Parquet readers may only support timestamps stored in millisecond -(``'ms'``) or microsecond (``'us'``) resolution. Since pandas uses nanoseconds -to represent timestamps, this can occasionally be a nuisance. When writing -older ``version='1.0'`` or ``version='2.4'`` Parquet files, the nanoseconds -will be cast to microseconds (``'us'``). - -In addition, We provide the ``coerce_timestamps`` option to allow you to select -the desired resolution: - -.. code-block:: python - - >>> pq.write_table(table, 'example.parquet', coerce_timestamps='ms') - -If a cast to a lower resolution value may result in a loss of data, by default -an exception will be raised. This can be suppressed by passing -``allow_truncated_timestamps=True``: - -.. code-block:: python - - >>> pq.write_table(table, 'example.parquet', coerce_timestamps='ms', - ... allow_truncated_timestamps=True) - -Timestamps with nanoseconds can be stored without casting when using -Parquet format version 2.6, which is the default: - -.. code-block:: python - - >>> pq.write_table(table, 'example.parquet', version='2.6') - -However, some Parquet readers may not yet support this newer format version. -When compatibility across different processing frameworks is required, the -older ``version='1.0'`` or ``version='2.4'`` remain available. - -Older Parquet implementations use ``INT96`` based storage of -timestamps, but this is now deprecated. This includes some older -versions of Apache Impala and Apache Spark. To write timestamps in -this format, set the ``use_deprecated_int96_timestamps`` option to -``True`` in ``write_table``. - -.. code-block:: python - - >>> pq.write_table(table, 'example.parquet', use_deprecated_int96_timestamps=True) - -Compression, Encoding, and File Compatibility ---------------------------------------------- - -The most commonly used Parquet implementations use dictionary encoding when -writing files; if the dictionaries grow too large, then they "fall back" to -plain encoding. Whether dictionary encoding is used can be toggled using the -``use_dictionary`` option: - -.. code-block:: python - - >>> pq.write_table(table, 'example.parquet', use_dictionary=False) - -The data pages within a column in a row group can be compressed after the -encoding passes (dictionary, RLE encoding). In PyArrow we use Snappy -compression by default, but Brotli, Gzip, ZSTD, LZ4, and uncompressed are -also supported: - -.. code-block:: python - - >>> pq.write_table(table, 'example.parquet', compression='snappy') - >>> pq.write_table(table, 'example.parquet', compression='gzip') - >>> pq.write_table(table, 'example.parquet', compression='brotli') - >>> pq.write_table(table, 'example.parquet', compression='zstd') - >>> pq.write_table(table, 'example.parquet', compression='lz4') - >>> pq.write_table(table, 'example.parquet', compression='none') - -Snappy generally results in better performance, while Gzip may yield smaller -files. - -``'lz4_raw'`` is also accepted as an alias for ``'lz4'``. Both use the -LZ4_RAW codec as defined in the Parquet specification. - -These settings can also be set on a per-column basis: - -.. code-block:: python - - >>> pq.write_table(table, 'example.parquet', compression={'one': 'snappy', 'two': 'gzip'}, - ... use_dictionary=['one', 'two']) - -Partitioned Datasets (Multiple Files) ------------------------------------------------- - -Multiple Parquet files constitute a Parquet *dataset*. These may present in a -number of ways: - -* A list of Parquet absolute file paths -* A directory name containing nested directories defining a partitioned dataset - -A dataset partitioned by year and month may look like on disk: - -.. code-block:: text - - dataset_name/ - year=2007/ - month=01/ - 0.parq - 1.parq - ... - month=02/ - 0.parq - 1.parq - ... - month=03/ - ... - year=2008/ - month=01/ - ... - ... - -Writing to Partitioned Datasets -------------------------------- - -You can write a partitioned dataset for any ``pyarrow`` file system that is a -file-store (e.g. local, HDFS, S3). The default behaviour when no filesystem is -added is to use the local filesystem. - -.. code-block:: python - - >>> # Local dataset write - >>> pq.write_to_dataset(table, root_path='dataset_name', - ... partition_cols=['one', 'two']) - -The root path in this case specifies the parent directory to which data will be -saved. The partition columns are the column names by which to partition the -dataset. Columns are partitioned in the order they are given. The partition -splits are determined by the unique values in the partition columns. - -To use another filesystem you only need to add the filesystem parameter, the -individual table writes are wrapped using ``with`` statements so the -``pq.write_to_dataset`` function does not need to be. - -.. code-block:: python - - >>> # Remote file-system example - >>> from pyarrow.fs import HadoopFileSystem # doctest: +SKIP - >>> fs = HadoopFileSystem(host, port, user=user, kerb_ticket=ticket_cache_path) # doctest: +SKIP - >>> pq.write_to_dataset(table, root_path='dataset_name', # doctest: +SKIP - ... partition_cols=['one', 'two'], filesystem=fs) - -Compatibility Note: if using ``pq.write_to_dataset`` to create a table that -will then be used by HIVE then partition column values must be compatible with -the allowed character set of the HIVE version you are running. - -Writing ``_metadata`` and ``_common_metadata`` files -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Some processing frameworks such as Spark or Dask (optionally) use ``_metadata`` -and ``_common_metadata`` files with partitioned datasets. - -Those files include information about the schema of the full dataset (for -``_common_metadata``) and potentially all row group metadata of all files in the -partitioned dataset as well (for ``_metadata``). The actual files are -metadata-only Parquet files. Note this is not a Parquet standard, but a -convention set in practice by those frameworks. - -Using those files can give a more efficient creation of a parquet Dataset, -since it can use the stored schema and file paths of all row groups, -instead of inferring the schema and crawling the directories for all Parquet -files (this is especially the case for filesystems where accessing files -is expensive). - -The :func:`~pyarrow.parquet.write_to_dataset` function does not automatically -write such metadata files, but you can use it to gather the metadata and -combine and write them manually: - -.. code-block:: python - - >>> # Write a dataset and collect metadata information of all written files - >>> metadata_collector = [] - >>> root_path = "dataset_name_1" - >>> pq.write_to_dataset(table, root_path, metadata_collector=metadata_collector) - - >>> # Write the ``_common_metadata`` parquet file without row groups statistics - >>> pq.write_metadata(table.schema, root_path + '/_common_metadata') - - >>> # Write the ``_metadata`` parquet file with row groups statistics of all files - >>> pq.write_metadata( - ... table.schema, root_path + '/_metadata', - ... metadata_collector=metadata_collector - ... ) - -When not using the :func:`~pyarrow.parquet.write_to_dataset` function, but -writing the individual files of the partitioned dataset using -:func:`~pyarrow.parquet.write_table` or :class:`~pyarrow.parquet.ParquetWriter`, -the ``metadata_collector`` keyword can also be used to collect the FileMetaData -of the written files. In this case, you need to ensure to set the file path -contained in the row group metadata yourself before combining the metadata, and -the schemas of all different files and collected FileMetaData objects should be -the same: - -.. code-block:: python - - >>> import os - >>> os.mkdir("year=2017") - - >>> metadata_collector = [] - >>> pq.write_table( - ... table, "year=2017/data1.parquet", - ... metadata_collector=metadata_collector - ... ) - - >>> # set the file path relative to the root of the partitioned dataset - >>> metadata_collector[-1].set_file_path("year=2017/data1.parquet") - - >>> # combine and write the metadata - >>> metadata = metadata_collector[0] - >>> for _meta in metadata_collector[1:]: - ... metadata.append_row_groups(_meta) - >>> metadata.write_metadata_file("_metadata") - - >>> # or use pq.write_metadata to combine and write in a single step - >>> pq.write_metadata( - ... table.schema, "_metadata", - ... metadata_collector=metadata_collector - ... ) - - >>> pq.read_metadata("_metadata") - - created_by: parquet-cpp-arrow version ... - num_columns: 3 - num_rows: 3 - num_row_groups: 1 - format_version: 2.6 - serialized_size: ... - -Reading from Partitioned Datasets ------------------------------------------------- - -The :class:`~.ParquetDataset` class accepts either a directory name or a list -of file paths, and can discover and infer some common partition structures, -such as those produced by Hive: - -.. code-block:: python - - >>> dataset = pq.ParquetDataset('dataset_name/') - >>> table = dataset.read() - >>> table - pyarrow.Table - three: bool - one: dictionary - two: dictionary - ---- - three: [[true],[true],[false]] - one: [ -- dictionary: - ["-1","2.5"] -- indices: - [0], -- dictionary: - ["-1","2.5"] -- indices: - [1], -- dictionary: - [null] -- indices: - [0]] - two: [ -- dictionary: - ["foo","baz","bar"] -- indices: - [0], -- dictionary: - ["foo","baz","bar"] -- indices: - [1], -- dictionary: - ["foo","baz","bar"] -- indices: - [2]] - -You can also use the convenience function ``read_table`` exposed by -``pyarrow.parquet`` that avoids the need for an additional Dataset object -creation step. - -.. code-block:: python - - >>> table = pq.read_table('dataset_name') - -Note: the partition columns in the original table will have their types -converted to Arrow dictionary types (pandas categorical) on load. Ordering of -partition columns is not preserved through the save/load process. If reading -from a remote filesystem into a pandas dataframe you may need to run -``sort_index`` to maintain row ordering (as long as the ``preserve_index`` -option was enabled on write). - -Other features: - -- Filtering on all columns (using row group statistics) instead of only on - the partition keys. -- Fine-grained partitioning: support for a directory partitioning scheme - in addition to the Hive-like partitioning (e.g. "/2019/11/15/" instead of - "/year=2019/month=11/day=15/"), and the ability to specify a schema for - the partition keys. - -Note: - -- The partition keys need to be explicitly included in the ``columns`` - keyword when you want to include them in the result while reading a - subset of the columns - - -Using with Spark ----------------- - -Spark places some constraints on the types of Parquet files it will read. The -option ``flavor='spark'`` will set these options automatically and also -sanitize field characters unsupported by Spark SQL. - -Multithreaded Reads -------------------- - -Each of the reading functions by default use multi-threading for reading -columns in parallel. Depending on the speed of IO -and how expensive it is to decode the columns in a particular file -(particularly with GZIP compression), this can yield significantly higher data -throughput. - -This can be disabled by specifying ``use_threads=False``. - -.. note:: - The number of threads to use concurrently is automatically inferred by Arrow - and can be inspected using the :func:`~pyarrow.cpu_count()` function. - -Reading from cloud storage --------------------------- - -In addition to local files, pyarrow supports other filesystems, such as cloud -filesystems, through the ``filesystem`` keyword: - -.. code-block:: python - - >>> from pyarrow import fs - - >>> s3 = fs.S3FileSystem(region="us-east-2") # doctest: +SKIP - >>> table = pq.read_table("bucket/object/key/prefix", filesystem=s3) # doctest: +SKIP - -Currently, :class:`HDFS ` and -:class:`Amazon S3-compatible storage ` are -supported. See the :ref:`filesystem` docs for more details. For those -built-in filesystems, the filesystem can also be inferred from the file path, -if specified as a URI: - -.. code-block:: python - - >>> table = pq.read_table("s3://bucket/object/key/prefix") # doctest: +SKIP - -Other filesystems can still be supported if there is an -`fsspec `__-compatible -implementation available. See :ref:`filesystem-fsspec` for more details. -One example is Azure Blob storage, which can be interfaced through the -`adlfs `__ package. - -.. code-block:: python - - >>> from adlfs import AzureBlobFileSystem # doctest: +SKIP - >>> abfs = AzureBlobFileSystem(account_name="XXXX", account_key="XXXX", container_name="XXXX") # doctest: +SKIP - >>> table = pq.read_table("file.parquet", filesystem=abfs) # doctest: +SKIP - -Parquet Modular Encryption (Columnar Encryption) ------------------------------------------------- - -Columnar encryption is supported for Parquet files in C++ starting from -Apache Arrow 4.0.0 and in PyArrow starting from Apache Arrow 6.0.0. - -Parquet uses the envelope encryption practice, where file parts are encrypted -with "data encryption keys" (DEKs), and the DEKs are encrypted with "master -encryption keys" (MEKs). The DEKs are randomly generated by Parquet for each -encrypted file/column. The MEKs are generated, stored and managed in a Key -Management Service (KMS) of user’s choice. - -Reading and writing encrypted Parquet files involves passing file encryption -and decryption properties to :class:`~pyarrow.parquet.ParquetWriter` and to -:class:`~.ParquetFile`, respectively. - -Writing an encrypted Parquet file: - -.. code-block:: python - - >>> encryption_properties = crypto_factory.file_encryption_properties( # doctest: +SKIP - ... kms_connection_config, encryption_config) - >>> with pq.ParquetWriter(filename, schema, # doctest: +SKIP - ... encryption_properties=encryption_properties) as writer: - ... writer.write_table(table) - -Reading an encrypted Parquet file: - -.. code-block:: python - - >>> decryption_properties = crypto_factory.file_decryption_properties( # doctest: +SKIP - ... kms_connection_config) - >>> parquet_file = pq.ParquetFile(filename, # doctest: +SKIP - ... decryption_properties=decryption_properties) - - -In order to create the encryption and decryption properties, a -:class:`pyarrow.parquet.encryption.CryptoFactory` should be created and -initialized with KMS Client details, as described below. - - -KMS Client -~~~~~~~~~~ - -The master encryption keys should be kept and managed in a production-grade -Key Management System (KMS), deployed in the user's organization. Using Parquet -encryption requires implementation of a client class for the KMS server. -Any KmsClient implementation should implement the informal interface -defined by :class:`pyarrow.parquet.encryption.KmsClient` as following: - -.. code-block:: python - - >>> import pyarrow.parquet.encryption as pe - >>> class MyKmsClient(pe.KmsClient): - ... - ... """An example KmsClient implementation skeleton""" - ... def __init__(self, kms_connection_configuration): - ... pe.KmsClient.__init__(self) - ... # Any KMS-specific initialization based on - ... # kms_connection_configuration comes here - ... - ... def wrap_key(self, key_bytes, master_key_identifier): - ... wrapped_key = ... # call KMS to wrap key_bytes with key specified by - ... # master_key_identifier - ... return wrapped_key - ... - ... def unwrap_key(self, wrapped_key, master_key_identifier): - ... key_bytes = ... # call KMS to unwrap wrapped_key with key specified by - ... # master_key_identifier - ... return key_bytes - -The concrete implementation will be loaded at runtime by a factory function -provided by the user. This factory function will be used to initialize the -:class:`pyarrow.parquet.encryption.CryptoFactory` for creating file encryption -and decryption properties. - -For example, in order to use the ``MyKmsClient`` defined above: - -.. code-block:: python - - >>> def kms_client_factory(kms_connection_configuration): - ... return MyKmsClient(kms_connection_configuration) - - >>> crypto_factory = pe.CryptoFactory(kms_client_factory) - -An :download:`example <../../../python/examples/parquet_encryption/sample_vault_kms_client.py>` -of such a class for an open source -`KMS `_ can be found in the Apache -Arrow GitHub repository. The production KMS client should be designed in -cooperation with an organization's security administrators, and built by -developers with experience in access control management. Once such a class is -created, it can be passed to applications via a factory method and leveraged -by general PyArrow users as shown in the encrypted parquet write/read sample -above. - -KMS connection configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Configuration of connection to KMS (:class:`pyarrow.parquet.encryption.KmsConnectionConfig` -used when creating file encryption and decryption properties) includes the -following options: - -* ``kms_instance_url``, URL of the KMS instance. -* ``kms_instance_id``, ID of the KMS instance that will be used for encryption - (if multiple KMS instances are available). -* ``key_access_token``, authorization token that will be passed to KMS. -* ``custom_kms_conf``, a string dictionary with KMS-type-specific configuration. - -Encryption configuration -~~~~~~~~~~~~~~~~~~~~~~~~ - -:class:`pyarrow.parquet.encryption.EncryptionConfiguration` (used when -creating file encryption properties) includes the following options: - -* ``footer_key``, the ID of the master key for footer encryption/signing. -* ``column_keys``, which columns to encrypt with which key. Dictionary with - master key IDs as the keys, and column name lists as the values, - e.g. ``{key1: [col1, col2], key2: [col3]}``. See notes on nested fields below. -* ``encryption_algorithm``, the Parquet encryption algorithm. - Can be ``AES_GCM_V1`` (default) or ``AES_GCM_CTR_V1``. -* ``plaintext_footer``, whether to write the file footer in plain text (otherwise it is encrypted). -* ``double_wrapping``, whether to use double wrapping - where data encryption keys (DEKs) - are encrypted with key encryption keys (KEKs), which in turn are encrypted - with master encryption keys (MEKs). If set to ``false``, single wrapping is - used - where DEKs are encrypted directly with MEKs. -* ``cache_lifetime``, the lifetime of cached entities (key encryption keys, - local wrapping keys, KMS client objects) represented as a ``datetime.timedelta``. -* ``internal_key_material``, whether to store key material inside Parquet file footers; - this mode doesn’t produce additional files. If set to ``false``, key material is - stored in separate files in the same folder, which enables key rotation for - immutable Parquet files. -* ``data_key_length_bits``, the length of data encryption keys (DEKs), randomly - generated by Parquet key management tools. Can be 128, 192 or 256 bits. - -.. note:: - When ``double_wrapping`` is true, Parquet implements a "double envelope - encryption" mode that minimizes the interaction of the program with a KMS - server. In this mode, the DEKs are encrypted with "key encryption keys" - (KEKs, randomly generated by Parquet). The KEKs are encrypted with "master - encryption keys" (MEKs) in the KMS; the result and the KEK itself are - cached in the process memory. - -An example encryption configuration: - -.. code-block:: python - - >>> encryption_config = pe.EncryptionConfiguration( - ... footer_key="footer_key_name", - ... column_keys={ - ... "column_key_name": ["Column1", "Column2"], - ... }, - ... ) - -.. note:: - - Columns with nested fields (struct or map data types) can be encrypted as a whole, or only - individual fields. Configure an encryption key for the root column name to encrypt all nested - fields with this key, or configure a key for individual leaf nested fields. - - Conventionally, the key and value fields of a map column ``m`` have the names - ``m.key_value.key`` and ``m.key_value.value``, respectively. - An inner field ``f`` of a struct column ``s`` has the name ``s.f``. - - With above example, *all* inner fields are encrypted with the same key by configuring that key - for column ``m`` and ``s``, respectively. - -An example encryption configuration for columns with nested fields, where -all columns are encrypted with the same key identified by ``column_key_id``: - -.. code-block:: python - - >>> import pyarrow.parquet.encryption as pe - - schema = pa.schema([ - ("MapColumn", pa.map_(pa.string(), pa.int32())), - ("StructColumn", pa.struct([("f1", pa.int32()), ("f2", pa.string())])), - ]) - - encryption_config = pe.EncryptionConfiguration( - footer_key="footer_key_name", - column_keys={ - "column_key_id": [ "MapColumn", "StructColumn" ], - }, - ) - -An example encryption configuration for columns with nested fields, where -some inner fields are encrypted with the same key identified by ``column_key_id``: - -.. code-block:: python - - >>> import pyarrow.parquet.encryption as pe - - >>> schema = pa.schema([ - ... ("MapColumn", pa.map_(pa.string(), pa.int32())), - ... ("StructColumn", pa.struct([("f1", pa.int32()), ("f2", pa.string())])), - ... ]) - - >>> encryption_config = pe.EncryptionConfiguration( - ... footer_key="footer_key_name", - ... column_keys={ - ... "column_key_id": [ "MapColumn.key_value.value", "StructColumn.f1" ], - ... }, - ... ) - -Decryption configuration -~~~~~~~~~~~~~~~~~~~~~~~~ - -:class:`pyarrow.parquet.encryption.DecryptionConfiguration` (used when creating -file decryption properties) is optional and it includes the following options: - -* ``cache_lifetime``, the lifetime of cached entities (key encryption keys, local - wrapping keys, KMS client objects) represented as a ``datetime.timedelta``. - - -Content-Defined Chunking ------------------------- - -.. note:: - This feature is experimental and may change in future releases. - -PyArrow introduces an experimental feature for optimizing Parquet files for content -addressable storage (CAS) systems using content-defined chunking (CDC). This feature -enables efficient deduplication of data across files, improving network transfers and -storage efficiency. - -When enabled, data pages are written according to content-defined chunk boundaries, -determined by a rolling hash algorithm that identifies chunk boundaries based on the -actual content of the data. When data in a column is modified (e.g., inserted, deleted, -or updated), this approach minimizes the number of changed data pages. - -The feature can be enabled by setting the ``use_content_defined_chunking`` parameter in -the Parquet writer. It accepts either a boolean or a dictionary for configuration: - -- ``True``: Uses the default configuration with: - - Minimum chunk size: 256 KiB - - Maximum chunk size: 1024 KiB - - Normalization level: 0 - -- ``dict``: Allows customization of the chunking parameters: - - ``min_chunk_size``: Minimum chunk size in bytes (default: 256 KiB). - - ``max_chunk_size``: Maximum chunk size in bytes (default: 1024 KiB). - - ``norm_level``: Normalization level to adjust chunk size distribution (default: 0). - -Note that the chunk size is calculated on the logical values before applying any encoding -or compression. The actual size of the data pages may vary based on the encoding and -compression used. - -.. note:: - To make the most of this feature, you should ensure that Parquet write options - remain consistent across writes and files. - Using different write options (like compression, encoding, or row group size) - for different files may prevent proper deduplication and lead to suboptimal - storage efficiency. - -.. code-block:: python - - >>> table = pa.Table.from_pandas(df) - - >>> # Enable content-defined chunking with default settings - >>> pq.write_table(table, 'example.parquet', use_content_defined_chunking=True) - - >>> # Enable content-defined chunking with custom settings - >>> pq.write_table( - ... table, - ... 'example_custom.parquet', - ... use_content_defined_chunking={ - ... 'min_chunk_size': 128 * 1024, # 128 KiB - ... 'max_chunk_size': 512 * 1024, # 512 KiB - ... } - ... ) diff --git a/docs/source/python/parquet/index.rst b/docs/source/python/parquet/index.rst new file mode 100644 index 000000000000..1da5f8f00ce3 --- /dev/null +++ b/docs/source/python/parquet/index.rst @@ -0,0 +1,89 @@ +.. 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. + +.. currentmodule:: pyarrow +.. _parquet: + +Reading and Writing the Apache Parquet Format +============================================= + +The `Apache Parquet `_ project provides a +standardized open-source columnar storage format for use in data analysis +systems. It was created originally for use in `Apache Hadoop +`_ with systems like `Apache Drill +`_, `Apache Hive `_, `Apache +Impala `_, and `Apache Spark +`_ adopting it as a shared standard for high +performance data IO. + +Apache Arrow is an ideal in-memory transport layer for data that is being read +or written with Parquet files. We have been concurrently developing the `C++ +implementation of +Apache Parquet `_, +which includes a native, multithreaded C++ adapter to and from in-memory Arrow +data. PyArrow includes Python bindings to this code, which thus enables reading +and writing Parquet files with pandas as well. + +Obtaining pyarrow with Parquet Support +-------------------------------------- + +If you installed ``pyarrow`` with pip or conda, it should be built with Parquet +support bundled: + +.. code-block:: python + + >>> import pyarrow.parquet as pq + +If you are building ``pyarrow`` from source, you must use ``-DARROW_PARQUET=ON`` +when compiling the C++ libraries and enable the Parquet extensions when +building ``pyarrow``. If you want to use Parquet Encryption, then you must +use ``-DPARQUET_REQUIRE_ENCRYPTION=ON`` too when compiling the C++ libraries. +See the :ref:`Python Development ` page for more details. + + +Reading and Writing Single Files +-------------------------------- + +.. toctree:: + :maxdepth: 2 + + parquet + +Data Type Handling +------------------ + +.. toctree:: + :maxdepth: 2 + + parquet_type_handling + + +Partitioned Datasets (Multiple Files) +------------------------------------- + +.. toctree:: + :maxdepth: 2 + + parquet_datasets + +Parquet Modular Encryption +-------------------------- + +.. toctree:: + :maxdepth: 2 + + parquet_encryption diff --git a/docs/source/python/parquet/parquet.rst b/docs/source/python/parquet/parquet.rst new file mode 100644 index 000000000000..e31edff2389e --- /dev/null +++ b/docs/source/python/parquet/parquet.rst @@ -0,0 +1,458 @@ +.. 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. + +.. currentmodule:: pyarrow +.. _parquet_single_file: + + +Reading and Writing Single Files +================================ + +The functions :func:`~.parquet.read_table` and :func:`~.parquet.write_table` +read and write the :ref:`pyarrow.Table ` object, respectively. + +Let's look at a simple table: + +.. code-block:: python + + >>> import numpy as np + >>> import pandas as pd + >>> import pyarrow as pa + >>> df = pd.DataFrame({'one': [-1, np.nan, 2.5], + ... 'two': ['foo', 'bar', 'baz'], + ... 'three': [True, False, True]}, + ... index=list('abc')) + >>> table = pa.Table.from_pandas(df) + +We write this to Parquet format with ``write_table``: + +.. code-block:: python + + >>> import pyarrow.parquet as pq + >>> pq.write_table(table, 'example.parquet') + +This creates a single Parquet file. In practice, a Parquet dataset may consist +of many files in many directories. We can read a single file back with +``read_table``: + +.. code-block:: python + + >>> table2 = pq.read_table('example.parquet') + >>> table2.to_pandas() + one two three + a -1.0 foo True + b NaN bar False + c 2.5 baz True + +You can pass a subset of columns to read, which can be much faster than reading +the whole file (due to the columnar layout): + +.. code-block:: python + + >>> pq.read_table('example.parquet', columns=['one', 'three']) + pyarrow.Table + one: double + three: bool + ---- + one: [[-1,null,2.5]] + three: [[true,false,true]] + +When reading a subset of columns from a file that used a Pandas dataframe as the +source, we use ``read_pandas`` to maintain any additional index column data: + +.. code-block:: python + + >>> pq.read_pandas('example.parquet', columns=['two']).to_pandas() + two + a foo + b bar + c baz + +We do not need to use a string to specify the origin of the file. It can be any of: + +* A file path as a string +* A :ref:`NativeFile ` from PyArrow +* A Python file object + +In general, a Python file object will have the worst read performance, while a +string file path or an instance of :class:`~.NativeFile` (especially memory +maps) will perform the best. + +.. _parquet_mmap: + +Reading Parquet and Memory Mapping +---------------------------------- + +Because Parquet data needs to be decoded from the Parquet format +and compression, it can't be directly mapped from disk. +Thus the ``memory_map`` option might perform better on some systems +but won't help much with resident memory consumption. + +.. code-block:: python + + >>> pq_array = pa.parquet.read_table(path, memory_map=True) # doctest: +SKIP + >>> print("RSS: {}MB".format(pa.total_allocated_bytes() >> 20)) # doctest: +SKIP + RSS: 4299MB + + >>> pq_array = pa.parquet.read_table(path, memory_map=False) # doctest: +SKIP + >>> print("RSS: {}MB".format(pa.total_allocated_bytes() >> 20)) # doctest: +SKIP + RSS: 4299MB + +If you need to deal with Parquet data bigger than memory, +the :ref:`dataset` and partitioning is probably what you are looking for. + +Parquet file writing options +---------------------------- + +:func:`~pyarrow.parquet.write_table()` has a number of options to +control various settings when writing a Parquet file. + +* ``version``, the Parquet format version to use. ``'1.0'`` ensures + compatibility with older readers, while ``'2.4'`` and greater values + enable more Parquet types and encodings. +* ``data_page_size``, to control the approximate size of encoded data + pages within a column chunk. This currently defaults to 1MB. +* ``flavor``, to set compatibility options particular to a Parquet + consumer like ``'spark'`` for Apache Spark. + +See the :func:`~pyarrow.parquet.write_table()` docstring for more details. + +There are some additional data type handling-specific options +described below. + +Omitting the DataFrame index +---------------------------- + +When using ``pa.Table.from_pandas`` to convert to an Arrow table, by default +one or more special columns are added to keep track of the index (row +labels). Storing the index takes extra space, so if your index is not valuable, +you may choose to omit it by passing ``preserve_index=False`` + +.. code-block:: python + + >>> df = pd.DataFrame({'one': [-1, np.nan, 2.5], + ... 'two': ['foo', 'bar', 'baz'], + ... 'three': [True, False, True]}, + ... index=list('abc')) + >>> table = pa.Table.from_pandas(df, preserve_index=False) + +Then we have: + +.. code-block:: python + + >>> pq.write_table(table, 'example_noindex.parquet') + >>> t = pq.read_table('example_noindex.parquet') + >>> t.to_pandas() + one two three + 0 -1.0 foo True + 1 NaN bar False + 2 2.5 baz True + +Here you see the index did not survive the round trip. + +Finer-grained Reading and Writing +--------------------------------- + +``read_table`` uses the :class:`~.ParquetFile` class, which has other features: + +.. code-block:: python + + >>> parquet_file = pq.ParquetFile('example.parquet') + >>> parquet_file.metadata + + created_by: parquet-cpp-arrow version ... + num_columns: 4 + num_rows: 3 + num_row_groups: 1 + format_version: 2.6 + serialized_size: ... + >>> parquet_file.schema + + required group field_id=-1 schema { + optional double field_id=-1 one; + optional binary field_id=-1 two (String); + optional boolean field_id=-1 three; + optional binary field_id=-1 __index_level_0__ (String); + } + + +As you can learn more in the `Apache Parquet format +`_, a Parquet file consists of +multiple row groups. ``read_table`` will read all of the row groups and +concatenate them into a single table. You can read individual row groups with +``read_row_group``: + +.. code-block:: python + + >>> parquet_file.num_row_groups + 1 + >>> parquet_file.read_row_group(0) + pyarrow.Table + one: double + two: large_string + three: bool + __index_level_0__: large_string + ---- + one: [[-1,null,2.5]] + two: [["foo","bar","baz"]] + three: [[true,false,true]] + __index_level_0__: [["a","b","c"]] + +We can similarly write a Parquet file with multiple row groups by using +``ParquetWriter``: + +.. code-block:: python + + >>> with pq.ParquetWriter('example2.parquet', table.schema) as writer: + ... for i in range(3): + ... writer.write_table(table) + >>> pf2 = pq.ParquetFile('example2.parquet') + >>> pf2.num_row_groups + 3 + +Inspecting the Parquet File Metadata +------------------------------------ + +The ``FileMetaData`` of a Parquet file can be accessed through +:class:`~.ParquetFile` as shown above: + +.. code-block:: python + + >>> parquet_file = pq.ParquetFile('example.parquet') + >>> metadata = parquet_file.metadata + >>> metadata + + created_by: parquet-cpp-arrow version ... + num_columns: 4 + num_rows: 3 + num_row_groups: 1 + format_version: 2.6 + serialized_size: ... + +or can also be read directly using :func:`~parquet.read_metadata`: + +.. code-block:: python + + >>> metadata = pq.read_metadata('example.parquet') + >>> metadata + + created_by: parquet-cpp-arrow version ... + num_columns: 4 + num_rows: 3 + num_row_groups: 1 + format_version: 2.6 + serialized_size: ... + +The returned ``FileMetaData`` object allows to inspect the +`Parquet file metadata `__, +such as the row groups and column chunk metadata and statistics: + +.. code-block:: python + + >>> metadata.row_group(0) + + num_columns: 4 + num_rows: 3 + total_byte_size: 290 + sorting_columns: () + >>> metadata.row_group(0).column(0) + + file_offset: 0 + file_path:... + physical_type: DOUBLE + num_values: 3 + path_in_schema: one + is_stats_set: True + statistics: + + has_min_max: True + min: -1.0 + max: 2.5 + null_count: 1 + distinct_count: None + num_values: 2 + physical_type: DOUBLE + logical_type: None + converted_type (legacy): NONE + geo_statistics: + None + compression: SNAPPY + encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY') + has_dictionary_page: True + dictionary_page_offset: 4 + data_page_offset: 36 + total_compressed_size: 106 + total_uncompressed_size: 102 + bloom_filter_offset: None + bloom_filter_length: None + +Multithreaded Reads +------------------- + +Each of the reading functions by default use multi-threading for reading +columns in parallel. Depending on the speed of IO +and how expensive it is to decode the columns in a particular file +(particularly with GZIP compression), this can yield significantly higher data +throughput. + +This can be disabled by specifying ``use_threads=False``. + +.. note:: + The number of threads to use concurrently is automatically inferred by Arrow + and can be inspected using the :func:`~pyarrow.cpu_count()` function. + + +Compression, Encoding, and File Compatibility +--------------------------------------------- + +The most commonly used Parquet implementations use dictionary encoding when +writing files; if the dictionaries grow too large, then they "fall back" to +plain encoding. Whether dictionary encoding is used can be toggled using the +``use_dictionary`` option: + +.. code-block:: python + + >>> pq.write_table(table, 'example.parquet', use_dictionary=False) + +The data pages within a column in a row group can be compressed after the +encoding passes (dictionary, RLE encoding). In PyArrow we use Snappy +compression by default, but Brotli, Gzip, ZSTD, LZ4, and uncompressed are +also supported: + +.. code-block:: python + + >>> pq.write_table(table, 'example.parquet', compression='snappy') + >>> pq.write_table(table, 'example.parquet', compression='gzip') + >>> pq.write_table(table, 'example.parquet', compression='brotli') + >>> pq.write_table(table, 'example.parquet', compression='zstd') + >>> pq.write_table(table, 'example.parquet', compression='lz4') + >>> pq.write_table(table, 'example.parquet', compression='none') + +Snappy generally results in better performance, while Gzip may yield smaller +files. + +``'lz4_raw'`` is also accepted as an alias for ``'lz4'``. Both use the +LZ4_RAW codec as defined in the Parquet specification. + +These settings can also be set on a per-column basis: + +.. code-block:: python + + >>> pq.write_table(table, 'example.parquet', compression={'one': 'snappy', 'two': 'gzip'}, + ... use_dictionary=['one', 'two']) + + +Using with Spark +---------------- + +Spark places some constraints on the types of Parquet files it will read. The +option ``flavor='spark'`` will set these options automatically and also +sanitize field characters unsupported by Spark SQL. + + + +Reading from cloud storage +-------------------------- + +In addition to local files, pyarrow supports other filesystems, such as cloud +filesystems, through the ``filesystem`` keyword: + +.. code-block:: python + + >>> from pyarrow import fs + + >>> s3 = fs.S3FileSystem(region="us-east-2") # doctest: +SKIP + >>> table = pq.read_table("bucket/object/key/prefix", filesystem=s3) # doctest: +SKIP + +Currently, :class:`HDFS ` and +:class:`Amazon S3-compatible storage ` are +supported. See the :ref:`filesystem` docs for more details. For those +built-in filesystems, the filesystem can also be inferred from the file path, +if specified as a URI: + +.. code-block:: python + + >>> table = pq.read_table("s3://bucket/object/key/prefix") # doctest: +SKIP + +Other filesystems can still be supported if there is an +`fsspec `__-compatible +implementation available. See :ref:`filesystem-fsspec` for more details. +One example is Azure Blob storage, which can be interfaced through the +`adlfs `__ package. + +.. code-block:: python + + >>> from adlfs import AzureBlobFileSystem # doctest: +SKIP + >>> abfs = AzureBlobFileSystem(account_name="XXXX", account_key="XXXX", container_name="XXXX") # doctest: +SKIP + >>> table = pq.read_table("file.parquet", filesystem=abfs) # doctest: +SKIP + +Content-Defined Chunking +------------------------ + +.. note:: + This feature is experimental and may change in future releases. + +PyArrow introduces an experimental feature for optimizing Parquet files for content +addressable storage (CAS) systems using content-defined chunking (CDC). This feature +enables efficient deduplication of data across files, improving network transfers and +storage efficiency. + +When enabled, data pages are written according to content-defined chunk boundaries, +determined by a rolling hash algorithm that identifies chunk boundaries based on the +actual content of the data. When data in a column is modified (e.g., inserted, deleted, +or updated), this approach minimizes the number of changed data pages. + +The feature can be enabled by setting the ``use_content_defined_chunking`` parameter in +the Parquet writer. It accepts either a boolean or a dictionary for configuration: + +- ``True``: Uses the default configuration with: + - Minimum chunk size: 256 KiB + - Maximum chunk size: 1024 KiB + - Normalization level: 0 + +- ``dict``: Allows customization of the chunking parameters: + - ``min_chunk_size``: Minimum chunk size in bytes (default: 256 KiB). + - ``max_chunk_size``: Maximum chunk size in bytes (default: 1024 KiB). + - ``norm_level``: Normalization level to adjust chunk size distribution (default: 0). + +Note that the chunk size is calculated on the logical values before applying any encoding +or compression. The actual size of the data pages may vary based on the encoding and +compression used. + +.. note:: + To make the most of this feature, you should ensure that Parquet write options + remain consistent across writes and files. + Using different write options (like compression, encoding, or row group size) + for different files may prevent proper deduplication and lead to suboptimal + storage efficiency. + +.. code-block:: python + + >>> table = pa.Table.from_pandas(df) + + >>> # Enable content-defined chunking with default settings + >>> pq.write_table(table, 'example.parquet', use_content_defined_chunking=True) + + >>> # Enable content-defined chunking with custom settings + >>> pq.write_table( + ... table, + ... 'example_custom.parquet', + ... use_content_defined_chunking={ + ... 'min_chunk_size': 128 * 1024, # 128 KiB + ... 'max_chunk_size': 512 * 1024, # 512 KiB + ... } + ... ) diff --git a/docs/source/python/parquet/parquet_datasets.rst b/docs/source/python/parquet/parquet_datasets.rst new file mode 100644 index 000000000000..10abfa4242cc --- /dev/null +++ b/docs/source/python/parquet/parquet_datasets.rst @@ -0,0 +1,229 @@ +.. 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. + +.. currentmodule:: pyarrow +.. _parquet_datasets: + + +Partitioned Datasets (Multiple Files) +===================================== + +Multiple Parquet files constitute a Parquet *dataset*. These may present in a +number of ways: + +* A list of Parquet absolute file paths +* A directory name containing nested directories defining a partitioned dataset + +A dataset partitioned by year and month may look like on disk: + +.. code-block:: text + + dataset_name/ + year=2007/ + month=01/ + 0.parq + 1.parq + ... + month=02/ + 0.parq + 1.parq + ... + month=03/ + ... + year=2008/ + month=01/ + ... + ... + +Writing to Partitioned Datasets +------------------------------- + +You can write a partitioned dataset for any ``pyarrow`` file system that is a +file-store (e.g. local, HDFS, S3). The default behaviour when no filesystem is +added is to use the local filesystem. + +.. code-block:: python + + >>> # Local dataset write + >>> pq.write_to_dataset(table, root_path='dataset_name', + ... partition_cols=['one', 'two']) + +The root path in this case specifies the parent directory to which data will be +saved. The partition columns are the column names by which to partition the +dataset. Columns are partitioned in the order they are given. The partition +splits are determined by the unique values in the partition columns. + +To use another filesystem you only need to add the filesystem parameter, the +individual table writes are wrapped using ``with`` statements so the +``pq.write_to_dataset`` function does not need to be. + +.. code-block:: python + + >>> # Remote file-system example + >>> from pyarrow.fs import HadoopFileSystem # doctest: +SKIP + >>> fs = HadoopFileSystem(host, port, user=user, kerb_ticket=ticket_cache_path) # doctest: +SKIP + >>> pq.write_to_dataset(table, root_path='dataset_name', # doctest: +SKIP + ... partition_cols=['one', 'two'], filesystem=fs) + +Compatibility Note: if using ``pq.write_to_dataset`` to create a table that +will then be used by HIVE then partition column values must be compatible with +the allowed character set of the HIVE version you are running. + +Writing ``_metadata`` and ``_common_metadata`` files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some processing frameworks such as Spark or Dask (optionally) use ``_metadata`` +and ``_common_metadata`` files with partitioned datasets. + +Those files include information about the schema of the full dataset (for +``_common_metadata``) and potentially all row group metadata of all files in the +partitioned dataset as well (for ``_metadata``). The actual files are +metadata-only Parquet files. Note this is not a Parquet standard, but a +convention set in practice by those frameworks. + +Using those files can give a more efficient creation of a parquet Dataset, +since it can use the stored schema and file paths of all row groups, +instead of inferring the schema and crawling the directories for all Parquet +files (this is especially the case for filesystems where accessing files +is expensive). + +The :func:`~pyarrow.parquet.write_to_dataset` function does not automatically +write such metadata files, but you can use it to gather the metadata and +combine and write them manually: + +.. code-block:: python + + >>> # Write a dataset and collect metadata information of all written files + >>> metadata_collector = [] + >>> root_path = "dataset_name_1" + >>> pq.write_to_dataset(table, root_path, metadata_collector=metadata_collector) + + >>> # Write the ``_common_metadata`` parquet file without row groups statistics + >>> pq.write_metadata(table.schema, root_path + '/_common_metadata') + + >>> # Write the ``_metadata`` parquet file with row groups statistics of all files + >>> pq.write_metadata( + ... table.schema, root_path + '/_metadata', + ... metadata_collector=metadata_collector + ... ) + +When not using the :func:`~pyarrow.parquet.write_to_dataset` function, but +writing the individual files of the partitioned dataset using +:func:`~pyarrow.parquet.write_table` or :class:`~pyarrow.parquet.ParquetWriter`, +the ``metadata_collector`` keyword can also be used to collect the FileMetaData +of the written files. In this case, you need to ensure to set the file path +contained in the row group metadata yourself before combining the metadata, and +the schemas of all different files and collected FileMetaData objects should be +the same: + +.. code-block:: python + + >>> import os + >>> os.mkdir("year=2017") + + >>> metadata_collector = [] + >>> pq.write_table( + ... table, "year=2017/data1.parquet", + ... metadata_collector=metadata_collector + ... ) + + >>> # set the file path relative to the root of the partitioned dataset + >>> metadata_collector[-1].set_file_path("year=2017/data1.parquet") + + >>> # combine and write the metadata + >>> metadata = metadata_collector[0] + >>> for _meta in metadata_collector[1:]: + ... metadata.append_row_groups(_meta) + >>> metadata.write_metadata_file("_metadata") + + >>> # or use pq.write_metadata to combine and write in a single step + >>> pq.write_metadata( + ... table.schema, "_metadata", + ... metadata_collector=metadata_collector + ... ) + + >>> pq.read_metadata("_metadata") + + created_by: parquet-cpp-arrow version ... + num_columns: 3 + num_rows: 3 + num_row_groups: 1 + format_version: 2.6 + serialized_size: ... + +Reading from Partitioned Datasets +------------------------------------------------ + +The :class:`~.ParquetDataset` class accepts either a directory name or a list +of file paths, and can discover and infer some common partition structures, +such as those produced by Hive: + +.. code-block:: python + + >>> dataset = pq.ParquetDataset('dataset_name/') + >>> table = dataset.read() + >>> table + pyarrow.Table + three: bool + one: dictionary + two: dictionary + ---- + three: [[true],[true],[false]] + one: [ -- dictionary: + ["-1","2.5"] -- indices: + [0], -- dictionary: + ["-1","2.5"] -- indices: + [1], -- dictionary: + [null] -- indices: + [0]] + two: [ -- dictionary: + ["foo","baz","bar"] -- indices: + [0], -- dictionary: + ["foo","baz","bar"] -- indices: + [1], -- dictionary: + ["foo","baz","bar"] -- indices: + [2]] + +You can also use the convenience function ``read_table`` exposed by +``pyarrow.parquet`` that avoids the need for an additional Dataset object +creation step. + +.. code-block:: python + + >>> table = pq.read_table('dataset_name') + +Note: the partition columns in the original table will have their types +converted to Arrow dictionary types (pandas categorical) on load. Ordering of +partition columns is not preserved through the save/load process. If reading +from a remote filesystem into a pandas dataframe you may need to run +``sort_index`` to maintain row ordering (as long as the ``preserve_index`` +option was enabled on write). + +Other features: + +- Filtering on all columns (using row group statistics) instead of only on + the partition keys. +- Fine-grained partitioning: support for a directory partitioning scheme + in addition to the Hive-like partitioning (e.g. "/2019/11/15/" instead of + "/year=2019/month=11/day=15/"), and the ability to specify a schema for + the partition keys. + +Note: + +- The partition keys need to be explicitly included in the ``columns`` + keyword when you want to include them in the result while reading a + subset of the columns diff --git a/docs/source/python/parquet/parquet_encryption.rst b/docs/source/python/parquet/parquet_encryption.rst new file mode 100644 index 000000000000..4541baa099f3 --- /dev/null +++ b/docs/source/python/parquet/parquet_encryption.rst @@ -0,0 +1,233 @@ +.. 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. + +.. currentmodule:: pyarrow +.. _parquet_encryption: + + +Parquet Modular Encryption (Columnar Encryption) +------------------------------------------------ + +Columnar encryption is supported for Parquet files in C++ starting from +Apache Arrow 4.0.0 and in PyArrow starting from Apache Arrow 6.0.0. + +Parquet uses the envelope encryption practice, where file parts are encrypted +with "data encryption keys" (DEKs), and the DEKs are encrypted with "master +encryption keys" (MEKs). The DEKs are randomly generated by Parquet for each +encrypted file/column. The MEKs are generated, stored and managed in a Key +Management Service (KMS) of user’s choice. + +Reading and writing encrypted Parquet files involves passing file encryption +and decryption properties to :class:`~pyarrow.parquet.ParquetWriter` and to +:class:`~.ParquetFile`, respectively. + +Writing an encrypted Parquet file: + +.. code-block:: python + + >>> encryption_properties = crypto_factory.file_encryption_properties( # doctest: +SKIP + ... kms_connection_config, encryption_config) + >>> with pq.ParquetWriter(filename, schema, # doctest: +SKIP + ... encryption_properties=encryption_properties) as writer: + ... writer.write_table(table) + +Reading an encrypted Parquet file: + +.. code-block:: python + + >>> decryption_properties = crypto_factory.file_decryption_properties( # doctest: +SKIP + ... kms_connection_config) + >>> parquet_file = pq.ParquetFile(filename, # doctest: +SKIP + ... decryption_properties=decryption_properties) + + +In order to create the encryption and decryption properties, a +:class:`pyarrow.parquet.encryption.CryptoFactory` should be created and +initialized with KMS Client details, as described below. + + +KMS Client +~~~~~~~~~~ + +The master encryption keys should be kept and managed in a production-grade +Key Management System (KMS), deployed in the user's organization. Using Parquet +encryption requires implementation of a client class for the KMS server. +Any KmsClient implementation should implement the informal interface +defined by :class:`pyarrow.parquet.encryption.KmsClient` as following: + +.. code-block:: python + + >>> import pyarrow.parquet.encryption as pe + >>> class MyKmsClient(pe.KmsClient): + ... + ... """An example KmsClient implementation skeleton""" + ... def __init__(self, kms_connection_configuration): + ... pe.KmsClient.__init__(self) + ... # Any KMS-specific initialization based on + ... # kms_connection_configuration comes here + ... + ... def wrap_key(self, key_bytes, master_key_identifier): + ... wrapped_key = ... # call KMS to wrap key_bytes with key specified by + ... # master_key_identifier + ... return wrapped_key + ... + ... def unwrap_key(self, wrapped_key, master_key_identifier): + ... key_bytes = ... # call KMS to unwrap wrapped_key with key specified by + ... # master_key_identifier + ... return key_bytes + +The concrete implementation will be loaded at runtime by a factory function +provided by the user. This factory function will be used to initialize the +:class:`pyarrow.parquet.encryption.CryptoFactory` for creating file encryption +and decryption properties. + +For example, in order to use the ``MyKmsClient`` defined above: + +.. code-block:: python + + >>> def kms_client_factory(kms_connection_configuration): + ... return MyKmsClient(kms_connection_configuration) + + >>> crypto_factory = pe.CryptoFactory(kms_client_factory) + +An :download:`example <../../../python/examples/parquet_encryption/sample_vault_kms_client.py>` +of such a class for an open source +`KMS `_ can be found in the Apache +Arrow GitHub repository. The production KMS client should be designed in +cooperation with an organization's security administrators, and built by +developers with experience in access control management. Once such a class is +created, it can be passed to applications via a factory method and leveraged +by general PyArrow users as shown in the encrypted parquet write/read sample +above. + +KMS connection configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Configuration of connection to KMS (:class:`pyarrow.parquet.encryption.KmsConnectionConfig` +used when creating file encryption and decryption properties) includes the +following options: + +* ``kms_instance_url``, URL of the KMS instance. +* ``kms_instance_id``, ID of the KMS instance that will be used for encryption + (if multiple KMS instances are available). +* ``key_access_token``, authorization token that will be passed to KMS. +* ``custom_kms_conf``, a string dictionary with KMS-type-specific configuration. + +Encryption configuration +~~~~~~~~~~~~~~~~~~~~~~~~ + +:class:`pyarrow.parquet.encryption.EncryptionConfiguration` (used when +creating file encryption properties) includes the following options: + +* ``footer_key``, the ID of the master key for footer encryption/signing. +* ``column_keys``, which columns to encrypt with which key. Dictionary with + master key IDs as the keys, and column name lists as the values, + e.g. ``{key1: [col1, col2], key2: [col3]}``. See notes on nested fields below. +* ``encryption_algorithm``, the Parquet encryption algorithm. + Can be ``AES_GCM_V1`` (default) or ``AES_GCM_CTR_V1``. +* ``plaintext_footer``, whether to write the file footer in plain text (otherwise it is encrypted). +* ``double_wrapping``, whether to use double wrapping - where data encryption keys (DEKs) + are encrypted with key encryption keys (KEKs), which in turn are encrypted + with master encryption keys (MEKs). If set to ``false``, single wrapping is + used - where DEKs are encrypted directly with MEKs. +* ``cache_lifetime``, the lifetime of cached entities (key encryption keys, + local wrapping keys, KMS client objects) represented as a ``datetime.timedelta``. +* ``internal_key_material``, whether to store key material inside Parquet file footers; + this mode doesn’t produce additional files. If set to ``false``, key material is + stored in separate files in the same folder, which enables key rotation for + immutable Parquet files. +* ``data_key_length_bits``, the length of data encryption keys (DEKs), randomly + generated by Parquet key management tools. Can be 128, 192 or 256 bits. + +.. note:: + When ``double_wrapping`` is true, Parquet implements a "double envelope + encryption" mode that minimizes the interaction of the program with a KMS + server. In this mode, the DEKs are encrypted with "key encryption keys" + (KEKs, randomly generated by Parquet). The KEKs are encrypted with "master + encryption keys" (MEKs) in the KMS; the result and the KEK itself are + cached in the process memory. + +An example encryption configuration: + +.. code-block:: python + + >>> encryption_config = pe.EncryptionConfiguration( + ... footer_key="footer_key_name", + ... column_keys={ + ... "column_key_name": ["Column1", "Column2"], + ... }, + ... ) + +.. note:: + + Columns with nested fields (struct or map data types) can be encrypted as a whole, or only + individual fields. Configure an encryption key for the root column name to encrypt all nested + fields with this key, or configure a key for individual leaf nested fields. + + Conventionally, the key and value fields of a map column ``m`` have the names + ``m.key_value.key`` and ``m.key_value.value``, respectively. + An inner field ``f`` of a struct column ``s`` has the name ``s.f``. + + With above example, *all* inner fields are encrypted with the same key by configuring that key + for column ``m`` and ``s``, respectively. + +An example encryption configuration for columns with nested fields, where +all columns are encrypted with the same key identified by ``column_key_id``: + +.. code-block:: python + + >>> import pyarrow.parquet.encryption as pe + + schema = pa.schema([ + ("MapColumn", pa.map_(pa.string(), pa.int32())), + ("StructColumn", pa.struct([("f1", pa.int32()), ("f2", pa.string())])), + ]) + + encryption_config = pe.EncryptionConfiguration( + footer_key="footer_key_name", + column_keys={ + "column_key_id": [ "MapColumn", "StructColumn" ], + }, + ) + +An example encryption configuration for columns with nested fields, where +some inner fields are encrypted with the same key identified by ``column_key_id``: + +.. code-block:: python + + >>> import pyarrow.parquet.encryption as pe + + >>> schema = pa.schema([ + ... ("MapColumn", pa.map_(pa.string(), pa.int32())), + ... ("StructColumn", pa.struct([("f1", pa.int32()), ("f2", pa.string())])), + ... ]) + + >>> encryption_config = pe.EncryptionConfiguration( + ... footer_key="footer_key_name", + ... column_keys={ + ... "column_key_id": [ "MapColumn.key_value.value", "StructColumn.f1" ], + ... }, + ... ) + +Decryption configuration +~~~~~~~~~~~~~~~~~~~~~~~~ + +:class:`pyarrow.parquet.encryption.DecryptionConfiguration` (used when creating +file decryption properties) is optional and it includes the following options: + +* ``cache_lifetime``, the lifetime of cached entities (key encryption keys, local + wrapping keys, KMS client objects) represented as a ``datetime.timedelta``. diff --git a/docs/source/python/parquet/parquet_type_handling.rst b/docs/source/python/parquet/parquet_type_handling.rst new file mode 100644 index 000000000000..09ed13f81ae5 --- /dev/null +++ b/docs/source/python/parquet/parquet_type_handling.rst @@ -0,0 +1,95 @@ +.. 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. + +.. currentmodule:: pyarrow +.. _parquet_type_handling: + + +Data Type Handling +================== + +Reading types as DictionaryArray +-------------------------------- + +The ``read_dictionary`` option in ``read_table`` and ``ParquetDataset`` will +cause columns to be read as ``DictionaryArray``, which will become +``pandas.Categorical`` when converted to pandas. This option is only valid for +string and binary column types, and it can yield significantly lower memory use +and improved performance for columns with many repeated string values. + +.. code-block:: python + + >>> pq.read_table('example.parquet', read_dictionary=['two']) + pyarrow.Table + one: double + two: dictionary + three: bool + __index_level_0__: large_string + ---- + one: [[-1,null,2.5]] + two: [ -- dictionary: + ["foo","bar","baz"] -- indices: + [0,1,2]] + three: [[true,false,true]] + __index_level_0__: [["a","b","c"]] + +Storing timestamps +------------------ + +Some Parquet readers may only support timestamps stored in millisecond +(``'ms'``) or microsecond (``'us'``) resolution. Since pandas uses nanoseconds +to represent timestamps, this can occasionally be a nuisance. By default +(when writing version 1.0 Parquet files), the nanoseconds will be cast to +microseconds ('us'). + +In addition, We provide the ``coerce_timestamps`` option to allow you to select +the desired resolution: + +.. code-block:: python + + >>> pq.write_table(table, 'example.parquet', coerce_timestamps='ms') + +If a cast to a lower resolution value may result in a loss of data, by default +an exception will be raised. This can be suppressed by passing +``allow_truncated_timestamps=True``: + +.. code-block:: python + + >>> pq.write_table(table, 'example.parquet', coerce_timestamps='ms', + ... allow_truncated_timestamps=True) + +Timestamps with nanoseconds can be stored without casting when using the +more recent Parquet format version 2.6: + +.. code-block:: python + + >>> pq.write_table(table, 'example.parquet', version='2.6') + +However, many Parquet readers do not yet support this newer format version, and +therefore the default is to write version 1.0 files. When compatibility across +different processing frameworks is required, it is recommended to use the +default version 1.0. + +Older Parquet implementations use ``INT96`` based storage of +timestamps, but this is now deprecated. This includes some older +versions of Apache Impala and Apache Spark. To write timestamps in +this format, set the ``use_deprecated_int96_timestamps`` option to +``True`` in ``write_table``. + +.. code-block:: python + + >>> pq.write_table(table, 'example.parquet', use_deprecated_int96_timestamps=True) \ No newline at end of file From 0baa52b0c3e3c9a423a2a5761c833f9faabd4279 Mon Sep 17 00:00:00 2001 From: AlenkaF Date: Tue, 23 Jun 2026 12:53:13 +0200 Subject: [PATCH 2/8] Add filters and bloom filters section to parquet.rst --- docs/source/python/parquet/parquet.rst | 119 ++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 4 deletions(-) diff --git a/docs/source/python/parquet/parquet.rst b/docs/source/python/parquet/parquet.rst index e31edff2389e..08b8743c0d22 100644 --- a/docs/source/python/parquet/parquet.rst +++ b/docs/source/python/parquet/parquet.rst @@ -224,6 +224,8 @@ We can similarly write a Parquet file with multiple row groups by using >>> pf2.num_row_groups 3 +.. _inspecting_parquet_file_metadata: + Inspecting the Parquet File Metadata ------------------------------------ @@ -300,6 +302,119 @@ such as the row groups and column chunk metadata and statistics: bloom_filter_offset: None bloom_filter_length: None +.. _parquet_filtering: + +Filtering / Predicate Pushdown +------------------------------ + +The ``filters`` parameter of :func:`~pyarrow.parquet.read_table` pushes +predicates into the reader, skipping entire row groups based on their +column statistics (min/max values stored in the row group metadata). +For large files with many row groups this can dramatically reduce the +data read. + +Predicates can be expressed as a :class:`~pyarrow.compute.Expression`: + +.. code-block:: python + + >>> import pyarrow.compute as pc + >>> pq.read_table('example.parquet', filters=pc.field('three') == True) + pyarrow.Table + one: double + two: large_string + three: bool + __index_level_0__: large_string + ---- + one: [[-1,2.5]] + two: [["foo","baz"]] + three: [[true,true]] + __index_level_0__: [["a","c"]] + +Or in disjunctive normal form (DNF) as a list of ``(column, operator, +value)`` tuples: + +.. code-block:: python + + >>> pq.read_table('example.parquet', filters=[('three', '==', True)]) + pyarrow.Table + one: double + two: large_string + three: bool + __index_level_0__: large_string + ---- + one: [[-1,2.5]] + two: [["foo","baz"]] + three: [[true,true]] + __index_level_0__: [["a","c"]] + +Supported operators are ``=`` / ``==``, ``!=``, ``<``, ``>``, ``<=``, +``>=``, ``in``, and ``not in``. Multiple predicates in the inner list are +combined as AND; the outer list combines predicate groups as OR: + +.. code-block:: python + + >>> filters = [ + ... [('one', '>', 0), ('three', '==', True)], + ... [('two', 'in', ['foo', 'bar'])], + ... ] + >>> pq.read_table('example.parquet', filters=filters) + pyarrow.Table + one: double + two: large_string + three: bool + __index_level_0__: large_string + ---- + one: [[-1,null,2.5]] + two: [["foo","bar","baz"]] + three: [[true,false,true]] + __index_level_0__: [["a","b","c"]] + +Row group skipping based on column statistics is automatic when filters are +applied. + +.. _parquet_bloom_filters: + +Bloom Filters +------------- + +Bloom filters are a probabilistic data structure stored per column per row +group that can answer, for a given value, either that value is +“definitely not present” or “probably present”. This makes them useful for +readers that support Bloom filter-based row group skipping. + +The probability of false positives is configurable. + +.. note:: + + PyArrow reader does not currently use Bloom filters during + filtering on read. + +Bloom filters are enabled per column via the ``bloom_filter_options`` parameter: + +.. code-block:: python + + >>> pq.write_table(table, 'bloom.parquet', + ... bloom_filter_options={'two': True}) + +Passing ``True`` uses the defaults: ``ndv=1048576`` (number of distinct values) +and ``fpp=0.05`` (false-positive probability). + +Both can also be configured explicitly via a dictionary: + +.. code-block:: python + + >>> pq.write_table(table, 'bloom.parquet', + ... bloom_filter_options={ + ... 'two': {'ndv': 100, 'fpp': 0.01}, + ... }) + +Recommnded value for ``ndv`` is the number of rows. Lower values of ``fpp`` +reduce false positives but require more space (space grows roughly proportional +to ``log(1/FPP)``). Recommended values are 0.1, 0.05, or 0.01. + +Whether a Bloom filter was written can be confirmed with observing column chunk +metadata, see: :ref:`inspecting_parquet_file_metadata`. + Multithreaded Reads ------------------- @@ -315,7 +430,6 @@ This can be disabled by specifying ``use_threads=False``. The number of threads to use concurrently is automatically inferred by Arrow and can be inspected using the :func:`~pyarrow.cpu_count()` function. - Compression, Encoding, and File Compatibility --------------------------------------------- @@ -355,7 +469,6 @@ These settings can also be set on a per-column basis: >>> pq.write_table(table, 'example.parquet', compression={'one': 'snappy', 'two': 'gzip'}, ... use_dictionary=['one', 'two']) - Using with Spark ---------------- @@ -363,8 +476,6 @@ Spark places some constraints on the types of Parquet files it will read. The option ``flavor='spark'`` will set these options automatically and also sanitize field characters unsupported by Spark SQL. - - Reading from cloud storage -------------------------- From 5542e8415c1dc86c685ce0be15570941f264e908 Mon Sep 17 00:00:00 2001 From: AlenkaF Date: Tue, 23 Jun 2026 16:26:28 +0200 Subject: [PATCH 3/8] Add content about iter_batches --- docs/source/python/parquet/parquet.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/source/python/parquet/parquet.rst b/docs/source/python/parquet/parquet.rst index 08b8743c0d22..87ce1459dcaa 100644 --- a/docs/source/python/parquet/parquet.rst +++ b/docs/source/python/parquet/parquet.rst @@ -224,6 +224,22 @@ We can similarly write a Parquet file with multiple row groups by using >>> pf2.num_row_groups 3 +For memory-efficient reads of large files, :meth:`~.ParquetFile.iter_batches` +streams the file as a sequence of :class:`~pyarrow.RecordBatch` objects rather +than loading the entire file into a single table: + +.. code-block:: python + + >>> parquet_file = pq.ParquetFile('example.parquet') + >>> for batch in parquet_file.iter_batches(batch_size=2): + ... print(batch.num_rows) + 2 + 1 + +The ``batch_size`` parameter controls the maximum number of rows per +batch. The ``row_groups`` and ``columns`` parameters allow limiting +which row groups and columns are read. + .. _inspecting_parquet_file_metadata: Inspecting the Parquet File Metadata From 8da1e4ecc5ac6586a4978447914b622250325458 Mon Sep 17 00:00:00 2001 From: AlenkaF Date: Wed, 24 Jun 2026 12:32:05 +0200 Subject: [PATCH 4/8] Add binary/list_type and extension type support sections --- .../python/parquet/parquet_type_handling.rst | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/docs/source/python/parquet/parquet_type_handling.rst b/docs/source/python/parquet/parquet_type_handling.rst index 09ed13f81ae5..adb5807ec2b2 100644 --- a/docs/source/python/parquet/parquet_type_handling.rst +++ b/docs/source/python/parquet/parquet_type_handling.rst @@ -33,19 +33,79 @@ and improved performance for columns with many repeated string values. .. code-block:: python + >>> import pyarrow as pa + >>> import pyarrow.parquet as pq + + >>> table = pa.table({'one': [-1, None, 2.5], + ... 'two': ['foo', 'bar', 'baz'], + ... 'three': [True, False, True]}) + ... + >>> pq.write_table(table, 'example.parquet') + >>> pq.read_table('example.parquet', read_dictionary=['two']) pyarrow.Table one: double two: dictionary three: bool - __index_level_0__: large_string ---- one: [[-1,null,2.5]] two: [ -- dictionary: ["foo","bar","baz"] -- indices: [0,1,2]] three: [[true,false,true]] - __index_level_0__: [["a","b","c"]] + +Reading binary and list columns +------------------------------- + +By default, Parquet ``BYTE_ARRAY`` columns are read as Arrow ``binary`` type +and Parquet ``LIST`` columns are read as Arrow ``list`` type, both of which use +32-bit offsets. For very large datasets this can overflow. The ``binary_type`` +and ``list_type`` parameters let you choose a different Arrow type on read. + +``binary_type`` accepts ``pa.binary()`` (default), ``pa.large_binary()``, or +``pa.binary_view()``: + +.. code-block:: python + + >>> import pyarrow as pa + >>> table = pa.table({'data': pa.array([b'hello', b'world'], pa.binary())}) + >>> pq.write_table(table, 'binary.parquet', store_schema=False) + >>> pq.read_table('binary.parquet', binary_type=pa.large_binary()).schema + data: large_binary + +``list_type`` accepts ``pa.ListType`` or ``pa.LargeListType``: + +.. code-block:: python + + >>> table = pa.table({'lists': pa.array([[1, 2], [3]], pa.list_(pa.int32()))}) + >>> pq.write_table(table, 'lists.parquet', store_schema=False) + >>> pq.read_table('lists.parquet', list_type=pa.LargeListType).schema + lists: large_list + child 0, element: int32 + +.. note:: + Both settings are ignored when a serialized Arrow schema is present in the + Parquet file metadata (i.e. when the parquet file was written with + ``store_schema=True``). + +Read in Arrow Extension Types +----------------------------- + +Certain Parquet logical types (JSON, UUID, Geometry, Geography) are supported +and read as Arrow extension types by default (``arrow.json``, ``arrow.uuid`` +and ``geoarrow.wkb`` respectively). This support is enabled via the +``arrow_extensions_enabled`` parameter and is used in :func:`~pyarrow.parquet.read_table`, +:class:`~pyarrow.parquet.ParquetFile`, and :class:`~pyarrow.parquet.ParquetDataset`. + +To read these Parquet logical types as storage types (default behavior +until PyArrow version ``21.0.0``), set ``arrow_extensions_enabled=False``. + +.. note:: + Reading GEOMETRY/GEOGRAPHY columns as ``geoarrow.wkb`` additionally + requires the ``geoarrow.wkb`` extension type to be registered. For that + you can install Python bindings for GeoArrow + (`geoarrow-pyarrow `_) + and import ``geoarrow.pyarrow`` module. Storing timestamps ------------------ @@ -92,4 +152,4 @@ this format, set the ``use_deprecated_int96_timestamps`` option to .. code-block:: python - >>> pq.write_table(table, 'example.parquet', use_deprecated_int96_timestamps=True) \ No newline at end of file + >>> pq.write_table(table, 'example.parquet', use_deprecated_int96_timestamps=True) From d0f4c4a0e8cf3d8793f887b1b0be43657e5bc58f Mon Sep 17 00:00:00 2001 From: AlenkaF Date: Wed, 24 Jun 2026 15:29:50 +0200 Subject: [PATCH 5/8] Add note about partitions in single file reads --- .../python/parquet/parquet_datasets.rst | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/source/python/parquet/parquet_datasets.rst b/docs/source/python/parquet/parquet_datasets.rst index 10abfa4242cc..f7833fe0cb66 100644 --- a/docs/source/python/parquet/parquet_datasets.rst +++ b/docs/source/python/parquet/parquet_datasets.rst @@ -58,6 +58,14 @@ added is to use the local filesystem. .. code-block:: python + >>> import pyarrow as pa + >>> import pyarrow.parquet as pq + + >>> table = pa.table({'one': [-1, None, 2.5], + ... 'two': ['foo', 'bar', 'baz'], + ... 'three': [True, False, True]}) + ... + >>> # Local dataset write >>> pq.write_to_dataset(table, root_path='dataset_name', ... partition_cols=['one', 'two']) @@ -166,7 +174,7 @@ the same: serialized_size: ... Reading from Partitioned Datasets ------------------------------------------------- +--------------------------------- The :class:`~.ParquetDataset` class accepts either a directory name or a list of file paths, and can discover and infer some common partition structures, @@ -222,8 +230,22 @@ Other features: "/year=2019/month=11/day=15/"), and the ability to specify a schema for the partition keys. -Note: +.. note:: + The partition keys need to be explicitly included in the ``columns`` + keyword when you want to include them in the result while reading a + subset of the columns. + +.. note:: + + When passing a single file path to :func:`~pyarrow.parquet.read_table` + or :class:`~pyarrow.parquet.ParquetDataset`, partition columns are not + inferred from the file path, even if the path contains Hive-like segments. + To get partition columns, pass the parent directory instead: + + .. code-block:: python + + >>> # Doesn't include 'year' as a column + >>> pq.read_table('dataset_name/year=2017/data1.parquet') # doctest: +SKIP -- The partition keys need to be explicitly included in the ``columns`` - keyword when you want to include them in the result while reading a - subset of the columns + >>> # Includes 'year' as a partition column + >>> pq.read_table('dataset_name/') # doctest: +SKIP From 32c29e02a78d1e56a5145fe4e2e56eeaffdccab9 Mon Sep 17 00:00:00 2001 From: AlenkaF Date: Fri, 26 Jun 2026 09:18:42 +0200 Subject: [PATCH 6/8] Update writer options list --- docs/source/python/parquet/parquet.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/source/python/parquet/parquet.rst b/docs/source/python/parquet/parquet.rst index 87ce1459dcaa..28329d1ec9a6 100644 --- a/docs/source/python/parquet/parquet.rst +++ b/docs/source/python/parquet/parquet.rst @@ -126,8 +126,28 @@ control various settings when writing a Parquet file. enable more Parquet types and encodings. * ``data_page_size``, to control the approximate size of encoded data pages within a column chunk. This currently defaults to 1MB. +* ``max_rows_per_page``, to cap the number of rows per data page within + a column chunk (default 20000). Smaller values reduce memory usage + during reads at the cost of more page metadata. * ``flavor``, to set compatibility options particular to a Parquet consumer like ``'spark'`` for Apache Spark. +* ``store_decimal_as_integer``, to store decimals with precision 1–18 + as ``int32`` or ``int64`` instead of ``fixed_len_byte_array``. + This produces more compact files but may not be supported by all readers. +* ``write_time_adjusted_to_utc``, to mark ``TIME`` columns as + adjusted to UTC (``isAdjustedToUTC=True``). When ``False`` (the default), + the time is treated as local/unknown timezone. +* ``write_page_index``, to write statistics to the page index + instead of writing it to each data page header. + Note that PyArrow does not yet use the page index on the read side. +* ``write_page_checksum``, to write a page checksum. Use with + ``page_checksum_verification=True`` on read to detect data corruption. +* ``sorting_columns``, to record the sort order of the data in each + row group's metadata. The writer does not sort the data nor does it verify + that the data is sorted. Readers can use this metadata to optimize queries. + + Sort order is expressed as a sequence of :class:`~pyarrow.parquet.SortingColumn` + objects. See the :func:`~pyarrow.parquet.write_table()` docstring for more details. From f38e1cad230fa834f125153a9bb264275c552298 Mon Sep 17 00:00:00 2001 From: AlenkaF Date: Wed, 8 Jul 2026 15:26:59 +0200 Subject: [PATCH 7/8] Update encryption sections --- docs/source/python/parquet/parquet.rst | 2 +- .../python/parquet/parquet_encryption.rst | 53 +++++++++++++++++-- .../python/parquet/parquet_type_handling.rst | 3 +- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/docs/source/python/parquet/parquet.rst b/docs/source/python/parquet/parquet.rst index 28329d1ec9a6..db9778684068 100644 --- a/docs/source/python/parquet/parquet.rst +++ b/docs/source/python/parquet/parquet.rst @@ -444,7 +444,7 @@ Both can also be configured explicitly via a dictionary: ... 'two': {'ndv': 100, 'fpp': 0.01}, ... }) -Recommnded value for ``ndv`` is the number of rows. Lower values of ``fpp`` +Recommended value for ``ndv`` is the number of rows. Lower values of ``fpp`` reduce false positives but require more space (space grows roughly proportional to ``log(1/FPP)``). Recommended values are 0.1, 0.05, or 0.01. diff --git a/docs/source/python/parquet/parquet_encryption.rst b/docs/source/python/parquet/parquet_encryption.rst index 4541baa099f3..7e9ad781683b 100644 --- a/docs/source/python/parquet/parquet_encryption.rst +++ b/docs/source/python/parquet/parquet_encryption.rst @@ -137,6 +137,9 @@ creating file encryption properties) includes the following options: * ``column_keys``, which columns to encrypt with which key. Dictionary with master key IDs as the keys, and column name lists as the values, e.g. ``{key1: [col1, col2], key2: [col3]}``. See notes on nested fields below. +* ``uniform_encryption``, whether to encrypt the footer and all columns with + the same ``footer_key``, instead of specifying ``column_keys`` + individually. Cannot be used together with ``column_keys``. * ``encryption_algorithm``, the Parquet encryption algorithm. Can be ``AES_GCM_V1`` (default) or ``AES_GCM_CTR_V1``. * ``plaintext_footer``, whether to write the file footer in plain text (otherwise it is encrypted). @@ -190,8 +193,6 @@ all columns are encrypted with the same key identified by ``column_key_id``: .. code-block:: python - >>> import pyarrow.parquet.encryption as pe - schema = pa.schema([ ("MapColumn", pa.map_(pa.string(), pa.int32())), ("StructColumn", pa.struct([("f1", pa.int32()), ("f2", pa.string())])), @@ -209,8 +210,6 @@ some inner fields are encrypted with the same key identified by ``column_key_id` .. code-block:: python - >>> import pyarrow.parquet.encryption as pe - >>> schema = pa.schema([ ... ("MapColumn", pa.map_(pa.string(), pa.int32())), ... ("StructColumn", pa.struct([("f1", pa.int32()), ("f2", pa.string())])), @@ -231,3 +230,49 @@ file decryption properties) is optional and it includes the following options: * ``cache_lifetime``, the lifetime of cached entities (key encryption keys, local wrapping keys, KMS client objects) represented as a ``datetime.timedelta``. + +External key material and key rotation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When ``internal_key_material=False`` is set on ``EncryptionConfiguration``, +key material is stored in a separate file next to the Parquet file instead of +in its footer. + +Storing key material externally is what enables key rotation: +``crypto_factory.rotate_master_keys()`` re-wraps the data encryption keys of +a file that uses external key material under new master keys, without rewriting +the file itself: + +.. code-block:: python + + >>> crypto_factory.rotate_master_keys( # doctest: +SKIP + ... kms_connection_config, parquet_file_path="table.parquet", + ... ) + +Direct Key Encryption (without KMS) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:func:`pyarrow.parquet.encryption.create_encryption_properties` and +:func:`pyarrow.parquet.encryption.create_decryption_properties` build +encryption/decryption properties directly from a plaintext key, bypassing +:class:`~pyarrow.parquet.encryption.CryptoFactory` and the KMS-based flow. + +.. note:: + Only uniform encryption (a single key for the footer and all columns) is + supported by these functions. For per-column keys, use the + ``CryptoFactory``/``EncryptionConfiguration`` flow described above. + +.. code-block:: python + + >>> props = pe.create_encryption_properties( # doctest: +SKIP + ... footer_key=b'0123456789abcdef', + ... aad_prefix=b'table_id', + ... store_aad_prefix=False, + ... ) + >>> pq.write_table(table, 'encrypted.parquet', encryption_properties=props) # doctest: +SKIP + + >>> decryption_props = pe.create_decryption_properties( # doctest: +SKIP + ... footer_key=b'0123456789abcdef', + ... aad_prefix=b'table_id', + ... ) + >>> pq.read_table('encrypted.parquet', decryption_properties=decryption_props) # doctest: +SKIP diff --git a/docs/source/python/parquet/parquet_type_handling.rst b/docs/source/python/parquet/parquet_type_handling.rst index adb5807ec2b2..de36c2cca714 100644 --- a/docs/source/python/parquet/parquet_type_handling.rst +++ b/docs/source/python/parquet/parquet_type_handling.rst @@ -95,7 +95,8 @@ Certain Parquet logical types (JSON, UUID, Geometry, Geography) are supported and read as Arrow extension types by default (``arrow.json``, ``arrow.uuid`` and ``geoarrow.wkb`` respectively). This support is enabled via the ``arrow_extensions_enabled`` parameter and is used in :func:`~pyarrow.parquet.read_table`, -:class:`~pyarrow.parquet.ParquetFile`, and :class:`~pyarrow.parquet.ParquetDataset`. +:class:`~pyarrow.parquet.ParquetFile`, :class:`~pyarrow.parquet.ParquetDataset`, and +:func:`~pyarrow.parquet.read_schema`. To read these Parquet logical types as storage types (default behavior until PyArrow version ``21.0.0``), set ``arrow_extensions_enabled=False``. From 0e9118c07b994d6e54d43e76798b973f88c53692 Mon Sep 17 00:00:00 2001 From: AlenkaF Date: Wed, 8 Jul 2026 15:54:00 +0200 Subject: [PATCH 8/8] Minor change to bloom filters title --- docs/source/python/parquet/parquet.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/python/parquet/parquet.rst b/docs/source/python/parquet/parquet.rst index db9778684068..d3c0ab294479 100644 --- a/docs/source/python/parquet/parquet.rst +++ b/docs/source/python/parquet/parquet.rst @@ -410,8 +410,8 @@ applied. .. _parquet_bloom_filters: -Bloom Filters -------------- +Writing Bloom Filters +--------------------- Bloom filters are a probabilistic data structure stored per column per row group that can answer, for a given value, either that value is