From aa7d5a074a7fc2f68a4ad3e1401d452362af8237 Mon Sep 17 00:00:00 2001 From: Mikhail Kot Date: Tue, 14 Jul 2026 11:44:40 +0100 Subject: [PATCH 1/2] C++ api tests Signed-off-by: Mikhail Kot --- .github/workflows/ci.yml | 7 +- .github/workflows/rust-instrumented.yml | 38 ++- docs/api/cpp/index.rst | 9 +- lang/cpp/.gitignore | 1 + lang/cpp/CMakeLists.txt | 23 ++ lang/cpp/README.md | 8 + lang/cpp/gcov-report.sh | 14 + lang/cpp/include/vortex/array.hpp | 4 +- lang/cpp/include/vortex/common.hpp | 42 ++- lang/cpp/include/vortex/scan.hpp | 7 +- lang/cpp/include/vortex/writer.hpp | 4 + lang/cpp/src/array.cpp | 17 +- lang/cpp/src/f16.cpp | 40 --- lang/cpp/src/writer.cpp | 17 +- lang/cpp/tests/CMakeLists.txt | 24 ++ lang/cpp/tests/array.cpp | 184 +++++++++++++ lang/cpp/tests/arrow.cpp | 115 ++++++++ lang/cpp/tests/common.hpp | 110 ++++++++ lang/cpp/tests/dtype.cpp | 81 ++++++ lang/cpp/tests/enum_sizes.cpp | 15 + lang/cpp/tests/expression.cpp | 102 +++++++ lang/cpp/tests/float16_t.cpp | 35 +++ lang/cpp/tests/float16_t_compat.cpp | 38 +++ lang/cpp/tests/scalar.cpp | 100 +++++++ lang/cpp/tests/scan.cpp | 348 ++++++++++++++++++++++++ lang/cpp/tests/session.cpp | 17 ++ lang/cpp/tests/string_binary.cpp | 133 +++++++++ lang/cpp/tests/writer.cpp | 53 ++++ vortex-ffi/src/array.rs | 59 ++-- 29 files changed, 1560 insertions(+), 85 deletions(-) create mode 100644 lang/cpp/.gitignore create mode 100755 lang/cpp/gcov-report.sh delete mode 100644 lang/cpp/src/f16.cpp create mode 100644 lang/cpp/tests/CMakeLists.txt create mode 100644 lang/cpp/tests/array.cpp create mode 100644 lang/cpp/tests/arrow.cpp create mode 100644 lang/cpp/tests/common.hpp create mode 100644 lang/cpp/tests/dtype.cpp create mode 100644 lang/cpp/tests/enum_sizes.cpp create mode 100644 lang/cpp/tests/expression.cpp create mode 100644 lang/cpp/tests/float16_t.cpp create mode 100644 lang/cpp/tests/float16_t_compat.cpp create mode 100644 lang/cpp/tests/scalar.cpp create mode 100644 lang/cpp/tests/scan.cpp create mode 100644 lang/cpp/tests/session.cpp create mode 100644 lang/cpp/tests/string_binary.cpp create mode 100644 lang/cpp/tests/writer.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7aae5dc647..5d57716ecc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -211,7 +211,7 @@ jobs: # including all private docs. cargo doc --profile ci --no-deps --document-private-items --workspace --exclude vortex-python --exclude vortex-python-cuda # nextest doesn't support doc tests, so we run it here - cargo test --profile ci --doc --workspace --all-features --exclude vortex-cxx --exclude vortex-jni --exclude vortex-ffi --exclude xtask --exclude vortex-python-cuda --no-fail-fast + cargo test --profile ci --doc --workspace --all-features --exclude vortex-jni --exclude vortex-ffi --exclude xtask --exclude vortex-python-cuda --no-fail-fast build-rust: name: "Rust build (${{matrix.config.name}})" @@ -553,8 +553,11 @@ jobs: - name: Build C++ library (asan) run: | mkdir -p build - cmake -S lang/cpp -B build -DSANITIZER=asan -DTARGET_TRIPLE="x86_64-unknown-linux-gnu" + cmake -S lang/cpp -Bbuild -DSANITIZER=asan -DBUILD_TESTS=1 -DTARGET_TRIPLE="x86_64-unknown-linux-gnu" cmake --build build --parallel $(nproc) + - name: Run C++ tests + run: | + build/tests/vortex_cxx_test sqllogic-test: name: "SQL logic tests" diff --git a/.github/workflows/rust-instrumented.yml b/.github/workflows/rust-instrumented.yml index 5430f1512fc..f4ff732be74 100644 --- a/.github/workflows/rust-instrumented.yml +++ b/.github/workflows/rust-instrumented.yml @@ -101,7 +101,7 @@ jobs: --ignore 'home/*' --ignore 'xtask/*' --ignore 'target/*' --ignore 'vortex-error/*' \ --ignore 'vortex-python/*' --ignore 'vortex-jni/*' --ignore 'vortex-flatbuffers/*' \ --ignore 'vortex-proto/*' --ignore 'vortex-tui/*' --ignore 'vortex-datafusion/examples/*' \ - --ignore 'vortex-ffi/examples/*' --ignore '*/arbitrary/*' --ignore '*/arbitrary.rs' --ignore 'vortex-cxx/*' \ + --ignore 'vortex-ffi/examples/*' --ignore '*/arbitrary/*' --ignore '*/arbitrary.rs' \ --ignore benchmarks/* --ignore 'vortex-test/*' \ -o ${{ env.GRCOV_OUTPUT_FILE }} test -s ${{ env.GRCOV_OUTPUT_FILE }} @@ -114,6 +114,42 @@ jobs: flags: ${{ matrix.suite }} use_oidc: true + cxx-coverage: + name: "C++ API (coverage)" + timeout-minutes: 15 + permissions: + id-token: write + 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-coverage', github.run_id) + || 'ubuntu-latest' }} + steps: + - uses: runs-on/action@v2 + if: github.repository == 'vortex-data/vortex' + with: + sccache: s3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: ./.github/actions/setup-prebuild + with: + enable-sccache: "true" + - name: Install lcov + run: | + sudo apt-get update + sudo apt-get install -y lcov libjson-xs-perl + - name: Build FFI library + run: cargo build -p vortex-ffi + - name: Build and test C++ API with coverage + working-directory: lang/cpp + run: ./gcov-report.sh + - name: Codecov + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + with: + name: cxx-api + files: lang/cpp/coverage.info + disable_search: true + flags: cxx-api + use_oidc: true + rust-test-sanitizer: strategy: fail-fast: false diff --git a/docs/api/cpp/index.rst b/docs/api/cpp/index.rst index 6d1f33b177d..642b3d18ce9 100644 --- a/docs/api/cpp/index.rst +++ b/docs/api/cpp/index.rst @@ -164,8 +164,8 @@ Source code for this example is `writer.cpp .. code-block:: cpp - const Session session; - const DataType dtype = dtype::struct_({ + Session session; + DataType dtype = dtype::struct_({ {"age", dtype::uint8()}, {"height", dtype::uint16(Nullable)}, }); @@ -187,15 +187,14 @@ Source code for this example is `writer.cpp Expression age_gt_10 = expr::gt(expr::col("age"), expr::lit(10)); Array validity_array = array.apply(age_gt_10); - const Validity validity = Validity::from_array(validity_array); + Validity validity = Validity::from_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.push({array, array2}); writer.finish(); DataType diff --git a/lang/cpp/.gitignore b/lang/cpp/.gitignore new file mode 100644 index 00000000000..3ea99d6c913 --- /dev/null +++ b/lang/cpp/.gitignore @@ -0,0 +1 @@ +coverage* diff --git a/lang/cpp/CMakeLists.txt b/lang/cpp/CMakeLists.txt index 28099a44baa..8949c1977cc 100644 --- a/lang/cpp/CMakeLists.txt +++ b/lang/cpp/CMakeLists.txt @@ -21,6 +21,8 @@ else () message(STATUS "Sccache not found") endif () +option(BUILD_TESTS "Build tests" OFF) + 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") @@ -122,3 +124,24 @@ if(TARGET vortex_ffi_shared) target_link_libraries(vortex_cxx_shared PRIVATE ${APPLE_LINK_FLAGS}) endif() endif() + +if (BUILD_TESTS) + FetchContent_Declare( + Nanoarrow + GIT_REPOSITORY https://github.com/apache/arrow-nanoarrow + GIT_TAG apache-arrow-nanoarrow-0.8.0 + ) + FetchContent_MakeAvailable(Nanoarrow) + + FetchContent_Declare( + Catch + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.8.1 + ) + FetchContent_MakeAvailable(Catch) + include(Catch) + target_compile_definitions(Catch2 PRIVATE CATCH_CONFIG_NO_POSIX_SIGNALS) + + include(CTest) + add_subdirectory(tests) +endif() diff --git a/lang/cpp/README.md b/lang/cpp/README.md index a0b92f21835..fe9d2527e63 100644 --- a/lang/cpp/README.md +++ b/lang/cpp/README.md @@ -35,3 +35,11 @@ cmake -Bbuild -DBUILD_EXAMPLES=ON cmake --build build -j ./build/examples/hello-vortex ``` + +## Check coverage + +This will generate an LCOV directory `coverage`: + +```sh +./gcov-report.sh generate +``` diff --git a/lang/cpp/gcov-report.sh b/lang/cpp/gcov-report.sh new file mode 100755 index 00000000000..d7c2ec45d38 --- /dev/null +++ b/lang/cpp/gcov-report.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -eu +cmake -Bbuild -DBUILD_TESTS=1 -DCMAKE_CXX_FLAGS='-fprofile-arcs -ftest-coverage' +cmake --build build -j +ctest --test-dir build --output-on-failure + +geninfo build/CMakeFiles/vortex_cxx_shared.dir/ \ + build/tests/CMakeFiles/vortex_cxx_test.dir/ \ + --rc geninfo_unexecuted_blocks=1 \ + --exclude /usr --exclude build/_deps --exclude tests \ + -j -b src -o coverage.info +if [ $# -gt 0 ]; then + genhtml coverage.info -o coverage +fi diff --git a/lang/cpp/include/vortex/array.hpp b/lang/cpp/include/vortex/array.hpp index 82c1b5d747d..43b986f4b42 100644 --- a/lang/cpp/include/vortex/array.hpp +++ b/lang/cpp/include/vortex/array.hpp @@ -40,8 +40,8 @@ enum class ValidityType { 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, + // Item validity is set from a boolean array: true = valid, false = invalid + FromArray = VX_VALIDITY_ARRAY, }; /** diff --git a/lang/cpp/include/vortex/common.hpp b/lang/cpp/include/vortex/common.hpp index 1248f6d53bd..cc8559e96c6 100644 --- a/lang/cpp/include/vortex/common.hpp +++ b/lang/cpp/include/vortex/common.hpp @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: Copyright the Vortex contributors #pragma once +#include #include #include #include @@ -13,9 +14,34 @@ namespace vortex { struct float16_t { uint16_t bits; - friend bool operator==(float16_t, float16_t) = default; + constexpr friend bool operator==(float16_t, float16_t) = default; // NOLINTNEXTLINE - operator float() const; + constexpr operator float() const { + float result; + const uint32_t sign = (bits >> 15) & 1; + const uint32_t exponent = (bits >> 10) & 0x1F; + const uint32_t mantissa = bits & 0x3FF; + + uint32_t out; + if (exponent == 0x1F) { + out = (sign << 31) | 0x7F800000 | (mantissa << 13); + } else if (exponent == 0) { + if (mantissa == 0) { + out = sign << 31; + } else { + uint32_t m = mantissa; + int e = -1; + do { + m <<= 1; + ++e; + } while ((m & 0x400) == 0); + out = (sign << 31) | ((127 - 15 - e) << 23) | ((m & 0x3FF) << 13); + } + } else { + out = (sign << 31) | ((exponent - 15 + 127) << 23) | (mantissa << 13); + } + return std::bit_cast(out); + } }; static_assert(sizeof(float16_t) == 2 && std::is_trivially_copyable_v); } // namespace vortex @@ -64,6 +90,10 @@ constexpr vx_ptype to_ptype() { return PTYPE_I32; } else if constexpr (std::is_same_v) { return PTYPE_I64; +#if __STDCPP_FLOAT16_T__ == 1 + } else if constexpr (std::is_same_v) { + return PTYPE_F16; +#endif } else if constexpr (std::is_same_v) { return PTYPE_F16; } else if constexpr (std::is_same_v) { @@ -79,7 +109,13 @@ 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; + std::is_same_v +#if __STDCPP_FLOAT16_T__ == 1 + || std::is_same_v +#else + || std::is_same_v +#endif + ; } // namespace vortex::detail namespace vortex { diff --git a/lang/cpp/include/vortex/scan.hpp b/lang/cpp/include/vortex/scan.hpp index 7175f494bef..1b8cc2ef7a4 100644 --- a/lang/cpp/include/vortex/scan.hpp +++ b/lang/cpp/include/vortex/scan.hpp @@ -20,8 +20,9 @@ namespace vortex { namespace detail { + // range-for support for Scan and Partition -template +template (Source::*Next)()> class PullRange { public: class iterator { @@ -37,7 +38,7 @@ class PullRange { return *cur_; } iterator &operator++() { - cur_ = Next(src_); + cur_ = (src_->*Next)(); return *this; } void operator++(int) { @@ -55,7 +56,7 @@ class PullRange { explicit PullRange(Source &src) : src_(&src) { } iterator begin() { - return iterator(src_, Next(src_)); + return iterator(src_, (src_->*Next)()); } std::default_sentinel_t end() { return std::default_sentinel; diff --git a/lang/cpp/include/vortex/writer.hpp b/lang/cpp/include/vortex/writer.hpp index da761c334f9..8826423e995 100644 --- a/lang/cpp/include/vortex/writer.hpp +++ b/lang/cpp/include/vortex/writer.hpp @@ -18,6 +18,8 @@ namespace vortex { * * finish() writes the footer and finalizes the file. * Not calling finish() leaves file corrupted. + * + * Writer methods are thread-unsafe. */ class Writer { public: @@ -32,7 +34,9 @@ class Writer { * Append Array to output file. * Throws if "array"'s DataType doesn't match writer's DataType. */ + void push(std::span arrays); void push(const Array &array); + void push(std::initializer_list arrays); /* * Write footer and finalize the file. diff --git a/lang/cpp/src/array.cpp b/lang/cpp/src/array.cpp index 11b012449e2..b4adf69fe5c 100644 --- a/lang/cpp/src/array.cpp +++ b/lang/cpp/src/array.cpp @@ -23,14 +23,17 @@ Validity::Validity(const Validity &other) } Validity::Validity(ValidityType type) : type_(type), array_(nullptr) { - if (type == ValidityType::Array) { - throw VortexException("Validity(ValidityType) called with ValidityType::Array", + if (type == ValidityType::FromArray) { + throw VortexException("Validity(ValidityType) called with ValidityType::FromArray", ErrorCode::InvalidArgument); } } Validity Validity::from_array(const Array &bools) { - return {ValidityType::Array, vx_array_clone(Access::c_ptr(bools))}; + if (!bools.has_dtype(DataTypeVariant::Bool)) { + throw VortexException("Validity array isn't a Bool array", ErrorCode::InvalidArgument); + } + return {ValidityType::FromArray, vx_array_clone(Access::c_ptr(bools))}; } Validity::Validity(Validity &&other) noexcept : type_(other.type_), array_(other.array_) { @@ -61,7 +64,7 @@ Validity::~Validity() { } Array Validity::array() const { - if (type_ != ValidityType::Array || array_ == nullptr) { + if (type_ != ValidityType::FromArray || array_ == nullptr) { throw VortexException("validity has no backing array", ErrorCode::InvalidArgument); } return Access::adopt(vx_array_clone(array_)); @@ -93,7 +96,7 @@ ValidityBits::ValidityBits(const Session &session, const vx_array *canonical) { case ValidityType::AllInvalid: all_invalid_ = true; return; - case ValidityType::Array: + case ValidityType::FromArray: break; } @@ -163,7 +166,7 @@ Array Array::primitive_raw(vx_ptype ptype, const void *data, size_t len, const V std::optional keep_alive; vx_validity raw {}; raw.type = static_cast(validity.type()); - if (validity.type() == ValidityType::Array) { + if (validity.type() == ValidityType::FromArray) { keep_alive = validity.array(); raw.array = Access::c_ptr(*keep_alive); } @@ -264,7 +267,7 @@ Array make_struct(std::span fields, const Validity &validity) raw.type = static_cast(validity.type()); std::optional keep_alive; - if (validity.type() == ValidityType::Array) { + if (validity.type() == ValidityType::FromArray) { keep_alive = validity.array(); raw.array = Access::c_ptr(*keep_alive); } diff --git a/lang/cpp/src/f16.cpp b/lang/cpp/src/f16.cpp deleted file mode 100644 index de1058cc7d4..00000000000 --- a/lang/cpp/src/f16.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -#include "vortex/common.hpp" - -#include -#include - -namespace vortex { - -#if __STDCPP_FLOAT16_T__ != 1 -float16_t::operator float() const { - float result; - const uint32_t sign = (bits >> 15) & 1; - const uint32_t exponent = (bits >> 10) & 0x1F; - const uint32_t mantissa = bits & 0x3FF; - - uint32_t out; - if (exponent == 0x1F) { - out = (sign << 31) | 0x7F800000 | (mantissa << 13); - } else if (exponent == 0) { - if (mantissa == 0) { - out = sign << 31; - } else { - uint32_t m = mantissa; - int e = -1; - do { - m <<= 1; - ++e; - } while ((m & 0x400) == 0); - out = (sign << 31) | ((127 - 15 - e) << 23) | ((m & 0x3FF) << 13); - } - } else { - out = (sign << 31) | ((exponent - 15 + 127) << 23) | (mantissa << 13); - } - std::memcpy(&result, &out, sizeof(result)); - return result; -} -#endif -} // namespace vortex diff --git a/lang/cpp/src/writer.cpp b/lang/cpp/src/writer.cpp index 4adc5d77e50..57035171a14 100644 --- a/lang/cpp/src/writer.cpp +++ b/lang/cpp/src/writer.cpp @@ -8,6 +8,7 @@ #include "vortex/writer.hpp" #include "vortex/session.hpp" +#include #include #include @@ -30,13 +31,23 @@ Writer Writer::open(const Session &session, std::string_view path, const DataTyp return Writer(sink); } -void Writer::push(const Array &array) { +void Writer::push(std::span arrays) { if (handle_ == nullptr) { throw VortexException("null handle_", ErrorCode::InvalidArgument); } vx_error *error = nullptr; - vx_array_sink_push(handle_.get(), Access::c_ptr(array), &error); - throw_on_error(error); + for (const Array &array : arrays) { + vx_array_sink_push(handle_.get(), Access::c_ptr(array), &error); + throw_on_error(error); + } +} + +void Writer::push(const Array &array) { + push(std::span {&array, 1}); +} + +void Writer::push(std::initializer_list arrays) { + push({arrays.begin(), arrays.end()}); } void Writer::finish() { diff --git a/lang/cpp/tests/CMakeLists.txt b/lang/cpp/tests/CMakeLists.txt new file mode 100644 index 00000000000..16a6e0290b9 --- /dev/null +++ b/lang/cpp/tests/CMakeLists.txt @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright the Vortex contributors + +# need this for std::float16_t +set(CMAKE_CXX_STANDARD 23) + +FetchContent_Declare( + magic_enum + GIT_REPOSITORY https://github.com/Neargye/magic_enum.git + GIT_TAG v0.9.7 +) +FetchContent_MakeAvailable(magic_enum) + +file(GLOB TEST_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") + +add_executable(vortex_cxx_test ${TEST_FILES}) +target_include_directories(vortex_cxx_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(vortex_cxx_test PRIVATE + vortex_cxx_shared + Catch2::Catch2WithMain + magic_enum::magic_enum + nanoarrow_shared) + +catch_discover_tests(vortex_cxx_test) diff --git a/lang/cpp/tests/array.cpp b/lang/cpp/tests/array.cpp new file mode 100644 index 00000000000..998cb945833 --- /dev/null +++ b/lang/cpp/tests/array.cpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +#include +#include +#include +#include +#include + +using namespace vortex; +using namespace vortex::expr::ops; + +namespace { +using enum vortex::PType; +using enum ValidityType; + +TEST_CASE("Null array", "[array]") { + Array a = Array::null(1999); + REQUIRE(a.size() == 1999); + REQUIRE(a.nullable()); + REQUIRE(a.has_dtype(DataTypeVariant::Null)); +} + +TEST_CASE("Empty array", "[array]") { + Session session; + + auto empty = Array::primitive({}); + REQUIRE(empty.size() == 0); + REQUIRE(empty.is_primitive(I32)); + + REQUIRE(empty.null_count() == 0); + + auto view = empty.values(session); + REQUIRE(view.size() == 0); + auto values = view.values(); + REQUIRE(values.empty()); +} + +void test_primitive_array(Array array, const int32_t *begin) { + Session session; + REQUIRE(array.size() == 3); + REQUIRE(array.is_primitive(I32)); + REQUIRE_FALSE(array.nullable()); + REQUIRE(array.null_count() == 0); + + auto view = array.values(session); + REQUIRE(view.size() == 3); + REQUIRE(std::equal(view.values().begin(), view.values().end(), begin)); + REQUIRE_FALSE(view.is_null(1)); +} + +TEST_CASE("Primitive array", "[array]") { + int32_t c_array[3] = {10, 20, 30}; + test_primitive_array(Array::primitive(c_array), c_array); + + const int32_t const_c_array[3] = {10, 20, 30}; + test_primitive_array(Array::primitive(const_c_array), const_c_array); + + const std::array cpp_array = {10, 20, 30}; + test_primitive_array(Array::primitive(cpp_array), cpp_array.begin()); + + std::vector cpp_vector = {10, 20, 30}; + test_primitive_array(Array::primitive(cpp_vector), cpp_vector.data()); +} + +TEST_CASE("values with wrong type", "[array]") { + Session session; + std::vector data = {1}; + Array a = Array::primitive(data); + REQUIRE_THROWS_AS(a.values(session), VortexException); +} + +TEST_CASE("Validity from a boolean mask", "[array]") { + Session session; + std::vector data = {10, 20, 30}; + std::vector mask_bytes = {1, 0, 1}; + + Array mask_u8 = Array::primitive(std::span(mask_bytes)); + Array mask = mask_u8.apply(expr::root() == expr::lit(1)); + + Array a = Array::primitive(std::span(data), Validity::from_array(mask)); + REQUIRE(a.nullable()); + REQUIRE(a.null_count() == 1); + + auto view = a.values(session); + REQUIRE_FALSE(view.is_null(0)); + REQUIRE(view.is_null(1)); + REQUIRE_FALSE(view.is_null(2)); + REQUIRE(view.values()[0] == 10); + REQUIRE(view.values()[2] == 30); + + Validity validity = a.validity(); + REQUIRE(validity.type() == FromArray); + REQUIRE(validity.array().size() == 3); +} + +TEST_CASE("Invalid validity", "[array]") { + std::vector invalid_mask = {1, 2, 3}; + Array mask = Array::primitive(invalid_mask); + REQUIRE_THROWS_AS(Validity::from_array(mask), VortexException); +} + +TEST_CASE("AllInvalid", "[array]") { + Session session; + std::vector data = {1, 2}; + Array a = Array::primitive(std::span(data), AllInvalid); + REQUIRE(a.null_count() == 2); + auto view = a.values(session); + REQUIRE(view.is_null(0)); + REQUIRE(view.is_null(1)); +} + +TEST_CASE("make_struct and fields", "[array]") { + Array empty = make_struct({}); + REQUIRE(empty.size() == 0); + REQUIRE(empty.has_dtype(DataTypeVariant::Struct)); + REQUIRE(empty.dtype().fields().size() == 0); + + std::vector ages = {10, 20, 30}; + std::vector heights = {150, 160, 170}; + + Array s = make_struct({ + {"age", Array::primitive(ages)}, + {"height", Array::primitive(heights, AllValid)}, + }); + + REQUIRE(s.size() == 3); + REQUIRE(s.has_dtype(DataTypeVariant::Struct)); + REQUIRE(s.dtype().fields().size() == 2); + + Array by_index = s.field(0); + REQUIRE(by_index.is_primitive(U8)); + + Session session; + Array by_name = s.field("height"); + REQUIRE(by_name.is_primitive(U16)); + auto view = by_name.values(session); + REQUIRE(view.values()[2] == 170); + + REQUIRE_THROWS_AS(s.field(2), VortexException); + REQUIRE_THROWS_AS(s.field("nope"), VortexException); + + std::vector fields_vec; + fields_vec.emplace_back("age", Array::primitive(ages)); + Array other = make_struct(fields_vec); + REQUIRE(other.size() == 3); + REQUIRE(other.has_dtype(DataTypeVariant::Struct)); +} + +TEST_CASE("Mismatched field length", "[array]") { + std::vector a = {1, 2}; + std::vector b = {1, 2, 3}; + REQUIRE_THROWS_AS(make_struct({ + {"a", Array::primitive(a)}, + {"b", Array::primitive(b)}, + }), + VortexException); +} + +TEST_CASE("Slice", "[array]") { + Session session; + std::vector data = {0, 1, 2, 3, 4, 5}; + Array a = Array::primitive(data); + Array sliced = a.slice(2, 5); + REQUIRE(sliced.size() == 3); + auto view = sliced.values(session); + REQUIRE(view.values()[0] == 2); + REQUIRE(view.values()[2] == 4); + + REQUIRE_THROWS_AS(a.slice(2, 100), VortexException); +} + +TEST_CASE("Error with a code", "[array]") { + std::vector data = {0}; + Array a = Array::primitive(data); + try { + (void)a.slice(2, 100); + FAIL("expected exception"); + } catch (const VortexException &e) { + REQUIRE_FALSE(std::string(e.what()).empty()); + (void)e.code(); + } +} +} // namespace diff --git a/lang/cpp/tests/arrow.cpp b/lang/cpp/tests/arrow.cpp new file mode 100644 index 00000000000..8675d816067 --- /dev/null +++ b/lang/cpp/tests/arrow.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +#include + +#include +#include + +typedef struct ArrowSchema FFI_ArrowSchema; +typedef struct ArrowArray FFI_ArrowArray; +typedef struct ArrowArrayStream FFI_ArrowArrayStream; +#define USE_OWN_ARROW 1 + +#include + +#include "common.hpp" + +using namespace vortex; +using vortex_test::sample_dtype; +using vortex_test::TempPath; +using vortex_test::write_sample; + +namespace { +using enum vortex::PType; + +TEST_CASE("dtype to ArrowSchema", "[arrow]") { + DataType d = sample_dtype(); + ArrowSchema schema = d.to_arrow(); + + nanoarrow::UniqueSchema unique_schema; + ArrowSchemaMove(&schema, unique_schema.get()); + REQUIRE(unique_schema->format != nullptr); + REQUIRE(unique_schema->n_children == 2); +} + +TEST_CASE("dtype from ArrowSchema", "[arrow]") { + nanoarrow::UniqueSchema schema; + REQUIRE(ArrowSchemaInitFromType(schema.get(), NANOARROW_TYPE_STRUCT) == NANOARROW_OK); + REQUIRE(ArrowSchemaAllocateChildren(schema.get(), 1) == NANOARROW_OK); + REQUIRE(ArrowSchemaInitFromType(schema->children[0], NANOARROW_TYPE_INT64) == NANOARROW_OK); + REQUIRE(ArrowSchemaSetName(schema->children[0], "n") == NANOARROW_OK); + + ArrowSchema raw = {}; + ArrowSchemaMove(schema.get(), &raw); + DataType d = DataType::from_arrow(&raw); + REQUIRE(d.variant() == DataTypeVariant::Struct); + const std::vector fields = d.fields(); + REQUIRE(fields.size() == 1); + REQUIRE(fields[0].name == "n"); + REQUIRE(fields[0].dtype.primitive_type() == I64); +} + +TEST_CASE("Import Arrow array as Vortex array", "[arrow]") { + Session session; + nanoarrow::UniqueSchema schema; + REQUIRE(ArrowSchemaInitFromType(schema.get(), NANOARROW_TYPE_STRUCT) == NANOARROW_OK); + REQUIRE(ArrowSchemaAllocateChildren(schema.get(), 1) == NANOARROW_OK); + REQUIRE(ArrowSchemaInitFromType(schema->children[0], NANOARROW_TYPE_INT32) == NANOARROW_OK); + REQUIRE(ArrowSchemaSetName(schema->children[0], "a") == NANOARROW_OK); + + nanoarrow::UniqueArray arr; + REQUIRE(ArrowArrayInitFromSchema(arr.get(), schema.get(), nullptr) == NANOARROW_OK); + REQUIRE(ArrowArrayStartAppending(arr.get()) == NANOARROW_OK); + for (int i : {10, 20, 30}) { + REQUIRE(ArrowArrayAppendInt(arr->children[0], i) == NANOARROW_OK); + REQUIRE(ArrowArrayFinishElement(arr.get()) == NANOARROW_OK); + } + REQUIRE(ArrowArrayFinishBuildingDefault(arr.get(), nullptr) == NANOARROW_OK); + + ArrowArray raw_arr = {}; + ArrowSchema raw_schema = {}; + ArrowArrayMove(arr.get(), &raw_arr); + ArrowSchemaMove(schema.get(), &raw_schema); + + Array vx = Array::from_arrow(&raw_arr, &raw_schema, false); + REQUIRE(vx.size() == 3); + REQUIRE(vx.has_dtype(DataTypeVariant::Struct)); + + Array a = vx.field(0); + REQUIRE(a.is_primitive(I32)); + auto view = a.values(session); + REQUIRE(view.values()[0] == 10); + REQUIRE(view.values()[2] == 30); +} + +TEST_CASE("Scan partition to ArrowArrayStream", "[arrow]") { + Session session; + TempPath path = write_sample(session); + DataSource ds = DataSource::open(session, {path.string()}); + + Scan scan = ds.scan(); + auto partition = scan.next_partition(); + REQUIRE(partition.has_value()); + + ArrowStream vx_stream = std::move(partition.value()).into_arrow_stream(); + nanoarrow::UniqueArrayStream owned; + ArrowArrayStreamMove(vx_stream.raw(), owned.get()); + + nanoarrow::UniqueSchema schema; + ArrowError err {}; + REQUIRE(ArrowArrayStreamGetSchema(owned.get(), schema.get(), &err) == NANOARROW_OK); + REQUIRE(schema->n_children == 2); + + size_t rows = 0; + while (true) { + nanoarrow::UniqueArray chunk; + int rc = owned->get_next(owned.get(), chunk.get()); + REQUIRE(rc == NANOARROW_OK); + if (chunk->release == nullptr) { + break; + } + rows += chunk->length; + } + REQUIRE(rows == vortex_test::SAMPLE_ROWS); +} +} // namespace diff --git a/lang/cpp/tests/common.hpp b/lang/cpp/tests/common.hpp new file mode 100644 index 00000000000..b1b3d982ffa --- /dev/null +++ b/lang/cpp/tests/common.hpp @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +#pragma once + +#include "vortex/array.hpp" +#include +#include +#include +#include + +#include +#include + +namespace vortex_test { + +namespace fs = std::filesystem; +using namespace vortex; + +class TempPath { +public: + TempPath() = default; + explicit TempPath(fs::path p) : path_(std::move(p)) { + } + + TempPath(const TempPath &) = delete; + TempPath &operator=(const TempPath &) = delete; + + TempPath(TempPath &&other) noexcept : path_(std::move(other.path_)) { + other.path_.clear(); + } + TempPath &operator=(TempPath &&other) noexcept { + if (this != &other) { + reset(); + path_ = std::move(other.path_); + other.path_.clear(); + } + return *this; + } + + ~TempPath() { + reset(); + } + + const fs::path &path() const noexcept { + return path_; + } + std::string string() const { + return path_.string(); + } + + static TempPath unique() { + auto dir = fs::temp_directory_path() / "vortex_cxx_test"; + fs::create_directories(dir); + std::string name = std::to_string(std::random_device {}()) + ".vortex"; + return TempPath {dir / name}; + } + +private: + void reset() noexcept { + if (!path_.empty()) { + std::error_code ec; + fs::remove(path_, ec); + } + } + + fs::path path_; +}; + +inline DataType sample_dtype() { + return dtype::struct_({ + {"age", dtype::uint8()}, + {"height", dtype::uint16(dtype::Nullable)}, + }); +} + +constexpr size_t SAMPLE_ROWS = 100; + +inline std::vector sample_ages() { + std::vector buf(SAMPLE_ROWS); + for (size_t i = 0; i < SAMPLE_ROWS; ++i) { + buf[i] = static_cast(i); + } + return buf; +} + +inline std::vector sample_heights() { + std::vector buf(SAMPLE_ROWS); + for (size_t i = 0; i < SAMPLE_ROWS; ++i) { + buf[i] = static_cast((i + 1) % 200); + } + return buf; +} + +inline Array sample_array() { + auto ages = sample_ages(); + auto heights = sample_heights(); + return make_struct({ + {"age", Array::primitive(ages)}, + {"height", Array::primitive(heights, ValidityType::AllValid)}, + }); +} + +inline TempPath write_sample(const Session &session) { + TempPath path = TempPath::unique(); + Writer writer = Writer::open(session, path.string(), sample_dtype()); + writer.push(sample_array()); + writer.finish(); + return path; +} +} // namespace vortex_test diff --git a/lang/cpp/tests/dtype.cpp b/lang/cpp/tests/dtype.cpp new file mode 100644 index 00000000000..1b5e3573092 --- /dev/null +++ b/lang/cpp/tests/dtype.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +#include "vortex/dtype.hpp" +#include +#include + +using namespace vortex; + +namespace { +using enum vortex::PType; + +TEST_CASE("Null dtype", "[dtype]") { + auto d = dtype::null(); + REQUIRE(d.variant() == DataTypeVariant::Null); + REQUIRE(d.nullable()); +} + +TEST_CASE("Decimal dtype", "[dtype]") { + auto d = dtype::decimal(5, 2, false); + REQUIRE(d.variant() == DataTypeVariant::Decimal); + REQUIRE(d.decimal_precision() == 5); + REQUIRE(d.decimal_scale() == 2); + REQUIRE_FALSE(d.nullable()); + + REQUIRE_THROWS_AS(d.fields(), VortexException); + REQUIRE_THROWS_AS(d.list_element(), VortexException); +} + +TEST_CASE("copy dtype", "[dtype]") { + auto d = dtype::int32(true); + DataType d2 = d; + REQUIRE(d2.variant() == DataTypeVariant::Primitive); + REQUIRE(d2.primitive_type() == I32); + REQUIRE(d2.nullable()); + REQUIRE(d.variant() == DataTypeVariant::Primitive); +} + +TEST_CASE("list dtype", "[dtype]") { + auto d = dtype::list(dtype::float64(), dtype::Nullable); + REQUIRE(d.variant() == DataTypeVariant::List); + REQUIRE(d.nullable()); + REQUIRE(d.list_element().primitive_type() == F64); + + auto fsl = dtype::fixed_size_list(dtype::int16(), 4); + REQUIRE(fsl.variant() == DataTypeVariant::FixedSizeList); + REQUIRE(fsl.fixed_size_list_size() == 4); + REQUIRE(fsl.fixed_size_list_element().primitive_type() == I16); +} + +TEST_CASE("Struct DataType", "[dtype]") { + DataType d = dtype::struct_({ + {"col1", dtype::uint8()}, + {"col2", dtype::binary(dtype::Nullable)}, + }); + + REQUIRE(d.variant() == DataTypeVariant::Struct); + REQUIRE_FALSE(d.nullable()); + const std::vector fields = d.fields(); + REQUIRE(fields.size() == 2); + REQUIRE(fields[0].name == "col1"); + REQUIRE(fields[1].name == "col2"); + REQUIRE(fields[0].dtype.primitive_type() == U8); + REQUIRE(fields[1].dtype.variant() == DataTypeVariant::Binary); + REQUIRE(fields[1].dtype.nullable()); + + std::vector fields_vec; + fields_vec.emplace_back("col1", dtype::uint8()); + fields_vec.emplace_back("col2", dtype::utf8()); + d = dtype::struct_(fields_vec, dtype::Nullable); + + REQUIRE(d.variant() == DataTypeVariant::Struct); + REQUIRE(d.nullable()); + const std::vector built = d.fields(); + REQUIRE(built.size() == 2); + REQUIRE(built[1].name == "col2"); + + fields_vec = {}; + fields_vec.emplace_back("\xFF\xFE", dtype::uint8()); + REQUIRE_THROWS_AS(dtype::struct_(fields_vec), VortexException); +} +} // namespace diff --git a/lang/cpp/tests/enum_sizes.cpp b/lang/cpp/tests/enum_sizes.cpp new file mode 100644 index 00000000000..a100fa3b583 --- /dev/null +++ b/lang/cpp/tests/enum_sizes.cpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +#include + +#include +#include + +using magic_enum::enum_count; + +static_assert(enum_count() == enum_count()); +static_assert(enum_count() == enum_count()); +static_assert(enum_count() == enum_count()); +static_assert(enum_count() == enum_count()); +static_assert(enum_count() == enum_count()); +static_assert(enum_count() == enum_count()); diff --git a/lang/cpp/tests/expression.cpp b/lang/cpp/tests/expression.cpp new file mode 100644 index 00000000000..1a55364df02 --- /dev/null +++ b/lang/cpp/tests/expression.cpp @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +#include +#include + +#include +#include +#include + +using namespace vortex; + +namespace { +using enum vortex::PType; + +TEST_CASE("Expression copy", "[expr]") { + Expression clone = expr::root(); + { + Expression orig = expr::col("age"); + clone = orig; + } + Expression another = clone; + (void)another; + SUCCEED(); +} + +TEST_CASE("Apply root()", "[expr]") { + Session session; + std::vector data = {10, 20, 30, 40, 50}; + Array array = Array::primitive(data); + + Array applied = array.apply(expr::root()); + + REQUIRE(applied.size() == data.size()); + REQUIRE(applied.is_primitive(I32)); + auto values = applied.values(session); + for (size_t i = 0; i < data.size(); ++i) { + REQUIRE(values.values()[i] == data[i]); + } +} + +TEST_CASE("Apply projection", "[expr]") { + Session session; + std::vector ages = {10, 20, 30}; + std::vector heights = {150, 160, 170}; + + Array struct_arr = make_struct({ + {"age", Array::primitive(ages)}, + {"height", Array::primitive(heights)}, + }); + + Array projected = struct_arr.apply(expr::col("age")); + REQUIRE(projected.size() == ages.size()); + REQUIRE(projected.is_primitive(U8)); + auto values = projected.values(session); + for (size_t i = 0; i < ages.size(); ++i) { + REQUIRE(values.values()[i] == ages[i]); + } +} + +TEST_CASE("Apply arithmetic", "[expr]") { + Session session; + std::vector data = {1, 2, 3, 4, 5}; + Array array = Array::primitive(data); + + Expression e = expr::add(expr::root(), expr::lit(10)); + Array applied = array.apply(e); + + REQUIRE(applied.is_primitive(I32)); + auto values = applied.values(session); + for (size_t i = 0; i < data.size(); ++i) { + REQUIRE(values.values()[i] == data[i] + 10); + } +} + +TEST_CASE("Operator overloading", "[expr]") { + using namespace vortex::expr::ops; + Session session; + std::vector data = {1, 2, 2, 7}; + Array array = Array::primitive(data); + + Array applied = array.apply(expr::root() == expr::lit(2)); + auto bits = applied.bools(session); + REQUIRE(bits.size() == data.size()); + REQUIRE_FALSE(bits.value(0)); + REQUIRE(bits.value(1)); + REQUIRE(bits.value(2)); + REQUIRE_FALSE(bits.value(3)); +} + +TEST_CASE("Apply error", "[expr]") { + std::vector data = {1, 2, 3}; + Array array = Array::primitive(data); + + Expression bad = expr::add(expr::root(), expr::lit(1)); + REQUIRE_THROWS_AS(array.apply(bad), VortexException); +} + +TEST_CASE("Empty conjunction", "[expr]") { + REQUIRE_THROWS_AS(expr::and_all({}), VortexException); + REQUIRE_THROWS_AS(expr::or_all({}), VortexException); +} +} // namespace diff --git a/lang/cpp/tests/float16_t.cpp b/lang/cpp/tests/float16_t.cpp new file mode 100644 index 00000000000..7a08a92a05b --- /dev/null +++ b/lang/cpp/tests/float16_t.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +#include "vortex/dtype.hpp" +#include +#include + +using namespace vortex; + +namespace { + +static_assert(float(1.0f16) == 1.0f); + +TEST_CASE("float16_t to_float", "[float]") { + REQUIRE(float(1.0f16) == 1.0F); + REQUIRE(float(-2.0f16) == -2.0F); + REQUIRE(float(0.0f) == 0.0F); + REQUIRE(float(-0.0f) == -0.0F); +} + +#if __STDCPP_FLOAT16_T__ == 1 +TEST_CASE("F16 scalar", "[scalar]") { + std::float16_t float16t = 1.0f16; + + Scalar scalar = scalar::of(float16t); + REQUIRE(scalar.dtype().variant() == DataTypeVariant::Primitive); + REQUIRE(scalar.dtype().primitive_type() == vortex::PType::F16); + + _Float16 float16t_alias = 1.0f16; + scalar = scalar::of(float16t_alias); + REQUIRE(scalar.dtype().variant() == DataTypeVariant::Primitive); + REQUIRE(scalar.dtype().primitive_type() == vortex::PType::F16); +} +#endif +} // namespace diff --git a/lang/cpp/tests/float16_t_compat.cpp b/lang/cpp/tests/float16_t_compat.cpp new file mode 100644 index 00000000000..94acb0df071 --- /dev/null +++ b/lang/cpp/tests/float16_t_compat.cpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +#include +#include +// This is UB but no other way to test compatibility macro, fine +// as we're running tests +#undef __STDCPP_FLOAT16_T__ +#include +#include + +using namespace vortex; + +namespace { + +constexpr float16_t one {0x3C00}; +static_assert(static_cast(one) == 1.0f); + +TEST_CASE("float16_t to_float (compatibility)", "[float]") { + REQUIRE(float(float16_t {0x3C00}) == 1.0F); + REQUIRE(float(float16_t {0xC000}) == -2.0F); + REQUIRE(float(float16_t {0}) == 0.0F); + REQUIRE(float(float16_t {0x8000}) == -0.0F); + REQUIRE(float(float16_t {0x7C00}) == std::numeric_limits::infinity()); + REQUIRE(float(float16_t {0xFC00}) == -std::numeric_limits::infinity()); + REQUIRE(std::fpclassify(float(float16_t {0x7E01})) == FP_NAN); + // Denormalized float16_t always gets to normal floats on conversion to float + REQUIRE(std::fpclassify(float(float16_t {0x0001})) == FP_NORMAL); + REQUIRE(std::fpclassify(float(float16_t {0x83FF})) == FP_NORMAL); +} + +TEST_CASE("F16 scalar (compatibility)", "[scalar]") { + const float16_t float16t {0x3C00}; + Scalar scalar = scalar::of(float16t); + REQUIRE(scalar.dtype().variant() == DataTypeVariant::Primitive); + REQUIRE(scalar.dtype().primitive_type() == vortex::PType::F16); +} +} // namespace diff --git a/lang/cpp/tests/scalar.cpp b/lang/cpp/tests/scalar.cpp new file mode 100644 index 00000000000..27ff73f656a --- /dev/null +++ b/lang/cpp/tests/scalar.cpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +#include "vortex/error.hpp" +#include +#include +#include + +using namespace vortex; +using namespace std::string_view_literals; + +namespace { +using enum vortex::PType; + +TEST_CASE("Boolean scalar", "[scalar]") { + Scalar s = scalar::of(true); + REQUIRE_FALSE(s.is_null()); + REQUIRE(s.dtype().variant() == DataTypeVariant::Bool); +} + +TEST_CASE("Integer scalars", "[scalar]") { + REQUIRE(scalar::of(42).dtype().primitive_type() == U8); + REQUIRE(scalar::of(42).dtype().primitive_type() == U16); + REQUIRE(scalar::of(42).dtype().primitive_type() == U32); + REQUIRE(scalar::of(42).dtype().primitive_type() == U64); + REQUIRE(scalar::of(-1).dtype().primitive_type() == I8); + REQUIRE(scalar::of(-1).dtype().primitive_type() == I16); + REQUIRE(scalar::of(-1).dtype().primitive_type() == I32); + REQUIRE(scalar::of(-1).dtype().primitive_type() == I64); +} + +TEST_CASE("Float scalars", "[scalar]") { + REQUIRE(scalar::of(1.5F).dtype().primitive_type() == F32); + REQUIRE(scalar::of(1.5).dtype().primitive_type() == F64); + REQUIRE(scalar::of(float16_t {0x3C00}).dtype().primitive_type() == F16); +} + +TEST_CASE("Nullable scalars", "[scalar]") { + Scalar s = scalar::of(0, true); + REQUIRE(s.dtype().nullable()); + REQUIRE_FALSE(s.is_null()); +} + +TEST_CASE("Null scalar", "[scalar]") { + Scalar s = scalar::null(dtype::int32(true)); + REQUIRE(s.is_null()); + REQUIRE(s.dtype().variant() == DataTypeVariant::Primitive); +} + +TEST_CASE("UTF-8 scalar", "[scalar]") { + Scalar s = scalar::of("hello"sv); + REQUIRE_FALSE(s.is_null()); + REQUIRE(s.dtype().variant() == DataTypeVariant::Utf8); + + REQUIRE_FALSE(scalar::of(""sv).is_null()); + s = scalar::of("Широкая строка"sv); + REQUIRE(s.dtype().variant() == DataTypeVariant::Utf8); + + REQUIRE_THROWS_AS(scalar::of("\xFF\xFE"sv), VortexException); +} + +TEST_CASE("Binary scalar", "[scalar]") { + const std::byte bytes[] = {std::byte {1}, std::byte {2}, std::byte {0}, std::byte {4}}; + Scalar s = scalar::of(std::span {bytes}); + REQUIRE_FALSE(s.is_null()); + REQUIRE(s.dtype().variant() == DataTypeVariant::Binary); +} + +TEST_CASE("Decimal scalars", "[scalar]") { + Scalar d8 = scalar::decimal(56, 5, 2); + REQUIRE(d8.dtype().variant() == DataTypeVariant::Decimal); + REQUIRE(d8.dtype().decimal_precision() == 5); + REQUIRE(d8.dtype().decimal_scale() == 2); + + Scalar d16 = scalar::decimal(int16_t(1234), 5, 2); + REQUIRE(d16.dtype().variant() == DataTypeVariant::Decimal); + REQUIRE(d16.dtype().decimal_precision() == 5); + REQUIRE(d16.dtype().decimal_scale() == 2); + + Scalar d32 = scalar::decimal(int32_t(1234), 5, 2); + REQUIRE(d32.dtype().variant() == DataTypeVariant::Decimal); + REQUIRE(d32.dtype().decimal_precision() == 5); + REQUIRE(d32.dtype().decimal_scale() == 2); + + Scalar d64 = scalar::decimal(99999, 12, 3); + REQUIRE(d64.dtype().variant() == DataTypeVariant::Decimal); + REQUIRE(d64.dtype().decimal_precision() == 12); + REQUIRE(d64.dtype().decimal_scale() == 3); +} + +TEST_CASE("Copy scalar", "[scalar]") { + Scalar a = scalar::of(42); + Scalar b = a; + REQUIRE(a.dtype().primitive_type() == I64); + REQUIRE(b.dtype().primitive_type() == I64); + + Scalar c = scalar::of(1); + c = b; + REQUIRE(c.dtype().primitive_type() == I64); +} +} // namespace diff --git a/lang/cpp/tests/scan.cpp b/lang/cpp/tests/scan.cpp new file mode 100644 index 00000000000..82854e11f2a --- /dev/null +++ b/lang/cpp/tests/scan.cpp @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +#include +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "vortex/estimate.hpp" + +using namespace vortex; +using Catch::Matchers::ContainsSubstring; +using vortex_test::SAMPLE_ROWS; +using vortex_test::TempPath; +using vortex_test::write_sample; + +namespace { +using enum vortex::PType; + +TEST_CASE("Non-existent data source", "[datasource]") { + Session session; + + try { + DataSource::open(session, {"nonexistent"}); + FAIL("expected exception"); + } catch (const VortexException &e) { + REQUIRE_THAT(std::string(e.what()), ContainsSubstring("No files matched")); + } +} + +TEST_CASE("Read dtype and row count", "[datasource]") { + Session session; + TempPath path = write_sample(session); + + std::array paths = {path.string()}; + + DataSource other = DataSource::open(session, paths); + DataSource ds(other); + + Estimate row_count = ds.row_count(); + REQUIRE(row_count.type() == EstimateType::Exact); + REQUIRE(row_count.value() == SAMPLE_ROWS); + + DataType dt = ds.dtype(); + REQUIRE(dt.variant() == DataTypeVariant::Struct); + const std::vector fields = dt.fields(); + REQUIRE(fields.size() == 2); + REQUIRE(fields[0].name == "age"); + REQUIRE(fields[1].name == "height"); +} + +void verify_age_field(const Session &session, + const Array &age, + uint8_t first = 0, + size_t rows = SAMPLE_ROWS) { + REQUIRE(age.is_primitive(U8)); + REQUIRE(age.size() == rows); + auto view = age.values(session); + for (size_t i = 0; i < rows; ++i) { + REQUIRE(view.values()[i] == static_cast(first + i)); + } +} + +void verify_sample_array(const Session &session, const Array &array) { + REQUIRE(array.size() == SAMPLE_ROWS); + REQUIRE(array.has_dtype(DataTypeVariant::Struct)); + verify_age_field(session, array.field(0)); + + Array height = array.field(1); + REQUIRE(height.is_primitive(U16)); + auto view = height.values(session); + for (size_t i = 0; i < SAMPLE_ROWS; ++i) { + REQUIRE(view.values()[i] == (i + 1) % 200); + } + + REQUIRE_THROWS_AS(array.field(2), VortexException); +} + +TEST_CASE("Basic scan", "[scan]") { + Session session; + TempPath path = write_sample(session); + DataSource ds = DataSource::open(session, {path.string()}); + + Scan scan = ds.scan(); + REQUIRE(scan.partition_count().type() == EstimateType::Exact); + REQUIRE(scan.partition_count().value() == 1); + REQUIRE(scan.dtype().variant() == DataTypeVariant::Struct); + + auto partition = scan.next_partition(); + REQUIRE(partition.has_value()); + + Estimate rows = partition->row_count(); + REQUIRE(rows.type() == EstimateType::Exact); + REQUIRE(rows.value() == SAMPLE_ROWS); + + REQUIRE_FALSE(scan.next_partition().has_value()); + + auto array = partition->next(); + REQUIRE(array.has_value()); + REQUIRE_FALSE(partition->next().has_value()); + + verify_sample_array(session, *array); +} + +TEST_CASE("Range-for over partitions and batches", "[scan]") { + Session session; + TempPath path = write_sample(session); + DataSource ds = DataSource::open(session, {path.string()}); + + Scan scan = ds.scan(); + size_t partitions = 0; + size_t rows = 0; + for (Partition &partition : scan.partitions()) { + ++partitions; + for (Array &batch : partition.batches()) { + rows += batch.size(); + } + } + REQUIRE(partitions == 1); + REQUIRE(rows == SAMPLE_ROWS); +} + +TEST_CASE("Scan from buffer", "[scan]") { + Session session; + TempPath path = write_sample(session); + + std::ifstream file(path.path(), std::ios::binary | std::ios::ate); + const auto size = file.tellg(); + file.seekg(0, std::ios::beg); + std::vector buffer(static_cast(size)); + REQUIRE(file.read(reinterpret_cast(buffer.data()), size)); + + DataSource ds = DataSource::from_buffer(session, buffer); + Scan scan = ds.scan(); + auto partition = scan.next_partition(); + REQUIRE(partition.has_value()); + auto array = partition->next(); + REQUIRE(array.has_value()); + verify_sample_array(session, *array); +} + +TEST_CASE("Multiple paths", "[scan]") { + Session session; + TempPath first = write_sample(session); + TempPath second = write_sample(session); + + const std::vector paths = {first.string(), second.string()}; + DataSource ds = DataSource::open(session, paths); + REQUIRE(ds.row_count().value_or(0) >= SAMPLE_ROWS); +} + +TEST_CASE("Multithreaded scan", "[scan]") { + Session session; + + constexpr size_t NUM_FILES = 8; + std::vector paths; + std::vector path_strings; + for (size_t i = 0; i < NUM_FILES; ++i) { + paths.push_back(write_sample(session)); + path_strings.push_back(paths.back().string()); + } + + DataSource ds = DataSource::open(session, path_strings); + Scan scan = ds.scan(); + + std::vector threads; + threads.reserve(NUM_FILES); + std::vector rows(NUM_FILES, 0); + + for (size_t i = 0; i < NUM_FILES; ++i) { + threads.emplace_back([&, i] { + while (auto partition = scan.next_partition()) { + while (auto batch = partition->next()) { + rows[i] += batch->size(); + } + } + }); + } + for (auto &t : threads) { + t.join(); + } + + size_t total = 0; + for (size_t n : rows) { + total += n; + } + REQUIRE(total == NUM_FILES * SAMPLE_ROWS); +} + +TEST_CASE("Project field", "[projection]") { + Session session; + TempPath path = write_sample(session); + DataSource ds = DataSource::open(session, {path.string()}); + + Scan scan = ds.scan({.projection = expr::col("age")}); + auto partition = scan.next_partition(); + REQUIRE(partition.has_value()); + auto array = partition->next(); + REQUIRE(array.has_value()); + verify_age_field(session, *array); +} + +TEST_CASE("Project none fields", "[projection]") { + Session session; + TempPath path = write_sample(session); + DataSource ds = DataSource::open(session, {path.string()}); + + Scan scan = ds.scan({.projection = expr::root().select({})}); + auto partition = scan.next_partition(); + REQUIRE(partition.has_value()); + auto array = partition->next(); + REQUIRE(array.has_value()); + REQUIRE(array->dtype().fields().empty()); +} + +TEST_CASE("Project multiple fields", "[projection]") { + Session session; + TempPath path = write_sample(session); + DataSource ds = DataSource::open(session, {path.string()}); + + Scan scan = ds.scan({.projection = expr::root().select({"age", "height"})}); + auto partition = scan.next_partition(); + REQUIRE(partition.has_value()); + auto array = partition->next(); + REQUIRE(array.has_value()); + verify_age_field(session, array->field("age")); +} + +TEST_CASE("Filter age", "[filter]") { + Session session; + TempPath path = write_sample(session); + DataSource ds = DataSource::open(session, {path.string()}); + + constexpr uint8_t threshold = 50; + Scan scan = ds.scan({ + .filter = expr::gte(expr::col("age"), expr::lit(threshold)), + }); + auto partition = scan.next_partition(); + REQUIRE(partition.has_value()); + auto array = partition->next(); + REQUIRE(array.has_value()); + + REQUIRE(array->size() == SAMPLE_ROWS - threshold); + verify_age_field(session, array->field(0), threshold, SAMPLE_ROWS - threshold); +} + +TEST_CASE("Filter invalid values", "[filter]") { + Session session; + TempPath path = write_sample(session); + DataSource ds = DataSource::open(session, {path.string()}); + + Scan scan = ds.scan({.filter = expr::col("age").is_null()}); + auto partition = scan.next_partition(); + REQUIRE(!partition.has_value()); +} + +TEST_CASE("Filter with operators", "[filter]") { + using namespace vortex::expr::ops; + Session session; + TempPath path = write_sample(session); + const std::string path_string = path.string(); + std::array paths = {path_string}; + DataSource ds = DataSource::open(session, paths); + + Scan scan1 = ds.scan({ + .filter = expr::col("age") >= expr::lit(90) && expr::col("age") < expr::lit(95), + }); + + Scan scan2 = ds.scan({ + .filter = !(expr::col("age") < expr::lit(90)) && expr::col("age") < expr::lit(95), + }); + + Scan scans[2] = {std::move(scan1), std::move(scan2)}; + for (Scan &scan : scans) { + auto partition = scan.next_partition(); + REQUIRE(partition.has_value()); + auto array = partition->next(); + REQUIRE(array.has_value()); + REQUIRE(array->size() == 5); + } +} + +TEST_CASE("Type-mismatched filter", "[filter]") { + Session session; + TempPath path = write_sample(session); + DataSource ds = DataSource::open(session, {path.string()}); + + Scan scan = ds.scan({ + .filter = expr::eq(expr::col("age"), expr::lit(67)), + }); + auto partition = scan.next_partition(); + REQUIRE(partition.has_value()); + REQUIRE_THROWS_AS(partition->next(), VortexException); +} + +TEST_CASE("Row range and limit", "[scan]") { + Session session; + TempPath path = write_sample(session); + DataSource ds = DataSource::open(session, {path.string()}); + + Scan scan = ds.scan({ + .row_range = RowRange {10, 25}, + .limit = 5, + }); + auto partition = scan.next_partition(); + REQUIRE(partition.has_value()); + auto array = partition->next(); + REQUIRE(array.has_value()); + + REQUIRE(array->size() == 5); + verify_age_field(session, array->field(0), 10, 5); +} + +TEST_CASE("Selection", "[scan]") { + using enum Selection::Kind; + Session session; + TempPath path = write_sample(session); + DataSource ds = DataSource::open(session, {path.string()}); + + Scan scan = ds.scan({ + .selection = {{Include, {3, 5, 8}}}, + }); + auto partition = scan.next_partition(); + REQUIRE(partition.has_value()); + auto array = partition->next(); + REQUIRE(array.has_value()); + REQUIRE(array->size() == 3); + + auto ages = array->field(0).values(session); + REQUIRE(ages.values()[0] == 3); + REQUIRE(ages.values()[1] == 5); + REQUIRE(ages.values()[2] == 8); + + scan = ds.scan({ + .selection = {{Exclude, {0, 1, 2}}}, + }); + partition = scan.next_partition(); + REQUIRE(partition.has_value()); + array = partition->next(); + REQUIRE(array.has_value()); + REQUIRE(array->size() == 97); + + ages = array->field(0).values(session); + REQUIRE(ages.values()[0] == 3); +} +} // namespace diff --git a/lang/cpp/tests/session.cpp b/lang/cpp/tests/session.cpp new file mode 100644 index 00000000000..f99d7d3bb19 --- /dev/null +++ b/lang/cpp/tests/session.cpp @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +#include +#include + +using namespace vortex; + +namespace { +TEST_CASE("Session copy and move", "[session]") { + const Session s; + Session other; + other = s; + Session moved; + moved = std::move(other); + Session other2(std::move(moved)); +} +} // namespace diff --git a/lang/cpp/tests/string_binary.cpp b/lang/cpp/tests/string_binary.cpp new file mode 100644 index 00000000000..16ddb7eaa3c --- /dev/null +++ b/lang/cpp/tests/string_binary.cpp @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +#include + +#include +#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 + +#include "common.hpp" + +using namespace vortex; +using namespace std::string_view_literals; +using vortex_test::TempPath; + +namespace { + +Array strings_from_arrow(std::span values, bool with_null) { + nanoarrow::UniqueSchema schema; + REQUIRE(ArrowSchemaInitFromType(schema.get(), NANOARROW_TYPE_STRING) == NANOARROW_OK); + nanoarrow::UniqueArray arr; + REQUIRE(ArrowArrayInitFromSchema(arr.get(), schema.get(), nullptr) == NANOARROW_OK); + REQUIRE(ArrowArrayStartAppending(arr.get()) == NANOARROW_OK); + for (const auto &value : values) { + const ArrowStringView view {value.data(), static_cast(value.size())}; + REQUIRE(ArrowArrayAppendString(arr.get(), view) == NANOARROW_OK); + } + if (with_null) { + REQUIRE(ArrowArrayAppendNull(arr.get(), 1) == NANOARROW_OK); + } + REQUIRE(ArrowArrayFinishBuildingDefault(arr.get(), nullptr) == NANOARROW_OK); + + ArrowArray raw_arr = {}; + ArrowSchema raw_schema = {}; + ArrowArrayMove(arr.get(), &raw_arr); + ArrowSchemaMove(schema.get(), &raw_schema); + return Array::from_arrow(&raw_arr, &raw_schema, true); +} + +Array bytes_from_arrow(std::span values) { + nanoarrow::UniqueSchema schema; + REQUIRE(ArrowSchemaInitFromType(schema.get(), NANOARROW_TYPE_BINARY) == NANOARROW_OK); + nanoarrow::UniqueArray arr; + REQUIRE(ArrowArrayInitFromSchema(arr.get(), schema.get(), nullptr) == NANOARROW_OK); + REQUIRE(ArrowArrayStartAppending(arr.get()) == NANOARROW_OK); + for (const auto &value : values) { + const ArrowBufferView view {{reinterpret_cast(value.data())}, + static_cast(value.size())}; + REQUIRE(ArrowArrayAppendBytes(arr.get(), view) == NANOARROW_OK); + } + REQUIRE(ArrowArrayFinishBuildingDefault(arr.get(), nullptr) == NANOARROW_OK); + + ArrowArray raw_arr = {}; + ArrowSchema raw_schema = {}; + ArrowArrayMove(arr.get(), &raw_arr); + ArrowSchemaMove(schema.get(), &raw_schema); + return Array::from_arrow(&raw_arr, &raw_schema, true); +} + +TEST_CASE("String view over utf8 array", "[strings]") { + Session session; + + const std::string long1(40, 'x'); + const std::vector values = {"short"sv, "Широкая строка"sv, long1, ""sv}; + Array array = strings_from_arrow(values, true); + + StringView view = array.strings(session); + REQUIRE(view.size() == values.size() + 1); + for (size_t i = 0; i < values.size(); ++i) { + REQUIRE_FALSE(view.is_null(i)); + REQUIRE(view[i] == values[i]); + } + REQUIRE(view.is_null(values.size())); + REQUIRE_THROWS_AS(view[view.size()], VortexException); +} + +TEST_CASE("Bytes view over binary array", "[strings]") { + Session session; + + // Includes a NUL byte + const std::vector values = {std::string_view {"abc\0def", 7}, "ffff"sv}; + Array array = bytes_from_arrow(values); + + BytesView view = array.bytes(session); + REQUIRE(view.size() == 2); + for (size_t i = 0; i < values.size(); ++i) { + const auto bytes = view[i]; + REQUIRE(std::string_view(reinterpret_cast(bytes.data()), bytes.size()) == values[i]); + } +} + +TEST_CASE("Strings roundtrip", "[strings]") { + Session session; + TempPath path = TempPath::unique(); + + const std::string long1(64, 'y'); + const std::vector values = {"inlined"sv, long1}; + Array strings = strings_from_arrow(values, false); + + Writer writer = + Writer::open(session, path.string(), dtype::struct_({{"s", dtype::utf8(dtype::Nullable)}})); + writer.push(make_struct({{"s", strings}})); + writer.finish(); + + DataSource ds = DataSource::open(session, {path.string()}); + Scan scan = ds.scan(); + auto partition = scan.next_partition(); + REQUIRE(partition.has_value()); + auto batch = partition->next(); + REQUIRE(batch.has_value()); + + StringView view = batch->field(0).strings(session); + REQUIRE(view.size() == 2); + REQUIRE(view[0] == "inlined"sv); + REQUIRE(view[1] == long1); +} + +TEST_CASE("strings() on a non-utf8 array", "[strings]") { + Session session; + std::vector data = {1}; + Array a = Array::primitive(data); + REQUIRE_THROWS_AS(a.strings(session), VortexException); + REQUIRE_THROWS_AS(a.bytes(session), VortexException); +} +} // namespace diff --git a/lang/cpp/tests/writer.cpp b/lang/cpp/tests/writer.cpp new file mode 100644 index 00000000000..f8dc2676fa5 --- /dev/null +++ b/lang/cpp/tests/writer.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors +#include + +#include "common.hpp" +#include "vortex/dtype.hpp" +#include "vortex/error.hpp" +#include "vortex/session.hpp" + +using namespace vortex; +using vortex_test::TempPath; + +TEST_CASE("Dangling session writer", "[writer]") { + TempPath path = TempPath::unique(); + + { + Writer writer = Writer::open(Session {}, path.string(), vortex_test::sample_dtype()); + writer.push(vortex_test::sample_array()); + } + + REQUIRE_THROWS_AS(DataSource::open(Session {}, {path.string()}), VortexException); +} + +TEST_CASE("Unfinished writer", "[writer]") { + Session session; + TempPath path = TempPath::unique(); + + { + Writer writer = Writer::open(session, path.string(), vortex_test::sample_dtype()); + writer.push({vortex_test::sample_array()}); + } + + REQUIRE_THROWS_AS(DataSource::open(session, {path.string()}), VortexException); +} + +TEST_CASE("Write finish() called twice", "[writer]") { + Session session; + TempPath path = TempPath::unique(); + + Writer writer = Writer::open(session, path.string(), vortex_test::sample_dtype()); + writer.push(vortex_test::sample_array()); + writer.finish(); + REQUIRE_THROWS_AS(writer.finish(), VortexException); + REQUIRE_THROWS_AS(writer.push(vortex_test::sample_array()), VortexException); +} + +TEST_CASE("Writer push with invalid dtype", "[writer]") { + Session session; + TempPath path = TempPath::unique(); + Writer writer = Writer::open(session, path.string(), dtype::null()); + const std::vector arrays = {vortex_test::sample_array(), vortex_test::sample_array()}; + REQUIRE_THROWS_AS(writer.push(arrays), VortexException); +} diff --git a/vortex-ffi/src/array.rs b/vortex-ffi/src/array.rs index 577bd4d8be5..7c20bb72d46 100644 --- a/vortex-ffi/src/array.rs +++ b/vortex-ffi/src/array.rs @@ -305,15 +305,18 @@ unsafe fn primitive_from_raw( ptr: *const T, len: usize, validity: &vx_validity, + error: *mut *mut vx_error, ) -> *const vx_array { - let slice = if ptr.is_null() { - unsafe { std::slice::from_raw_parts(NonNull::dangling().as_ptr(), len) } - } else { - unsafe { std::slice::from_raw_parts(ptr, len) } - }; - let buffer = Buffer::copy_from(slice); - let array = PrimitiveArray::new(buffer, validity.into()); - vx_array::new(Arc::new(array.into_array())) + try_or_default(error, || { + let slice = if ptr.is_null() { + unsafe { std::slice::from_raw_parts(NonNull::dangling().as_ptr(), len) } + } else { + unsafe { std::slice::from_raw_parts(ptr, len) } + }; + let buffer = Buffer::copy_from(slice); + let array = PrimitiveArray::try_new(buffer, validity.into())?; + Ok(vx_array::new(Arc::new(array.into_array()))) + }) } /// Create a new primitive array from an existing buffer. @@ -345,17 +348,35 @@ pub extern "C-unwind" fn vx_array_new_primitive( let validity = unsafe { &*validity }; match ptype { - vx_ptype::PTYPE_U8 => unsafe { primitive_from_raw(ptr as *const u8, len, validity) }, - vx_ptype::PTYPE_U16 => unsafe { primitive_from_raw(ptr as *const u16, len, validity) }, - vx_ptype::PTYPE_U32 => unsafe { primitive_from_raw(ptr as *const u32, len, validity) }, - vx_ptype::PTYPE_U64 => unsafe { primitive_from_raw(ptr as *const u64, len, validity) }, - vx_ptype::PTYPE_I8 => unsafe { primitive_from_raw(ptr as *const i8, len, validity) }, - vx_ptype::PTYPE_I16 => unsafe { primitive_from_raw(ptr as *const i16, len, validity) }, - vx_ptype::PTYPE_I32 => unsafe { primitive_from_raw(ptr as *const i32, len, validity) }, - vx_ptype::PTYPE_I64 => unsafe { primitive_from_raw(ptr as *const i64, len, validity) }, - vx_ptype::PTYPE_F16 => unsafe { primitive_from_raw(ptr as *const f16, len, validity) }, - vx_ptype::PTYPE_F32 => unsafe { primitive_from_raw(ptr as *const f32, len, validity) }, - vx_ptype::PTYPE_F64 => unsafe { primitive_from_raw(ptr as *const f64, len, validity) }, + vx_ptype::PTYPE_U8 => unsafe { primitive_from_raw(ptr as *const u8, len, validity, error) }, + vx_ptype::PTYPE_U16 => unsafe { + primitive_from_raw(ptr as *const u16, len, validity, error) + }, + vx_ptype::PTYPE_U32 => unsafe { + primitive_from_raw(ptr as *const u32, len, validity, error) + }, + vx_ptype::PTYPE_U64 => unsafe { + primitive_from_raw(ptr as *const u64, len, validity, error) + }, + vx_ptype::PTYPE_I8 => unsafe { primitive_from_raw(ptr as *const i8, len, validity, error) }, + vx_ptype::PTYPE_I16 => unsafe { + primitive_from_raw(ptr as *const i16, len, validity, error) + }, + vx_ptype::PTYPE_I32 => unsafe { + primitive_from_raw(ptr as *const i32, len, validity, error) + }, + vx_ptype::PTYPE_I64 => unsafe { + primitive_from_raw(ptr as *const i64, len, validity, error) + }, + vx_ptype::PTYPE_F16 => unsafe { + primitive_from_raw(ptr as *const f16, len, validity, error) + }, + vx_ptype::PTYPE_F32 => unsafe { + primitive_from_raw(ptr as *const f32, len, validity, error) + }, + vx_ptype::PTYPE_F64 => unsafe { + primitive_from_raw(ptr as *const f64, len, validity, error) + }, } } From 36e23e8c3aaabac0dc31ce485ff18cd60870f906 Mon Sep 17 00:00:00 2001 From: Mikhail Kot Date: Thu, 16 Jul 2026 10:46:15 +0100 Subject: [PATCH 2/2] fix Signed-off-by: Mikhail Kot --- lang/cpp/gcov-report.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lang/cpp/gcov-report.sh b/lang/cpp/gcov-report.sh index d7c2ec45d38..6c1365df57a 100755 --- a/lang/cpp/gcov-report.sh +++ b/lang/cpp/gcov-report.sh @@ -1,4 +1,8 @@ #!/bin/sh + +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright the Vortex contributors + set -eu cmake -Bbuild -DBUILD_TESTS=1 -DCMAKE_CXX_FLAGS='-fprofile-arcs -ftest-coverage' cmake --build build -j