|
1 | 1 | C++ API |
2 | 2 | ======= |
3 | 3 |
|
4 | | -The Vortex C++ API provides an idiomatic C++ wrapper around the Vortex C FFI, built using |
5 | | -`cxx <https://cxx.rs/>`_. It currently supports reading and writing Vortex files and integrates |
6 | | -with the Arrow C Data Interface via `nanoarrow <https://arrow.apache.org/nanoarrow/>`_. |
7 | | - |
8 | | -In the future we will expand the C++ API to cover Vortex's plugin and extension points. Please |
9 | | -reach out if you are interested in extending Vortex from C++ so we can prioritize these features. |
| 4 | +Vortex C++ API allows you to read and write `.vortex` files directly or via |
| 5 | +Arrow compatibility layer `nanoarrow <https://arrow.apache.org/nanoarrow/>`_. |
| 6 | +The only dependency apart from nanoarrow are Vortex C bindings. |
10 | 7 |
|
11 | 8 | .. note:: |
12 | | - Both the C++ API and this documentation are a work in progress. The API surface may change |
13 | | - significantly. Please reach out if you are interested in using Vortex from C++ so we can |
14 | | - prioritize stabilization. |
15 | | - |
| 9 | + C++ API is a work in progress. Please reach out to us if you are interested |
| 10 | + in using Vortex from C++ or you want a feature not covered yet e.g. |
| 11 | + extension DataType support. |
16 | 12 |
|
17 | 13 | Installation |
18 | 14 | ------------ |
19 | 15 |
|
20 | | -The C++ bindings are built using CMake. Requirements: |
| 16 | +We don't provide prebuilt library files (yet) so you will need to build Vortex |
| 17 | +from source, and for that you will need: |
| 18 | + |
| 19 | +- C++20, |
| 20 | +- Rust toolchain, |
| 21 | +- and CMake 3.10. |
| 22 | + |
| 23 | +``` |
| 24 | +git clone --depth 1 https://github.com/vortex-data/vortex |
| 25 | +cd vortex |
| 26 | +cargo build --release -p vortex-ffi |
| 27 | +cd vortex-cxx |
| 28 | +mkdir build |
| 29 | +cmake -Bbuild |
| 30 | +cmake --build build -j |
| 31 | +``` |
| 32 | + |
| 33 | +This will produce a shared and a static library in `vortex/target/release/` |
| 34 | +which you can use directly or via |
| 35 | + |
| 36 | +``` |
| 37 | +# static library |
| 38 | +target_link_libraries(target PRIVATE vortex_cxx nanoarrow) |
| 39 | +# shared library |
| 40 | +target_link_libraries(target PRIVATE vortex_cxx_shared nanoarrow_shared) |
| 41 | +``` |
| 42 | + |
| 43 | +Have a look at `examples <https://github.com/vortex-data/vortex/tree/develop/vortex-cxx/examples>`_ |
| 44 | +directory as well. |
| 45 | + |
| 46 | +Reading files |
| 47 | +------------- |
21 | 48 |
|
22 | | -* CMake 3.22 or higher |
23 | | -* C++20 compatible compiler |
24 | | -* Rust toolchain (for building the underlying Rust library) |
| 49 | +TODO provide sample files. |
25 | 50 |
|
26 | | -.. code-block:: bash |
| 51 | +Let's start with an example. Assuming you have Vortex files `people0`, `people1`, |
| 52 | +and `me` in a local folder, each containing two U64 columns "age" and "height", |
| 53 | +this is how you print all ages for specific heights: |
27 | 54 |
|
28 | | - cd vortex-cxx |
29 | | - mkdir build && cd build |
30 | | - cmake .. |
31 | | - make -j$(nproc) |
| 55 | +.. code-block:: cpp |
32 | 56 |
|
| 57 | + using namespace vortex; |
| 58 | + using namespace vortex::expr; |
| 59 | + using namespace vortex::expr::ops; |
| 60 | +
|
| 61 | + const Session session; |
| 62 | + const DataSource ds = DataSource::open(session, {"people*.vortex", "me.vortex"}); |
| 63 | + Scan scan = ds.scan({ |
| 64 | + .projection = col("age"), |
| 65 | + .filter = col("height") >= lit<uint64_t>(180) |
| 66 | + }); |
| 67 | +
|
| 68 | + for (Partition& partition : scan.partitions()) { |
| 69 | + for (Array& array : partition.batches()) { |
| 70 | + PrimitiveView<uint64_t> age_view = batch.field("age").values<uint64_t>(session); |
| 71 | + std::span<const uint64_t> age_values = age_view.values(); |
| 72 | + for (uint64_t value: age_values) { |
| 73 | + std::cout << value << " "; |
| 74 | + } |
| 75 | + } |
| 76 | + } |
33 | 77 |
|
34 | | -Compatibility |
35 | | -------------- |
| 78 | +DataSource and Scan |
| 79 | +^^^^^^^^^^^^^^^^^^^ |
| 80 | + |
| 81 | +First, you need to create a Vortex session which holds stuff like object store |
| 82 | +credentials and does extension bookkeeping (we'll get back to it later). Then |
| 83 | +you need to create a DataSource, which is a view over multiple files which may |
| 84 | +also be remote. You can specify globs for every item as it's shown. |
| 85 | + |
| 86 | +Once you have a DataSource, you can create Scans out of it. A Scan is a single |
| 87 | +traversal of a DataSource which projects columns and filters on them. Our Scan |
| 88 | +is consumed by following calls so it needs to be non-`const`. ScanOptions which |
| 89 | +are passed to Scan are a simple C++ aggregate so you can initialize any fields |
| 90 | +you want or avoid them altogether (`auto scan = ds.scan()`). Options have |
| 91 | +reasonable defaults: empty projection returns all fields, empty filter doesn't |
| 92 | +filter rows, and so on. |
| 93 | + |
| 94 | +Expressions |
| 95 | +^^^^^^^^^^^ |
| 96 | + |
| 97 | +We want to return only the "age" column so we pass it a `col` Expression - it |
| 98 | +gets the root struct of the file (a `DataTypeVariant::Struct`) and then |
| 99 | +extracts the column we want by name. A filter works in a similar manner - we |
| 100 | +return an Expression which returns false for some "height" values, and the scan |
| 101 | +filters them out. |
| 102 | + |
| 103 | +Two additional things to look in `.filter`: first, we allow overloading |
| 104 | +Expression operators which produce Expressions themselves a-la Eigen. This is |
| 105 | +opt-in via `using namespace vortex::expr::ops`. If you prefer, you can use |
| 106 | +`eq(col("height"), lit<uint64_t>(180))` instead. Second thing is that we |
| 107 | +explicitly pass the type to the `lit` expression, which creates a literal |
| 108 | +constant. We don't do any type coersion in Expression enging, so if you were to |
| 109 | +write `lit(180)`, C++ would likely deduce the type to `uint8_t` and fail in |
| 110 | +runtime. |
| 111 | + |
| 112 | +Once we're done with the scan, we need to consume the data it provides. If |
| 113 | +you want, you can get the data in Arrow format, but now we want our own |
| 114 | +Partitions and Arrays. |
| 115 | + |
| 116 | +Partitions and Arrays |
| 117 | +^^^^^^^^^^^^^^^^^^^^^ |
| 118 | + |
| 119 | +A Partition is an independent unit of work. Assuming your Vortex file |
| 120 | +processing will likely be multithreaded, each thread can pull Partitions from |
| 121 | +a Scan and handle them in parallel. For simplicity we don't cover it here, but |
| 122 | +look for the next sections! Each Partition produces Arrays which are batches of |
| 123 | +rows and columns. As we've requested "age", each Array will have one named |
| 124 | +column "age". |
| 125 | + |
| 126 | +.. note:: |
| 127 | + Arrays, DataTypes, and Expressions are reference-counted so copying them is |
| 128 | + cheap |
36 | 129 |
|
37 | | -The C++ bindings are supported on the following architectures: |
| 130 | +Once we have an Array, we can get access to its values. First, we need to |
| 131 | +extract "age" field. Then we want to get access to the raw bytes. However, |
| 132 | +Array likely references compressed data, so now we need to learn about |
| 133 | +canonicalization. |
38 | 134 |
|
39 | | -* x86_64 Linux |
40 | | -* ARM64 Linux |
41 | | -* Apple Silicon macOS |
| 135 | +Canonicalization |
| 136 | +^^^^^^^^^^^^^^^^ |
42 | 137 |
|
43 | | -They support any Linux distribution with a GLIBC version >= 2.31. This includes |
| 138 | +Vortex files hold layers of compressed data. Each layer is a specific encoding |
| 139 | +(zstd, FSST, delta) on top of another layer. This is good for performance |
| 140 | +because we defer decompression and we can also pass compressed data directly to |
| 141 | +other systems. Say, if a reader knows how to deal with bitpacked integers but |
| 142 | +not RLE and we have `RLE(Bitpacked(U64))`, we can decompress just RLE and pass |
| 143 | +bitpacked array directly to the reader. |
44 | 144 |
|
45 | | -* Amazon Linux 2022 or newer |
46 | | -* Ubuntu 20.04 or newer |
| 145 | +In this example we want to remove all encodings and uncompress all data fully. |
| 146 | +This form is called a canonical Array, and the process is canonicalization. |
47 | 147 |
|
| 148 | +When we request `.values(session)`, we canonicalize the array and get a |
| 149 | +PrimitiveView because we already know the type of the column. As PrimitiveView |
| 150 | +holds uncompressed data, we can get raw `uint64_t` numbers by calling |
| 151 | +`.values()` on the view. |
48 | 152 |
|
49 | | -Usage Example |
| 153 | +Writing files |
50 | 154 | ------------- |
51 | 155 |
|
52 | | -Here's a basic example of reading a Vortex file into an Arrow array stream: |
| 156 | +Now let's write the files to be read by our previous example. |
53 | 157 |
|
54 | 158 | .. code-block:: cpp |
| 159 | + // Assume you have imported the namespaces |
| 160 | + using dtype::Nullable; |
| 161 | +
|
| 162 | + const Session session; |
| 163 | + const DataType dtype = dtype::struct({ |
| 164 | + {"age", dtype::uint8()}, |
| 165 | + {"height", dtype::uint16(Nullable)}, |
| 166 | + }); |
| 167 | +
|
| 168 | + constexpr size_t SAMPLE_ROWS = 100; |
| 169 | + std::vector<uint8_t> ages(SAMPLE_ROWS); |
| 170 | + std::vector<uint16_t> heights(SAMPLE_ROWS); |
| 171 | + for (size_t i = 0; i < SAMPLE_ROWS; ++i) { |
| 172 | + ages[i] = static_cast<uint8_t>(i); |
| 173 | + heights[i] = static_cast<uint16_t>((i + 1) % 200); |
| 174 | + } |
55 | 175 |
|
56 | | - #include "vortex/file.hpp" |
57 | | - #include "vortex/scan.hpp" |
| 176 | + Array array = make_struct({ |
| 177 | + {"age", Array::primitive<uint8_t>(ages)}, |
| 178 | + {"height", Array::primitive<uint16_t>(heights, AllValid)}, |
| 179 | + }); |
58 | 180 |
|
59 | | - // Open a Vortex file and scan with a row limit |
60 | | - auto stream = vortex::VortexFile::Open("data.vortex") |
61 | | - .CreateScanBuilder() |
62 | | - .WithLimit(1000) |
63 | | - .IntoStream(); |
| 181 | + const Validity validity{ValidityType::Array, validity_array}; |
| 182 | + Array array2 = make_struct({ |
| 183 | + {"age", Array::primitive<uint8_t>(ages)}, |
| 184 | + {"height", Array::primitive<uint16_t>(heights, validity)}, |
| 185 | + }); |
64 | 186 |
|
65 | | - // Consume the Arrow C Data stream |
66 | | - ArrowArray array; |
67 | | - while (stream.get_next(&stream, &array) == 0 && array.release != nullptr) { |
68 | | - // Process each batch... |
69 | | - } |
| 187 | + FileWriter writer = FileWriter::open(session, "people0.vortex", dtype); |
| 188 | + writer.push(array); |
| 189 | + writer.push(array2); |
| 190 | + writer.finish(); |
70 | 191 |
|
| 192 | +DataType |
| 193 | +^^^^^^^^ |
| 194 | + |
| 195 | +First, you want to create a schema. DataType is a logical representation of |
| 196 | +Vortex's type system. "Logical" as opposed to "physical" means DataType doesn't |
| 197 | +know what encodings the data uses. As another example, Arrow and Parquet's types |
| 198 | +are physical. There's a convenience function `struct_` which creates a |
| 199 | +`DataTypeVariant::Struct` with fields passed as a `intializer_list`. |
| 200 | + |
| 201 | +Each DataType has a notion of nullability: whether some items in the row can be |
| 202 | +invalid (Vortex uses "null" and "invalid" terms interchangeably). DataTypes are |
| 203 | +non-nullable by default, so "age" column is not nullable, but "height" is. |
| 204 | + |
| 205 | +Then we prepare individual columns and use `make_struct` to assemble them into |
| 206 | +Array, and here we see Validity. |
| 207 | + |
| 208 | +Validity |
| 209 | +^^^^^^^^ |
| 210 | + |
| 211 | +DataType's Nullable flag tells us we may theoretically have invalid items in |
| 212 | +the Array, but to know this for sure we need Validity. "age" is non-Nullable |
| 213 | +so its Validity is NonNullable which means, again, we can't have invalid |
| 214 | +elements. |
| 215 | + |
| 216 | +"height" in first Array is AllValid which means in this Array we don't have |
| 217 | +invalid items. |
71 | 218 |
|
72 | | -API Reference |
73 | | -------------- |
74 | 219 |
|
75 | | -.. toctree:: |
76 | | - :maxdepth: 2 |
| 220 | +Multithreaded scans |
| 221 | +------------------- |
77 | 222 |
|
78 | | - dtypes |
79 | | - scalars |
80 | | - expr |
81 | | - file |
82 | | - scan |
| 223 | +Arrow roundtrip |
| 224 | +--------------- |
0 commit comments