From 3b8850a8b62163fef5891bf6a9f08defd73d34a0 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Jul 2025 09:33:49 -0700 Subject: [PATCH 001/163] Improved test [skip ci] --- test/pqxx_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index a60f0d1..5e41904 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -65,7 +65,7 @@ void test_bit(pqxx::connection &conn) { assert(res.size() == 3); assert(res[0][0].as() == embedding2); assert(res[1][0].as() == embedding); - assert(!res[2][0].as>().has_value()); + assert(!res[2][0].as>().has_value()); } void test_sparsevec(pqxx::connection &conn) { From 5d6a52fcbb12fd2f3a5f5b6b1d966ce22c62f8c7 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Jul 2025 09:39:37 -0700 Subject: [PATCH 002/163] Improved tests --- test/pqxx_test.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 5e41904..620c447 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -34,6 +34,9 @@ void test_vector(pqxx::connection &conn) { assert(res[0][0].as() == embedding2); assert(res[1][0].as() == embedding); assert(!res[2][0].as>().has_value()); + + assert(pqxx::to_string(embedding) == "[1,2,3]"); + assert(pqxx::from_string("[1,2,3]") == embedding); } void test_halfvec(pqxx::connection &conn) { @@ -51,6 +54,9 @@ void test_halfvec(pqxx::connection &conn) { assert(res[0][0].as() == embedding2); assert(res[1][0].as() == embedding); assert(!res[2][0].as>().has_value()); + + assert(pqxx::to_string(embedding) == "[1,2,3]"); + assert(pqxx::from_string("[1,2,3]") == embedding); } void test_bit(pqxx::connection &conn) { @@ -81,6 +87,10 @@ void test_sparsevec(pqxx::connection &conn) { assert(res[0][0].as() == "{1:4,2:5,3:6}/3"); assert(res[1][0].as() == "{1:1,2:2,3:3}/3"); assert(!res[2][0].as>().has_value()); + + assert(pqxx::to_string(embedding) == "{1:1,2:2,3:3}/3"); + // TODO add + // assert(pqxx::from_string("{1:1,2:2,3:3}/3") == embedding); } void test_sparsevec_nnz(pqxx::connection &conn) { From 1abf840e8605430df5df80894a9dbafb4f72d521 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Jul 2025 09:43:25 -0700 Subject: [PATCH 003/163] Improved code [skip ci] --- include/pgvector/pqxx.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 8c0812e..3e8fb01 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -40,7 +40,7 @@ template <> struct string_traits { std::stringstream ss(std::string(text.substr(1, text.size() - 2))); while (ss.good()) { std::string substr; - getline(ss, substr, ','); + std::getline(ss, substr, ','); result.push_back(std::stof(substr)); } return pgvector::Vector(result); @@ -85,7 +85,7 @@ template <> struct string_traits { std::stringstream ss(std::string(text.substr(1, text.size() - 2))); while (ss.good()) { std::string substr; - getline(ss, substr, ','); + std::getline(ss, substr, ','); result.push_back(std::stof(substr)); } return pgvector::HalfVector(result); From ccc5dce95c0488a8f615104fc97097e15a8bf261 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Jul 2025 09:45:42 -0700 Subject: [PATCH 004/163] Improved test --- test/pqxx_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 620c447..f6c589d 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -103,7 +103,7 @@ void test_sparsevec_nnz(pqxx::connection &conn) { tx.exec("INSERT INTO items (sparse_embedding) VALUES ($1)", {embedding}); assert(false); } catch (const pqxx::conversion_overrun& e) { - assert(std::strcmp(e.what(), "sparsevec cannot have more than 16000 dimensions") == 0); + assert(std::string_view(e.what()) == "sparsevec cannot have more than 16000 dimensions"); } } From 7bda624a33fdd3af14e05bd9f7d5cf910d5365d5 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Jul 2025 09:51:13 -0700 Subject: [PATCH 005/163] Improved code [skip ci] --- include/pgvector/pqxx.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 3e8fb01..7d27e68 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -37,7 +37,7 @@ template <> struct string_traits { // TODO don't copy string std::vector result; - std::stringstream ss(std::string(text.substr(1, text.size() - 2))); + std::istringstream ss(std::string(text.substr(1, text.size() - 2))); while (ss.good()) { std::string substr; std::getline(ss, substr, ','); @@ -82,7 +82,7 @@ template <> struct string_traits { // TODO don't copy string std::vector result; - std::stringstream ss(std::string(text.substr(1, text.size() - 2))); + std::istringstream ss(std::string(text.substr(1, text.size() - 2))); while (ss.good()) { std::string substr; std::getline(ss, substr, ','); From 847c4ba894cba5096451f86bb78751229663fbfb Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Jul 2025 09:57:56 -0700 Subject: [PATCH 006/163] Improved tests --- test/pqxx_test.cpp | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index f6c589d..fba5fdb 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -34,9 +34,6 @@ void test_vector(pqxx::connection &conn) { assert(res[0][0].as() == embedding2); assert(res[1][0].as() == embedding); assert(!res[2][0].as>().has_value()); - - assert(pqxx::to_string(embedding) == "[1,2,3]"); - assert(pqxx::from_string("[1,2,3]") == embedding); } void test_halfvec(pqxx::connection &conn) { @@ -54,9 +51,6 @@ void test_halfvec(pqxx::connection &conn) { assert(res[0][0].as() == embedding2); assert(res[1][0].as() == embedding); assert(!res[2][0].as>().has_value()); - - assert(pqxx::to_string(embedding) == "[1,2,3]"); - assert(pqxx::from_string("[1,2,3]") == embedding); } void test_bit(pqxx::connection &conn) { @@ -87,10 +81,6 @@ void test_sparsevec(pqxx::connection &conn) { assert(res[0][0].as() == "{1:4,2:5,3:6}/3"); assert(res[1][0].as() == "{1:1,2:2,3:3}/3"); assert(!res[2][0].as>().has_value()); - - assert(pqxx::to_string(embedding) == "{1:1,2:2,3:3}/3"); - // TODO add - // assert(pqxx::from_string("{1:1,2:2,3:3}/3") == embedding); } void test_sparsevec_nnz(pqxx::connection &conn) { @@ -145,6 +135,30 @@ void test_precision(pqxx::connection &conn) { assert(res[0][0].as() == embedding); } +void test_vector_to_string() { + assert(pqxx::to_string(pgvector::Vector({1, 2, 3})) == "[1,2,3]"); +} + +void test_vector_from_string() { + assert(pqxx::from_string("[1,2,3]") == pgvector::Vector({1, 2, 3})); +} + +void test_halfvec_to_string() { + assert(pqxx::to_string(pgvector::HalfVector({1, 2, 3})) == "[1,2,3]"); +} + +void test_halfvec_from_string() { + assert(pqxx::from_string("[1,2,3]") == pgvector::HalfVector({1, 2, 3})); +} + +void test_sparsevec_to_string() { + assert(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})) == "{1:1,3:2,5:3}/6"); +} + +void test_sparsevec_from_string() { + // TODO add +} + void test_pqxx() { pqxx::connection conn("dbname=pgvector_cpp_test"); setup(conn); @@ -157,4 +171,11 @@ void test_pqxx() { test_stream(conn); test_stream_to(conn); test_precision(conn); + + test_vector_to_string(); + test_vector_from_string(); + test_halfvec_to_string(); + test_halfvec_from_string(); + test_sparsevec_to_string(); + test_sparsevec_from_string(); } From 255cda1da9389fa2363ab85ce97ee7246c48a617 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Jul 2025 10:11:56 -0700 Subject: [PATCH 007/163] Improved tests for from_string --- test/pqxx_test.cpp | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index fba5fdb..64a093d 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -141,6 +141,29 @@ void test_vector_to_string() { void test_vector_from_string() { assert(pqxx::from_string("[1,2,3]") == pgvector::Vector({1, 2, 3})); + + try { + auto unused = pqxx::from_string(""); + assert(false); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Malformed vector literal"); + } + + // TODO change to pqxx::conversion_error + try { + auto unused = pqxx::from_string("[hello]"); + assert(false); + } catch (const std::invalid_argument& e) { + assert(true); + } + + // TODO change to pqxx::conversion_error + try { + auto unused = pqxx::from_string("[4e38]"); + assert(false); + } catch (const std::out_of_range& e) { + assert(true); + } } void test_halfvec_to_string() { @@ -149,6 +172,21 @@ void test_halfvec_to_string() { void test_halfvec_from_string() { assert(pqxx::from_string("[1,2,3]") == pgvector::HalfVector({1, 2, 3})); + + try { + auto unused = pqxx::from_string(""); + assert(false); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Malformed halfvec literal"); + } + + // TODO change to pqxx::conversion_error + try { + auto unused = pqxx::from_string("[hello]"); + assert(false); + } catch (const std::invalid_argument& e) { + assert(true); + } } void test_sparsevec_to_string() { From cc16b4666c1ce2b9a38bd3b95a481c3fc05b5c09 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Jul 2025 10:19:36 -0700 Subject: [PATCH 008/163] Improved tests for from_string [skip ci] --- test/pqxx_test.cpp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 64a093d..6d9ca2f 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -149,6 +149,21 @@ void test_vector_from_string() { assert(std::string_view(e.what()) == "Malformed vector literal"); } + try { + auto unused = pqxx::from_string("["); + assert(false); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Malformed vector literal"); + } + + // TODO change to no error? + try { + auto unused = pqxx::from_string("[]"); + assert(false); + } catch (const std::invalid_argument& e) { + assert(true); + } + // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("[hello]"); @@ -180,6 +195,21 @@ void test_halfvec_from_string() { assert(std::string_view(e.what()) == "Malformed halfvec literal"); } + try { + auto unused = pqxx::from_string("["); + assert(false); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Malformed halfvec literal"); + } + + // TODO change to no error? + try { + auto unused = pqxx::from_string("[]"); + assert(false); + } catch (const std::invalid_argument& e) { + assert(true); + } + // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("[hello]"); From 320f97fd76ca4966a430adf5e3b63ada040f6b86 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 10 Jul 2025 11:01:49 -0700 Subject: [PATCH 009/163] Improved test includes --- test/halfvec_test.cpp | 2 +- test/pqxx_test.cpp | 2 +- test/sparsevec_test.cpp | 2 +- test/vector_test.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index cfbc48e..84ce8b4 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -1,6 +1,6 @@ #include -#include "../include/pgvector/halfvec.hpp" +#include #if __cplusplus >= 202002L #include diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 6d9ca2f..b02b778 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -5,7 +5,7 @@ #include -#include "../include/pgvector/pqxx.hpp" +#include void setup(pqxx::connection &conn) { pqxx::nontransaction tx(conn); diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index 9b9dff2..fc94c69 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -1,7 +1,7 @@ #include #include -#include "../include/pgvector/sparsevec.hpp" +#include #if __cplusplus >= 202002L #include diff --git a/test/vector_test.cpp b/test/vector_test.cpp index 3580a8d..db43836 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -1,6 +1,6 @@ #include -#include "../include/pgvector/vector.hpp" +#include #if __cplusplus >= 202002L #include From 62f4b3059464fbbdb179c4841e49e55dae185a5a Mon Sep 17 00:00:00 2001 From: dylanlanigansmith <97814717+dylanlanigansmith@users.noreply.github.com> Date: Sun, 13 Jul 2025 12:36:12 -0700 Subject: [PATCH 010/163] Inline pqxx type_name variables (#6) avoids ODR violations and makes library much easier to integrate into larger projects. --- include/pgvector/pqxx.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 7d27e68..30268f6 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -21,7 +21,7 @@ /// @cond namespace pqxx { -template <> std::string const type_name{"vector"}; +template <> inline std::string const type_name{"vector"}; template <> struct nullness : pqxx::no_null {}; @@ -66,7 +66,7 @@ template <> struct string_traits { } }; -template <> std::string const type_name{"halfvec"}; +template <> inline std::string const type_name{"halfvec"}; template <> struct nullness : pqxx::no_null {}; @@ -111,7 +111,7 @@ template <> struct string_traits { } }; -template <> std::string const type_name{"sparsevec"}; +template <> inline std::string const type_name{"sparsevec"}; template <> struct nullness : pqxx::no_null {}; From cc07471c14d87e1a52d5850ac3985c2eb7138432 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 13 Jul 2025 12:37:29 -0700 Subject: [PATCH 011/163] Updated return type for type_name - #6 --- include/pgvector/pqxx.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 30268f6..7945391 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -21,7 +21,7 @@ /// @cond namespace pqxx { -template <> inline std::string const type_name{"vector"}; +template <> inline std::string_view const type_name{"vector"}; template <> struct nullness : pqxx::no_null {}; @@ -66,7 +66,7 @@ template <> struct string_traits { } }; -template <> inline std::string const type_name{"halfvec"}; +template <> inline std::string_view const type_name{"halfvec"}; template <> struct nullness : pqxx::no_null {}; @@ -111,7 +111,7 @@ template <> struct string_traits { } }; -template <> inline std::string const type_name{"sparsevec"}; +template <> inline std::string_view const type_name{"sparsevec"}; template <> struct nullness : pqxx::no_null {}; From 85b678e8d1e69d358ed5cb85d31b056a351565e7 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 13 Jul 2025 12:45:03 -0700 Subject: [PATCH 012/163] Updated changelog [skip ci] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c57cbc..37cf061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.3 (unreleased) + +- Fixed `duplicate symbol` error + ## 0.2.2 (2025-02-23) - Added map constructor to `SparseVector` From bc790720d3e34d05d1db637ac2c5e625df27a987 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 13 Jul 2025 12:45:40 -0700 Subject: [PATCH 013/163] Updated changelog [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37cf061..f06acad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.2.3 (unreleased) -- Fixed `duplicate symbol` error +- Fixed `duplicate symbol` errors ## 0.2.2 (2025-02-23) From 84f63f4c3d0b010b41eb5db6636445e4d4eb31a3 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 13 Jul 2025 12:49:24 -0700 Subject: [PATCH 014/163] Added test for ODR --- test/main.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/main.cpp b/test/main.cpp index 28d0239..c2bcb27 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -1,3 +1,6 @@ +// Test ODR +#include + void test_vector(); void test_halfvec(); void test_sparsevec(); From bd3ca7bad00b53dc44bff73fd672a985f144a567 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 13 Jul 2025 12:51:14 -0700 Subject: [PATCH 015/163] Version bump to 0.2.3 [skip ci] --- CHANGELOG.md | 2 +- CMakeLists.txt | 2 +- README.md | 4 ++-- include/pgvector/halfvec.hpp | 2 +- include/pgvector/pqxx.hpp | 2 +- include/pgvector/sparsevec.hpp | 2 +- include/pgvector/vector.hpp | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f06acad..5c927af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.2.3 (unreleased) +## 0.2.3 (2025-07-13) - Fixed `duplicate symbol` errors diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f5e98a..b9e60de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.18) -project(pgvector VERSION 0.2.2 LANGUAGES CXX) +project(pgvector VERSION 0.2.3 LANGUAGES CXX) include(GNUInstallDirs) diff --git a/README.md b/README.md index 9879343..c5ba308 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ Supports [libpqxx](https://github.com/jtv/libpqxx) ## Installation -Add [the headers](https://github.com/pgvector/pgvector-cpp/tree/v0.2.2/include) to your project (supports C++17 and greater). +Add [the headers](https://github.com/pgvector/pgvector-cpp/tree/v0.2.3/include) to your project (supports C++17 and greater). There is also support for CMake and FetchContent: ```cmake include(FetchContent) -FetchContent_Declare(pgvector GIT_REPOSITORY https://github.com/pgvector/pgvector-cpp.git GIT_TAG v0.2.2) +FetchContent_Declare(pgvector GIT_REPOSITORY https://github.com/pgvector/pgvector-cpp.git GIT_TAG v0.2.3) FetchContent_MakeAvailable(pgvector) target_link_libraries(app PRIVATE pgvector::pgvector) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index d0cae75..87eadf5 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -1,5 +1,5 @@ /* - * pgvector-cpp v0.2.2 + * pgvector-cpp v0.2.3 * https://github.com/pgvector/pgvector-cpp * MIT License */ diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 7945391..b90fad6 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -1,5 +1,5 @@ /* - * pgvector-cpp v0.2.2 + * pgvector-cpp v0.2.3 * https://github.com/pgvector/pgvector-cpp * MIT License */ diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 6686178..16c27be 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -1,5 +1,5 @@ /* - * pgvector-cpp v0.2.2 + * pgvector-cpp v0.2.3 * https://github.com/pgvector/pgvector-cpp * MIT License */ diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index fcc385a..c88c247 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -1,5 +1,5 @@ /* - * pgvector-cpp v0.2.2 + * pgvector-cpp v0.2.3 * https://github.com/pgvector/pgvector-cpp * MIT License */ From 716f3ed8cc7fe1d18aa21dd781d191ce641272bf Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Mon, 1 Sep 2025 12:13:57 -0700 Subject: [PATCH 016/163] Removed unneeded namespaces --- include/pgvector/pqxx.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index b90fad6..69fbbfa 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -23,7 +23,7 @@ namespace pqxx { template <> inline std::string_view const type_name{"vector"}; -template <> struct nullness : pqxx::no_null {}; +template <> struct nullness : no_null {}; template <> struct string_traits { static constexpr bool converts_to_string{true}; @@ -68,7 +68,7 @@ template <> struct string_traits { template <> inline std::string_view const type_name{"halfvec"}; -template <> struct nullness : pqxx::no_null {}; +template <> struct nullness : no_null {}; template <> struct string_traits { static constexpr bool converts_to_string{true}; @@ -113,7 +113,7 @@ template <> struct string_traits { template <> inline std::string_view const type_name{"sparsevec"}; -template <> struct nullness : pqxx::no_null {}; +template <> struct nullness : no_null {}; template <> struct string_traits { static constexpr bool converts_to_string{true}; From 14ff4c9341288e6c313d04ea2c446660fe86534c Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 2 Sep 2025 12:40:55 -0700 Subject: [PATCH 017/163] Added check for text length --- include/pgvector/pqxx.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 69fbbfa..19675cb 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -31,7 +31,7 @@ template <> struct string_traits { static constexpr bool converts_from_string{true}; static pgvector::Vector from_string(std::string_view text) { - if (text.front() != '[' || text.back() != ']') { + if (text.size() < 2 || text.front() != '[' || text.back() != ']') { throw conversion_error("Malformed vector literal"); } @@ -76,7 +76,7 @@ template <> struct string_traits { static constexpr bool converts_from_string{true}; static pgvector::HalfVector from_string(std::string_view text) { - if (text.front() != '[' || text.back() != ']') { + if (text.size() < 2 || text.front() != '[' || text.back() != ']') { throw conversion_error("Malformed halfvec literal"); } From 30c5a8bc8acbe6d733e6478bc480951bb5e45388 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 2 Sep 2025 13:08:03 -0700 Subject: [PATCH 018/163] Added from_string support for SparseVector --- CHANGELOG.md | 4 ++++ include/pgvector/pqxx.hpp | 33 +++++++++++++++++++++++++++++++-- test/pqxx_test.cpp | 8 ++++---- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c927af..2dbc48b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.4 (unreleased) + +- Added `from_string` support for `SparseVector` + ## 0.2.3 (2025-07-13) - Fixed `duplicate symbol` errors diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 19675cb..7745d2d 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -118,8 +118,37 @@ template <> struct nullness : no_null struct string_traits { static constexpr bool converts_to_string{true}; - // TODO add from_string - static constexpr bool converts_from_string{false}; + static constexpr bool converts_from_string{true}; + + static pgvector::SparseVector from_string(std::string_view text) { + if (text.size() < 4 || text.front() != '{') { + throw conversion_error("Malformed sparsevec literal"); + } + + size_t n = text.find("}/", 1); + if (n == std::string_view::npos) { + throw conversion_error("Malformed sparsevec literal"); + } + + std::vector indices; + std::vector values; + std::istringstream ss(std::string(text.substr(1, n))); + while (ss.good()) { + std::string substr; + std::getline(ss, substr, ','); + + size_t ne = substr.find(":"); + if (ne == std::string::npos) { + throw conversion_error("Malformed sparsevec literal"); + } + + indices.push_back(std::stoi(substr.substr(0, ne)) - 1); + values.push_back(std::stof(substr.substr(ne + 1))); + } + + int dimensions = std::stoi(std::string(text.substr(n + 2))); + return pgvector::SparseVector(dimensions, indices, values); + } static zview to_buf(char* begin, char* end, const pgvector::SparseVector& value) { char *const next = into_buf(begin, end, value); diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index b02b778..4f7541d 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -78,9 +78,9 @@ void test_sparsevec(pqxx::connection &conn) { pqxx::result res = tx.exec("SELECT sparse_embedding FROM items ORDER BY sparse_embedding <-> $1", {embedding2}); assert(res.size() == 3); - assert(res[0][0].as() == "{1:4,2:5,3:6}/3"); - assert(res[1][0].as() == "{1:1,2:2,3:3}/3"); - assert(!res[2][0].as>().has_value()); + assert(res[0][0].as() == embedding2); + assert(res[1][0].as() == embedding); + assert(!res[2][0].as>().has_value()); } void test_sparsevec_nnz(pqxx::connection &conn) { @@ -224,7 +224,7 @@ void test_sparsevec_to_string() { } void test_sparsevec_from_string() { - // TODO add + assert(pqxx::from_string("{1:1,3:2,5:3}/6") == pgvector::SparseVector({1, 0, 2, 0, 3, 0})); } void test_pqxx() { From 3a2fb74c98f15305c70129eb7fbec6151e67e712 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 2 Sep 2025 13:22:04 -0700 Subject: [PATCH 019/163] Improved from_string function for SparseVector --- include/pgvector/pqxx.hpp | 29 ++++++++++++------- test/pqxx_test.cpp | 61 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 7745d2d..07a2c97 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -130,23 +130,30 @@ template <> struct string_traits { throw conversion_error("Malformed sparsevec literal"); } + int dimensions = std::stoi(std::string(text.substr(n + 2))); + if (dimensions < 0) { + throw conversion_error("Malformed sparsevec literal"); + } + std::vector indices; std::vector values; - std::istringstream ss(std::string(text.substr(1, n))); - while (ss.good()) { - std::string substr; - std::getline(ss, substr, ','); - size_t ne = substr.find(":"); - if (ne == std::string::npos) { - throw conversion_error("Malformed sparsevec literal"); - } + if (n > 1) { + std::istringstream ss(std::string(text.substr(1, n))); + while (ss.good()) { + std::string substr; + std::getline(ss, substr, ','); + + size_t ne = substr.find(":"); + if (ne == std::string::npos) { + throw conversion_error("Malformed sparsevec literal"); + } - indices.push_back(std::stoi(substr.substr(0, ne)) - 1); - values.push_back(std::stof(substr.substr(ne + 1))); + indices.push_back(std::stoi(substr.substr(0, ne)) - 1); + values.push_back(std::stof(substr.substr(ne + 1))); + } } - int dimensions = std::stoi(std::string(text.substr(n + 2))); return pgvector::SparseVector(dimensions, indices, values); } diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 4f7541d..39f3645 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -225,6 +225,67 @@ void test_sparsevec_to_string() { void test_sparsevec_from_string() { assert(pqxx::from_string("{1:1,3:2,5:3}/6") == pgvector::SparseVector({1, 0, 2, 0, 3, 0})); + assert(pqxx::from_string("{}/6") == pgvector::SparseVector({0, 0, 0, 0, 0, 0})); + + try { + auto unused = pqxx::from_string(""); + assert(false); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Malformed sparsevec literal"); + } + + try { + auto unused = pqxx::from_string("{"); + assert(false); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Malformed sparsevec literal"); + } + + try { + auto unused = pqxx::from_string("{}/-1"); + assert(false); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Malformed sparsevec literal"); + } + + // TODO change to pqxx::conversion_error + try { + auto unused = pqxx::from_string("{:}/1"); + assert(false); + } catch (const std::invalid_argument& e) { + assert(true); + } + + try { + auto unused = pqxx::from_string("{,}/1"); + assert(false); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Malformed sparsevec literal"); + } + + // TODO change to pqxx::conversion_error + try { + auto unused = pqxx::from_string("{a:1}/1"); + assert(false); + } catch (const std::invalid_argument& e) { + assert(true); + } + + // TODO change to pqxx::conversion_error + try { + auto unused = pqxx::from_string("{1:a}/1"); + assert(false); + } catch (const std::invalid_argument& e) { + assert(true); + } + + // TODO change to pqxx::conversion_error + try { + auto unused = pqxx::from_string("{}/a"); + assert(false); + } catch (const std::invalid_argument& e) { + assert(true); + } } void test_pqxx() { From 4e5adc7412cd8562664a9eabe8a6148af2fce545 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 2 Sep 2025 13:27:19 -0700 Subject: [PATCH 020/163] Improved from_string function for SparseVector --- include/pgvector/pqxx.hpp | 11 +++++++++-- test/pqxx_test.cpp | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 07a2c97..d084019 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -149,8 +149,15 @@ template <> struct string_traits { throw conversion_error("Malformed sparsevec literal"); } - indices.push_back(std::stoi(substr.substr(0, ne)) - 1); - values.push_back(std::stof(substr.substr(ne + 1))); + int index = std::stoi(substr.substr(0, ne)); + float value = std::stof(substr.substr(ne + 1)); + + if (index < 1) { + throw conversion_error("Malformed sparsevec literal"); + } + + indices.push_back(index - 1); + values.push_back(value); } } diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 39f3645..2c609db 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -263,6 +263,20 @@ void test_sparsevec_from_string() { assert(std::string_view(e.what()) == "Malformed sparsevec literal"); } + try { + auto unused = pqxx::from_string("{0:1}/1"); + assert(false); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Malformed sparsevec literal"); + } + + try { + auto unused = pqxx::from_string("{-2147483648:1}/1"); + assert(false); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Malformed sparsevec literal"); + } + // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("{a:1}/1"); From b4e5d18a686f87913a708ac25255d522b58c5e22 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 2 Sep 2025 14:02:52 -0700 Subject: [PATCH 021/163] Added another from_string test for SparseVector [skip ci] --- test/pqxx_test.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 2c609db..d93a53b 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -277,6 +277,14 @@ void test_sparsevec_from_string() { assert(std::string_view(e.what()) == "Malformed sparsevec literal"); } + // TODO change to pqxx::conversion_error + try { + auto unused = pqxx::from_string("{1:4e38}/1"); + assert(false); + } catch (const std::out_of_range& e) { + assert(true); + } + // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("{a:1}/1"); From 936da20ba8832922775219ee7ad4fbb133ac5970 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 6 Sep 2025 00:12:31 -0700 Subject: [PATCH 022/163] Updated pgvector on CI --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42c04d4..66f0d83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: dev-files: true - run: | cd /tmp - git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git + git clone --branch v0.8.1 https://github.com/pgvector/pgvector.git cd pgvector make sudo make install From 5f3aff797ff1a8dffe7937ed542ea1c7cf3823fe Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 10 Sep 2025 17:34:39 -0700 Subject: [PATCH 023/163] Added test --- test/pqxx_test.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index d93a53b..3d5c4e5 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -241,6 +241,14 @@ void test_sparsevec_from_string() { assert(std::string_view(e.what()) == "Malformed sparsevec literal"); } + // TODO change to pqxx::conversion_error + try { + auto unused = pqxx::from_string("{ }/"); + assert(false); + } catch (const std::invalid_argument& e) { + assert(true); + } + try { auto unused = pqxx::from_string("{}/-1"); assert(false); From 74c0767b156d98260a86ea558719ff2265421fe6 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 10 Sep 2025 18:33:29 -0700 Subject: [PATCH 024/163] Improved from_string for SparseVector --- include/pgvector/pqxx.hpp | 12 ++++++------ test/pqxx_test.cpp | 36 +++++++++++++++--------------------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index d084019..3948658 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -130,16 +130,16 @@ template <> struct string_traits { throw conversion_error("Malformed sparsevec literal"); } - int dimensions = std::stoi(std::string(text.substr(n + 2))); + int dimensions = pqxx::from_string(text.substr(n + 2)); if (dimensions < 0) { - throw conversion_error("Malformed sparsevec literal"); + throw conversion_error("Dimensions cannot be negative"); } std::vector indices; std::vector values; if (n > 1) { - std::istringstream ss(std::string(text.substr(1, n))); + std::istringstream ss(std::string(text.substr(1, n - 1))); while (ss.good()) { std::string substr; std::getline(ss, substr, ','); @@ -149,11 +149,11 @@ template <> struct string_traits { throw conversion_error("Malformed sparsevec literal"); } - int index = std::stoi(substr.substr(0, ne)); - float value = std::stof(substr.substr(ne + 1)); + int index = pqxx::from_string(substr.substr(0, ne)); + float value = pqxx::from_string(substr.substr(ne + 1)); if (index < 1) { - throw conversion_error("Malformed sparsevec literal"); + throw conversion_error("Index out of bounds"); } indices.push_back(index - 1); diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 3d5c4e5..7e5b344 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -241,27 +241,25 @@ void test_sparsevec_from_string() { assert(std::string_view(e.what()) == "Malformed sparsevec literal"); } - // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("{ }/"); assert(false); - } catch (const std::invalid_argument& e) { - assert(true); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Could not convert '' to int"); } try { auto unused = pqxx::from_string("{}/-1"); assert(false); } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Malformed sparsevec literal"); + assert(std::string_view(e.what()) == "Dimensions cannot be negative"); } - // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("{:}/1"); assert(false); - } catch (const std::invalid_argument& e) { - assert(true); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Could not convert '' to int"); } try { @@ -275,46 +273,42 @@ void test_sparsevec_from_string() { auto unused = pqxx::from_string("{0:1}/1"); assert(false); } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Malformed sparsevec literal"); + assert(std::string_view(e.what()) == "Index out of bounds"); } try { auto unused = pqxx::from_string("{-2147483648:1}/1"); assert(false); } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Malformed sparsevec literal"); + assert(std::string_view(e.what()) == "Index out of bounds"); } - // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("{1:4e38}/1"); assert(false); - } catch (const std::out_of_range& e) { - assert(true); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Could not convert string to numeric value: '4e38'." || std::string_view(e.what()) == "Could not convert '4e38' to float"); } - // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("{a:1}/1"); assert(false); - } catch (const std::invalid_argument& e) { - assert(true); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Could not convert 'a' to int"); } - // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("{1:a}/1"); assert(false); - } catch (const std::invalid_argument& e) { - assert(true); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Could not convert string to numeric value: 'a'." || std::string_view(e.what()) == "Could not convert 'a' to float"); } - // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("{}/a"); assert(false); - } catch (const std::invalid_argument& e) { - assert(true); + } catch (const pqxx::conversion_error& e) { + assert(std::string_view(e.what()) == "Could not convert 'a' to int"); } } From e35f71ea9f44a1b56460b0d37a7470f9fecb7712 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 10 Sep 2025 20:34:54 -0700 Subject: [PATCH 025/163] Use string_traits like other methods --- include/pgvector/pqxx.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 3948658..01093c7 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -130,7 +130,7 @@ template <> struct string_traits { throw conversion_error("Malformed sparsevec literal"); } - int dimensions = pqxx::from_string(text.substr(n + 2)); + int dimensions = string_traits::from_string(text.substr(n + 2)); if (dimensions < 0) { throw conversion_error("Dimensions cannot be negative"); } @@ -149,8 +149,8 @@ template <> struct string_traits { throw conversion_error("Malformed sparsevec literal"); } - int index = pqxx::from_string(substr.substr(0, ne)); - float value = pqxx::from_string(substr.substr(ne + 1)); + int index = string_traits::from_string(substr.substr(0, ne)); + float value = string_traits::from_string(substr.substr(ne + 1)); if (index < 1) { throw conversion_error("Index out of bounds"); From 3d658fb9dad4af7f408a60ced5fc581fe308056e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 12 Sep 2025 11:16:52 -0700 Subject: [PATCH 026/163] Version bump to 0.2.4 [skip ci] --- CHANGELOG.md | 2 +- CMakeLists.txt | 2 +- README.md | 4 ++-- include/pgvector/halfvec.hpp | 2 +- include/pgvector/pqxx.hpp | 2 +- include/pgvector/sparsevec.hpp | 2 +- include/pgvector/vector.hpp | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dbc48b..2716cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.2.4 (unreleased) +## 0.2.4 (2025-09-12) - Added `from_string` support for `SparseVector` diff --git a/CMakeLists.txt b/CMakeLists.txt index b9e60de..1aff571 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.18) -project(pgvector VERSION 0.2.3 LANGUAGES CXX) +project(pgvector VERSION 0.2.4 LANGUAGES CXX) include(GNUInstallDirs) diff --git a/README.md b/README.md index c5ba308..59f9b3e 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ Supports [libpqxx](https://github.com/jtv/libpqxx) ## Installation -Add [the headers](https://github.com/pgvector/pgvector-cpp/tree/v0.2.3/include) to your project (supports C++17 and greater). +Add [the headers](https://github.com/pgvector/pgvector-cpp/tree/v0.2.4/include) to your project (supports C++17 and greater). There is also support for CMake and FetchContent: ```cmake include(FetchContent) -FetchContent_Declare(pgvector GIT_REPOSITORY https://github.com/pgvector/pgvector-cpp.git GIT_TAG v0.2.3) +FetchContent_Declare(pgvector GIT_REPOSITORY https://github.com/pgvector/pgvector-cpp.git GIT_TAG v0.2.4) FetchContent_MakeAvailable(pgvector) target_link_libraries(app PRIVATE pgvector::pgvector) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 87eadf5..6048e62 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -1,5 +1,5 @@ /* - * pgvector-cpp v0.2.3 + * pgvector-cpp v0.2.4 * https://github.com/pgvector/pgvector-cpp * MIT License */ diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 01093c7..1e6a580 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -1,5 +1,5 @@ /* - * pgvector-cpp v0.2.3 + * pgvector-cpp v0.2.4 * https://github.com/pgvector/pgvector-cpp * MIT License */ diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 16c27be..1cd053a 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -1,5 +1,5 @@ /* - * pgvector-cpp v0.2.3 + * pgvector-cpp v0.2.4 * https://github.com/pgvector/pgvector-cpp * MIT License */ diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index c88c247..8e15685 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -1,5 +1,5 @@ /* - * pgvector-cpp v0.2.3 + * pgvector-cpp v0.2.4 * https://github.com/pgvector/pgvector-cpp * MIT License */ From 8132687a088db09b3c9fd79e635b02d157984fb1 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 2 Oct 2025 11:06:17 -0700 Subject: [PATCH 027/163] Updated libpqxx for tests and examples --- CMakeLists.txt | 2 +- examples/citus/CMakeLists.txt | 2 +- examples/cohere/CMakeLists.txt | 2 +- examples/disco/CMakeLists.txt | 2 +- examples/hybrid/CMakeLists.txt | 2 +- examples/loading/CMakeLists.txt | 2 +- examples/openai/CMakeLists.txt | 2 +- examples/rdkit/CMakeLists.txt | 2 +- examples/sparse/CMakeLists.txt | 2 +- test/pqxx_test.cpp | 8 ++++---- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1aff571..e109dc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,7 +26,7 @@ if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) if(BUILD_TESTING) include(FetchContent) - FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.1) + FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) FetchContent_MakeAvailable(libpqxx) add_executable(test test/halfvec_test.cpp test/main.cpp test/pqxx_test.cpp test/sparsevec_test.cpp test/vector_test.cpp) diff --git a/examples/citus/CMakeLists.txt b/examples/citus/CMakeLists.txt index cdaa70f..ee09163 100644 --- a/examples/citus/CMakeLists.txt +++ b/examples/citus/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 17) include(FetchContent) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.1) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) FetchContent_MakeAvailable(libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/cohere/CMakeLists.txt b/examples/cohere/CMakeLists.txt index c2acad2..a35ce2d 100644 --- a/examples/cohere/CMakeLists.txt +++ b/examples/cohere/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.1) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/disco/CMakeLists.txt b/examples/disco/CMakeLists.txt index c734b42..d2a6e09 100644 --- a/examples/disco/CMakeLists.txt +++ b/examples/disco/CMakeLists.txt @@ -7,7 +7,7 @@ set(CMAKE_CXX_STANDARD 20) include(FetchContent) FetchContent_Declare(disco GIT_REPOSITORY https://github.com/ankane/disco-cpp.git GIT_TAG v0.1.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.1) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) FetchContent_MakeAvailable(disco libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/hybrid/CMakeLists.txt b/examples/hybrid/CMakeLists.txt index c2acad2..a35ce2d 100644 --- a/examples/hybrid/CMakeLists.txt +++ b/examples/hybrid/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.1) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/loading/CMakeLists.txt b/examples/loading/CMakeLists.txt index cdaa70f..ee09163 100644 --- a/examples/loading/CMakeLists.txt +++ b/examples/loading/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 17) include(FetchContent) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.1) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) FetchContent_MakeAvailable(libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/openai/CMakeLists.txt b/examples/openai/CMakeLists.txt index c2acad2..a35ce2d 100644 --- a/examples/openai/CMakeLists.txt +++ b/examples/openai/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.1) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/rdkit/CMakeLists.txt b/examples/rdkit/CMakeLists.txt index 1d607ee..caf4e8a 100644 --- a/examples/rdkit/CMakeLists.txt +++ b/examples/rdkit/CMakeLists.txt @@ -9,7 +9,7 @@ find_package(Boost COMPONENTS iostreams serialization system REQUIRED) include(FetchContent) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.1) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) FetchContent_MakeAvailable(libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/sparse/CMakeLists.txt b/examples/sparse/CMakeLists.txt index c2acad2..a35ce2d 100644 --- a/examples/sparse/CMakeLists.txt +++ b/examples/sparse/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.1) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 7e5b344..7e08828 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -245,7 +245,7 @@ void test_sparsevec_from_string() { auto unused = pqxx::from_string("{ }/"); assert(false); } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert '' to int"); + assert(std::string_view(e.what()) == "Could not convert '' to int: Invalid argument."); } try { @@ -259,7 +259,7 @@ void test_sparsevec_from_string() { auto unused = pqxx::from_string("{:}/1"); assert(false); } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert '' to int"); + assert(std::string_view(e.what()) == "Could not convert '' to int: Invalid argument."); } try { @@ -294,7 +294,7 @@ void test_sparsevec_from_string() { auto unused = pqxx::from_string("{a:1}/1"); assert(false); } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert 'a' to int"); + assert(std::string_view(e.what()) == "Could not convert 'a' to int: Invalid argument."); } try { @@ -308,7 +308,7 @@ void test_sparsevec_from_string() { auto unused = pqxx::from_string("{}/a"); assert(false); } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert 'a' to int"); + assert(std::string_view(e.what()) == "Could not convert 'a' to int: Invalid argument."); } } From 70ca395831928ef5d3bc251025c3334e82a55638 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 2 Oct 2025 11:09:50 -0700 Subject: [PATCH 028/163] Fixed CI --- test/pqxx_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 7e08828..71da654 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -287,7 +287,7 @@ void test_sparsevec_from_string() { auto unused = pqxx::from_string("{1:4e38}/1"); assert(false); } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert string to numeric value: '4e38'." || std::string_view(e.what()) == "Could not convert '4e38' to float"); + assert(std::string_view(e.what()) == "Could not convert string to numeric value: '4e38'." || std::string_view(e.what()) == "Could not convert '4e38' to float: Invalid argument."); } try { From 2a97d8e7c0e551994d7554a16fb171b07f59d273 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 2 Oct 2025 11:17:25 -0700 Subject: [PATCH 029/163] Fixed CI --- test/pqxx_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 71da654..5f1c774 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -287,7 +287,7 @@ void test_sparsevec_from_string() { auto unused = pqxx::from_string("{1:4e38}/1"); assert(false); } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert string to numeric value: '4e38'." || std::string_view(e.what()) == "Could not convert '4e38' to float: Invalid argument."); + assert(std::string_view(e.what()) == "Could not convert string to numeric value: '4e38'." || std::string_view(e.what()) == "Could not convert '4e38' to float" || std::string_view(e.what()) == "Could not convert '4e38' to float: Value out of range."); } try { @@ -301,7 +301,7 @@ void test_sparsevec_from_string() { auto unused = pqxx::from_string("{1:a}/1"); assert(false); } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert string to numeric value: 'a'." || std::string_view(e.what()) == "Could not convert 'a' to float"); + assert(std::string_view(e.what()) == "Could not convert string to numeric value: 'a'." || std::string_view(e.what()) == "Could not convert 'a' to float" || std::string_view(e.what()) == "Could not convert 'a' to float: Invalid argument."); } try { From f9c733b3dd07f5d7e0e2ecf24bd7378819378f39 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 23 Nov 2025 17:53:40 -0800 Subject: [PATCH 030/163] Added todos [skip ci] --- include/pgvector/pqxx.hpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 1e6a580..a6c84aa 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -35,12 +35,14 @@ template <> struct string_traits { throw conversion_error("Malformed vector literal"); } - // TODO don't copy string std::vector result; + // TODO support empty array + // TODO don't copy string std::istringstream ss(std::string(text.substr(1, text.size() - 2))); while (ss.good()) { std::string substr; std::getline(ss, substr, ','); + // TODO use pqxx::from_string result.push_back(std::stof(substr)); } return pgvector::Vector(result); @@ -80,12 +82,14 @@ template <> struct string_traits { throw conversion_error("Malformed halfvec literal"); } - // TODO don't copy string std::vector result; + // TODO support empty array + // TODO don't copy string std::istringstream ss(std::string(text.substr(1, text.size() - 2))); while (ss.good()) { std::string substr; std::getline(ss, substr, ','); + // TODO use pqxx::from_string result.push_back(std::stof(substr)); } return pgvector::HalfVector(result); From a110db7ebbcf8890c7e4aacf33e861337d1edc9c Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 23 Nov 2025 18:07:04 -0800 Subject: [PATCH 031/163] Added todos [skip ci] --- include/pgvector/halfvec.hpp | 1 + include/pgvector/vector.hpp | 1 + 2 files changed, 2 insertions(+) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 6048e62..e4d31cb 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -36,6 +36,7 @@ class HalfVector { } /// Creates a half vector from an array. + // TODO remove in 0.3.0 HalfVector(const float* value, size_t n) { value_ = std::vector{value, value + n}; } diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index 8e15685..4da600c 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -36,6 +36,7 @@ class Vector { } /// Creates a vector from an array. + // TODO remove in 0.3.0 Vector(const float* value, size_t n) { value_ = std::vector{value, value + n}; } From 29fe7f1a02428c8359b14de7870d25606e035d8c Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 23 Nov 2025 21:45:19 -0800 Subject: [PATCH 032/163] Switched to write_values --- test/pqxx_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 5f1c774..25699d1 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -116,8 +116,8 @@ void test_stream_to(pqxx::connection &conn) { pqxx::nontransaction tx(conn); auto stream = pqxx::stream_to::table(tx, {"items"}, {"embedding"}); - stream << pgvector::Vector({1, 2, 3}); - stream << pgvector::Vector({4, 5, 6}); + stream.write_values(pgvector::Vector({1, 2, 3})); + stream.write_values(pgvector::Vector({4, 5, 6})); stream.complete(); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY id"); assert(res[0][0].as() == "[1,2,3]"); From fba18e21f3a92ff9a2419b62c046a20e456295ed Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 6 Dec 2025 09:17:17 -0800 Subject: [PATCH 033/163] Updated libpqxx for tests and examples --- CMakeLists.txt | 2 +- examples/citus/CMakeLists.txt | 2 +- examples/cohere/CMakeLists.txt | 2 +- examples/disco/CMakeLists.txt | 2 +- examples/hybrid/CMakeLists.txt | 2 +- examples/loading/CMakeLists.txt | 2 +- examples/openai/CMakeLists.txt | 2 +- examples/rdkit/CMakeLists.txt | 2 +- examples/sparse/CMakeLists.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e109dc2..9fa9eaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,7 +26,7 @@ if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) if(BUILD_TESTING) include(FetchContent) - FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) + FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) FetchContent_MakeAvailable(libpqxx) add_executable(test test/halfvec_test.cpp test/main.cpp test/pqxx_test.cpp test/sparsevec_test.cpp test/vector_test.cpp) diff --git a/examples/citus/CMakeLists.txt b/examples/citus/CMakeLists.txt index ee09163..e99bf70 100644 --- a/examples/citus/CMakeLists.txt +++ b/examples/citus/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 17) include(FetchContent) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) FetchContent_MakeAvailable(libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/cohere/CMakeLists.txt b/examples/cohere/CMakeLists.txt index a35ce2d..c139c7b 100644 --- a/examples/cohere/CMakeLists.txt +++ b/examples/cohere/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/disco/CMakeLists.txt b/examples/disco/CMakeLists.txt index d2a6e09..86238ab 100644 --- a/examples/disco/CMakeLists.txt +++ b/examples/disco/CMakeLists.txt @@ -7,7 +7,7 @@ set(CMAKE_CXX_STANDARD 20) include(FetchContent) FetchContent_Declare(disco GIT_REPOSITORY https://github.com/ankane/disco-cpp.git GIT_TAG v0.1.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) FetchContent_MakeAvailable(disco libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/hybrid/CMakeLists.txt b/examples/hybrid/CMakeLists.txt index a35ce2d..c139c7b 100644 --- a/examples/hybrid/CMakeLists.txt +++ b/examples/hybrid/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/loading/CMakeLists.txt b/examples/loading/CMakeLists.txt index ee09163..e99bf70 100644 --- a/examples/loading/CMakeLists.txt +++ b/examples/loading/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 17) include(FetchContent) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) FetchContent_MakeAvailable(libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/openai/CMakeLists.txt b/examples/openai/CMakeLists.txt index a35ce2d..c139c7b 100644 --- a/examples/openai/CMakeLists.txt +++ b/examples/openai/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/rdkit/CMakeLists.txt b/examples/rdkit/CMakeLists.txt index caf4e8a..90cb8a6 100644 --- a/examples/rdkit/CMakeLists.txt +++ b/examples/rdkit/CMakeLists.txt @@ -9,7 +9,7 @@ find_package(Boost COMPONENTS iostreams serialization system REQUIRED) include(FetchContent) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) FetchContent_MakeAvailable(libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/sparse/CMakeLists.txt b/examples/sparse/CMakeLists.txt index a35ce2d..c139c7b 100644 --- a/examples/sparse/CMakeLists.txt +++ b/examples/sparse/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.2) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) From 4b5f13ab8fe9a17c7c7e0e363a8e8f65bdc97988 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 31 Dec 2025 10:45:58 -0800 Subject: [PATCH 034/163] Updated libpqxx for tests and examples --- CMakeLists.txt | 2 +- examples/citus/CMakeLists.txt | 2 +- examples/cohere/CMakeLists.txt | 2 +- examples/disco/CMakeLists.txt | 2 +- examples/hybrid/CMakeLists.txt | 2 +- examples/loading/CMakeLists.txt | 2 +- examples/openai/CMakeLists.txt | 2 +- examples/rdkit/CMakeLists.txt | 2 +- examples/sparse/CMakeLists.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fa9eaf..2fc6cd4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,7 +26,7 @@ if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) if(BUILD_TESTING) include(FetchContent) - FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) + FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) FetchContent_MakeAvailable(libpqxx) add_executable(test test/halfvec_test.cpp test/main.cpp test/pqxx_test.cpp test/sparsevec_test.cpp test/vector_test.cpp) diff --git a/examples/citus/CMakeLists.txt b/examples/citus/CMakeLists.txt index e99bf70..79c6e4b 100644 --- a/examples/citus/CMakeLists.txt +++ b/examples/citus/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 17) include(FetchContent) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) FetchContent_MakeAvailable(libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/cohere/CMakeLists.txt b/examples/cohere/CMakeLists.txt index c139c7b..46097e9 100644 --- a/examples/cohere/CMakeLists.txt +++ b/examples/cohere/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/disco/CMakeLists.txt b/examples/disco/CMakeLists.txt index 86238ab..e99f7f9 100644 --- a/examples/disco/CMakeLists.txt +++ b/examples/disco/CMakeLists.txt @@ -7,7 +7,7 @@ set(CMAKE_CXX_STANDARD 20) include(FetchContent) FetchContent_Declare(disco GIT_REPOSITORY https://github.com/ankane/disco-cpp.git GIT_TAG v0.1.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) FetchContent_MakeAvailable(disco libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/hybrid/CMakeLists.txt b/examples/hybrid/CMakeLists.txt index c139c7b..46097e9 100644 --- a/examples/hybrid/CMakeLists.txt +++ b/examples/hybrid/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/loading/CMakeLists.txt b/examples/loading/CMakeLists.txt index e99bf70..79c6e4b 100644 --- a/examples/loading/CMakeLists.txt +++ b/examples/loading/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 17) include(FetchContent) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) FetchContent_MakeAvailable(libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/openai/CMakeLists.txt b/examples/openai/CMakeLists.txt index c139c7b..46097e9 100644 --- a/examples/openai/CMakeLists.txt +++ b/examples/openai/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/rdkit/CMakeLists.txt b/examples/rdkit/CMakeLists.txt index 90cb8a6..44f3783 100644 --- a/examples/rdkit/CMakeLists.txt +++ b/examples/rdkit/CMakeLists.txt @@ -9,7 +9,7 @@ find_package(Boost COMPONENTS iostreams serialization system REQUIRED) include(FetchContent) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) FetchContent_MakeAvailable(libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/sparse/CMakeLists.txt b/examples/sparse/CMakeLists.txt index c139c7b..46097e9 100644 --- a/examples/sparse/CMakeLists.txt +++ b/examples/sparse/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.4) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) From 2e2ba17823bb98cf2c626781916c9fd2590b1ba6 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 7 Jan 2026 14:26:03 -0800 Subject: [PATCH 035/163] Updated checkout action --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66f0d83..9f04708 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ jobs: build: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ankane/setup-postgres@v1 with: database: pgvector_cpp_test From d50b2e2dc5f7da9b802bd68ada026c41172abec9 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Mon, 12 Jan 2026 09:34:23 -0800 Subject: [PATCH 036/163] Added todos [skip ci] --- include/pgvector/halfvec.hpp | 1 + include/pgvector/vector.hpp | 1 + 2 files changed, 2 insertions(+) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index e4d31cb..19f69e4 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -31,6 +31,7 @@ class HalfVector { /// Creates a half vector from a `std::vector`. // TODO add explicit in 0.3.0 + // TODO add noexcept in 0.3.0 HalfVector(std::vector&& value) { value_ = std::move(value); } diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index 4da600c..121a61c 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -31,6 +31,7 @@ class Vector { /// Creates a vector from a `std::vector`. // TODO add explicit in 0.3.0 + // TODO add noexcept in 0.3.0 Vector(std::vector&& value) { value_ = std::move(value); } From 2fbe758f7a59135e4b568a6c5ef81b81e5597da6 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 11:32:21 -0800 Subject: [PATCH 037/163] Added support for libpqxx 8 --- .github/workflows/build.yml | 4 -- CHANGELOG.md | 6 +++ CMakeLists.txt | 6 +-- include/pgvector/pqxx.hpp | 90 +++++++++++++------------------------ 4 files changed, 39 insertions(+), 67 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f04708..1ced9b9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,10 +16,6 @@ jobs: make sudo make install - - run: cmake -S . -B build -DBUILD_TESTING=ON -DCMAKE_CXX_STANDARD=17 - - run: cmake --build build - - run: build/test - - run: cmake -S . -B build -DBUILD_TESTING=ON -DCMAKE_CXX_STANDARD=20 - run: cmake --build build - run: build/test diff --git a/CHANGELOG.md b/CHANGELOG.md index 2716cc9..f4d13e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.3.0 (unreleased) + +- Added support for libpqxx 8 +- Dropped support for libpqxx 7 +- Dropped support for C++17 + ## 0.2.4 (2025-09-12) - Added `from_string` support for `SparseVector` diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fc6cd4..d5648da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ include(GNUInstallDirs) add_library(pgvector INTERFACE) add_library(pgvector::pgvector ALIAS pgvector) -target_compile_features(pgvector INTERFACE cxx_std_17) +target_compile_features(pgvector INTERFACE cxx_std_20) target_include_directories( pgvector @@ -26,13 +26,13 @@ if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) if(BUILD_TESTING) include(FetchContent) - FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) + FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(libpqxx) add_executable(test test/halfvec_test.cpp test/main.cpp test/pqxx_test.cpp test/sparsevec_test.cpp test/vector_test.cpp) target_link_libraries(test PRIVATE libpqxx::pqxx pgvector::pgvector) if(NOT MSVC) - target_compile_options(test PRIVATE -Wall -Wextra -Wpedantic -Werror) + target_compile_options(test PRIVATE -Wall -Wextra -Wpedantic -Werror -Wno-unused-parameter) endif() endif() endif() diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index a6c84aa..7dcc58f 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -21,16 +21,12 @@ /// @cond namespace pqxx { -template <> inline std::string_view const type_name{"vector"}; +template <> inline constexpr std::string_view name_type() noexcept { return "vector"; }; template <> struct nullness : no_null {}; template <> struct string_traits { - static constexpr bool converts_to_string{true}; - - static constexpr bool converts_from_string{true}; - - static pgvector::Vector from_string(std::string_view text) { + static pgvector::Vector from_string(std::string_view text, ctx c = {}) { if (text.size() < 2 || text.front() != '[' || text.back() != ']') { throw conversion_error("Malformed vector literal"); } @@ -48,18 +44,12 @@ template <> struct string_traits { return pgvector::Vector(result); } - static zview to_buf(char* begin, char* end, const pgvector::Vector& value) { - char *const next = into_buf(begin, end, value); - return zview{begin, next - begin - 1}; - } - - static char* into_buf(char* begin, char* end, const pgvector::Vector& value) { - auto ret = string_traits>::into_buf( - begin, end, static_cast>(value)); + static std::string_view to_buf(std::span buf, const pgvector::Vector& value, ctx c = {}) { + auto len = pqxx::into_buf(buf, static_cast>(value), c); // replace array brackets - *begin = '['; - *(ret - 2) = ']'; - return ret; + buf[0] = '['; + buf[len - 1] = ']'; + return {std::data(buf), len}; } static size_t size_buffer(const pgvector::Vector& value) noexcept { @@ -68,16 +58,12 @@ template <> struct string_traits { } }; -template <> inline std::string_view const type_name{"halfvec"}; +template <> inline constexpr std::string_view name_type() noexcept { return "halfvec"; }; template <> struct nullness : no_null {}; template <> struct string_traits { - static constexpr bool converts_to_string{true}; - - static constexpr bool converts_from_string{true}; - - static pgvector::HalfVector from_string(std::string_view text) { + static pgvector::HalfVector from_string(std::string_view text, ctx c = {}) { if (text.size() < 2 || text.front() != '[' || text.back() != ']') { throw conversion_error("Malformed halfvec literal"); } @@ -95,18 +81,12 @@ template <> struct string_traits { return pgvector::HalfVector(result); } - static zview to_buf(char* begin, char* end, const pgvector::HalfVector& value) { - char *const next = into_buf(begin, end, value); - return zview{begin, next - begin - 1}; - } - - static char* into_buf(char* begin, char* end, const pgvector::HalfVector& value) { - auto ret = string_traits>::into_buf( - begin, end, static_cast>(value)); + static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { + auto len = pqxx::into_buf(buf, static_cast>(value), c); // replace array brackets - *begin = '['; - *(ret - 2) = ']'; - return ret; + buf[0] = '['; + buf[len - 1] = ']'; + return {std::data(buf), len}; } static size_t size_buffer(const pgvector::HalfVector& value) noexcept { @@ -115,16 +95,12 @@ template <> struct string_traits { } }; -template <> inline std::string_view const type_name{"sparsevec"}; +template <> inline constexpr std::string_view name_type() noexcept { return "sparsevec"; }; template <> struct nullness : no_null {}; template <> struct string_traits { - static constexpr bool converts_to_string{true}; - - static constexpr bool converts_from_string{true}; - - static pgvector::SparseVector from_string(std::string_view text) { + static pgvector::SparseVector from_string(std::string_view text, ctx c = {}) { if (text.size() < 4 || text.front() != '{') { throw conversion_error("Malformed sparsevec literal"); } @@ -134,7 +110,7 @@ template <> struct string_traits { throw conversion_error("Malformed sparsevec literal"); } - int dimensions = string_traits::from_string(text.substr(n + 2)); + int dimensions = string_traits::from_string(text.substr(n + 2), c); if (dimensions < 0) { throw conversion_error("Dimensions cannot be negative"); } @@ -153,8 +129,8 @@ template <> struct string_traits { throw conversion_error("Malformed sparsevec literal"); } - int index = string_traits::from_string(substr.substr(0, ne)); - float value = string_traits::from_string(substr.substr(ne + 1)); + int index = string_traits::from_string(substr.substr(0, ne), c); + float value = string_traits::from_string(substr.substr(ne + 1), c); if (index < 1) { throw conversion_error("Index out of bounds"); @@ -168,12 +144,7 @@ template <> struct string_traits { return pgvector::SparseVector(dimensions, indices, values); } - static zview to_buf(char* begin, char* end, const pgvector::SparseVector& value) { - char *const next = into_buf(begin, end, value); - return zview{begin, next - begin - 1}; - } - - static char* into_buf(char* begin, char* end, const pgvector::SparseVector& value) { + static std::string_view to_buf(std::span buf, const pgvector::SparseVector& value, ctx c = {}) { int dimensions = value.dimensions(); auto indices = value.indices(); auto values = value.values(); @@ -185,25 +156,24 @@ template <> struct string_traits { throw conversion_overrun{"sparsevec cannot have more than 16000 dimensions"}; } - char *here = begin; - *here++ = '{'; + size_t here = 0; + buf[here++] = '{'; for (size_t i = 0; i < nnz; i++) { if (i != 0) { - *here++ = ','; + buf[here++] = ','; } - here = string_traits::into_buf(here, end, indices[i] + 1) - 1; - *here++ = ':'; - here = string_traits::into_buf(here, end, values[i]) - 1; + here += pqxx::into_buf(buf.subspan(here), indices[i] + 1, c); + buf[here++] = ':'; + here += pqxx::into_buf(buf.subspan(here), values[i], c); } - *here++ = '}'; - *here++ = '/'; - here = string_traits::into_buf(here, end, dimensions) - 1; - *here++ = '\0'; + buf[here++] = '}'; + buf[here++] = '/'; + here += pqxx::into_buf(buf.subspan(here), dimensions, c); - return here; + return {std::data(buf), here}; } static size_t size_buffer(const pgvector::SparseVector& value) noexcept { From 6511e4e587fe4ddd7617f899a4437879f4546a51 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 11:33:08 -0800 Subject: [PATCH 038/163] Updated checkout action [skip ci] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ced9b9..e971e23 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ jobs: build: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: ankane/setup-postgres@v1 with: database: pgvector_cpp_test From 944bd65a198992bd2748e433f01027c336684aea Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 11:37:30 -0800 Subject: [PATCH 039/163] Added C++23 to CI [skip ci] --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e971e23..73b885f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,6 +20,10 @@ jobs: - run: cmake --build build - run: build/test + - run: cmake -S . -B build -DBUILD_TESTING=ON -DCMAKE_CXX_STANDARD=23 + - run: cmake --build build + - run: build/test + - run: | sudo apt-get install valgrind valgrind --leak-check=yes build/test From 338bf32da8bc9f29788c7337a692d69a8242a845 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 11:43:33 -0800 Subject: [PATCH 040/163] Removed conditional code [skip ci] --- include/pgvector/halfvec.hpp | 7 +------ include/pgvector/sparsevec.hpp | 7 +------ include/pgvector/vector.hpp | 7 +------ test/halfvec_test.cpp | 9 +-------- test/sparsevec_test.cpp | 9 +-------- test/vector_test.cpp | 9 +-------- 6 files changed, 6 insertions(+), 42 deletions(-) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 19f69e4..2be244d 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -8,13 +8,10 @@ #include #include +#include #include #include -#if __cplusplus >= 202002L -#include -#endif - namespace pgvector { /// A half vector. class HalfVector { @@ -42,13 +39,11 @@ class HalfVector { value_ = std::vector{value, value + n}; } -#if __cplusplus >= 202002L /// Creates a half vector from a span. // TODO add explicit in 0.3.0 HalfVector(std::span value) { value_ = std::vector(value.begin(), value.end()); } -#endif /// Returns the number of dimensions. size_t dimensions() const { diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 1cd053a..25c37f0 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -9,14 +9,11 @@ #include #include #include +#include #include #include #include -#if __cplusplus >= 202002L -#include -#endif - namespace pgvector { /// A sparse vector. class SparseVector { @@ -48,7 +45,6 @@ class SparseVector { } } -#if __cplusplus >= 202002L /// Creates a sparse vector from a span. // TODO add explicit in 0.3.0 SparseVector(std::span value) { @@ -61,7 +57,6 @@ class SparseVector { } } } -#endif /// Creates a sparse vector from a map of non-zero elements. SparseVector(const std::unordered_map& map, int dimensions) { diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index 121a61c..507d40f 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -8,13 +8,10 @@ #include #include +#include #include #include -#if __cplusplus >= 202002L -#include -#endif - namespace pgvector { /// A vector. class Vector { @@ -42,13 +39,11 @@ class Vector { value_ = std::vector{value, value + n}; } -#if __cplusplus >= 202002L /// Creates a vector from a span. // TODO add explicit in 0.3.0 Vector(std::span value) { value_ = std::vector(value.begin(), value.end()); } -#endif /// Returns the number of dimensions. size_t dimensions() const { diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index 84ce8b4..3c54881 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -1,11 +1,8 @@ #include +#include #include -#if __cplusplus >= 202002L -#include -#endif - using pgvector::HalfVector; static void test_constructor_vector() { @@ -13,16 +10,12 @@ static void test_constructor_vector() { assert(vec.dimensions() == 3); } -#if __cplusplus >= 202002L static void test_constructor_span() { auto vec = HalfVector(std::span({1, 2, 3})); assert(vec.dimensions() == 3); } -#endif void test_halfvec() { test_constructor_vector(); -#if __cplusplus >= 202002L test_constructor_span(); -#endif } diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index fc94c69..9704d1c 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -1,12 +1,9 @@ #include +#include #include #include -#if __cplusplus >= 202002L -#include -#endif - using pgvector::SparseVector; static void test_constructor_vector() { @@ -16,12 +13,10 @@ static void test_constructor_vector() { assert(vec.values() == (std::vector{1, 2, 3})); } -#if __cplusplus >= 202002L static void test_constructor_span() { auto vec = SparseVector(std::span({1, 0, 2, 0, 3, 0})); assert(vec.dimensions() == 6); } -#endif static void test_constructor_map() { std::unordered_map map = {{2, 2}, {4, 3}, {3, 0}, {0, 1}}; @@ -33,8 +28,6 @@ static void test_constructor_map() { void test_sparsevec() { test_constructor_vector(); -#if __cplusplus >= 202002L test_constructor_span(); -#endif test_constructor_map(); } diff --git a/test/vector_test.cpp b/test/vector_test.cpp index db43836..ef04411 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -1,11 +1,8 @@ #include +#include #include -#if __cplusplus >= 202002L -#include -#endif - using pgvector::Vector; static void test_constructor_vector() { @@ -13,16 +10,12 @@ static void test_constructor_vector() { assert(vec.dimensions() == 3); } -#if __cplusplus >= 202002L static void test_constructor_span() { auto vec = Vector(std::span({1, 2, 3})); assert(vec.dimensions() == 3); } -#endif void test_vector() { test_constructor_vector(); -#if __cplusplus >= 202002L test_constructor_span(); -#endif } From 07e8ce8a9ec5082e9568ef59f277407cd9012a1d Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 11:51:24 -0800 Subject: [PATCH 041/163] Improved constructors [skip ci] --- include/pgvector/halfvec.hpp | 20 +++----------------- include/pgvector/sparsevec.hpp | 10 ++-------- include/pgvector/vector.hpp | 20 +++----------------- test/pqxx_test.cpp | 4 ++-- 4 files changed, 10 insertions(+), 44 deletions(-) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 2be244d..e118144 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -16,32 +16,18 @@ namespace pgvector { /// A half vector. class HalfVector { public: - /// @private - // TODO remove in 0.3.0 - HalfVector() = default; - /// Creates a half vector from a `std::vector`. - // TODO add explicit in 0.3.0 - HalfVector(const std::vector& value) { + explicit HalfVector(const std::vector& value) { value_ = value; } /// Creates a half vector from a `std::vector`. - // TODO add explicit in 0.3.0 - // TODO add noexcept in 0.3.0 - HalfVector(std::vector&& value) { + explicit HalfVector(std::vector&& value) noexcept { value_ = std::move(value); } - /// Creates a half vector from an array. - // TODO remove in 0.3.0 - HalfVector(const float* value, size_t n) { - value_ = std::vector{value, value + n}; - } - /// Creates a half vector from a span. - // TODO add explicit in 0.3.0 - HalfVector(std::span value) { + explicit HalfVector(std::span value) { value_ = std::vector(value.begin(), value.end()); } diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 25c37f0..a5e6475 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -18,10 +18,6 @@ namespace pgvector { /// A sparse vector. class SparseVector { public: - /// @private - // TODO remove in 0.3.0 - SparseVector() = default; - /// @private SparseVector(int dimensions, const std::vector& indices, const std::vector& values) { if (values.size() != indices.size()) { @@ -33,8 +29,7 @@ class SparseVector { } /// Creates a sparse vector from a dense vector. - // TODO add explicit in 0.3.0 - SparseVector(const std::vector& value) { + explicit SparseVector(const std::vector& value) { dimensions_ = value.size(); for (size_t i = 0; i < value.size(); i++) { float v = value[i]; @@ -46,8 +41,7 @@ class SparseVector { } /// Creates a sparse vector from a span. - // TODO add explicit in 0.3.0 - SparseVector(std::span value) { + explicit SparseVector(std::span value) { dimensions_ = value.size(); for (size_t i = 0; i < value.size(); i++) { float v = value[i]; diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index 507d40f..cea7db1 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -16,32 +16,18 @@ namespace pgvector { /// A vector. class Vector { public: - /// @private - // TODO remove in 0.3.0 - Vector() = default; - /// Creates a vector from a `std::vector`. - // TODO add explicit in 0.3.0 - Vector(const std::vector& value) { + explicit Vector(const std::vector& value) { value_ = value; } /// Creates a vector from a `std::vector`. - // TODO add explicit in 0.3.0 - // TODO add noexcept in 0.3.0 - Vector(std::vector&& value) { + explicit Vector(std::vector&& value) noexcept { value_ = std::move(value); } - /// Creates a vector from an array. - // TODO remove in 0.3.0 - Vector(const float* value, size_t n) { - value_ = std::vector{value, value + n}; - } - /// Creates a vector from a span. - // TODO add explicit in 0.3.0 - Vector(std::span value) { + explicit Vector(std::span value) { value_ = std::vector(value.begin(), value.end()); } diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 25699d1..e7cee3c 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -26,7 +26,7 @@ void test_vector(pqxx::connection &conn) { auto embedding = pgvector::Vector({1, 2, 3}); assert(embedding.dimensions() == 3); float arr[] = {4, 5, 6}; - auto embedding2 = pgvector::Vector(arr, 3); + auto embedding2 = pgvector::Vector(std::span{arr, 3}); tx.exec("INSERT INTO items (embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY embedding <-> $1", {embedding2}); @@ -43,7 +43,7 @@ void test_halfvec(pqxx::connection &conn) { auto embedding = pgvector::HalfVector({1, 2, 3}); assert(embedding.dimensions() == 3); float arr[] = {4, 5, 6}; - auto embedding2 = pgvector::HalfVector(arr, 3); + auto embedding2 = pgvector::HalfVector(std::span{arr, 3}); tx.exec("INSERT INTO items (half_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT half_embedding FROM items ORDER BY half_embedding <-> $1", {embedding2}); From 10b798fd50e0c697c550e7b8eadcecdb8e28da6d Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 11:58:24 -0800 Subject: [PATCH 042/163] Improved libpqxx conversions --- CMakeLists.txt | 2 +- include/pgvector/pqxx.hpp | 32 ++++++++++++++++---------------- test/pqxx_test.cpp | 27 +++++---------------------- 3 files changed, 22 insertions(+), 39 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d5648da..e2e5daf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) add_executable(test test/halfvec_test.cpp test/main.cpp test/pqxx_test.cpp test/sparsevec_test.cpp test/vector_test.cpp) target_link_libraries(test PRIVATE libpqxx::pqxx pgvector::pgvector) if(NOT MSVC) - target_compile_options(test PRIVATE -Wall -Wextra -Wpedantic -Werror -Wno-unused-parameter) + target_compile_options(test PRIVATE -Wall -Wextra -Wpedantic -Werror) endif() endif() endif() diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 7dcc58f..e32fcc3 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -32,14 +32,14 @@ template <> struct string_traits { } std::vector result; - // TODO support empty array - // TODO don't copy string - std::istringstream ss(std::string(text.substr(1, text.size() - 2))); - while (ss.good()) { - std::string substr; - std::getline(ss, substr, ','); - // TODO use pqxx::from_string - result.push_back(std::stof(substr)); + if (text.size() > 2) { + // TODO don't copy string + std::istringstream ss(std::string(text.substr(1, text.size() - 2))); + while (ss.good()) { + std::string substr; + std::getline(ss, substr, ','); + result.push_back(string_traits::from_string(substr, c)); + } } return pgvector::Vector(result); } @@ -69,14 +69,14 @@ template <> struct string_traits { } std::vector result; - // TODO support empty array - // TODO don't copy string - std::istringstream ss(std::string(text.substr(1, text.size() - 2))); - while (ss.good()) { - std::string substr; - std::getline(ss, substr, ','); - // TODO use pqxx::from_string - result.push_back(std::stof(substr)); + if (text.size() > 2) { + // TODO don't copy string + std::istringstream ss(std::string(text.substr(1, text.size() - 2))); + while (ss.good()) { + std::string substr; + std::getline(ss, substr, ','); + result.push_back(string_traits::from_string(substr, c)); + } } return pgvector::HalfVector(result); } diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index e7cee3c..bcb6511 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -141,6 +141,7 @@ void test_vector_to_string() { void test_vector_from_string() { assert(pqxx::from_string("[1,2,3]") == pgvector::Vector({1, 2, 3})); + assert(pqxx::from_string("[]") == pgvector::Vector(std::vector{})); try { auto unused = pqxx::from_string(""); @@ -156,27 +157,17 @@ void test_vector_from_string() { assert(std::string_view(e.what()) == "Malformed vector literal"); } - // TODO change to no error? - try { - auto unused = pqxx::from_string("[]"); - assert(false); - } catch (const std::invalid_argument& e) { - assert(true); - } - - // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("[hello]"); assert(false); - } catch (const std::invalid_argument& e) { + } catch (const pqxx::conversion_error& e) { assert(true); } - // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("[4e38]"); assert(false); - } catch (const std::out_of_range& e) { + } catch (const pqxx::conversion_error& e) { assert(true); } } @@ -187,6 +178,7 @@ void test_halfvec_to_string() { void test_halfvec_from_string() { assert(pqxx::from_string("[1,2,3]") == pgvector::HalfVector({1, 2, 3})); + assert(pqxx::from_string("[]") == pgvector::HalfVector(std::vector{})); try { auto unused = pqxx::from_string(""); @@ -202,19 +194,10 @@ void test_halfvec_from_string() { assert(std::string_view(e.what()) == "Malformed halfvec literal"); } - // TODO change to no error? - try { - auto unused = pqxx::from_string("[]"); - assert(false); - } catch (const std::invalid_argument& e) { - assert(true); - } - - // TODO change to pqxx::conversion_error try { auto unused = pqxx::from_string("[hello]"); assert(false); - } catch (const std::invalid_argument& e) { + } catch (const pqxx::conversion_error& e) { assert(true); } } From c14123ca84eb21f26421ee1434156f18acc3db0c Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 12:31:55 -0800 Subject: [PATCH 043/163] Switched to move contructor --- include/pgvector/pqxx.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index e32fcc3..7d97e7f 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -41,7 +41,7 @@ template <> struct string_traits { result.push_back(string_traits::from_string(substr, c)); } } - return pgvector::Vector(result); + return pgvector::Vector(std::move(result)); } static std::string_view to_buf(std::span buf, const pgvector::Vector& value, ctx c = {}) { @@ -78,7 +78,7 @@ template <> struct string_traits { result.push_back(string_traits::from_string(substr, c)); } } - return pgvector::HalfVector(result); + return pgvector::HalfVector(std::move(result)); } static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { From 289bb91c8e90001c11b12a84bb14b80e8947f538 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 12:33:11 -0800 Subject: [PATCH 044/163] Updated todos [skip ci] --- include/pgvector/pqxx.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 7d97e7f..9679e06 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -33,7 +33,7 @@ template <> struct string_traits { std::vector result; if (text.size() > 2) { - // TODO don't copy string + // TODO remove all copying std::istringstream ss(std::string(text.substr(1, text.size() - 2))); while (ss.good()) { std::string substr; @@ -70,7 +70,7 @@ template <> struct string_traits { std::vector result; if (text.size() > 2) { - // TODO don't copy string + // TODO remove all copying std::istringstream ss(std::string(text.substr(1, text.size() - 2))); while (ss.good()) { std::string substr; From 9e7dc6f04d85d3e6bb51ab28df7974922fe20f07 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 15:44:32 -0800 Subject: [PATCH 045/163] Removed unnecessary copying --- include/pgvector/pqxx.hpp | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 9679e06..2437fe8 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -33,13 +33,14 @@ template <> struct string_traits { std::vector result; if (text.size() > 2) { - // TODO remove all copying - std::istringstream ss(std::string(text.substr(1, text.size() - 2))); - while (ss.good()) { - std::string substr; - std::getline(ss, substr, ','); - result.push_back(string_traits::from_string(substr, c)); + size_t start = 1; + for (size_t i = start; i < text.size() - 2; i++) { + if (text[i] == ',') { + result.push_back(string_traits::from_string(text.substr(start, i - start), c)); + start = i + 1; + } } + result.push_back(string_traits::from_string(text.substr(start, text.size() - start - 1), c)); } return pgvector::Vector(std::move(result)); } @@ -70,13 +71,14 @@ template <> struct string_traits { std::vector result; if (text.size() > 2) { - // TODO remove all copying - std::istringstream ss(std::string(text.substr(1, text.size() - 2))); - while (ss.good()) { - std::string substr; - std::getline(ss, substr, ','); - result.push_back(string_traits::from_string(substr, c)); + size_t start = 1; + for (size_t i = start; i < text.size() - 2; i++) { + if (text[i] == ',') { + result.push_back(string_traits::from_string(text.substr(start, i - start), c)); + start = i + 1; + } } + result.push_back(string_traits::from_string(text.substr(start, text.size() - start - 1), c)); } return pgvector::HalfVector(std::move(result)); } From 0307cc1f8d232d15e3588d7891563d814b607a04 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 15:54:25 -0800 Subject: [PATCH 046/163] Removed unneeded noexcept [skip ci] --- include/pgvector/halfvec.hpp | 2 +- include/pgvector/vector.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index e118144..63fc130 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -22,7 +22,7 @@ class HalfVector { } /// Creates a half vector from a `std::vector`. - explicit HalfVector(std::vector&& value) noexcept { + explicit HalfVector(std::vector&& value) { value_ = std::move(value); } diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index cea7db1..b325ead 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -22,7 +22,7 @@ class Vector { } /// Creates a vector from a `std::vector`. - explicit Vector(std::vector&& value) noexcept { + explicit Vector(std::vector&& value) { value_ = std::move(value); } From 5416014dab6a40b91b87de36338d2c603f28f3f9 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 17:22:15 -0800 Subject: [PATCH 047/163] Updated examples [skip ci] --- examples/citus/CMakeLists.txt | 4 ++-- examples/cohere/CMakeLists.txt | 8 ++++---- examples/disco/CMakeLists.txt | 2 +- examples/hybrid/CMakeLists.txt | 8 ++++---- examples/loading/CMakeLists.txt | 4 ++-- examples/loading/example.cpp | 2 +- examples/openai/CMakeLists.txt | 8 ++++---- examples/rdkit/CMakeLists.txt | 6 +++--- examples/sparse/CMakeLists.txt | 8 ++++---- 9 files changed, 25 insertions(+), 25 deletions(-) diff --git a/examples/citus/CMakeLists.txt b/examples/citus/CMakeLists.txt index 79c6e4b..f05b59c 100644 --- a/examples/citus/CMakeLists.txt +++ b/examples/citus/CMakeLists.txt @@ -2,11 +2,11 @@ cmake_minimum_required(VERSION 3.18) project(example) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) include(FetchContent) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/cohere/CMakeLists.txt b/examples/cohere/CMakeLists.txt index 46097e9..149c11c 100644 --- a/examples/cohere/CMakeLists.txt +++ b/examples/cohere/CMakeLists.txt @@ -2,13 +2,13 @@ cmake_minimum_required(VERSION 3.18) project(example) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) include(FetchContent) -FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) -FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) +FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.14.2) +FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.12.0) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/disco/CMakeLists.txt b/examples/disco/CMakeLists.txt index e99f7f9..55017e1 100644 --- a/examples/disco/CMakeLists.txt +++ b/examples/disco/CMakeLists.txt @@ -7,7 +7,7 @@ set(CMAKE_CXX_STANDARD 20) include(FetchContent) FetchContent_Declare(disco GIT_REPOSITORY https://github.com/ankane/disco-cpp.git GIT_TAG v0.1.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(disco libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/hybrid/CMakeLists.txt b/examples/hybrid/CMakeLists.txt index 46097e9..149c11c 100644 --- a/examples/hybrid/CMakeLists.txt +++ b/examples/hybrid/CMakeLists.txt @@ -2,13 +2,13 @@ cmake_minimum_required(VERSION 3.18) project(example) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) include(FetchContent) -FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) -FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) +FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.14.2) +FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.12.0) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/loading/CMakeLists.txt b/examples/loading/CMakeLists.txt index 79c6e4b..f05b59c 100644 --- a/examples/loading/CMakeLists.txt +++ b/examples/loading/CMakeLists.txt @@ -2,11 +2,11 @@ cmake_minimum_required(VERSION 3.18) project(example) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) include(FetchContent) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/loading/example.cpp b/examples/loading/example.cpp index 86e15b1..9192522 100644 --- a/examples/loading/example.cpp +++ b/examples/loading/example.cpp @@ -41,7 +41,7 @@ int main() { std::cout << '.' << std::flush; } - stream << pgvector::Vector(embeddings[i]); + stream.write_values(pgvector::Vector(embeddings[i])); } stream.complete(); std::cout << std::endl << "Success!" << std::endl; diff --git a/examples/openai/CMakeLists.txt b/examples/openai/CMakeLists.txt index 46097e9..149c11c 100644 --- a/examples/openai/CMakeLists.txt +++ b/examples/openai/CMakeLists.txt @@ -2,13 +2,13 @@ cmake_minimum_required(VERSION 3.18) project(example) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) include(FetchContent) -FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) -FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) +FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.14.2) +FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.12.0) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/rdkit/CMakeLists.txt b/examples/rdkit/CMakeLists.txt index 44f3783..504d619 100644 --- a/examples/rdkit/CMakeLists.txt +++ b/examples/rdkit/CMakeLists.txt @@ -2,14 +2,14 @@ cmake_minimum_required(VERSION 3.18) project(example) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) find_package(RDKit REQUIRED) -find_package(Boost COMPONENTS iostreams serialization system REQUIRED) +find_package(Boost COMPONENTS iostreams serialization REQUIRED) include(FetchContent) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) diff --git a/examples/sparse/CMakeLists.txt b/examples/sparse/CMakeLists.txt index 46097e9..149c11c 100644 --- a/examples/sparse/CMakeLists.txt +++ b/examples/sparse/CMakeLists.txt @@ -2,13 +2,13 @@ cmake_minimum_required(VERSION 3.18) project(example) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) include(FetchContent) -FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.11.1) -FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) -FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 7.10.5) +FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.14.2) +FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.12.0) +FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(cpr json libpqxx) add_subdirectory("${PROJECT_SOURCE_DIR}/../.." pgvector) From bc55f6cabafc8637a9e886b5bed3a86c49609f4f Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 17:35:55 -0800 Subject: [PATCH 048/163] Updated example [skip ci] --- examples/citus/example.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/citus/example.cpp b/examples/citus/example.cpp index 7ac53c5..38eaacb 100644 --- a/examples/citus/example.cpp +++ b/examples/citus/example.cpp @@ -74,7 +74,7 @@ int main() { std::cout << "Loading data in parallel" << std::endl; auto stream = pqxx::stream_to::table(tx2, {"items"}, {"embedding", "category_id"}); for (size_t i = 0; i < embeddings.size(); i++) { - stream << std::make_tuple(pgvector::Vector(embeddings[i]), categories[i]); + stream.write_values(pgvector::Vector(embeddings[i]), categories[i]); } stream.complete(); From 8d6c34ee82ec4d7fad7cb8b00f066a57bfb21898 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 28 Feb 2026 18:51:28 -0800 Subject: [PATCH 049/163] Updated pgvector on CI --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 73b885f..6dbd1ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: dev-files: true - run: | cd /tmp - git clone --branch v0.8.1 https://github.com/pgvector/pgvector.git + git clone --branch v0.8.2 https://github.com/pgvector/pgvector.git cd pgvector make sudo make install From 5d68d6cb6412622c7407e010781eab08480e0911 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 12:19:28 -0800 Subject: [PATCH 050/163] Removed unnecessary copying --- include/pgvector/pqxx.hpp | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 2437fe8..3bdf95f 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -120,27 +120,32 @@ template <> struct string_traits { std::vector indices; std::vector values; - if (n > 1) { - std::istringstream ss(std::string(text.substr(1, n - 1))); - while (ss.good()) { - std::string substr; - std::getline(ss, substr, ','); - - size_t ne = substr.find(":"); - if (ne == std::string::npos) { - throw conversion_error("Malformed sparsevec literal"); - } + auto cb = [&](std::string_view substr) { + size_t ne = substr.find(":"); + if (ne == std::string::npos) { + throw conversion_error("Malformed sparsevec literal"); + } - int index = string_traits::from_string(substr.substr(0, ne), c); - float value = string_traits::from_string(substr.substr(ne + 1), c); + int index = string_traits::from_string(substr.substr(0, ne), c); + float value = string_traits::from_string(substr.substr(ne + 1), c); - if (index < 1) { - throw conversion_error("Index out of bounds"); - } + if (index < 1) { + throw conversion_error("Index out of bounds"); + } + + indices.push_back(index - 1); + values.push_back(value); + }; - indices.push_back(index - 1); - values.push_back(value); + if (n > 1) { + size_t start = 1; + for (size_t i = start; i < n - 1; i++) { + if (text[i] == ',') { + cb(text.substr(start, i - start)); + start = i + 1; + } } + cb(text.substr(start, n - start)); } return pgvector::SparseVector(dimensions, indices, values); From 8b390703513df67f8b36b7da7d0cc308920607c7 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 12:20:33 -0800 Subject: [PATCH 051/163] Improved code [skip ci] --- include/pgvector/pqxx.hpp | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 3bdf95f..8931a2f 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -120,24 +120,24 @@ template <> struct string_traits { std::vector indices; std::vector values; - auto cb = [&](std::string_view substr) { - size_t ne = substr.find(":"); - if (ne == std::string::npos) { - throw conversion_error("Malformed sparsevec literal"); - } + if (n > 1) { + auto cb = [&](std::string_view substr) { + size_t ne = substr.find(":"); + if (ne == std::string::npos) { + throw conversion_error("Malformed sparsevec literal"); + } - int index = string_traits::from_string(substr.substr(0, ne), c); - float value = string_traits::from_string(substr.substr(ne + 1), c); + int index = string_traits::from_string(substr.substr(0, ne), c); + float value = string_traits::from_string(substr.substr(ne + 1), c); - if (index < 1) { - throw conversion_error("Index out of bounds"); - } + if (index < 1) { + throw conversion_error("Index out of bounds"); + } - indices.push_back(index - 1); - values.push_back(value); - }; + indices.push_back(index - 1); + values.push_back(value); + }; - if (n > 1) { size_t start = 1; for (size_t i = start; i < n - 1; i++) { if (text[i] == ',') { From f5d62f43734add5dd81e4f9aefc0351f56603bab Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 12:39:49 -0800 Subject: [PATCH 052/163] Improved code [skip ci] --- include/pgvector/pqxx.hpp | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 8931a2f..87769c2 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -33,14 +33,15 @@ template <> struct string_traits { std::vector result; if (text.size() > 2) { - size_t start = 1; - for (size_t i = start; i < text.size() - 2; i++) { - if (text[i] == ',') { - result.push_back(string_traits::from_string(text.substr(start, i - start), c)); + std::string_view inner = text.substr(1, text.size() - 2); + size_t start = 0; + for (size_t i = 0; i < inner.size(); i++) { + if (inner[i] == ',') { + result.push_back(string_traits::from_string(inner.substr(start, i - start), c)); start = i + 1; } } - result.push_back(string_traits::from_string(text.substr(start, text.size() - start - 1), c)); + result.push_back(string_traits::from_string(inner.substr(start), c)); } return pgvector::Vector(std::move(result)); } @@ -71,14 +72,15 @@ template <> struct string_traits { std::vector result; if (text.size() > 2) { - size_t start = 1; - for (size_t i = start; i < text.size() - 2; i++) { - if (text[i] == ',') { - result.push_back(string_traits::from_string(text.substr(start, i - start), c)); + std::string_view inner = text.substr(1, text.size() - 2); + size_t start = 0; + for (size_t i = 0; i < inner.size(); i++) { + if (inner[i] == ',') { + result.push_back(string_traits::from_string(inner.substr(start, i - start), c)); start = i + 1; } } - result.push_back(string_traits::from_string(text.substr(start, text.size() - start - 1), c)); + result.push_back(string_traits::from_string(inner.substr(start), c)); } return pgvector::HalfVector(std::move(result)); } @@ -138,14 +140,15 @@ template <> struct string_traits { values.push_back(value); }; - size_t start = 1; - for (size_t i = start; i < n - 1; i++) { - if (text[i] == ',') { - cb(text.substr(start, i - start)); + std::string_view inner = text.substr(1, n - 1); + size_t start = 0; + for (size_t i = 0; i < inner.size(); i++) { + if (inner[i] == ',') { + cb(inner.substr(start, i - start)); start = i + 1; } } - cb(text.substr(start, n - start)); + cb(inner.substr(start)); } return pgvector::SparseVector(dimensions, indices, values); From 597e3f8e11a282e160dcdcef4e1a0cd21cfeb1b3 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 12:51:07 -0800 Subject: [PATCH 053/163] Updated to_buf and size_buffer to no longer depend on array code --- include/pgvector/pqxx.hpp | 58 +++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 87769c2..b0fcd84 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -47,16 +47,31 @@ template <> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::Vector& value, ctx c = {}) { - auto len = pqxx::into_buf(buf, static_cast>(value), c); - // replace array brackets - buf[0] = '['; - buf[len - 1] = ']'; - return {std::data(buf), len}; + auto values = static_cast>(value); + + size_t here = 0; + buf[here++] = '['; + + for (size_t i = 0; i < values.size(); i++) { + if (i != 0) { + buf[here++] = ','; + } + + here += pqxx::into_buf(buf.subspan(here), values[i], c); + } + + buf[here++] = ']'; + + return {std::data(buf), here}; } static size_t size_buffer(const pgvector::Vector& value) noexcept { - return string_traits>::size_buffer( - static_cast>(value)); + size_t size = 2; // [ and ] + for (const auto v : static_cast>(value)) { + size += 1; // , + size += string_traits::size_buffer(v); + } + return size; } }; @@ -86,16 +101,31 @@ template <> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { - auto len = pqxx::into_buf(buf, static_cast>(value), c); - // replace array brackets - buf[0] = '['; - buf[len - 1] = ']'; - return {std::data(buf), len}; + auto values = static_cast>(value); + + size_t here = 0; + buf[here++] = '['; + + for (size_t i = 0; i < values.size(); i++) { + if (i != 0) { + buf[here++] = ','; + } + + here += pqxx::into_buf(buf.subspan(here), values[i], c); + } + + buf[here++] = ']'; + + return {std::data(buf), here}; } static size_t size_buffer(const pgvector::HalfVector& value) noexcept { - return string_traits>::size_buffer( - static_cast>(value)); + size_t size = 2; // [ and ] + for (const auto v : static_cast>(value)) { + size += 1; // , + size += string_traits::size_buffer(v); + } + return size; } }; From 6c2a64a078b3cb43e48ec7a288a70e341be4f4a7 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 12:53:39 -0800 Subject: [PATCH 054/163] Added dimension checks [skip ci] --- include/pgvector/pqxx.hpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index b0fcd84..ca9e81e 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -49,6 +49,12 @@ template <> struct string_traits { static std::string_view to_buf(std::span buf, const pgvector::Vector& value, ctx c = {}) { auto values = static_cast>(value); + // important! size_buffer cannot throw an exception on overflow + // so perform this check before writing any data + if (values.size() > 16000) { + throw conversion_overrun{"vector cannot have more than 16000 dimensions"}; + } + size_t here = 0; buf[here++] = '['; @@ -103,6 +109,12 @@ template <> struct string_traits { static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { auto values = static_cast>(value); + // important! size_buffer cannot throw an exception on overflow + // so perform this check before writing any data + if (values.size() > 16000) { + throw conversion_overrun{"halfvec cannot have more than 16000 dimensions"}; + } + size_t here = 0; buf[here++] = '['; From 42d3d998d2d07945473ca83a0e901ef50c82e81e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 12:55:44 -0800 Subject: [PATCH 055/163] Improved consistency [skip ci] --- include/pgvector/pqxx.hpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index ca9e81e..0f96ef3 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -72,8 +72,13 @@ template <> struct string_traits { } static size_t size_buffer(const pgvector::Vector& value) noexcept { + auto values = static_cast>(value); + + // cannot throw an exception here on overflow + // so throw in into_buf + size_t size = 2; // [ and ] - for (const auto v : static_cast>(value)) { + for (const auto v : values) { size += 1; // , size += string_traits::size_buffer(v); } @@ -132,8 +137,13 @@ template <> struct string_traits { } static size_t size_buffer(const pgvector::HalfVector& value) noexcept { + auto values = static_cast>(value); + + // cannot throw an exception here on overflow + // so throw in into_buf + size_t size = 2; // [ and ] - for (const auto v : static_cast>(value)) { + for (const auto v : values) { size += 1; // , size += string_traits::size_buffer(v); } @@ -237,7 +247,7 @@ template <> struct string_traits { // cannot throw an exception here on overflow // so throw in into_buf - size_t size = 4; // {, }, /, and \0 + size_t size = 3; // {, }, and / size += string_traits::size_buffer(dimensions); for (size_t i = 0; i < nnz; i++) { size += 2; // : and , From 026960e7592f543a25a243be0c2d968d7f340119 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 12:57:44 -0800 Subject: [PATCH 056/163] Updated style [skip ci] --- include/pgvector/pqxx.hpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 0f96ef3..ae489a4 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -62,7 +62,6 @@ template <> struct string_traits { if (i != 0) { buf[here++] = ','; } - here += pqxx::into_buf(buf.subspan(here), values[i], c); } @@ -127,7 +126,6 @@ template <> struct string_traits { if (i != 0) { buf[here++] = ','; } - here += pqxx::into_buf(buf.subspan(here), values[i], c); } @@ -225,7 +223,6 @@ template <> struct string_traits { if (i != 0) { buf[here++] = ','; } - here += pqxx::into_buf(buf.subspan(here), indices[i] + 1, c); buf[here++] = ':'; here += pqxx::into_buf(buf.subspan(here), values[i], c); From fd096e2c28fc36d479ae28a0678bf9c66778323f Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 13:14:04 -0800 Subject: [PATCH 057/163] Added tests for dimensions --- test/pqxx_test.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index bcb6511..8aeb2d1 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -137,6 +137,13 @@ void test_precision(pqxx::connection &conn) { void test_vector_to_string() { assert(pqxx::to_string(pgvector::Vector({1, 2, 3})) == "[1,2,3]"); + + try { + auto unused = pqxx::to_string(pgvector::Vector(std::vector(16001))); + assert(false); + } catch (const pqxx::conversion_overrun& e) { + assert(std::string_view(e.what()) == "vector cannot have more than 16000 dimensions"); + } } void test_vector_from_string() { @@ -174,6 +181,13 @@ void test_vector_from_string() { void test_halfvec_to_string() { assert(pqxx::to_string(pgvector::HalfVector({1, 2, 3})) == "[1,2,3]"); + + try { + auto unused = pqxx::to_string(pgvector::HalfVector(std::vector(16001))); + assert(false); + } catch (const pqxx::conversion_overrun& e) { + assert(std::string_view(e.what()) == "halfvec cannot have more than 16000 dimensions"); + } } void test_halfvec_from_string() { @@ -293,6 +307,13 @@ void test_sparsevec_from_string() { } catch (const pqxx::conversion_error& e) { assert(std::string_view(e.what()) == "Could not convert 'a' to int: Invalid argument."); } + + try { + auto unused = pqxx::to_string(pgvector::SparseVector(std::vector(16001, 1))); + assert(false); + } catch (const pqxx::conversion_overrun& e) { + assert(std::string_view(e.what()) == "sparsevec cannot have more than 16000 dimensions"); + } } void test_pqxx() { From 9a6facbc64b8a5d15d46315317c26493c593d492 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 13:33:51 -0800 Subject: [PATCH 058/163] Added assert_exception function --- test/pqxx_test.cpp | 175 ++++++++++++++++----------------------------- 1 file changed, 60 insertions(+), 115 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 8aeb2d1..d3f6876 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -19,6 +19,20 @@ void before_each(pqxx::connection &conn) { tx.exec("TRUNCATE items"); } +template +void assert_exception(std::function code, std::optional message = std::nullopt) { + bool exception = false; + try { + code(); + } catch (const T& e) { + exception = true; + if (message) { + assert(std::string_view(e.what()) == *message); + } + } + assert(exception); +} + void test_vector(pqxx::connection &conn) { before_each(conn); @@ -89,12 +103,9 @@ void test_sparsevec_nnz(pqxx::connection &conn) { pqxx::nontransaction tx(conn); std::vector vec(16001, 1); auto embedding = pgvector::SparseVector(vec); - try { + assert_exception([&] { tx.exec("INSERT INTO items (sparse_embedding) VALUES ($1)", {embedding}); - assert(false); - } catch (const pqxx::conversion_overrun& e) { - assert(std::string_view(e.what()) == "sparsevec cannot have more than 16000 dimensions"); - } + }, "sparsevec cannot have more than 16000 dimensions"); } void test_stream(pqxx::connection &conn) { @@ -138,82 +149,55 @@ void test_precision(pqxx::connection &conn) { void test_vector_to_string() { assert(pqxx::to_string(pgvector::Vector({1, 2, 3})) == "[1,2,3]"); - try { + assert_exception([] { auto unused = pqxx::to_string(pgvector::Vector(std::vector(16001))); - assert(false); - } catch (const pqxx::conversion_overrun& e) { - assert(std::string_view(e.what()) == "vector cannot have more than 16000 dimensions"); - } + }, "vector cannot have more than 16000 dimensions"); } void test_vector_from_string() { assert(pqxx::from_string("[1,2,3]") == pgvector::Vector({1, 2, 3})); assert(pqxx::from_string("[]") == pgvector::Vector(std::vector{})); - try { + assert_exception([] { auto unused = pqxx::from_string(""); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Malformed vector literal"); - } + }, "Malformed vector literal"); - try { + assert_exception([] { auto unused = pqxx::from_string("["); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Malformed vector literal"); - } + }, "Malformed vector literal"); - try { + assert_exception([] { auto unused = pqxx::from_string("[hello]"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(true); - } + }); - try { + assert_exception([] { auto unused = pqxx::from_string("[4e38]"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(true); - } + }); } void test_halfvec_to_string() { assert(pqxx::to_string(pgvector::HalfVector({1, 2, 3})) == "[1,2,3]"); - try { + assert_exception([] { auto unused = pqxx::to_string(pgvector::HalfVector(std::vector(16001))); - assert(false); - } catch (const pqxx::conversion_overrun& e) { - assert(std::string_view(e.what()) == "halfvec cannot have more than 16000 dimensions"); - } + }, "halfvec cannot have more than 16000 dimensions"); } void test_halfvec_from_string() { assert(pqxx::from_string("[1,2,3]") == pgvector::HalfVector({1, 2, 3})); assert(pqxx::from_string("[]") == pgvector::HalfVector(std::vector{})); - try { + assert_exception([] { auto unused = pqxx::from_string(""); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Malformed halfvec literal"); - } + }, "Malformed halfvec literal"); - try { + assert_exception([] { auto unused = pqxx::from_string("["); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Malformed halfvec literal"); - } + }, "Malformed halfvec literal"); - try { + assert_exception([] { auto unused = pqxx::from_string("[hello]"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(true); - } + }); } void test_sparsevec_to_string() { @@ -224,96 +208,57 @@ void test_sparsevec_from_string() { assert(pqxx::from_string("{1:1,3:2,5:3}/6") == pgvector::SparseVector({1, 0, 2, 0, 3, 0})); assert(pqxx::from_string("{}/6") == pgvector::SparseVector({0, 0, 0, 0, 0, 0})); - try { + assert_exception([] { auto unused = pqxx::from_string(""); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Malformed sparsevec literal"); - } + }, "Malformed sparsevec literal"); - try { + assert_exception([] { auto unused = pqxx::from_string("{"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Malformed sparsevec literal"); - } + }, "Malformed sparsevec literal"); - try { + assert_exception([] { auto unused = pqxx::from_string("{ }/"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert '' to int: Invalid argument."); - } + }, "Could not convert '' to int: Invalid argument."); - try { + assert_exception([] { auto unused = pqxx::from_string("{}/-1"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Dimensions cannot be negative"); - } + }, "Dimensions cannot be negative"); - try { + assert_exception([] { auto unused = pqxx::from_string("{:}/1"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert '' to int: Invalid argument."); - } + }, "Could not convert '' to int: Invalid argument."); - try { + assert_exception([] { auto unused = pqxx::from_string("{,}/1"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Malformed sparsevec literal"); - } + }, "Malformed sparsevec literal"); - try { + assert_exception([] { auto unused = pqxx::from_string("{0:1}/1"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Index out of bounds"); - } + }, "Index out of bounds"); - try { + assert_exception([] { auto unused = pqxx::from_string("{-2147483648:1}/1"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Index out of bounds"); - } + }, "Index out of bounds"); - try { + assert_exception([] { auto unused = pqxx::from_string("{1:4e38}/1"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert string to numeric value: '4e38'." || std::string_view(e.what()) == "Could not convert '4e38' to float" || std::string_view(e.what()) == "Could not convert '4e38' to float: Value out of range."); - } + }); - try { + assert_exception([] { auto unused = pqxx::from_string("{a:1}/1"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert 'a' to int: Invalid argument."); - } + }, "Could not convert 'a' to int: Invalid argument."); - try { + assert_exception([] { auto unused = pqxx::from_string("{1:a}/1"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert string to numeric value: 'a'." || std::string_view(e.what()) == "Could not convert 'a' to float" || std::string_view(e.what()) == "Could not convert 'a' to float: Invalid argument."); - } + }); - try { + assert_exception([] { auto unused = pqxx::from_string("{}/a"); - assert(false); - } catch (const pqxx::conversion_error& e) { - assert(std::string_view(e.what()) == "Could not convert 'a' to int: Invalid argument."); - } + }, "Could not convert 'a' to int: Invalid argument."); - try { + assert_exception([] { auto unused = pqxx::to_string(pgvector::SparseVector(std::vector(16001, 1))); - assert(false); - } catch (const pqxx::conversion_overrun& e) { - assert(std::string_view(e.what()) == "sparsevec cannot have more than 16000 dimensions"); - } + }, "sparsevec cannot have more than 16000 dimensions"); } void test_pqxx() { From 8529df1e9bd50b9a0d4c408f2e6318be0124135b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 13:50:02 -0800 Subject: [PATCH 059/163] Removed unnecessary copying --- include/pgvector/pqxx.hpp | 2 +- include/pgvector/sparsevec.hpp | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index ae489a4..0af6543 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -201,7 +201,7 @@ template <> struct string_traits { cb(inner.substr(start)); } - return pgvector::SparseVector(dimensions, indices, values); + return pgvector::SparseVector(dimensions, std::move(indices), std::move(values)); } static std::string_view to_buf(std::span buf, const pgvector::SparseVector& value, ctx c = {}) { diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index a5e6475..b10e5ba 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -28,6 +28,16 @@ class SparseVector { values_ = values; } + /// @private + SparseVector(int dimensions, std::vector&& indices, std::vector&& values) { + if (values.size() != indices.size()) { + throw std::invalid_argument("indices and values must be the same length"); + } + dimensions_ = dimensions; + indices_ = std::move(indices); + values_ = std::move(values); + } + /// Creates a sparse vector from a dense vector. explicit SparseVector(const std::vector& value) { dimensions_ = value.size(); From 7dcba7552f31199f8ec39b8478630c670a728090 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 14:04:56 -0800 Subject: [PATCH 060/163] Improved naming [skip ci] --- include/pgvector/pqxx.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 0af6543..c6a2e91 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -173,7 +173,7 @@ template <> struct string_traits { std::vector values; if (n > 1) { - auto cb = [&](std::string_view substr) { + auto add_element = [&](std::string_view substr) { size_t ne = substr.find(":"); if (ne == std::string::npos) { throw conversion_error("Malformed sparsevec literal"); @@ -194,11 +194,11 @@ template <> struct string_traits { size_t start = 0; for (size_t i = 0; i < inner.size(); i++) { if (inner[i] == ',') { - cb(inner.substr(start, i - start)); + add_element(inner.substr(start, i - start)); start = i + 1; } } - cb(inner.substr(start)); + add_element(inner.substr(start)); } return pgvector::SparseVector(dimensions, std::move(indices), std::move(values)); From 37d8db252b5d713f67d37185671776372c74c1e5 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 15:23:38 -0800 Subject: [PATCH 061/163] Removed unnecessary copying --- include/pgvector/pqxx.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index c6a2e91..86c41b1 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -206,8 +206,8 @@ template <> struct string_traits { static std::string_view to_buf(std::span buf, const pgvector::SparseVector& value, ctx c = {}) { int dimensions = value.dimensions(); - auto indices = value.indices(); - auto values = value.values(); + auto& indices = value.indices(); + auto& values = value.values(); size_t nnz = indices.size(); // important! size_buffer cannot throw an exception on overflow @@ -237,8 +237,8 @@ template <> struct string_traits { static size_t size_buffer(const pgvector::SparseVector& value) noexcept { int dimensions = value.dimensions(); - auto indices = value.indices(); - auto values = value.values(); + auto& indices = value.indices(); + auto& values = value.values(); size_t nnz = indices.size(); // cannot throw an exception here on overflow From dab46256cffdc826dcb81cc8692de2f4fe4c15a2 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 15:29:43 -0800 Subject: [PATCH 062/163] Removed unnecessary copying --- include/pgvector/halfvec.hpp | 5 +++++ include/pgvector/pqxx.hpp | 8 ++++---- include/pgvector/vector.hpp | 5 +++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 63fc130..dee7796 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -41,6 +41,11 @@ class HalfVector { return value_; } + /// Returns the half vector as a `std::span`. + operator const std::span() const { + return value_; + } + friend bool operator==(const HalfVector& lhs, const HalfVector& rhs) { return lhs.value_ == rhs.value_; } diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 86c41b1..b62835d 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -47,7 +47,7 @@ template <> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::Vector& value, ctx c = {}) { - auto values = static_cast>(value); + auto values = static_cast>(value); // important! size_buffer cannot throw an exception on overflow // so perform this check before writing any data @@ -71,7 +71,7 @@ template <> struct string_traits { } static size_t size_buffer(const pgvector::Vector& value) noexcept { - auto values = static_cast>(value); + auto values = static_cast>(value); // cannot throw an exception here on overflow // so throw in into_buf @@ -111,7 +111,7 @@ template <> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { - auto values = static_cast>(value); + auto values = static_cast>(value); // important! size_buffer cannot throw an exception on overflow // so perform this check before writing any data @@ -135,7 +135,7 @@ template <> struct string_traits { } static size_t size_buffer(const pgvector::HalfVector& value) noexcept { - auto values = static_cast>(value); + auto values = static_cast>(value); // cannot throw an exception here on overflow // so throw in into_buf diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index b325ead..ab5bb7a 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -41,6 +41,11 @@ class Vector { return value_; } + /// Returns the vector as a `std::span`. + operator const std::span() const { + return value_; + } + friend bool operator==(const Vector& lhs, const Vector& rhs) { return lhs.value_ == rhs.value_; } From 9989174b496ad0182dc6366afb678bc6a2d200be Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 15:33:53 -0800 Subject: [PATCH 063/163] Removed unneeded includes [skip ci] --- include/pgvector/pqxx.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index b62835d..dcb3810 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -7,8 +7,6 @@ #pragma once #include -#include -#include #include #include From 9dacbdfae1f187ebca3ec2a0b23ceb283454f3ba Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 15:43:40 -0800 Subject: [PATCH 064/163] Simplified code [skip ci] --- include/pgvector/pqxx.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index dcb3810..f3eb4f3 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -45,7 +45,7 @@ template <> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::Vector& value, ctx c = {}) { - auto values = static_cast>(value); + std::span values{value}; // important! size_buffer cannot throw an exception on overflow // so perform this check before writing any data @@ -69,7 +69,7 @@ template <> struct string_traits { } static size_t size_buffer(const pgvector::Vector& value) noexcept { - auto values = static_cast>(value); + std::span values{value}; // cannot throw an exception here on overflow // so throw in into_buf @@ -109,7 +109,7 @@ template <> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { - auto values = static_cast>(value); + std::span values{value}; // important! size_buffer cannot throw an exception on overflow // so perform this check before writing any data @@ -133,7 +133,7 @@ template <> struct string_traits { } static size_t size_buffer(const pgvector::HalfVector& value) noexcept { - auto values = static_cast>(value); + std::span values{value}; // cannot throw an exception here on overflow // so throw in into_buf From ccb4ab9cbe57681195a58532ff83f80f2726b93f Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 15:47:01 -0800 Subject: [PATCH 065/163] Improved naming [skip ci] --- include/pgvector/pqxx.hpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index f3eb4f3..314b518 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -29,19 +29,19 @@ template <> struct string_traits { throw conversion_error("Malformed vector literal"); } - std::vector result; + std::vector values; if (text.size() > 2) { std::string_view inner = text.substr(1, text.size() - 2); size_t start = 0; for (size_t i = 0; i < inner.size(); i++) { if (inner[i] == ',') { - result.push_back(string_traits::from_string(inner.substr(start, i - start), c)); + values.push_back(string_traits::from_string(inner.substr(start, i - start), c)); start = i + 1; } } - result.push_back(string_traits::from_string(inner.substr(start), c)); + values.push_back(string_traits::from_string(inner.substr(start), c)); } - return pgvector::Vector(std::move(result)); + return pgvector::Vector(std::move(values)); } static std::string_view to_buf(std::span buf, const pgvector::Vector& value, ctx c = {}) { @@ -93,19 +93,19 @@ template <> struct string_traits { throw conversion_error("Malformed halfvec literal"); } - std::vector result; + std::vector values; if (text.size() > 2) { std::string_view inner = text.substr(1, text.size() - 2); size_t start = 0; for (size_t i = 0; i < inner.size(); i++) { if (inner[i] == ',') { - result.push_back(string_traits::from_string(inner.substr(start, i - start), c)); + values.push_back(string_traits::from_string(inner.substr(start, i - start), c)); start = i + 1; } } - result.push_back(string_traits::from_string(inner.substr(start), c)); + values.push_back(string_traits::from_string(inner.substr(start), c)); } - return pgvector::HalfVector(std::move(result)); + return pgvector::HalfVector(std::move(values)); } static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { From 7106718960888739ecfd31502c8f6f8dce05c06c Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 15:59:41 -0800 Subject: [PATCH 066/163] Improved code [skip ci] --- include/pgvector/halfvec.hpp | 4 +--- include/pgvector/vector.hpp | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index dee7796..f0a41ea 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -17,9 +17,7 @@ namespace pgvector { class HalfVector { public: /// Creates a half vector from a `std::vector`. - explicit HalfVector(const std::vector& value) { - value_ = value; - } + explicit HalfVector(const std::vector& value) : value_{value} {} /// Creates a half vector from a `std::vector`. explicit HalfVector(std::vector&& value) { diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index ab5bb7a..a411a90 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -17,9 +17,7 @@ namespace pgvector { class Vector { public: /// Creates a vector from a `std::vector`. - explicit Vector(const std::vector& value) { - value_ = value; - } + explicit Vector(const std::vector& value) : value_{value} {} /// Creates a vector from a `std::vector`. explicit Vector(std::vector&& value) { From 171948c4524cfee24d1d0026a9d24dbad527a4af Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 16:05:03 -0800 Subject: [PATCH 067/163] Updated license year [skip ci] --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index b612d6d..17e5210 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021-2025 Andrew Kane +Copyright (c) 2021-2026 Andrew Kane Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 8344aefc980492221a812358d1c1cb2cb8af3ccb Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 16:58:31 -0800 Subject: [PATCH 068/163] Improved code [skip ci] --- include/pgvector/halfvec.hpp | 8 ++------ include/pgvector/vector.hpp | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index f0a41ea..3712dc6 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -20,14 +20,10 @@ class HalfVector { explicit HalfVector(const std::vector& value) : value_{value} {} /// Creates a half vector from a `std::vector`. - explicit HalfVector(std::vector&& value) { - value_ = std::move(value); - } + explicit HalfVector(std::vector&& value) : value_{std::move(value)} {} /// Creates a half vector from a span. - explicit HalfVector(std::span value) { - value_ = std::vector(value.begin(), value.end()); - } + explicit HalfVector(std::span value) : value_{std::vector(value.begin(), value.end())} {} /// Returns the number of dimensions. size_t dimensions() const { diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index a411a90..d8adbd8 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -20,14 +20,10 @@ class Vector { explicit Vector(const std::vector& value) : value_{value} {} /// Creates a vector from a `std::vector`. - explicit Vector(std::vector&& value) { - value_ = std::move(value); - } + explicit Vector(std::vector&& value) : value_{std::move(value)} {} /// Creates a vector from a span. - explicit Vector(std::span value) { - value_ = std::vector(value.begin(), value.end()); - } + explicit Vector(std::span value) : value_{std::vector(value.begin(), value.end())} {} /// Returns the number of dimensions. size_t dimensions() const { From 57814d29a346400bdaa0d9fd54d4721d0c45cc83 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 17:02:39 -0800 Subject: [PATCH 069/163] Removed private constructor [skip ci] --- include/pgvector/sparsevec.hpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index b10e5ba..121f088 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -18,16 +18,6 @@ namespace pgvector { /// A sparse vector. class SparseVector { public: - /// @private - SparseVector(int dimensions, const std::vector& indices, const std::vector& values) { - if (values.size() != indices.size()) { - throw std::invalid_argument("indices and values must be the same length"); - } - dimensions_ = dimensions; - indices_ = indices; - values_ = values; - } - /// @private SparseVector(int dimensions, std::vector&& indices, std::vector&& values) { if (values.size() != indices.size()) { From b1bba2dda556442d2e0e098d9585cfcc5738489f Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 1 Mar 2026 18:03:26 -0800 Subject: [PATCH 070/163] Updated style [skip ci] --- include/pgvector/sparsevec.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 121f088..d656636 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -59,7 +59,7 @@ class SparseVector { } dimensions_ = dimensions; - for (auto [i, v] : map) { + for (const auto [i, v] : map) { if (i < 0 || i >= dimensions) { throw std::invalid_argument("sparsevec index out of bounds"); } @@ -71,7 +71,7 @@ class SparseVector { std::sort(indices_.begin(), indices_.end()); values_.reserve(indices_.size()); - for (auto i : indices_) { + for (const auto i : indices_) { values_.push_back(map.at(i)); } } From a0e89e3c0d94684e85d97eaf5a92e08710237207 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 21:14:03 -0800 Subject: [PATCH 071/163] Added more tests --- test/pqxx_test.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index d3f6876..f379b4a 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -173,6 +173,14 @@ void test_vector_from_string() { assert_exception([] { auto unused = pqxx::from_string("[4e38]"); }); + + assert_exception([] { + auto unused = pqxx::from_string("[,]"); + }); + + assert_exception([] { + auto unused = pqxx::from_string("[1,]"); + }); } void test_halfvec_to_string() { @@ -198,6 +206,18 @@ void test_halfvec_from_string() { assert_exception([] { auto unused = pqxx::from_string("[hello]"); }); + + assert_exception([] { + auto unused = pqxx::from_string("[4e38]"); + }); + + assert_exception([] { + auto unused = pqxx::from_string("[,]"); + }); + + assert_exception([] { + auto unused = pqxx::from_string("[1,]"); + }); } void test_sparsevec_to_string() { From 23dee7fbb63d346c39ba722a56b7e6937f020acf Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 21:17:11 -0800 Subject: [PATCH 072/163] Improved tests --- test/pqxx_test.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index f379b4a..ce4fe59 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -168,7 +168,7 @@ void test_vector_from_string() { assert_exception([] { auto unused = pqxx::from_string("[hello]"); - }); + }, "Could not convert 'hello' to float: Invalid argument."); assert_exception([] { auto unused = pqxx::from_string("[4e38]"); @@ -176,11 +176,11 @@ void test_vector_from_string() { assert_exception([] { auto unused = pqxx::from_string("[,]"); - }); + }, "Could not convert '' to float: Invalid argument."); assert_exception([] { auto unused = pqxx::from_string("[1,]"); - }); + }, "Could not convert '' to float: Invalid argument."); } void test_halfvec_to_string() { @@ -205,7 +205,7 @@ void test_halfvec_from_string() { assert_exception([] { auto unused = pqxx::from_string("[hello]"); - }); + }, "Could not convert 'hello' to float: Invalid argument."); assert_exception([] { auto unused = pqxx::from_string("[4e38]"); @@ -213,11 +213,11 @@ void test_halfvec_from_string() { assert_exception([] { auto unused = pqxx::from_string("[,]"); - }); + }, "Could not convert '' to float: Invalid argument."); assert_exception([] { auto unused = pqxx::from_string("[1,]"); - }); + }, "Could not convert '' to float: Invalid argument."); } void test_sparsevec_to_string() { From c4a29c78416ade689f81877d3ab0c7a6b42defee Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 21:21:21 -0800 Subject: [PATCH 073/163] Improved tests --- test/pqxx_test.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index ce4fe59..f4ab00a 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -172,7 +172,7 @@ void test_vector_from_string() { assert_exception([] { auto unused = pqxx::from_string("[4e38]"); - }); + }, "Could not convert '4e38' to float: Value out of range."); assert_exception([] { auto unused = pqxx::from_string("[,]"); @@ -209,7 +209,7 @@ void test_halfvec_from_string() { assert_exception([] { auto unused = pqxx::from_string("[4e38]"); - }); + }, "Could not convert '4e38' to float: Value out of range."); assert_exception([] { auto unused = pqxx::from_string("[,]"); @@ -262,7 +262,7 @@ void test_sparsevec_from_string() { assert_exception([] { auto unused = pqxx::from_string("{1:4e38}/1"); - }); + }, "Could not convert '4e38' to float: Value out of range."); assert_exception([] { auto unused = pqxx::from_string("{a:1}/1"); @@ -270,7 +270,7 @@ void test_sparsevec_from_string() { assert_exception([] { auto unused = pqxx::from_string("{1:a}/1"); - }); + }, "Could not convert 'a' to float: Invalid argument."); assert_exception([] { auto unused = pqxx::from_string("{}/a"); From d66ba0c5d3afb9360efb75bc0d64ace6b82ec74e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 21:25:43 -0800 Subject: [PATCH 074/163] Improved valgrind setup [skip ci] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6dbd1ff..4557a0c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: - run: | sudo apt-get install valgrind - valgrind --leak-check=yes build/test + valgrind --leak-check=yes --error-exitcode=1 build/test # test install - run: rm -r build From a6dda87df289d66e8e659112487cbbfab694acfb Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 21:53:51 -0800 Subject: [PATCH 075/163] Added Mac to CI --- .github/workflows/build.yml | 9 +++++++-- test/pqxx_test.cpp | 28 ++++++++++++++++++---------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4557a0c..12901dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,11 @@ name: build on: [push, pull_request] jobs: build: - runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - uses: ankane/setup-postgres@v1 @@ -24,7 +28,8 @@ jobs: - run: cmake --build build - run: build/test - - run: | + - if: ${{ runner.os == 'Linux' }} + run: | sudo apt-get install valgrind valgrind --leak-check=yes --error-exitcode=1 build/test diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index f4ab00a..611c8a5 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -33,6 +33,14 @@ void assert_exception(std::function code, std::optional float_error(std::string_view message) { +#ifdef __linux__ + return message; +#else + return std::nullopt; +#endif +} + void test_vector(pqxx::connection &conn) { before_each(conn); @@ -168,19 +176,19 @@ void test_vector_from_string() { assert_exception([] { auto unused = pqxx::from_string("[hello]"); - }, "Could not convert 'hello' to float: Invalid argument."); + }, float_error("Could not convert 'hello' to float: Invalid argument.")); assert_exception([] { auto unused = pqxx::from_string("[4e38]"); - }, "Could not convert '4e38' to float: Value out of range."); + }, float_error("Could not convert '4e38' to float: Value out of range.")); assert_exception([] { auto unused = pqxx::from_string("[,]"); - }, "Could not convert '' to float: Invalid argument."); + }, float_error("Could not convert '' to float: Invalid argument.")); assert_exception([] { auto unused = pqxx::from_string("[1,]"); - }, "Could not convert '' to float: Invalid argument."); + }, float_error("Could not convert '' to float: Invalid argument.")); } void test_halfvec_to_string() { @@ -205,19 +213,19 @@ void test_halfvec_from_string() { assert_exception([] { auto unused = pqxx::from_string("[hello]"); - }, "Could not convert 'hello' to float: Invalid argument."); + }, float_error("Could not convert 'hello' to float: Invalid argument.")); assert_exception([] { auto unused = pqxx::from_string("[4e38]"); - }, "Could not convert '4e38' to float: Value out of range."); + }, float_error("Could not convert '4e38' to float: Value out of range.")); assert_exception([] { auto unused = pqxx::from_string("[,]"); - }, "Could not convert '' to float: Invalid argument."); + }, float_error("Could not convert '' to float: Invalid argument.")); assert_exception([] { auto unused = pqxx::from_string("[1,]"); - }, "Could not convert '' to float: Invalid argument."); + }, float_error("Could not convert '' to float: Invalid argument.")); } void test_sparsevec_to_string() { @@ -262,7 +270,7 @@ void test_sparsevec_from_string() { assert_exception([] { auto unused = pqxx::from_string("{1:4e38}/1"); - }, "Could not convert '4e38' to float: Value out of range."); + }, float_error("Could not convert '4e38' to float: Value out of range.")); assert_exception([] { auto unused = pqxx::from_string("{a:1}/1"); @@ -270,7 +278,7 @@ void test_sparsevec_from_string() { assert_exception([] { auto unused = pqxx::from_string("{1:a}/1"); - }, "Could not convert 'a' to float: Invalid argument."); + }, float_error("Could not convert 'a' to float: Invalid argument.")); assert_exception([] { auto unused = pqxx::from_string("{}/a"); From 64e05ae94dee26423d43e1351cd0343c56c3108b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 22:38:25 -0800 Subject: [PATCH 076/163] Added more tests --- test/pqxx_test.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 611c8a5..d3a5bb2 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -156,6 +156,7 @@ void test_precision(pqxx::connection &conn) { void test_vector_to_string() { assert(pqxx::to_string(pgvector::Vector({1, 2, 3})) == "[1,2,3]"); + assert(pqxx::to_string(pgvector::Vector({1.234567890123})) == "[1.2345679]"); assert_exception([] { auto unused = pqxx::to_string(pgvector::Vector(std::vector(16001))); @@ -193,6 +194,7 @@ void test_vector_from_string() { void test_halfvec_to_string() { assert(pqxx::to_string(pgvector::HalfVector({1, 2, 3})) == "[1,2,3]"); + assert(pqxx::to_string(pgvector::HalfVector({1.234567890123})) == "[1.2345679]"); assert_exception([] { auto unused = pqxx::to_string(pgvector::HalfVector(std::vector(16001))); @@ -230,6 +232,8 @@ void test_halfvec_from_string() { void test_sparsevec_to_string() { assert(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})) == "{1:1,3:2,5:3}/6"); + std::unordered_map map = {{999999999, 1.234567890123}}; + assert(pqxx::to_string(pgvector::SparseVector(map, 1000000000)) == "{1000000000:1.2345679}/1000000000"); } void test_sparsevec_from_string() { From c8a2312e9b893830819b8de2bfe52008e3f428ac Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 22:42:41 -0800 Subject: [PATCH 077/163] Improved tests [skip ci] --- test/pqxx_test.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index d3a5bb2..e0138d6 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -156,7 +156,7 @@ void test_precision(pqxx::connection &conn) { void test_vector_to_string() { assert(pqxx::to_string(pgvector::Vector({1, 2, 3})) == "[1,2,3]"); - assert(pqxx::to_string(pgvector::Vector({1.234567890123})) == "[1.2345679]"); + assert(pqxx::to_string(pgvector::Vector({-1.234567890123})) == "[-1.2345679]"); assert_exception([] { auto unused = pqxx::to_string(pgvector::Vector(std::vector(16001))); @@ -194,7 +194,7 @@ void test_vector_from_string() { void test_halfvec_to_string() { assert(pqxx::to_string(pgvector::HalfVector({1, 2, 3})) == "[1,2,3]"); - assert(pqxx::to_string(pgvector::HalfVector({1.234567890123})) == "[1.2345679]"); + assert(pqxx::to_string(pgvector::HalfVector({-1.234567890123})) == "[-1.2345679]"); assert_exception([] { auto unused = pqxx::to_string(pgvector::HalfVector(std::vector(16001))); @@ -232,8 +232,8 @@ void test_halfvec_from_string() { void test_sparsevec_to_string() { assert(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})) == "{1:1,3:2,5:3}/6"); - std::unordered_map map = {{999999999, 1.234567890123}}; - assert(pqxx::to_string(pgvector::SparseVector(map, 1000000000)) == "{1000000000:1.2345679}/1000000000"); + std::unordered_map map = {{999999999, -1.234567890123}}; + assert(pqxx::to_string(pgvector::SparseVector(map, 1000000000)) == "{1000000000:-1.2345679}/1000000000"); } void test_sparsevec_from_string() { From 93e6abbb959f2defb74a85c7ed05ab029edb3a41 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 22:44:09 -0800 Subject: [PATCH 078/163] Fixed CI --- test/pqxx_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index e0138d6..66cfa6e 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -33,7 +33,7 @@ void assert_exception(std::function code, std::optional float_error(std::string_view message) { +std::optional float_error([[maybe_unused]] std::string_view message) { #ifdef __linux__ return message; #else From 3b36e4797f1a2f64b91e6957c4125c6063087484 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 22:50:33 -0800 Subject: [PATCH 079/163] Improved includes --- test/pqxx_test.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 66cfa6e..5d288d1 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -1,6 +1,8 @@ #include +#include #include #include +#include #include #include From a2fae30b406f6630201a0b951d57f4d0a77a2166 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 22:57:24 -0800 Subject: [PATCH 080/163] Added more tests for SparseVector --- test/helper.hpp | 20 ++++++++++++++++++++ test/pqxx_test.cpp | 18 ++---------------- test/sparsevec_test.cpp | 10 ++++++++++ 3 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 test/helper.hpp diff --git a/test/helper.hpp b/test/helper.hpp new file mode 100644 index 0000000..404f87d --- /dev/null +++ b/test/helper.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include + +template +void assert_exception(std::function code, std::optional message = std::nullopt) { + bool exception = false; + try { + code(); + } catch (const T& e) { + exception = true; + if (message) { + assert(std::string_view(e.what()) == *message); + } + } + assert(exception); +} diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 5d288d1..4514fe6 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -1,14 +1,14 @@ #include -#include #include #include #include #include #include - #include +#include "helper.hpp" + void setup(pqxx::connection &conn) { pqxx::nontransaction tx(conn); tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); @@ -21,20 +21,6 @@ void before_each(pqxx::connection &conn) { tx.exec("TRUNCATE items"); } -template -void assert_exception(std::function code, std::optional message = std::nullopt) { - bool exception = false; - try { - code(); - } catch (const T& e) { - exception = true; - if (message) { - assert(std::string_view(e.what()) == *message); - } - } - assert(exception); -} - std::optional float_error([[maybe_unused]] std::string_view message) { #ifdef __linux__ return message; diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index 9704d1c..222ab04 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -4,6 +4,8 @@ #include +#include "helper.hpp" + using pgvector::SparseVector; static void test_constructor_vector() { @@ -24,6 +26,14 @@ static void test_constructor_map() { assert(vec.dimensions() == 6); assert(vec.indices() == (std::vector{0, 2, 4})); assert(vec.values() == (std::vector{1, 2, 3})); + + assert_exception([&]{ + auto unused = SparseVector(map, 0); + }, "sparsevec must have at least 1 dimension"); + + assert_exception([&]{ + auto unused = SparseVector(map, 4); + }, "sparsevec index out of bounds"); } void test_sparsevec() { From 0a0bc40e5ad934923a124270418e2945736db6b1 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 23:42:05 -0800 Subject: [PATCH 081/163] Improved assertions --- test/halfvec_test.cpp | 7 ++-- test/helper.hpp | 21 ++++++++++-- test/pqxx_test.cpp | 75 ++++++++++++++++++++--------------------- test/sparsevec_test.cpp | 15 ++++----- test/vector_test.cpp | 7 ++-- 5 files changed, 69 insertions(+), 56 deletions(-) diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index 3c54881..d3bffd5 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -1,18 +1,19 @@ -#include #include #include +#include "helper.hpp" + using pgvector::HalfVector; static void test_constructor_vector() { auto vec = HalfVector({1, 2, 3}); - assert(vec.dimensions() == 3); + assert_equal(vec.dimensions(), static_cast(3)); } static void test_constructor_span() { auto vec = HalfVector(std::span({1, 2, 3})); - assert(vec.dimensions() == 3); + assert_equal(vec.dimensions(), static_cast(3)); } void test_halfvec() { diff --git a/test/helper.hpp b/test/helper.hpp index 404f87d..bcd4a6c 100644 --- a/test/helper.hpp +++ b/test/helper.hpp @@ -1,10 +1,25 @@ #pragma once -#include #include #include +#include +#include #include +template +void assert_equal(const T& left, const T& right, const std::source_location& loc = std::source_location::current()) { + if (left != right) { + std::ostringstream message; + message << left << " != " << right; + message << " in " << loc.function_name() << " " << loc.file_name() << ":" << loc.line(); + throw std::runtime_error(message.str()); + } +} + +inline void assert_true(bool condition, const std::source_location& loc = std::source_location::current()) { + assert_equal(condition, true, loc); +} + template void assert_exception(std::function code, std::optional message = std::nullopt) { bool exception = false; @@ -13,8 +28,8 @@ void assert_exception(std::function code, std::optional #include #include #include @@ -34,16 +33,15 @@ void test_vector(pqxx::connection &conn) { pqxx::nontransaction tx(conn); auto embedding = pgvector::Vector({1, 2, 3}); - assert(embedding.dimensions() == 3); float arr[] = {4, 5, 6}; auto embedding2 = pgvector::Vector(std::span{arr, 3}); tx.exec("INSERT INTO items (embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY embedding <-> $1", {embedding2}); - assert(res.size() == 3); - assert(res[0][0].as() == embedding2); - assert(res[1][0].as() == embedding); - assert(!res[2][0].as>().has_value()); + assert_equal(res.size(), 3); + assert_equal(res[0][0].as(), embedding2); + assert_equal(res[1][0].as(), embedding); + assert_true(!res[2][0].as>().has_value()); } void test_halfvec(pqxx::connection &conn) { @@ -51,31 +49,30 @@ void test_halfvec(pqxx::connection &conn) { pqxx::nontransaction tx(conn); auto embedding = pgvector::HalfVector({1, 2, 3}); - assert(embedding.dimensions() == 3); float arr[] = {4, 5, 6}; auto embedding2 = pgvector::HalfVector(std::span{arr, 3}); tx.exec("INSERT INTO items (half_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT half_embedding FROM items ORDER BY half_embedding <-> $1", {embedding2}); - assert(res.size() == 3); - assert(res[0][0].as() == embedding2); - assert(res[1][0].as() == embedding); - assert(!res[2][0].as>().has_value()); + assert_equal(res.size(), 3); + assert_equal(res[0][0].as(), embedding2); + assert_equal(res[1][0].as(), embedding); + assert_true(!res[2][0].as>().has_value()); } void test_bit(pqxx::connection &conn) { before_each(conn); pqxx::nontransaction tx(conn); - auto embedding = "101"; - auto embedding2 = "111"; + std::string embedding = "101"; + std::string embedding2 = "111"; tx.exec("INSERT INTO items (binary_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT binary_embedding FROM items ORDER BY binary_embedding <~> $1", pqxx::params{embedding2}); - assert(res.size() == 3); - assert(res[0][0].as() == embedding2); - assert(res[1][0].as() == embedding); - assert(!res[2][0].as>().has_value()); + assert_equal(res.size(), 3); + assert_equal(res[0][0].as(), embedding2); + assert_equal(res[1][0].as(), embedding); + assert_true(!res[2][0].as>().has_value()); } void test_sparsevec(pqxx::connection &conn) { @@ -87,10 +84,10 @@ void test_sparsevec(pqxx::connection &conn) { tx.exec("INSERT INTO items (sparse_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT sparse_embedding FROM items ORDER BY sparse_embedding <-> $1", {embedding2}); - assert(res.size() == 3); - assert(res[0][0].as() == embedding2); - assert(res[1][0].as() == embedding); - assert(!res[2][0].as>().has_value()); + assert_equal(res.size(), 3); + assert_equal(res[0][0].as(), embedding2); + assert_equal(res[1][0].as(), embedding); + assert_true(!res[2][0].as>().has_value()); } void test_sparsevec_nnz(pqxx::connection &conn) { @@ -111,11 +108,11 @@ void test_stream(pqxx::connection &conn) { auto embedding = pgvector::Vector({1, 2, 3}); tx.exec("INSERT INTO items (embedding) VALUES ($1)", {embedding}); int count = 0; - for (auto [id, embedding] : tx.stream("SELECT id, embedding FROM items WHERE embedding IS NOT NULL")) { - assert(embedding.dimensions() == 3); + for (auto [id, embedding2] : tx.stream("SELECT id, embedding FROM items WHERE embedding IS NOT NULL")) { + assert_equal(embedding2, embedding); count++; } - assert(count == 1); + assert_equal(count, 1); } void test_stream_to(pqxx::connection &conn) { @@ -127,8 +124,8 @@ void test_stream_to(pqxx::connection &conn) { stream.write_values(pgvector::Vector({4, 5, 6})); stream.complete(); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY id"); - assert(res[0][0].as() == "[1,2,3]"); - assert(res[1][0].as() == "[4,5,6]"); + assert_true(res[0][0].as() == "[1,2,3]"); + assert_true(res[1][0].as() == "[4,5,6]"); } void test_precision(pqxx::connection &conn) { @@ -139,12 +136,12 @@ void test_precision(pqxx::connection &conn) { tx.exec("INSERT INTO items (embedding) VALUES ($1)", {embedding}); tx.exec("SET extra_float_digits = 3"); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY id DESC LIMIT 1"); - assert(res[0][0].as() == embedding); + assert_equal(res[0][0].as(), embedding); } void test_vector_to_string() { - assert(pqxx::to_string(pgvector::Vector({1, 2, 3})) == "[1,2,3]"); - assert(pqxx::to_string(pgvector::Vector({-1.234567890123})) == "[-1.2345679]"); + assert_true(pqxx::to_string(pgvector::Vector({1, 2, 3})) == "[1,2,3]"); + assert_true(pqxx::to_string(pgvector::Vector({-1.234567890123})) == "[-1.2345679]"); assert_exception([] { auto unused = pqxx::to_string(pgvector::Vector(std::vector(16001))); @@ -152,8 +149,8 @@ void test_vector_to_string() { } void test_vector_from_string() { - assert(pqxx::from_string("[1,2,3]") == pgvector::Vector({1, 2, 3})); - assert(pqxx::from_string("[]") == pgvector::Vector(std::vector{})); + assert_equal(pqxx::from_string("[1,2,3]"), pgvector::Vector({1, 2, 3})); + assert_equal(pqxx::from_string("[]"), pgvector::Vector(std::vector{})); assert_exception([] { auto unused = pqxx::from_string(""); @@ -181,8 +178,8 @@ void test_vector_from_string() { } void test_halfvec_to_string() { - assert(pqxx::to_string(pgvector::HalfVector({1, 2, 3})) == "[1,2,3]"); - assert(pqxx::to_string(pgvector::HalfVector({-1.234567890123})) == "[-1.2345679]"); + assert_true(pqxx::to_string(pgvector::HalfVector({1, 2, 3})) == "[1,2,3]"); + assert_true(pqxx::to_string(pgvector::HalfVector({-1.234567890123})) == "[-1.2345679]"); assert_exception([] { auto unused = pqxx::to_string(pgvector::HalfVector(std::vector(16001))); @@ -190,8 +187,8 @@ void test_halfvec_to_string() { } void test_halfvec_from_string() { - assert(pqxx::from_string("[1,2,3]") == pgvector::HalfVector({1, 2, 3})); - assert(pqxx::from_string("[]") == pgvector::HalfVector(std::vector{})); + assert_equal(pqxx::from_string("[1,2,3]"), pgvector::HalfVector({1, 2, 3})); + assert_equal(pqxx::from_string("[]"), pgvector::HalfVector(std::vector{})); assert_exception([] { auto unused = pqxx::from_string(""); @@ -219,14 +216,14 @@ void test_halfvec_from_string() { } void test_sparsevec_to_string() { - assert(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})) == "{1:1,3:2,5:3}/6"); + assert_true(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})) == "{1:1,3:2,5:3}/6"); std::unordered_map map = {{999999999, -1.234567890123}}; - assert(pqxx::to_string(pgvector::SparseVector(map, 1000000000)) == "{1000000000:-1.2345679}/1000000000"); + assert_true(pqxx::to_string(pgvector::SparseVector(map, 1000000000)) == "{1000000000:-1.2345679}/1000000000"); } void test_sparsevec_from_string() { - assert(pqxx::from_string("{1:1,3:2,5:3}/6") == pgvector::SparseVector({1, 0, 2, 0, 3, 0})); - assert(pqxx::from_string("{}/6") == pgvector::SparseVector({0, 0, 0, 0, 0, 0})); + assert_equal(pqxx::from_string("{1:1,3:2,5:3}/6"), pgvector::SparseVector({1, 0, 2, 0, 3, 0})); + assert_equal(pqxx::from_string("{}/6"), pgvector::SparseVector({0, 0, 0, 0, 0, 0})); assert_exception([] { auto unused = pqxx::from_string(""); diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index 222ab04..9465bde 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -1,4 +1,3 @@ -#include #include #include @@ -10,22 +9,22 @@ using pgvector::SparseVector; static void test_constructor_vector() { auto vec = SparseVector({1, 0, 2, 0, 3, 0}); - assert(vec.dimensions() == 6); - assert(vec.indices() == (std::vector{0, 2, 4})); - assert(vec.values() == (std::vector{1, 2, 3})); + assert_equal(vec.dimensions(), 6); + assert_true(vec.indices() == (std::vector{0, 2, 4})); + assert_true(vec.values() == (std::vector{1, 2, 3})); } static void test_constructor_span() { auto vec = SparseVector(std::span({1, 0, 2, 0, 3, 0})); - assert(vec.dimensions() == 6); + assert_equal(vec.dimensions(), 6); } static void test_constructor_map() { std::unordered_map map = {{2, 2}, {4, 3}, {3, 0}, {0, 1}}; auto vec = SparseVector(map, 6); - assert(vec.dimensions() == 6); - assert(vec.indices() == (std::vector{0, 2, 4})); - assert(vec.values() == (std::vector{1, 2, 3})); + assert_equal(vec.dimensions(), 6); + assert_true(vec.indices() == (std::vector{0, 2, 4})); + assert_true(vec.values() == (std::vector{1, 2, 3})); assert_exception([&]{ auto unused = SparseVector(map, 0); diff --git a/test/vector_test.cpp b/test/vector_test.cpp index ef04411..87e8a0c 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -1,18 +1,19 @@ -#include #include #include +#include "helper.hpp" + using pgvector::Vector; static void test_constructor_vector() { auto vec = Vector({1, 2, 3}); - assert(vec.dimensions() == 3); + assert_equal(vec.dimensions(), static_cast(3)); } static void test_constructor_span() { auto vec = Vector(std::span({1, 2, 3})); - assert(vec.dimensions() == 3); + assert_equal(vec.dimensions(), static_cast(3)); } void test_vector() { From 57afcf907f9ecac2229b84cfaea6b56fffb3f5d8 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 23:47:16 -0800 Subject: [PATCH 082/163] Improved assertions --- test/pqxx_test.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index d1dea92..f409573 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -8,6 +8,8 @@ #include "helper.hpp" +using namespace std::string_literals; + void setup(pqxx::connection &conn) { pqxx::nontransaction tx(conn); tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); @@ -124,8 +126,8 @@ void test_stream_to(pqxx::connection &conn) { stream.write_values(pgvector::Vector({4, 5, 6})); stream.complete(); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY id"); - assert_true(res[0][0].as() == "[1,2,3]"); - assert_true(res[1][0].as() == "[4,5,6]"); + assert_equal(res[0][0].as(), "[1,2,3]"s); + assert_equal(res[1][0].as(), "[4,5,6]"s); } void test_precision(pqxx::connection &conn) { @@ -140,8 +142,8 @@ void test_precision(pqxx::connection &conn) { } void test_vector_to_string() { - assert_true(pqxx::to_string(pgvector::Vector({1, 2, 3})) == "[1,2,3]"); - assert_true(pqxx::to_string(pgvector::Vector({-1.234567890123})) == "[-1.2345679]"); + assert_equal(pqxx::to_string(pgvector::Vector({1, 2, 3})), "[1,2,3]"s); + assert_equal(pqxx::to_string(pgvector::Vector({-1.234567890123})), "[-1.2345679]"s); assert_exception([] { auto unused = pqxx::to_string(pgvector::Vector(std::vector(16001))); @@ -178,8 +180,8 @@ void test_vector_from_string() { } void test_halfvec_to_string() { - assert_true(pqxx::to_string(pgvector::HalfVector({1, 2, 3})) == "[1,2,3]"); - assert_true(pqxx::to_string(pgvector::HalfVector({-1.234567890123})) == "[-1.2345679]"); + assert_equal(pqxx::to_string(pgvector::HalfVector({1, 2, 3})), "[1,2,3]"s); + assert_equal(pqxx::to_string(pgvector::HalfVector({-1.234567890123})), "[-1.2345679]"s); assert_exception([] { auto unused = pqxx::to_string(pgvector::HalfVector(std::vector(16001))); @@ -216,9 +218,9 @@ void test_halfvec_from_string() { } void test_sparsevec_to_string() { - assert_true(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})) == "{1:1,3:2,5:3}/6"); + assert_equal(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})), "{1:1,3:2,5:3}/6"s); std::unordered_map map = {{999999999, -1.234567890123}}; - assert_true(pqxx::to_string(pgvector::SparseVector(map, 1000000000)) == "{1000000000:-1.2345679}/1000000000"); + assert_equal(pqxx::to_string(pgvector::SparseVector(map, 1000000000)), "{1000000000:-1.2345679}/1000000000"s); } void test_sparsevec_from_string() { From 98be8d48b17ce934f50150c875da6eefd6018ece Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 23:55:11 -0800 Subject: [PATCH 083/163] Improved tests [skip ci] --- test/pqxx_test.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index f409573..5afff31 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -8,8 +8,6 @@ #include "helper.hpp" -using namespace std::string_literals; - void setup(pqxx::connection &conn) { pqxx::nontransaction tx(conn); tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); @@ -126,8 +124,8 @@ void test_stream_to(pqxx::connection &conn) { stream.write_values(pgvector::Vector({4, 5, 6})); stream.complete(); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY id"); - assert_equal(res[0][0].as(), "[1,2,3]"s); - assert_equal(res[1][0].as(), "[4,5,6]"s); + assert_equal(res[0][0].as(), std::string("[1,2,3]")); + assert_equal(res[1][0].as(), std::string("[4,5,6]")); } void test_precision(pqxx::connection &conn) { @@ -142,8 +140,8 @@ void test_precision(pqxx::connection &conn) { } void test_vector_to_string() { - assert_equal(pqxx::to_string(pgvector::Vector({1, 2, 3})), "[1,2,3]"s); - assert_equal(pqxx::to_string(pgvector::Vector({-1.234567890123})), "[-1.2345679]"s); + assert_equal(pqxx::to_string(pgvector::Vector({1, 2, 3})), std::string("[1,2,3]")); + assert_equal(pqxx::to_string(pgvector::Vector({-1.234567890123})), std::string("[-1.2345679]")); assert_exception([] { auto unused = pqxx::to_string(pgvector::Vector(std::vector(16001))); @@ -180,8 +178,8 @@ void test_vector_from_string() { } void test_halfvec_to_string() { - assert_equal(pqxx::to_string(pgvector::HalfVector({1, 2, 3})), "[1,2,3]"s); - assert_equal(pqxx::to_string(pgvector::HalfVector({-1.234567890123})), "[-1.2345679]"s); + assert_equal(pqxx::to_string(pgvector::HalfVector({1, 2, 3})), std::string("[1,2,3]")); + assert_equal(pqxx::to_string(pgvector::HalfVector({-1.234567890123})), std::string("[-1.2345679]")); assert_exception([] { auto unused = pqxx::to_string(pgvector::HalfVector(std::vector(16001))); @@ -218,9 +216,9 @@ void test_halfvec_from_string() { } void test_sparsevec_to_string() { - assert_equal(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})), "{1:1,3:2,5:3}/6"s); + assert_equal(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})), std::string("{1:1,3:2,5:3}/6")); std::unordered_map map = {{999999999, -1.234567890123}}; - assert_equal(pqxx::to_string(pgvector::SparseVector(map, 1000000000)), "{1000000000:-1.2345679}/1000000000"s); + assert_equal(pqxx::to_string(pgvector::SparseVector(map, 1000000000)), std::string("{1000000000:-1.2345679}/1000000000")); } void test_sparsevec_from_string() { From 820646ce8b5cdb55cc43cc46fab3c2258ab56e2a Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Mar 2026 23:58:25 -0800 Subject: [PATCH 084/163] Improved tests --- test/halfvec_test.cpp | 4 ++-- test/helper.hpp | 4 ++-- test/pqxx_test.cpp | 16 ++++++++-------- test/vector_test.cpp | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index d3bffd5..a81b7b1 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -8,12 +8,12 @@ using pgvector::HalfVector; static void test_constructor_vector() { auto vec = HalfVector({1, 2, 3}); - assert_equal(vec.dimensions(), static_cast(3)); + assert_equal(vec.dimensions(), 3u); } static void test_constructor_span() { auto vec = HalfVector(std::span({1, 2, 3})); - assert_equal(vec.dimensions(), static_cast(3)); + assert_equal(vec.dimensions(), 3u); } void test_halfvec() { diff --git a/test/helper.hpp b/test/helper.hpp index bcd4a6c..f403ba3 100644 --- a/test/helper.hpp +++ b/test/helper.hpp @@ -6,8 +6,8 @@ #include #include -template -void assert_equal(const T& left, const T& right, const std::source_location& loc = std::source_location::current()) { +template +void assert_equal(const T& left, const U& right, const std::source_location& loc = std::source_location::current()) { if (left != right) { std::ostringstream message; message << left << " != " << right; diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 5afff31..a51f5b6 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -124,8 +124,8 @@ void test_stream_to(pqxx::connection &conn) { stream.write_values(pgvector::Vector({4, 5, 6})); stream.complete(); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY id"); - assert_equal(res[0][0].as(), std::string("[1,2,3]")); - assert_equal(res[1][0].as(), std::string("[4,5,6]")); + assert_equal(res[0][0].as(), "[1,2,3]"); + assert_equal(res[1][0].as(), "[4,5,6]"); } void test_precision(pqxx::connection &conn) { @@ -140,8 +140,8 @@ void test_precision(pqxx::connection &conn) { } void test_vector_to_string() { - assert_equal(pqxx::to_string(pgvector::Vector({1, 2, 3})), std::string("[1,2,3]")); - assert_equal(pqxx::to_string(pgvector::Vector({-1.234567890123})), std::string("[-1.2345679]")); + assert_equal(pqxx::to_string(pgvector::Vector({1, 2, 3})), "[1,2,3]"); + assert_equal(pqxx::to_string(pgvector::Vector({-1.234567890123})), "[-1.2345679]"); assert_exception([] { auto unused = pqxx::to_string(pgvector::Vector(std::vector(16001))); @@ -178,8 +178,8 @@ void test_vector_from_string() { } void test_halfvec_to_string() { - assert_equal(pqxx::to_string(pgvector::HalfVector({1, 2, 3})), std::string("[1,2,3]")); - assert_equal(pqxx::to_string(pgvector::HalfVector({-1.234567890123})), std::string("[-1.2345679]")); + assert_equal(pqxx::to_string(pgvector::HalfVector({1, 2, 3})), "[1,2,3]"); + assert_equal(pqxx::to_string(pgvector::HalfVector({-1.234567890123})), "[-1.2345679]"); assert_exception([] { auto unused = pqxx::to_string(pgvector::HalfVector(std::vector(16001))); @@ -216,9 +216,9 @@ void test_halfvec_from_string() { } void test_sparsevec_to_string() { - assert_equal(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})), std::string("{1:1,3:2,5:3}/6")); + assert_equal(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})), "{1:1,3:2,5:3}/6"); std::unordered_map map = {{999999999, -1.234567890123}}; - assert_equal(pqxx::to_string(pgvector::SparseVector(map, 1000000000)), std::string("{1000000000:-1.2345679}/1000000000")); + assert_equal(pqxx::to_string(pgvector::SparseVector(map, 1000000000)), "{1000000000:-1.2345679}/1000000000"); } void test_sparsevec_from_string() { diff --git a/test/vector_test.cpp b/test/vector_test.cpp index 87e8a0c..8fbf8bb 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -8,12 +8,12 @@ using pgvector::Vector; static void test_constructor_vector() { auto vec = Vector({1, 2, 3}); - assert_equal(vec.dimensions(), static_cast(3)); + assert_equal(vec.dimensions(), 3u); } static void test_constructor_span() { auto vec = Vector(std::span({1, 2, 3})); - assert_equal(vec.dimensions(), static_cast(3)); + assert_equal(vec.dimensions(), 3u); } void test_vector() { From 77edf9fa6a6df6ea0b0a3b80d9ff82d258840cfd Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 00:02:19 -0800 Subject: [PATCH 085/163] Removed assert_true function --- test/helper.hpp | 6 +----- test/pqxx_test.cpp | 8 ++++---- test/sparsevec_test.cpp | 8 ++++---- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/test/helper.hpp b/test/helper.hpp index f403ba3..4ed89e6 100644 --- a/test/helper.hpp +++ b/test/helper.hpp @@ -16,10 +16,6 @@ void assert_equal(const T& left, const U& right, const std::source_location& loc } } -inline void assert_true(bool condition, const std::source_location& loc = std::source_location::current()) { - assert_equal(condition, true, loc); -} - template void assert_exception(std::function code, std::optional message = std::nullopt) { bool exception = false; @@ -31,5 +27,5 @@ void assert_exception(std::function code, std::optional(), embedding2); assert_equal(res[1][0].as(), embedding); - assert_true(!res[2][0].as>().has_value()); + assert_equal(res[2][0].as>().has_value(), false); } void test_halfvec(pqxx::connection &conn) { @@ -57,7 +57,7 @@ void test_halfvec(pqxx::connection &conn) { assert_equal(res.size(), 3); assert_equal(res[0][0].as(), embedding2); assert_equal(res[1][0].as(), embedding); - assert_true(!res[2][0].as>().has_value()); + assert_equal(res[2][0].as>().has_value(), false); } void test_bit(pqxx::connection &conn) { @@ -72,7 +72,7 @@ void test_bit(pqxx::connection &conn) { assert_equal(res.size(), 3); assert_equal(res[0][0].as(), embedding2); assert_equal(res[1][0].as(), embedding); - assert_true(!res[2][0].as>().has_value()); + assert_equal(res[2][0].as>().has_value(), false); } void test_sparsevec(pqxx::connection &conn) { @@ -87,7 +87,7 @@ void test_sparsevec(pqxx::connection &conn) { assert_equal(res.size(), 3); assert_equal(res[0][0].as(), embedding2); assert_equal(res[1][0].as(), embedding); - assert_true(!res[2][0].as>().has_value()); + assert_equal(res[2][0].as>().has_value(), false); } void test_sparsevec_nnz(pqxx::connection &conn) { diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index 9465bde..7f41e36 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -10,8 +10,8 @@ using pgvector::SparseVector; static void test_constructor_vector() { auto vec = SparseVector({1, 0, 2, 0, 3, 0}); assert_equal(vec.dimensions(), 6); - assert_true(vec.indices() == (std::vector{0, 2, 4})); - assert_true(vec.values() == (std::vector{1, 2, 3})); + assert_equal(vec.indices() == (std::vector{0, 2, 4}), true); + assert_equal(vec.values() == (std::vector{1, 2, 3}), true); } static void test_constructor_span() { @@ -23,8 +23,8 @@ static void test_constructor_map() { std::unordered_map map = {{2, 2}, {4, 3}, {3, 0}, {0, 1}}; auto vec = SparseVector(map, 6); assert_equal(vec.dimensions(), 6); - assert_true(vec.indices() == (std::vector{0, 2, 4})); - assert_true(vec.values() == (std::vector{1, 2, 3})); + assert_equal(vec.indices() == (std::vector{0, 2, 4}), true); + assert_equal(vec.values() == (std::vector{1, 2, 3}), true); assert_exception([&]{ auto unused = SparseVector(map, 0); From 2aa26cccb2e66fe51bc39ff389c536891d41e69e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 00:04:45 -0800 Subject: [PATCH 086/163] Simplified code [skip ci] --- test/sparsevec_test.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index 7f41e36..40df0ce 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -10,8 +10,8 @@ using pgvector::SparseVector; static void test_constructor_vector() { auto vec = SparseVector({1, 0, 2, 0, 3, 0}); assert_equal(vec.dimensions(), 6); - assert_equal(vec.indices() == (std::vector{0, 2, 4}), true); - assert_equal(vec.values() == (std::vector{1, 2, 3}), true); + assert_equal(vec.indices() == std::vector{0, 2, 4}, true); + assert_equal(vec.values() == std::vector{1, 2, 3}, true); } static void test_constructor_span() { @@ -23,8 +23,8 @@ static void test_constructor_map() { std::unordered_map map = {{2, 2}, {4, 3}, {3, 0}, {0, 1}}; auto vec = SparseVector(map, 6); assert_equal(vec.dimensions(), 6); - assert_equal(vec.indices() == (std::vector{0, 2, 4}), true); - assert_equal(vec.values() == (std::vector{1, 2, 3}), true); + assert_equal(vec.indices() == std::vector{0, 2, 4}, true); + assert_equal(vec.values() == std::vector{1, 2, 3}, true); assert_exception([&]{ auto unused = SparseVector(map, 0); From aaf624181b9ebfe042b2e05c1e5298a273d1543e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 00:06:38 -0800 Subject: [PATCH 087/163] Improved tests [skip ci] --- test/pqxx_test.cpp | 12 ++++++------ test/sparsevec_test.cpp | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index da093de..01251dc 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -144,7 +144,7 @@ void test_vector_to_string() { assert_equal(pqxx::to_string(pgvector::Vector({-1.234567890123})), "[-1.2345679]"); assert_exception([] { - auto unused = pqxx::to_string(pgvector::Vector(std::vector(16001))); + pqxx::to_string(pgvector::Vector(std::vector(16001))); }, "vector cannot have more than 16000 dimensions"); } @@ -182,7 +182,7 @@ void test_halfvec_to_string() { assert_equal(pqxx::to_string(pgvector::HalfVector({-1.234567890123})), "[-1.2345679]"); assert_exception([] { - auto unused = pqxx::to_string(pgvector::HalfVector(std::vector(16001))); + pqxx::to_string(pgvector::HalfVector(std::vector(16001))); }, "halfvec cannot have more than 16000 dimensions"); } @@ -219,6 +219,10 @@ void test_sparsevec_to_string() { assert_equal(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})), "{1:1,3:2,5:3}/6"); std::unordered_map map = {{999999999, -1.234567890123}}; assert_equal(pqxx::to_string(pgvector::SparseVector(map, 1000000000)), "{1000000000:-1.2345679}/1000000000"); + + assert_exception([] { + pqxx::to_string(pgvector::SparseVector(std::vector(16001, 1))); + }, "sparsevec cannot have more than 16000 dimensions"); } void test_sparsevec_from_string() { @@ -272,10 +276,6 @@ void test_sparsevec_from_string() { assert_exception([] { auto unused = pqxx::from_string("{}/a"); }, "Could not convert 'a' to int: Invalid argument."); - - assert_exception([] { - auto unused = pqxx::to_string(pgvector::SparseVector(std::vector(16001, 1))); - }, "sparsevec cannot have more than 16000 dimensions"); } void test_pqxx() { diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index 40df0ce..9bb3324 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -27,11 +27,11 @@ static void test_constructor_map() { assert_equal(vec.values() == std::vector{1, 2, 3}, true); assert_exception([&]{ - auto unused = SparseVector(map, 0); + SparseVector(map, 0); }, "sparsevec must have at least 1 dimension"); assert_exception([&]{ - auto unused = SparseVector(map, 4); + SparseVector(map, 4); }, "sparsevec index out of bounds"); } From 30d34784d63cbac505748e838b75b705ac70c81f Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 00:12:08 -0800 Subject: [PATCH 088/163] Improved tests [skip ci] --- test/pqxx_test.cpp | 48 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 01251dc..faab643 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -153,27 +153,27 @@ void test_vector_from_string() { assert_equal(pqxx::from_string("[]"), pgvector::Vector(std::vector{})); assert_exception([] { - auto unused = pqxx::from_string(""); + auto _ = pqxx::from_string(""); }, "Malformed vector literal"); assert_exception([] { - auto unused = pqxx::from_string("["); + auto _ = pqxx::from_string("["); }, "Malformed vector literal"); assert_exception([] { - auto unused = pqxx::from_string("[hello]"); + auto _ = pqxx::from_string("[hello]"); }, float_error("Could not convert 'hello' to float: Invalid argument.")); assert_exception([] { - auto unused = pqxx::from_string("[4e38]"); + auto _ = pqxx::from_string("[4e38]"); }, float_error("Could not convert '4e38' to float: Value out of range.")); assert_exception([] { - auto unused = pqxx::from_string("[,]"); + auto _ = pqxx::from_string("[,]"); }, float_error("Could not convert '' to float: Invalid argument.")); assert_exception([] { - auto unused = pqxx::from_string("[1,]"); + auto _ = pqxx::from_string("[1,]"); }, float_error("Could not convert '' to float: Invalid argument.")); } @@ -191,27 +191,27 @@ void test_halfvec_from_string() { assert_equal(pqxx::from_string("[]"), pgvector::HalfVector(std::vector{})); assert_exception([] { - auto unused = pqxx::from_string(""); + auto _ = pqxx::from_string(""); }, "Malformed halfvec literal"); assert_exception([] { - auto unused = pqxx::from_string("["); + auto _ = pqxx::from_string("["); }, "Malformed halfvec literal"); assert_exception([] { - auto unused = pqxx::from_string("[hello]"); + auto _ = pqxx::from_string("[hello]"); }, float_error("Could not convert 'hello' to float: Invalid argument.")); assert_exception([] { - auto unused = pqxx::from_string("[4e38]"); + auto _ = pqxx::from_string("[4e38]"); }, float_error("Could not convert '4e38' to float: Value out of range.")); assert_exception([] { - auto unused = pqxx::from_string("[,]"); + auto _ = pqxx::from_string("[,]"); }, float_error("Could not convert '' to float: Invalid argument.")); assert_exception([] { - auto unused = pqxx::from_string("[1,]"); + auto _ = pqxx::from_string("[1,]"); }, float_error("Could not convert '' to float: Invalid argument.")); } @@ -230,51 +230,51 @@ void test_sparsevec_from_string() { assert_equal(pqxx::from_string("{}/6"), pgvector::SparseVector({0, 0, 0, 0, 0, 0})); assert_exception([] { - auto unused = pqxx::from_string(""); + auto _ = pqxx::from_string(""); }, "Malformed sparsevec literal"); assert_exception([] { - auto unused = pqxx::from_string("{"); + auto _ = pqxx::from_string("{"); }, "Malformed sparsevec literal"); assert_exception([] { - auto unused = pqxx::from_string("{ }/"); + auto _ = pqxx::from_string("{ }/"); }, "Could not convert '' to int: Invalid argument."); assert_exception([] { - auto unused = pqxx::from_string("{}/-1"); + auto _ = pqxx::from_string("{}/-1"); }, "Dimensions cannot be negative"); assert_exception([] { - auto unused = pqxx::from_string("{:}/1"); + auto _ = pqxx::from_string("{:}/1"); }, "Could not convert '' to int: Invalid argument."); assert_exception([] { - auto unused = pqxx::from_string("{,}/1"); + auto _ = pqxx::from_string("{,}/1"); }, "Malformed sparsevec literal"); assert_exception([] { - auto unused = pqxx::from_string("{0:1}/1"); + auto _ = pqxx::from_string("{0:1}/1"); }, "Index out of bounds"); assert_exception([] { - auto unused = pqxx::from_string("{-2147483648:1}/1"); + auto _ = pqxx::from_string("{-2147483648:1}/1"); }, "Index out of bounds"); assert_exception([] { - auto unused = pqxx::from_string("{1:4e38}/1"); + auto _ = pqxx::from_string("{1:4e38}/1"); }, float_error("Could not convert '4e38' to float: Value out of range.")); assert_exception([] { - auto unused = pqxx::from_string("{a:1}/1"); + auto _ = pqxx::from_string("{a:1}/1"); }, "Could not convert 'a' to int: Invalid argument."); assert_exception([] { - auto unused = pqxx::from_string("{1:a}/1"); + auto _ = pqxx::from_string("{1:a}/1"); }, float_error("Could not convert 'a' to float: Invalid argument.")); assert_exception([] { - auto unused = pqxx::from_string("{}/a"); + auto _ = pqxx::from_string("{}/a"); }, "Could not convert 'a' to int: Invalid argument."); } From 0b94a2001568c570e2598e773e1b0669cad0157e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 00:55:27 -0800 Subject: [PATCH 089/163] Changed HalfVector to use std::float16_t when available --- CHANGELOG.md | 1 + include/pgvector/halfvec.hpp | 32 +++++++++++++++++++++----------- include/pgvector/pqxx.hpp | 14 +++++++------- test/halfvec_test.cpp | 2 +- test/pqxx_test.cpp | 10 +++++++--- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4d13e3..84d1ee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.3.0 (unreleased) - Added support for libpqxx 8 +- Changed `HalfVector` to use `std::float16_t` when available - Dropped support for libpqxx 7 - Dropped support for C++17 diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 3712dc6..db1eb5c 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -12,31 +12,42 @@ #include #include +#if __STDCPP_FLOAT16_T__ +#include +#endif + namespace pgvector { + +#if __STDCPP_FLOAT16_T__ +using HalfType = std::float16_t; +#else +using HalfType = float; +#endif + /// A half vector. class HalfVector { public: - /// Creates a half vector from a `std::vector`. - explicit HalfVector(const std::vector& value) : value_{value} {} + /// Creates a half vector from a `std::vector`. + explicit HalfVector(const std::vector& value) : value_{value} {} - /// Creates a half vector from a `std::vector`. - explicit HalfVector(std::vector&& value) : value_{std::move(value)} {} + /// Creates a half vector from a `std::vector`. + explicit HalfVector(std::vector&& value) : value_{std::move(value)} {} /// Creates a half vector from a span. - explicit HalfVector(std::span value) : value_{std::vector(value.begin(), value.end())} {} + explicit HalfVector(std::span value) : value_{std::vector(value.begin(), value.end())} {} /// Returns the number of dimensions. size_t dimensions() const { return value_.size(); } - /// Returns the half vector as a `std::vector`. - operator const std::vector() const { + /// Returns the half vector as a `std::vector`. + operator const std::vector() const { return value_; } - /// Returns the half vector as a `std::span`. - operator const std::span() const { + /// Returns the half vector as a `std::span`. + operator const std::span() const { return value_; } @@ -57,7 +68,6 @@ class HalfVector { } private: - // TODO use std::float16_t for C++23 - std::vector value_; + std::vector value_; }; } // namespace pgvector diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 314b518..a6ef4c4 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -93,23 +93,23 @@ template <> struct string_traits { throw conversion_error("Malformed halfvec literal"); } - std::vector values; + std::vector values; if (text.size() > 2) { std::string_view inner = text.substr(1, text.size() - 2); size_t start = 0; for (size_t i = 0; i < inner.size(); i++) { if (inner[i] == ',') { - values.push_back(string_traits::from_string(inner.substr(start, i - start), c)); + values.push_back(static_cast(string_traits::from_string(inner.substr(start, i - start), c))); start = i + 1; } } - values.push_back(string_traits::from_string(inner.substr(start), c)); + values.push_back(static_cast(string_traits::from_string(inner.substr(start), c))); } return pgvector::HalfVector(std::move(values)); } static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { - std::span values{value}; + std::span values{value}; // important! size_buffer cannot throw an exception on overflow // so perform this check before writing any data @@ -124,7 +124,7 @@ template <> struct string_traits { if (i != 0) { buf[here++] = ','; } - here += pqxx::into_buf(buf.subspan(here), values[i], c); + here += pqxx::into_buf(buf.subspan(here), static_cast(values[i]), c); } buf[here++] = ']'; @@ -133,7 +133,7 @@ template <> struct string_traits { } static size_t size_buffer(const pgvector::HalfVector& value) noexcept { - std::span values{value}; + std::span values{value}; // cannot throw an exception here on overflow // so throw in into_buf @@ -141,7 +141,7 @@ template <> struct string_traits { size_t size = 2; // [ and ] for (const auto v : values) { size += 1; // , - size += string_traits::size_buffer(v); + size += string_traits::size_buffer(static_cast(v)); } return size; } diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index a81b7b1..b10afcc 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -12,7 +12,7 @@ static void test_constructor_vector() { } static void test_constructor_span() { - auto vec = HalfVector(std::span({1, 2, 3})); + auto vec = HalfVector(std::span({1, 2, 3})); assert_equal(vec.dimensions(), 3u); } diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index faab643..bfc1eef 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -49,7 +49,7 @@ void test_halfvec(pqxx::connection &conn) { pqxx::nontransaction tx(conn); auto embedding = pgvector::HalfVector({1, 2, 3}); - float arr[] = {4, 5, 6}; + pgvector::HalfType arr[] = {4, 5, 6}; auto embedding2 = pgvector::HalfVector(std::span{arr, 3}); tx.exec("INSERT INTO items (half_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); @@ -179,16 +179,20 @@ void test_vector_from_string() { void test_halfvec_to_string() { assert_equal(pqxx::to_string(pgvector::HalfVector({1, 2, 3})), "[1,2,3]"); +#if __STDCPP_FLOAT16_T__ + assert_equal(pqxx::to_string(pgvector::HalfVector({-1.234567890123f16})), "[-1.234375]"); +#else assert_equal(pqxx::to_string(pgvector::HalfVector({-1.234567890123})), "[-1.2345679]"); +#endif assert_exception([] { - pqxx::to_string(pgvector::HalfVector(std::vector(16001))); + pqxx::to_string(pgvector::HalfVector(std::vector(16001))); }, "halfvec cannot have more than 16000 dimensions"); } void test_halfvec_from_string() { assert_equal(pqxx::from_string("[1,2,3]"), pgvector::HalfVector({1, 2, 3})); - assert_equal(pqxx::from_string("[]"), pgvector::HalfVector(std::vector{})); + assert_equal(pqxx::from_string("[]"), pgvector::HalfVector(std::vector{})); assert_exception([] { auto _ = pqxx::from_string(""); From c7fadb268c86d7139d7d3cc6585398aa92ee8db5 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 01:14:25 -0800 Subject: [PATCH 090/163] Renamed HalfType to Half --- include/pgvector/halfvec.hpp | 24 ++++++++++++------------ include/pgvector/pqxx.hpp | 10 +++++----- test/halfvec_test.cpp | 2 +- test/pqxx_test.cpp | 6 +++--- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index db1eb5c..9608a0b 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -19,35 +19,35 @@ namespace pgvector { #if __STDCPP_FLOAT16_T__ -using HalfType = std::float16_t; +using Half = std::float16_t; #else -using HalfType = float; +using Half = float; #endif /// A half vector. class HalfVector { public: - /// Creates a half vector from a `std::vector`. - explicit HalfVector(const std::vector& value) : value_{value} {} + /// Creates a half vector from a `std::vector`. + explicit HalfVector(const std::vector& value) : value_{value} {} - /// Creates a half vector from a `std::vector`. - explicit HalfVector(std::vector&& value) : value_{std::move(value)} {} + /// Creates a half vector from a `std::vector`. + explicit HalfVector(std::vector&& value) : value_{std::move(value)} {} /// Creates a half vector from a span. - explicit HalfVector(std::span value) : value_{std::vector(value.begin(), value.end())} {} + explicit HalfVector(std::span value) : value_{std::vector(value.begin(), value.end())} {} /// Returns the number of dimensions. size_t dimensions() const { return value_.size(); } - /// Returns the half vector as a `std::vector`. - operator const std::vector() const { + /// Returns the half vector as a `std::vector`. + operator const std::vector() const { return value_; } - /// Returns the half vector as a `std::span`. - operator const std::span() const { + /// Returns the half vector as a `std::span`. + operator const std::span() const { return value_; } @@ -68,6 +68,6 @@ class HalfVector { } private: - std::vector value_; + std::vector value_; }; } // namespace pgvector diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index a6ef4c4..4f0b3b1 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -93,23 +93,23 @@ template <> struct string_traits { throw conversion_error("Malformed halfvec literal"); } - std::vector values; + std::vector values; if (text.size() > 2) { std::string_view inner = text.substr(1, text.size() - 2); size_t start = 0; for (size_t i = 0; i < inner.size(); i++) { if (inner[i] == ',') { - values.push_back(static_cast(string_traits::from_string(inner.substr(start, i - start), c))); + values.push_back(static_cast(string_traits::from_string(inner.substr(start, i - start), c))); start = i + 1; } } - values.push_back(static_cast(string_traits::from_string(inner.substr(start), c))); + values.push_back(static_cast(string_traits::from_string(inner.substr(start), c))); } return pgvector::HalfVector(std::move(values)); } static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { - std::span values{value}; + std::span values{value}; // important! size_buffer cannot throw an exception on overflow // so perform this check before writing any data @@ -133,7 +133,7 @@ template <> struct string_traits { } static size_t size_buffer(const pgvector::HalfVector& value) noexcept { - std::span values{value}; + std::span values{value}; // cannot throw an exception here on overflow // so throw in into_buf diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index b10afcc..c8b54ca 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -12,7 +12,7 @@ static void test_constructor_vector() { } static void test_constructor_span() { - auto vec = HalfVector(std::span({1, 2, 3})); + auto vec = HalfVector(std::span({1, 2, 3})); assert_equal(vec.dimensions(), 3u); } diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index bfc1eef..98fe57f 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -49,7 +49,7 @@ void test_halfvec(pqxx::connection &conn) { pqxx::nontransaction tx(conn); auto embedding = pgvector::HalfVector({1, 2, 3}); - pgvector::HalfType arr[] = {4, 5, 6}; + pgvector::Half arr[] = {4, 5, 6}; auto embedding2 = pgvector::HalfVector(std::span{arr, 3}); tx.exec("INSERT INTO items (half_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); @@ -186,13 +186,13 @@ void test_halfvec_to_string() { #endif assert_exception([] { - pqxx::to_string(pgvector::HalfVector(std::vector(16001))); + pqxx::to_string(pgvector::HalfVector(std::vector(16001))); }, "halfvec cannot have more than 16000 dimensions"); } void test_halfvec_from_string() { assert_equal(pqxx::from_string("[1,2,3]"), pgvector::HalfVector({1, 2, 3})); - assert_equal(pqxx::from_string("[]"), pgvector::HalfVector(std::vector{})); + assert_equal(pqxx::from_string("[]"), pgvector::HalfVector(std::vector{})); assert_exception([] { auto _ = pqxx::from_string(""); From f61957afc4ff547ec86363c3bcf491cceb4036e5 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 01:25:52 -0800 Subject: [PATCH 091/163] Updated HalfVector to use _Float16 when available --- CHANGELOG.md | 2 +- include/pgvector/halfvec.hpp | 9 +++++++++ test/pqxx_test.cpp | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84d1ee6..a510c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## 0.3.0 (unreleased) - Added support for libpqxx 8 -- Changed `HalfVector` to use `std::float16_t` when available +- Changed `HalfVector` to use `std::float16_t` or `_Float16` when available - Dropped support for libpqxx 7 - Dropped support for C++17 diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 9608a0b..91b352d 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -14,12 +14,17 @@ #if __STDCPP_FLOAT16_T__ #include +#else +#define __STDC_WANT_IEC_60559_TYPES_EXT__ +#include #endif namespace pgvector { #if __STDCPP_FLOAT16_T__ using Half = std::float16_t; +#elif defined(__FLT16_MAX__) +using Half = _Float16; #else using Half = float; #endif @@ -61,7 +66,11 @@ class HalfVector { if (i > 0) { os << ","; } +#if __STDCPP_FLOAT16_T__ os << value.value_[i]; +#else + os << static_cast(value.value_[i]); +#endif } os << "]"; return os; diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 98fe57f..be265b7 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -179,8 +179,8 @@ void test_vector_from_string() { void test_halfvec_to_string() { assert_equal(pqxx::to_string(pgvector::HalfVector({1, 2, 3})), "[1,2,3]"); -#if __STDCPP_FLOAT16_T__ - assert_equal(pqxx::to_string(pgvector::HalfVector({-1.234567890123f16})), "[-1.234375]"); +#if __STDCPP_FLOAT16_T__ || defined(__FLT16_MAX__) + assert_equal(pqxx::to_string(pgvector::HalfVector({static_cast(-1.234567890123)})), "[-1.234375]"); #else assert_equal(pqxx::to_string(pgvector::HalfVector({-1.234567890123})), "[-1.2345679]"); #endif From 9b9ca2b2becb45ff149543550a4588f9fc636d46 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 01:52:29 -0800 Subject: [PATCH 092/163] Updated readme [skip ci] --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 59f9b3e..46ce63d 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,8 @@ std::unordered_map map = {{0, 1}, {2, 2}, {4, 3}}; auto vec = pgvector::SparseVector(map, 6); ``` +Note: Indices start at 0 + Get the number of dimensions ```cpp @@ -134,13 +136,13 @@ int dim = vec.dimensions(); Get the indices of non-zero elements ```cpp -auto indices = vec.indices(); +auto& indices = vec.indices(); ``` Get the values of non-zero elements ```cpp -auto values = vec.values(); +auto& values = vec.values(); ``` ## History From 90696fd952a278d5e66d62f0a0dc814b204ea617 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 13:35:28 -0800 Subject: [PATCH 093/163] Added tests for explicit conversion [skip ci] --- test/halfvec_test.cpp | 7 +++++++ test/vector_test.cpp | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index c8b54ca..fe8ad1c 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -16,7 +16,14 @@ static void test_constructor_span() { assert_equal(vec.dimensions(), 3u); } +static void test_conversion() { + auto vec = HalfVector({1, 2, 3}); + auto half_vec = static_cast>(vec); + assert_equal(half_vec == std::vector{1, 2, 3}, true); +} + void test_halfvec() { test_constructor_vector(); test_constructor_span(); + test_conversion(); } diff --git a/test/vector_test.cpp b/test/vector_test.cpp index 8fbf8bb..48f1b4b 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -16,7 +16,14 @@ static void test_constructor_span() { assert_equal(vec.dimensions(), 3u); } +static void test_conversion() { + auto vec = Vector({1, 2, 3}); + auto float_vec = static_cast>(vec); + assert_equal(float_vec == std::vector{1, 2, 3}, true); +} + void test_vector() { test_constructor_vector(); test_constructor_span(); + test_conversion(); } From 9fe34c2c943006bc545031d8c70448b01a167016 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 13:39:38 -0800 Subject: [PATCH 094/163] Replaced conversion with as_vector function for Vector and HalfVector --- CHANGELOG.md | 1 + include/pgvector/halfvec.hpp | 7 +------ include/pgvector/pqxx.hpp | 8 ++++---- include/pgvector/vector.hpp | 7 +------ test/halfvec_test.cpp | 6 +++--- test/vector_test.cpp | 6 +++--- 6 files changed, 13 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a510c28..237977d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Added support for libpqxx 8 - Changed `HalfVector` to use `std::float16_t` or `_Float16` when available +- Replaced conversion with `as_vector` function for `Vector` and `HalfVector` - Dropped support for libpqxx 7 - Dropped support for C++17 diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 91b352d..4a24e4b 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -47,12 +47,7 @@ class HalfVector { } /// Returns the half vector as a `std::vector`. - operator const std::vector() const { - return value_; - } - - /// Returns the half vector as a `std::span`. - operator const std::span() const { + const std::vector& as_vector() const { return value_; } diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 4f0b3b1..e9deddd 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -45,7 +45,7 @@ template <> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::Vector& value, ctx c = {}) { - std::span values{value}; + auto& values = value.as_vector(); // important! size_buffer cannot throw an exception on overflow // so perform this check before writing any data @@ -69,7 +69,7 @@ template <> struct string_traits { } static size_t size_buffer(const pgvector::Vector& value) noexcept { - std::span values{value}; + auto& values = value.as_vector(); // cannot throw an exception here on overflow // so throw in into_buf @@ -109,7 +109,7 @@ template <> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { - std::span values{value}; + auto& values = value.as_vector(); // important! size_buffer cannot throw an exception on overflow // so perform this check before writing any data @@ -133,7 +133,7 @@ template <> struct string_traits { } static size_t size_buffer(const pgvector::HalfVector& value) noexcept { - std::span values{value}; + auto& values = value.as_vector(); // cannot throw an exception here on overflow // so throw in into_buf diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index d8adbd8..d90e3a7 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -31,12 +31,7 @@ class Vector { } /// Returns the vector as a `std::vector`. - operator const std::vector() const { - return value_; - } - - /// Returns the vector as a `std::span`. - operator const std::span() const { + const std::vector& as_vector() const { return value_; } diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index fe8ad1c..c0e18ac 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -16,14 +16,14 @@ static void test_constructor_span() { assert_equal(vec.dimensions(), 3u); } -static void test_conversion() { +static void test_as_vector() { auto vec = HalfVector({1, 2, 3}); - auto half_vec = static_cast>(vec); + auto& half_vec = vec.as_vector(); assert_equal(half_vec == std::vector{1, 2, 3}, true); } void test_halfvec() { test_constructor_vector(); test_constructor_span(); - test_conversion(); + test_as_vector(); } diff --git a/test/vector_test.cpp b/test/vector_test.cpp index 48f1b4b..b40843f 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -16,14 +16,14 @@ static void test_constructor_span() { assert_equal(vec.dimensions(), 3u); } -static void test_conversion() { +static void test_as_vector() { auto vec = Vector({1, 2, 3}); - auto float_vec = static_cast>(vec); + auto& float_vec = vec.as_vector(); assert_equal(float_vec == std::vector{1, 2, 3}, true); } void test_vector() { test_constructor_vector(); test_constructor_span(); - test_conversion(); + test_as_vector(); } From 63872805a31074a9ee7f963c21df541b78956582 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 13:49:52 -0800 Subject: [PATCH 095/163] Improved tests --- test/halfvec_test.cpp | 2 +- test/sparsevec_test.cpp | 2 +- test/vector_test.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index c0e18ac..8cadbfc 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -7,7 +7,7 @@ using pgvector::HalfVector; static void test_constructor_vector() { - auto vec = HalfVector({1, 2, 3}); + auto vec = HalfVector(std::vector{1, 2, 3}); assert_equal(vec.dimensions(), 3u); } diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index 9bb3324..166a4e6 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -8,7 +8,7 @@ using pgvector::SparseVector; static void test_constructor_vector() { - auto vec = SparseVector({1, 0, 2, 0, 3, 0}); + auto vec = SparseVector(std::vector{1, 0, 2, 0, 3, 0}); assert_equal(vec.dimensions(), 6); assert_equal(vec.indices() == std::vector{0, 2, 4}, true); assert_equal(vec.values() == std::vector{1, 2, 3}, true); diff --git a/test/vector_test.cpp b/test/vector_test.cpp index b40843f..c649269 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -7,7 +7,7 @@ using pgvector::Vector; static void test_constructor_vector() { - auto vec = Vector({1, 2, 3}); + auto vec = Vector(std::vector{1, 2, 3}); assert_equal(vec.dimensions(), 3u); } From 1ba7cefa9a83c5afeb012445370a9215d6ab5620 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 13:51:01 -0800 Subject: [PATCH 096/163] Simplified tests [skip ci] --- test/pqxx_test.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index be265b7..cb91939 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -33,8 +33,7 @@ void test_vector(pqxx::connection &conn) { pqxx::nontransaction tx(conn); auto embedding = pgvector::Vector({1, 2, 3}); - float arr[] = {4, 5, 6}; - auto embedding2 = pgvector::Vector(std::span{arr, 3}); + auto embedding2 = pgvector::Vector({4, 5, 6}); tx.exec("INSERT INTO items (embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY embedding <-> $1", {embedding2}); @@ -49,8 +48,7 @@ void test_halfvec(pqxx::connection &conn) { pqxx::nontransaction tx(conn); auto embedding = pgvector::HalfVector({1, 2, 3}); - pgvector::Half arr[] = {4, 5, 6}; - auto embedding2 = pgvector::HalfVector(std::span{arr, 3}); + auto embedding2 = pgvector::HalfVector({4, 5, 6}); tx.exec("INSERT INTO items (half_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT half_embedding FROM items ORDER BY half_embedding <-> $1", {embedding2}); From c99b157c2c41ebe757df6ca41fbed15c9f8f85a9 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 13:57:32 -0800 Subject: [PATCH 097/163] Simplified tests [skip ci] --- test/halfvec_test.cpp | 3 +-- test/vector_test.cpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index 8cadbfc..4996efc 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -18,8 +18,7 @@ static void test_constructor_span() { static void test_as_vector() { auto vec = HalfVector({1, 2, 3}); - auto& half_vec = vec.as_vector(); - assert_equal(half_vec == std::vector{1, 2, 3}, true); + assert_equal(vec.as_vector() == std::vector{1, 2, 3}, true); } void test_halfvec() { diff --git a/test/vector_test.cpp b/test/vector_test.cpp index c649269..37d5e74 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -18,8 +18,7 @@ static void test_constructor_span() { static void test_as_vector() { auto vec = Vector({1, 2, 3}); - auto& float_vec = vec.as_vector(); - assert_equal(float_vec == std::vector{1, 2, 3}, true); + assert_equal(vec.as_vector() == std::vector{1, 2, 3}, true); } void test_vector() { From 5bded6aef2622abf86b61c223413a113290ddef6 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 14:10:22 -0800 Subject: [PATCH 098/163] Updated doc comments [skip ci] --- include/pgvector/halfvec.hpp | 6 +++--- include/pgvector/vector.hpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 4a24e4b..1d96413 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -32,10 +32,10 @@ using Half = float; /// A half vector. class HalfVector { public: - /// Creates a half vector from a `std::vector`. + /// Creates a half vector from a `std::vector`. explicit HalfVector(const std::vector& value) : value_{value} {} - /// Creates a half vector from a `std::vector`. + /// Creates a half vector from a `std::vector`. explicit HalfVector(std::vector&& value) : value_{std::move(value)} {} /// Creates a half vector from a span. @@ -46,7 +46,7 @@ class HalfVector { return value_.size(); } - /// Returns the half vector as a `std::vector`. + /// Returns the half vector as a `std::vector`. const std::vector& as_vector() const { return value_; } diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index d90e3a7..52c4a7a 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -16,10 +16,10 @@ namespace pgvector { /// A vector. class Vector { public: - /// Creates a vector from a `std::vector`. + /// Creates a vector from a `std::vector`. explicit Vector(const std::vector& value) : value_{value} {} - /// Creates a vector from a `std::vector`. + /// Creates a vector from a `std::vector`. explicit Vector(std::vector&& value) : value_{std::move(value)} {} /// Creates a vector from a span. @@ -30,7 +30,7 @@ class Vector { return value_.size(); } - /// Returns the vector as a `std::vector`. + /// Returns the vector as a `std::vector`. const std::vector& as_vector() const { return value_; } From 4fadcad3e8cb556f378eb10675e59d18d2960cbb Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 14:15:28 -0800 Subject: [PATCH 099/163] Added doc comment for Half [skip ci] --- include/pgvector/halfvec.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 1d96413..222ec19 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -21,6 +21,7 @@ namespace pgvector { +/// A half vector element. #if __STDCPP_FLOAT16_T__ using Half = std::float16_t; #elif defined(__FLT16_MAX__) From b6dfe3435a98950d5f3c1e39b13b86645a01e898 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 15:04:57 -0800 Subject: [PATCH 100/163] Updated changelog [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 237977d..11fae73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Added support for libpqxx 8 - Changed `HalfVector` to use `std::float16_t` or `_Float16` when available - Replaced conversion with `as_vector` function for `Vector` and `HalfVector` +- Removed default constructor from `Vector` and `HalfVector` - Dropped support for libpqxx 7 - Dropped support for C++17 From 8777f471992038a1306e2872eff4c04bc1a39b77 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 15:05:43 -0800 Subject: [PATCH 101/163] Updated changelog [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11fae73..6ebacb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ - Added support for libpqxx 8 - Changed `HalfVector` to use `std::float16_t` or `_Float16` when available -- Replaced conversion with `as_vector` function for `Vector` and `HalfVector` +- Replaced `std::vector` conversion with `as_vector` function for `Vector` and `HalfVector` - Removed default constructor from `Vector` and `HalfVector` - Dropped support for libpqxx 7 - Dropped support for C++17 From 15fd0f3fd0fae5d70a45af7c16216838034740ed Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 15:09:34 -0800 Subject: [PATCH 102/163] Updated readme [skip ci] --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 46ce63d..85d74e5 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ pqxx::result r = tx.exec("SELECT * FROM items ORDER BY embedding <-> $1 LIMIT 5" Retrieve a vector ```cpp -auto row = tx.exec("SELECT embedding FROM items LIMIT 1").one_row(); +pqxx::row row = tx.exec("SELECT embedding FROM items LIMIT 1").one_row(); auto embedding = row[0].as(); ``` @@ -136,13 +136,13 @@ int dim = vec.dimensions(); Get the indices of non-zero elements ```cpp -auto& indices = vec.indices(); +const std::vector& indices = vec.indices(); ``` Get the values of non-zero elements ```cpp -auto& values = vec.values(); +const std::vector& values = vec.values(); ``` ## History From cc35e6f3118622e1864fa64ec436daccd23639d7 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 15:37:51 -0800 Subject: [PATCH 103/163] Updated style [skip ci] --- README.md | 12 ++++++------ test/halfvec_test.cpp | 6 +++--- test/pqxx_test.cpp | 24 ++++++++++++------------ test/sparsevec_test.cpp | 8 ++++---- test/vector_test.cpp | 6 +++--- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 85d74e5..5fa5616 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ tx.exec("CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(3))"); Insert a vector ```cpp -auto embedding = pgvector::Vector({1, 2, 3}); +pgvector::Vector embedding({1, 2, 3}); tx.exec("INSERT INTO items (embedding) VALUES ($1)", {embedding}); ``` @@ -87,7 +87,7 @@ Use `std::optional` if the value could be `NULL` Create a vector from a `std::vector` ```cpp -auto vec = pgvector::Vector({1, 2, 3}); +pgvector::Vector vec(std::vector{1, 2, 3}); ``` Convert to a `std::vector` @@ -101,7 +101,7 @@ auto float_vec = static_cast>(vec); Create a half vector from a `std::vector` ```cpp -auto vec = pgvector::HalfVector({1, 2, 3}); +pgvector::HalfVector vec(std::vector{1, 2, 3}); ``` Convert to a `std::vector` @@ -115,14 +115,14 @@ auto float_vec = static_cast>(vec); Create a sparse vector from a `std::vector` ```cpp -auto vec = pgvector::SparseVector({1, 0, 2, 0, 3, 0}); +pgvector::SparseVector vec({1, 0, 2, 0, 3, 0}); ``` Or a map of non-zero elements ```cpp -std::unordered_map map = {{0, 1}, {2, 2}, {4, 3}}; -auto vec = pgvector::SparseVector(map, 6); +std::unordered_map map{{0, 1}, {2, 2}, {4, 3}}; +pgvector::SparseVector vec(map, 6); ``` Note: Indices start at 0 diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index 4996efc..c1bccd5 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -7,17 +7,17 @@ using pgvector::HalfVector; static void test_constructor_vector() { - auto vec = HalfVector(std::vector{1, 2, 3}); + HalfVector vec(std::vector{1, 2, 3}); assert_equal(vec.dimensions(), 3u); } static void test_constructor_span() { - auto vec = HalfVector(std::span({1, 2, 3})); + HalfVector vec(std::span{{1, 2, 3}}); assert_equal(vec.dimensions(), 3u); } static void test_as_vector() { - auto vec = HalfVector({1, 2, 3}); + HalfVector vec({1, 2, 3}); assert_equal(vec.as_vector() == std::vector{1, 2, 3}, true); } diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index cb91939..02bca62 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -32,8 +32,8 @@ void test_vector(pqxx::connection &conn) { before_each(conn); pqxx::nontransaction tx(conn); - auto embedding = pgvector::Vector({1, 2, 3}); - auto embedding2 = pgvector::Vector({4, 5, 6}); + pgvector::Vector embedding({1, 2, 3}); + pgvector::Vector embedding2({4, 5, 6}); tx.exec("INSERT INTO items (embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY embedding <-> $1", {embedding2}); @@ -47,8 +47,8 @@ void test_halfvec(pqxx::connection &conn) { before_each(conn); pqxx::nontransaction tx(conn); - auto embedding = pgvector::HalfVector({1, 2, 3}); - auto embedding2 = pgvector::HalfVector({4, 5, 6}); + pgvector::HalfVector embedding({1, 2, 3}); + pgvector::HalfVector embedding2({4, 5, 6}); tx.exec("INSERT INTO items (half_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT half_embedding FROM items ORDER BY half_embedding <-> $1", {embedding2}); @@ -62,8 +62,8 @@ void test_bit(pqxx::connection &conn) { before_each(conn); pqxx::nontransaction tx(conn); - std::string embedding = "101"; - std::string embedding2 = "111"; + std::string embedding{"101"}; + std::string embedding2{"111"}; tx.exec("INSERT INTO items (binary_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT binary_embedding FROM items ORDER BY binary_embedding <~> $1", pqxx::params{embedding2}); @@ -77,8 +77,8 @@ void test_sparsevec(pqxx::connection &conn) { before_each(conn); pqxx::nontransaction tx(conn); - auto embedding = pgvector::SparseVector({1, 2, 3}); - auto embedding2 = pgvector::SparseVector({4, 5, 6}); + pgvector::SparseVector embedding({1, 2, 3}); + pgvector::SparseVector embedding2({4, 5, 6}); tx.exec("INSERT INTO items (sparse_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT sparse_embedding FROM items ORDER BY sparse_embedding <-> $1", {embedding2}); @@ -93,7 +93,7 @@ void test_sparsevec_nnz(pqxx::connection &conn) { pqxx::nontransaction tx(conn); std::vector vec(16001, 1); - auto embedding = pgvector::SparseVector(vec); + pgvector::SparseVector embedding(vec); assert_exception([&] { tx.exec("INSERT INTO items (sparse_embedding) VALUES ($1)", {embedding}); }, "sparsevec cannot have more than 16000 dimensions"); @@ -103,7 +103,7 @@ void test_stream(pqxx::connection &conn) { before_each(conn); pqxx::nontransaction tx(conn); - auto embedding = pgvector::Vector({1, 2, 3}); + pgvector::Vector embedding({1, 2, 3}); tx.exec("INSERT INTO items (embedding) VALUES ($1)", {embedding}); int count = 0; for (auto [id, embedding2] : tx.stream("SELECT id, embedding FROM items WHERE embedding IS NOT NULL")) { @@ -117,7 +117,7 @@ void test_stream_to(pqxx::connection &conn) { before_each(conn); pqxx::nontransaction tx(conn); - auto stream = pqxx::stream_to::table(tx, {"items"}, {"embedding"}); + pqxx::stream_to stream = pqxx::stream_to::table(tx, {"items"}, {"embedding"}); stream.write_values(pgvector::Vector({1, 2, 3})); stream.write_values(pgvector::Vector({4, 5, 6})); stream.complete(); @@ -130,7 +130,7 @@ void test_precision(pqxx::connection &conn) { before_each(conn); pqxx::nontransaction tx(conn); - auto embedding = pgvector::Vector({1.23456789, 0, 0}); + pgvector::Vector embedding({1.23456789, 0, 0}); tx.exec("INSERT INTO items (embedding) VALUES ($1)", {embedding}); tx.exec("SET extra_float_digits = 3"); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY id DESC LIMIT 1"); diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index 166a4e6..a6f890b 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -8,20 +8,20 @@ using pgvector::SparseVector; static void test_constructor_vector() { - auto vec = SparseVector(std::vector{1, 0, 2, 0, 3, 0}); + SparseVector vec(std::vector{1, 0, 2, 0, 3, 0}); assert_equal(vec.dimensions(), 6); assert_equal(vec.indices() == std::vector{0, 2, 4}, true); assert_equal(vec.values() == std::vector{1, 2, 3}, true); } static void test_constructor_span() { - auto vec = SparseVector(std::span({1, 0, 2, 0, 3, 0})); + SparseVector vec(std::span{{1, 0, 2, 0, 3, 0}}); assert_equal(vec.dimensions(), 6); } static void test_constructor_map() { - std::unordered_map map = {{2, 2}, {4, 3}, {3, 0}, {0, 1}}; - auto vec = SparseVector(map, 6); + std::unordered_map map{{2, 2}, {4, 3}, {3, 0}, {0, 1}}; + SparseVector vec(map, 6); assert_equal(vec.dimensions(), 6); assert_equal(vec.indices() == std::vector{0, 2, 4}, true); assert_equal(vec.values() == std::vector{1, 2, 3}, true); diff --git a/test/vector_test.cpp b/test/vector_test.cpp index 37d5e74..75c2fb7 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -7,17 +7,17 @@ using pgvector::Vector; static void test_constructor_vector() { - auto vec = Vector(std::vector{1, 2, 3}); + Vector vec(std::vector{1, 2, 3}); assert_equal(vec.dimensions(), 3u); } static void test_constructor_span() { - auto vec = Vector(std::span({1, 2, 3})); + Vector vec(std::span{{1, 2, 3}}); assert_equal(vec.dimensions(), 3u); } static void test_as_vector() { - auto vec = Vector({1, 2, 3}); + Vector vec({1, 2, 3}); assert_equal(vec.as_vector() == std::vector{1, 2, 3}, true); } From 24c3844c433c07c22644c6b280894a656e2368d2 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 15:51:00 -0800 Subject: [PATCH 104/163] Updated style [skip ci] --- README.md | 10 ++--- include/pgvector/pqxx.hpp | 22 +++++------ include/pgvector/sparsevec.hpp | 2 +- test/halfvec_test.cpp | 6 +-- test/pqxx_test.cpp | 68 +++++++++++++++++----------------- test/sparsevec_test.cpp | 6 +-- test/vector_test.cpp | 6 +-- 7 files changed, 60 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 5fa5616..e6ecc57 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ tx.exec("CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(3))"); Insert a vector ```cpp -pgvector::Vector embedding({1, 2, 3}); +pgvector::Vector embedding{{1, 2, 3}}; tx.exec("INSERT INTO items (embedding) VALUES ($1)", {embedding}); ``` @@ -87,7 +87,7 @@ Use `std::optional` if the value could be `NULL` Create a vector from a `std::vector` ```cpp -pgvector::Vector vec(std::vector{1, 2, 3}); +pgvector::Vector vec{std::vector{1, 2, 3}}; ``` Convert to a `std::vector` @@ -101,7 +101,7 @@ auto float_vec = static_cast>(vec); Create a half vector from a `std::vector` ```cpp -pgvector::HalfVector vec(std::vector{1, 2, 3}); +pgvector::HalfVector vec{std::vector{1, 2, 3}}; ``` Convert to a `std::vector` @@ -115,14 +115,14 @@ auto float_vec = static_cast>(vec); Create a sparse vector from a `std::vector` ```cpp -pgvector::SparseVector vec({1, 0, 2, 0, 3, 0}); +pgvector::SparseVector vec{{1, 0, 2, 0, 3, 0}}; ``` Or a map of non-zero elements ```cpp std::unordered_map map{{0, 1}, {2, 2}, {4, 3}}; -pgvector::SparseVector vec(map, 6); +pgvector::SparseVector vec{map, 6}; ``` Note: Indices start at 0 diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index e9deddd..5bff35d 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -41,11 +41,11 @@ template <> struct string_traits { } values.push_back(string_traits::from_string(inner.substr(start), c)); } - return pgvector::Vector(std::move(values)); + return pgvector::Vector{std::move(values)}; } static std::string_view to_buf(std::span buf, const pgvector::Vector& value, ctx c = {}) { - auto& values = value.as_vector(); + const std::vector& values = value.as_vector(); // important! size_buffer cannot throw an exception on overflow // so perform this check before writing any data @@ -69,7 +69,7 @@ template <> struct string_traits { } static size_t size_buffer(const pgvector::Vector& value) noexcept { - auto& values = value.as_vector(); + const std::vector& values = value.as_vector(); // cannot throw an exception here on overflow // so throw in into_buf @@ -105,11 +105,11 @@ template <> struct string_traits { } values.push_back(static_cast(string_traits::from_string(inner.substr(start), c))); } - return pgvector::HalfVector(std::move(values)); + return pgvector::HalfVector{std::move(values)}; } static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { - auto& values = value.as_vector(); + const std::vector& values = value.as_vector(); // important! size_buffer cannot throw an exception on overflow // so perform this check before writing any data @@ -133,7 +133,7 @@ template <> struct string_traits { } static size_t size_buffer(const pgvector::HalfVector& value) noexcept { - auto& values = value.as_vector(); + const std::vector& values = value.as_vector(); // cannot throw an exception here on overflow // so throw in into_buf @@ -199,13 +199,13 @@ template <> struct string_traits { add_element(inner.substr(start)); } - return pgvector::SparseVector(dimensions, std::move(indices), std::move(values)); + return pgvector::SparseVector{dimensions, std::move(indices), std::move(values)}; } static std::string_view to_buf(std::span buf, const pgvector::SparseVector& value, ctx c = {}) { int dimensions = value.dimensions(); - auto& indices = value.indices(); - auto& values = value.values(); + const std::vector& indices = value.indices(); + const std::vector& values = value.values(); size_t nnz = indices.size(); // important! size_buffer cannot throw an exception on overflow @@ -235,8 +235,8 @@ template <> struct string_traits { static size_t size_buffer(const pgvector::SparseVector& value) noexcept { int dimensions = value.dimensions(); - auto& indices = value.indices(); - auto& values = value.values(); + const std::vector& indices = value.indices(); + const std::vector& values = value.values(); size_t nnz = indices.size(); // cannot throw an exception here on overflow diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index d656636..f899501 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -59,7 +59,7 @@ class SparseVector { } dimensions_ = dimensions; - for (const auto [i, v] : map) { + for (const auto& [i, v] : map) { if (i < 0 || i >= dimensions) { throw std::invalid_argument("sparsevec index out of bounds"); } diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index c1bccd5..d2f5fa4 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -7,17 +7,17 @@ using pgvector::HalfVector; static void test_constructor_vector() { - HalfVector vec(std::vector{1, 2, 3}); + HalfVector vec{std::vector{1, 2, 3}}; assert_equal(vec.dimensions(), 3u); } static void test_constructor_span() { - HalfVector vec(std::span{{1, 2, 3}}); + HalfVector vec{std::span{{1, 2, 3}}}; assert_equal(vec.dimensions(), 3u); } static void test_as_vector() { - HalfVector vec({1, 2, 3}); + HalfVector vec{{1, 2, 3}}; assert_equal(vec.as_vector() == std::vector{1, 2, 3}, true); } diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 02bca62..4d27dfd 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -9,14 +9,14 @@ #include "helper.hpp" void setup(pqxx::connection &conn) { - pqxx::nontransaction tx(conn); + pqxx::nontransaction tx{conn}; tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); tx.exec("DROP TABLE IF EXISTS items"); tx.exec("CREATE TABLE items (id serial PRIMARY KEY, embedding vector(3), half_embedding halfvec(3), binary_embedding bit(3), sparse_embedding sparsevec(3))"); } void before_each(pqxx::connection &conn) { - pqxx::nontransaction tx(conn); + pqxx::nontransaction tx{conn}; tx.exec("TRUNCATE items"); } @@ -31,9 +31,9 @@ std::optional float_error([[maybe_unused]] std::string_view me void test_vector(pqxx::connection &conn) { before_each(conn); - pqxx::nontransaction tx(conn); - pgvector::Vector embedding({1, 2, 3}); - pgvector::Vector embedding2({4, 5, 6}); + pqxx::nontransaction tx{conn}; + pgvector::Vector embedding{{1, 2, 3}}; + pgvector::Vector embedding2{{4, 5, 6}}; tx.exec("INSERT INTO items (embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY embedding <-> $1", {embedding2}); @@ -46,9 +46,9 @@ void test_vector(pqxx::connection &conn) { void test_halfvec(pqxx::connection &conn) { before_each(conn); - pqxx::nontransaction tx(conn); - pgvector::HalfVector embedding({1, 2, 3}); - pgvector::HalfVector embedding2({4, 5, 6}); + pqxx::nontransaction tx{conn}; + pgvector::HalfVector embedding{{1, 2, 3}}; + pgvector::HalfVector embedding2{{4, 5, 6}}; tx.exec("INSERT INTO items (half_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT half_embedding FROM items ORDER BY half_embedding <-> $1", {embedding2}); @@ -61,7 +61,7 @@ void test_halfvec(pqxx::connection &conn) { void test_bit(pqxx::connection &conn) { before_each(conn); - pqxx::nontransaction tx(conn); + pqxx::nontransaction tx{conn}; std::string embedding{"101"}; std::string embedding2{"111"}; tx.exec("INSERT INTO items (binary_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); @@ -76,9 +76,9 @@ void test_bit(pqxx::connection &conn) { void test_sparsevec(pqxx::connection &conn) { before_each(conn); - pqxx::nontransaction tx(conn); - pgvector::SparseVector embedding({1, 2, 3}); - pgvector::SparseVector embedding2({4, 5, 6}); + pqxx::nontransaction tx{conn}; + pgvector::SparseVector embedding{{1, 2, 3}}; + pgvector::SparseVector embedding2{{4, 5, 6}}; tx.exec("INSERT INTO items (sparse_embedding) VALUES ($1), ($2), ($3)", {embedding, embedding2, std::nullopt}); pqxx::result res = tx.exec("SELECT sparse_embedding FROM items ORDER BY sparse_embedding <-> $1", {embedding2}); @@ -91,9 +91,9 @@ void test_sparsevec(pqxx::connection &conn) { void test_sparsevec_nnz(pqxx::connection &conn) { before_each(conn); - pqxx::nontransaction tx(conn); + pqxx::nontransaction tx{conn}; std::vector vec(16001, 1); - pgvector::SparseVector embedding(vec); + pgvector::SparseVector embedding{vec}; assert_exception([&] { tx.exec("INSERT INTO items (sparse_embedding) VALUES ($1)", {embedding}); }, "sparsevec cannot have more than 16000 dimensions"); @@ -102,7 +102,7 @@ void test_sparsevec_nnz(pqxx::connection &conn) { void test_stream(pqxx::connection &conn) { before_each(conn); - pqxx::nontransaction tx(conn); + pqxx::nontransaction tx{conn}; pgvector::Vector embedding({1, 2, 3}); tx.exec("INSERT INTO items (embedding) VALUES ($1)", {embedding}); int count = 0; @@ -116,10 +116,10 @@ void test_stream(pqxx::connection &conn) { void test_stream_to(pqxx::connection &conn) { before_each(conn); - pqxx::nontransaction tx(conn); + pqxx::nontransaction tx{conn}; pqxx::stream_to stream = pqxx::stream_to::table(tx, {"items"}, {"embedding"}); - stream.write_values(pgvector::Vector({1, 2, 3})); - stream.write_values(pgvector::Vector({4, 5, 6})); + stream.write_values(pgvector::Vector{{1, 2, 3}}); + stream.write_values(pgvector::Vector{{4, 5, 6}}); stream.complete(); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY id"); assert_equal(res[0][0].as(), "[1,2,3]"); @@ -129,8 +129,8 @@ void test_stream_to(pqxx::connection &conn) { void test_precision(pqxx::connection &conn) { before_each(conn); - pqxx::nontransaction tx(conn); - pgvector::Vector embedding({1.23456789, 0, 0}); + pqxx::nontransaction tx{conn}; + pgvector::Vector embedding{{1.23456789, 0, 0}}; tx.exec("INSERT INTO items (embedding) VALUES ($1)", {embedding}); tx.exec("SET extra_float_digits = 3"); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY id DESC LIMIT 1"); @@ -138,8 +138,8 @@ void test_precision(pqxx::connection &conn) { } void test_vector_to_string() { - assert_equal(pqxx::to_string(pgvector::Vector({1, 2, 3})), "[1,2,3]"); - assert_equal(pqxx::to_string(pgvector::Vector({-1.234567890123})), "[-1.2345679]"); + assert_equal(pqxx::to_string(pgvector::Vector{{1, 2, 3}}), "[1,2,3]"); + assert_equal(pqxx::to_string(pgvector::Vector{{-1.234567890123}}), "[-1.2345679]"); assert_exception([] { pqxx::to_string(pgvector::Vector(std::vector(16001))); @@ -147,8 +147,8 @@ void test_vector_to_string() { } void test_vector_from_string() { - assert_equal(pqxx::from_string("[1,2,3]"), pgvector::Vector({1, 2, 3})); - assert_equal(pqxx::from_string("[]"), pgvector::Vector(std::vector{})); + assert_equal(pqxx::from_string("[1,2,3]"), pgvector::Vector{{1, 2, 3}}); + assert_equal(pqxx::from_string("[]"), pgvector::Vector{std::vector{}}); assert_exception([] { auto _ = pqxx::from_string(""); @@ -178,9 +178,9 @@ void test_vector_from_string() { void test_halfvec_to_string() { assert_equal(pqxx::to_string(pgvector::HalfVector({1, 2, 3})), "[1,2,3]"); #if __STDCPP_FLOAT16_T__ || defined(__FLT16_MAX__) - assert_equal(pqxx::to_string(pgvector::HalfVector({static_cast(-1.234567890123)})), "[-1.234375]"); + assert_equal(pqxx::to_string(pgvector::HalfVector{{static_cast(-1.234567890123)}}), "[-1.234375]"); #else - assert_equal(pqxx::to_string(pgvector::HalfVector({-1.234567890123})), "[-1.2345679]"); + assert_equal(pqxx::to_string(pgvector::HalfVector{{-1.234567890123}}), "[-1.2345679]"); #endif assert_exception([] { @@ -189,8 +189,8 @@ void test_halfvec_to_string() { } void test_halfvec_from_string() { - assert_equal(pqxx::from_string("[1,2,3]"), pgvector::HalfVector({1, 2, 3})); - assert_equal(pqxx::from_string("[]"), pgvector::HalfVector(std::vector{})); + assert_equal(pqxx::from_string("[1,2,3]"), pgvector::HalfVector{{1, 2, 3}}); + assert_equal(pqxx::from_string("[]"), pgvector::HalfVector{std::vector{}}); assert_exception([] { auto _ = pqxx::from_string(""); @@ -218,9 +218,9 @@ void test_halfvec_from_string() { } void test_sparsevec_to_string() { - assert_equal(pqxx::to_string(pgvector::SparseVector({1, 0, 2, 0, 3, 0})), "{1:1,3:2,5:3}/6"); - std::unordered_map map = {{999999999, -1.234567890123}}; - assert_equal(pqxx::to_string(pgvector::SparseVector(map, 1000000000)), "{1000000000:-1.2345679}/1000000000"); + assert_equal(pqxx::to_string(pgvector::SparseVector{{1, 0, 2, 0, 3, 0}}), "{1:1,3:2,5:3}/6"); + std::unordered_map map{{999999999, -1.234567890123}}; + assert_equal(pqxx::to_string(pgvector::SparseVector{map, 1000000000}), "{1000000000:-1.2345679}/1000000000"); assert_exception([] { pqxx::to_string(pgvector::SparseVector(std::vector(16001, 1))); @@ -228,8 +228,8 @@ void test_sparsevec_to_string() { } void test_sparsevec_from_string() { - assert_equal(pqxx::from_string("{1:1,3:2,5:3}/6"), pgvector::SparseVector({1, 0, 2, 0, 3, 0})); - assert_equal(pqxx::from_string("{}/6"), pgvector::SparseVector({0, 0, 0, 0, 0, 0})); + assert_equal(pqxx::from_string("{1:1,3:2,5:3}/6"), pgvector::SparseVector{{1, 0, 2, 0, 3, 0}}); + assert_equal(pqxx::from_string("{}/6"), pgvector::SparseVector{{0, 0, 0, 0, 0, 0}}); assert_exception([] { auto _ = pqxx::from_string(""); @@ -281,7 +281,7 @@ void test_sparsevec_from_string() { } void test_pqxx() { - pqxx::connection conn("dbname=pgvector_cpp_test"); + pqxx::connection conn{"dbname=pgvector_cpp_test"}; setup(conn); test_vector(conn); diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index a6f890b..b9d752c 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -8,20 +8,20 @@ using pgvector::SparseVector; static void test_constructor_vector() { - SparseVector vec(std::vector{1, 0, 2, 0, 3, 0}); + SparseVector vec{std::vector{1, 0, 2, 0, 3, 0}}; assert_equal(vec.dimensions(), 6); assert_equal(vec.indices() == std::vector{0, 2, 4}, true); assert_equal(vec.values() == std::vector{1, 2, 3}, true); } static void test_constructor_span() { - SparseVector vec(std::span{{1, 0, 2, 0, 3, 0}}); + SparseVector vec{std::span{{1, 0, 2, 0, 3, 0}}}; assert_equal(vec.dimensions(), 6); } static void test_constructor_map() { std::unordered_map map{{2, 2}, {4, 3}, {3, 0}, {0, 1}}; - SparseVector vec(map, 6); + SparseVector vec{map, 6}; assert_equal(vec.dimensions(), 6); assert_equal(vec.indices() == std::vector{0, 2, 4}, true); assert_equal(vec.values() == std::vector{1, 2, 3}, true); diff --git a/test/vector_test.cpp b/test/vector_test.cpp index 75c2fb7..1a7267d 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -7,17 +7,17 @@ using pgvector::Vector; static void test_constructor_vector() { - Vector vec(std::vector{1, 2, 3}); + Vector vec{std::vector{1, 2, 3}}; assert_equal(vec.dimensions(), 3u); } static void test_constructor_span() { - Vector vec(std::span{{1, 2, 3}}); + Vector vec{std::span{{1, 2, 3}}}; assert_equal(vec.dimensions(), 3u); } static void test_as_vector() { - Vector vec({1, 2, 3}); + Vector vec{{1, 2, 3}}; assert_equal(vec.as_vector() == std::vector{1, 2, 3}, true); } From 3a9ace92a8d810c3f618d23105e6f1c7fbff20a8 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 15:55:43 -0800 Subject: [PATCH 105/163] Updated readme [skip ci] --- README.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e6ecc57..046b809 100644 --- a/README.md +++ b/README.md @@ -84,38 +84,56 @@ Use `std::optional` if the value could be `NULL` ### Vectors -Create a vector from a `std::vector` +Create a vector from a `std::vector` ```cpp pgvector::Vector vec{std::vector{1, 2, 3}}; ``` -Convert to a `std::vector` +Or a span ```cpp -auto float_vec = static_cast>(vec); +pgvector::Vector vec{std::span{{1, 2, 3}}}; +``` + +Convert to a `std::vector` + +```cpp +std::vector float_vec = static_cast>(vec); ``` ### Half Vectors -Create a half vector from a `std::vector` +Create a half vector from a `std::vector` ```cpp pgvector::HalfVector vec{std::vector{1, 2, 3}}; ``` -Convert to a `std::vector` +Or a span + +```cpp +pgvector::HalfVector vec{std::span{{1, 2, 3}}}; +``` + +Convert to a `std::vector` ```cpp -auto float_vec = static_cast>(vec); +std::vector float_vec = static_cast>(vec); ``` ### Sparse Vectors -Create a sparse vector from a `std::vector` +Create a sparse vector from a `std::vector` + +```cpp +pgvector::SparseVector vec{std::vector{1, 0, 2, 0, 3, 0}}; +``` + +Or a span ```cpp -pgvector::SparseVector vec{{1, 0, 2, 0, 3, 0}}; +pgvector::SparseVector vec{std::span{{1, 0, 2, 0, 3, 0}}}; ``` Or a map of non-zero elements From e07ca582186a25c2f804b11996150b82477e3d54 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 16:23:58 -0800 Subject: [PATCH 106/163] Updated style [skip ci] --- test/pqxx_test.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 4d27dfd..e49b820 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -142,7 +142,7 @@ void test_vector_to_string() { assert_equal(pqxx::to_string(pgvector::Vector{{-1.234567890123}}), "[-1.2345679]"); assert_exception([] { - pqxx::to_string(pgvector::Vector(std::vector(16001))); + pqxx::to_string(pgvector::Vector{std::vector(16001)}); }, "vector cannot have more than 16000 dimensions"); } @@ -176,7 +176,7 @@ void test_vector_from_string() { } void test_halfvec_to_string() { - assert_equal(pqxx::to_string(pgvector::HalfVector({1, 2, 3})), "[1,2,3]"); + assert_equal(pqxx::to_string(pgvector::HalfVector{{1, 2, 3}}), "[1,2,3]"); #if __STDCPP_FLOAT16_T__ || defined(__FLT16_MAX__) assert_equal(pqxx::to_string(pgvector::HalfVector{{static_cast(-1.234567890123)}}), "[-1.234375]"); #else @@ -184,7 +184,7 @@ void test_halfvec_to_string() { #endif assert_exception([] { - pqxx::to_string(pgvector::HalfVector(std::vector(16001))); + pqxx::to_string(pgvector::HalfVector{std::vector(16001)}); }, "halfvec cannot have more than 16000 dimensions"); } @@ -223,7 +223,7 @@ void test_sparsevec_to_string() { assert_equal(pqxx::to_string(pgvector::SparseVector{map, 1000000000}), "{1000000000:-1.2345679}/1000000000"); assert_exception([] { - pqxx::to_string(pgvector::SparseVector(std::vector(16001, 1))); + pqxx::to_string(pgvector::SparseVector{std::vector(16001, 1)}); }, "sparsevec cannot have more than 16000 dimensions"); } From 6042e9f02a3aebcf4bef3e90e2b54265f338fae2 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 16:29:44 -0800 Subject: [PATCH 107/163] Updated style [skip ci] --- test/pqxx_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index e49b820..71728ec 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -106,7 +106,7 @@ void test_stream(pqxx::connection &conn) { pgvector::Vector embedding({1, 2, 3}); tx.exec("INSERT INTO items (embedding) VALUES ($1)", {embedding}); int count = 0; - for (auto [id, embedding2] : tx.stream("SELECT id, embedding FROM items WHERE embedding IS NOT NULL")) { + for (const auto& [id, embedding2] : tx.stream("SELECT id, embedding FROM items WHERE embedding IS NOT NULL")) { assert_equal(embedding2, embedding); count++; } From 2bc3fbb0cc914f74e40cc55f903c8b1e1b626bd0 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 16:32:41 -0800 Subject: [PATCH 108/163] Updated style for examples [skip ci] --- examples/citus/example.cpp | 30 +++++++++++++++--------------- examples/cohere/example.cpp | 18 +++++++++--------- examples/disco/example.cpp | 20 ++++++++++---------- examples/hybrid/example.cpp | 28 ++++++++++++++-------------- examples/loading/example.cpp | 10 +++++----- examples/openai/example.cpp | 22 +++++++++++----------- examples/rdkit/example.cpp | 18 +++++++++--------- examples/sparse/example.cpp | 28 +++++++++++++--------------- 8 files changed, 86 insertions(+), 88 deletions(-) diff --git a/examples/citus/example.cpp b/examples/citus/example.cpp index 38eaacb..1222cb8 100644 --- a/examples/citus/example.cpp +++ b/examples/citus/example.cpp @@ -8,8 +8,8 @@ std::vector> random_embeddings(int rows, int dimensions) { std::random_device rd; - std::mt19937_64 prng(rd()); - std::uniform_real_distribution dist(0, 1); + std::mt19937_64 prng{rd()}; + std::uniform_real_distribution dist{0, 1}; std::vector> embeddings; embeddings.reserve(rows); @@ -26,8 +26,8 @@ std::vector> random_embeddings(int rows, int dimensions) { std::vector random_categories(int rows) { std::random_device rd; - std::mt19937_64 prng(rd()); - std::uniform_int_distribution dist(1, 100); + std::mt19937_64 prng{rd()}; + std::uniform_int_distribution dist{1, 100}; std::vector categories; categories.reserve(rows); @@ -41,13 +41,13 @@ int main() { // generate random data int rows = 100000; int dimensions = 128; - auto embeddings = random_embeddings(rows, dimensions); - auto categories = random_categories(rows); - auto queries = random_embeddings(10, dimensions); + std::vector> embeddings = random_embeddings(rows, dimensions); + std::vector categories = random_categories(rows); + std::vector> queries = random_embeddings(10, dimensions); // enable extensions - pqxx::connection conn("dbname=pgvector_citus"); - pqxx::nontransaction tx(conn); + pqxx::connection conn{"dbname=pgvector_citus"}; + pqxx::nontransaction tx{conn}; tx.exec("CREATE EXTENSION IF NOT EXISTS citus"); tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); @@ -61,8 +61,8 @@ int main() { conn.close(); // reconnect for updated GUC variables to take effect - pqxx::connection conn2("dbname=pgvector_citus"); - pqxx::nontransaction tx2(conn2); + pqxx::connection conn2{"dbname=pgvector_citus"}; + pqxx::nontransaction tx2{conn2}; std::cout << "Creating distributed table" << std::endl; tx2.exec("DROP TABLE IF EXISTS items"); @@ -72,9 +72,9 @@ int main() { // libpqxx does not support binary COPY std::cout << "Loading data in parallel" << std::endl; - auto stream = pqxx::stream_to::table(tx2, {"items"}, {"embedding", "category_id"}); + pqxx::stream_to stream = pqxx::stream_to::table(tx2, {"items"}, {"embedding", "category_id"}); for (size_t i = 0; i < embeddings.size(); i++) { - stream.write_values(pgvector::Vector(embeddings[i]), categories[i]); + stream.write_values(pgvector::Vector{embeddings[i]}, categories[i]); } stream.complete(); @@ -82,10 +82,10 @@ int main() { tx2.exec("CREATE INDEX ON items USING hnsw (embedding vector_l2_ops)"); std::cout << "Running distributed queries" << std::endl; - for (auto& query : queries) { + for (const auto& query : queries) { pqxx::result result = tx2.exec( "SELECT id FROM items ORDER BY embedding <-> $1 LIMIT 10", - pqxx::params{pgvector::Vector(query)} + pqxx::params{pgvector::Vector{query}} ); for (const auto& row : result) { std::cout << row[0].as() << " "; diff --git a/examples/cohere/example.cpp b/examples/cohere/example.cpp index 6660f45..72041d7 100644 --- a/examples/cohere/example.cpp +++ b/examples/cohere/example.cpp @@ -13,8 +13,8 @@ using json = nlohmann::json; // https://docs.cohere.com/reference/embed std::vector embed(const std::vector& texts, const std::string& input_type, char *api_key) { - std::string url = "https://api.cohere.com/v2/embed"; - json data = { + std::string url{"https://api.cohere.com/v2/embed"}; + json data{ {"texts", texts}, {"model", "embed-v4.0"}, {"input_type", input_type}, @@ -33,7 +33,7 @@ std::vector embed(const std::vector& texts, const std: json response = json::parse(r.text); std::vector embeddings; - for (auto& v : response["embeddings"]["ubinary"]) { + for (const auto& v : response["embeddings"]["ubinary"]) { std::stringstream buf; for (uint8_t c : v) { std::bitset<8> b{c}; @@ -51,25 +51,25 @@ int main() { return 1; } - pqxx::connection conn("dbname=pgvector_example"); + pqxx::connection conn{"dbname=pgvector_example"}; - pqxx::nontransaction tx(conn); + pqxx::nontransaction tx{conn}; tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); tx.exec("DROP TABLE IF EXISTS documents"); tx.exec("CREATE TABLE documents (id bigserial PRIMARY KEY, content text, embedding bit(1536))"); - std::vector input = { + std::vector input{ "The dog is barking", "The cat is purring", "The bear is growling" }; - auto embeddings = embed(input, "search_document", api_key); + std::vector embeddings = embed(input, "search_document", api_key); for (size_t i = 0; i < input.size(); i++) { tx.exec("INSERT INTO documents (content, embedding) VALUES ($1, $2)", pqxx::params{input[i], embeddings[i]}); } - std::string query = "forest"; - auto query_embedding = embed({query}, "search_query", api_key)[0]; + std::string query{"forest"}; + std::string query_embedding = embed({query}, "search_query", api_key)[0]; pqxx::result result = tx.exec("SELECT content FROM documents ORDER BY embedding <~> $1 LIMIT 5", pqxx::params{query_embedding}); for (const auto& row : result) { std::cout << row[0].as() << std::endl; diff --git a/examples/disco/example.cpp b/examples/disco/example.cpp index 0e1985c..0e22fe2 100644 --- a/examples/disco/example.cpp +++ b/examples/disco/example.cpp @@ -16,7 +16,7 @@ using disco::Recommender; std::string convert_to_utf8(const std::string& str) { std::stringstream buf; - for (auto &v : str) { + for (const auto& v : str) { if (v >= 0) { buf << v; } else { @@ -40,7 +40,7 @@ Dataset load_movielens(const std::string& path) { } // read ratings and create dataset - auto data = Dataset(); + Dataset data; std::ifstream ratings_file(path + "/u.data"); assert(ratings_file.is_open()); while (std::getline(ratings_file, line)) { @@ -65,29 +65,29 @@ int main() { return 1; } - pqxx::connection conn("dbname=pgvector_example"); + pqxx::connection conn{"dbname=pgvector_example"}; - pqxx::nontransaction tx(conn); + pqxx::nontransaction tx{conn}; tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); tx.exec("DROP TABLE IF EXISTS users"); tx.exec("DROP TABLE IF EXISTS movies"); tx.exec("CREATE TABLE users (id integer PRIMARY KEY, factors vector(20))"); tx.exec("CREATE TABLE movies (name text PRIMARY KEY, factors vector(20))"); - auto data = load_movielens(movielens_path); + Dataset data = load_movielens(movielens_path); auto recommender = Recommender::fit_explicit(data, { .factors = 20 }); - for (auto& user_id : recommender.user_ids()) { - auto factors = pgvector::Vector(*recommender.user_factors(user_id)); + for (const auto& user_id : recommender.user_ids()) { + pgvector::Vector factors{*recommender.user_factors(user_id)}; tx.exec("INSERT INTO users (id, factors) VALUES ($1, $2)", pqxx::params{user_id, factors}); } - for (auto& item_id : recommender.item_ids()) { - auto factors = pgvector::Vector(*recommender.item_factors(item_id)); + for (const auto& item_id : recommender.item_ids()) { + pgvector::Vector factors{*recommender.item_factors(item_id)}; tx.exec("INSERT INTO movies (name, factors) VALUES ($1, $2)", pqxx::params{item_id, factors}); } - std::string movie = "Star Wars (1977)"; + std::string movie{"Star Wars (1977)"}; std::cout << "Item-based recommendations for " << movie << std::endl; pqxx::result result = tx.exec("SELECT name FROM movies WHERE name != $1 ORDER BY factors <=> (SELECT factors FROM movies WHERE name = $1) LIMIT 5", pqxx::params{movie}); for (const auto& row : result) { diff --git a/examples/hybrid/example.cpp b/examples/hybrid/example.cpp index c9bb3a6..86206fc 100644 --- a/examples/hybrid/example.cpp +++ b/examples/hybrid/example.cpp @@ -18,12 +18,12 @@ std::vector> embed(const std::vector& texts, con // https://huggingface.co/nomic-ai/nomic-embed-text-v1.5 std::vector input; input.reserve(texts.size()); - for (auto& v : texts) { + for (const auto& v : texts) { input.push_back(taskType + ": " + v); } - std::string url = "http://localhost:8080/v1/embeddings"; - json data = { + std::string url{"http://localhost:8080/v1/embeddings"}; + json data{ {"input", input} }; @@ -38,33 +38,33 @@ std::vector> embed(const std::vector& texts, con json response = json::parse(r.text); std::vector> embeddings; - for (auto& v : response["data"]) { + for (const auto& v : response["data"]) { embeddings.emplace_back(v["embedding"]); } return embeddings; } int main() { - pqxx::connection conn("dbname=pgvector_example"); + pqxx::connection conn{"dbname=pgvector_example"}; - pqxx::nontransaction tx(conn); + pqxx::nontransaction tx{conn}; tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); tx.exec("DROP TABLE IF EXISTS documents"); tx.exec("CREATE TABLE documents (id bigserial PRIMARY KEY, content text, embedding vector(768))"); tx.exec("CREATE INDEX ON documents USING GIN (to_tsvector('english', content))"); - std::vector input = { + std::vector input{ "The dog is barking", "The cat is purring", "The bear is growling" }; - auto embeddings = embed(input, "search_document"); + std::vector> embeddings = embed(input, "search_document"); for (size_t i = 0; i < input.size(); i++) { - tx.exec("INSERT INTO documents (content, embedding) VALUES ($1, $2)", pqxx::params{input[i], pgvector::Vector(embeddings[i])}); + tx.exec("INSERT INTO documents (content, embedding) VALUES ($1, $2)", pqxx::params{input[i], pgvector::Vector{embeddings[i]}}); } - std::string sql = R"( + std::string sql{R"( WITH semantic_search AS ( SELECT id, RANK () OVER (ORDER BY embedding <=> $2) AS rank FROM documents @@ -86,11 +86,11 @@ int main() { FULL OUTER JOIN keyword_search ON semantic_search.id = keyword_search.id ORDER BY score DESC LIMIT 5 - )"; - std::string query = "growling bear"; - auto query_embedding = embed({query}, "search_query")[0]; + )"}; + std::string query{"growling bear"}; + std::vector query_embedding = embed({query}, "search_query")[0]; double k = 60; - pqxx::result result = tx.exec(sql, pqxx::params{query, pgvector::Vector(query_embedding), k}); + pqxx::result result = tx.exec(sql, pqxx::params{query, pgvector::Vector{query_embedding}, k}); for (const auto& row : result) { std::cout << "document: " << row[0].as() << ", RRF score: " << row[1].as() << std::endl; } diff --git a/examples/loading/example.cpp b/examples/loading/example.cpp index 9192522..54918fc 100644 --- a/examples/loading/example.cpp +++ b/examples/loading/example.cpp @@ -12,7 +12,7 @@ int main() { std::vector> embeddings; embeddings.reserve(rows); std::mt19937_64 prng; - std::uniform_real_distribution dist(0, 1); + std::uniform_real_distribution dist{0, 1}; for (int i = 0; i < rows; i++) { std::vector embedding; embedding.reserve(dimensions); @@ -23,8 +23,8 @@ int main() { } // enable extension - pqxx::connection conn("dbname=pgvector_example"); - pqxx::nontransaction tx(conn); + pqxx::connection conn{"dbname=pgvector_example"}; + pqxx::nontransaction tx{conn}; tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); // create table @@ -34,14 +34,14 @@ int main() { // load data // libpqxx does not support binary COPY std::cout << "Loading " << rows << " rows" << std::endl; - auto stream = pqxx::stream_to::table(tx, {"items"}, {"embedding"}); + pqxx::stream_to stream = pqxx::stream_to::table(tx, {"items"}, {"embedding"}); for (size_t i = 0; i < embeddings.size(); i++) { // show progress if (i % 10000 == 0) { std::cout << '.' << std::flush; } - stream.write_values(pgvector::Vector(embeddings[i])); + stream.write_values(pgvector::Vector{embeddings[i]}); } stream.complete(); std::cout << std::endl << "Success!" << std::endl; diff --git a/examples/openai/example.cpp b/examples/openai/example.cpp index ef0feea..91dc19d 100644 --- a/examples/openai/example.cpp +++ b/examples/openai/example.cpp @@ -12,8 +12,8 @@ using json = nlohmann::json; // https://platform.openai.com/docs/guides/embeddings/how-to-get-embeddings // input can be an array with 2048 elements std::vector> embed(const std::vector& input, char *api_key) { - std::string url = "https://api.openai.com/v1/embeddings"; - json data = { + std::string url{"https://api.openai.com/v1/embeddings"}; + json data{ {"input", input}, {"model", "text-embedding-3-small"} }; @@ -30,7 +30,7 @@ std::vector> embed(const std::vector& input, cha json response = json::parse(r.text); std::vector> embeddings; - for (auto& v : response["data"]) { + for (const auto& v : response["data"]) { embeddings.emplace_back(v["embedding"]); } return embeddings; @@ -43,26 +43,26 @@ int main() { return 1; } - pqxx::connection conn("dbname=pgvector_example"); + pqxx::connection conn{"dbname=pgvector_example"}; - pqxx::nontransaction tx(conn); + pqxx::nontransaction tx{conn}; tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); tx.exec("DROP TABLE IF EXISTS documents"); tx.exec("CREATE TABLE documents (id bigserial PRIMARY KEY, content text, embedding vector(1536))"); - std::vector input = { + std::vector input{ "The dog is barking", "The cat is purring", "The bear is growling" }; - auto embeddings = embed(input, api_key); + std::vector> embeddings = embed(input, api_key); for (size_t i = 0; i < input.size(); i++) { - tx.exec("INSERT INTO documents (content, embedding) VALUES ($1, $2)", pqxx::params{input[i], pgvector::Vector(embeddings[i])}); + tx.exec("INSERT INTO documents (content, embedding) VALUES ($1, $2)", pqxx::params{input[i], pgvector::Vector{embeddings[i]}}); } - std::string query = "forest"; - auto query_embedding = embed({query}, api_key)[0]; - pqxx::result result = tx.exec("SELECT content FROM documents ORDER BY embedding <=> $1 LIMIT 5", pqxx::params{pgvector::Vector(query_embedding)}); + std::string query{"forest"}; + std::vector query_embedding = embed({query}, api_key)[0]; + pqxx::result result = tx.exec("SELECT content FROM documents ORDER BY embedding <=> $1 LIMIT 5", pqxx::params{pgvector::Vector{query_embedding}}); for (const auto& row : result) { std::cout << row[0].as() << std::endl; } diff --git a/examples/rdkit/example.cpp b/examples/rdkit/example.cpp index b09f882..9111763 100644 --- a/examples/rdkit/example.cpp +++ b/examples/rdkit/example.cpp @@ -10,8 +10,8 @@ #include std::string generate_fingerprint(const std::string& molecule) { - std::unique_ptr mol(RDKit::SmilesToMol(molecule)); - std::unique_ptr fp(RDKit::MorganFingerprints::getFingerprintAsBitVect(*mol, 3, 2048)); + std::unique_ptr mol{RDKit::SmilesToMol(molecule)}; + std::unique_ptr fp{RDKit::MorganFingerprints::getFingerprintAsBitVect(*mol, 3, 2048)}; std::stringstream buf; for (size_t i = 0; i < fp->getNumBits(); i++) { buf << (fp->getBit(i) ? '1' : '0'); @@ -20,21 +20,21 @@ std::string generate_fingerprint(const std::string& molecule) { } int main() { - pqxx::connection conn("dbname=pgvector_example"); + pqxx::connection conn{"dbname=pgvector_example"}; - pqxx::nontransaction tx(conn); + pqxx::nontransaction tx{conn}; tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); tx.exec("DROP TABLE IF EXISTS molecules"); tx.exec("CREATE TABLE molecules (id text PRIMARY KEY, fingerprint bit(2048))"); - std::vector molecules = {"Cc1ccccc1", "Cc1ncccc1", "c1ccccn1"}; - for (auto& molecule : molecules) { - auto fingerprint = generate_fingerprint(molecule); + std::vector molecules{"Cc1ccccc1", "Cc1ncccc1", "c1ccccn1"}; + for (const auto& molecule : molecules) { + std::string fingerprint = generate_fingerprint(molecule); tx.exec("INSERT INTO molecules (id, fingerprint) VALUES ($1, $2)", pqxx::params{molecule, fingerprint}); } - std::string query_molecule = "c1ccco1"; - auto query_fingerprint = generate_fingerprint(query_molecule); + std::string query_molecule{"c1ccco1"}; + std::string query_fingerprint = generate_fingerprint(query_molecule); pqxx::result result = tx.exec("SELECT id, fingerprint <%> $1 AS distance FROM molecules ORDER BY distance LIMIT 5", pqxx::params{query_fingerprint}); for (const auto& row : result) { std::cout << row[0].as() << ": " << row[1].as() << std::endl; diff --git a/examples/sparse/example.cpp b/examples/sparse/example.cpp index efe461a..08ebf61 100644 --- a/examples/sparse/example.cpp +++ b/examples/sparse/example.cpp @@ -18,8 +18,8 @@ using json = nlohmann::json; std::vector embed(const std::vector& inputs) { - std::string url = "http://localhost:3000/embed_sparse"; - json data = { + std::string url{"http://localhost:3000/embed_sparse"}; + json data{ {"inputs", inputs} }; @@ -34,38 +34,36 @@ std::vector embed(const std::vector& inputs json response = json::parse(r.text); std::vector embeddings; - for (auto& item : response) { - std::vector indices; - std::vector values; - for (auto& e : item) { - indices.emplace_back(e["index"]); - values.emplace_back(e["value"]); + for (const auto& item : response) { + std::unordered_map map; + for (const auto& e : item) { + map.insert({e["index"], e["value"]}); } - embeddings.emplace_back(pgvector::SparseVector(30522, indices, values)); + embeddings.emplace_back(pgvector::SparseVector{map, 30522}); } return embeddings; } int main() { - pqxx::connection conn("dbname=pgvector_example"); + pqxx::connection conn{"dbname=pgvector_example"}; - pqxx::nontransaction tx(conn); + pqxx::nontransaction tx{conn}; tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); tx.exec("DROP TABLE IF EXISTS documents"); tx.exec("CREATE TABLE documents (id bigserial PRIMARY KEY, content text, embedding sparsevec(30522))"); - std::vector input = { + std::vector input{ "The dog is barking", "The cat is purring", "The bear is growling" }; - auto embeddings = embed(input); + std::vector embeddings = embed(input); for (size_t i = 0; i < input.size(); i++) { tx.exec("INSERT INTO documents (content, embedding) VALUES ($1, $2)", pqxx::params{input[i], embeddings[i]}); } - std::string query = "forest"; - auto query_embedding = embed({query})[0]; + std::string query{"forest"}; + pgvector::SparseVector query_embedding = embed({query})[0]; pqxx::result result = tx.exec("SELECT content FROM documents ORDER BY embedding <#> $1 LIMIT 5", pqxx::params{query_embedding}); for (const auto& row : result) { std::cout << row[0].as() << std::endl; From b5ceab4cbd748339a2f05abef0c32cb85f74f82b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 16:51:12 -0800 Subject: [PATCH 109/163] Updated style [skip ci] --- include/pgvector/pqxx.hpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 5bff35d..6dcc976 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -19,11 +19,11 @@ /// @cond namespace pqxx { -template <> inline constexpr std::string_view name_type() noexcept { return "vector"; }; +template<> inline constexpr std::string_view name_type() noexcept { return "vector"; }; -template <> struct nullness : no_null {}; +template<> struct nullness : no_null {}; -template <> struct string_traits { +template<> struct string_traits { static pgvector::Vector from_string(std::string_view text, ctx c = {}) { if (text.size() < 2 || text.front() != '[' || text.back() != ']') { throw conversion_error("Malformed vector literal"); @@ -83,11 +83,11 @@ template <> struct string_traits { } }; -template <> inline constexpr std::string_view name_type() noexcept { return "halfvec"; }; +template<> inline constexpr std::string_view name_type() noexcept { return "halfvec"; }; -template <> struct nullness : no_null {}; +template<> struct nullness : no_null {}; -template <> struct string_traits { +template<> struct string_traits { static pgvector::HalfVector from_string(std::string_view text, ctx c = {}) { if (text.size() < 2 || text.front() != '[' || text.back() != ']') { throw conversion_error("Malformed halfvec literal"); @@ -147,11 +147,11 @@ template <> struct string_traits { } }; -template <> inline constexpr std::string_view name_type() noexcept { return "sparsevec"; }; +template<> inline constexpr std::string_view name_type() noexcept { return "sparsevec"; }; -template <> struct nullness : no_null {}; +template<> struct nullness : no_null {}; -template <> struct string_traits { +template<> struct string_traits { static pgvector::SparseVector from_string(std::string_view text, ctx c = {}) { if (text.size() < 4 || text.front() != '{') { throw conversion_error("Malformed sparsevec literal"); From affe84f4b67f60e2488e34b2f7659ec9bcbc0c1b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 16:54:02 -0800 Subject: [PATCH 110/163] Updated style [skip ci] --- include/pgvector/halfvec.hpp | 1 - include/pgvector/pqxx.hpp | 12 +++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 222ec19..9f327c7 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -20,7 +20,6 @@ #endif namespace pgvector { - /// A half vector element. #if __STDCPP_FLOAT16_T__ using Half = std::float16_t; diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 6dcc976..bde21d9 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -19,7 +19,9 @@ /// @cond namespace pqxx { -template<> inline constexpr std::string_view name_type() noexcept { return "vector"; }; +template<> inline constexpr std::string_view name_type() noexcept { + return "vector"; +}; template<> struct nullness : no_null {}; @@ -83,7 +85,9 @@ template<> struct string_traits { } }; -template<> inline constexpr std::string_view name_type() noexcept { return "halfvec"; }; +template<> inline constexpr std::string_view name_type() noexcept { + return "halfvec"; +}; template<> struct nullness : no_null {}; @@ -147,7 +151,9 @@ template<> struct string_traits { } }; -template<> inline constexpr std::string_view name_type() noexcept { return "sparsevec"; }; +template<> inline constexpr std::string_view name_type() noexcept { + return "sparsevec"; +}; template<> struct nullness : no_null {}; From 71ffa7b90e0e32852aaca72a50fac4e8eb91a310 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 17:03:30 -0800 Subject: [PATCH 111/163] Improved assert_exception --- test/helper.hpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/helper.hpp b/test/helper.hpp index 4ed89e6..97aca0f 100644 --- a/test/helper.hpp +++ b/test/helper.hpp @@ -18,14 +18,14 @@ void assert_equal(const T& left, const U& right, const std::source_location& loc template void assert_exception(std::function code, std::optional message = std::nullopt) { - bool exception = false; + std::optional exception; try { code(); } catch (const T& e) { - exception = true; - if (message) { - assert_equal(std::string_view(e.what()), *message); - } + exception = e; + } + assert_equal(exception.has_value(), true); + if (message) { + assert_equal(std::string_view((*exception).what()), *message); } - assert_equal(exception, true); } From 89d2d72ceada3a0efbfecca0d36c6fbceda63e2a Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 17:54:52 -0800 Subject: [PATCH 112/163] Switched to system curl for examples [skip ci] --- examples/cohere/CMakeLists.txt | 1 + examples/hybrid/CMakeLists.txt | 1 + examples/openai/CMakeLists.txt | 1 + examples/sparse/CMakeLists.txt | 1 + 4 files changed, 4 insertions(+) diff --git a/examples/cohere/CMakeLists.txt b/examples/cohere/CMakeLists.txt index 149c11c..c05f196 100644 --- a/examples/cohere/CMakeLists.txt +++ b/examples/cohere/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.18) project(example) set(CMAKE_CXX_STANDARD 20) +set(CPR_USE_SYSTEM_CURL ON) include(FetchContent) diff --git a/examples/hybrid/CMakeLists.txt b/examples/hybrid/CMakeLists.txt index 149c11c..c05f196 100644 --- a/examples/hybrid/CMakeLists.txt +++ b/examples/hybrid/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.18) project(example) set(CMAKE_CXX_STANDARD 20) +set(CPR_USE_SYSTEM_CURL ON) include(FetchContent) diff --git a/examples/openai/CMakeLists.txt b/examples/openai/CMakeLists.txt index 149c11c..c05f196 100644 --- a/examples/openai/CMakeLists.txt +++ b/examples/openai/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.18) project(example) set(CMAKE_CXX_STANDARD 20) +set(CPR_USE_SYSTEM_CURL ON) include(FetchContent) diff --git a/examples/sparse/CMakeLists.txt b/examples/sparse/CMakeLists.txt index 149c11c..c05f196 100644 --- a/examples/sparse/CMakeLists.txt +++ b/examples/sparse/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.18) project(example) set(CMAKE_CXX_STANDARD 20) +set(CPR_USE_SYSTEM_CURL ON) include(FetchContent) From 2a892ee614acab2bd9693645437adbea18232a24 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 18:13:28 -0800 Subject: [PATCH 113/163] Switched to URL for json as recommended by project [skip ci] --- examples/cohere/CMakeLists.txt | 2 +- examples/hybrid/CMakeLists.txt | 2 +- examples/openai/CMakeLists.txt | 2 +- examples/sparse/CMakeLists.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/cohere/CMakeLists.txt b/examples/cohere/CMakeLists.txt index c05f196..5c40779 100644 --- a/examples/cohere/CMakeLists.txt +++ b/examples/cohere/CMakeLists.txt @@ -8,7 +8,7 @@ set(CPR_USE_SYSTEM_CURL ON) include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.14.2) -FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.12.0) +FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz DOWNLOAD_EXTRACT_TIMESTAMP TRUE) FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(cpr json libpqxx) diff --git a/examples/hybrid/CMakeLists.txt b/examples/hybrid/CMakeLists.txt index c05f196..5c40779 100644 --- a/examples/hybrid/CMakeLists.txt +++ b/examples/hybrid/CMakeLists.txt @@ -8,7 +8,7 @@ set(CPR_USE_SYSTEM_CURL ON) include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.14.2) -FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.12.0) +FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz DOWNLOAD_EXTRACT_TIMESTAMP TRUE) FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(cpr json libpqxx) diff --git a/examples/openai/CMakeLists.txt b/examples/openai/CMakeLists.txt index c05f196..5c40779 100644 --- a/examples/openai/CMakeLists.txt +++ b/examples/openai/CMakeLists.txt @@ -8,7 +8,7 @@ set(CPR_USE_SYSTEM_CURL ON) include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.14.2) -FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.12.0) +FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz DOWNLOAD_EXTRACT_TIMESTAMP TRUE) FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(cpr json libpqxx) diff --git a/examples/sparse/CMakeLists.txt b/examples/sparse/CMakeLists.txt index c05f196..5c40779 100644 --- a/examples/sparse/CMakeLists.txt +++ b/examples/sparse/CMakeLists.txt @@ -8,7 +8,7 @@ set(CPR_USE_SYSTEM_CURL ON) include(FetchContent) FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git GIT_TAG 1.14.2) -FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.12.0) +FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz DOWNLOAD_EXTRACT_TIMESTAMP TRUE) FetchContent_Declare(libpqxx GIT_REPOSITORY https://github.com/jtv/libpqxx.git GIT_TAG 8.0.0) FetchContent_MakeAvailable(cpr json libpqxx) From 31f101e1d9893704467d9bb9027cfb905e9a6abe Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 18:31:35 -0800 Subject: [PATCH 114/163] Updated changelog [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ebacb4..2fec624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ - Added support for libpqxx 8 - Changed `HalfVector` to use `std::float16_t` or `_Float16` when available - Replaced `std::vector` conversion with `as_vector` function for `Vector` and `HalfVector` -- Removed default constructor from `Vector` and `HalfVector` +- Removed default constructors - Dropped support for libpqxx 7 - Dropped support for C++17 From c89d11fe0efc5afc9b5281d354c27d9dde0c5f00 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 18:33:45 -0800 Subject: [PATCH 115/163] Updated changelog [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fec624..932a6e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ - Added support for libpqxx 8 - Changed `HalfVector` to use `std::float16_t` or `_Float16` when available - Replaced `std::vector` conversion with `as_vector` function for `Vector` and `HalfVector` -- Removed default constructors +- Removed default constructors (no longer needed for streaming) - Dropped support for libpqxx 7 - Dropped support for C++17 From c8c3cb1c40f1c530ffa2372a7eff9f923d369c7c Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 18:47:02 -0800 Subject: [PATCH 116/163] Updated readme [skip ci] --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 046b809..6ead46b 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Or a span pgvector::Vector vec{std::span{{1, 2, 3}}}; ``` -Convert to a `std::vector` +Get a `std::vector` ```cpp std::vector float_vec = static_cast>(vec); @@ -116,7 +116,7 @@ Or a span pgvector::HalfVector vec{std::span{{1, 2, 3}}}; ``` -Convert to a `std::vector` +Get a `std::vector` ```cpp std::vector float_vec = static_cast>(vec); From cdd9438458eefc5b65e425ffd8758ebbb5af4943 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 23:07:46 -0800 Subject: [PATCH 117/163] Renamed as_vector function to values --- CHANGELOG.md | 2 +- include/pgvector/halfvec.hpp | 4 ++-- include/pgvector/pqxx.hpp | 8 ++++---- include/pgvector/vector.hpp | 4 ++-- test/halfvec_test.cpp | 6 +++--- test/vector_test.cpp | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 932a6e8..93a15cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ - Added support for libpqxx 8 - Changed `HalfVector` to use `std::float16_t` or `_Float16` when available -- Replaced `std::vector` conversion with `as_vector` function for `Vector` and `HalfVector` +- Replaced `std::vector` conversion with `values` function for `Vector` and `HalfVector` - Removed default constructors (no longer needed for streaming) - Dropped support for libpqxx 7 - Dropped support for C++17 diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 9f327c7..317ad3a 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -46,8 +46,8 @@ class HalfVector { return value_.size(); } - /// Returns the half vector as a `std::vector`. - const std::vector& as_vector() const { + /// Returns the values. + const std::vector& values() const { return value_; } diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index bde21d9..b6ea892 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -47,7 +47,7 @@ template<> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::Vector& value, ctx c = {}) { - const std::vector& values = value.as_vector(); + const std::vector& values = value.values(); // important! size_buffer cannot throw an exception on overflow // so perform this check before writing any data @@ -71,7 +71,7 @@ template<> struct string_traits { } static size_t size_buffer(const pgvector::Vector& value) noexcept { - const std::vector& values = value.as_vector(); + const std::vector& values = value.values(); // cannot throw an exception here on overflow // so throw in into_buf @@ -113,7 +113,7 @@ template<> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { - const std::vector& values = value.as_vector(); + const std::vector& values = value.values(); // important! size_buffer cannot throw an exception on overflow // so perform this check before writing any data @@ -137,7 +137,7 @@ template<> struct string_traits { } static size_t size_buffer(const pgvector::HalfVector& value) noexcept { - const std::vector& values = value.as_vector(); + const std::vector& values = value.values(); // cannot throw an exception here on overflow // so throw in into_buf diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index 52c4a7a..44829cb 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -30,8 +30,8 @@ class Vector { return value_.size(); } - /// Returns the vector as a `std::vector`. - const std::vector& as_vector() const { + /// Returns the values. + const std::vector& values() const { return value_; } diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index d2f5fa4..9ee1e1d 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -16,13 +16,13 @@ static void test_constructor_span() { assert_equal(vec.dimensions(), 3u); } -static void test_as_vector() { +static void test_values() { HalfVector vec{{1, 2, 3}}; - assert_equal(vec.as_vector() == std::vector{1, 2, 3}, true); + assert_equal(vec.values() == std::vector{1, 2, 3}, true); } void test_halfvec() { test_constructor_vector(); test_constructor_span(); - test_as_vector(); + test_values(); } diff --git a/test/vector_test.cpp b/test/vector_test.cpp index 1a7267d..25e64c3 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -16,13 +16,13 @@ static void test_constructor_span() { assert_equal(vec.dimensions(), 3u); } -static void test_as_vector() { +static void test_values() { Vector vec{{1, 2, 3}}; - assert_equal(vec.as_vector() == std::vector{1, 2, 3}, true); + assert_equal(vec.values() == std::vector{1, 2, 3}, true); } void test_vector() { test_constructor_vector(); test_constructor_span(); - test_as_vector(); + test_values(); } From 632462fdfbeb67b0f1e7d1331ff142b3968c2a89 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 5 Mar 2026 23:56:27 -0800 Subject: [PATCH 118/163] Updated size_buffer function for sparse vectors (not an issue since int size_buffer is constant and libpqxx < 8 also adds terminating zero) [skip ci] --- include/pgvector/pqxx.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index b6ea892..13dfbc7 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -252,7 +252,7 @@ template<> struct string_traits { size += string_traits::size_buffer(dimensions); for (size_t i = 0; i < nnz; i++) { size += 2; // : and , - size += string_traits::size_buffer(indices[i]); + size += string_traits::size_buffer(indices[i] + 1); size += string_traits::size_buffer(values[i]); } return size; From 157b63319d8c8e117a4a43907c2e4b7bdaec3482 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 01:09:58 -0800 Subject: [PATCH 119/163] Switched to into_buf for bounds checking [skip ci] --- include/pgvector/pqxx.hpp | 50 ++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 13dfbc7..681d9f6 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -56,16 +56,16 @@ template<> struct string_traits { } size_t here = 0; - buf[here++] = '['; + here += pqxx::into_buf(buf.subspan(here), "[", c); for (size_t i = 0; i < values.size(); i++) { if (i != 0) { - buf[here++] = ','; + here += pqxx::into_buf(buf.subspan(here), ",", c); } here += pqxx::into_buf(buf.subspan(here), values[i], c); } - buf[here++] = ']'; + here += pqxx::into_buf(buf.subspan(here), "]", c); return {std::data(buf), here}; } @@ -76,11 +76,13 @@ template<> struct string_traits { // cannot throw an exception here on overflow // so throw in into_buf - size_t size = 2; // [ and ] + size_t size = 0; + size += pqxx::size_buffer("["); for (const auto v : values) { - size += 1; // , - size += string_traits::size_buffer(v); + size += pqxx::size_buffer(","); + size += pqxx::size_buffer(v); } + size += pqxx::size_buffer("]"); return size; } }; @@ -122,16 +124,16 @@ template<> struct string_traits { } size_t here = 0; - buf[here++] = '['; + here += pqxx::into_buf(buf.subspan(here), "[", c); for (size_t i = 0; i < values.size(); i++) { if (i != 0) { - buf[here++] = ','; + here += pqxx::into_buf(buf.subspan(here), ",", c); } here += pqxx::into_buf(buf.subspan(here), static_cast(values[i]), c); } - buf[here++] = ']'; + here += pqxx::into_buf(buf.subspan(here), "]", c); return {std::data(buf), here}; } @@ -142,11 +144,13 @@ template<> struct string_traits { // cannot throw an exception here on overflow // so throw in into_buf - size_t size = 2; // [ and ] + size_t size = 0; + size += pqxx::size_buffer("["); for (const auto v : values) { - size += 1; // , - size += string_traits::size_buffer(static_cast(v)); + size += pqxx::size_buffer(","); + size += pqxx::size_buffer(static_cast(v)); } + size += pqxx::size_buffer("]"); return size; } }; @@ -221,19 +225,18 @@ template<> struct string_traits { } size_t here = 0; - buf[here++] = '{'; + here += pqxx::into_buf(buf.subspan(here), "{", c); for (size_t i = 0; i < nnz; i++) { if (i != 0) { - buf[here++] = ','; + here += pqxx::into_buf(buf.subspan(here), ",", c); } here += pqxx::into_buf(buf.subspan(here), indices[i] + 1, c); - buf[here++] = ':'; + here += pqxx::into_buf(buf.subspan(here), ":", c); here += pqxx::into_buf(buf.subspan(here), values[i], c); } - buf[here++] = '}'; - buf[here++] = '/'; + here += pqxx::into_buf(buf.subspan(here), "}/", c); here += pqxx::into_buf(buf.subspan(here), dimensions, c); return {std::data(buf), here}; @@ -248,13 +251,16 @@ template<> struct string_traits { // cannot throw an exception here on overflow // so throw in into_buf - size_t size = 3; // {, }, and / - size += string_traits::size_buffer(dimensions); + size_t size = 0; + size += pqxx::size_buffer("{"); for (size_t i = 0; i < nnz; i++) { - size += 2; // : and , - size += string_traits::size_buffer(indices[i] + 1); - size += string_traits::size_buffer(values[i]); + size += pqxx::size_buffer(","); + size += pqxx::size_buffer(indices[i] + 1); + size += pqxx::size_buffer(":"); + size += pqxx::size_buffer(values[i]); } + size += pqxx::size_buffer("}/"); + size += pqxx::size_buffer(dimensions); return size; } }; From 7c51883d2fa4df9b7e5ffabc616804bb44168231 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 01:17:33 -0800 Subject: [PATCH 120/163] Switched to pqxx::from_string --- include/pgvector/pqxx.hpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 681d9f6..5dbf443 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -37,11 +37,11 @@ template<> struct string_traits { size_t start = 0; for (size_t i = 0; i < inner.size(); i++) { if (inner[i] == ',') { - values.push_back(string_traits::from_string(inner.substr(start, i - start), c)); + values.push_back(pqxx::from_string(inner.substr(start, i - start), c)); start = i + 1; } } - values.push_back(string_traits::from_string(inner.substr(start), c)); + values.push_back(pqxx::from_string(inner.substr(start), c)); } return pgvector::Vector{std::move(values)}; } @@ -105,11 +105,11 @@ template<> struct string_traits { size_t start = 0; for (size_t i = 0; i < inner.size(); i++) { if (inner[i] == ',') { - values.push_back(static_cast(string_traits::from_string(inner.substr(start, i - start), c))); + values.push_back(static_cast(pqxx::from_string(inner.substr(start, i - start), c))); start = i + 1; } } - values.push_back(static_cast(string_traits::from_string(inner.substr(start), c))); + values.push_back(static_cast(pqxx::from_string(inner.substr(start), c))); } return pgvector::HalfVector{std::move(values)}; } @@ -172,7 +172,7 @@ template<> struct string_traits { throw conversion_error("Malformed sparsevec literal"); } - int dimensions = string_traits::from_string(text.substr(n + 2), c); + int dimensions = pqxx::from_string(text.substr(n + 2), c); if (dimensions < 0) { throw conversion_error("Dimensions cannot be negative"); } @@ -187,8 +187,8 @@ template<> struct string_traits { throw conversion_error("Malformed sparsevec literal"); } - int index = string_traits::from_string(substr.substr(0, ne), c); - float value = string_traits::from_string(substr.substr(ne + 1), c); + int index = pqxx::from_string(substr.substr(0, ne), c); + float value = pqxx::from_string(substr.substr(ne + 1), c); if (index < 1) { throw conversion_error("Index out of bounds"); From 321185e274d8080bd5b1ad3aad1195e56e633d69 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 02:35:58 -0800 Subject: [PATCH 121/163] Added tests for to_buf and into_buf functions --- test/pqxx_test.cpp | 67 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 71728ec..0dfa64d 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -280,6 +280,66 @@ void test_sparsevec_from_string() { }, "Could not convert 'a' to int: Invalid argument."); } +void test_vector_to_buf() { + char buf[10]; + assert_equal(pqxx::to_buf(std::span{buf}, pgvector::Vector{{1, 2, 3}}), "[1,2,3]"); + + assert_exception([] { + return pqxx::to_buf(std::span{}, pgvector::Vector{{1, 2, 3}}); + }); +} + +void test_vector_into_buf() { + char buf[10]; + size_t size = pqxx::into_buf(std::span{buf}, pgvector::Vector{{1, 2, 3}}); + assert_equal(size, 7u); + assert_equal(std::string_view{buf, size}, "[1,2,3]"); + + assert_exception([] { + return pqxx::into_buf(std::span{}, pgvector::Vector{{1, 2, 3}}); + }); +} + +void test_halfvec_to_buf() { + char buf[10]; + assert_equal(pqxx::to_buf(std::span{buf}, pgvector::HalfVector{{1, 2, 3}}), "[1,2,3]"); + + assert_exception([] { + return pqxx::to_buf(std::span{}, pgvector::HalfVector{{1, 2, 3}}); + }); +} + +void test_halfvec_into_buf() { + char buf[10]; + size_t size = pqxx::into_buf(std::span{buf}, pgvector::HalfVector{{1, 2, 3}}); + assert_equal(size, 7u); + assert_equal(std::string_view{buf, size}, "[1,2,3]"); + + assert_exception([] { + return pqxx::into_buf(std::span{}, pgvector::HalfVector{{1, 2, 3}}); + }); +} + +void test_sparsevec_to_buf() { + char buf[40]; + assert_equal(pqxx::to_buf(std::span{buf}, pgvector::SparseVector{{1, 2, 3}}), "{1:1,2:2,3:3}/3"); + + assert_exception([] { + return pqxx::to_buf(std::span{}, pgvector::SparseVector{{1, 2, 3}}); + }); +} + +void test_sparsevec_into_buf() { + char buf[40]; + size_t size = pqxx::into_buf(std::span{buf}, pgvector::SparseVector{{1, 2, 3}}); + assert_equal(size, 15u); + assert_equal(std::string_view{buf, size}, "{1:1,2:2,3:3}/3"); + + assert_exception([] { + return pqxx::into_buf(std::span{}, pgvector::SparseVector{{1, 2, 3}}); + }); +} + void test_pqxx() { pqxx::connection conn{"dbname=pgvector_cpp_test"}; setup(conn); @@ -299,4 +359,11 @@ void test_pqxx() { test_halfvec_from_string(); test_sparsevec_to_string(); test_sparsevec_from_string(); + + test_vector_to_buf(); + test_vector_into_buf(); + test_halfvec_to_buf(); + test_halfvec_into_buf(); + test_sparsevec_to_buf(); + test_sparsevec_into_buf(); } From 61c8878e72aaf88c974824ab6a8f167addc0f327 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 02:59:32 -0800 Subject: [PATCH 122/163] Updated style [skip ci] --- examples/cohere/example.cpp | 2 +- examples/hybrid/example.cpp | 2 +- examples/openai/example.cpp | 2 +- examples/sparse/example.cpp | 2 +- include/pgvector/pqxx.hpp | 14 +++++++------- include/pgvector/sparsevec.hpp | 6 +++--- test/helper.hpp | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/cohere/example.cpp b/examples/cohere/example.cpp index 72041d7..aba2cc6 100644 --- a/examples/cohere/example.cpp +++ b/examples/cohere/example.cpp @@ -28,7 +28,7 @@ std::vector embed(const std::vector& texts, const std: cpr::Header{{"Content-Type", "application/json"}} ); if (r.status_code != 200) { - throw std::runtime_error("Bad status: " + std::to_string(r.status_code)); + throw std::runtime_error{"Bad status: " + std::to_string(r.status_code)}; } json response = json::parse(r.text); diff --git a/examples/hybrid/example.cpp b/examples/hybrid/example.cpp index 86206fc..ed368a6 100644 --- a/examples/hybrid/example.cpp +++ b/examples/hybrid/example.cpp @@ -33,7 +33,7 @@ std::vector> embed(const std::vector& texts, con cpr::Header{{"Content-Type", "application/json"}} ); if (r.status_code != 200) { - throw std::runtime_error("Bad status: " + std::to_string(r.status_code)); + throw std::runtime_error{"Bad status: " + std::to_string(r.status_code)}; } json response = json::parse(r.text); diff --git a/examples/openai/example.cpp b/examples/openai/example.cpp index 91dc19d..eddf6be 100644 --- a/examples/openai/example.cpp +++ b/examples/openai/example.cpp @@ -25,7 +25,7 @@ std::vector> embed(const std::vector& input, cha cpr::Header{{"Content-Type", "application/json"}} ); if (r.status_code != 200) { - throw std::runtime_error("Bad status: " + std::to_string(r.status_code)); + throw std::runtime_error{"Bad status: " + std::to_string(r.status_code)}; } json response = json::parse(r.text); diff --git a/examples/sparse/example.cpp b/examples/sparse/example.cpp index 08ebf61..12c602b 100644 --- a/examples/sparse/example.cpp +++ b/examples/sparse/example.cpp @@ -29,7 +29,7 @@ std::vector embed(const std::vector& inputs cpr::Header{{"Content-Type", "application/json"}} ); if (r.status_code != 200) { - throw std::runtime_error("Bad status: " + std::to_string(r.status_code)); + throw std::runtime_error{"Bad status: " + std::to_string(r.status_code)}; } json response = json::parse(r.text); diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 5dbf443..f056062 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -28,7 +28,7 @@ template<> struct nullness : no_null {}; template<> struct string_traits { static pgvector::Vector from_string(std::string_view text, ctx c = {}) { if (text.size() < 2 || text.front() != '[' || text.back() != ']') { - throw conversion_error("Malformed vector literal"); + throw conversion_error{"Malformed vector literal"}; } std::vector values; @@ -96,7 +96,7 @@ template<> struct nullness : no_null template<> struct string_traits { static pgvector::HalfVector from_string(std::string_view text, ctx c = {}) { if (text.size() < 2 || text.front() != '[' || text.back() != ']') { - throw conversion_error("Malformed halfvec literal"); + throw conversion_error{"Malformed halfvec literal"}; } std::vector values; @@ -164,17 +164,17 @@ template<> struct nullness : no_null struct string_traits { static pgvector::SparseVector from_string(std::string_view text, ctx c = {}) { if (text.size() < 4 || text.front() != '{') { - throw conversion_error("Malformed sparsevec literal"); + throw conversion_error{"Malformed sparsevec literal"}; } size_t n = text.find("}/", 1); if (n == std::string_view::npos) { - throw conversion_error("Malformed sparsevec literal"); + throw conversion_error{"Malformed sparsevec literal"}; } int dimensions = pqxx::from_string(text.substr(n + 2), c); if (dimensions < 0) { - throw conversion_error("Dimensions cannot be negative"); + throw conversion_error{"Dimensions cannot be negative"}; } std::vector indices; @@ -184,14 +184,14 @@ template<> struct string_traits { auto add_element = [&](std::string_view substr) { size_t ne = substr.find(":"); if (ne == std::string::npos) { - throw conversion_error("Malformed sparsevec literal"); + throw conversion_error{"Malformed sparsevec literal"}; } int index = pqxx::from_string(substr.substr(0, ne), c); float value = pqxx::from_string(substr.substr(ne + 1), c); if (index < 1) { - throw conversion_error("Index out of bounds"); + throw conversion_error{"Index out of bounds"}; } indices.push_back(index - 1); diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index f899501..1a3dd90 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -21,7 +21,7 @@ class SparseVector { /// @private SparseVector(int dimensions, std::vector&& indices, std::vector&& values) { if (values.size() != indices.size()) { - throw std::invalid_argument("indices and values must be the same length"); + throw std::invalid_argument{"indices and values must be the same length"}; } dimensions_ = dimensions; indices_ = std::move(indices); @@ -55,13 +55,13 @@ class SparseVector { /// Creates a sparse vector from a map of non-zero elements. SparseVector(const std::unordered_map& map, int dimensions) { if (dimensions < 1) { - throw std::invalid_argument("sparsevec must have at least 1 dimension"); + throw std::invalid_argument{"sparsevec must have at least 1 dimension"}; } dimensions_ = dimensions; for (const auto& [i, v] : map) { if (i < 0 || i >= dimensions) { - throw std::invalid_argument("sparsevec index out of bounds"); + throw std::invalid_argument{"sparsevec index out of bounds"}; } if (v != 0) { diff --git a/test/helper.hpp b/test/helper.hpp index 97aca0f..5c8c1ae 100644 --- a/test/helper.hpp +++ b/test/helper.hpp @@ -12,7 +12,7 @@ void assert_equal(const T& left, const U& right, const std::source_location& loc std::ostringstream message; message << left << " != " << right; message << " in " << loc.function_name() << " " << loc.file_name() << ":" << loc.line(); - throw std::runtime_error(message.str()); + throw std::runtime_error{message.str()}; } } @@ -26,6 +26,6 @@ void assert_exception(std::function code, std::optional Date: Fri, 6 Mar 2026 03:28:12 -0800 Subject: [PATCH 123/163] Added tests for size_buffer functions [skip ci] --- test/pqxx_test.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 0dfa64d..3f222cb 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -340,6 +340,18 @@ void test_sparsevec_into_buf() { }); } +void test_vector_size_buffer() { + assert_equal(pqxx::size_buffer(pgvector::Vector{{1, 2, 3}}), 55u); +} + +void test_halfvec_size_buffer() { + assert_equal(pqxx::size_buffer(pgvector::HalfVector{{1, 2, 3}}), 55u); +} + +void test_sparsevec_size_buffer() { + assert_equal(pqxx::size_buffer(pgvector::SparseVector{{1, 2, 3}}), 106u); +} + void test_pqxx() { pqxx::connection conn{"dbname=pgvector_cpp_test"}; setup(conn); @@ -366,4 +378,8 @@ void test_pqxx() { test_halfvec_into_buf(); test_sparsevec_to_buf(); test_sparsevec_into_buf(); + + test_vector_size_buffer(); + test_halfvec_size_buffer(); + test_sparsevec_size_buffer(); } From a0bb20ae9499184618a770a207e97c717b76c974 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 12:28:54 -0800 Subject: [PATCH 124/163] Added initial check for buffer space [skip ci] --- include/pgvector/pqxx.hpp | 15 +++++++++++++++ test/pqxx_test.cpp | 24 ++++++++++++------------ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index f056062..2128c2c 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -47,6 +47,11 @@ template<> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::Vector& value, ctx c = {}) { + // confirm caller provided estimated buffer space + if (buf.size() < size_buffer(value)) { + throw conversion_overrun{"Not enough space in buffer for vector"}; + } + const std::vector& values = value.values(); // important! size_buffer cannot throw an exception on overflow @@ -115,6 +120,11 @@ template<> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::HalfVector& value, ctx c = {}) { + // confirm caller provided estimated buffer space + if (buf.size() < size_buffer(value)) { + throw conversion_overrun{"Not enough space in buffer for halfvec"}; + } + const std::vector& values = value.values(); // important! size_buffer cannot throw an exception on overflow @@ -213,6 +223,11 @@ template<> struct string_traits { } static std::string_view to_buf(std::span buf, const pgvector::SparseVector& value, ctx c = {}) { + // confirm caller provided estimated buffer space + if (buf.size() < size_buffer(value)) { + throw conversion_overrun{"Not enough space in buffer for sparsevec"}; + } + int dimensions = value.dimensions(); const std::vector& indices = value.indices(); const std::vector& values = value.values(); diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 3f222cb..ea7229b 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -281,63 +281,63 @@ void test_sparsevec_from_string() { } void test_vector_to_buf() { - char buf[10]; + char buf[60]; assert_equal(pqxx::to_buf(std::span{buf}, pgvector::Vector{{1, 2, 3}}), "[1,2,3]"); assert_exception([] { return pqxx::to_buf(std::span{}, pgvector::Vector{{1, 2, 3}}); - }); + }, "Not enough space in buffer for vector"); } void test_vector_into_buf() { - char buf[10]; + char buf[60]; size_t size = pqxx::into_buf(std::span{buf}, pgvector::Vector{{1, 2, 3}}); assert_equal(size, 7u); assert_equal(std::string_view{buf, size}, "[1,2,3]"); assert_exception([] { return pqxx::into_buf(std::span{}, pgvector::Vector{{1, 2, 3}}); - }); + }, "Not enough space in buffer for vector"); } void test_halfvec_to_buf() { - char buf[10]; + char buf[60]; assert_equal(pqxx::to_buf(std::span{buf}, pgvector::HalfVector{{1, 2, 3}}), "[1,2,3]"); assert_exception([] { return pqxx::to_buf(std::span{}, pgvector::HalfVector{{1, 2, 3}}); - }); + }, "Not enough space in buffer for halfvec"); } void test_halfvec_into_buf() { - char buf[10]; + char buf[60]; size_t size = pqxx::into_buf(std::span{buf}, pgvector::HalfVector{{1, 2, 3}}); assert_equal(size, 7u); assert_equal(std::string_view{buf, size}, "[1,2,3]"); assert_exception([] { return pqxx::into_buf(std::span{}, pgvector::HalfVector{{1, 2, 3}}); - }); + }, "Not enough space in buffer for halfvec"); } void test_sparsevec_to_buf() { - char buf[40]; + char buf[120]; assert_equal(pqxx::to_buf(std::span{buf}, pgvector::SparseVector{{1, 2, 3}}), "{1:1,2:2,3:3}/3"); assert_exception([] { return pqxx::to_buf(std::span{}, pgvector::SparseVector{{1, 2, 3}}); - }); + }, "Not enough space in buffer for sparsevec"); } void test_sparsevec_into_buf() { - char buf[40]; + char buf[120]; size_t size = pqxx::into_buf(std::span{buf}, pgvector::SparseVector{{1, 2, 3}}); assert_equal(size, 15u); assert_equal(std::string_view{buf, size}, "{1:1,2:2,3:3}/3"); assert_exception([] { return pqxx::into_buf(std::span{}, pgvector::SparseVector{{1, 2, 3}}); - }); + }, "Not enough space in buffer for sparsevec"); } void test_vector_size_buffer() { From ec2fb0ced825de12f4747a30ba26d383881c0854 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 12:32:29 -0800 Subject: [PATCH 125/163] DRYed constructor [skip ci] --- include/pgvector/sparsevec.hpp | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 1a3dd90..9abd192 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -29,16 +29,7 @@ class SparseVector { } /// Creates a sparse vector from a dense vector. - explicit SparseVector(const std::vector& value) { - dimensions_ = value.size(); - for (size_t i = 0; i < value.size(); i++) { - float v = value[i]; - if (v != 0) { - indices_.push_back(i); - values_.push_back(v); - } - } - } + explicit SparseVector(const std::vector& value) : SparseVector(std::span{value}) {} /// Creates a sparse vector from a span. explicit SparseVector(std::span value) { From 4b3c87a207f02eda954a30b0a7cc3fc80e3e49b0 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 12:46:20 -0800 Subject: [PATCH 126/163] Added static casts [skip ci] --- CMakeLists.txt | 2 +- include/pgvector/sparsevec.hpp | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e2e5daf..0791742 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) add_executable(test test/halfvec_test.cpp test/main.cpp test/pqxx_test.cpp test/sparsevec_test.cpp test/vector_test.cpp) target_link_libraries(test PRIVATE libpqxx::pqxx pgvector::pgvector) if(NOT MSVC) - target_compile_options(test PRIVATE -Wall -Wextra -Wpedantic -Werror) + target_compile_options(test PRIVATE -Wall -Wextra -Wpedantic -Wconversion -Werror) endif() endif() endif() diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 9abd192..190da21 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -33,11 +33,12 @@ class SparseVector { /// Creates a sparse vector from a span. explicit SparseVector(std::span value) { - dimensions_ = value.size(); + // TODO throw exception + dimensions_ = static_cast(value.size()); for (size_t i = 0; i < value.size(); i++) { float v = value[i]; if (v != 0) { - indices_.push_back(i); + indices_.push_back(static_cast(i)); values_.push_back(v); } } From d3476676a64079812042d9ea472837960d19a2eb Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 13:30:45 -0800 Subject: [PATCH 127/163] Added scan-build to CI --- .github/workflows/build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12901dd..09288c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-26] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 @@ -33,6 +33,9 @@ jobs: sudo apt-get install valgrind valgrind --leak-check=yes --error-exitcode=1 build/test + - if: ${{ runner.os == 'macOS' }} + run: /opt/homebrew/opt/llvm@20/bin/scan-build --status-bugs cmake --build build --clean-first + # test install - run: rm -r build - run: cmake -S . -B build From 9771322af880201f0398790b58cdb6d3fc40d20f Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 13:34:08 -0800 Subject: [PATCH 128/163] Fixed CI --- test/pqxx_test.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index ea7229b..69efc77 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -130,7 +130,7 @@ void test_precision(pqxx::connection &conn) { before_each(conn); pqxx::nontransaction tx{conn}; - pgvector::Vector embedding{{1.23456789, 0, 0}}; + pgvector::Vector embedding{{1.23456789f, 0, 0}}; tx.exec("INSERT INTO items (embedding) VALUES ($1)", {embedding}); tx.exec("SET extra_float_digits = 3"); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY id DESC LIMIT 1"); @@ -139,7 +139,7 @@ void test_precision(pqxx::connection &conn) { void test_vector_to_string() { assert_equal(pqxx::to_string(pgvector::Vector{{1, 2, 3}}), "[1,2,3]"); - assert_equal(pqxx::to_string(pgvector::Vector{{-1.234567890123}}), "[-1.2345679]"); + assert_equal(pqxx::to_string(pgvector::Vector{{-1.234567890123f}}), "[-1.2345679]"); assert_exception([] { pqxx::to_string(pgvector::Vector{std::vector(16001)}); @@ -178,9 +178,9 @@ void test_vector_from_string() { void test_halfvec_to_string() { assert_equal(pqxx::to_string(pgvector::HalfVector{{1, 2, 3}}), "[1,2,3]"); #if __STDCPP_FLOAT16_T__ || defined(__FLT16_MAX__) - assert_equal(pqxx::to_string(pgvector::HalfVector{{static_cast(-1.234567890123)}}), "[-1.234375]"); + assert_equal(pqxx::to_string(pgvector::HalfVector{{static_cast(-1.234567890123f)}}), "[-1.234375]"); #else - assert_equal(pqxx::to_string(pgvector::HalfVector{{-1.234567890123}}), "[-1.2345679]"); + assert_equal(pqxx::to_string(pgvector::HalfVector{{-1.234567890123f}}), "[-1.2345679]"); #endif assert_exception([] { @@ -219,7 +219,7 @@ void test_halfvec_from_string() { void test_sparsevec_to_string() { assert_equal(pqxx::to_string(pgvector::SparseVector{{1, 0, 2, 0, 3, 0}}), "{1:1,3:2,5:3}/6"); - std::unordered_map map{{999999999, -1.234567890123}}; + std::unordered_map map{{999999999, -1.234567890123f}}; assert_equal(pqxx::to_string(pgvector::SparseVector{map, 1000000000}), "{1000000000:-1.2345679}/1000000000"); assert_exception([] { From 4e28ff3df1bdd2a7fcb4882cf29deb6503a1fb8c Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 16:57:11 -0800 Subject: [PATCH 129/163] Improved test [skip ci] --- test/pqxx_test.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 69efc77..3e97724 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -324,6 +325,9 @@ void test_sparsevec_to_buf() { char buf[120]; assert_equal(pqxx::to_buf(std::span{buf}, pgvector::SparseVector{{1, 2, 3}}), "{1:1,2:2,3:3}/3"); + int max = std::numeric_limits::max(); + assert_equal(pqxx::to_buf(std::span{buf}, pgvector::SparseVector{{{max - 1, 1}}, max}), "{2147483647:1}/2147483647"); + assert_exception([] { return pqxx::to_buf(std::span{}, pgvector::SparseVector{{1, 2, 3}}); }, "Not enough space in buffer for sparsevec"); From 957cec29c4d11487df402ffb19543096ef43af67 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 17:02:57 -0800 Subject: [PATCH 130/163] Added cast to to_buf for SparseVector [skip ci] --- include/pgvector/pqxx.hpp | 5 +++-- test/pqxx_test.cpp | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 2128c2c..59b697e 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -246,7 +246,8 @@ template<> struct string_traits { if (i != 0) { here += pqxx::into_buf(buf.subspan(here), ",", c); } - here += pqxx::into_buf(buf.subspan(here), indices[i] + 1, c); + // cast to prevent undefined behavior on overflow and require less buffer space + here += pqxx::into_buf(buf.subspan(here), static_cast(indices[i]) + 1, c); here += pqxx::into_buf(buf.subspan(here), ":", c); here += pqxx::into_buf(buf.subspan(here), values[i], c); } @@ -270,7 +271,7 @@ template<> struct string_traits { size += pqxx::size_buffer("{"); for (size_t i = 0; i < nnz; i++) { size += pqxx::size_buffer(","); - size += pqxx::size_buffer(indices[i] + 1); + size += pqxx::size_buffer(static_cast(indices[i]) + 1); size += pqxx::size_buffer(":"); size += pqxx::size_buffer(values[i]); } diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 3e97724..09b7711 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -353,7 +353,7 @@ void test_halfvec_size_buffer() { } void test_sparsevec_size_buffer() { - assert_equal(pqxx::size_buffer(pgvector::SparseVector{{1, 2, 3}}), 106u); + assert_equal(pqxx::size_buffer(pgvector::SparseVector{{1, 2, 3}}), 103u); } void test_pqxx() { From 5cdf935d955e809b199afd4b81b4447e2d044600 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 17:08:25 -0800 Subject: [PATCH 131/163] Improved index check for from_string [skip ci] --- include/pgvector/pqxx.hpp | 2 +- test/pqxx_test.cpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 59b697e..9e665e6 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -200,7 +200,7 @@ template<> struct string_traits { int index = pqxx::from_string(substr.substr(0, ne), c); float value = pqxx::from_string(substr.substr(ne + 1), c); - if (index < 1) { + if (index < 1 || index > dimensions) { throw conversion_error{"Index out of bounds"}; } diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 09b7711..59091ce 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -264,6 +264,10 @@ void test_sparsevec_from_string() { auto _ = pqxx::from_string("{-2147483648:1}/1"); }, "Index out of bounds"); + assert_exception([] { + auto _ = pqxx::from_string("{2:1}/1"); + }, "Index out of bounds"); + assert_exception([] { auto _ = pqxx::from_string("{1:4e38}/1"); }, float_error("Could not convert '4e38' to float: Value out of range.")); From f1917e615b361ea27d535705b2e875d66918ea43 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 19:45:27 -0800 Subject: [PATCH 132/163] Added dimensions check [skip ci] --- include/pgvector/sparsevec.hpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 190da21..83a4fd5 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -33,7 +34,9 @@ class SparseVector { /// Creates a sparse vector from a span. explicit SparseVector(std::span value) { - // TODO throw exception + if (value.size() > std::numeric_limits::max()) { + throw std::invalid_argument{"too many dimensions"}; + } dimensions_ = static_cast(value.size()); for (size_t i = 0; i < value.size(); i++) { float v = value[i]; From 4ccdf525cf5f755456cc3578d04fc2eed1e9ddcd Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 21:16:16 -0800 Subject: [PATCH 133/163] Switched to public constructor for from_string for SparseVector [skip ci] --- include/pgvector/pqxx.hpp | 22 +++++++++++----------- include/pgvector/sparsevec.hpp | 10 ---------- test/pqxx_test.cpp | 8 ++++---- 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 9e665e6..5502707 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include @@ -182,13 +183,8 @@ template<> struct string_traits { throw conversion_error{"Malformed sparsevec literal"}; } + std::unordered_map map; int dimensions = pqxx::from_string(text.substr(n + 2), c); - if (dimensions < 0) { - throw conversion_error{"Dimensions cannot be negative"}; - } - - std::vector indices; - std::vector values; if (n > 1) { auto add_element = [&](std::string_view substr) { @@ -200,12 +196,12 @@ template<> struct string_traits { int index = pqxx::from_string(substr.substr(0, ne), c); float value = pqxx::from_string(substr.substr(ne + 1), c); - if (index < 1 || index > dimensions) { - throw conversion_error{"Index out of bounds"}; + // check to avoid undefined behavior + if (index > std::numeric_limits::min()) { + index -= 1; } - indices.push_back(index - 1); - values.push_back(value); + map.insert({index, value}); }; std::string_view inner = text.substr(1, n - 1); @@ -219,7 +215,11 @@ template<> struct string_traits { add_element(inner.substr(start)); } - return pgvector::SparseVector{dimensions, std::move(indices), std::move(values)}; + try { + return pgvector::SparseVector{map, dimensions}; + } catch (const std::invalid_argument& e) { + throw conversion_error{e.what()}; + } } static std::string_view to_buf(std::span buf, const pgvector::SparseVector& value, ctx c = {}) { diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 83a4fd5..36fb4d0 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -19,16 +19,6 @@ namespace pgvector { /// A sparse vector. class SparseVector { public: - /// @private - SparseVector(int dimensions, std::vector&& indices, std::vector&& values) { - if (values.size() != indices.size()) { - throw std::invalid_argument{"indices and values must be the same length"}; - } - dimensions_ = dimensions; - indices_ = std::move(indices); - values_ = std::move(values); - } - /// Creates a sparse vector from a dense vector. explicit SparseVector(const std::vector& value) : SparseVector(std::span{value}) {} diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 59091ce..0d2a2cc 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -246,7 +246,7 @@ void test_sparsevec_from_string() { assert_exception([] { auto _ = pqxx::from_string("{}/-1"); - }, "Dimensions cannot be negative"); + }, "sparsevec must have at least 1 dimension"); assert_exception([] { auto _ = pqxx::from_string("{:}/1"); @@ -258,15 +258,15 @@ void test_sparsevec_from_string() { assert_exception([] { auto _ = pqxx::from_string("{0:1}/1"); - }, "Index out of bounds"); + }, "sparsevec index out of bounds"); assert_exception([] { auto _ = pqxx::from_string("{-2147483648:1}/1"); - }, "Index out of bounds"); + }, "sparsevec index out of bounds"); assert_exception([] { auto _ = pqxx::from_string("{2:1}/1"); - }, "Index out of bounds"); + }, "sparsevec index out of bounds"); assert_exception([] { auto _ = pqxx::from_string("{1:4e38}/1"); From 7c946e14e48bef4229a9214acc9f9a28c51c72a3 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 21:29:49 -0800 Subject: [PATCH 134/163] Updated SparseVector constructor to support zero dimensions like other constructors [skip ci] --- include/pgvector/sparsevec.hpp | 4 ++-- test/pqxx_test.cpp | 2 +- test/sparsevec_test.cpp | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 36fb4d0..ad45711 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -39,8 +39,8 @@ class SparseVector { /// Creates a sparse vector from a map of non-zero elements. SparseVector(const std::unordered_map& map, int dimensions) { - if (dimensions < 1) { - throw std::invalid_argument{"sparsevec must have at least 1 dimension"}; + if (dimensions < 0) { + throw std::invalid_argument{"sparsevec dimensions cannot be negative"}; } dimensions_ = dimensions; diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 0d2a2cc..05ef587 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -246,7 +246,7 @@ void test_sparsevec_from_string() { assert_exception([] { auto _ = pqxx::from_string("{}/-1"); - }, "sparsevec must have at least 1 dimension"); + }, "sparsevec dimensions cannot be negative"); assert_exception([] { auto _ = pqxx::from_string("{:}/1"); diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index b9d752c..d50285e 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -27,8 +27,8 @@ static void test_constructor_map() { assert_equal(vec.values() == std::vector{1, 2, 3}, true); assert_exception([&]{ - SparseVector(map, 0); - }, "sparsevec must have at least 1 dimension"); + SparseVector(map, -1); + }, "sparsevec dimensions cannot be negative"); assert_exception([&]{ SparseVector(map, 4); From 61910ee9d1cff38fb505b52e925f5a0c46ca3b10 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 21:38:35 -0800 Subject: [PATCH 135/163] Added tests for vectors with zero dimensions [skip ci] --- test/halfvec_test.cpp | 6 ++++++ test/pqxx_test.cpp | 1 + test/sparsevec_test.cpp | 9 +++++++++ test/vector_test.cpp | 6 ++++++ 4 files changed, 22 insertions(+) diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index 9ee1e1d..8402847 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -16,6 +16,11 @@ static void test_constructor_span() { assert_equal(vec.dimensions(), 3u); } +static void test_constructor_empty() { + HalfVector vec{std::vector{}}; + assert_equal(vec.dimensions(), 0u); +} + static void test_values() { HalfVector vec{{1, 2, 3}}; assert_equal(vec.values() == std::vector{1, 2, 3}, true); @@ -24,5 +29,6 @@ static void test_values() { void test_halfvec() { test_constructor_vector(); test_constructor_span(); + test_constructor_empty(); test_values(); } diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 05ef587..8e6dc8e 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -231,6 +231,7 @@ void test_sparsevec_to_string() { void test_sparsevec_from_string() { assert_equal(pqxx::from_string("{1:1,3:2,5:3}/6"), pgvector::SparseVector{{1, 0, 2, 0, 3, 0}}); assert_equal(pqxx::from_string("{}/6"), pgvector::SparseVector{{0, 0, 0, 0, 0, 0}}); + assert_equal(pqxx::from_string("{}/0"), pgvector::SparseVector{std::vector{}}); assert_exception([] { auto _ = pqxx::from_string(""); diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index d50285e..faa7851 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -35,8 +35,17 @@ static void test_constructor_map() { }, "sparsevec index out of bounds"); } +static void test_constructor_empty() { + SparseVector vec{std::vector{}}; + assert_equal(vec.dimensions(), 0); + + SparseVector vec2{{}, 0}; + assert_equal(vec2.dimensions(), 0); +} + void test_sparsevec() { test_constructor_vector(); test_constructor_span(); + test_constructor_empty(); test_constructor_map(); } diff --git a/test/vector_test.cpp b/test/vector_test.cpp index 25e64c3..1907e6c 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -16,6 +16,11 @@ static void test_constructor_span() { assert_equal(vec.dimensions(), 3u); } +static void test_constructor_empty() { + Vector vec{std::vector{}}; + assert_equal(vec.dimensions(), 0u); +} + static void test_values() { Vector vec{{1, 2, 3}}; assert_equal(vec.values() == std::vector{1, 2, 3}, true); @@ -24,5 +29,6 @@ static void test_values() { void test_vector() { test_constructor_vector(); test_constructor_span(); + test_constructor_empty(); test_values(); } From 6bbe005387cb3f0f3c55fd23ad98246335372c72 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 21:47:12 -0800 Subject: [PATCH 136/163] Improved tests [skip ci] --- test/pqxx_test.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 8e6dc8e..622debe 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -149,6 +149,8 @@ void test_vector_to_string() { void test_vector_from_string() { assert_equal(pqxx::from_string("[1,2,3]"), pgvector::Vector{{1, 2, 3}}); + + // not valid, but test current behavior assert_equal(pqxx::from_string("[]"), pgvector::Vector{std::vector{}}); assert_exception([] { @@ -191,6 +193,8 @@ void test_halfvec_to_string() { void test_halfvec_from_string() { assert_equal(pqxx::from_string("[1,2,3]"), pgvector::HalfVector{{1, 2, 3}}); + + // not valid, but test current behavior assert_equal(pqxx::from_string("[]"), pgvector::HalfVector{std::vector{}}); assert_exception([] { @@ -231,7 +235,14 @@ void test_sparsevec_to_string() { void test_sparsevec_from_string() { assert_equal(pqxx::from_string("{1:1,3:2,5:3}/6"), pgvector::SparseVector{{1, 0, 2, 0, 3, 0}}); assert_equal(pqxx::from_string("{}/6"), pgvector::SparseVector{{0, 0, 0, 0, 0, 0}}); + + // not valid, but test current behavior assert_equal(pqxx::from_string("{}/0"), pgvector::SparseVector{std::vector{}}); + assert_equal(pqxx::from_string("{1:2,1:3}/1"), pgvector::SparseVector{{2}}); + + auto vec = pqxx::from_string("{2:4,1:3}/2"); + assert_equal(vec.indices() == std::vector{0, 1}, true); + assert_equal(vec.values() == std::vector{3, 4}, true); assert_exception([] { auto _ = pqxx::from_string(""); From 61851c9034558bb2d928b6dce8e5cc77ffd68f88 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 21:59:25 -0800 Subject: [PATCH 137/163] Added comments [skip ci] --- include/pgvector/sparsevec.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index ad45711..e17bfd6 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -28,6 +28,8 @@ class SparseVector { throw std::invalid_argument{"too many dimensions"}; } dimensions_ = static_cast(value.size()); + + // do not reserve capacity for indices/values since likely many zeros for (size_t i = 0; i < value.size(); i++) { float v = value[i]; if (v != 0) { @@ -44,6 +46,7 @@ class SparseVector { } dimensions_ = dimensions; + // could probably reserve capacity for indices since not expecting zeros for (const auto& [i, v] : map) { if (i < 0 || i >= dimensions) { throw std::invalid_argument{"sparsevec index out of bounds"}; From 8c70109df45b001daba3c510d03f082d3aa76e3d Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 22:01:20 -0800 Subject: [PATCH 138/163] Improved test [skip ci] --- test/pqxx_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 622debe..0c05660 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -240,7 +240,7 @@ void test_sparsevec_from_string() { assert_equal(pqxx::from_string("{}/0"), pgvector::SparseVector{std::vector{}}); assert_equal(pqxx::from_string("{1:2,1:3}/1"), pgvector::SparseVector{{2}}); - auto vec = pqxx::from_string("{2:4,1:3}/2"); + auto vec = pqxx::from_string("{2:4,1:3,3:0}/3"); assert_equal(vec.indices() == std::vector{0, 1}, true); assert_equal(vec.values() == std::vector{3, 4}, true); From faf43524b1c06309022d01a11b5772cdfc037ca6 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 22:02:03 -0800 Subject: [PATCH 139/163] Improved test [skip ci] --- test/pqxx_test.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 0c05660..3fd360d 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -280,6 +280,10 @@ void test_sparsevec_from_string() { auto _ = pqxx::from_string("{2:1}/1"); }, "sparsevec index out of bounds"); + assert_exception([] { + auto _ = pqxx::from_string("{1:1}/0"); + }, "sparsevec index out of bounds"); + assert_exception([] { auto _ = pqxx::from_string("{1:4e38}/1"); }, float_error("Could not convert '4e38' to float: Value out of range.")); From 2659e4512a03a756353944f1a97fe843f724d2d1 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 22:18:22 -0800 Subject: [PATCH 140/163] Updated error messages [skip ci] --- include/pgvector/sparsevec.hpp | 4 ++-- test/pqxx_test.cpp | 2 +- test/sparsevec_test.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index e17bfd6..a69b161 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -25,7 +25,7 @@ class SparseVector { /// Creates a sparse vector from a span. explicit SparseVector(std::span value) { if (value.size() > std::numeric_limits::max()) { - throw std::invalid_argument{"too many dimensions"}; + throw std::invalid_argument{"sparsevec cannot have more than max int dimensions"}; } dimensions_ = static_cast(value.size()); @@ -42,7 +42,7 @@ class SparseVector { /// Creates a sparse vector from a map of non-zero elements. SparseVector(const std::unordered_map& map, int dimensions) { if (dimensions < 0) { - throw std::invalid_argument{"sparsevec dimensions cannot be negative"}; + throw std::invalid_argument{"sparsevec cannot have negative dimensions"}; } dimensions_ = dimensions; diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 3fd360d..3c7a61b 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -258,7 +258,7 @@ void test_sparsevec_from_string() { assert_exception([] { auto _ = pqxx::from_string("{}/-1"); - }, "sparsevec dimensions cannot be negative"); + }, "sparsevec cannot have negative dimensions"); assert_exception([] { auto _ = pqxx::from_string("{:}/1"); diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index faa7851..2489d3a 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -28,7 +28,7 @@ static void test_constructor_map() { assert_exception([&]{ SparseVector(map, -1); - }, "sparsevec dimensions cannot be negative"); + }, "sparsevec cannot have negative dimensions"); assert_exception([&]{ SparseVector(map, 4); From a48e54c2edb750c8829f0a7945e5fca7fbb22d24 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 22:26:14 -0800 Subject: [PATCH 141/163] Improved test --- test/sparsevec_test.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index 2489d3a..fd986a4 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -27,11 +27,15 @@ static void test_constructor_map() { assert_equal(vec.values() == std::vector{1, 2, 3}, true); assert_exception([&]{ - SparseVector(map, -1); + SparseVector{map, -1}; }, "sparsevec cannot have negative dimensions"); assert_exception([&]{ - SparseVector(map, 4); + SparseVector{map, 4}; + }, "sparsevec index out of bounds"); + + assert_exception([]{ + SparseVector{{{0, 1}}, 0}; }, "sparsevec index out of bounds"); } From f91c13c7da454c0f7f47da6cc85bc062c7953235 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 22:29:55 -0800 Subject: [PATCH 142/163] Updated comment [skip ci] --- include/pgvector/pqxx.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 5502707..c7dd3c0 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -246,7 +246,7 @@ template<> struct string_traits { if (i != 0) { here += pqxx::into_buf(buf.subspan(here), ",", c); } - // cast to prevent undefined behavior on overflow and require less buffer space + // cast to prevent undefined behavior and require less buffer space here += pqxx::into_buf(buf.subspan(here), static_cast(indices[i]) + 1, c); here += pqxx::into_buf(buf.subspan(here), ":", c); here += pqxx::into_buf(buf.subspan(here), values[i], c); From 1c89ee4cb5a285017ca9ffbaca4624a4c1b8dfa9 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 23:54:58 -0800 Subject: [PATCH 143/163] Switched to std::views::split for splitting --- include/pgvector/pqxx.hpp | 42 +++++++++++++-------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index c7dd3c0..2f2e1e0 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -35,14 +36,10 @@ template<> struct string_traits { std::vector values; if (text.size() > 2) { std::string_view inner = text.substr(1, text.size() - 2); - size_t start = 0; - for (size_t i = 0; i < inner.size(); i++) { - if (inner[i] == ',') { - values.push_back(pqxx::from_string(inner.substr(start, i - start), c)); - start = i + 1; - } + for (const auto& v : std::views::split(inner, ',')) { + std::string_view sv{v.begin(), v.end()}; + values.push_back(pqxx::from_string(sv, c)); } - values.push_back(pqxx::from_string(inner.substr(start), c)); } return pgvector::Vector{std::move(values)}; } @@ -108,14 +105,10 @@ template<> struct string_traits { std::vector values; if (text.size() > 2) { std::string_view inner = text.substr(1, text.size() - 2); - size_t start = 0; - for (size_t i = 0; i < inner.size(); i++) { - if (inner[i] == ',') { - values.push_back(static_cast(pqxx::from_string(inner.substr(start, i - start), c))); - start = i + 1; - } + for (const auto& v : std::views::split(inner, ',')) { + std::string_view sv{v.begin(), v.end()}; + values.push_back(static_cast(pqxx::from_string(sv, c))); } - values.push_back(static_cast(pqxx::from_string(inner.substr(start), c))); } return pgvector::HalfVector{std::move(values)}; } @@ -187,14 +180,17 @@ template<> struct string_traits { int dimensions = pqxx::from_string(text.substr(n + 2), c); if (n > 1) { - auto add_element = [&](std::string_view substr) { - size_t ne = substr.find(":"); + std::string_view inner = text.substr(1, n - 1); + for (const auto& v : std::views::split(inner, ',')) { + std::string_view sv{v.begin(), v.end()}; + + size_t ne = sv.find(":"); if (ne == std::string::npos) { throw conversion_error{"Malformed sparsevec literal"}; } - int index = pqxx::from_string(substr.substr(0, ne), c); - float value = pqxx::from_string(substr.substr(ne + 1), c); + int index = pqxx::from_string(sv.substr(0, ne), c); + float value = pqxx::from_string(sv.substr(ne + 1), c); // check to avoid undefined behavior if (index > std::numeric_limits::min()) { @@ -202,17 +198,7 @@ template<> struct string_traits { } map.insert({index, value}); - }; - - std::string_view inner = text.substr(1, n - 1); - size_t start = 0; - for (size_t i = 0; i < inner.size(); i++) { - if (inner[i] == ',') { - add_element(inner.substr(start, i - start)); - start = i + 1; - } } - add_element(inner.substr(start)); } try { From a89d566326fdfbbf1a40c6507ce86817a0d654cf Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 6 Mar 2026 23:58:30 -0800 Subject: [PATCH 144/163] Updated style [skip ci] --- include/pgvector/pqxx.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 2f2e1e0..b842fdc 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -176,9 +176,9 @@ template<> struct string_traits { throw conversion_error{"Malformed sparsevec literal"}; } - std::unordered_map map; int dimensions = pqxx::from_string(text.substr(n + 2), c); + std::unordered_map map; if (n > 1) { std::string_view inner = text.substr(1, n - 1); for (const auto& v : std::views::split(inner, ',')) { From d58a06913c619376a953f3a6554c3f475a9943f7 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 7 Mar 2026 00:05:03 -0800 Subject: [PATCH 145/163] Updated comment [skip ci] --- include/pgvector/pqxx.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index b842fdc..2d3d18e 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -232,7 +232,7 @@ template<> struct string_traits { if (i != 0) { here += pqxx::into_buf(buf.subspan(here), ",", c); } - // cast to prevent undefined behavior and require less buffer space + // cast to avoid undefined behavior and require less buffer space here += pqxx::into_buf(buf.subspan(here), static_cast(indices[i]) + 1, c); here += pqxx::into_buf(buf.subspan(here), ":", c); here += pqxx::into_buf(buf.subspan(here), values[i], c); From c53e285b57a5125f656004569705006aca14e5d9 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sun, 8 Mar 2026 13:15:00 -0700 Subject: [PATCH 146/163] Version bump to 0.3.0 [skip ci] --- CHANGELOG.md | 2 +- CMakeLists.txt | 2 +- README.md | 16 ++++++++++------ include/pgvector/halfvec.hpp | 2 +- include/pgvector/pqxx.hpp | 2 +- include/pgvector/sparsevec.hpp | 2 +- include/pgvector/vector.hpp | 2 +- 7 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93a15cd..7eaa3a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.3.0 (unreleased) +## 0.3.0 (2026-03-08) - Added support for libpqxx 8 - Changed `HalfVector` to use `std::float16_t` or `_Float16` when available diff --git a/CMakeLists.txt b/CMakeLists.txt index 0791742..e7f5a95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.18) -project(pgvector VERSION 0.2.4 LANGUAGES CXX) +project(pgvector VERSION 0.3.0 LANGUAGES CXX) include(GNUInstallDirs) diff --git a/README.md b/README.md index 6ead46b..7e4aecd 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ Supports [libpqxx](https://github.com/jtv/libpqxx) ## Installation -Add [the headers](https://github.com/pgvector/pgvector-cpp/tree/v0.2.4/include) to your project (supports C++17 and greater). +Add [the headers](https://github.com/pgvector/pgvector-cpp/tree/v0.3.0/include) to your project (supports C++20 and greater). There is also support for CMake and FetchContent: ```cmake include(FetchContent) -FetchContent_Declare(pgvector GIT_REPOSITORY https://github.com/pgvector/pgvector-cpp.git GIT_TAG v0.2.4) +FetchContent_Declare(pgvector GIT_REPOSITORY https://github.com/pgvector/pgvector-cpp.git GIT_TAG v0.3.0) FetchContent_MakeAvailable(pgvector) target_link_libraries(app PRIVATE pgvector::pgvector) @@ -46,6 +46,8 @@ Include the header #include ``` +The latest version works libpqxx 8. For libpqxx 7, use version 0.2.4 and [this readme](https://github.com/pgvector/pgvector-cpp/blob/v0.2.4/README.md#libpqxx). + Enable the extension ```cpp @@ -99,7 +101,7 @@ pgvector::Vector vec{std::span{{1, 2, 3}}}; Get a `std::vector` ```cpp -std::vector float_vec = static_cast>(vec); +const std::vector& values = vec.values(); ``` ### Half Vectors @@ -107,19 +109,21 @@ std::vector float_vec = static_cast>(vec); Create a half vector from a `std::vector` ```cpp -pgvector::HalfVector vec{std::vector{1, 2, 3}}; +pgvector::HalfVector vec{std::vector{1, 2, 3}}; ``` +Note: `pgvector::Half` is `std::float16_t` or `_Float16` when available, or `float` otherwise + Or a span ```cpp -pgvector::HalfVector vec{std::span{{1, 2, 3}}}; +pgvector::HalfVector vec{std::span{{1, 2, 3}}}; ``` Get a `std::vector` ```cpp -std::vector float_vec = static_cast>(vec); +const std::vector& values = vec.values(); ``` ### Sparse Vectors diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 317ad3a..56b8477 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -1,5 +1,5 @@ /* - * pgvector-cpp v0.2.4 + * pgvector-cpp v0.3.0 * https://github.com/pgvector/pgvector-cpp * MIT License */ diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 2d3d18e..48b2ad7 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -1,5 +1,5 @@ /* - * pgvector-cpp v0.2.4 + * pgvector-cpp v0.3.0 * https://github.com/pgvector/pgvector-cpp * MIT License */ diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index a69b161..4bb5cb2 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -1,5 +1,5 @@ /* - * pgvector-cpp v0.2.4 + * pgvector-cpp v0.3.0 * https://github.com/pgvector/pgvector-cpp * MIT License */ diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index 44829cb..30e5f42 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -1,5 +1,5 @@ /* - * pgvector-cpp v0.2.4 + * pgvector-cpp v0.3.0 * https://github.com/pgvector/pgvector-cpp * MIT License */ From 6ce7633eb1158ed3ca857f741eff9ad9ed2f5501 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 10 Mar 2026 02:37:42 -0700 Subject: [PATCH 147/163] Use value for std::optional for tests [skip ci] --- test/helper.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helper.hpp b/test/helper.hpp index 5c8c1ae..baadfb8 100644 --- a/test/helper.hpp +++ b/test/helper.hpp @@ -26,6 +26,6 @@ void assert_exception(std::function code, std::optional Date: Tue, 10 Mar 2026 14:15:45 -0700 Subject: [PATCH 148/163] Fixed cpplint warnings --- examples/sparse/example.cpp | 1 + include/pgvector/pqxx.hpp | 4 +++- test/halfvec_test.cpp | 1 + test/pqxx_test.cpp | 1 + test/sparsevec_test.cpp | 1 + test/vector_test.cpp | 1 + 6 files changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/sparse/example.cpp b/examples/sparse/example.cpp index 12c602b..463ab53 100644 --- a/examples/sparse/example.cpp +++ b/examples/sparse/example.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 48b2ad7..4f43166 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include @@ -185,7 +187,7 @@ template<> struct string_traits { std::string_view sv{v.begin(), v.end()}; size_t ne = sv.find(":"); - if (ne == std::string::npos) { + if (ne == std::string_view::npos) { throw conversion_error{"Malformed sparsevec literal"}; } diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index 8402847..59ce205 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -1,4 +1,5 @@ #include +#include #include diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 3c7a61b..53a0c0d 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index fd986a4..9ea72fa 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -1,5 +1,6 @@ #include #include +#include #include diff --git a/test/vector_test.cpp b/test/vector_test.cpp index 1907e6c..7c2e601 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -1,4 +1,5 @@ #include +#include #include From cb1ff15a35eed2e70daea570286f0aafb7843f83 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 10 Mar 2026 14:39:35 -0700 Subject: [PATCH 149/163] Improved style [skip ci] --- include/pgvector/sparsevec.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 4bb5cb2..0ef3b8a 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -56,7 +57,7 @@ class SparseVector { indices_.push_back(i); } } - std::sort(indices_.begin(), indices_.end()); + std::ranges::sort(indices_); values_.reserve(indices_.size()); for (const auto i : indices_) { From 3ea97a5e8a57fb9e78ccfd3bfece79ae9a3ff801 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 10 Mar 2026 15:07:02 -0700 Subject: [PATCH 150/163] Fixed clang-tidy warnings --- include/pgvector/pqxx.hpp | 1 + include/pgvector/sparsevec.hpp | 1 - test/pqxx_test.cpp | 2 ++ test/sparsevec_test.cpp | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 4f43166..70333ae 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 0ef3b8a..7e13f66 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 53a0c0d..129f954 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -1,5 +1,7 @@ +#include #include #include +#include #include #include #include diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index 9ea72fa..01d91ed 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -1,4 +1,5 @@ #include +#include #include #include From 4fe2b456724a6b4d10bb60c2e797c4fb278e18b7 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 10 Mar 2026 15:07:56 -0700 Subject: [PATCH 151/163] Fixed clang-tidy warning [skip ci] --- include/pgvector/pqxx.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 70333ae..5c0a234 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include From 2b0842f6c4577936717a082e7d39fcb88fb3109f Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 10 Mar 2026 15:14:49 -0700 Subject: [PATCH 152/163] Fixed clang-tidy warnings [skip ci] --- test/pqxx_test.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 129f954..17c5dd4 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -305,7 +305,7 @@ void test_sparsevec_from_string() { } void test_vector_to_buf() { - char buf[60]; + std::array buf{}; assert_equal(pqxx::to_buf(std::span{buf}, pgvector::Vector{{1, 2, 3}}), "[1,2,3]"); assert_exception([] { @@ -314,10 +314,10 @@ void test_vector_to_buf() { } void test_vector_into_buf() { - char buf[60]; + std::array buf{}; size_t size = pqxx::into_buf(std::span{buf}, pgvector::Vector{{1, 2, 3}}); assert_equal(size, 7u); - assert_equal(std::string_view{buf, size}, "[1,2,3]"); + assert_equal(std::string_view{buf.data(), size}, "[1,2,3]"); assert_exception([] { return pqxx::into_buf(std::span{}, pgvector::Vector{{1, 2, 3}}); @@ -325,7 +325,7 @@ void test_vector_into_buf() { } void test_halfvec_to_buf() { - char buf[60]; + std::array buf{}; assert_equal(pqxx::to_buf(std::span{buf}, pgvector::HalfVector{{1, 2, 3}}), "[1,2,3]"); assert_exception([] { @@ -334,10 +334,10 @@ void test_halfvec_to_buf() { } void test_halfvec_into_buf() { - char buf[60]; + std::array buf{}; size_t size = pqxx::into_buf(std::span{buf}, pgvector::HalfVector{{1, 2, 3}}); assert_equal(size, 7u); - assert_equal(std::string_view{buf, size}, "[1,2,3]"); + assert_equal(std::string_view{buf.data(), size}, "[1,2,3]"); assert_exception([] { return pqxx::into_buf(std::span{}, pgvector::HalfVector{{1, 2, 3}}); @@ -345,7 +345,7 @@ void test_halfvec_into_buf() { } void test_sparsevec_to_buf() { - char buf[120]; + std::array buf{}; assert_equal(pqxx::to_buf(std::span{buf}, pgvector::SparseVector{{1, 2, 3}}), "{1:1,2:2,3:3}/3"); int max = std::numeric_limits::max(); @@ -357,10 +357,10 @@ void test_sparsevec_to_buf() { } void test_sparsevec_into_buf() { - char buf[120]; + std::array buf{}; size_t size = pqxx::into_buf(std::span{buf}, pgvector::SparseVector{{1, 2, 3}}); assert_equal(size, 15u); - assert_equal(std::string_view{buf, size}, "{1:1,2:2,3:3}/3"); + assert_equal(std::string_view{buf.data(), size}, "{1:1,2:2,3:3}/3"); assert_exception([] { return pqxx::into_buf(std::span{}, pgvector::SparseVector{{1, 2, 3}}); From 6afb8775649266c6fc877bd860ac84fb7ab784ce Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 10 Mar 2026 18:07:47 -0700 Subject: [PATCH 153/163] Added bounds checking --- include/pgvector/halfvec.hpp | 9 ++++++--- include/pgvector/pqxx.hpp | 24 ++++++++++++++++-------- include/pgvector/sparsevec.hpp | 11 +++++++---- include/pgvector/vector.hpp | 7 +++++-- test/pqxx_test.cpp | 30 +++++++++++++++--------------- 5 files changed, 49 insertions(+), 32 deletions(-) diff --git a/include/pgvector/halfvec.hpp b/include/pgvector/halfvec.hpp index 56b8477..5513936 100644 --- a/include/pgvector/halfvec.hpp +++ b/include/pgvector/halfvec.hpp @@ -57,15 +57,18 @@ class HalfVector { friend std::ostream& operator<<(std::ostream& os, const HalfVector& value) { os << "["; - for (size_t i = 0; i < value.value_.size(); i++) { + // TODO use std::views::enumerate for C++23 + size_t i = 0; + for (auto v : value.value_) { if (i > 0) { os << ","; } #if __STDCPP_FLOAT16_T__ - os << value.value_[i]; + os << v; #else - os << static_cast(value.value_[i]); + os << static_cast(v); #endif + i++; } os << "]"; return os; diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 5c0a234..9f7b1dc 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -65,11 +65,14 @@ template<> struct string_traits { size_t here = 0; here += pqxx::into_buf(buf.subspan(here), "[", c); - for (size_t i = 0; i < values.size(); i++) { + // TODO use std::views::enumerate for C++23 + size_t i = 0; + for (auto v : values) { if (i != 0) { here += pqxx::into_buf(buf.subspan(here), ",", c); } - here += pqxx::into_buf(buf.subspan(here), values[i], c); + here += pqxx::into_buf(buf.subspan(here), v, c); + i++; } here += pqxx::into_buf(buf.subspan(here), "]", c); @@ -134,11 +137,14 @@ template<> struct string_traits { size_t here = 0; here += pqxx::into_buf(buf.subspan(here), "[", c); - for (size_t i = 0; i < values.size(); i++) { + // TODO use std::views::enumerate for C++23 + size_t i = 0; + for (auto v : values) { if (i != 0) { here += pqxx::into_buf(buf.subspan(here), ",", c); } - here += pqxx::into_buf(buf.subspan(here), static_cast(values[i]), c); + here += pqxx::into_buf(buf.subspan(here), static_cast(v), c); + i++; } here += pqxx::into_buf(buf.subspan(here), "]", c); @@ -232,14 +238,15 @@ template<> struct string_traits { size_t here = 0; here += pqxx::into_buf(buf.subspan(here), "{", c); + // TODO use std::views::zip for C++23 for (size_t i = 0; i < nnz; i++) { if (i != 0) { here += pqxx::into_buf(buf.subspan(here), ",", c); } // cast to avoid undefined behavior and require less buffer space - here += pqxx::into_buf(buf.subspan(here), static_cast(indices[i]) + 1, c); + here += pqxx::into_buf(buf.subspan(here), static_cast(indices.at(i)) + 1, c); here += pqxx::into_buf(buf.subspan(here), ":", c); - here += pqxx::into_buf(buf.subspan(here), values[i], c); + here += pqxx::into_buf(buf.subspan(here), values.at(i), c); } here += pqxx::into_buf(buf.subspan(here), "}/", c); @@ -259,11 +266,12 @@ template<> struct string_traits { size_t size = 0; size += pqxx::size_buffer("{"); + // TODO use std::views::zip for C++23 for (size_t i = 0; i < nnz; i++) { size += pqxx::size_buffer(","); - size += pqxx::size_buffer(static_cast(indices[i]) + 1); + size += pqxx::size_buffer(static_cast(indices.at(i)) + 1); size += pqxx::size_buffer(":"); - size += pqxx::size_buffer(values[i]); + size += pqxx::size_buffer(values.at(i)); } size += pqxx::size_buffer("}/"); size += pqxx::size_buffer(dimensions); diff --git a/include/pgvector/sparsevec.hpp b/include/pgvector/sparsevec.hpp index 7e13f66..995f7e2 100644 --- a/include/pgvector/sparsevec.hpp +++ b/include/pgvector/sparsevec.hpp @@ -30,12 +30,14 @@ class SparseVector { dimensions_ = static_cast(value.size()); // do not reserve capacity for indices/values since likely many zeros - for (size_t i = 0; i < value.size(); i++) { - float v = value[i]; + // TODO use std::views::enumerate for C++23 + size_t i = 0; + for (auto v : value) { if (v != 0) { indices_.push_back(static_cast(i)); values_.push_back(v); } + i++; } } @@ -85,13 +87,14 @@ class SparseVector { friend std::ostream& operator<<(std::ostream& os, const SparseVector& value) { os << "{"; + // TODO use std::views::zip for C++23 for (size_t i = 0; i < value.indices_.size(); i++) { if (i > 0) { os << ","; } - os << value.indices_[i] + 1; + os << value.indices_.at(i) + 1; os << ":"; - os << value.values_[i]; + os << value.values_.at(i); } os << "}/"; os << value.dimensions_; diff --git a/include/pgvector/vector.hpp b/include/pgvector/vector.hpp index 30e5f42..43804ed 100644 --- a/include/pgvector/vector.hpp +++ b/include/pgvector/vector.hpp @@ -41,11 +41,14 @@ class Vector { friend std::ostream& operator<<(std::ostream& os, const Vector& value) { os << "["; - for (size_t i = 0; i < value.value_.size(); i++) { + // TODO use std::views::enumerate for C++23 + size_t i = 0; + for (auto v : value.value_) { if (i > 0) { os << ","; } - os << value.value_[i]; + os << v; + i++; } os << "]"; return os; diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 17c5dd4..cf93a25 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -42,9 +42,9 @@ void test_vector(pqxx::connection &conn) { pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY embedding <-> $1", {embedding2}); assert_equal(res.size(), 3); - assert_equal(res[0][0].as(), embedding2); - assert_equal(res[1][0].as(), embedding); - assert_equal(res[2][0].as>().has_value(), false); + assert_equal(res.at(0).at(0).as(), embedding2); + assert_equal(res.at(1).at(0).as(), embedding); + assert_equal(res.at(2).at(0).as>().has_value(), false); } void test_halfvec(pqxx::connection &conn) { @@ -57,9 +57,9 @@ void test_halfvec(pqxx::connection &conn) { pqxx::result res = tx.exec("SELECT half_embedding FROM items ORDER BY half_embedding <-> $1", {embedding2}); assert_equal(res.size(), 3); - assert_equal(res[0][0].as(), embedding2); - assert_equal(res[1][0].as(), embedding); - assert_equal(res[2][0].as>().has_value(), false); + assert_equal(res.at(0).at(0).as(), embedding2); + assert_equal(res.at(1).at(0).as(), embedding); + assert_equal(res.at(2).at(0).as>().has_value(), false); } void test_bit(pqxx::connection &conn) { @@ -72,9 +72,9 @@ void test_bit(pqxx::connection &conn) { pqxx::result res = tx.exec("SELECT binary_embedding FROM items ORDER BY binary_embedding <~> $1", pqxx::params{embedding2}); assert_equal(res.size(), 3); - assert_equal(res[0][0].as(), embedding2); - assert_equal(res[1][0].as(), embedding); - assert_equal(res[2][0].as>().has_value(), false); + assert_equal(res.at(0).at(0).as(), embedding2); + assert_equal(res.at(1).at(0).as(), embedding); + assert_equal(res.at(2).at(0).as>().has_value(), false); } void test_sparsevec(pqxx::connection &conn) { @@ -87,9 +87,9 @@ void test_sparsevec(pqxx::connection &conn) { pqxx::result res = tx.exec("SELECT sparse_embedding FROM items ORDER BY sparse_embedding <-> $1", {embedding2}); assert_equal(res.size(), 3); - assert_equal(res[0][0].as(), embedding2); - assert_equal(res[1][0].as(), embedding); - assert_equal(res[2][0].as>().has_value(), false); + assert_equal(res.at(0).at(0).as(), embedding2); + assert_equal(res.at(1).at(0).as(), embedding); + assert_equal(res.at(2).at(0).as>().has_value(), false); } void test_sparsevec_nnz(pqxx::connection &conn) { @@ -126,8 +126,8 @@ void test_stream_to(pqxx::connection &conn) { stream.write_values(pgvector::Vector{{4, 5, 6}}); stream.complete(); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY id"); - assert_equal(res[0][0].as(), "[1,2,3]"); - assert_equal(res[1][0].as(), "[4,5,6]"); + assert_equal(res.at(0).at(0).as(), "[1,2,3]"); + assert_equal(res.at(1).at(0).as(), "[4,5,6]"); } void test_precision(pqxx::connection &conn) { @@ -138,7 +138,7 @@ void test_precision(pqxx::connection &conn) { tx.exec("INSERT INTO items (embedding) VALUES ($1)", {embedding}); tx.exec("SET extra_float_digits = 3"); pqxx::result res = tx.exec("SELECT embedding FROM items ORDER BY id DESC LIMIT 1"); - assert_equal(res[0][0].as(), embedding); + assert_equal(res.at(0).at(0).as(), embedding); } void test_vector_to_string() { From bfa1b091f15274aa6e983c3cf83af0c26184f493 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 11 Mar 2026 21:16:35 -0700 Subject: [PATCH 154/163] Added more tests --- test/halfvec_test.cpp | 9 +++++++++ test/sparsevec_test.cpp | 9 +++++++++ test/vector_test.cpp | 9 +++++++++ 3 files changed, 27 insertions(+) diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index 59ce205..3589c1d 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -27,9 +28,17 @@ static void test_values() { assert_equal(vec.values() == std::vector{1, 2, 3}, true); } +static void test_string() { + HalfVector vec{{1, 2, 3}}; + std::ostringstream oss; + oss << vec; + assert_equal(oss.str(), "[1,2,3]"); +} + void test_halfvec() { test_constructor_vector(); test_constructor_span(); test_constructor_empty(); test_values(); + test_string(); } diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index 01d91ed..609db40 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -49,9 +50,17 @@ static void test_constructor_empty() { assert_equal(vec2.dimensions(), 0); } +static void test_string() { + SparseVector vec{std::vector{1, 0, 2, 0, 3, 0}}; + std::ostringstream oss; + oss << vec; + assert_equal(oss.str(), "{1:1,3:2,5:3}/6"); +} + void test_sparsevec() { test_constructor_vector(); test_constructor_span(); test_constructor_empty(); test_constructor_map(); + test_string(); } diff --git a/test/vector_test.cpp b/test/vector_test.cpp index 7c2e601..2b83a56 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -27,9 +28,17 @@ static void test_values() { assert_equal(vec.values() == std::vector{1, 2, 3}, true); } +static void test_string() { + Vector vec{{1, 2, 3}}; + std::ostringstream oss; + oss << vec; + assert_equal(oss.str(), "[1,2,3]"); +} + void test_vector() { test_constructor_vector(); test_constructor_span(); test_constructor_empty(); test_values(); + test_string(); } From 9de20b8326bd86530d3b477d0e3de5577ab16566 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 11 Mar 2026 21:27:31 -0700 Subject: [PATCH 155/163] Added more tests --- test/halfvec_test.cpp | 6 ++++++ test/sparsevec_test.cpp | 18 ++++++++++++++++++ test/vector_test.cpp | 6 ++++++ 3 files changed, 30 insertions(+) diff --git a/test/halfvec_test.cpp b/test/halfvec_test.cpp index 3589c1d..94c68f6 100644 --- a/test/halfvec_test.cpp +++ b/test/halfvec_test.cpp @@ -23,6 +23,11 @@ static void test_constructor_empty() { assert_equal(vec.dimensions(), 0u); } +static void test_dimensions() { + HalfVector vec{{1, 2, 3}}; + assert_equal(vec.dimensions(), 3u); +} + static void test_values() { HalfVector vec{{1, 2, 3}}; assert_equal(vec.values() == std::vector{1, 2, 3}, true); @@ -39,6 +44,7 @@ void test_halfvec() { test_constructor_vector(); test_constructor_span(); test_constructor_empty(); + test_dimensions(); test_values(); test_string(); } diff --git a/test/sparsevec_test.cpp b/test/sparsevec_test.cpp index 609db40..b2dd65e 100644 --- a/test/sparsevec_test.cpp +++ b/test/sparsevec_test.cpp @@ -50,6 +50,21 @@ static void test_constructor_empty() { assert_equal(vec2.dimensions(), 0); } +static void test_dimensions() { + SparseVector vec{std::vector{1, 0, 2, 0, 3, 0}}; + assert_equal(vec.dimensions(), 6); +} + +static void test_indices() { + SparseVector vec{std::vector{1, 0, 2, 0, 3, 0}}; + assert_equal(vec.indices() == std::vector{0, 2, 4}, true); +} + +static void test_values() { + SparseVector vec{std::vector{1, 0, 2, 0, 3, 0}}; + assert_equal(vec.values() == std::vector{1, 2, 3}, true); +} + static void test_string() { SparseVector vec{std::vector{1, 0, 2, 0, 3, 0}}; std::ostringstream oss; @@ -62,5 +77,8 @@ void test_sparsevec() { test_constructor_span(); test_constructor_empty(); test_constructor_map(); + test_dimensions(); + test_indices(); + test_values(); test_string(); } diff --git a/test/vector_test.cpp b/test/vector_test.cpp index 2b83a56..44b1d7a 100644 --- a/test/vector_test.cpp +++ b/test/vector_test.cpp @@ -23,6 +23,11 @@ static void test_constructor_empty() { assert_equal(vec.dimensions(), 0u); } +static void test_dimensions() { + Vector vec{{1, 2, 3}}; + assert_equal(vec.dimensions(), 3u); +} + static void test_values() { Vector vec{{1, 2, 3}}; assert_equal(vec.values() == std::vector{1, 2, 3}, true); @@ -39,6 +44,7 @@ void test_vector() { test_constructor_vector(); test_constructor_span(); test_constructor_empty(); + test_dimensions(); test_values(); test_string(); } From 7081d0667e4dbc504fa8958542c8d215390f5869 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 12 Mar 2026 12:10:07 -0700 Subject: [PATCH 156/163] Improved code [skip ci] --- include/pgvector/pqxx.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 9f7b1dc..5ba64d1 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -194,7 +194,7 @@ template<> struct string_traits { for (const auto& v : std::views::split(inner, ',')) { std::string_view sv{v.begin(), v.end()}; - size_t ne = sv.find(":"); + size_t ne = sv.find(':'); if (ne == std::string_view::npos) { throw conversion_error{"Malformed sparsevec literal"}; } From 16f6a1d106522a5da9da9a2ec3bf4a849b739dc6 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 12 Mar 2026 12:11:08 -0700 Subject: [PATCH 157/163] Improved code [skip ci] --- test/helper.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helper.hpp b/test/helper.hpp index baadfb8..7f7f9db 100644 --- a/test/helper.hpp +++ b/test/helper.hpp @@ -17,7 +17,7 @@ void assert_equal(const T& left, const U& right, const std::source_location& loc } template -void assert_exception(std::function code, std::optional message = std::nullopt) { +void assert_exception(const std::function& code, std::optional message = std::nullopt) { std::optional exception; try { code(); From a86725e1980413dbcb5d8393af10542e0704686b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 12 Mar 2026 12:51:41 -0700 Subject: [PATCH 158/163] Updated style [skip ci] --- test/pqxx_test.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index cf93a25..06fe419 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -12,14 +12,14 @@ #include "helper.hpp" -void setup(pqxx::connection &conn) { +void setup(pqxx::connection& conn) { pqxx::nontransaction tx{conn}; tx.exec("CREATE EXTENSION IF NOT EXISTS vector"); tx.exec("DROP TABLE IF EXISTS items"); tx.exec("CREATE TABLE items (id serial PRIMARY KEY, embedding vector(3), half_embedding halfvec(3), binary_embedding bit(3), sparse_embedding sparsevec(3))"); } -void before_each(pqxx::connection &conn) { +void before_each(pqxx::connection& conn) { pqxx::nontransaction tx{conn}; tx.exec("TRUNCATE items"); } @@ -32,7 +32,7 @@ std::optional float_error([[maybe_unused]] std::string_view me #endif } -void test_vector(pqxx::connection &conn) { +void test_vector(pqxx::connection& conn) { before_each(conn); pqxx::nontransaction tx{conn}; @@ -47,7 +47,7 @@ void test_vector(pqxx::connection &conn) { assert_equal(res.at(2).at(0).as>().has_value(), false); } -void test_halfvec(pqxx::connection &conn) { +void test_halfvec(pqxx::connection& conn) { before_each(conn); pqxx::nontransaction tx{conn}; @@ -62,7 +62,7 @@ void test_halfvec(pqxx::connection &conn) { assert_equal(res.at(2).at(0).as>().has_value(), false); } -void test_bit(pqxx::connection &conn) { +void test_bit(pqxx::connection& conn) { before_each(conn); pqxx::nontransaction tx{conn}; @@ -77,7 +77,7 @@ void test_bit(pqxx::connection &conn) { assert_equal(res.at(2).at(0).as>().has_value(), false); } -void test_sparsevec(pqxx::connection &conn) { +void test_sparsevec(pqxx::connection& conn) { before_each(conn); pqxx::nontransaction tx{conn}; @@ -92,7 +92,7 @@ void test_sparsevec(pqxx::connection &conn) { assert_equal(res.at(2).at(0).as>().has_value(), false); } -void test_sparsevec_nnz(pqxx::connection &conn) { +void test_sparsevec_nnz(pqxx::connection& conn) { before_each(conn); pqxx::nontransaction tx{conn}; @@ -103,7 +103,7 @@ void test_sparsevec_nnz(pqxx::connection &conn) { }, "sparsevec cannot have more than 16000 dimensions"); } -void test_stream(pqxx::connection &conn) { +void test_stream(pqxx::connection& conn) { before_each(conn); pqxx::nontransaction tx{conn}; @@ -117,7 +117,7 @@ void test_stream(pqxx::connection &conn) { assert_equal(count, 1); } -void test_stream_to(pqxx::connection &conn) { +void test_stream_to(pqxx::connection& conn) { before_each(conn); pqxx::nontransaction tx{conn}; @@ -130,7 +130,7 @@ void test_stream_to(pqxx::connection &conn) { assert_equal(res.at(1).at(0).as(), "[4,5,6]"); } -void test_precision(pqxx::connection &conn) { +void test_precision(pqxx::connection& conn) { before_each(conn); pqxx::nontransaction tx{conn}; From f9127d4096d2d198e1bfbed7433a18400aae1fde Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 14 Mar 2026 20:18:15 -0700 Subject: [PATCH 159/163] Updated style [skip ci] --- examples/cohere/example.cpp | 4 ++-- examples/disco/example.cpp | 2 +- examples/openai/example.cpp | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/cohere/example.cpp b/examples/cohere/example.cpp index aba2cc6..51342d7 100644 --- a/examples/cohere/example.cpp +++ b/examples/cohere/example.cpp @@ -12,7 +12,7 @@ using json = nlohmann::json; // https://docs.cohere.com/reference/embed -std::vector embed(const std::vector& texts, const std::string& input_type, char *api_key) { +std::vector embed(const std::vector& texts, const std::string& input_type, char* api_key) { std::string url{"https://api.cohere.com/v2/embed"}; json data{ {"texts", texts}, @@ -45,7 +45,7 @@ std::vector embed(const std::vector& texts, const std: } int main() { - char *api_key = std::getenv("CO_API_KEY"); + char* api_key = std::getenv("CO_API_KEY"); if (!api_key) { std::cout << "Set CO_API_KEY" << std::endl; return 1; diff --git a/examples/disco/example.cpp b/examples/disco/example.cpp index 0e22fe2..b87f631 100644 --- a/examples/disco/example.cpp +++ b/examples/disco/example.cpp @@ -59,7 +59,7 @@ Dataset load_movielens(const std::string& path) { int main() { // https://grouplens.org/datasets/movielens/100k/ - char *movielens_path = std::getenv("MOVIELENS_100K_PATH"); + char* movielens_path = std::getenv("MOVIELENS_100K_PATH"); if (!movielens_path) { std::cout << "Set MOVIELENS_100K_PATH" << std::endl; return 1; diff --git a/examples/openai/example.cpp b/examples/openai/example.cpp index eddf6be..9098192 100644 --- a/examples/openai/example.cpp +++ b/examples/openai/example.cpp @@ -11,7 +11,7 @@ using json = nlohmann::json; // https://platform.openai.com/docs/guides/embeddings/how-to-get-embeddings // input can be an array with 2048 elements -std::vector> embed(const std::vector& input, char *api_key) { +std::vector> embed(const std::vector& input, char* api_key) { std::string url{"https://api.openai.com/v1/embeddings"}; json data{ {"input", input}, @@ -37,7 +37,7 @@ std::vector> embed(const std::vector& input, cha } int main() { - char *api_key = std::getenv("OPENAI_API_KEY"); + char* api_key = std::getenv("OPENAI_API_KEY"); if (!api_key) { std::cout << "Set OPENAI_API_KEY" << std::endl; return 1; From d2027eb6739a726d136ea7de175c6eee51976208 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 14 Mar 2026 20:21:04 -0700 Subject: [PATCH 160/163] Updated style [skip ci] --- examples/cohere/example.cpp | 6 +++++- examples/hybrid/example.cpp | 5 ++++- examples/rdkit/example.cpp | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/examples/cohere/example.cpp b/examples/cohere/example.cpp index 51342d7..e076476 100644 --- a/examples/cohere/example.cpp +++ b/examples/cohere/example.cpp @@ -12,7 +12,11 @@ using json = nlohmann::json; // https://docs.cohere.com/reference/embed -std::vector embed(const std::vector& texts, const std::string& input_type, char* api_key) { +std::vector embed( + const std::vector& texts, + const std::string& input_type, + char* api_key +) { std::string url{"https://api.cohere.com/v2/embed"}; json data{ {"texts", texts}, diff --git a/examples/hybrid/example.cpp b/examples/hybrid/example.cpp index ed368a6..7cd81f6 100644 --- a/examples/hybrid/example.cpp +++ b/examples/hybrid/example.cpp @@ -13,7 +13,10 @@ using json = nlohmann::json; -std::vector> embed(const std::vector& texts, const std::string& taskType) { +std::vector> embed( + const std::vector& texts, + const std::string& taskType +) { // nomic-embed-text-v1.5 uses a task prefix // https://huggingface.co/nomic-ai/nomic-embed-text-v1.5 std::vector input; diff --git a/examples/rdkit/example.cpp b/examples/rdkit/example.cpp index 9111763..8caf453 100644 --- a/examples/rdkit/example.cpp +++ b/examples/rdkit/example.cpp @@ -4,8 +4,8 @@ #include #include -#include #include +#include #include #include From b25dc3654d3b6527f987853e7574b940c2a39959 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 14 Mar 2026 20:22:09 -0700 Subject: [PATCH 161/163] Updated style [skip ci] --- include/pgvector/pqxx.hpp | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/include/pgvector/pqxx.hpp b/include/pgvector/pqxx.hpp index 5ba64d1..4fd5893 100644 --- a/include/pgvector/pqxx.hpp +++ b/include/pgvector/pqxx.hpp @@ -25,13 +25,16 @@ /// @cond namespace pqxx { -template<> inline constexpr std::string_view name_type() noexcept { +template<> +inline constexpr std::string_view name_type() noexcept { return "vector"; }; -template<> struct nullness : no_null {}; +template<> +struct nullness : no_null {}; -template<> struct string_traits { +template<> +struct string_traits { static pgvector::Vector from_string(std::string_view text, ctx c = {}) { if (text.size() < 2 || text.front() != '[' || text.back() != ']') { throw conversion_error{"Malformed vector literal"}; @@ -97,13 +100,16 @@ template<> struct string_traits { } }; -template<> inline constexpr std::string_view name_type() noexcept { +template<> +inline constexpr std::string_view name_type() noexcept { return "halfvec"; }; -template<> struct nullness : no_null {}; +template<> +struct nullness : no_null {}; -template<> struct string_traits { +template<> +struct string_traits { static pgvector::HalfVector from_string(std::string_view text, ctx c = {}) { if (text.size() < 2 || text.front() != '[' || text.back() != ']') { throw conversion_error{"Malformed halfvec literal"}; @@ -169,13 +175,16 @@ template<> struct string_traits { } }; -template<> inline constexpr std::string_view name_type() noexcept { +template<> +inline constexpr std::string_view name_type() noexcept { return "sparsevec"; }; -template<> struct nullness : no_null {}; +template<> +struct nullness : no_null {}; -template<> struct string_traits { +template<> +struct string_traits { static pgvector::SparseVector from_string(std::string_view text, ctx c = {}) { if (text.size() < 4 || text.front() != '{') { throw conversion_error{"Malformed sparsevec literal"}; From 6d3c5d319c6c6488cd563646ef9e1806504dcefe Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 14 Mar 2026 20:23:35 -0700 Subject: [PATCH 162/163] Updated style [skip ci] --- test/helper.hpp | 11 +++++++++-- test/pqxx_test.cpp | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/test/helper.hpp b/test/helper.hpp index 7f7f9db..08d3e00 100644 --- a/test/helper.hpp +++ b/test/helper.hpp @@ -7,7 +7,11 @@ #include template -void assert_equal(const T& left, const U& right, const std::source_location& loc = std::source_location::current()) { +void assert_equal( + const T& left, + const U& right, + const std::source_location& loc = std::source_location::current() +) { if (left != right) { std::ostringstream message; message << left << " != " << right; @@ -17,7 +21,10 @@ void assert_equal(const T& left, const U& right, const std::source_location& loc } template -void assert_exception(const std::function& code, std::optional message = std::nullopt) { +void assert_exception( + const std::function& code, + std::optional message = std::nullopt +) { std::optional exception; try { code(); diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 06fe419..1a58142 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -7,8 +7,8 @@ #include #include -#include #include +#include #include "helper.hpp" From d3f5414f7c79f1e3ae976aa7c1ac0ebf92341461 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 14 Mar 2026 20:26:19 -0700 Subject: [PATCH 163/163] Updated style [skip ci] --- test/pqxx_test.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/pqxx_test.cpp b/test/pqxx_test.cpp index 1a58142..f87e215 100644 --- a/test/pqxx_test.cpp +++ b/test/pqxx_test.cpp @@ -110,7 +110,8 @@ void test_stream(pqxx::connection& conn) { pgvector::Vector embedding({1, 2, 3}); tx.exec("INSERT INTO items (embedding) VALUES ($1)", {embedding}); int count = 0; - for (const auto& [id, embedding2] : tx.stream("SELECT id, embedding FROM items WHERE embedding IS NOT NULL")) { + auto stream = tx.stream("SELECT id, embedding FROM items WHERE embedding IS NOT NULL"); + for (const auto& [id, embedding2] : stream) { assert_equal(embedding2, embedding); count++; }