From 54c19938d1b2dcff5bdcdcca673f353f9a9d9610 Mon Sep 17 00:00:00 2001 From: Dan King Date: Tue, 29 Oct 2024 10:44:25 -0400 Subject: [PATCH] feat: expanded Python docs + Rust docs (#1137) --- .../workflows/{python-docs.yml => docs.yml} | 3 +- docs/Makefile | 6 + docs/_static/example.parquet | Bin 0 -> 35631 bytes docs/_static/example.vortex | Bin 0 -> 76904 bytes docs/_static/file-format-2024-10-23-1642.svg | 10 + docs/_static/style.css | 3 + docs/_static/vortex_spiral_logo.svg | 2 + .../_static/vortex_spiral_logo_dark_theme.svg | 2 + docs/{ => api}/dataset.rst | 10 + docs/api/dtype.rst | 27 ++ docs/api/encoding.rst | 26 ++ docs/api/expr.rst | 26 ++ docs/api/index.rst | 12 + docs/api/io.rst | 20 ++ docs/api/scalar.rst | 25 ++ docs/conf.py | 48 +++- docs/dtype.rst | 7 - docs/encoding.rst | 7 - docs/expr.rst | 6 - docs/file_format.rst | 79 ++++++ docs/guide.rst | 173 ++++++++++++ docs/index.rst | 72 ++++- docs/io.rst | 6 - docs/pyproject.toml | 7 +- docs/quickstart.rst | 199 ++++++++++++++ docs/scalar.rst | 6 - pyvortex/pyproject.toml | 1 + pyvortex/python/vortex/__init__.py | 1 + pyvortex/python/vortex/dataset.py | 247 +++++++++++++++++- pyvortex/python/vortex/encoding.py | 97 +++++-- pyvortex/src/array.rs | 56 ++-- pyvortex/src/compress.rs | 14 +- pyvortex/src/dtype.rs | 6 +- pyvortex/src/expr.rs | 10 +- pyvortex/src/io.rs | 28 +- pyvortex/src/scalar.rs | 6 +- requirements-dev.lock | 5 +- requirements.lock | 5 +- uv.lock | 16 +- 39 files changed, 1144 insertions(+), 130 deletions(-) rename .github/workflows/{python-docs.yml => docs.yml} (92%) create mode 100644 docs/_static/example.parquet create mode 100644 docs/_static/example.vortex create mode 100644 docs/_static/file-format-2024-10-23-1642.svg create mode 100644 docs/_static/style.css create mode 100644 docs/_static/vortex_spiral_logo.svg create mode 100644 docs/_static/vortex_spiral_logo_dark_theme.svg rename docs/{ => api}/dataset.rst (77%) create mode 100644 docs/api/dtype.rst create mode 100644 docs/api/encoding.rst create mode 100644 docs/api/expr.rst create mode 100644 docs/api/index.rst create mode 100644 docs/api/io.rst create mode 100644 docs/api/scalar.rst delete mode 100644 docs/dtype.rst delete mode 100644 docs/encoding.rst delete mode 100644 docs/expr.rst create mode 100644 docs/file_format.rst create mode 100644 docs/guide.rst delete mode 100644 docs/io.rst create mode 100644 docs/quickstart.rst delete mode 100644 docs/scalar.rst diff --git a/.github/workflows/python-docs.yml b/.github/workflows/docs.yml similarity index 92% rename from .github/workflows/python-docs.yml rename to .github/workflows/docs.yml index fc0f0adc1f..4dac3655b2 100644 --- a/.github/workflows/python-docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,4 @@ -name: Python docs +name: Python & Rust docs on: push: @@ -31,6 +31,7 @@ jobs: built_sha=$(git rev-parse HEAD) + rm -rf docs/_build/html/rust/CACHETAG.DIR docs/_build/html/rust/debug mv docs/_build/html /tmp/html git fetch origin diff --git a/docs/Makefile b/docs/Makefile index 1b1224a5b8..122224bb74 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -18,3 +18,9 @@ help: # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +html: rust-html + +.PHONY: rust-html +rust-html: + cargo doc --no-deps --workspace --all-features --target-dir $(BUILDDIR)/html/rust diff --git a/docs/_static/example.parquet b/docs/_static/example.parquet new file mode 100644 index 0000000000000000000000000000000000000000..46c86166bf4d88f6fa4f43c67801383cc70266ea GIT binary patch literal 35631 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 literal 0 HcmV?d00001 diff --git a/docs/_static/file-format-2024-10-23-1642.svg b/docs/_static/file-format-2024-10-23-1642.svg new file mode 100644 index 0000000000..46eb3b5e5d --- /dev/null +++ b/docs/_static/file-format-2024-10-23-1642.svg @@ -0,0 +1,10 @@ + + + + + + + + Statistics"Tables"SchemaFooterPostscriptData.........AABBStruct { names: ["A", "B"]; dtypes: [Primitive { I32; nullable: false }, Utf8 { nullable: false }]; nullable: false }RowOffsetChunkIndex012......MinMaxOne Metadata Table Per ColumnNullCount...TrueCountColumns Chunked{has_metadata=true}Flat { begin: u64, end: u64 }FlatFlatFlat1 Flat Layout (i.e., Byte Offset Range)Per Column ChunkColumn AColumn BFirst child contains the byte offsetsfor the column's metadata tableChunked...FlatFlatFlat Chunked{has_metadata=true}Example: a Layout with Row GroupsEOFVersion Info (4 bytes)Magic (4 bytes)EOF: fixed sizeof 8-bytes(forever)Schema OffsetLayout Offset...Postscript: compile-time known size;guaranteed to fitinto initial readLayoutRow Count...Footer: variable-sizemetadata that isnecessary for pruning& pushdown \ No newline at end of file diff --git a/docs/_static/style.css b/docs/_static/style.css new file mode 100644 index 0000000000..2d137b32a9 --- /dev/null +++ b/docs/_static/style.css @@ -0,0 +1,3 @@ +html .pst-navbar-icon { + font-size: 1.5rem; +} diff --git a/docs/_static/vortex_spiral_logo.svg b/docs/_static/vortex_spiral_logo.svg new file mode 100644 index 0000000000..026901c94f --- /dev/null +++ b/docs/_static/vortex_spiral_logo.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/_static/vortex_spiral_logo_dark_theme.svg b/docs/_static/vortex_spiral_logo_dark_theme.svg new file mode 100644 index 0000000000..0c4d52bab2 --- /dev/null +++ b/docs/_static/vortex_spiral_logo_dark_theme.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/dataset.rst b/docs/api/dataset.rst similarity index 77% rename from docs/dataset.rst rename to docs/api/dataset.rst index 848e6592ca..16d564868d 100644 --- a/docs/dataset.rst +++ b/docs/api/dataset.rst @@ -6,5 +6,15 @@ query engines like DuckDB and Polars. In particular, Vortex will read data propo number of rows passing a filter condition and the number of columns in a selection. For most Vortex encodings, this property holds true even when the filter condition specifies a single row. +.. autosummary:: + :nosignatures: + + ~vortex.dataset.VortexDataset + ~vortex.dataset.VortexScanner + +.. raw:: html + +
+ .. automodule:: vortex.dataset :members: diff --git a/docs/api/dtype.rst b/docs/api/dtype.rst new file mode 100644 index 0000000000..4f529feea9 --- /dev/null +++ b/docs/api/dtype.rst @@ -0,0 +1,27 @@ +Array Data Types +================ + +The logical types of the elements of an Array. Each logical type is implemented by a variety of +Array encodings which describe both a representation-as-bytes as well as how to apply operations on +that representation. + +.. autosummary:: + :nosignatures: + + ~vortex.dtype.DType + ~vortex.dtype.binary + ~vortex.dtype.bool + ~vortex.dtype.float + ~vortex.dtype.int + ~vortex.dtype.null + ~vortex.dtype.uint + ~vortex.dtype.utf8 + +.. raw:: html + +
+ +.. automodule:: vortex.dtype + :members: + :imported-members: + diff --git a/docs/api/encoding.rst b/docs/api/encoding.rst new file mode 100644 index 0000000000..3ec5cb449c --- /dev/null +++ b/docs/api/encoding.rst @@ -0,0 +1,26 @@ +Arrays +====== + +A Vortex array is a possibly compressed ordered set of homogeneously typed values. Each array has a +logical type and a physical encoding. The logical type describes the set of operations applicable to +the values of this array. The physical encoding describes how this array is realized in memory, on +disk, and over the wire and how to apply operations to that realization. + +.. autosummary:: + :nosignatures: + + ~vortex.encoding.array + ~vortex.encoding.compress + ~vortex.encoding.Array + +.. raw:: html + +
+ +.. autofunction:: vortex.encoding.array + +.. autofunction:: vortex.encoding.compress + +.. autoclass:: vortex.encoding.Array + :members: + :special-members: __len__ diff --git a/docs/api/expr.rst b/docs/api/expr.rst new file mode 100644 index 0000000000..3fd6ab3390 --- /dev/null +++ b/docs/api/expr.rst @@ -0,0 +1,26 @@ +Expressions +=========== + +Vortex expressions represent simple filtering conditions on the rows of a Vortex array. For example, +the following expression represents the set of rows for which the `age` column lies between 23 and +55: + +.. doctest:: + + >>> import vortex + >>> age = vortex.expr.column("age") + >>> (23 > age) & (age < 55) # doctest: +SKIP + +.. autosummary:: + :nosignatures: + + ~vortex.expr.column + ~vortex.expr.Expr + +.. raw:: html + +
+ +.. autofunction:: vortex.expr.column + +.. autoclass:: vortex.expr.Expr diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 0000000000..b67c96ad6e --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,12 @@ +Python API +========== + +.. toctree:: + :maxdepth: 5 + + encoding + dtype + io + dataset + expr + scalar diff --git a/docs/api/io.rst b/docs/api/io.rst new file mode 100644 index 0000000000..1dee8dea5d --- /dev/null +++ b/docs/api/io.rst @@ -0,0 +1,20 @@ +Input and Output +================ + +Vortex arrays support reading and writing to local and remote file systems, including plain-old +HTTP, S3, Google Cloud Storage, and Azure Blob Storage. + +.. autosummary:: + :nosignatures: + + ~vortex.io.read_path + ~vortex.io.read_url + ~vortex.io.write_path + +.. raw:: html + +
+ +.. automodule:: vortex.io + :members: + :imported-members: diff --git a/docs/api/scalar.rst b/docs/api/scalar.rst new file mode 100644 index 0000000000..288673a5c1 --- /dev/null +++ b/docs/api/scalar.rst @@ -0,0 +1,25 @@ +Scalars +======= + +A scalar is a single atomic value like the integer ``1``, the string ``"hello"``, or the structure +``{"age": 55, "name": "Angela"}``. The :meth:`.Array.scalar_at` method +returns a native Python value when the cost of doing so is small. However, for larger values like +binary data, UTF-8 strings, variable-length lists, and structures, Vortex returns a zero-copy *view* +of the Array data. The ``into_python`` method of each view will copy the scalar into a native Python +value. + +.. autosummary:: + :nosignatures: + + ~vortex.scalar.Buffer + ~vortex.scalar.BufferString + ~vortex.scalar.VortexList + ~vortex.scalar.VortexStruct + +.. raw:: html + +
+ +.. automodule:: vortex.scalar + :members: + :imported-members: diff --git a/docs/conf.py b/docs/conf.py index 719854b5e1..0fe652707d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,8 +15,11 @@ extensions = [ "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", + "sphinx.ext.autosummary", "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx_design", ] templates_path = ["_templates"] @@ -24,10 +27,10 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), - "pyarrow": ("https://arrow.apache.org/docs/", None), - "pandas": ("https://pandas.pydata.org/docs/", None), - "numpy": ("https://numpy.org/doc/stable/", None), - "polars": ("https://docs.pola.rs/api/python/stable/", None), + "pyarrow": ("https://arrow.apache.org/docs", None), + "pandas": ("https://pandas.pydata.org/docs", None), + "numpy": ("https://numpy.org/doc/stable", None), + "polars": ("https://docs.pola.rs/api/python/stable", None), } nitpicky = True # ensures all :class:, :obj:, etc. links are valid @@ -38,4 +41,37 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "pydata_sphinx_theme" -# html_static_path = ['_static'] # no static files yet +html_static_path = ["_static"] +html_css_files = ["style.css"] # relative to _static/ + +# -- Options for PyData Theme ------------------------------------------------ +html_theme_options = { + "show_toc_level": 2, + "logo": { + "alt_text": "The Vortex logo.", + "text": "Vortex", + "image_light": "_static/vortex_spiral_logo.svg", + "image_dark": "_static/vortex_spiral_logo_dark_theme.svg", + }, + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/spiraldb/vortex", + "icon": "fa-brands fa-github", + "type": "fontawesome", + }, + { + "name": "PyPI", + "url": "https://pypi.org/project/vortex-array", + "icon": "fa-brands fa-python", + "type": "fontawesome", + }, + ], + "header_links_before_dropdown": 3, +} +html_sidebars = { + # hide the primary (left-hand) sidebar on pages without sub-pages + "quickstart": [], + "guide": [], + "file_format": [], +} diff --git a/docs/dtype.rst b/docs/dtype.rst deleted file mode 100644 index 9c30bc80b9..0000000000 --- a/docs/dtype.rst +++ /dev/null @@ -1,7 +0,0 @@ -Array Data Types -================ - -.. automodule:: vortex.dtype - :members: - :imported-members: - diff --git a/docs/encoding.rst b/docs/encoding.rst deleted file mode 100644 index 8448777fba..0000000000 --- a/docs/encoding.rst +++ /dev/null @@ -1,7 +0,0 @@ -Arrays -====== - -.. automodule:: vortex.encoding - :members: - :imported-members: - :special-members: __len__ diff --git a/docs/expr.rst b/docs/expr.rst deleted file mode 100644 index 854aec35ce..0000000000 --- a/docs/expr.rst +++ /dev/null @@ -1,6 +0,0 @@ -Row Filter Expressions -====================== - -.. automodule:: vortex.expr - :members: - :imported-members: diff --git a/docs/file_format.rst b/docs/file_format.rst new file mode 100644 index 0000000000..322bcd503b --- /dev/null +++ b/docs/file_format.rst @@ -0,0 +1,79 @@ +File Format +=========== + +Intuition +--------- + +The Vortex file format has both *layouts*, which describe how different chunks of columns are stored +relative to one another, and *encodings* which describe the byte representation of a contiguous +sequence of values. A layout describes how to contiguously store one or more arrays as is necessary +for storing an array on disk or transmitting it over the wire. An encoding defines one binary +representation for memory, disk, and the wire. + +.. _file-format--layouts: + +Layouts +^^^^^^^ + +Vortex arrays have the same binary representation in-memory, on-disk, and over-the-wire; however, +all the rows of all the columns are not necessarily contiguously laid out. Vortex has three kinds of +*layouts* which recursively compose: the *flat layout*, the *column layout*, and the *chunked +layout*. + +The flat layout is a contiguous sequence of bytes. Any Vortex array encoding can be serialized into +the flat layout. + +The column layout lays out each column of a struct-typed array as a separate sequence of bytes. Each +column may or may not recursively use a chunked layout. Column layouts permit readers to push-down +column projections. + +The chunked layout lays out an array as a sequence of row chunks. Each chunk may have a different +size. A chunked layout permits reader to push-down row filters based on statistics which we describe +later. Note that, if the laid out array is a struct array, each column uses the same chunk +size. This is equivalent to Parquet's row groups. + +A few examples of concrete layouts: + +1. Chunked of struct of chunked of flat: essentially a Parquet layout with row groups in which each + column's values are contiguously stored in pages. +2. Struct of chunked of flat: eliminates row groups, retaining only pages. +3. Struct of flat: prevents row filter push-down because each array is, to the layout, an opaque + sequence of bytes. + +The chunked layout stores, per chunk, metadata necessary for effective row filtering such as +sortedness, constancy, the minimum value, the maximum value, and the number of null rows. Readers +consult these metadata tables to avoid reading chunks without relevant data. + +.. card:: + + .. figure:: _static/file-format-2024-10-23-1642.svg + :width: 800px + :alt: A schematic of the file format + + +++ + + The Vortex file format has five sections: data, statistics, schema, footer, and postscript. The + postscript describes the locating of the schema and layout which in turn describe how to + interpret the data and metadata. The schema describes the logical type. The metadata contains + information necessary for row filtering. + +.. _included-codecs: + +Encodings +^^^^^^^^^ + +- Most of the Arrow encodings. +- Chunked, a sequence of arrays. +- Constant, a value and a length. +- Sparse, a value plus a pair of arrays representing exceptions: an array of indices and of values. +- FastLanes Frame-of-Reference, BitPacking, and Delta. +- Fast Static Symbol Table (FSST). +- Adapative Lossless Floating Point (ALP). +- ALP Real Double (ALP-RD). +- ByteBool, one byte per Boolean value. +- ZigZag. + +Specification +------------- + +TODO! diff --git a/docs/guide.rst b/docs/guide.rst new file mode 100644 index 0000000000..068d12e708 --- /dev/null +++ b/docs/guide.rst @@ -0,0 +1,173 @@ +Guide +===== + +.. admonition:: Rustaceans + + See the `Vortex Rust documentation `_, for details on Vortex in Rust. + +Python +------ + +Construct a Vortex array from lists of simple Python values: + +.. doctest:: + + >>> import vortex + >>> vtx = vortex.array([1, 2, 3, 4]) + >>> vtx.dtype + int(64, False) + +Python's :obj:`None` represents a missing or null value and changes the dtype of the array from +non-nullable 64-bit integers to nullable 64-bit integers: + +.. doctest:: + + >>> vtx = vortex.array([1, 2, None, 4]) + >>> vtx.dtype + int(64, True) + +A list of :class:`dict` is converted to an array of structures. Missing values may appear at any +level: + +.. doctest:: + + >>> vtx = vortex.array([ + ... {'name': 'Joseph', 'age': 25}, + ... {'name': None, 'age': 31}, + ... {'name': 'Angela', 'age': None}, + ... {'name': 'Mikhail', 'age': 57}, + ... {'name': None, 'age': None}, + ... None, + ... ]) + >>> vtx.dtype + struct({"age": int(64, True), "name": utf8(True)}, True) + +:meth:`.Array.to_pylist` converts a Vortex array into a list of Python values. + +.. doctest:: + + >>> vtx.to_pylist() + [{'age': 25, 'name': 'Joseph'}, {'age': 31, 'name': None}, {'age': None, 'name': 'Angela'}, {'age': 57, 'name': 'Mikhail'}, {'age': None, 'name': None}, {'age': None, 'name': None}] + +Arrow +^^^^^ + +The :func:`~vortex.encoding.array` function constructs a Vortex array from an Arrow one without any +copies: + +.. doctest:: + + >>> import pyarrow as pa + >>> arrow = pa.array([1, 2, None, 3]) + >>> arrow.type + DataType(int64) + >>> vtx = vortex.array(arrow) + >>> vtx.dtype + int(64, True) + +:meth:`.Array.to_arrow_array` converts back to an Arrow array: + +.. doctest:: + + >>> vtx.to_arrow_array() + + [ + 1, + 2, + null, + 3 + ] + +If you have a struct array, use :meth:`.Array.to_arrow_table` to construct an Arrow table: + +.. doctest:: + + >>> struct_vtx = vortex.array([ + ... {'name': 'Joseph', 'age': 25}, + ... {'name': 'Narendra', 'age': 31}, + ... {'name': 'Angela', 'age': 33}, + ... {'name': 'Mikhail', 'age': 57}, + ... ]) + >>> struct_vtx.to_arrow_table() + pyarrow.Table + age: int64 + name: string_view + ---- + age: [[25,31,33,57]] + name: [["Joseph","Narendra","Angela","Mikhail"]] + +Pandas +^^^^^^ + +:meth:`.Array.to_pandas_df` converts a Vortex array into a Pandas DataFrame: + +.. doctest:: + + >>> df = struct_vtx.to_pandas_df() + >>> df + age name + 0 25 Joseph + 1 31 Narendra + 2 33 Angela + 3 57 Mikhail + +:func:`~vortex.encoding.array` converts from a Pandas DataFrame into a Vortex array: + + >>> vortex.array(df).to_arrow_table() + pyarrow.Table + age: int64 + name: string_view + ---- + age: [[25,31,33,57]] + name: [["Joseph","Narendra","Angela","Mikhail"]] + + +.. _query-engine-integration: + +Query Engines +------------- + +:class:`~vortex.dataset.VortexDataset` implements the :class:`pyarrow.dataset.Dataset` API which +enables many Python-based query engines to pushdown row filters and column projections on Vortex +files. + +Polars +^^^^^^ + + >>> import polars as pl + >>> ds = vortex.dataset.from_path( + ... '_static/example.vortex' + ... ) + >>> lf = pl.scan_pyarrow_dataset(ds) + >>> lf = lf.select('tip_amount', 'fare_amount') + >>> lf = lf.head(3) + >>> lf.collect() + shape: (3, 2) + ┌────────────┬─────────────┐ + │ tip_amount ┆ fare_amount │ + │ --- ┆ --- │ + │ f64 ┆ f64 │ + ╞════════════╪═════════════╡ + │ 0.0 ┆ 61.8 │ + │ 5.1 ┆ 20.5 │ + │ 16.54 ┆ 70.0 │ + └────────────┴─────────────┘ + +DuckDB +^^^^^^ + + >>> import duckdb + >>> ds = vortex.dataset.from_path( + ... '_static/example.vortex' + ... ) + >>> duckdb.sql('select ds.tip_amount, ds.fare_amount from ds limit 3').show() + ┌────────────┬─────────────┐ + │ tip_amount │ fare_amount │ + │ double │ double │ + ├────────────┼─────────────┤ + │ 0.0 │ 61.8 │ + │ 5.1 │ 20.5 │ + │ 16.54 │ 70.0 │ + └────────────┴─────────────┘ + + diff --git a/docs/index.rst b/docs/index.rst index c89a19c9a3..2a2d9e9232 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,18 +3,68 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Vortex documentation -==================== +Wide, Fast & Compact. Pick Three. +================================== -Vortex is an Apache Arrow-compatible toolkit for working with compressed array data. +.. grid:: 1 1 2 2 + :gutter: 4 4 4 4 + + .. grid-item-card:: The File Format + :link: file_format + :link-type: doc + + Currently just a schematic. Specification forthcoming. + + .. grid-item-card:: The Rust API + :link: https://spiraldb.github.io/vortex/docs2/rust/doc/vortex + + The primary interface to the Vortex toolkit. + + .. grid-item-card:: Quickstart + :link: quickstart + :link-type: doc + + For end-users looking to read and write Vortex files. + + .. grid-item-card:: The Benchmarks + :link: https://bench.vortex.dev/ + + Random access, throughput, and TPC-H. + + +Vortex is a fast & extensible columnar file format that is based around state-of-the-art research +from the database community. It is built around cascading compression with lightweight encodings (no +block compression), allowing for both efficient random access and extremely fast decompression. + +Vortex also includes an accompanying in-memory format for these (recursively) compressed arrays, +that is zero-copy compatible with Apache Arrow in uncompressed form. Taken together, the Vortex +library is a useful toolkit with compressed Arrow data in-memory, on-disk, & over-the-wire. + +Vortex aspires to succeed Apache Parquet by pushing the Pareto frontier outwards: 1-2x faster +writes, 2-10x faster scans, and 100-200x faster random access reads, while preserving the same +approximate compression ratio as Parquet v2 with zstd. + +Its features include: + +- A zero-copy data layout for disk, memory, and the wire. +- Kernels for computing on, filtering, slicing, indexing, and projecting compressed arrays. +- Builtin state-of-the-art codecs including FastLanes (integer bit-packing), ALP (floating point), + and FSST (strings). +- Support for custom user-implemented codecs. +- Support for, but no requirement for, row groups. +- A read sub-system supporting filter and projection pushdown. + +Vortex's flexible layout empowers writers to choose the right layout for their setting: fast writes, +fast reads, small files, few columns, many columns, over-sized columns, etc. + +Documentation +------------- .. toctree:: :maxdepth: 2 - :caption: Contents: - - encoding - dtype - io - dataset - expr - scalar + + quickstart + guide + file_format + api/index + Rust API diff --git a/docs/io.rst b/docs/io.rst deleted file mode 100644 index f2cc405ce9..0000000000 --- a/docs/io.rst +++ /dev/null @@ -1,6 +0,0 @@ -Input and Output -================ - -.. automodule:: vortex.io - :members: - :imported-members: diff --git a/docs/pyproject.toml b/docs/pyproject.toml index 53ceee91e1..a93d7459ed 100644 --- a/docs/pyproject.toml +++ b/docs/pyproject.toml @@ -3,7 +3,12 @@ name = "docs" version = "0.1.0" description = "Vortex documentation." authors = [] -dependencies = ["pydata-sphinx-theme>=0.15.4", "sphinx>=8.0.2", "pyvortex"] +dependencies = [ + "pydata-sphinx-theme>=0.16.0", + "sphinx>=8.0.2", + "pyvortex", + "sphinx-design>=0.6.1", +] requires-python = ">= 3.10" [tool.uv] diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 0000000000..65a71cb7c3 --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,199 @@ +Quickstart +========== + +The reference implementation exposes both a Rust and Python API. A C API is currently in progress. + +- :ref:`Quickstart for Python ` +- :ref:`Quickstart for Rust ` +- :ref:`Quickstart for C ` + +.. _python-quickstart: + +Python +------ + +Install +^^^^^^^ + +:: + + pip install vortex-array + +Convert +^^^^^^^ + +You can either use your own Parquet file or download the `example used here +`__. + +Use Arrow to read a Parquet file and then use :func:`~vortex.encoding.array` to construct an uncompressed +Vortex array: + +.. doctest:: + + >>> import pyarrow.parquet as pq + >>> import vortex + >>> parquet = pq.read_table("_static/example.parquet") + >>> vtx = vortex.array(parquet) + >>> vtx.nbytes + 141024 + +Compress +^^^^^^^^ + +Use :func:`~vortex.encoding.compress` to compress the Vortex array and check the relative size: + +.. doctest:: + + >>> cvtx = vortex.compress(vtx) + >>> cvtx.nbytes + 13970 + >>> cvtx.nbytes / vtx.nbytes + 0.099... + +Vortex uses nearly ten times fewer bytes than Arrow. Fewer bytes means more of your data fits in +cache and RAM. + +Write +^^^^^ + +Use :func:`~vortex.io.write_path` to write the Vortex array to disk: + +.. doctest:: + + >>> vortex.io.write_path(cvtx, "example.vortex") + +Small Vortex files (this one is just 71KiB) currently have substantial overhead relative to their +size. This will be addressed shortly. On files with at least tens of megabytes of data, Vortex is +similar to or smaller than Parquet. + +.. doctest:: + + >>> from os.path import getsize + >>> getsize("example.vortex") / getsize("_static/example.parquet") + 2.1... + +Read +^^^^ + +Use :func:`~vortex.io.read_path` to read the Vortex array from disk: + +.. doctest:: + + >>> cvtx = vortex.io.read_path("example.vortex") + +.. _rust-quickstart: + +Rust +---- + +Install +^^^^^^^ + +Install vortex and all the first-party array encodings:: + + cargo add vortex-array vortex-alp vortex-fsst vortex-fastlanes \ + vortex-bytebool vortex-datetime-dtype vortex-datetime-parts \ + vortex-dict vortex-runend vortex-runend-bool vortex-zigzag \ + vortex-sampling-compressor vortex-serde + +Convert +^^^^^^^ + +You can either use your own Parquet file or download the `example used here +`__. + +Use Arrow to read a Parquet file and then construct an uncompressed Vortex array: + +.. code-block:: rust + + use std::fs::File; + + use arrow_array::RecordBatchReader; + use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; + use vortex::array::ChunkedArray; + use vortex::arrow::FromArrowType; + use vortex::{Array, IntoArray}; + use vortex_dtype::DType; + + let reader = + ParquetRecordBatchReaderBuilder::try_new(File::open("_static/example.parquet").unwrap()) + .unwrap() + .build() + .unwrap(); + let dtype = DType::from_arrow(reader.schema()); + let chunks = reader + .map(|x| Array::try_from(x.unwrap()).unwrap()) + .collect::>(); + let vtx = ChunkedArray::try_new(chunks, dtype).unwrap().into_array(); + +Compress +^^^^^^^^ + +Use the sampling compressor to compress the Vortex array and check the relative size: + +.. code-block:: rust + + use std::collections::HashSet; + + use vortex_sampling_compressor::{SamplingCompressor, DEFAULT_COMPRESSORS}; + + let compressor = SamplingCompressor::new(HashSet::from(*DEFAULT_COMPRESSORS)); + let cvtx = compressor.compress(&vtx, None).unwrap().into_array(); + println!("{}", cvtx.nbytes()); + +Write +^^^^^ + +Reading and writing both require an async runtime, in this example we use Tokio. The LayoutWriter +knows how to write Vortex arrays to disk: + +.. code-block:: rust + + use std::path::Path; + + use tokio::fs::File as TokioFile; + use vortex_serde::layouts::LayoutWriter; + + let file = TokioFile::create(Path::new("example.vortex")) + .await + .unwrap(); + let writer = LayoutWriter::new(file) + .write_array_columns(cvtx.clone()) + .await + .unwrap(); + writer.finalize().await.unwrap(); + +Read +^^^^ + +.. code-block:: rust + + use futures::TryStreamExt; + use vortex_sampling_compressor::ALL_COMPRESSORS_CONTEXT; + use vortex_serde::layouts::{LayoutContext, LayoutDeserializer, LayoutReaderBuilder}; + + let file = TokioFile::open(Path::new("example.vortex")).await.unwrap(); + let builder = LayoutReaderBuilder::new( + file, + LayoutDeserializer::new( + ALL_COMPRESSORS_CONTEXT.clone(), + LayoutContext::default().into(), + ), + ); + + let stream = builder.build().await.unwrap(); + let dtype = stream.schema().clone().into(); + let vecs: Vec = stream.try_collect().await.unwrap(); + let cvtx = ChunkedArray::try_new(vecs, dtype) + .unwrap() + .into_array(); + + println!("{}", cvtx.nbytes()); + + +.. _c-quickstart: + +C +- + +Coming soon! diff --git a/docs/scalar.rst b/docs/scalar.rst deleted file mode 100644 index 9fb3b26cfc..0000000000 --- a/docs/scalar.rst +++ /dev/null @@ -1,6 +0,0 @@ -Scalar Values -============= - -.. automodule:: vortex.scalar - :members: - :imported-members: diff --git a/pyvortex/pyproject.toml b/pyvortex/pyproject.toml index f482ce0541..08f78c3ceb 100644 --- a/pyvortex/pyproject.toml +++ b/pyvortex/pyproject.toml @@ -41,4 +41,5 @@ features = ["pyo3/extension-module"] include = [ { path = "rust-toolchain.toml", format = "sdist" }, { path = "README.md", format = "sdist" }, + { path = "python/vortex/py.typed", format = "sdist" }, ] diff --git a/pyvortex/python/vortex/__init__.py b/pyvortex/python/vortex/__init__.py index b7101a7ccc..6a50c5978b 100644 --- a/pyvortex/python/vortex/__init__.py +++ b/pyvortex/python/vortex/__init__.py @@ -5,5 +5,6 @@ __doc__ = module_docs del module_docs array = encoding.array +compress = encoding.compress __all__ = ["array", dtype, expr, io, encoding, scalar, dataset] diff --git a/pyvortex/python/vortex/dataset.py b/pyvortex/python/vortex/dataset.py index d8b3254966..7f9d8d5d3b 100644 --- a/pyvortex/python/vortex/dataset.py +++ b/pyvortex/python/vortex/dataset.py @@ -12,7 +12,12 @@ class VortexDataset(pyarrow.dataset.Dataset): - """Read Vortex files with row filter and column selection pushdown.""" + """Read Vortex files with row filter and column selection pushdown. + + This class implements the :class:`.pyarrow.dataset.Dataset` interface which enables its use with + Polars, DuckDB, Pandas and others. + + """ def __init__(self, dataset): self._dataset = dataset @@ -62,6 +67,35 @@ def head( use_threads: bool | None = None, memory_pool: pa.MemoryPool = None, ) -> pa.Table: + """Load the first `num_rows` of the dataset. + + Parameters + ---------- + num_rows : int + The number of rows to load. + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ if batch_readahead is not None: raise ValueError("batch_readahead not supported") if fragment_readahead is not None: @@ -114,7 +148,33 @@ def scanner( use_threads: bool | None = None, memory_pool: pa.MemoryPool = None, ) -> pa.dataset.Scanner: - """Not implemented.""" + """Construct a :class:`.pyarrow.dataset.Scanner`. + + Parameters + ---------- + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ return VortexScanner( self, columns, @@ -143,6 +203,35 @@ def take( use_threads: bool | None = None, memory_pool: pa.MemoryPool = None, ) -> pa.Table: + """Load a subset of rows identified by their absolute indices. + + Parameters + ---------- + indices : :class:`.pyarrow.Array` + A numeric array of absolute indices into `self` indicating which rows to keep. + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ return ( self._dataset.to_array(columns=columns, batch_size=batch_size, row_filter=filter) .take(encoding.array(indices)) @@ -160,6 +249,33 @@ def to_record_batch_reader( use_threads: bool | None = None, memory_pool: pa.MemoryPool = None, ) -> pa.RecordBatchReader: + """Construct a :class:`.pyarrow.RecordBatchReader`. + + Parameters + ---------- + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ if batch_readahead is not None: raise ValueError("batch_readahead not supported") if fragment_readahead is not None: @@ -186,6 +302,33 @@ def to_batches( use_threads: bool | None = None, memory_pool: pa.MemoryPool = None, ) -> Iterator[pa.RecordBatch]: + """Construct an iterator of :class:`.pyarrow.RecordBatch`. + + Parameters + ---------- + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ record_batch_reader = self.to_record_batch_reader( columns, filter, @@ -213,6 +356,33 @@ def to_table( use_threads: bool | None = None, memory_pool: pa.MemoryPool = None, ) -> pa.Table: + """Construct an Arrow :class:`.pyarrow.Table`. + + Parameters + ---------- + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ if batch_readahead is not None: raise ValueError("batch_readahead not supported") if fragment_readahead is not None: @@ -229,8 +399,44 @@ def to_table( return self._dataset.to_array(columns=columns, batch_size=batch_size, row_filter=filter).to_arrow_table() +def from_path(path: str) -> VortexDataset: + return VortexDataset(_lib_dataset.dataset_from_path(path)) + + +def from_url(url: str) -> VortexDataset: + return VortexDataset(_lib_dataset.dataset_from_url(url)) + + class VortexScanner(pa.dataset.Scanner): - """A PyArrow Dataset Scanner that reads from a Vortex Array.""" + """A PyArrow Dataset Scanner that reads from a Vortex Array. + + Parameters + ---------- + dataset : VortexDataset + The dataset to scan. + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ def __init__( self, @@ -270,6 +476,18 @@ def count_rows(self): ) def head(self, num_rows: int) -> pa.Table: + """Load the first `num_rows` of the dataset. + + Parameters + ---------- + num_rows : int + The number of rows to read. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ return self._dataset.head( num_rows, self._columns, @@ -287,6 +505,13 @@ def scan_batches(self) -> Iterator[pa.dataset.TaggedRecordBatch]: raise NotImplementedError("scan batches") def to_batches(self) -> Iterator[pa.RecordBatch]: + """Construct an iterator of :class:`.pyarrow.RecordBatch`. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ return self._dataset.to_batches( self._columns, self._filter, @@ -299,6 +524,14 @@ def to_batches(self) -> Iterator[pa.RecordBatch]: ) def to_reader(self) -> pa.RecordBatchReader: + """Construct a :class:`.pyarrow.RecordBatchReader`. + + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ return self._dataset.to_record_batch_reader( self._columns, self._filter, @@ -311,6 +544,14 @@ def to_reader(self) -> pa.RecordBatchReader: ) def to_table(self) -> pa.Table: + """Construct an Arrow :class:`.pyarrow.Table`. + + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ return self._dataset.to_table( self._columns, self._filter, diff --git a/pyvortex/python/vortex/encoding.py b/pyvortex/python/vortex/encoding.py index ac522d3750..75eeb5655c 100644 --- a/pyvortex/python/vortex/encoding.py +++ b/pyvortex/python/vortex/encoding.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import pandas import pyarrow @@ -61,7 +61,7 @@ def _Array_to_arrow_table(self: _encoding.Array) -> pyarrow.Table: Examples -------- - >>> array = vortex.encoding.array([ + >>> array = vortex.array([ ... {'name': 'Joseph', 'age': 25}, ... {'name': 'Narendra', 'age': 31}, ... {'name': 'Angela', 'age': 33}, @@ -82,7 +82,7 @@ def _Array_to_arrow_table(self: _encoding.Array) -> pyarrow.Table: Array.to_arrow_table = _Array_to_arrow_table -def _Array_to_pandas(self: _encoding.Array) -> "pandas.DataFrame": +def _Array_to_pandas_df(self: _encoding.Array) -> "pandas.DataFrame": """Construct a Pandas dataframe from this Vortex array. Warning @@ -99,27 +99,24 @@ def _Array_to_pandas(self: _encoding.Array) -> "pandas.DataFrame": Construct a dataframe from a Vortex array: - >>> array = vortex.encoding.array([ + >>> array = vortex.array([ ... {'name': 'Joseph', 'age': 25}, ... {'name': 'Narendra', 'age': 31}, ... {'name': 'Angela', 'age': 33}, ... {'name': 'Mikhail', 'age': 57}, ... ]) - >>> array.to_pandas() + >>> array.to_pandas_df() age name 0 25 Joseph 1 31 Narendra 2 33 Angela 3 57 Mikhail - - Lift the struct fields to the top-level in the dataframe: - """ return self.to_arrow_table().to_pandas(types_mapper=pandas.ArrowDtype) -Array.to_pandas = _Array_to_pandas +Array.to_pandas_df = _Array_to_pandas_df def _Array_to_polars_dataframe( @@ -146,7 +143,7 @@ def _Array_to_polars_dataframe( Examples -------- - >>> array = vortex.encoding.array([ + >>> array = vortex.array([ ... {'name': 'Joseph', 'age': 25}, ... {'name': 'Narendra', 'age': 31}, ... {'name': 'Angela', 'age': 33}, @@ -193,7 +190,7 @@ def _Array_to_polars_series(self: _encoding.Array): # -> 'polars.Series': # br Convert a numeric array with nulls to a Polars Series: - >>> vortex.encoding.array([1, None, 2, 3]).to_polars_series() # doctest: +NORMALIZE_WHITESPACE + >>> vortex.array([1, None, 2, 3]).to_polars_series() # doctest: +NORMALIZE_WHITESPACE shape: (4,) Series: '' [i64] [ @@ -205,7 +202,7 @@ def _Array_to_polars_series(self: _encoding.Array): # -> 'polars.Series': # br Convert a UTF-8 string array to a Polars Series: - >>> vortex.encoding.array(['hello, ', 'is', 'it', 'me?']).to_polars_series() # doctest: +NORMALIZE_WHITESPACE + >>> vortex.array(['hello, ', 'is', 'it', 'me?']).to_polars_series() # doctest: +NORMALIZE_WHITESPACE shape: (4,) Series: '' [str] [ @@ -217,7 +214,7 @@ def _Array_to_polars_series(self: _encoding.Array): # -> 'polars.Series': # br Convert a struct array to a Polars Series: - >>> array = vortex.encoding.array([ + >>> array = vortex.array([ ... {'name': 'Joseph', 'age': 25}, ... {'name': 'Narendra', 'age': 31}, ... {'name': 'Angela', 'age': 33}, @@ -262,7 +259,7 @@ def _Array_to_numpy(self: _encoding.Array, *, zero_copy_only: bool = True) -> "n Construct an immutable ndarray from a Vortex array: - >>> array = vortex.encoding.array([1, 0, 0, 1]) + >>> array = vortex.array([1, 0, 0, 1]) >>> array.to_numpy() array([1, 0, 0, 1]) @@ -273,14 +270,39 @@ def _Array_to_numpy(self: _encoding.Array, *, zero_copy_only: bool = True) -> "n Array.to_numpy = _Array_to_numpy -def array(obj: pyarrow.Array | list) -> Array: +def _Array_to_pylist(self: _encoding.Array) -> list[Any]: + """Deeply copy an Array into a Python list. + + Returns + ------- + :class:`list` + + Examples + -------- + + >>> array = vortex.array([ + ... {'name': 'Joseph', 'age': 25}, + ... {'name': 'Narendra', 'age': 31}, + ... {'name': 'Angela', 'age': 33}, + ... ]) + >>> array.to_pylist() + [{'age': 25, 'name': 'Joseph'}, {'age': 31, 'name': 'Narendra'}, {'age': 33, 'name': 'Angela'}] + + """ + return self.to_arrow_table().to_pylist() + + +Array.to_pylist = _Array_to_pylist + + +def array(obj: pyarrow.Array | list | Any) -> Array: """The main entry point for creating Vortex arrays from other Python objects. This function is also available as ``vortex.array``. Parameters ---------- - obj : :class:`pyarrow.Array` or :class:`list` + obj : :class:`pyarrow.Array`, :class:`list`, :class:`pandas.DataFrame` The elements of this array or list become the elements of the Vortex array. Returns @@ -290,9 +312,9 @@ def array(obj: pyarrow.Array | list) -> Array: Examples -------- - A Vortex array containing the first three integers. + A Vortex array containing the first three integers: - >>> vortex.encoding.array([1, 2, 3]).to_arrow_array() + >>> vortex.array([1, 2, 3]).to_arrow_array() [ 1, @@ -300,9 +322,9 @@ def array(obj: pyarrow.Array | list) -> Array: 3 ] - The same Vortex array with a null value in the third position. + The same Vortex array with a null value in the third position: - >>> vortex.encoding.array([1, 2, None, 3]).to_arrow_array() + >>> vortex.array([1, 2, None, 3]).to_arrow_array() [ 1, @@ -314,7 +336,7 @@ def array(obj: pyarrow.Array | list) -> Array: Initialize a Vortex array from an Arrow array: >>> arrow = pyarrow.array(['Hello', 'it', 'is', 'me']) - >>> vortex.encoding.array(arrow).to_arrow_array() + >>> vortex.array(arrow).to_arrow_array() [ "Hello", @@ -323,7 +345,40 @@ def array(obj: pyarrow.Array | list) -> Array: "me" ] + Initialize a Vortex array from a Pandas dataframe: + + >>> import pandas as pd + >>> df = pd.DataFrame({ + ... "Name": ["Braund", "Allen", "Bonnell"], + ... "Age": [22, 35, 58], + ... }) + >>> vortex.array(df).to_arrow_array() + + [ + -- is_valid: all not null + -- child 0 type: string_view + [ + "Braund", + "Allen", + "Bonnell" + ] + -- child 1 type: int64 + [ + 22, + 35, + 58 + ] + ] + """ + if isinstance(obj, list): return _encoding._encode(pyarrow.array(obj)) + try: + import pandas + + if isinstance(obj, pandas.DataFrame): + return _encoding._encode(pyarrow.Table.from_pandas(obj)) + except ImportError: + pass return _encoding._encode(obj) diff --git a/pyvortex/src/array.rs b/pyvortex/src/array.rs index 5e2f14d263..5022157117 100644 --- a/pyvortex/src/array.rs +++ b/pyvortex/src/array.rs @@ -20,8 +20,8 @@ use crate::scalar::scalar_into_py; /// /// Arrays support all the standard comparison operations: /// -/// >>> a = vortex.encoding.array(['dog', None, 'cat', 'mouse', 'fish']) -/// >>> b = vortex.encoding.array(['doug', 'jennifer', 'casper', 'mouse', 'faust']) +/// >>> a = vortex.array(['dog', None, 'cat', 'mouse', 'fish']) +/// >>> b = vortex.array(['doug', 'jennifer', 'casper', 'mouse', 'faust']) /// >>> (a < b).to_arrow_array() /// /// [ @@ -106,7 +106,7 @@ impl PyArray { /// /// Round-trip an Arrow array through a Vortex array: /// - /// >>> vortex.encoding.array([1, 2, 3]).to_arrow_array() + /// >>> vortex.array([1, 2, 3]).to_arrow_array() /// /// [ /// 1, @@ -179,19 +179,19 @@ impl PyArray { /// Examples /// -------- /// - /// By default, :func:`vortex.encoding.array` uses the largest available bit-width: + /// By default, :func:`~vortex.encoding.array` uses the largest available bit-width: /// - /// >>> vortex.encoding.array([1, 2, 3]).dtype + /// >>> vortex.array([1, 2, 3]).dtype /// int(64, False) /// /// Including a :obj:`None` forces a nullable type: /// - /// >>> vortex.encoding.array([1, None, 2, 3]).dtype + /// >>> vortex.array([1, None, 2, 3]).dtype /// int(64, True) /// /// A UTF-8 string array: /// - /// >>> vortex.encoding.array(['hello, ', 'is', 'it', 'me?']).dtype + /// >>> vortex.array(['hello, ', 'is', 'it', 'me?']).dtype /// utf8(False) #[getter] fn dtype(self_: PyRef) -> PyResult> { @@ -244,19 +244,19 @@ impl PyArray { /// /// Parameters /// ---------- - /// filter : :class:`vortex.encoding.Array` + /// filter : :class:`~vortex.encoding.Array` /// Keep all the rows in ``self`` for which the correspondingly indexed row in `filter` is True. /// /// Returns /// ------- - /// :class:`vortex.encoding.Array` + /// :class:`~vortex.encoding.Array` /// /// Examples /// -------- /// /// Keep only the single digit positive integers. /// - /// >>> a = vortex.encoding.array([0, 42, 1_000, -23, 10, 9, 5]) + /// >>> a = vortex.array([0, 42, 1_000, -23, 10, 9, 5]) /// >>> filter = vortex.array([True, False, False, False, False, True, True]) /// >>> a.filter(filter).to_arrow_array() /// @@ -279,7 +279,7 @@ impl PyArray { /// Fill forward sensor values over intermediate missing values. Note that leading nulls are /// replaced with 0.0: /// - /// >>> a = vortex.encoding.array([ + /// >>> a = vortex.array([ /// ... None, None, 30.29, 30.30, 30.30, None, None, 30.27, 30.25, /// ... 30.22, None, None, None, None, 30.12, 30.11, 30.11, 30.11, /// ... 30.10, 30.08, None, 30.21, 30.03, 30.03, 30.05, 30.07, 30.07, @@ -334,12 +334,12 @@ impl PyArray { /// /// Retrieve the last element from an array of integers: /// - /// >>> vortex.encoding.array([10, 42, 999, 1992]).scalar_at(3) + /// >>> vortex.array([10, 42, 999, 1992]).scalar_at(3) /// 1992 /// /// Retrieve the third element from an array of strings: /// - /// >>> array = vortex.encoding.array(["hello", "goodbye", "it", "is"]) + /// >>> array = vortex.array(["hello", "goodbye", "it", "is"]) /// >>> array.scalar_at(2) /// /// @@ -352,7 +352,7 @@ impl PyArray { /// /// Retrieve an element from an array of structures: /// - /// >>> array = vortex.encoding.array([ + /// >>> array = vortex.array([ /// ... {'name': 'Joseph', 'age': 25}, /// ... {'name': 'Narendra', 'age': 31}, /// ... {'name': 'Angela', 'age': 33}, @@ -376,7 +376,7 @@ impl PyArray { /// /// Out of bounds accesses are prohibited: /// - /// >>> vortex.encoding.array([10, 42, 999, 1992]).scalar_at(10) + /// >>> vortex.array([10, 42, 999, 1992]).scalar_at(10) /// Traceback (most recent call last): /// ... /// ValueError: index 10 out of bounds from 0 to 4 @@ -384,7 +384,7 @@ impl PyArray { /// /// Unlike Python, negative indices are not supported: /// - /// >>> vortex.encoding.array([10, 42, 999, 1992]).scalar_at(-2) + /// >>> vortex.array([10, 42, 999, 1992]).scalar_at(-2) /// Traceback (most recent call last): /// ... /// OverflowError: can't convert negative int to unsigned @@ -398,20 +398,20 @@ impl PyArray { /// /// Parameters /// ---------- - /// indices : :class:`vortex.encoding.Array` + /// indices : :class:`~vortex.encoding.Array` /// An array of indices to keep. /// /// Returns /// ------- - /// :class:`vortex.encoding.Array` + /// :class:`~vortex.encoding.Array` /// /// Examples /// -------- /// /// Keep only the first and third elements: /// - /// >>> a = vortex.encoding.array(['a', 'b', 'c', 'd']) - /// >>> indices = vortex.encoding.array([0, 2]) + /// >>> a = vortex.array(['a', 'b', 'c', 'd']) + /// >>> indices = vortex.array([0, 2]) /// >>> a.take(indices).to_arrow_array() /// /// [ @@ -421,8 +421,8 @@ impl PyArray { /// /// Permute and repeat the first and second elements: /// - /// >>> a = vortex.encoding.array(['a', 'b', 'c', 'd']) - /// >>> indices = vortex.encoding.array([0, 1, 1, 0]) + /// >>> a = vortex.array(['a', 'b', 'c', 'd']) + /// >>> indices = vortex.array([0, 1, 1, 0]) /// >>> a.take(indices).to_arrow_array() /// /// [ @@ -457,14 +457,14 @@ impl PyArray { /// /// Returns /// ------- - /// :class:`vortex.encoding.Array` + /// :class:`~vortex.encoding.Array` /// /// Examples /// -------- /// /// Keep only the second through third elements: /// - /// >>> a = vortex.encoding.array(['a', 'b', 'c', 'd']) + /// >>> a = vortex.array(['a', 'b', 'c', 'd']) /// >>> a.slice(1, 3).to_arrow_array() /// /// [ @@ -474,14 +474,14 @@ impl PyArray { /// /// Keep none of the elements: /// - /// >>> a = vortex.encoding.array(['a', 'b', 'c', 'd']) + /// >>> a = vortex.array(['a', 'b', 'c', 'd']) /// >>> a.slice(3, 3).to_arrow_array() /// /// [] /// /// Unlike Python, it is an error to slice outside the bounds of the array: /// - /// >>> a = vortex.encoding.array(['a', 'b', 'c', 'd']) + /// >>> a = vortex.array(['a', 'b', 'c', 'd']) /// >>> a.slice(2, 10).to_arrow_array() /// Traceback (most recent call last): /// ... @@ -489,7 +489,7 @@ impl PyArray { /// /// Or to slice with a negative value: /// - /// >>> a = vortex.encoding.array(['a', 'b', 'c', 'd']) + /// >>> a = vortex.array(['a', 'b', 'c', 'd']) /// >>> a.slice(-2, -1).to_arrow_array() /// Traceback (most recent call last): /// ... @@ -516,7 +516,7 @@ impl PyArray { /// /// Uncompressed arrays have straightforward encodings: /// - /// >>> arr = vortex.encoding.array([1, 2, None, 3]) + /// >>> arr = vortex.array([1, 2, None, 3]) /// >>> print(arr.tree_display()) /// root: vortex.primitive(0x03)(i64?, len=4) nbytes=33 B (100.00%) /// metadata: PrimitiveMetadata { validity: Array } diff --git a/pyvortex/src/compress.rs b/pyvortex/src/compress.rs index 31768fc008..be84bf018d 100644 --- a/pyvortex/src/compress.rs +++ b/pyvortex/src/compress.rs @@ -8,7 +8,7 @@ use crate::array::PyArray; /// /// Parameters /// ---------- -/// array : :class:`vortex.encoding.Array` +/// array : :class:`~vortex.encoding.Array` /// The array. /// /// Examples @@ -16,23 +16,23 @@ use crate::array::PyArray; /// /// Compress a very sparse array of integers: /// -/// >>> a = vortex.encoding.array([42 for _ in range(1000)]) -/// >>> str(vortex.encoding.compress(a)) +/// >>> a = vortex.array([42 for _ in range(1000)]) +/// >>> str(vortex.compress(a)) /// 'vortex.constant(0x09)(i64, len=1000)' /// /// Compress an array of increasing integers: /// -/// >>> a = vortex.encoding.array(list(range(1000))) -/// >>> str(vortex.encoding.compress(a)) +/// >>> a = vortex.array(list(range(1000))) +/// >>> str(vortex.compress(a)) /// 'fastlanes.for(0x17)(i64, len=1000)' /// /// Compress an array of increasing floating-point numbers and a few nulls: /// -/// >>> a = vortex.encoding.array([ +/// >>> a = vortex.array([ /// ... float(x) if x % 20 != 0 else None /// ... for x in range(1000) /// ... ]) -/// >>> str(vortex.encoding.compress(a)) +/// >>> str(vortex.compress(a)) /// 'vortex.alp(0x11)(f64?, len=1000)' pub fn compress(array: &Bound) -> PyResult { let compressor = SamplingCompressor::default(); diff --git a/pyvortex/src/dtype.rs b/pyvortex/src/dtype.rs index 7a9e49a79a..61f419d0f7 100644 --- a/pyvortex/src/dtype.rs +++ b/pyvortex/src/dtype.rs @@ -119,7 +119,7 @@ pub fn dtype_bool(py: Python<'_>, nullable: bool) -> PyResult> { /// /// Parameters /// ---------- -/// width : one of 8, 16, 32, and 64. +/// width : Literal[8, 16, 32, 64]. /// The bit width determines the span of valid values. If :obj:`None`, 64 is used. /// /// nullable : :class:`bool` @@ -162,7 +162,7 @@ pub fn dtype_int(py: Python<'_>, width: Option, nullable: bool) -> PyResult /// /// Parameters /// ---------- -/// width : one of 8, 16, 32, and 64. +/// width : Literal[8, 16, 32, 64]. /// The bit width determines the span of valid values. If :obj:`None`, 64 is used. /// /// nullable : :class:`bool` @@ -205,7 +205,7 @@ pub fn dtype_uint(py: Python<'_>, width: Option, nullable: bool) -> PyResul /// /// Parameters /// ---------- -/// width : one of 16, 32, and 64. +/// width : Literal[16, 32, 64]. /// The bit width determines the range and precision of the floating-point values. If /// :obj:`None`, 64 is used. /// diff --git a/pyvortex/src/expr.rs b/pyvortex/src/expr.rs index 8bac88fe67..3b27fc120a 100644 --- a/pyvortex/src/expr.rs +++ b/pyvortex/src/expr.rs @@ -13,12 +13,15 @@ use crate::dtype::PyDType; /// An expression describes how to filter rows when reading an array from a file. /// +/// .. seealso:: +/// :func:`.column` +/// /// Examples /// ======== /// /// All the examples read the following file. /// -/// >>> a = vortex.encoding.array([ +/// >>> a = vortex.array([ /// ... {'name': 'Joseph', 'age': 25}, /// ... {'name': None, 'age': 31}, /// ... {'name': 'Angela', 'age': None}, @@ -209,7 +212,8 @@ impl PyExpr { /// A named column. /// -/// See :class:`.Expr` for more examples. +/// .. seealso:: +/// :class:`.Expr` /// /// Example /// ======= @@ -219,6 +223,8 @@ impl PyExpr { /// >>> name = vortex.expr.column("name") /// >>> filter = name == "Joseph" /// +/// See :class:`.Expr` for more examples. +/// #[pyfunction] pub fn column<'py>(name: &Bound<'py, PyString>) -> PyResult> { let py = name.py(); diff --git a/pyvortex/src/io.rs b/pyvortex/src/io.rs index ac32aed647..d93350df95 100644 --- a/pyvortex/src/io.rs +++ b/pyvortex/src/io.rs @@ -5,6 +5,7 @@ use pyo3::pyfunction; use pyo3::types::PyString; use tokio::fs::File; use vortex::Array; +use vortex_sampling_compressor::SamplingCompressor; use vortex_serde::layouts::LayoutWriter; use crate::dataset::{ObjectStoreUrlDataset, TokioFileDataset}; @@ -27,7 +28,7 @@ use crate::{PyArray, TOKIO_RUNTIME}; /// /// Read an array with a structured column and nulls at multiple levels and in multiple columns. /// -/// >>> a = vortex.encoding.array([ +/// >>> a = vortex.array([ /// ... {'name': 'Joseph', 'age': 25}, /// ... {'name': None, 'age': 31}, /// ... {'name': 'Angela', 'age': None}, @@ -111,7 +112,7 @@ use crate::{PyArray, TOKIO_RUNTIME}; /// /// TODO(DK): Top-level nullness does not work. /// -/// >>> a = vortex.encoding.array([ +/// >>> a = vortex.array([ /// ... {'name': 'Joseph', 'age': 25}, /// ... {'name': None, 'age': 31}, /// ... {'name': 'Angela', 'age': None}, @@ -186,23 +187,25 @@ pub fn read_url( dataset.to_array(projection, None, row_filter) } -#[pyfunction] /// Write a vortex struct array to the local filesystem. /// /// Parameters /// ---------- -/// array : :class:`vortex.encoding.Array` +/// array : :class:`~vortex.encoding.Array` /// The array. Must be an array of structures. /// /// f : :class:`str` /// The file path. /// +/// compress : :class:`bool` +/// Compress the array before writing, defaults to ``True``. +/// /// Examples /// -------- /// /// Write the array `a` to the local file `a.vortex`. /// -/// >>> a = vortex.encoding.array([ +/// >>> a = vortex.array([ /// ... {'x': 1}, /// ... {'x': 2}, /// ... {'x': 10}, @@ -211,7 +214,13 @@ pub fn read_url( /// ... ]) /// >>> vortex.io.write_path(a, "a.vortex") /// -pub fn write_path(array: &Bound<'_, PyArray>, f: &Bound<'_, PyString>) -> PyResult<()> { +#[pyfunction] +#[pyo3(signature = (array, f, *, compress=true))] +pub fn write_path( + array: &Bound<'_, PyArray>, + f: &Bound<'_, PyString>, + compress: bool, +) -> PyResult<()> { async fn run(array: &Array, fname: &str) -> PyResult<()> { let file = File::create(Path::new(fname)).await?; let mut writer = LayoutWriter::new(file); @@ -222,7 +231,12 @@ pub fn write_path(array: &Bound<'_, PyArray>, f: &Bound<'_, PyString>) -> PyResu } let fname = f.to_str()?; // TODO(dk): support file objects - let array = array.borrow().unwrap().clone(); + let mut array = array.borrow().unwrap().clone(); + + if compress { + let compressor = SamplingCompressor::default(); + array = compressor.compress(&array, None)?.into_array(); + } TOKIO_RUNTIME.block_on(run(&array, fname)) } diff --git a/pyvortex/src/scalar.rs b/pyvortex/src/scalar.rs index abee1bf5dc..dbf9a5ed1e 100644 --- a/pyvortex/src/scalar.rs +++ b/pyvortex/src/scalar.rs @@ -134,7 +134,7 @@ impl PyBufferString { #[pymethods] impl PyBufferString { - /// Copy this buffer string from array memory into a Python str. + /// Copy this buffer string from array memory into a :class:`str`. #[pyo3(signature = (*, recursive = false))] #[allow(unused_variables)] // we want the same Python name across all methods pub fn into_python(self_: PyRef, recursive: bool) -> PyResult { @@ -178,7 +178,7 @@ impl PyVortexList { #[pymethods] impl PyVortexList { - /// Copy the elements of this list from array memory into a list of Python objects. + /// Copy the elements of this list from array memory into a :class:`list`. #[pyo3(signature = (*, recursive = false))] pub fn into_python(self_: PyRef, recursive: bool) -> PyResult { to_python_list(self_.py(), &self_.inner, &self_.dtype, recursive) @@ -236,7 +236,7 @@ impl PyVortexStruct { #[pymethods] impl PyVortexStruct { #[pyo3(signature = (*, recursive = false))] - /// Copy the elements of this list from array memory into a list of Python objects. + /// Copy the elements of this list from array memory into a :class:`dict`. pub fn into_python(self_: PyRef, recursive: bool) -> PyResult { to_python_dict(self_.py(), &self_.inner, &self_.dtype, recursive) } diff --git a/requirements-dev.lock b/requirements-dev.lock index 5a9bc870cc..6df3b41447 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -72,7 +72,6 @@ numpy==1.26.4 # via xarray packaging==24.0 # via matplotlib - # via pydata-sphinx-theme # via pytest # via sphinx # via xarray @@ -103,7 +102,7 @@ py-cpuinfo==9.0.0 # via pytest-benchmark pyarrow==17.0.0 # via vortex-array -pydata-sphinx-theme==0.15.4 +pydata-sphinx-theme==0.16.0 pygments==2.17.2 # via accessible-pygments # via ipython @@ -133,6 +132,8 @@ soupsieve==2.6 # via beautifulsoup4 sphinx==8.0.2 # via pydata-sphinx-theme + # via sphinx-design +sphinx-design==0.6.1 sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 diff --git a/requirements.lock b/requirements.lock index 88d7d0993d..d8458d2455 100644 --- a/requirements.lock +++ b/requirements.lock @@ -55,7 +55,6 @@ numpy==2.1.2 # via xarray packaging==24.1 # via matplotlib - # via pydata-sphinx-theme # via sphinx # via xarray pandas==2.2.3 @@ -70,7 +69,7 @@ protobuf==5.28.2 # via substrait pyarrow==17.0.0 # via vortex-array -pydata-sphinx-theme==0.15.4 +pydata-sphinx-theme==0.16.0 pygments==2.18.0 # via accessible-pygments # via pydata-sphinx-theme @@ -93,6 +92,8 @@ soupsieve==2.6 # via beautifulsoup4 sphinx==8.1.3 # via pydata-sphinx-theme + # via sphinx-design +sphinx-design==0.6.1 sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 diff --git a/uv.lock b/uv.lock index 519b3c0a95..0e5b0edd34 100644 --- a/uv.lock +++ b/uv.lock @@ -242,13 +242,15 @@ dependencies = [ { name = "pydata-sphinx-theme" }, { name = "pyvortex" }, { name = "sphinx" }, + { name = "sphinx-design" }, ] [package.metadata] requires-dist = [ - { name = "pydata-sphinx-theme", specifier = ">=0.15.4" }, + { name = "pydata-sphinx-theme", specifier = ">=0.16.0" }, { name = "pyvortex" }, { name = "sphinx", specifier = ">=8.0.2" }, + { name = "sphinx-design", specifier = ">=0.6.1" }, ] [[package]] @@ -1189,6 +1191,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 }, ] +[[package]] +name = "sphinx-design" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/69/b34e0cb5336f09c6866d53b4a19d76c227cdec1bbc7ac4de63ca7d58c9c7/sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632", size = 2193689 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/43/65c0acbd8cc6f50195a3a1fc195c404988b15c67090e73c7a41a9f57d6bd/sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c", size = 2215338 }, +] + [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0"