diff --git a/.clang-format b/.clang-format
index 587f746da78..0dfb09d7dfe 100644
--- a/.clang-format
+++ b/.clang-format
@@ -7,7 +7,7 @@ SortIncludes: false
TabWidth: 4
IndentWidth: 4
ColumnLimit: 110
-AllowShortFunctionsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: true
---
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
@@ -25,7 +25,7 @@ DerivePointerAlignment: false
IncludeBlocks: Regroup
InsertBraces: true
Language: Cpp
-NamespaceIndentation: Inner
+NamespaceIndentation: None
PointerAlignment: Right
SpaceBeforeCpp11BracedList: true
SpaceBeforeCtorInitializerColon: true
diff --git a/vortex-cxx/.clang-tidy b/.clang-tidy
similarity index 95%
rename from vortex-cxx/.clang-tidy
rename to .clang-tidy
index 80c94310cbc..8b08b258113 100644
--- a/vortex-cxx/.clang-tidy
+++ b/.clang-tidy
@@ -1,5 +1,5 @@
-# SPDX-License-Identifier: Apache-2.0
-# SPDX-FileCopyrightText: Copyright the Vortex contributors
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright the Vortex contributors
Checks: |
-*,
@@ -43,7 +43,7 @@ Checks: |
-bugprone-unused-local-non-trivial-variable,
-bugprone-empty-catch,
WarningsAsErrors: '*'
-HeaderFilterRegex: '(cpp|examples)/.*\.(cpp|hpp)$'
+HeaderFilterRegex: '(examples)/.*\.(cpp|hpp)$'
FormatStyle: none
CheckOptions:
- key: readability-identifier-naming.ClassCase
@@ -90,4 +90,3 @@ CheckOptions:
value: lower_case
- key: cppcoreguidelines-rvalue-reference-param-not-moved.IgnoreUnnamedParams
value: true
-
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 23af8e2c5ff..56b62e333a6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -523,8 +523,8 @@ jobs:
command: check ${{ matrix.checks }}
cxx-test:
- name: "C++ build"
- timeout-minutes: 30
+ name: "C++ API tests"
+ timeout-minutes: 5
runs-on: >-
${{ github.repository == 'vortex-data/vortex'
&& format('runs-on={0}/runner=amd64-medium/image=ubuntu24-full-x64-pre-v2/extras=s3-cache/tag=cxx-build', github.run_id)
@@ -538,20 +538,42 @@ jobs:
- uses: ./.github/actions/setup-prebuild
with:
enable-sccache: "true"
- - name: Build and run C++ unit tests
+ - name: Install Rust nightly toolchain
+ run: |
+ rustup toolchain install $NIGHTLY_TOOLCHAIN
+ rustup component add --toolchain $NIGHTLY_TOOLCHAIN rust-src rustfmt clippy llvm-tools-preview
+ - name: Build FFI library (asan)
+ run: |
+ RUSTFLAGS="-A warnings -Cunsafe-allow-abi-mismatch=sanitizer \
+ -C debuginfo=2 -C opt-level=0 -C strip=none -Zexternal-clangrt \
+ -Zsanitizer=address,leak" \
+ cargo +$NIGHTLY_TOOLCHAIN build --locked --no-default-features \
+ --target x86_64-unknown-linux-gnu -Zbuild-std \
+ -p vortex-ffi
+ - name: Build C++ tests (asan)
run: |
mkdir -p vortex-cxx/build
- cmake -S vortex-cxx -B vortex-cxx/build -DVORTEX_ENABLE_TESTING=ON -DVORTEX_ENABLE_ASAN=ON
+ cmake -S vortex-cxx -B vortex-cxx/build -DBUILD_TESTS=ON -DSANITIZER=asan -DTARGET_TRIPLE="x86_64-unknown-linux-gnu"
cmake --build vortex-cxx/build --parallel $(nproc)
- ctest --test-dir vortex-cxx/build -j $(nproc) -V
- - name: Build and run the example in release mode
+ - name: Run C++ tests
+ run: |
+ vortex-cxx/build/tests/vortex_cxx_test
+ - name: Build and run examples
run: |
- cmake -S vortex-cxx/examples -B vortex-cxx/examples/build -DCMAKE_BUILD_TYPE=Release
+ cmake -S vortex-cxx -B vortex-cxx/examples/build -DSANITIZER=asan -DBUILD_EXAMPLES=1 -DTARGET_TRIPLE="x86_64-unknown-linux-gnu"
cmake --build vortex-cxx/examples/build --parallel $(nproc)
- vortex-cxx/examples/build/hello-vortex vortex-cxx/examples/goldenfiles/example.vortex
- - uses: ./.github/actions/check-rebuild
- with:
- command: "cargo build --profile ci --locked -p vortex-cxx --lib"
+ cd vortex-cxx/examples/build/examples
+ # filesystem loop detected
+ rm -fr ../_deps/nanoarrow-src/python/subprojects/arrow-nanoarrow
+
+ ./writer people0.vortex
+ ./writer people1.vortex
+ ./writer me.vortex
+
+ ./scan '*.vortex'
+ ./dtype me.vortex
+
+ ./reader
sqllogic-test:
name: "SQL logic tests"
diff --git a/Cargo.lock b/Cargo.lock
index 7a3b82f3dce..278ce65401d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1436,17 +1436,6 @@ version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
-[[package]]
-name = "codespan-reporting"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
-dependencies = [
- "serde",
- "termcolor",
- "unicode-width 0.2.2",
-]
-
[[package]]
name = "codspeed"
version = "5.0.1"
@@ -1998,68 +1987,6 @@ dependencies = [
"pin-project-lite",
]
-[[package]]
-name = "cxx"
-version = "1.0.196"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac6f8908ed0826ab0aec7c8358a4de3a9b26c5ed391a5dc5135765d2fd1aa8fb"
-dependencies = [
- "cc",
- "cxx-build",
- "cxxbridge-cmd",
- "cxxbridge-flags",
- "cxxbridge-macro",
- "foldhash 0.2.0",
- "link-cplusplus",
-]
-
-[[package]]
-name = "cxx-build"
-version = "1.0.196"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d8ae25c0ce72ac21a77b5deec4f49b452238ef97072f697dd6fd752b1355ecd"
-dependencies = [
- "cc",
- "codespan-reporting",
- "indexmap 2.14.0",
- "proc-macro2",
- "quote",
- "scratch",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "cxxbridge-cmd"
-version = "1.0.196"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27d53791812143bd27f74ab6055f22e875f04c59bbef0ca03d714ebec6f6f484"
-dependencies = [
- "clap",
- "codespan-reporting",
- "indexmap 2.14.0",
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "cxxbridge-flags"
-version = "1.0.196"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd557619f7dc2252bf7373f6bec5600ce60a2b477e5b3b84eb4e074bb297795e"
-
-[[package]]
-name = "cxxbridge-macro"
-version = "1.0.196"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fe465fc5b9d0231ea141c4fae714a08f4f907855e1a7ca10cc90ab6c8b12ece"
-dependencies = [
- "indexmap 2.14.0",
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
[[package]]
name = "darling"
version = "0.23.0"
@@ -5903,15 +5830,6 @@ dependencies = [
"bitflags 2.13.0",
]
-[[package]]
-name = "link-cplusplus"
-version = "1.0.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82"
-dependencies = [
- "cc",
-]
-
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -8259,12 +8177,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-[[package]]
-name = "scratch"
-version = "1.0.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2"
-
[[package]]
name = "scroll"
version = "0.11.0"
@@ -8990,12 +8902,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
-[[package]]
-name = "take_mut"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
-
[[package]]
name = "tap"
version = "1.0.1"
@@ -9041,15 +8947,6 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "termcolor"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
-dependencies = [
- "winapi-util",
-]
-
[[package]]
name = "terminal_size"
version = "0.4.4"
@@ -10114,21 +10011,6 @@ dependencies = [
"syn 2.0.118",
]
-[[package]]
-name = "vortex-cxx"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "arrow-array 58.3.0",
- "arrow-schema 58.3.0",
- "async-fs",
- "cxx",
- "futures",
- "paste",
- "take_mut",
- "vortex",
-]
-
[[package]]
name = "vortex-datafusion"
version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index e781ec222d1..543aa93b71b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,7 +32,6 @@ members = [
"vortex-cuda/gpu-scan-cli",
"vortex-cuda/macros",
"vortex-cuda/nvcomp",
- "vortex-cxx",
"vortex-ffi",
"fuzz",
"vortex-jni",
diff --git a/docs/api/cpp/dtypes.rst b/docs/api/cpp/dtypes.rst
deleted file mode 100644
index ae026af604d..00000000000
--- a/docs/api/cpp/dtypes.rst
+++ /dev/null
@@ -1,17 +0,0 @@
-Data Types
-==========
-
-Logical data types for Vortex arrays. ``DType`` represents a logical type, and ``PType``
-enumerates the physical primitive types.
-
-PType
------
-
-.. doxygenenum:: vortex::PType
-
-DType
------
-
-.. doxygennamespace:: vortex::dtype
- :members:
- :undoc-members:
diff --git a/docs/api/cpp/expr.rst b/docs/api/cpp/expr.rst
deleted file mode 100644
index 4e904fff558..00000000000
--- a/docs/api/cpp/expr.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-Expressions
-===========
-
-Expression trees used for filter and projection pushdowns in the scan API. Build expressions
-from columns, literals, and comparison operators.
-
-.. doxygennamespace:: vortex::expr
- :members:
- :undoc-members:
diff --git a/docs/api/cpp/file.rst b/docs/api/cpp/file.rst
deleted file mode 100644
index 26736a88979..00000000000
--- a/docs/api/cpp/file.rst
+++ /dev/null
@@ -1,17 +0,0 @@
-File I/O
-========
-
-Read and write Vortex files from C++. ``VortexFile`` opens an existing file for scanning, while
-``VortexWriteOptions`` writes an Arrow array stream to a new file.
-
-VortexFile
-----------
-
-.. doxygenclass:: vortex::VortexFile
- :members:
-
-VortexWriteOptions
-------------------
-
-.. doxygenclass:: vortex::VortexWriteOptions
- :members:
diff --git a/docs/api/cpp/index.rst b/docs/api/cpp/index.rst
index befb980f6a9..1e768b421aa 100644
--- a/docs/api/cpp/index.rst
+++ b/docs/api/cpp/index.rst
@@ -1,82 +1,260 @@
C++ API
=======
-The Vortex C++ API provides an idiomatic C++ wrapper around the Vortex C FFI, built using
-`cxx `_. It currently supports reading and writing Vortex files and integrates
-with the Arrow C Data Interface via `nanoarrow `_.
-
-In the future we will expand the C++ API to cover Vortex's plugin and extension points. Please
-reach out if you are interested in extending Vortex from C++ so we can prioritize these features.
+Vortex C++ API allows you to read and write `.vortex` files directly or via
+Arrow compatibility layer `nanoarrow `_.
+The only dependency apart from nanoarrow are Vortex C bindings.
.. note::
- Both the C++ API and this documentation are a work in progress. The API surface may change
- significantly. Please reach out if you are interested in using Vortex from C++ so we can
- prioritize stabilization.
-
+ C++ API is a work in progress. Please reach out to us if you are interested
+ in using Vortex from C++ or you want a feature not covered yet e.g.
+ extension DataType support.
Installation
------------
-The C++ bindings are built using CMake. Requirements:
+We don't provide prebuilt library files (yet) so you will need to build Vortex
+from source, and for that you will need:
-* CMake 3.22 or higher
-* C++20 compatible compiler
-* Rust toolchain (for building the underlying Rust library)
+- C++20,
+- Rust toolchain,
+- and CMake 3.10.
.. code-block:: bash
+ git clone --depth 1 https://github.com/vortex-data/vortex
+ cd vortex
+ cargo build --release -p vortex-ffi
cd vortex-cxx
- mkdir build && cd build
- cmake ..
- make -j$(nproc)
+ mkdir build
+ cmake -Bbuild -DCMAKE_BUILD_TYPE=Release
+ cmake --build build -j
+
+This will produce a shared and a static library in `vortex/target/release/`
+which you can use directly or via
+
+.. code-block:: bash
+
+ # static library
+ target_link_libraries(target PRIVATE vortex_cxx nanoarrow)
+ # shared library
+ target_link_libraries(target PRIVATE vortex_cxx_shared nanoarrow_shared)
+Have a look at `examples `_
+directory as well.
-Compatibility
+Reading files
-------------
-The C++ bindings are supported on the following architectures:
+Full source code for this example is `reader.cpp
+`_.
+For brevity we omit `main()`, system includes and some `using namespace`
+directives.
+
+Assuming you have Vortex files `people0`, `people1`, and `me` in a local folder,
+each containing U8 column "age" and U16 column "height", this is how you
+print all ages for specific heights:
+
+.. code-block:: cpp
+
+ using namespace vortex::expr::ops;
+
+ const Session session;
+ const DataSource ds = DataSource::open(session, {"people*.vortex", "me.vortex"});
+ Scan scan = ds.scan({.filter = col("height") >= lit(50)});
+
+ for (Partition &partition : scan.partitions()) {
+ for (Array &array : partition.batches()) {
+ const Array age = array.field("age");
+ const PrimitiveView age_view = age.values(session);
+ const std::span age_values = age_view.values();
+ for (uint8_t value : age_values) {
+ std::cout << int(value) << " ";
+ }
+ }
+ }
+ std::cout << "\n";
+
+DataSource and Scan
+^^^^^^^^^^^^^^^^^^^
+
+First, you need to create a Vortex session which holds stuff like object store
+credentials and does extension bookkeeping (we'll get back to it later). Then
+you need to create a DataSource, which is a view over multiple files which may
+also be remote. You can specify globs for every item as it's shown.
+
+Once you have a DataSource, you can create Scans out of it. A Scan is a single
+traversal of a DataSource which projects columns and filters on them. Our Scan
+is consumed by following calls so it needs to be non-`const`. ScanOptions which
+are passed to Scan are a simple C++ aggregate so you can initialize any fields
+you want or avoid them altogether (`auto scan = ds.scan()`). Options have
+reasonable defaults: empty projection returns all fields, empty filter doesn't
+filter rows, and so on.
+
+Expressions
+^^^^^^^^^^^
+
+We want to return both column so we omit `.projection`. If we'd want to return
+only the "age" column we'd write `.projection = col("age")`. We pass an
+Expression to a filter which returns false for some "height" values, and the
+scan filters them out.
+
+Two additional things to look for in `.filter`: first, we allow overloading
+Expression operators which produce Expressions themselves a-la Eigen. This is
+opt-in via `using namespace vortex::expr::ops`. If you prefer, you can use
+`eq(col("height"), lit(180))` instead. Second thing is that we
+explicitly pass the type to the `lit` expression, which creates a literal
+constant. We don't do any type coercion in Vortex so if you were to write
+`lit(180)`, C++ would likely deduce the type to `uint8_t` and fail in runtime.
+
+Once we're done with the scan, we need to consume the data it provides. If
+you want, you can get the data in Arrow format, but now we want our own
+Partitions and Arrays.
+
+Partitions and Arrays
+^^^^^^^^^^^^^^^^^^^^^
+
+A Partition is an independent unit of work. Assuming your Vortex file
+processing will likely be multithreaded, each thread can pull Partitions from
+a Scan and handle them in parallel. For simplicity we don't cover it here, but
+look for the next sections! Each Partition produces Arrays which are batches of
+rows and columns. As we've requested "age", each Array will have one named
+column "age".
-* x86_64 Linux
-* ARM64 Linux
-* Apple Silicon macOS
+.. note::
+ Arrays, DataTypes, and Expressions are reference-counted so copying them is
+ cheap.
+
+Once we have an Array, we can get access to its values. First, we need to
+extract "age" field.
+
+.. note::
+ Default projection returns the root Array which is a Struct so to get
+ a field from it we need to use `field()`. If we were to use `col()` in
+ projection, the array we got would be "age" column already, so we wouldn't
+ need to use `field()`.
+
+
+Then we want to get access to the raw bytes. However,
+Array likely references compressed data, so now we need to learn about
+canonicalization.
-They support any Linux distribution with a GLIBC version >= 2.31. This includes
+Canonicalization
+^^^^^^^^^^^^^^^^
-* Amazon Linux 2022 or newer
-* Ubuntu 20.04 or newer
+Vortex files hold layers of compressed data. Each layer is a specific encoding
+(zstd, FSST, delta) on top of another layer. This is good for performance
+because we defer decompression and we can also pass compressed data directly to
+other systems. Say, if a reader knows how to deal with bitpacked integers but
+not RLE and we have `RLE(Bitpacked(U64))`, we can decompress just RLE and pass
+bitpacked array directly to the reader.
+In this example we want to remove all encodings and uncompress all data fully.
+This form is called a canonical Array, and the process is canonicalization.
-Usage Example
+When we request `.values(session)`, we canonicalize the Array and get a
+PrimitiveView because we already know the type of the column. As PrimitiveView
+holds uncompressed data, we can get raw `uint64_t` numbers by calling
+`.values()` on the view.
+
+Writing files
-------------
-Here's a basic example of reading a Vortex file into an Arrow array stream:
+Now let's write the first files to be read by our previous example.
+Source code for this example is `writer.cpp
+`_.
.. code-block:: cpp
- #include "vortex/file.hpp"
- #include "vortex/scan.hpp"
+ using dtype::Nullable;
- // Open a Vortex file and scan with a row limit
- auto stream = vortex::VortexFile::Open("data.vortex")
- .CreateScanBuilder()
- .WithLimit(1000)
- .IntoStream();
+ const Session session;
+ const DataType dtype = dtype::struct_({
+ {"age", dtype::uint8()},
+ {"height", dtype::uint16(Nullable)},
+ });
- // Consume the Arrow C Data stream
- ArrowArray array;
- while (stream.get_next(&stream, &array) == 0 && array.release != nullptr) {
- // Process each batch...
+ constexpr size_t SAMPLE_ROWS = 100;
+ std::vector age_buffer(SAMPLE_ROWS);
+ std::vector height_buffer(SAMPLE_ROWS);
+ for (size_t i = 0; i < SAMPLE_ROWS; ++i) {
+ age_buffer[i] = static_cast(i);
+ height_buffer[i] = static_cast((i + 1) % 200);
}
+ Array age = Array::primitive(age_buffer);
+ Array array = make_struct({
+ {"age", age},
+ {"height", Array::primitive(height_buffer, AllValid)},
+ });
-API Reference
--------------
+ Expression age_gt_10 = expr::col("age") > expr::lit(10);
+ Array validity_array = array.apply(age_gt_10);
+
+ const Validity validity {ValidityType::Array, validity_array};
+ Array array2 = make_struct({
+ {"age", age},
+ {"height", Array::primitive(height_buffer, validity)},
+ });
+
+ Writer writer = Writer::open(session, argv[1], dtype);
+ writer.push(array);
+ writer.push(array2);
+ writer.finish();
+
+DataType
+^^^^^^^^
+
+First, you want to create a schema. DataType is a logical representation of
+Vortex's type system. "Logical" as opposed to "physical" means DataType doesn't
+know what encodings the data use. As another example, Arrow and Parquet's types
+are physical. There's a convenience function `struct_` which creates a
+`DataTypeVariant::Struct` with fields passed as a `initializer_list`.
+
+Each DataType has a notion of nullability: whether some items in the row can be
+invalid (Vortex uses "null" and "invalid" terms interchangeably). DataTypes are
+non-nullable by default, so "age" column is not nullable, but "height" is.
+
+Then we prepare individual columns and use `make_struct` to assemble them into
+Array, and here we see Validity.
+
+Validity
+^^^^^^^^
+
+DataType's Nullable flag tells us we may theoretically have invalid items in
+the Array, but to know this for sure we need Validity. "age" is non-Nullable
+so its Validity is NonNullable which means, again, we can't have invalid
+elements.
+
+"height" in first Array is AllValid which means in this Array we don't have
+invalid items. For the second Array we reuse age by copying it (which is a
+reference clone so it's cheap). But for "height" we say Validity is determined
+by another Array. This array consists of Bools and we obtained it by applying a
+comparison Expression to the "age" column of the first array. So, "height" field
+in `array2` will have null/invalid values if "age"'s item at same index was
+less than 11.
+
+Applying Expressions
+^^^^^^^^^^^^^^^^^^^^
+
+One important thing to know is that applying an Expression is a constant-time
+operation, and the returned array contains the original values along with some
+meta-information about the expression. Real application happens when you execute
+the array. One example of executing the array is canonicalization, which in this
+example happens later when we write both arrays to the file.
+
+Writing to a file
+^^^^^^^^^^^^^^^^^
+
+Once we're done with the arrays, we need to initialize the Writer and push
+arrays into it. `finish()` call writes the file footer. If you don't call it and
+let Writer go out of scope, the file will be visibly corrupted.
+
+Now you can build the example and read back the generated files:
-.. toctree::
- :maxdepth: 2
+.. code-block::
- dtypes
- scalars
- expr
- file
- scan
+ ./build/examples/writer people0.vortex
+ ./build/examples/writer people1.vortex
+ ./build/examples/writer me.vortex
+ ./build/examples/reader
diff --git a/docs/api/cpp/scalars.rst b/docs/api/cpp/scalars.rst
deleted file mode 100644
index ab52cc22af0..00000000000
--- a/docs/api/cpp/scalars.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-Scalars
-=======
-
-A scalar is a single typed value. Factory functions create scalars of each primitive type, and
-``cast`` converts between types.
-
-.. doxygennamespace:: vortex::scalar
- :members:
- :undoc-members:
diff --git a/docs/api/cpp/scan.rst b/docs/api/cpp/scan.rst
deleted file mode 100644
index dbbd9e3bd36..00000000000
--- a/docs/api/cpp/scan.rst
+++ /dev/null
@@ -1,18 +0,0 @@
-Scanning
-========
-
-The scan API provides a builder pattern for reading data from a Vortex file with optional
-filter, projection, row range, and limit pushdowns. The resulting stream exposes the
-Arrow C Data Interface (``ArrowArrayStream``).
-
-ScanBuilder
------------
-
-.. doxygenclass:: vortex::ScanBuilder
- :members:
-
-StreamDriver
-------------
-
-.. doxygenclass:: vortex::StreamDriver
- :members:
diff --git a/vortex-cuda/kernels/src/for.cu b/vortex-cuda/kernels/src/for.cu
index 702a0bfdf7b..eb4f123b46c 100644
--- a/vortex-cuda/kernels/src/for.cu
+++ b/vortex-cuda/kernels/src/for.cu
@@ -9,9 +9,7 @@ template
struct ForOp {
T reference;
- __device__ inline T operator()(T value) const {
- return value + reference;
- }
+ __device__ inline T operator()(T value) const { return value + reference; }
};
// Macro to generate in-place FoR kernel for each type.
diff --git a/vortex-cxx/CMakeLists.txt b/vortex-cxx/CMakeLists.txt
index ab5474d0617..9a477d34685 100644
--- a/vortex-cxx/CMakeLists.txt
+++ b/vortex-cxx/CMakeLists.txt
@@ -1,16 +1,16 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright the Vortex contributors
-cmake_minimum_required(VERSION 3.22)
+cmake_minimum_required(VERSION 3.10)
+project(VortexCXX
+ VERSION 0.0.1
+ LANGUAGES CXX)
-# FetchContent from URL timestamp handling
-cmake_policy(SET CMP0135 NEW)
-
-project(vortex)
+include(FetchContent)
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
-set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_program(SCCACHE_PROGRAM sccache)
if (SCCACHE_PROGRAM)
@@ -21,100 +21,125 @@ else ()
message(STATUS "Sccache not found")
endif ()
-if (NOT CMAKE_BUILD_TYPE)
- set(CMAKE_BUILD_TYPE Debug)
-endif()
+# GCC's -Wmissing-field-initializers is a false positive on ScanOptions
+#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror -Wpedantic -Wno-missing-field-initializers")
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic -Wno-dollar-in-identifier-extension")
+option(BUILD_TESTS "Build tests" OFF)
+option(BUILD_EXAMPLES "Build examples" OFF)
-option(VORTEX_ENABLE_TESTING "Enable building test binary for vortex-cxx" OFF)
-option(VORTEX_ENABLE_ASAN "Enable address sanitizer" OFF)
-
-include(FetchContent)
-FetchContent_Declare(
- Corrosion
- GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
- GIT_TAG v0.5
-)
-FetchContent_MakeAvailable(Corrosion)
+set(SANITIZER "" CACHE STRING "Build with sanitizers")
+set(TARGET_TRIPLE "" CACHE STRING "Rust target triple for FFI library")
+set(RUST_BUILD_PROFILE "" CACHE STRING "Cargo profile name for Rust FFI library")
-set(RUST_SOURCE_FILE lib.rs)
+if (NOT CMAKE_BUILD_TYPE)
+ set(CMAKE_BUILD_TYPE Debug)
+endif()
-# Import Rust crate using Corrosion
-corrosion_import_crate(
- MANIFEST_PATH ${CMAKE_CURRENT_SOURCE_DIR}/Cargo.toml
- FEATURES ${CORROSION_FEATURES}
- CRATES vortex-cxx
-)
+if (NOT SANITIZER STREQUAL "")
+ message(NOTICE "Sanitizer: ${SANITIZER}")
+ if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
+ message(FATAL_ERROR "Only debug build is supported for sanitizer builds")
+ endif()
-# Enable CXX bridge for the Rust crate
-corrosion_add_cxxbridge(vortex_cxx_bridge CRATE vortex_cxx FILES ${RUST_SOURCE_FILE})
+ if (SANITIZER STREQUAL "asan")
+ set(SANITIZER_FLAGS "-fsanitize=address,undefined,leak")
+ elseif (SANITIZER STREQUAL "tsan")
+ set(SANITIZER_FLAGS "-fsanitize=thread")
+ else()
+ message(FATAL_ERROR "Unknown sanitizer ${SANITIZER}")
+ endif()
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SANITIZER_FLAGS}")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SANITIZER_FLAGS}")
+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${SANITIZER_FLAGS}")
+endif()
-FetchContent_Declare(
- nanoarrow
- GIT_REPOSITORY https://github.com/apache/arrow-nanoarrow.git
- GIT_TAG a579fbf5d192e85b6249935e117de7d02a6dc4e9 # v0.8.0
-)
-FetchContent_MakeAvailable(nanoarrow)
+if (RUST_BUILD_PROFILE STREQUAL "")
+ if (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
+ set(RUST_BUILD_PROFILE "release_debug")
+ else()
+ string(TOLOWER "${CMAKE_BUILD_TYPE}" RUST_BUILD_PROFILE)
+ endif()
+endif()
-file(GLOB_RECURSE CPP_SOURCE_FILE CONFIGURE_DEPENDS
- "${CMAKE_CURRENT_SOURCE_DIR}/cpp/src/*.cpp"
-)
+set(VORTEX_FFI_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../vortex-ffi")
+set(VORTEX_FFI_LIB_DIR "${VORTEX_FFI_DIR}/../target/${TARGET_TRIPLE}/${RUST_BUILD_PROFILE}")
+set(VORTEX_FFI_HEADERS "${VORTEX_FFI_DIR}/cinclude")
+
+if(APPLE)
+ set(VORTEX_FFI_STATIC "${VORTEX_FFI_LIB_DIR}/libvortex_ffi.a")
+ set(VORTEX_FFI_SHARED "${VORTEX_FFI_LIB_DIR}/libvortex_ffi.dylib")
+elseif(WIN32)
+ set(VORTEX_FFI_STATIC "${VORTEX_FFI_LIB_DIR}/libvortex_ffi.lib")
+ set(VORTEX_FFI_SHARED "${VORTEX_FFI_LIB_DIR}/libvortex_ffi.dll")
+else()
+ set(VORTEX_FFI_STATIC "${VORTEX_FFI_LIB_DIR}/libvortex_ffi.a")
+ set(VORTEX_FFI_SHARED "${VORTEX_FFI_LIB_DIR}/libvortex_ffi.so")
+endif()
-# Public headers
-set(CPP_INCLUDE_DIRS
- ${CMAKE_CURRENT_SOURCE_DIR}/cpp/include
-)
+if(NOT EXISTS "${VORTEX_FFI_STATIC}")
+ message(FATAL_ERROR
+ "vortex-ffi static library not found at ${VORTEX_FFI_STATIC}. "
+ "Run: cargo build --profile -p vortex-ffi")
+endif()
-# Private headers
-set(CPP_PRIVATE_INCLUDE_DIRS
- ${CMAKE_CURRENT_SOURCE_DIR}/cpp/src
-)
+add_library(vortex_ffi STATIC IMPORTED)
+set_target_properties(vortex_ffi PROPERTIES
+ IMPORTED_LOCATION "${VORTEX_FFI_STATIC}"
+ INTERFACE_INCLUDE_DIRECTORIES "${VORTEX_FFI_HEADERS}")
+
+if(EXISTS "${VORTEX_FFI_SHARED}")
+ add_library(vortex_ffi_shared SHARED IMPORTED)
+ set_target_properties(vortex_ffi_shared PROPERTIES
+ IMPORTED_LOCATION "${VORTEX_FFI_SHARED}"
+ INTERFACE_INCLUDE_DIRECTORIES "${VORTEX_FFI_HEADERS}"
+ INTERFACE_LINK_OPTIONS "LINKER:-rpath,${VORTEX_FFI_LIB_DIR}")
+endif()
-# Create the main library combining C++ and Rust code
-add_library(vortex STATIC ${CPP_SOURCE_FILE})
-target_include_directories(vortex PUBLIC ${CPP_INCLUDE_DIRS}
- ${CMAKE_CURRENT_BINARY_DIR}/corrosion_generated/cxxbridge/vortex_cxx_bridge/include)
-target_include_directories(vortex PRIVATE
- ${CPP_PRIVATE_INCLUDE_DIRS}
-)
-target_link_libraries(vortex
- PUBLIC nanoarrow_static
- PRIVATE vortex_cxx_bridge
+file(GLOB VORTEX_CXX_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
+add_library(vortex_cxx STATIC ${VORTEX_CXX_SOURCES})
+target_include_directories(vortex_cxx PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_SOURCE_DIR}/include
+ ${CMAKE_CURRENT_SOURCE_DIR}/../vortex-ffi/cinclude
)
+target_link_libraries(vortex_cxx PUBLIC vortex_ffi)
+add_library(vortex::cxx ALIAS vortex_cxx)
+
+if(TARGET vortex_ffi_shared)
+ add_library(vortex_cxx_shared SHARED ${VORTEX_CXX_SOURCES})
+ set_target_properties(vortex_cxx_shared PROPERTIES POSITION_INDEPENDENT_CODE ON)
+ target_include_directories(vortex_cxx_shared PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_SOURCE_DIR}/include
+ ${CMAKE_CURRENT_SOURCE_DIR}/../vortex-ffi/cinclude
+ )
+ target_link_libraries(vortex_cxx_shared PUBLIC vortex_ffi_shared)
+ add_library(vortex::cxx_shared ALIAS vortex_cxx_shared)
+endif()
-if (VORTEX_ENABLE_ASAN)
- target_compile_options(vortex PRIVATE -fsanitize=leak,address,undefined -fno-omit-frame-pointer -fno-common -O1)
- target_link_options(vortex PRIVATE -fsanitize=leak,address,undefined)
+if (BUILD_TESTS OR BUILD_EXAMPLES)
+ FetchContent_Declare(
+ Nanoarrow
+ GIT_REPOSITORY https://github.com/apache/arrow-nanoarrow
+ GIT_TAG apache-arrow-nanoarrow-0.8.0
+ )
+ FetchContent_MakeAvailable(Nanoarrow)
endif()
-# Tests
-if (VORTEX_ENABLE_TESTING)
+if (BUILD_TESTS)
FetchContent_Declare(
- googletest
- GIT_REPOSITORY https://github.com/google/googletest.git
- GIT_TAG v1.17.0
- GIT_SHALLOW TRUE
+ Catch
+ GIT_REPOSITORY https://github.com/catchorg/Catch2.git
+ GIT_TAG v3.8.1
)
- FetchContent_MakeAvailable(googletest)
+ FetchContent_MakeAvailable(Catch)
+ include(Catch)
+ target_compile_definitions(Catch2 PRIVATE CATCH_CONFIG_NO_POSIX_SIGNALS)
enable_testing()
- add_executable(vortex_cxx_test cpp/tests/basic_test.cpp cpp/tests/test_data_generator.cpp)
- target_include_directories(vortex_cxx_test PUBLIC ${CPP_INCLUDE_DIRS})
- target_include_directories(vortex_cxx_test PRIVATE cpp/tests)
- target_link_libraries(vortex_cxx_test PRIVATE gtest_main vortex nanoarrow_static)
- target_include_directories(vortex_cxx_test PRIVATE
- ${CMAKE_CURRENT_BINARY_DIR}/corrosion_generated/cxxbridge/vortex_cxx_bridge/include
- )
- # Platform-specific configuration
- if(APPLE)
- set(APPLE_LINK_FLAGS "-framework CoreFoundation -framework Security")
- endif()
- target_link_libraries(vortex_cxx_test PRIVATE vortex_cxx_bridge ${APPLE_LINK_FLAGS})
- if (VORTEX_ENABLE_ASAN)
- target_compile_options(vortex_cxx_test PRIVATE -fsanitize=leak,address,undefined -fno-omit-frame-pointer -fno-common -O1)
- target_link_options(vortex_cxx_test PRIVATE -fsanitize=leak,address,undefined)
- endif()
- include(GoogleTest)
- gtest_discover_tests(vortex_cxx_test)
+ add_subdirectory(tests)
+endif()
+
+if (BUILD_EXAMPLES)
+ add_subdirectory(examples)
endif()
diff --git a/vortex-cxx/Cargo.toml b/vortex-cxx/Cargo.toml
deleted file mode 100644
index d75f7b74cee..00000000000
--- a/vortex-cxx/Cargo.toml
+++ /dev/null
@@ -1,32 +0,0 @@
-[package]
-name = "vortex-cxx"
-authors = { workspace = true }
-categories = { workspace = true }
-description = "C++ bindings for Vortex generated from Rust using cxx"
-edition = { workspace = true }
-homepage = { workspace = true }
-include = { workspace = true }
-keywords = { workspace = true }
-license = { workspace = true }
-publish = false
-readme = { workspace = true }
-repository = { workspace = true }
-rust-version = { workspace = true }
-version = { workspace = true }
-
-[lints]
-workspace = true
-
-[lib]
-crate-type = ["staticlib"]
-
-[dependencies]
-anyhow = { workspace = true }
-arrow-array = { workspace = true, features = ["ffi"] }
-arrow-schema = { workspace = true }
-async-fs = { workspace = true }
-cxx = "1.0"
-futures = { workspace = true }
-paste = { workspace = true }
-take_mut = { workspace = true }
-vortex = { workspace = true }
diff --git a/vortex-cxx/README.md b/vortex-cxx/README.md
index aa761f98b1a..a0c4b5a847f 100644
--- a/vortex-cxx/README.md
+++ b/vortex-cxx/README.md
@@ -1,35 +1,37 @@
-# Vortex C++ Bindings
+# Vortex C++ bindings
-This directory contains C++ bindings for Vortex using the [cxx](https://cxx.rs/) crate. The bindings provide a C++ interface to Vortex file operations, including roundtripping with Arrow Array stream with advanced pushdown support.
+For a usage guide, see docs/api/cpp/index.rst.
-## Building
+## Requirements
-### Requirements
+- CMake 3.10+
+- C++20 compiler
+- Rust toolchain for building `vortex-ffi`.
-- CMake 3.22 or higher
-- C++20 compatible compiler
-- Rust toolchain (for building the Rust components)
-- (optional) Ninja (`ninja-build`)
+## Build
-### Build Steps
-
-```bash
-mkdir build
-cmake -Bbuild -GNinja
+```sh
+cargo build --release -p vortex-ffi
+cmake -Bbuild -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
```
-### Running Tests
+This will generate `libvortex_cxx` shared and static libraries.
+You can use `vortex_cxx` and `vortex_cxx_shared` CMake targets.
+
+## Test
-```bash
-# Enable tests in CMake
-cmake -Bbuild -DVORTEX_ENABLE_TESTING=ON -GNinja
+```sh
+cargo build --release -p vortex-ffi
+cmake -Bbuild -DBUILD_TESTS=ON
cmake --build build -j
-./vortex_cxx_test
+ctest --test-dir build -j "$(nproc)"
```
-## C++ Coding Convention
-
-We use `.clang-tidy` and `.clang-format` to setup the coding convention. Both are borrowed from DuckDB.
+## Run examples
-`cppcoreguidelines-avoid-non-const-global-variables` is removed from `.clang-tidy` because GTest violates it.
+```sh
+cmake -Bbuild -DBUILD_EXAMPLES=ON
+cmake --build build -j
+./build/examples/hello-vortex
+```
diff --git a/vortex-cxx/cpp/include/vortex.hpp b/vortex-cxx/cpp/include/vortex.hpp
deleted file mode 100644
index 1f2dd1625c1..00000000000
--- a/vortex-cxx/cpp/include/vortex.hpp
+++ /dev/null
@@ -1,11 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#pragma once
-
-#include "vortex/file.hpp"
-#include "vortex/scan.hpp"
-#include "vortex/write_options.hpp"
-#include "vortex/exception.hpp"
-#include "vortex/expr.hpp"
-#include "vortex/scalar.hpp"
\ No newline at end of file
diff --git a/vortex-cxx/cpp/include/vortex/dtype.hpp b/vortex-cxx/cpp/include/vortex/dtype.hpp
deleted file mode 100644
index 9bf4a16d9bc..00000000000
--- a/vortex-cxx/cpp/include/vortex/dtype.hpp
+++ /dev/null
@@ -1,71 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#pragma once
-
-#include
-#include
-#include "vortex_cxx_bridge/lib.h"
-
-namespace vortex {
-
-enum class PType : uint8_t {
- U8 = 0,
- U16,
- U32,
- U64,
- I8,
- I16,
- I32,
- I64,
- F16,
- F32,
- F64,
-};
-
-namespace dtype {
- class DType {
- public:
- DType() = delete;
- explicit DType(rust::Box impl) : impl_(std::move(impl)) {
- }
- DType(DType &&other) noexcept = default;
- DType &operator=(DType &&other) = default;
- ~DType() = default;
-
- DType(const DType &) = delete;
- DType &operator=(const DType &) = delete;
-
- std::string ToString() const;
-
- const rust::Box &GetImpl() {
- return impl_;
- }
-
- private:
- rust::Box impl_;
- };
-
- // Factory functions
- DType null();
- DType bool_(bool nullable = false);
- DType primitive(PType ptype, bool nullable = false);
- DType int8(bool nullable = false);
- DType int16(bool nullable = false);
- DType int32(bool nullable = false);
- DType int64(bool nullable = false);
- DType uint8(bool nullable = false);
- DType uint16(bool nullable = false);
- DType uint32(bool nullable = false);
- DType uint64(bool nullable = false);
- DType float16(bool nullable = false);
- DType float32(bool nullable = false);
- DType float64(bool nullable = false);
- DType decimal(uint8_t precision = 10, int8_t scale = 0, bool nullable = false);
- DType utf8(bool nullable = false);
- DType binary(bool nullable = false);
- /// TODO: Other DTypes are only supported by creating from Arrow for now.
- DType from_arrow(struct ArrowSchema &schema, bool non_nullable = false);
-} // namespace dtype
-
-} // namespace vortex
\ No newline at end of file
diff --git a/vortex-cxx/cpp/include/vortex/exception.hpp b/vortex-cxx/cpp/include/vortex/exception.hpp
deleted file mode 100644
index a7fe44fb91c..00000000000
--- a/vortex-cxx/cpp/include/vortex/exception.hpp
+++ /dev/null
@@ -1,18 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#pragma once
-
-#include
-#include
-
-namespace vortex {
-
-/// TODO(xinyu): better error handling
-class VortexException : public std::runtime_error {
-public:
- explicit VortexException(const std::string &message) : std::runtime_error(message) {
- }
-};
-
-} // namespace vortex
\ No newline at end of file
diff --git a/vortex-cxx/cpp/include/vortex/expr.hpp b/vortex-cxx/cpp/include/vortex/expr.hpp
deleted file mode 100644
index 3060a8e3536..00000000000
--- a/vortex-cxx/cpp/include/vortex/expr.hpp
+++ /dev/null
@@ -1,51 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#pragma once
-
-#include "vortex/scalar.hpp"
-#include "vortex_cxx_bridge/lib.h"
-#include
-
-namespace vortex::expr {
-class Expr {
-public:
- Expr() = delete;
- explicit Expr(rust::Box impl) : impl_(std::move(impl)) {
- }
- Expr(Expr &&other) noexcept = default;
- Expr &operator=(Expr &&other) noexcept = default;
- ~Expr() = default;
-
- Expr(const Expr &) = delete;
- Expr &operator=(const Expr &) = delete;
-
- rust::Box IntoImpl() && {
- return std::move(impl_);
- }
-
- const ffi::Expr &Impl() const & {
- return *impl_;
- }
-
-private:
- rust::Box impl_;
-};
-
-Expr literal(scalar::Scalar scalar);
-Expr root();
-Expr column(std::string_view name);
-Expr get_item(std::string_view field, Expr expr);
-Expr not_(Expr expr);
-Expr is_null(Expr expr);
-Expr eq(Expr lhs, Expr rhs);
-Expr not_eq_(Expr lhs, Expr rhs);
-Expr gt(Expr lhs, Expr rhs);
-Expr gt_eq(Expr lhs, Expr rhs);
-Expr lt(Expr lhs, Expr rhs);
-Expr lt_eq(Expr lhs, Expr rhs);
-Expr and_(Expr lhs, Expr rhs);
-Expr or_(Expr lhs, Expr rhs);
-Expr checked_add(Expr lhs, Expr rhs);
-Expr select(const std::vector &fields, Expr child);
-} // namespace vortex::expr
\ No newline at end of file
diff --git a/vortex-cxx/cpp/include/vortex/file.hpp b/vortex-cxx/cpp/include/vortex/file.hpp
deleted file mode 100644
index c7b93e326d8..00000000000
--- a/vortex-cxx/cpp/include/vortex/file.hpp
+++ /dev/null
@@ -1,38 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#pragma once
-
-#include
-#include "vortex_cxx_bridge/lib.h"
-
-namespace vortex {
-class ScanBuilder;
-
-class VortexFile {
-public:
- static VortexFile Open(const std::string &path);
- static VortexFile Open(const uint8_t *data, size_t length);
-
- VortexFile(VortexFile &&other) noexcept = default;
- VortexFile &operator=(VortexFile &&other) noexcept = default;
- ~VortexFile() = default;
-
- VortexFile(const VortexFile &) = delete;
- VortexFile &operator=(const VortexFile &) = delete;
-
- /// Get the number of rows in the file.
- uint64_t RowCount() const;
-
- /// Create a scan builder for the file.
- /// The scan builder can be used to scan the file.
- ScanBuilder CreateScanBuilder() const;
-
-private:
- explicit VortexFile(rust::Box impl) : impl_(std::move(impl)) {
- }
-
- rust::Box impl_;
-};
-
-} // namespace vortex
\ No newline at end of file
diff --git a/vortex-cxx/cpp/include/vortex/scalar.hpp b/vortex-cxx/cpp/include/vortex/scalar.hpp
deleted file mode 100644
index 77706aef98a..00000000000
--- a/vortex-cxx/cpp/include/vortex/scalar.hpp
+++ /dev/null
@@ -1,47 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#pragma once
-
-#include
-#include "dtype.hpp"
-#include "vortex_cxx_bridge/lib.h"
-
-namespace vortex::scalar {
-class Scalar {
-public:
- Scalar() = delete;
- explicit Scalar(rust::Box impl) : impl_(std::move(impl)) {
- }
- Scalar(Scalar &&other) noexcept = default;
- Scalar &operator=(Scalar &&other) noexcept = default;
- ~Scalar() = default;
-
- Scalar(const Scalar &) = delete;
- Scalar &operator=(const Scalar &) = delete;
-
- rust::Box IntoImpl() && {
- return std::move(impl_);
- }
-
-private:
- rust::Box impl_;
-};
-
-// Factory functions for creating scalar values
-Scalar bool_(bool value);
-Scalar int8(int8_t value);
-Scalar int16(int16_t value);
-Scalar int32(int32_t value);
-Scalar int64(int64_t value);
-Scalar uint8(uint8_t value);
-Scalar uint16(uint16_t value);
-Scalar uint32(uint32_t value);
-Scalar uint64(uint64_t value);
-Scalar float32(float value);
-Scalar float64(double value);
-Scalar string(std::string_view value);
-Scalar binary(const uint8_t *data, size_t length);
-/// TODO: Other Scalars are only supported by casting for now.
-Scalar cast(Scalar scalar, dtype::DType dtype);
-} // namespace vortex::scalar
diff --git a/vortex-cxx/cpp/include/vortex/scan.hpp b/vortex-cxx/cpp/include/vortex/scan.hpp
deleted file mode 100644
index 7debb9ef6aa..00000000000
--- a/vortex-cxx/cpp/include/vortex/scan.hpp
+++ /dev/null
@@ -1,108 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#pragma once
-
-#include "vortex/expr.hpp"
-#include
-#include "vortex_cxx_bridge/lib.h"
-
-#include
-#include
-
-namespace vortex {
-/// The StreamDriver internally holds a `RecordBatchIteratorAdapter` from the Rust side, which is thread-safe
-/// and cloneable. The `RecordBatchIteratorAdapter` internally holds a `WorkStealingArrayIterator`.
-class StreamDriver {
-public:
- StreamDriver(StreamDriver &&other) noexcept = default;
- StreamDriver &operator=(StreamDriver &&other) noexcept = default;
- ~StreamDriver() = default;
-
- StreamDriver(const StreamDriver &) = delete;
- StreamDriver &operator=(const StreamDriver &) = delete;
-
- /// Create a stream of record batches.
- ///
- /// This function is thread-safe and can be called from multiple threads to create one stream per
- /// thread to make progress on the same StreamDriver that is built from a ScanBuilder concurrently.
- ///
- /// Within each thread, the record batches will be emitted in the original order they are within
- /// the scan. Between threads, the order is not guaranteed.
- ///
- /// Example: If the scan contains batches [b0, b1, b2, b3, b4, b5] and two threads call this
- /// function respectively to make progress on their own stream, Thread 1 might receive [b0,
- /// b2, b4] and Thread 2 might receive [b1, b3, b5]. Each thread maintains order within its
- /// subset, but overall ordering between threads is not guaranteed (e.g., Thread 2 could emit b1
- /// before Thread 1 emits b0).
- ArrowArrayStream CreateArrayStream() const;
-
-private:
- friend class ScanBuilder;
-
- explicit StreamDriver(rust::Box impl) : impl_(std::move(impl)) {
- }
-
- rust::Box impl_;
-};
-
-class ScanBuilder {
-public:
- ScanBuilder(ScanBuilder &&other) noexcept = default;
- ScanBuilder &operator=(ScanBuilder &&other) noexcept {
- if (this != &other) {
- impl_ = std::move(other.impl_);
- }
- return *this;
- }
- ~ScanBuilder() = default;
-
- ScanBuilder(const ScanBuilder &) = delete;
- ScanBuilder &operator=(const ScanBuilder &) = delete;
-
- /// Only include rows that match the filter expressions.
- ScanBuilder &WithFilter(expr::Expr &&expr) &;
- ScanBuilder &WithFilter(const expr::Expr &expr) &;
- ScanBuilder &&WithFilter(expr::Expr &&expr) &&;
- ScanBuilder &&WithFilter(const expr::Expr &expr) &&;
-
- /// Only include columns that match the projection expressions.
- ScanBuilder &WithProjection(expr::Expr &&expr) &;
- ScanBuilder &WithProjection(const expr::Expr &expr) &;
- ScanBuilder &&WithProjection(expr::Expr &&expr) &&;
- ScanBuilder &&WithProjection(const expr::Expr &expr) &&;
-
- /// Only include rows in the range [row_range_start, row_range_end).
- ScanBuilder &WithRowRange(uint64_t row_range_start, uint64_t row_range_end) &;
- ScanBuilder &&WithRowRange(uint64_t row_range_start, uint64_t row_range_end) &&;
-
- /// Only include rows with the given indices.
- ScanBuilder &WithIncludeByIndex(const uint64_t *indices, std::size_t size) &;
- ScanBuilder &&WithIncludeByIndex(const uint64_t *indices, std::size_t size) &&;
-
- /// Set the limit on the number of rows to scan out.
- ScanBuilder &WithLimit(uint64_t limit) &;
- ScanBuilder &&WithLimit(uint64_t limit) &&;
-
- /// Set the output schema on the scan builder.
- /// TODO: currently if pass in this option, the schema needs to be the schema after adding projection.
- ScanBuilder &WithOutputSchema(ArrowSchema &output_schema) &;
- ScanBuilder &&WithOutputSchema(ArrowSchema &output_schema) &&;
-
- /// Take ownership and consume the scan builder to a stream of record batches.
- ArrowArrayStream IntoStream() &&;
-
- /// Take ownership and consume the scan builder to a stream driver.
- /// Under the hood, this function calls `ScanBuilder::into_record_batch_reader` and holds a
- /// `WorkStealingArrayIterator` in StreamDriver.
- StreamDriver IntoStreamDriver() &&;
-
-private:
- friend class VortexFile;
-
- explicit ScanBuilder(rust::Box impl) : impl_(std::move(impl)) {
- }
-
- rust::Box impl_;
-};
-} // namespace vortex
\ No newline at end of file
diff --git a/vortex-cxx/cpp/include/vortex/write_options.hpp b/vortex-cxx/cpp/include/vortex/write_options.hpp
deleted file mode 100644
index 08b903bf2fb..00000000000
--- a/vortex-cxx/cpp/include/vortex/write_options.hpp
+++ /dev/null
@@ -1,29 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#pragma once
-
-#include
-#include "vortex_cxx_bridge/lib.h"
-
-namespace vortex {
-
-class VortexWriteOptions {
-public:
- VortexWriteOptions() : impl_(ffi::write_options_new()) {
- }
- VortexWriteOptions(VortexWriteOptions &&other) noexcept = default;
- VortexWriteOptions &operator=(VortexWriteOptions &&other) noexcept = default;
- ~VortexWriteOptions() = default;
-
- VortexWriteOptions(const VortexWriteOptions &) = delete;
- VortexWriteOptions &operator=(const VortexWriteOptions &) = delete;
-
- /// Write an ArrowArrayStream to a Vortex file
- void WriteArrayStream(ArrowArrayStream &stream, const std::string &path);
-
-private:
- rust::Box impl_;
-};
-
-} // namespace vortex
\ No newline at end of file
diff --git a/vortex-cxx/cpp/src/dtype.cpp b/vortex-cxx/cpp/src/dtype.cpp
deleted file mode 100644
index 5b85f36835e..00000000000
--- a/vortex-cxx/cpp/src/dtype.cpp
+++ /dev/null
@@ -1,90 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#include "vortex/dtype.hpp"
-#include "vortex/exception.hpp"
-
-#include "rust/cxx.h"
-
-namespace vortex::dtype {
-DType null() {
- return DType(ffi::dtype_null());
-}
-
-DType bool_(bool nullable) {
- return DType(ffi::dtype_bool(nullable));
-}
-
-DType primitive(PType ptype, bool nullable) {
- return DType(ffi::dtype_primitive(static_cast(ptype), nullable));
-}
-
-DType int8(bool nullable) {
- return primitive(PType::I8, nullable);
-}
-
-DType int16(bool nullable) {
- return primitive(PType::I16, nullable);
-}
-
-DType int32(bool nullable) {
- return primitive(PType::I32, nullable);
-}
-
-DType int64(bool nullable) {
- return primitive(PType::I64, nullable);
-}
-
-DType uint8(bool nullable) {
- return primitive(PType::U8, nullable);
-}
-
-DType uint16(bool nullable) {
- return primitive(PType::U16, nullable);
-}
-
-DType uint32(bool nullable) {
- return primitive(PType::U32, nullable);
-}
-
-DType uint64(bool nullable) {
- return primitive(PType::U64, nullable);
-}
-
-DType float16(bool nullable) {
- return primitive(PType::F16, nullable);
-}
-
-DType float32(bool nullable) {
- return primitive(PType::F32, nullable);
-}
-
-DType float64(bool nullable) {
- return primitive(PType::F64, nullable);
-}
-
-DType decimal(uint8_t precision, int8_t scale, bool nullable) {
- return DType(ffi::dtype_decimal(precision, scale, nullable));
-}
-
-DType utf8(bool nullable) {
- return DType(ffi::dtype_utf8(nullable));
-}
-
-DType binary(bool nullable) {
- return DType(ffi::dtype_binary(nullable));
-}
-
-DType from_arrow(struct ArrowSchema &schema, bool non_nullable) {
- try {
- return DType(ffi::from_arrow(reinterpret_cast(&schema), non_nullable));
- } catch (const rust::cxxbridge1::Error &e) {
- throw VortexException(e.what());
- }
-}
-// Methods
-std::string DType::ToString() const {
- auto rust_str = impl_->to_string();
- return std::string(rust_str.data(), rust_str.length());
-}
-} // namespace vortex::dtype
diff --git a/vortex-cxx/cpp/src/expr.cpp b/vortex-cxx/cpp/src/expr.cpp
deleted file mode 100644
index 0e2d67395c1..00000000000
--- a/vortex-cxx/cpp/src/expr.cpp
+++ /dev/null
@@ -1,60 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#include "vortex/expr.hpp"
-#include "vortex_cxx_bridge/lib.h"
-
-namespace vortex::expr {
-
-Expr literal(scalar::Scalar scalar) {
- return Expr(ffi::literal(std::move(scalar).IntoImpl()));
-}
-
-Expr root() {
- return Expr(ffi::root());
-}
-
-Expr column(std::string_view name) {
- return Expr(ffi::column(rust::String(name.data(), name.length())));
-}
-
-Expr get_item(std::string_view field, Expr child) {
- return Expr(ffi::get_item(rust::String(field.data(), field.length()), std::move(child).IntoImpl()));
-}
-
-Expr not_(Expr child) {
- return Expr(ffi::not_(std::move(child).IntoImpl()));
-}
-
-Expr is_null(Expr child) {
- return Expr(ffi::is_null(std::move(child).IntoImpl()));
-}
-
-// Macro to define binary operator functions
-#define DEFINE_BINARY_OP(name) \
- Expr name(Expr lhs, Expr rhs) { \
- return Expr(ffi::name(std::move(lhs).IntoImpl(), std::move(rhs).IntoImpl())); \
- }
-
-DEFINE_BINARY_OP(eq)
-DEFINE_BINARY_OP(not_eq_)
-DEFINE_BINARY_OP(gt)
-DEFINE_BINARY_OP(gt_eq)
-DEFINE_BINARY_OP(lt)
-DEFINE_BINARY_OP(lt_eq)
-DEFINE_BINARY_OP(and_)
-DEFINE_BINARY_OP(or_)
-DEFINE_BINARY_OP(checked_add)
-
-#undef DEFINE_BINARY_OP
-
-Expr select(const std::vector &fields, Expr child) {
- ::rust::Vec<::rust::String> rs_fields;
- rs_fields.reserve(fields.size());
- for (std::string_view f : fields) {
- rs_fields.emplace_back(f.data(), f.length());
- }
- return Expr(ffi::select(rs_fields, std::move(child).IntoImpl()));
-}
-
-} // namespace vortex::expr
\ No newline at end of file
diff --git a/vortex-cxx/cpp/src/file.cpp b/vortex-cxx/cpp/src/file.cpp
deleted file mode 100644
index 078c305639d..00000000000
--- a/vortex-cxx/cpp/src/file.cpp
+++ /dev/null
@@ -1,36 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#include "vortex/file.hpp"
-#include "vortex/scan.hpp"
-#include "vortex/exception.hpp"
-#include "rust/cxx.h"
-
-namespace vortex {
-
-VortexFile VortexFile::Open(const uint8_t *data, size_t length) {
- try {
- rust::Slice slice(data, length);
- return VortexFile(ffi::open_file_from_buffer(slice));
- } catch (const rust::cxxbridge1::Error &e) {
- throw VortexException(e.what());
- }
-}
-
-VortexFile VortexFile::Open(const std::string &path) {
- try {
- return VortexFile(ffi::open_file(path));
- } catch (const rust::cxxbridge1::Error &e) {
- throw VortexException(e.what());
- }
-}
-
-uint64_t VortexFile::RowCount() const {
- return impl_->row_count();
-}
-
-ScanBuilder VortexFile::CreateScanBuilder() const {
- return ScanBuilder(impl_->scan_builder());
-}
-
-} // namespace vortex
\ No newline at end of file
diff --git a/vortex-cxx/cpp/src/scalar.cpp b/vortex-cxx/cpp/src/scalar.cpp
deleted file mode 100644
index 87502ee2f00..00000000000
--- a/vortex-cxx/cpp/src/scalar.cpp
+++ /dev/null
@@ -1,66 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#include "vortex/scalar.hpp"
-
-#include "vortex_cxx_bridge/lib.h"
-
-namespace vortex::scalar {
-
-Scalar bool_(bool value) {
- return Scalar(ffi::bool_scalar_new(value));
-}
-
-Scalar int8(int8_t value) {
- return Scalar(ffi::i8_scalar_new(value));
-}
-
-Scalar int16(int16_t value) {
- return Scalar(ffi::i16_scalar_new(value));
-}
-
-Scalar int32(int32_t value) {
- return Scalar(ffi::i32_scalar_new(value));
-}
-
-Scalar int64(int64_t value) {
- return Scalar(ffi::i64_scalar_new(value));
-}
-
-Scalar uint8(uint8_t value) {
- return Scalar(ffi::u8_scalar_new(value));
-}
-
-Scalar uint16(uint16_t value) {
- return Scalar(ffi::u16_scalar_new(value));
-}
-
-Scalar uint32(uint32_t value) {
- return Scalar(ffi::u32_scalar_new(value));
-}
-
-Scalar uint64(uint64_t value) {
- return Scalar(ffi::u64_scalar_new(value));
-}
-
-Scalar float32(float value) {
- return Scalar(ffi::f32_scalar_new(value));
-}
-
-Scalar float64(double value) {
- return Scalar(ffi::f64_scalar_new(value));
-}
-
-Scalar string(std::string_view value) {
- return Scalar(ffi::string_scalar_new(rust::Str(value.data(), value.length())));
-}
-
-Scalar binary(const uint8_t *data, size_t length) {
- return Scalar(ffi::binary_scalar_new(rust::Slice(data, length)));
-}
-
-Scalar cast(Scalar scalar, dtype::DType dtype) {
- return Scalar(std::move(scalar).IntoImpl()->cast_scalar(*std::move(dtype).GetImpl()));
-}
-
-} // namespace vortex::scalar
\ No newline at end of file
diff --git a/vortex-cxx/cpp/src/scan.cpp b/vortex-cxx/cpp/src/scan.cpp
deleted file mode 100644
index 9b40ec00e0d..00000000000
--- a/vortex-cxx/cpp/src/scan.cpp
+++ /dev/null
@@ -1,116 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#include "vortex/scan.hpp"
-#include "vortex/exception.hpp"
-#include "rust/cxx.h"
-#include "vortex/expr.hpp"
-
-namespace vortex {
-ScanBuilder &ScanBuilder::WithFilter(expr::Expr &&expr) & {
- impl_->with_filter(std::move(expr).IntoImpl());
- return *this;
-}
-ScanBuilder &ScanBuilder::WithFilter(const expr::Expr &expr) & {
- impl_->with_filter_ref(expr.Impl());
- return *this;
-}
-ScanBuilder &&ScanBuilder::WithFilter(expr::Expr &&expr) && {
- impl_->with_filter(std::move(expr).IntoImpl());
- return std::move(*this);
-}
-ScanBuilder &&ScanBuilder::WithFilter(const expr::Expr &expr) && {
- impl_->with_filter_ref(expr.Impl());
- return std::move(*this);
-}
-
-ScanBuilder &ScanBuilder::WithProjection(expr::Expr &&expr) & {
- impl_->with_projection(std::move(expr).IntoImpl());
- return *this;
-}
-ScanBuilder &ScanBuilder::WithProjection(const expr::Expr &expr) & {
- impl_->with_projection_ref(expr.Impl());
- return *this;
-}
-ScanBuilder &&ScanBuilder::WithProjection(expr::Expr &&expr) && {
- impl_->with_projection(std::move(expr).IntoImpl());
- return std::move(*this);
-}
-ScanBuilder &&ScanBuilder::WithProjection(const expr::Expr &expr) && {
- impl_->with_projection_ref(expr.Impl());
- return std::move(*this);
-}
-
-ScanBuilder &ScanBuilder::WithRowRange(uint64_t row_range_start, uint64_t row_range_end) & {
- impl_->with_row_range(row_range_start, row_range_end);
- return *this;
-}
-ScanBuilder &&ScanBuilder::WithRowRange(uint64_t row_range_start, uint64_t row_range_end) && {
- impl_->with_row_range(row_range_start, row_range_end);
- return std::move(*this);
-}
-
-ScanBuilder &ScanBuilder::WithLimit(uint64_t limit) & {
- impl_->with_limit(limit);
- return *this;
-}
-
-ScanBuilder &&ScanBuilder::WithLimit(uint64_t limit) && {
- impl_->with_limit(limit);
- return std::move(*this);
-}
-
-ScanBuilder &ScanBuilder::WithIncludeByIndex(const uint64_t *indices, std::size_t size) & {
- impl_->with_include_by_index(rust::Slice(indices, size));
- return *this;
-}
-
-ScanBuilder &&ScanBuilder::WithIncludeByIndex(const uint64_t *indices, std::size_t size) && {
- impl_->with_include_by_index(rust::Slice(indices, size));
- return std::move(*this);
-}
-
-ScanBuilder &ScanBuilder::WithOutputSchema(ArrowSchema &output_schema) & {
- try {
- impl_->with_output_schema(reinterpret_cast(&output_schema));
- } catch (const rust::cxxbridge1::Error &e) {
- throw VortexException(e.what());
- }
- return *this;
-}
-
-ScanBuilder &&ScanBuilder::WithOutputSchema(ArrowSchema &output_schema) && {
- try {
- impl_->with_output_schema(reinterpret_cast(&output_schema));
- } catch (const rust::cxxbridge1::Error &e) {
- throw VortexException(e.what());
- }
- return std::move(*this);
-}
-
-ArrowArrayStream ScanBuilder::IntoStream() && {
- try {
- ArrowArrayStream stream;
- ffi::scan_builder_into_stream(std::move(impl_), reinterpret_cast(&stream));
- return stream;
- } catch (const rust::cxxbridge1::Error &e) {
- throw VortexException(e.what());
- }
-}
-
-StreamDriver ScanBuilder::IntoStreamDriver() && {
- try {
- rust::Box reader =
- ffi::scan_builder_into_threadsafe_cloneable_reader(std::move(impl_));
- return StreamDriver(std::move(reader));
- } catch (const rust::cxxbridge1::Error &e) {
- throw VortexException(e.what());
- }
-}
-
-ArrowArrayStream StreamDriver::CreateArrayStream() const {
- ArrowArrayStream stream;
- impl_->clone_a_stream(reinterpret_cast(&stream));
- return stream;
-}
-} // namespace vortex
\ No newline at end of file
diff --git a/vortex-cxx/cpp/src/write_options.cpp b/vortex-cxx/cpp/src/write_options.cpp
deleted file mode 100644
index 712a6933208..00000000000
--- a/vortex-cxx/cpp/src/write_options.cpp
+++ /dev/null
@@ -1,18 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#include "vortex/write_options.hpp"
-#include "vortex/exception.hpp"
-
-#include "rust/cxx.h"
-
-namespace vortex {
-void VortexWriteOptions::WriteArrayStream(ArrowArrayStream &stream, const std::string &path) {
- try {
- ffi::write_array_stream(std::move(impl_), reinterpret_cast(&stream), path);
- } catch (const rust::cxxbridge1::Error &e) {
- throw VortexException(e.what());
- }
-}
-
-} // namespace vortex
\ No newline at end of file
diff --git a/vortex-cxx/cpp/tests/basic_test.cpp b/vortex-cxx/cpp/tests/basic_test.cpp
deleted file mode 100644
index eb44715285a..00000000000
--- a/vortex-cxx/cpp/tests/basic_test.cpp
+++ /dev/null
@@ -1,510 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "vortex/file.hpp"
-#include "vortex/scan.hpp"
-#include "vortex/write_options.hpp"
-#include "vortex/scalar.hpp"
-#include "test_data_generator.hpp"
-#include "vortex_cxx_bridge/lib.h"
-
-#include
-#include
-
-class VortexTest : public ::testing::Test {
-protected:
- // Helper function to create unique temporary files for each test
- static std::string GetUniqueTempFile(const std::string &suffix = "vortex") {
- std::filesystem::path temp_dir = std::filesystem::temp_directory_path();
- std::filesystem::path vortex_test_dir = temp_dir / "vortex_test";
-
- if (!std::filesystem::exists(vortex_test_dir)) {
- std::filesystem::create_directories(vortex_test_dir);
- }
-
- // Use a unique random filename to prevent races between parallel test runs
- std::string unique_name = "test_" + std::to_string(std::random_device {}()) + "_" + suffix;
- return (vortex_test_dir / unique_name).string();
- }
-
- // Write test data to a unique temporary file and return the path
- static std::string WriteTestData(const std::string &suffix = "test_data.vortex") {
- std::string path = GetUniqueTempFile(suffix);
- auto stream = vortex::testing::CreateTestDataStream();
- auto write_options = vortex::ffi::write_options_new();
- vortex::ffi::write_array_stream(std::move(write_options),
- reinterpret_cast(&stream),
- path.c_str());
- return path;
- }
-
- // Helper function to create and initialize array view
- nanoarrow::UniqueArrayView CreateArrayView(const nanoarrow::UniqueArray &array,
- const nanoarrow::UniqueSchema &schema) {
- nanoarrow::UniqueArrayView array_view;
- ArrowError error;
- ArrowErrorCode init_result = ArrowArrayViewInitFromSchema(array_view.get(), schema.get(), &error);
- if (init_result != NANOARROW_OK) {
- std::cerr << "Error: " << error.message << '\n';
- std::abort();
- }
- ArrowErrorCode set_result = ArrowArrayViewSetArray(array_view.get(), array.get(), nullptr);
- if (set_result != NANOARROW_OK) {
- std::cerr << "Error: " << error.message << '\n';
- std::abort();
- }
- return array_view;
- }
-
- std::pair
- StreamToUniqueStreamSchema(ArrowArrayStream &stream) {
- nanoarrow::UniqueArrayStream array_stream;
- ArrowArrayStreamMove(&stream, array_stream.get());
- ArrowError error;
- nanoarrow::UniqueSchema schema;
- ArrowErrorCode set_result = ArrowArrayStreamGetSchema(array_stream.get(), schema.get(), &error);
- if (set_result != NANOARROW_OK) {
- std::cerr << "Error: " << error.message << '\n';
- std::abort();
- }
- return {std::move(array_stream), std::move(schema)};
- }
-
- nanoarrow::UniqueArray ReadFirstArrayFromUniqueStream(nanoarrow::UniqueArrayStream &array_stream) {
- nanoarrow::UniqueArray array;
- int get_next_result = array_stream->get_next(array_stream.get(), array.get());
- EXPECT_EQ(get_next_result, 0);
- return array;
- }
-
- std::pair
- ReadFirstArrayFromStream(ArrowArrayStream reference_stream) {
- auto [ref_array_stream, ref_schema] = StreamToUniqueStreamSchema(reference_stream);
- auto ref_array = ReadFirstArrayFromUniqueStream(ref_array_stream);
- return {std::move(ref_array), std::move(ref_schema)};
- }
-
- /// Both array are struct of int64
- void ValidateArray(const nanoarrow::UniqueArray &actual_array,
- const nanoarrow::UniqueSchema &actual_schema,
- const nanoarrow::UniqueArray &ref_array,
- const nanoarrow::UniqueSchema &ref_schema) {
- // Basic properties validation
- ASSERT_EQ(actual_schema->n_children, ref_schema->n_children);
-
- auto actual_view = CreateArrayView(actual_array, actual_schema);
- auto ref_view = CreateArrayView(ref_array, ref_schema);
-
- ASSERT_EQ(actual_array->length, ref_array->length);
-
- // Compare all fields
- for (int64_t field_idx = 0; field_idx < actual_schema->n_children; ++field_idx) {
- auto actual_field = actual_view->children[field_idx];
- auto expected_field = ref_view->children[field_idx];
-
- ASSERT_EQ(actual_field->array->length, expected_field->array->length);
-
- for (int64_t i = 0; i < actual_field->array->length; ++i) {
- int64_t actual_value = ArrowArrayViewGetIntUnsafe(actual_field, i);
- int64_t expected_value = ArrowArrayViewGetIntUnsafe(expected_field, i);
-
- ASSERT_EQ(actual_value, expected_value);
- }
- }
- }
-
- /// Both array are struct of int64
- void ValidateArrayWithSelection(const nanoarrow::UniqueArray &actual_array,
- const nanoarrow::UniqueSchema &actual_schema,
- const nanoarrow::UniqueArray &ref_array,
- const nanoarrow::UniqueSchema &ref_schema,
- const std::vector &row_indices) {
- // Basic properties validation
- ASSERT_EQ(actual_schema->n_children, ref_schema->n_children);
- if (row_indices.empty()) {
- ASSERT_EQ(actual_array->length, 0);
- return;
- }
- auto actual_view = CreateArrayView(actual_array, actual_schema);
- auto ref_view = CreateArrayView(ref_array, ref_schema);
-
- ASSERT_EQ(actual_array->length, row_indices.size());
-
- // Selective row comparison using indices
- ASSERT_EQ(actual_array->length, static_cast(row_indices.size()));
-
- for (int64_t i = 0; i < static_cast(row_indices.size()); ++i) {
- int64_t ref_idx = row_indices[i];
-
- for (int64_t field_idx = 0; field_idx < actual_schema->n_children; ++field_idx) {
- int64_t actual_val = ArrowArrayViewGetIntUnsafe(actual_view->children[field_idx], i);
- int64_t expected_val = ArrowArrayViewGetIntUnsafe(ref_view->children[field_idx], ref_idx);
-
- ASSERT_EQ(actual_val, expected_val);
- }
- }
- }
-
- // Helper to execute scan builder and get array+schema
- std::pair ScanFirstArrayFromTestData(
- const std::function &configureScanBuilder) {
- auto test_data_path = WriteTestData();
- auto file = vortex::VortexFile::Open(test_data_path);
- auto scan_builder = file.CreateScanBuilder();
- auto stream = configureScanBuilder(scan_builder);
-
- auto [array_stream, schema] = StreamToUniqueStreamSchema(stream);
- auto array = ReadFirstArrayFromUniqueStream(array_stream);
-
- return {std::move(array), std::move(schema)};
- }
-
- /// Validate array with projection - only checks specified field indices
- void ValidateArrayWithProjection(const nanoarrow::UniqueArray &actual_array,
- const nanoarrow::UniqueSchema &actual_schema,
- const nanoarrow::UniqueArray &ref_array,
- const nanoarrow::UniqueSchema &ref_schema,
- const std::vector &field_idxs) {
- ASSERT_EQ(actual_schema->n_children, field_idxs.size());
-
- auto actual_view = CreateArrayView(actual_array, actual_schema);
- auto ref_view = CreateArrayView(ref_array, ref_schema);
-
- ASSERT_EQ(actual_array->length, ref_array->length);
-
- // Compare only the specified fields
- for (int64_t i = 0; i < actual_array->length; ++i) {
- for (size_t proj_idx = 0; proj_idx < field_idxs.size(); ++proj_idx) {
- int64_t ref_field_idx = field_idxs[proj_idx];
- int64_t actual_val = ArrowArrayViewGetIntUnsafe(actual_view->children[proj_idx], i);
- int64_t expected_val = ArrowArrayViewGetIntUnsafe(ref_view->children[ref_field_idx], i);
- ASSERT_EQ(actual_val, expected_val);
- }
- }
- }
-
- // Top-level test helper that all tests can use
- void
- RunScanBuilderTest(const std::function &configureScanBuilder,
- ArrowArrayStream expected_stream,
- const std::vector &expected_row_indices = {},
- bool selection = false) {
-
- auto [array, schema] = ScanFirstArrayFromTestData(configureScanBuilder);
- auto [ref_array, ref_schema] = ReadFirstArrayFromStream(expected_stream);
- selection == false
- ? ValidateArray(array, schema, ref_array, ref_schema)
- : ValidateArrayWithSelection(array, schema, ref_array, ref_schema, expected_row_indices);
- }
-
- // New helper for projection tests
- void RunScanBuilderProjectionTest(
- const std::function &configureScanBuilder,
- ArrowArrayStream expected_stream,
- const std::vector &field_idxs) {
-
- auto [array, schema] = ScanFirstArrayFromTestData(configureScanBuilder);
- auto [ref_array, ref_schema] = ReadFirstArrayFromStream(expected_stream);
-
- ValidateArrayWithProjection(array, schema, ref_array, ref_schema, field_idxs);
- }
-};
-
-TEST_F(VortexTest, ScanToStream) {
- RunScanBuilderTest([](vortex::ScanBuilder &builder) { return std::move(builder).IntoStream(); },
- vortex::testing::CreateTestDataStream());
-}
-
-TEST_F(VortexTest, ScanBuilderWithLimitWithRowRange) {
- // Test field "a" and "b" - should contain values from rows 1-2 from original data (indices 1 and
- // 2)
- RunScanBuilderTest(
- [](vortex::ScanBuilder &scan_builder) {
- return std::move(scan_builder.WithLimit(2).WithRowRange(1, 4)).IntoStream();
- },
- vortex::testing::CreateTestDataStream(),
- {1, 2},
- true);
-}
-
-TEST_F(VortexTest, ScanBuilderWithIncludeByIndex) {
- std::vector include_by_index = {1, 3};
-
- RunScanBuilderTest(
- [&include_by_index](vortex::ScanBuilder &scan_builder) {
- return std::move(
- scan_builder.WithIncludeByIndex(include_by_index.data(), include_by_index.size()))
- .IntoStream();
- },
- vortex::testing::CreateTestDataStream(),
- {1, 3},
- true);
-}
-
-TEST_F(VortexTest, ScanBuilderWithRowRangeWithIncludeByIndex) {
- std::vector include_by_index = {1, 3, 4};
-
- RunScanBuilderTest(
- [&include_by_index](vortex::ScanBuilder &scan_builder) {
- return std::move(scan_builder.WithRowRange(2, 5).WithIncludeByIndex(include_by_index.data(),
- include_by_index.size()))
- .IntoStream();
- },
- vortex::testing::CreateTestDataStream(),
- {3, 4},
- true);
-}
-
-TEST_F(VortexTest, WriteArrayStream) {
- auto test_data_path = WriteTestData();
- auto file = vortex::VortexFile::Open(test_data_path);
- auto stream = file.CreateScanBuilder().IntoStream();
-
- // Write the stream to a new Vortex file
- std::string test_output_path = GetUniqueTempFile("write_output.vortex");
- vortex::VortexWriteOptions write_options;
- ASSERT_NO_THROW(write_options.WriteArrayStream(stream, test_output_path));
-
- // Verify the written file
- auto written_file = vortex::VortexFile::Open(test_output_path);
- ASSERT_EQ(written_file.RowCount(), 5);
-
- // Verify data integrity by reading from the written file
- auto out_stream = written_file.CreateScanBuilder().IntoStream();
- auto [array_stream, schema] = StreamToUniqueStreamSchema(out_stream);
- auto array = ReadFirstArrayFromUniqueStream(array_stream);
- auto [ref_array, ref_schema] = ReadFirstArrayFromStream(vortex::testing::CreateTestDataStream());
- ValidateArray(array, schema, ref_array, ref_schema);
-}
-
-TEST_F(VortexTest, ConcurrentMultiStreamRead) {
- std::string test_data_path_1m = GetUniqueTempFile("concurrent_1m.vortex");
- auto stream_1m = vortex::testing::CreateTestData1MStream();
- auto write_options = vortex::ffi::write_options_new();
- vortex::ffi::write_array_stream(std::move(write_options),
- reinterpret_cast(&stream_1m),
- test_data_path_1m.c_str());
-
- auto file = vortex::VortexFile::Open(test_data_path_1m);
- auto stream_driver = file.CreateScanBuilder().IntoStreamDriver();
-
- // Structure to hold batch data with first ID and nanoarrow array
- struct BatchData {
- int64_t first_id;
- nanoarrow::UniqueArray array;
-
- BatchData(int64_t first_id, nanoarrow::UniqueArray array)
- : first_id(first_id), array(std::move(array)) {
- }
- };
-
- std::vector thread1_batches;
- std::vector thread2_batches;
-
- // Helper function to read from a stream and collect batches
- auto read_stream = [&](std::vector &batches) {
- // Each thread creates its own stream
- auto stream = stream_driver.CreateArrayStream();
- auto [array_stream, schema] = StreamToUniqueStreamSchema(stream);
-
- std::vector local_batches;
-
- while (true) {
- nanoarrow::UniqueArray array;
- int get_next_result = array_stream->get_next(array_stream.get(), array.get());
-
- if (get_next_result != 0) {
- std::cerr << "Error: " << array_stream->get_last_error(array_stream.get()) << '\n';
- std::abort();
- }
-
- if (array->length == 0) {
- break; // Empty array indicates end
- }
-
- auto array_view = CreateArrayView(array, schema);
-
- int64_t first_id = ArrowArrayViewGetIntUnsafe(array_view->children[0], 0);
-
- local_batches.emplace_back(first_id, std::move(array));
- }
- batches = std::move(local_batches);
- };
-
- // Launch two threads
- std::thread thread1(read_stream, std::ref(thread1_batches));
- std::thread thread2(read_stream, std::ref(thread2_batches));
-
- // Wait for both threads to complete
- thread1.join();
- thread2.join();
-
- // Combine all batches from both threads
- std::vector all_batches;
- all_batches.insert(all_batches.end(),
- std::make_move_iterator(thread1_batches.begin()),
- std::make_move_iterator(thread1_batches.end()));
- all_batches.insert(all_batches.end(),
- std::make_move_iterator(thread2_batches.begin()),
- std::make_move_iterator(thread2_batches.end()));
-
- // Sort batches by first ID to ensure proper validation order
- std::sort(all_batches.begin(), all_batches.end(), [](const BatchData &a, const BatchData &b) {
- return a.first_id < b.first_id;
- });
-
- // Create reference data for validation
- auto [ref_array, ref_schema] = ReadFirstArrayFromStream(vortex::testing::CreateTestData1MStream());
- auto ref_array_view = CreateArrayView(ref_array, ref_schema);
-
- // Validate all data against reference
- constexpr size_t EXPECTED_ROWS = static_cast(1024) * 1024;
- size_t total_rows_read = 0;
- int64_t reference_offset = 0;
-
- auto stream_for_schema = stream_driver.CreateArrayStream();
- auto [_, schema] = StreamToUniqueStreamSchema(stream_for_schema);
- for (const auto &batch : all_batches) {
- auto array_view = CreateArrayView(batch.array, schema);
-
- for (int64_t i = 0; i < batch.array->length; ++i) {
-
- int64_t actual_id = ArrowArrayViewGetIntUnsafe(array_view->children[0], i);
- int32_t actual_value =
- static_cast(ArrowArrayViewGetIntUnsafe(array_view->children[1], i));
-
- int64_t expected_id = ArrowArrayViewGetIntUnsafe(ref_array_view->children[0], reference_offset);
- int32_t expected_value = static_cast(
- ArrowArrayViewGetIntUnsafe(ref_array_view->children[1], reference_offset));
-
- ASSERT_EQ(actual_id, expected_id);
- ASSERT_EQ(actual_value, expected_value);
- reference_offset++;
- }
- total_rows_read += batch.array->length;
- }
-
- // Verify we read all expected data
- ASSERT_EQ(total_rows_read, EXPECTED_ROWS)
- << "Expected to read " << EXPECTED_ROWS << " rows, but read " << total_rows_read << " rows";
- ASSERT_EQ(reference_offset, EXPECTED_ROWS) << "Reference validation didn't cover all rows";
-
- ASSERT_GT(all_batches.size(), 1) << "Expected multiple batches, but got " << all_batches.size();
-}
-
-namespace ve = vortex::expr;
-namespace vs = vortex::scalar;
-
-TEST_F(VortexTest, ScanBuilderWithFilter) {
- // Test filtering with eq(column("a"), val) - should return only rows where column "a" equals 30
- RunScanBuilderTest(
- [](vortex::ScanBuilder &scan_builder) {
- auto filter = ve::eq(ve::column("a"), ve::literal(vs::int32(30)));
- return std::move(scan_builder.WithFilter(std::move(filter))).IntoStream();
- },
- vortex::testing::CreateTestDataStream(),
- {2},
- true); // Row index 2 corresponds to value 30
-}
-
-TEST_F(VortexTest, ScanBuilderWithFilterLvalueref) {
- // Test filtering with eq(column("a"), val) - should return only rows where column "a" equals 30
- RunScanBuilderTest(
- [](vortex::ScanBuilder &scan_builder) {
- const auto filter = ve::eq(ve::column("a"), ve::literal(vs::int32(30)));
- return std::move(scan_builder.WithFilter(filter)).IntoStream();
- },
- vortex::testing::CreateTestDataStream(),
- {2},
- true); // Row index 2 corresponds to value 30
-}
-
-TEST_F(VortexTest, ScanBuilderWithFilterNoMatches) {
- // Test filtering with eq(column("a"), val) where no rows match - should return empty result
- RunScanBuilderTest(
- [](vortex::ScanBuilder &scan_builder) {
- auto filter = ve::eq(ve::column("a"),
- ve::literal(vs::int32(999)) // Value that doesn't exist in test data
- );
- return std::move(scan_builder.WithFilter(std::move(filter))).IntoStream();
- },
- vortex::testing::CreateTestDataStream(),
- {},
- true); // No matching rows
-}
-
-TEST_F(VortexTest, ScanBuilderWithFilterUsingDTypeFromArrowAndScalarCast) {
- // Test filtering using DType::from_arrow and Scalar::cast functionality
- // This test creates a filter expression by casting a scalar to match the column type
-
- // Test DType::from_arrow with int32 schema
- nanoarrow::UniqueSchema int32_schema;
- ArrowSchemaInit(int32_schema.get());
- ArrowErrorCode result = ArrowSchemaSetType(int32_schema.get(), NANOARROW_TYPE_INT32);
- EXPECT_EQ(result, NANOARROW_OK);
- result = ArrowSchemaSetName(int32_schema.get(), "test_field");
- EXPECT_EQ(result, NANOARROW_OK);
-
- auto dtype = vortex::dtype::from_arrow(*int32_schema.get());
-
- // Use the casted scalar in filter expression - create a new scalar for lambda
- RunScanBuilderTest(
- [&](vortex::ScanBuilder &scan_builder) {
- auto test_scalar = vs::cast(vs::int64(30), std::move(dtype));
- auto filter = ve::eq(ve::column("a"), ve::literal(std::move(test_scalar)));
- return std::move(scan_builder.WithFilter(std::move(filter))).IntoStream();
- },
- vortex::testing::CreateTestDataStream(),
- {2},
- true); // Row index 2 corresponds to value 30
-}
-
-TEST_F(VortexTest, ScanBuilderWithProjectionSingleColumn) {
- // Test projection selecting only column "a" (field index 0)
- RunScanBuilderProjectionTest(
- [](vortex::ScanBuilder &scan_builder) {
- auto projection = ve::select({"a"}, ve::root());
- return std::move(scan_builder.WithProjection(std::move(projection))).IntoStream();
- },
- vortex::testing::CreateTestDataStream(),
- {0});
-}
-
-TEST_F(VortexTest, OpenFromBuffer) {
- std::string test_file_path = GetUniqueTempFile("buffer.vortex");
- auto stream = vortex::testing::CreateTestDataStream();
- auto write_options = vortex::ffi::write_options_new();
- vortex::ffi::write_array_stream(std::move(write_options),
- reinterpret_cast(&stream),
- test_file_path.c_str());
-
- std::ifstream file(test_file_path, std::ios::binary | std::ios::ate);
- ASSERT_TRUE(file.is_open()) << "Failed to open file: " << test_file_path;
-
- std::streamsize file_size = file.tellg();
- file.seekg(0, std::ios::beg);
-
- std::vector buffer(file_size);
- ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), file_size))
- << "Failed to read file into buffer";
- file.close();
-
- auto vortex_file = vortex::VortexFile::Open(buffer.data(), buffer.size());
- ASSERT_EQ(vortex_file.RowCount(), 5);
-
- auto scan_stream = vortex_file.CreateScanBuilder().IntoStream();
- auto [array_stream, schema] = StreamToUniqueStreamSchema(scan_stream);
- auto array = ReadFirstArrayFromUniqueStream(array_stream);
-
- auto [ref_array, ref_schema] = ReadFirstArrayFromStream(vortex::testing::CreateTestDataStream());
- ValidateArray(array, schema, ref_array, ref_schema);
-}
diff --git a/vortex-cxx/cpp/tests/test_data_generator.cpp b/vortex-cxx/cpp/tests/test_data_generator.cpp
deleted file mode 100644
index 67dcb290509..00000000000
--- a/vortex-cxx/cpp/tests/test_data_generator.cpp
+++ /dev/null
@@ -1,125 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#include "test_data_generator.hpp"
-#include
-#include
-#include
-#include
-#include
-
-namespace vortex {
-namespace testing {
-
- ArrowArrayStream CreateTestDataStream() {
- // Create a simple two-column struct with int32 data
- // Schema: struct{a: int32, b: int32}
- nanoarrow::UniqueSchema schema;
- ArrowSchemaInit(schema.get());
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetType(schema.get(), NANOARROW_TYPE_STRUCT));
- NANOARROW_THROW_NOT_OK(ArrowSchemaAllocateChildren(schema.get(), 2));
- ArrowSchemaInit(schema->children[0]);
- ArrowSchemaInit(schema->children[1]);
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetName(schema->children[0], "a"));
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetType(schema->children[0], NANOARROW_TYPE_INT32));
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetName(schema->children[1], "b"));
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetType(schema->children[1], NANOARROW_TYPE_INT32));
-
- // Create arrays for each field
- nanoarrow::UniqueArray field_a, field_b;
- NANOARROW_THROW_NOT_OK(ArrowArrayInitFromType(field_a.get(), NANOARROW_TYPE_INT32));
- NANOARROW_THROW_NOT_OK(ArrowArrayInitFromType(field_b.get(), NANOARROW_TYPE_INT32));
-
- // Reserve for 5 elements
- NANOARROW_THROW_NOT_OK(ArrowArrayStartAppending(field_a.get()));
- NANOARROW_THROW_NOT_OK(ArrowArrayStartAppending(field_b.get()));
-
- // Add data: [10, 20, 30, 40, 50]
- std::vector data = {10, 20, 30, 40, 50};
- for (int32_t value : data) {
- NANOARROW_THROW_NOT_OK(ArrowArrayAppendInt(field_a.get(), value));
- NANOARROW_THROW_NOT_OK(ArrowArrayAppendInt(field_b.get(), value));
- }
-
- NANOARROW_THROW_NOT_OK(
- ArrowArrayFinishBuilding(field_a.get(), NANOARROW_VALIDATION_LEVEL_NONE, nullptr));
- NANOARROW_THROW_NOT_OK(
- ArrowArrayFinishBuilding(field_b.get(), NANOARROW_VALIDATION_LEVEL_NONE, nullptr));
-
- // Create struct array
- nanoarrow::UniqueArray struct_array;
- NANOARROW_THROW_NOT_OK(ArrowArrayInitFromType(struct_array.get(), NANOARROW_TYPE_STRUCT));
- NANOARROW_THROW_NOT_OK(ArrowArrayAllocateChildren(struct_array.get(), 2));
- struct_array->length = 5;
- ArrowArrayMove(field_a.get(), struct_array->children[0]);
- ArrowArrayMove(field_b.get(), struct_array->children[1]);
-
- // Create vector and move array into it
- std::vector arrays;
- arrays.push_back(std::move(struct_array));
-
- // Create stream
- ArrowArrayStream stream;
- nanoarrow::VectorArrayStream vector_stream(schema.get(), std::move(arrays));
- vector_stream.ToArrayStream(&stream);
-
- return stream;
- }
-
- ArrowArrayStream CreateTestData1MStream() {
- constexpr size_t NUM_ROWS = 1024UL * 1024;
-
- // Create schema: struct{id: int64, value: int32}
- nanoarrow::UniqueSchema schema;
- ArrowSchemaInit(schema.get());
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetType(schema.get(), NANOARROW_TYPE_STRUCT));
- NANOARROW_THROW_NOT_OK(ArrowSchemaAllocateChildren(schema.get(), 2));
- ArrowSchemaInit(schema->children[0]);
- ArrowSchemaInit(schema->children[1]);
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetName(schema->children[0], "id"));
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetType(schema->children[0], NANOARROW_TYPE_INT64));
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetName(schema->children[1], "value"));
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetType(schema->children[1], NANOARROW_TYPE_INT32));
-
- // Create arrays for each field
- nanoarrow::UniqueArray id_field, value_field;
- NANOARROW_THROW_NOT_OK(ArrowArrayInitFromType(id_field.get(), NANOARROW_TYPE_INT64));
- NANOARROW_THROW_NOT_OK(ArrowArrayInitFromType(value_field.get(), NANOARROW_TYPE_INT32));
-
- // Reserve space
- NANOARROW_THROW_NOT_OK(ArrowArrayStartAppending(id_field.get()));
- NANOARROW_THROW_NOT_OK(ArrowArrayStartAppending(value_field.get()));
-
- // Add data
- for (size_t i = 0; i < NUM_ROWS; ++i) {
- NANOARROW_THROW_NOT_OK(ArrowArrayAppendInt(id_field.get(), static_cast(i)));
- NANOARROW_THROW_NOT_OK(ArrowArrayAppendInt(value_field.get(), static_cast(i * 2)));
- }
-
- NANOARROW_THROW_NOT_OK(
- ArrowArrayFinishBuilding(id_field.get(), NANOARROW_VALIDATION_LEVEL_NONE, nullptr));
- NANOARROW_THROW_NOT_OK(
- ArrowArrayFinishBuilding(value_field.get(), NANOARROW_VALIDATION_LEVEL_NONE, nullptr));
-
- // Create struct array
- nanoarrow::UniqueArray struct_array;
- NANOARROW_THROW_NOT_OK(ArrowArrayInitFromType(struct_array.get(), NANOARROW_TYPE_STRUCT));
- NANOARROW_THROW_NOT_OK(ArrowArrayAllocateChildren(struct_array.get(), 2));
- struct_array->length = NUM_ROWS;
- ArrowArrayMove(id_field.get(), struct_array->children[0]);
- ArrowArrayMove(value_field.get(), struct_array->children[1]);
-
- // Create vector and move array into it
- std::vector arrays;
- arrays.push_back(std::move(struct_array));
-
- // Create stream
- ArrowArrayStream stream;
- nanoarrow::VectorArrayStream vector_stream(schema.get(), std::move(arrays));
- vector_stream.ToArrayStream(&stream);
-
- return stream;
- }
-
-} // namespace testing
-} // namespace vortex
\ No newline at end of file
diff --git a/vortex-cxx/cpp/tests/test_data_generator.hpp b/vortex-cxx/cpp/tests/test_data_generator.hpp
deleted file mode 100644
index ec5dba55d50..00000000000
--- a/vortex-cxx/cpp/tests/test_data_generator.hpp
+++ /dev/null
@@ -1,20 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#pragma once
-
-#include
-
-namespace vortex {
-namespace testing {
-
- /// Create test data with structure {a: [10, 20, 30, 40, 50], b: [10, 20, 30, 40, 50]}
- /// This stream only has one Array
- ArrowArrayStream CreateTestDataStream();
-
- /// Create 1M rows of test data with structure {id: [0..1M], value: [0, 2, 4, ..., 2M]}
- /// This stream only has one Array
- ArrowArrayStream CreateTestData1MStream();
-
-} // namespace testing
-} // namespace vortex
\ No newline at end of file
diff --git a/vortex-cxx/examples/.clang-tidy b/vortex-cxx/examples/.clang-tidy
new file mode 100644
index 00000000000..e302d65b598
--- /dev/null
+++ b/vortex-cxx/examples/.clang-tidy
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright the Vortex contributors
+InheritParentConfig: true
+Checks: |
+ -google-build-using-namespace,
+ -bugprone-unchecked-optional-access,
diff --git a/vortex-cxx/examples/CMakeLists.txt b/vortex-cxx/examples/CMakeLists.txt
index 348d80e1547..a8f90119fd7 100644
--- a/vortex-cxx/examples/CMakeLists.txt
+++ b/vortex-cxx/examples/CMakeLists.txt
@@ -1,34 +1,11 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright the Vortex contributors
-cmake_minimum_required(VERSION 3.22)
-
-project(vortex-examples)
-
-set(CMAKE_CXX_STANDARD 20)
-set(CMAKE_CXX_STANDARD_REQUIRED ON)
-
-find_program(SCCACHE_PROGRAM sccache)
-if (SCCACHE_PROGRAM)
- set(CMAKE_C_COMPILER_LAUNCHER "${SCCACHE_PROGRAM}")
- set(CMAKE_CXX_COMPILER_LAUNCHER "${SCCACHE_PROGRAM}")
- message(STATUS "Sccache found: ${SCCACHE_PROGRAM}")
-else ()
- message(STATUS "Sccache not found")
-endif ()
-
-include(FetchContent)
-FetchContent_Declare(
- vortex
- SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../
- SOURCE_SUBDIR vortex-cxx
-)
-FetchContent_MakeAvailable(vortex)
-
-add_executable(hello-vortex hello-vortex.cpp)
-
-if(APPLE)
- set(APPLE_LINK_FLAGS "-framework CoreFoundation -framework Security")
-endif()
-
-target_link_libraries(hello-vortex PRIVATE vortex ${APPLE_LINK_FLAGS})
+foreach(name reader writer dtype scan scan_to_arrow)
+ add_executable(${name} ${name}.cpp)
+ target_link_libraries(${name} PRIVATE vortex_cxx_shared nanoarrow_shared)
+ if(APPLE)
+ target_link_libraries(${name} PRIVATE
+ "-framework CoreFoundation" "-framework Security")
+ endif()
+endforeach()
diff --git a/vortex-cxx/examples/README.md b/vortex-cxx/examples/README.md
deleted file mode 100644
index 40e7708fa33..00000000000
--- a/vortex-cxx/examples/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# C++ examples
-
-This example shows how to interface with the C++ API using CMake.
-
-```bash
-mkdir -p build
-cd build
-cmake ..
-make -j$(nproc)
-./hello-vortex
-```
\ No newline at end of file
diff --git a/vortex-cxx/examples/dtype.cpp b/vortex-cxx/examples/dtype.cpp
new file mode 100644
index 00000000000..bdaaab1b3df
--- /dev/null
+++ b/vortex-cxx/examples/dtype.cpp
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: CC-BY-4.0
+// SPDX-FileCopyrightText: Copyright the Vortex contributors
+
+#include
+#include
+#include
+
+using vortex::DataSource;
+using vortex::DataType;
+using vortex::DataTypeVariant;
+using vortex::Field;
+using vortex::Session;
+using vortex::VortexException;
+
+static void print_dtype(const DataType &dtype);
+
+static std::string_view ptype_name(vortex::PType p) {
+ using enum vortex::PType;
+ switch (p) {
+ case U8:
+ return "uint8_t";
+ case U16:
+ return "uint16_t";
+ case U32:
+ return "uint32_t";
+ case U64:
+ return "uint64_t";
+ case I8:
+ return "int8_t";
+ case I16:
+ return "int16_t";
+ case I32:
+ return "int32_t";
+ case I64:
+ return "int64_t";
+ case F16:
+ return "float16";
+ case F32:
+ return "float";
+ case F64:
+ return "double";
+ }
+ return "?";
+}
+
+static void print_struct(const DataType &dtype) {
+ std::cout << "struct(\n";
+ for (const Field &field : dtype.fields()) {
+ std::cout << " " << field.name << " = ";
+ print_dtype(field.dtype);
+ }
+ std::cout << ")";
+}
+
+static void print_dtype(const DataType &dtype) {
+ using enum DataTypeVariant;
+ switch (dtype.variant()) {
+ case Null:
+ std::cout << "null";
+ break;
+ case Bool:
+ std::cout << "bool";
+ break;
+ case Utf8:
+ std::cout << "utf8";
+ break;
+ case Binary:
+ std::cout << "binary";
+ break;
+ case Extension:
+ std::cout << "extension";
+ break;
+ case Primitive:
+ std::cout << "primitive(" << ptype_name(dtype.primitive_type()) << ")";
+ break;
+ case Struct:
+ print_struct(dtype);
+ break;
+ case List:
+ std::cout << "list(";
+ print_dtype(dtype.list_element());
+ std::cout << ")";
+ break;
+ case FixedSizeList:
+ std::cout << "fixed_list(";
+ print_dtype(dtype.fixed_size_list_element());
+ std::cout << ")";
+ break;
+ case Decimal:
+ std::cout << "decimal(precision=" << static_cast(dtype.decimal_precision())
+ << ", scale=" << static_cast(dtype.decimal_scale()) << ")";
+ break;
+ }
+ std::cout << (dtype.nullable() ? '?' : ' ') << '\n';
+}
+
+int main(int argc, char **argv) {
+ if (argc != 2) {
+ std::cerr << "Usage: dtype \n";
+ return 1;
+ }
+
+ Session session;
+ DataSource ds = DataSource::open(session, {argv[1]});
+ DataType dt = ds.dtype();
+ std::cout << "dtype: ";
+ print_dtype(dt);
+ return 0;
+}
diff --git a/vortex-cxx/examples/hello-vortex.cpp b/vortex-cxx/examples/hello-vortex.cpp
deleted file mode 100644
index c6a7fec3b9a..00000000000
--- a/vortex-cxx/examples/hello-vortex.cpp
+++ /dev/null
@@ -1,129 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-FileCopyrightText: Copyright the Vortex contributors
-
-#include "nanoarrow/common/inline_types.h"
-#include "nanoarrow/hpp/unique.hpp"
-#include "nanoarrow/nanoarrow.hpp"
-#include "vortex/file.hpp"
-#include "vortex/scan.hpp"
-#include "vortex/write_options.hpp"
-#include
-#include
-#include
-#include
-#include
-
-/// Create test data with structure {a: [10, 20, 30, 40, 50], b: [100, 200, 300, 400, 500]}
-ArrowArrayStream CreateTestDataStream() {
- // Create a simple two-column struct with int32 data
- // Schema: struct{a: int32, b: int32}
- nanoarrow::UniqueSchema schema;
- ArrowSchemaInit(schema.get());
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetType(schema.get(), NANOARROW_TYPE_STRUCT));
- NANOARROW_THROW_NOT_OK(ArrowSchemaAllocateChildren(schema.get(), 2));
- ArrowSchemaInit(schema->children[0]);
- ArrowSchemaInit(schema->children[1]);
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetName(schema->children[0], "a"));
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetType(schema->children[0], NANOARROW_TYPE_INT32));
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetName(schema->children[1], "b"));
- NANOARROW_THROW_NOT_OK(ArrowSchemaSetType(schema->children[1], NANOARROW_TYPE_INT32));
-
- // Create arrays for each field
- nanoarrow::UniqueArray field_a, field_b;
- NANOARROW_THROW_NOT_OK(ArrowArrayInitFromType(field_a.get(), NANOARROW_TYPE_INT32));
- NANOARROW_THROW_NOT_OK(ArrowArrayInitFromType(field_b.get(), NANOARROW_TYPE_INT32));
-
- // Reserve for 5 elements
- NANOARROW_THROW_NOT_OK(ArrowArrayStartAppending(field_a.get()));
- NANOARROW_THROW_NOT_OK(ArrowArrayStartAppending(field_b.get()));
-
- // Add data: a=[10, 20, 30, 40, 50], b=[100, 200, 300, 400, 500]
- std::vector data_a = {10, 20, 30, 40, 50};
- std::vector data_b = {100, 200, 300, 400, 500};
- for (size_t i = 0; i < data_a.size(); ++i) {
- NANOARROW_THROW_NOT_OK(ArrowArrayAppendInt(field_a.get(), data_a[i]));
- NANOARROW_THROW_NOT_OK(ArrowArrayAppendInt(field_b.get(), data_b[i]));
- }
-
- NANOARROW_THROW_NOT_OK(ArrowArrayFinishBuilding(field_a.get(), NANOARROW_VALIDATION_LEVEL_NONE, nullptr));
- NANOARROW_THROW_NOT_OK(ArrowArrayFinishBuilding(field_b.get(), NANOARROW_VALIDATION_LEVEL_NONE, nullptr));
-
- // Create struct array
- nanoarrow::UniqueArray struct_array;
- NANOARROW_THROW_NOT_OK(ArrowArrayInitFromType(struct_array.get(), NANOARROW_TYPE_STRUCT));
- NANOARROW_THROW_NOT_OK(ArrowArrayAllocateChildren(struct_array.get(), 2));
- struct_array->length = 5;
- ArrowArrayMove(field_a.get(), struct_array->children[0]);
- ArrowArrayMove(field_b.get(), struct_array->children[1]);
-
- // Create vector and move array into it
- std::vector arrays;
- arrays.push_back(std::move(struct_array));
-
- // Create stream
- ArrowArrayStream stream;
- nanoarrow::VectorArrayStream vector_stream(schema.get(), std::move(arrays));
- vector_stream.ToArrayStream(&stream);
-
- return stream;
-}
-
-int main() {
- // Create a temporary file path
- std::filesystem::path temp_dir = std::filesystem::temp_directory_path();
- std::string vortex_file = (temp_dir / "hello_vortex_example.vortex").string();
-
- std::cout << "=== Vortex C++ Example ===" << '\n';
- std::cout << "Writing to: " << vortex_file << '\n';
-
- // Write test data to a Vortex file
- {
- auto stream = CreateTestDataStream();
- vortex::VortexWriteOptions write_options;
- write_options.WriteArrayStream(stream, vortex_file);
- std::cout << "Wrote test data to file" << '\n';
- }
-
- auto check_stream = [](ArrowArrayStream &stream) {
- nanoarrow::UniqueArray array;
- int get_next_result = stream.get_next(&stream, array.get());
- assert(get_next_result == 0);
- std::cout << "Number of rows: " << array->length << '\n';
- std::cout << "Number of columns in schema: " << array->n_children << '\n';
- };
-
- // 1. Classic C++ builder pattern
- std::cout << "\n1. Classic C++ builder pattern:" << '\n';
- {
- auto builder = vortex::VortexFile::Open(vortex_file).CreateScanBuilder();
- builder.WithLimit(3);
- auto stream = std::move(builder).IntoStream();
- check_stream(stream);
- }
- // 2. One-line Rusty function chain
- std::cout << "\n2. One-line Rusty function chain:" << '\n';
- {
- auto stream = vortex::VortexFile::Open(vortex_file).CreateScanBuilder().WithLimit(3).IntoStream();
- check_stream(stream);
- }
- // 3. Conditionally set the builder
- std::cout << "\n3. Conditionally set the builder:" << '\n';
- {
- auto limit = 1;
- auto builder = vortex::VortexFile::Open(vortex_file).CreateScanBuilder();
- if (limit > 0) {
- // prefer C++ way
- builder.WithLimit(1);
- // Rusty way is Ok, but you have to move the builder to an rvalue.
- // builder = std::move(builder).WithLimit(3);
- }
- auto stream = std::move(builder).IntoStream();
- check_stream(stream);
- }
-
- // Clean up
- std::filesystem::remove(vortex_file);
- std::cout << "\nCleaned up temporary file" << '\n';
-
- return 0;
-}
\ No newline at end of file
diff --git a/vortex-cxx/examples/reader.cpp b/vortex-cxx/examples/reader.cpp
new file mode 100644
index 00000000000..48656c046f7
--- /dev/null
+++ b/vortex-cxx/examples/reader.cpp
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright the Vortex contributors
+
+#include
+
+#include
+#include
+
+using namespace std::string_view_literals;
+using namespace vortex;
+using namespace expr;
+using namespace ops; // overloaded >= for Expressions
+namespace fs = std::filesystem;
+
+int main() {
+ const Session session;
+ const DataSource ds = DataSource::open(session, {"people*.vortex", "me.vortex"});
+ Scan scan = ds.scan({.filter = col("height") >= lit(50)});
+
+ for (Partition &partition : scan.partitions()) {
+ for (Array &array : partition.batches()) {
+ const Array age = array.field("age");
+ const PrimitiveView age_view = age.values(session);
+ const std::span age_values = age_view.values();
+ for (uint8_t value : age_values) {
+ std::cout << int(value) << " ";
+ }
+ }
+ }
+ std::cout << "\n";
+
+ return 0;
+}
diff --git a/vortex-cxx/examples/scan.cpp b/vortex-cxx/examples/scan.cpp
new file mode 100644
index 00000000000..5a8d6fcf14c
--- /dev/null
+++ b/vortex-cxx/examples/scan.cpp
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: CC-BY-4.0
+// SPDX-FileCopyrightText: Copyright the Vortex contributors
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+using vortex::DataSource;
+using vortex::Estimate;
+using vortex::Scan;
+using vortex::Session;
+
+static void print_estimate(const char *what, const Estimate &est) {
+ if (!est.known()) {
+ std::cout << what << ": unknown\n";
+ } else if (est.exact()) {
+ std::cout << what << ": " << est.value() << '\n';
+ } else {
+ std::cout << what << ": at most " << est.value() << '\n';
+ }
+}
+
+struct ScanStats {
+ size_t partitions = 0;
+ size_t arrays = 0;
+ size_t rows = 0;
+};
+
+static ScanStats worker(Scan &scan) {
+ ScanStats stats;
+ while (auto partition = scan.next_partition()) {
+ ++stats.partitions;
+ while (auto array = partition->next()) {
+ ++stats.arrays;
+ stats.rows += array->size();
+ }
+ }
+ return stats;
+}
+
+int main(int argc, char **argv) {
+ size_t num_threads = 0;
+ int opt = 0;
+ while ((opt = getopt(argc, argv, "j:")) != -1) {
+ switch (opt) {
+ case 'j':
+ num_threads = static_cast(std::atoi(optarg));
+ break;
+ default:
+ std::cerr << "Multi-threaded file scan\nUsage: scan [-j "
+ "threads] \n";
+ return 1;
+ }
+ }
+ if (optind + 1 != argc) {
+ std::cerr << "Multi-threaded file scan\nUsage: scan [-j threads] \n";
+ return 1;
+ }
+
+ const Session session;
+ std::cout << "Opening files: " << argv[optind] << '\n';
+ const DataSource ds = DataSource::open(session, {argv[optind]});
+
+ print_estimate("Data source row count", ds.row_count());
+
+ Scan scan = ds.scan();
+ print_estimate("Partition count", scan.partition_count());
+
+ if (num_threads == 0) {
+ num_threads = scan.partition_count().value_or(1);
+ }
+
+ std::cout << "Starting scan, using " << num_threads << " threads\n";
+ std::vector threads;
+ threads.reserve(num_threads);
+ std::vector results(num_threads);
+
+ for (size_t i = 0; i < num_threads; ++i) {
+ threads.emplace_back([i, &scan, &results] { results[i] = worker(scan); });
+ }
+ for (auto &t : threads) {
+ t.join();
+ }
+
+ ScanStats total;
+ for (const auto &r : results) {
+ total.partitions += r.partitions;
+ total.arrays += r.arrays;
+ total.rows += r.rows;
+ }
+ std::cout << "Finished scan, processed " << total.partitions << " partitions, " << total.arrays
+ << " arrays, " << total.rows << " rows\n";
+ return 0;
+}
diff --git a/vortex-cxx/examples/scan_to_arrow.cpp b/vortex-cxx/examples/scan_to_arrow.cpp
new file mode 100644
index 00000000000..707cd4a4103
--- /dev/null
+++ b/vortex-cxx/examples/scan_to_arrow.cpp
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: CC-BY-4.0
+// SPDX-FileCopyrightText: Copyright the Vortex contributors
+
+#include
+
+typedef struct ArrowSchema FFI_ArrowSchema;
+typedef struct ArrowArray FFI_ArrowArray;
+typedef struct ArrowArrayStream FFI_ArrowArrayStream;
+#define USE_OWN_ARROW 1
+
+#include
+#include
+#include
+#include
+
+using vortex::ArrowStream;
+using vortex::DataSource;
+using vortex::DataType;
+using vortex::Scan;
+using vortex::Session;
+using vortex::VortexException;
+
+int main(int argc, char **argv) {
+ if (argc != 2) {
+ std::cerr << "Scan vortex files to Arrow\nUsage: scan_to_arrow \n";
+ return 1;
+ }
+ const char *paths = argv[1];
+
+ Session session;
+ DataSource ds = DataSource::open(session, {paths});
+ Scan scan = ds.scan();
+
+ DataType out_dtype = ds.dtype();
+ ArrowSchema schema = out_dtype.to_arrow();
+ char schema_buf[10 * 1024];
+ const int64_t schema_len = ArrowSchemaToString(&schema, schema_buf, sizeof schema_buf, 1);
+ std::cout << "arrow schema: " << std::string_view {schema_buf, static_cast(schema_len)} << '\n';
+ if (schema.release != nullptr) {
+ schema.release(&schema);
+ }
+
+ ArrowError arrow_error {};
+ ArrowErrorInit(&arrow_error);
+
+ size_t partition_idx = 0;
+ while (auto partition = scan.next_partition()) {
+ ArrowStream stream = std::move(*partition).into_arrow_stream();
+
+ size_t rows = 0;
+ size_t arrays = 0;
+ ArrowArray array = {};
+ while (ArrowArrayStreamGetNext(stream.raw(), &array, &arrow_error) == NANOARROW_OK &&
+ array.release != nullptr) {
+ rows += array.length;
+ ++arrays;
+ array.release(&array);
+ std::memset(&array, 0, sizeof(array));
+ }
+ std::cout << "Read partition " << partition_idx << " to Arrow, " << arrays << " arrays, " << rows
+ << " rows\n";
+ ++partition_idx;
+ }
+ return 0;
+}
diff --git a/vortex-cxx/examples/writer.cpp b/vortex-cxx/examples/writer.cpp
new file mode 100644
index 00000000000..b4c88e458e1
--- /dev/null
+++ b/vortex-cxx/examples/writer.cpp
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: CC-BY-4.0
+// SPDX-FileCopyrightText: Copyright the Vortex contributors
+#include
+#include
+#include
+
+#include
+#include
+
+using namespace vortex;
+using namespace expr::ops;
+using dtype::Nullable;
+
+int main(int argc, char **argv) {
+ const Session session;
+ const DataType dtype = dtype::struct_({
+ {"age", dtype::uint8()},
+ {"height", dtype::uint16(Nullable)},
+ });
+
+ constexpr size_t SAMPLE_ROWS = 100;
+ std::vector age_buffer(SAMPLE_ROWS);
+ std::vector height_buffer(SAMPLE_ROWS);
+ for (size_t i = 0; i < SAMPLE_ROWS; ++i) {
+ age_buffer[i] = static_cast(i);
+ height_buffer[i] = static_cast((i + 1) % 200);
+ }
+
+ Array age = Array::primitive(age_buffer);
+ Array array = make_struct({
+ {"age", age},
+ {"height", Array::primitive(height_buffer, AllValid)},
+ });
+
+ Expression age_gt_10 = expr::col("age") > expr::lit(10);
+ Array validity_array = array.apply(age_gt_10);
+
+ const Validity validity {ValidityType::Array, validity_array};
+ Array array2 = make_struct({
+ {"age", age},
+ {"height", Array::primitive(height_buffer, validity)},
+ });
+
+ Writer writer = Writer::open(session, argv[1], dtype);
+ writer.push(array);
+ writer.push(array2);
+ writer.finish();
+
+ return 0;
+}
diff --git a/vortex-cxx/include/vortex/array.hpp b/vortex-cxx/include/vortex/array.hpp
new file mode 100644
index 00000000000..bd9a761278c
--- /dev/null
+++ b/vortex-cxx/include/vortex/array.hpp
@@ -0,0 +1,396 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright the Vortex contributors
+#pragma once
+
+#include "vortex/common.hpp"
+#include "vortex/dtype.hpp"
+#include "vortex/error.hpp"
+#include "vortex/expression.hpp"
+#include "vortex/session.hpp"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace vortex {
+
+// Types that a PrimitiveView can hold
+template
+concept primitive_view = primitive_type || std::is_same_v;
+
+template
+class PrimitiveView;
+
+class Array;
+class StringView;
+class BytesView;
+
+enum class ValidityType {
+ // Items can't be null
+ NonNullable = VX_VALIDITY_NON_NULLABLE,
+ // All items are valid
+ AllValid = VX_VALIDITY_ALL_VALID,
+ // All items are invalid
+ AllInvalid = VX_VALIDITY_ALL_INVALID,
+ // Item validity is set in a boolean array: true = valid, false = invalid
+ Array = VX_VALIDITY_ARRAY,
+};
+
+inline constexpr ValidityType NonNullable = ValidityType::NonNullable;
+inline constexpr ValidityType AllValid = ValidityType::AllValid;
+inline constexpr ValidityType AllInvalid = ValidityType::AllInvalid;
+
+/**
+ * Array per-element validity of type ValidityType.
+ * If ValidityType is ValidityType::Array, holds a boolean array
+ * with validity items.
+ *
+ * You can use shortcut constants NonNullable/AllValid/AllInvalid and
+ * function ValidityArray(validity_bools);
+ */
+class Validity {
+public:
+ // NOLINTNEXTLINE(google-explicit-constructor) for NonNullable constructor
+ Validity(ValidityType type) : type_(type), array_(nullptr) {}
+ Validity(ValidityType type, const Array &array);
+
+ Validity(const Validity &other);
+ Validity(Validity &&other) noexcept;
+ Validity &operator=(const Validity &other);
+ Validity &operator=(Validity &&other) noexcept;
+ ~Validity();
+
+ ValidityType type() const noexcept { return type_; }
+
+ // Boolean validity array. Throws if type() != ValidityType::Array.
+ Array array() const;
+
+private:
+ friend struct detail::Access;
+ friend Validity ValidityArray(const Array &bools);
+ Validity(ValidityType type, const vx_array *owned) : type_(type), array_(owned) {}
+
+ ValidityType type_;
+ const vx_array *array_;
+};
+
+// Validity determined by a boolean array, true = valid, false = invalid.
+Validity ValidityArray(const Array &bools);
+
+namespace detail {
+// Validity bitmap for typed views. Owns the arrays that back the bits
+class ValidityBits {
+public:
+ ValidityBits(ValidityBits &&other) noexcept;
+ ValidityBits &operator=(ValidityBits &&other) noexcept;
+ ValidityBits(const ValidityBits &) = delete;
+ ValidityBits &operator=(const ValidityBits &) = delete;
+ ~ValidityBits();
+
+ bool is_null(size_t index) const noexcept;
+
+private:
+ friend class vortex::Array;
+ // Materialize validity of "canonical"
+ ValidityBits(const Session &session, const vx_array *canonical);
+
+ const vx_array *owner_ = nullptr;
+ const uint8_t *bits_ = nullptr;
+ size_t bit_offset_ = 0;
+ bool all_invalid_ = false;
+};
+} // namespace detail
+
+// A reference-counted handle to columnar data in some encoding
+class Array {
+public:
+ Array(const Array &other);
+ Array(Array &&) noexcept = default;
+ Array &operator=(const Array &other);
+ Array &operator=(Array &&) noexcept = default;
+
+ // An all-null array with DataType Null.
+ static Array null(size_t len);
+
+ /**
+ * A Primitive array copied from a typed buffer.
+ *
+ * Example:
+ *
+ * std::array buffer = {0, 1, 2};
+ * auto array = Array::primitive(buffer);
+ */
+ template
+ static Array primitive(std::span data, const Validity &validity = NonNullable) {
+ return primitive_raw(detail::to_ptype(), data.data(), data.size(), validity);
+ }
+
+ /**
+ * Import an Arrow array. Consumes both "array" and "schema", do not use
+ * or release them afterwards. For a record batch pass nullable = false.
+ */
+ static Array from_arrow(ArrowArray *array, ArrowSchema *schema, bool nullable);
+
+ size_t size() const;
+ bool nullable() const;
+ bool has_dtype(DataTypeVariant variant) const;
+ bool is_primitive(PType ptype) const;
+ DataType dtype() const;
+ Validity validity() const;
+ size_t null_count() const;
+
+ // Struct field accessors. Throw if Array's DataType is not Struct
+
+ Array field(size_t index) const;
+ Array field(std::string_view name) const;
+
+ /*
+ * Zero-copy view of rows [begin, end).
+ *
+ * Example:
+ *
+ * std::array buffer = {0, 1, 2};
+ * auto array = Array::primitive(buffer);
+ * auto sliced = array.slice(1, 2);
+ */
+ Array slice(size_t begin, size_t end) const;
+
+ /**
+ * Apply an expression to an array.
+ *
+ * This function operates in constant time and doesn't execute the result
+ * array. To execute the array, canonicalise it.
+ *
+ * Example:
+ *
+ * using namespace vortex::expr::ops;
+ *
+ * std::array buffer = {0, 1, 2};
+ * Array array = Array::primitive(buffer);
+ * Expression expr = expr::root() > expr::lit(0);
+ * Array result = array.apply(expr);
+ */
+ Array apply(const Expression &expr) const;
+
+ /**
+ * Bulk view over values. Canonicalizes the array.
+ * Throws if T does not match the array's ptype.
+ *
+ *
+ * Example:
+ *
+ * const Session session;
+ * std::array buffer = {0, 1, 2};
+ * Array array = Array::primitive(buffer);
+ * auto view = array.values(session);
+ */
+ template
+ PrimitiveView values(const Session &session) const;
+
+ // Bulk view over Bool values
+ PrimitiveView bools(const Session &session) const;
+
+ // Bulk view over Utf8 values.
+ StringView strings(const Session &session) const;
+
+ // Bulk view over Binary values.
+ BytesView bytes(const Session &session) const;
+
+private:
+ friend struct detail::Access;
+ friend class StringView;
+ friend class BytesView;
+ template
+ friend class PrimitiveView;
+
+ explicit Array(const vx_array *owned) : handle_(owned) {}
+ const vx_array *release() && noexcept { return handle_.release(); }
+
+ static Array primitive_raw(vx_ptype ptype, const void *data, size_t len, const Validity &validity);
+ Array canonicalize(const Session &session) const;
+
+ struct Deleter {
+ void operator()(const vx_array *ptr) const noexcept;
+ };
+ std::unique_ptr handle_;
+};
+
+/**
+ * Create a Struct array from named columns of equal length.
+ *
+ * Example:
+ *
+ * std::array age_buffer = {0, 1, 2};
+ * std::array height_buffer = {0, 1, 2};
+ * Array ages = Array::primitive(age_buffer);
+ * Array heights = Array::primitive(height_buffer);
+ * Array result = make_struct(
+ * {{"age", ages}, {"height", heights}},
+ * NonNullable);
+ */
+Array make_struct(std::initializer_list> fields,
+ const Validity &validity = NonNullable);
+
+/**
+ * Create a Struct Array from a dynamic number of fields.
+ * Prefer make_struct() if you know all fields beforehand.
+ *
+ * Example:
+ *
+ * std::array age_buffer = {0, 1, 2};
+ * std::array height_buffer = {0, 1, 2};
+ * Array ages = Array::primitive(age_buffer);
+ * Array heights = Array::primitive(height_buffer);
+ *
+ * StructArrayBuilder b(NonNullable, 2);
+ * Array result = builder
+ * .add("age", ages)
+ * .add("height", heights)
+ * .build();
+ */
+class StructArrayBuilder {
+public:
+ explicit StructArrayBuilder(const Validity &validity, size_t capacity = 0);
+ StructArrayBuilder(const StructArrayBuilder &) = delete;
+ StructArrayBuilder &operator=(const StructArrayBuilder &) = delete;
+ StructArrayBuilder(StructArrayBuilder &&) noexcept = default;
+ StructArrayBuilder &operator=(StructArrayBuilder &&) noexcept = default;
+
+ StructArrayBuilder &add(std::string_view name, const Array &field) &;
+ StructArrayBuilder &&add(std::string_view name, const Array &field) &&;
+
+ // Consume the builder and return and Array
+ Array build() &&;
+
+private:
+ struct Deleter {
+ void operator()(vx_struct_column_builder *ptr) const noexcept;
+ };
+ std::unique_ptr handle_;
+};
+
+/**
+ * Typed read-only view over a Primitive array.
+ *
+ * Owns a canonicalized copy of Array. values() and anything derived from
+ * it are valid as long as the view lives.
+ */
+template
+class PrimitiveView {
+public:
+ /*
+ * Get raw values from this view. Values at null/invalid positions are
+ * unspecified.
+ */
+ std::span values() const noexcept { return {data_, size_}; }
+ bool is_null(size_t index) const noexcept { return validity_.is_null(index); }
+ size_t size() const noexcept { return size_; }
+
+private:
+ friend class Array;
+ PrimitiveView(Array canonical, detail::ValidityBits validity, const T *data, size_t size)
+ : canonical_(std::move(canonical)), validity_(std::move(validity)), data_(data), size_(size) {}
+
+ Array canonical_;
+ detail::ValidityBits validity_;
+ const T *data_;
+ size_t size_;
+};
+
+/**
+ * Read-only view over a Bool array. As Bool values are bit-packed, there's no
+ * span. Read individual values with value(i).
+ */
+template <>
+class PrimitiveView {
+public:
+ /*
+ * Get raw value from this view. Values at null/invalid positions are
+ * unspecified.
+ */
+ bool value(size_t index) const;
+ bool is_null(size_t index) const noexcept { return validity_.is_null(index); }
+ size_t size() const noexcept { return size_; }
+
+private:
+ friend class Array;
+ PrimitiveView(Array canonical, detail::ValidityBits validity, size_t size)
+ : canonical_(std::move(canonical)), validity_(std::move(validity)), size_(size) {}
+
+ Array canonical_;
+ detail::ValidityBits validity_;
+ size_t size_;
+};
+
+/**
+ * Read-only view over a Utf8 array.
+ *
+ * operator[] is O(1) and borrows from the view's canonical copy. Returned
+ * string_views are valid as long as the view lives.
+ */
+class StringView {
+public:
+ /*
+ * Get raw value from this view. Values at null/invalid positions are
+ * unspecified.
+ */
+ std::string_view operator[](size_t index) const;
+ bool is_null(size_t index) const noexcept { return validity_.is_null(index); }
+ size_t size() const noexcept { return size_; }
+
+private:
+ friend class Array;
+ StringView(Array canonical, detail::ValidityBits validity, size_t size)
+ : canonical_(std::move(canonical)), validity_(std::move(validity)), size_(size) {}
+
+ Array canonical_;
+ detail::ValidityBits validity_;
+ size_t size_;
+};
+
+/**
+ * Read-only view over a Binary array.
+ *
+ * Byte spans borrow from the view's canonical copy and are valid as long as
+ * the view lives.
+ */
+class BytesView {
+public:
+ /*
+ * Get raw value from this view. Values at null/invalid positions are
+ * unspecified.
+ */
+ BinaryView operator[](size_t index) const;
+ bool is_null(size_t index) const noexcept { return validity_.is_null(index); }
+ size_t size() const noexcept { return size_; }
+
+private:
+ friend class Array;
+ BytesView(Array canonical, detail::ValidityBits validity, size_t size)
+ : canonical_(std::move(canonical)), validity_(std::move(validity)), size_(size) {}
+
+ Array canonical_;
+ detail::ValidityBits validity_;
+ size_t size_;
+};
+
+template
+PrimitiveView Array::values(const Session &session) const {
+ Array canonical = canonicalize(session);
+ const vx_array *raw = detail::Access::c_ptr(canonical);
+ if (!vx_array_is_primitive(raw, detail::to_ptype())) {
+ throw VortexException("values: T does not match the array's ptype", ErrorCode::MismatchedTypes);
+ }
+ vx_error *error = nullptr;
+ const void *data = vx_array_data_ptr_primitive(raw, &error);
+ detail::throw_on_error(error);
+ detail::ValidityBits validity(session, raw);
+ const size_t n = vx_array_len(raw);
+ return PrimitiveView(std::move(canonical), std::move(validity), static_cast(data), n);
+}
+} // namespace vortex
diff --git a/vortex-cxx/include/vortex/common.hpp b/vortex-cxx/include/vortex/common.hpp
new file mode 100644
index 00000000000..79c79491520
--- /dev/null
+++ b/vortex-cxx/include/vortex/common.hpp
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright the Vortex contributors
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+namespace vortex {
+#if __STDCPP_FLOAT64_T__ != 1
+struct float16_t {
+ uint16_t bits;
+ friend bool operator==(float16_t, float16_t) = default;
+};
+static_assert(sizeof(float16_t) == 2 && std::is_trivially_copyable_v);
+#else
+#include
+using float16_t = std::float16_t;
+#endif
+
+float to_float(float16_t f16t);
+} // namespace vortex
+
+namespace vortex::detail {
+struct Access {
+ template
+ static T adopt(Args &&...args) {
+ return T(std::forward(args)...);
+ }
+ template
+ static auto release(T &&t) noexcept {
+ return std::forward(t).release();
+ }
+ template
+ static auto c_ptr(const T &t) noexcept {
+ return t.handle_.get();
+ }
+};
+
+template
+constexpr vx_ptype to_ptype() {
+ if constexpr (std::is_same_v) {
+ return PTYPE_U8;
+ } else if constexpr (std::is_same_v) {
+ return PTYPE_U16;
+ } else if constexpr (std::is_same_v) {
+ return PTYPE_U32;
+ } else if constexpr (std::is_same_v) {
+ return PTYPE_U64;
+ } else if constexpr (std::is_same_v) {
+ return PTYPE_I8;
+ } else if constexpr (std::is_same_v) {
+ return PTYPE_I16;
+ } else if constexpr (std::is_same_v) {
+ return PTYPE_I32;
+ } else if constexpr (std::is_same_v) {
+ return PTYPE_I64;
+ } else if constexpr (std::is_same_v) {
+ return PTYPE_F16;
+ } else if constexpr (std::is_same_v) {
+ return PTYPE_F32;
+ } else {
+ static_assert(std::is_same_v);
+ return PTYPE_F64;
+ }
+}
+
+template
+inline constexpr bool is_numeric_element =
+ std::is_same_v || std::is_same_v || std::is_same_v ||
+ std::is_same_v || std::is_same_v || std::is_same_v ||
+ std::is_same_v || std::is_same_v || std::is_same_v ||
+ std::is_same_v || std::is_same_v;
+} // namespace vortex::detail
+
+namespace vortex {
+
+// View over a single Binary byte range.
+using BinaryView = std::span;
+
+// Types that can be stored in a Primitive array
+template
+concept primitive_type = detail::is_numeric_element;
+
+// Types constructible as scalars or literals.
+template
+concept element_type = primitive_type || std::is_same_v || std::is_same_v ||
+ std::is_same_v;
+} // namespace vortex
diff --git a/vortex-cxx/include/vortex/data_source.hpp b/vortex-cxx/include/vortex/data_source.hpp
new file mode 100644
index 00000000000..f48e305f737
--- /dev/null
+++ b/vortex-cxx/include/vortex/data_source.hpp
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright the Vortex contributors
+#pragma once
+
+#include "vortex/common.hpp"
+#include "vortex/estimate.hpp"
+#include "vortex/scan.hpp"
+#include "vortex/session.hpp"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+namespace vortex {
+
+/**
+ * A reference to one or more (possibly remote, possibly glob) paths.
+ *
+ * Creating a DataSource opens the first matched path to read the schema. All
+ * other IO is deferred until a scan is requested. Multiple scans may be
+ * requested from one data source.
+ */
+class DataSource {
+public:
+ static DataSource open(const Session &session, std::initializer_list paths);
+ static DataSource open(const Session &session, std::span paths);
+ static DataSource open(const Session &session, std::span paths);
+
+ /**
+ * Create a DataSource from an in-memory Vortex file. Borrows the buffer:
+ * caller must keep it alive and unmodified while DataSource is alive.
+ */
+ static DataSource from_buffer(const Session &session, std::span data);
+
+ DataSource(const DataSource &other);
+ DataSource(DataSource &&) noexcept = default;
+ DataSource &operator=(const DataSource &other);
+ DataSource &operator=(DataSource &&) noexcept = default;
+
+ Estimate row_count() const;
+ DataType dtype() const;
+ Scan scan(ScanOptions options = {}) const;
+
+private:
+ friend struct detail::Access;
+ DataSource(const vx_data_source *owned, Session session) : handle_(owned), session_(std::move(session)) {}
+
+ struct Deleter {
+ void operator()(const vx_data_source *ptr) const noexcept;
+ };
+ std::unique_ptr handle_;
+ Session session_;
+};
+} // namespace vortex
diff --git a/vortex-cxx/include/vortex/dtype.hpp b/vortex-cxx/include/vortex/dtype.hpp
new file mode 100644
index 00000000000..e3e272e3312
--- /dev/null
+++ b/vortex-cxx/include/vortex/dtype.hpp
@@ -0,0 +1,187 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright the Vortex contributors
+#pragma once
+
+#include "vortex/common.hpp"
+
+#include
+
+#include