From 00cdd176f8fdfbffb2fb73720fbff8956d6a473d Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 13:38:07 -0300 Subject: [PATCH 01/19] update --- bindings/dart/lib/src/ffi/bindings.dart | 171 ++++++++++++++++++++++++ bindings/julia/src/c_api.jl | 24 ++++ bindings/julia/src/database.jl | 88 ++++++++++++ include/psr/c/database.h | 27 ++++ include/psr/database.h | 8 ++ src/c_api_database.cpp | 147 ++++++++++++++++++++ src/database.cpp | 109 +++++++++++++++ 7 files changed, 574 insertions(+) diff --git a/bindings/dart/lib/src/ffi/bindings.dart b/bindings/dart/lib/src/ffi/bindings.dart index e0e2dfe..91d30ed 100644 --- a/bindings/dart/lib/src/ffi/bindings.dart +++ b/bindings/dart/lib/src/ffi/bindings.dart @@ -373,6 +373,117 @@ class PsrDatabaseBindings { ffi.Pointer>>, ffi.Pointer)>(); + int psr_database_read_vector_ints( + ffi.Pointer db, + ffi.Pointer collection, + ffi.Pointer attribute, + ffi.Pointer>> out_vectors, + ffi.Pointer> out_sizes, + ffi.Pointer out_count, + ) { + return _psr_database_read_vector_ints( + db, + collection, + attribute, + out_vectors, + out_sizes, + out_count, + ); + } + + late final _psr_database_read_vector_intsPtr = _lookup< + ffi.NativeFunction< + ffi.Int32 Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>, + ffi.Pointer>, + ffi.Pointer)>>('psr_database_read_vector_ints'); + late final _psr_database_read_vector_ints = + _psr_database_read_vector_intsPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>, + ffi.Pointer>, + ffi.Pointer)>(); + + int psr_database_read_vector_doubles( + ffi.Pointer db, + ffi.Pointer collection, + ffi.Pointer attribute, + ffi.Pointer>> out_vectors, + ffi.Pointer> out_sizes, + ffi.Pointer out_count, + ) { + return _psr_database_read_vector_doubles( + db, + collection, + attribute, + out_vectors, + out_sizes, + out_count, + ); + } + + late final _psr_database_read_vector_doublesPtr = _lookup< + ffi.NativeFunction< + ffi.Int32 Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>, + ffi.Pointer>, + ffi.Pointer)>>('psr_database_read_vector_doubles'); + late final _psr_database_read_vector_doubles = + _psr_database_read_vector_doublesPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>, + ffi.Pointer>, + ffi.Pointer)>(); + + int psr_database_read_vector_strings( + ffi.Pointer db, + ffi.Pointer collection, + ffi.Pointer attribute, + ffi.Pointer>>> out_vectors, + ffi.Pointer> out_sizes, + ffi.Pointer out_count, + ) { + return _psr_database_read_vector_strings( + db, + collection, + attribute, + out_vectors, + out_sizes, + out_count, + ); + } + + late final _psr_database_read_vector_stringsPtr = _lookup< + ffi.NativeFunction< + ffi.Int32 Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>>, + ffi.Pointer>, + ffi.Pointer)>>('psr_database_read_vector_strings'); + late final _psr_database_read_vector_strings = + _psr_database_read_vector_stringsPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>>, + ffi.Pointer>, + ffi.Pointer)>(); + void psr_free_int_array( ffi.Pointer values, ) { @@ -418,6 +529,66 @@ class PsrDatabaseBindings { late final _psr_free_string_array = _psr_free_string_arrayPtr .asFunction>, int)>(); + void psr_free_int_vectors( + ffi.Pointer> vectors, + ffi.Pointer sizes, + int count, + ) { + return _psr_free_int_vectors( + vectors, + sizes, + count, + ); + } + + late final _psr_free_int_vectorsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer>, + ffi.Pointer, ffi.Size)>>('psr_free_int_vectors'); + late final _psr_free_int_vectors = _psr_free_int_vectorsPtr.asFunction< + void Function( + ffi.Pointer>, ffi.Pointer, int)>(); + + void psr_free_double_vectors( + ffi.Pointer> vectors, + ffi.Pointer sizes, + int count, + ) { + return _psr_free_double_vectors( + vectors, + sizes, + count, + ); + } + + late final _psr_free_double_vectorsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer>, + ffi.Pointer, ffi.Size)>>('psr_free_double_vectors'); + late final _psr_free_double_vectors = _psr_free_double_vectorsPtr.asFunction< + void Function( + ffi.Pointer>, ffi.Pointer, int)>(); + + void psr_free_string_vectors( + ffi.Pointer>> vectors, + ffi.Pointer sizes, + int count, + ) { + return _psr_free_string_vectors( + vectors, + sizes, + count, + ); + } + + late final _psr_free_string_vectorsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer>>, + ffi.Pointer, ffi.Size)>>('psr_free_string_vectors'); + late final _psr_free_string_vectors = _psr_free_string_vectorsPtr.asFunction< + void Function(ffi.Pointer>>, + ffi.Pointer, int)>(); + ffi.Pointer psr_element_create() { return _psr_element_create(); } diff --git a/bindings/julia/src/c_api.jl b/bindings/julia/src/c_api.jl index 34c3051..b06f182 100644 --- a/bindings/julia/src/c_api.jl +++ b/bindings/julia/src/c_api.jl @@ -146,6 +146,30 @@ function psr_free_string_array(values, count) @ccall libpsr_database_c.psr_free_string_array(values::Ptr{Ptr{Cchar}}, count::Csize_t)::Cvoid end +function psr_database_read_vector_ints(db, collection, attribute, out_vectors, out_sizes, out_count) + @ccall libpsr_database_c.psr_database_read_vector_ints(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, out_vectors::Ptr{Ptr{Ptr{Int64}}}, out_sizes::Ptr{Ptr{Csize_t}}, out_count::Ptr{Csize_t})::psr_error_t +end + +function psr_database_read_vector_doubles(db, collection, attribute, out_vectors, out_sizes, out_count) + @ccall libpsr_database_c.psr_database_read_vector_doubles(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, out_vectors::Ptr{Ptr{Ptr{Cdouble}}}, out_sizes::Ptr{Ptr{Csize_t}}, out_count::Ptr{Csize_t})::psr_error_t +end + +function psr_database_read_vector_strings(db, collection, attribute, out_vectors, out_sizes, out_count) + @ccall libpsr_database_c.psr_database_read_vector_strings(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, out_vectors::Ptr{Ptr{Ptr{Ptr{Cchar}}}}, out_sizes::Ptr{Ptr{Csize_t}}, out_count::Ptr{Csize_t})::psr_error_t +end + +function psr_free_int_vectors(vectors, sizes, count) + @ccall libpsr_database_c.psr_free_int_vectors(vectors::Ptr{Ptr{Int64}}, sizes::Ptr{Csize_t}, count::Csize_t)::Cvoid +end + +function psr_free_double_vectors(vectors, sizes, count) + @ccall libpsr_database_c.psr_free_double_vectors(vectors::Ptr{Ptr{Cdouble}}, sizes::Ptr{Csize_t}, count::Csize_t)::Cvoid +end + +function psr_free_string_vectors(vectors, sizes, count) + @ccall libpsr_database_c.psr_free_string_vectors(vectors::Ptr{Ptr{Ptr{Cchar}}}, sizes::Ptr{Csize_t}, count::Csize_t)::Cvoid +end + function psr_element_create() @ccall libpsr_database_c.psr_element_create()::Ptr{psr_element_t} end diff --git a/bindings/julia/src/database.jl b/bindings/julia/src/database.jl index 79cba01..0640d66 100644 --- a/bindings/julia/src/database.jl +++ b/bindings/julia/src/database.jl @@ -89,3 +89,91 @@ function read_scalar_strings(db::Database, collection::String, attribute::String C.psr_free_string_array(out_values[], count) return result end + +function read_vector_ints(db::Database, collection::String, attribute::String) + out_vectors = Ref{Ptr{Ptr{Int64}}}(C_NULL) + out_sizes = Ref{Ptr{Csize_t}}(C_NULL) + out_count = Ref{Csize_t}(0) + + err = C.psr_database_read_vector_ints(db.ptr, collection, attribute, out_vectors, out_sizes, out_count) + if err != C.PSR_OK + throw(DatabaseException("Failed to read vector ints from '$collection.$attribute'")) + end + + count = out_count[] + if count == 0 || out_vectors[] == C_NULL + return Vector{Int64}[] + end + + vectors_ptr = unsafe_wrap(Array, out_vectors[], count) + sizes_ptr = unsafe_wrap(Array, out_sizes[], count) + result = Vector{Int64}[] + for i in 1:count + if vectors_ptr[i] == C_NULL || sizes_ptr[i] == 0 + push!(result, Int64[]) + else + push!(result, copy(unsafe_wrap(Array, vectors_ptr[i], sizes_ptr[i]))) + end + end + C.psr_free_int_vectors(out_vectors[], out_sizes[], count) + return result +end + +function read_vector_doubles(db::Database, collection::String, attribute::String) + out_vectors = Ref{Ptr{Ptr{Cdouble}}}(C_NULL) + out_sizes = Ref{Ptr{Csize_t}}(C_NULL) + out_count = Ref{Csize_t}(0) + + err = C.psr_database_read_vector_doubles(db.ptr, collection, attribute, out_vectors, out_sizes, out_count) + if err != C.PSR_OK + throw(DatabaseException("Failed to read vector doubles from '$collection.$attribute'")) + end + + count = out_count[] + if count == 0 || out_vectors[] == C_NULL + return Vector{Float64}[] + end + + vectors_ptr = unsafe_wrap(Array, out_vectors[], count) + sizes_ptr = unsafe_wrap(Array, out_sizes[], count) + result = Vector{Float64}[] + for i in 1:count + if vectors_ptr[i] == C_NULL || sizes_ptr[i] == 0 + push!(result, Float64[]) + else + push!(result, copy(unsafe_wrap(Array, vectors_ptr[i], sizes_ptr[i]))) + end + end + C.psr_free_double_vectors(out_vectors[], out_sizes[], count) + return result +end + +function read_vector_strings(db::Database, collection::String, attribute::String) + out_vectors = Ref{Ptr{Ptr{Ptr{Cchar}}}}(C_NULL) + out_sizes = Ref{Ptr{Csize_t}}(C_NULL) + out_count = Ref{Csize_t}(0) + + err = C.psr_database_read_vector_strings(db.ptr, collection, attribute, out_vectors, out_sizes, out_count) + if err != C.PSR_OK + throw(DatabaseException("Failed to read vector strings from '$collection.$attribute'")) + end + + count = out_count[] + if count == 0 || out_vectors[] == C_NULL + return Vector{String}[] + end + + vectors_ptr = unsafe_wrap(Array, out_vectors[], count) + sizes_ptr = unsafe_wrap(Array, out_sizes[], count) + result = Vector{String}[] + for i in 1:count + if vectors_ptr[i] == C_NULL || sizes_ptr[i] == 0 + push!(result, String[]) + else + str_ptrs = unsafe_wrap(Array, vectors_ptr[i], sizes_ptr[i]) + push!(result, [unsafe_string(ptr) for ptr in str_ptrs]) + end + end + C.psr_free_string_vectors(out_vectors[], out_sizes[], count) + return result +end diff --git a/include/psr/c/database.h b/include/psr/c/database.h index 095588b..d6b5cb4 100644 --- a/include/psr/c/database.h +++ b/include/psr/c/database.h @@ -71,11 +71,38 @@ PSR_C_API psr_error_t psr_database_read_scalar_strings(psr_database_t* db, char*** out_values, size_t* out_count); +// Read vector attributes +PSR_C_API psr_error_t psr_database_read_vector_ints(psr_database_t* db, + const char* collection, + const char* attribute, + int64_t*** out_vectors, + size_t** out_sizes, + size_t* out_count); + +PSR_C_API psr_error_t psr_database_read_vector_doubles(psr_database_t* db, + const char* collection, + const char* attribute, + double*** out_vectors, + size_t** out_sizes, + size_t* out_count); + +PSR_C_API psr_error_t psr_database_read_vector_strings(psr_database_t* db, + const char* collection, + const char* attribute, + char**** out_vectors, + size_t** out_sizes, + size_t* out_count); + // Memory cleanup for read results PSR_C_API void psr_free_int_array(int64_t* values); PSR_C_API void psr_free_double_array(double* values); PSR_C_API void psr_free_string_array(char** values, size_t count); +// Memory cleanup for vector read results +PSR_C_API void psr_free_int_vectors(int64_t** vectors, size_t* sizes, size_t count); +PSR_C_API void psr_free_double_vectors(double** vectors, size_t* sizes, size_t count); +PSR_C_API void psr_free_string_vectors(char*** vectors, size_t* sizes, size_t count); + #ifdef __cplusplus } #endif diff --git a/include/psr/database.h b/include/psr/database.h index 29c87d7..8ecde3c 100644 --- a/include/psr/database.h +++ b/include/psr/database.h @@ -55,6 +55,14 @@ class PSR_API Database { std::vector read_scalar_doubles(const std::string& collection, const std::string& attribute); std::vector read_scalar_strings(const std::string& collection, const std::string& attribute); + // Read vector attributes + std::vector> read_vector_ints(const std::string& collection, + const std::string& attribute); + std::vector> read_vector_doubles(const std::string& collection, + const std::string& attribute); + std::vector> read_vector_strings(const std::string& collection, + const std::string& attribute); + // Transaction management void begin_transaction(); void commit(); diff --git a/src/c_api_database.cpp b/src/c_api_database.cpp index f644096..a6bc9cf 100644 --- a/src/c_api_database.cpp +++ b/src/c_api_database.cpp @@ -299,4 +299,151 @@ PSR_C_API void psr_free_string_array(char** values, size_t count) { delete[] values; } +PSR_C_API psr_error_t psr_database_read_vector_ints(psr_database_t* db, + const char* collection, + const char* attribute, + int64_t*** out_vectors, + size_t** out_sizes, + size_t* out_count) { + if (!db || !collection || !attribute || !out_vectors || !out_sizes || !out_count) { + return PSR_ERROR_INVALID_ARGUMENT; + } + + try { + auto vectors = db->db.read_vector_ints(collection, attribute); + *out_count = vectors.size(); + if (vectors.empty()) { + *out_vectors = nullptr; + *out_sizes = nullptr; + return PSR_OK; + } + *out_vectors = new int64_t*[vectors.size()]; + *out_sizes = new size_t[vectors.size()]; + for (size_t i = 0; i < vectors.size(); ++i) { + (*out_sizes)[i] = vectors[i].size(); + if (vectors[i].empty()) { + (*out_vectors)[i] = nullptr; + } else { + (*out_vectors)[i] = new int64_t[vectors[i].size()]; + std::copy(vectors[i].begin(), vectors[i].end(), (*out_vectors)[i]); + } + } + return PSR_OK; + } catch (const std::exception&) { + return PSR_ERROR_DATABASE; + } +} + +PSR_C_API psr_error_t psr_database_read_vector_doubles(psr_database_t* db, + const char* collection, + const char* attribute, + double*** out_vectors, + size_t** out_sizes, + size_t* out_count) { + if (!db || !collection || !attribute || !out_vectors || !out_sizes || !out_count) { + return PSR_ERROR_INVALID_ARGUMENT; + } + + try { + auto vectors = db->db.read_vector_doubles(collection, attribute); + *out_count = vectors.size(); + if (vectors.empty()) { + *out_vectors = nullptr; + *out_sizes = nullptr; + return PSR_OK; + } + *out_vectors = new double*[vectors.size()]; + *out_sizes = new size_t[vectors.size()]; + for (size_t i = 0; i < vectors.size(); ++i) { + (*out_sizes)[i] = vectors[i].size(); + if (vectors[i].empty()) { + (*out_vectors)[i] = nullptr; + } else { + (*out_vectors)[i] = new double[vectors[i].size()]; + std::copy(vectors[i].begin(), vectors[i].end(), (*out_vectors)[i]); + } + } + return PSR_OK; + } catch (const std::exception&) { + return PSR_ERROR_DATABASE; + } +} + +PSR_C_API psr_error_t psr_database_read_vector_strings(psr_database_t* db, + const char* collection, + const char* attribute, + char**** out_vectors, + size_t** out_sizes, + size_t* out_count) { + if (!db || !collection || !attribute || !out_vectors || !out_sizes || !out_count) { + return PSR_ERROR_INVALID_ARGUMENT; + } + + try { + auto vectors = db->db.read_vector_strings(collection, attribute); + *out_count = vectors.size(); + if (vectors.empty()) { + *out_vectors = nullptr; + *out_sizes = nullptr; + return PSR_OK; + } + *out_vectors = new char**[vectors.size()]; + *out_sizes = new size_t[vectors.size()]; + for (size_t i = 0; i < vectors.size(); ++i) { + (*out_sizes)[i] = vectors[i].size(); + if (vectors[i].empty()) { + (*out_vectors)[i] = nullptr; + } else { + (*out_vectors)[i] = new char*[vectors[i].size()]; + for (size_t j = 0; j < vectors[i].size(); ++j) { + (*out_vectors)[i][j] = new char[vectors[i][j].size() + 1]; + std::copy(vectors[i][j].begin(), vectors[i][j].end(), (*out_vectors)[i][j]); + (*out_vectors)[i][j][vectors[i][j].size()] = '\0'; + } + } + } + return PSR_OK; + } catch (const std::exception&) { + return PSR_ERROR_DATABASE; + } +} + +PSR_C_API void psr_free_int_vectors(int64_t** vectors, size_t* sizes, size_t count) { + if (!vectors) { + return; + } + for (size_t i = 0; i < count; ++i) { + delete[] vectors[i]; + } + delete[] vectors; + delete[] sizes; +} + +PSR_C_API void psr_free_double_vectors(double** vectors, size_t* sizes, size_t count) { + if (!vectors) { + return; + } + for (size_t i = 0; i < count; ++i) { + delete[] vectors[i]; + } + delete[] vectors; + delete[] sizes; +} + +PSR_C_API void psr_free_string_vectors(char*** vectors, size_t* sizes, size_t count) { + if (!vectors) { + return; + } + for (size_t i = 0; i < count; ++i) { + if (vectors[i]) { + for (size_t j = 0; j < sizes[i]; ++j) { + delete[] vectors[i][j]; + } + delete[] vectors[i]; + } + } + delete[] vectors; + delete[] sizes; +} + } // extern "C" diff --git a/src/database.cpp b/src/database.cpp index 1101923..65411e9 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -657,4 +657,113 @@ std::vector Database::read_scalar_strings(const std::string& collec return values; } +// Helper to find vector table for an attribute +static std::string find_vector_table(const Schema* schema, const std::string& collection, + const std::string& attribute) { + // First try: Collection_vector_attribute + auto vector_table = Schema::vector_table_name(collection, attribute); + if (schema->has_table(vector_table)) { + return vector_table; + } + + // Second try: search all vector tables for the collection + for (const auto& table_name : schema->table_names()) { + if (!schema->is_vector_table(table_name)) + continue; + if (schema->get_parent_collection(table_name) != collection) + continue; + + const auto* table_def = schema->get_table(table_name); + if (table_def && table_def->has_column(attribute)) { + return table_name; + } + } + + throw std::runtime_error("Vector attribute '" + attribute + "' not found for collection '" + collection + "'"); +} + +std::vector> Database::read_vector_ints(const std::string& collection, + const std::string& attribute) { + auto vector_table = find_vector_table(impl_->schema.get(), collection, attribute); + auto sql = "SELECT id, " + attribute + " FROM " + vector_table + " ORDER BY id, vector_index"; + auto result = execute(sql); + + std::vector> vectors; + int64_t current_id = -1; + + for (size_t i = 0; i < result.row_count(); ++i) { + auto id = result[i].get_int(0); + auto val = result[i].get_int(1); + + if (!id) + continue; + + if (*id != current_id) { + vectors.emplace_back(); + current_id = *id; + } + + if (val) { + vectors.back().push_back(*val); + } + } + return vectors; +} + +std::vector> Database::read_vector_doubles(const std::string& collection, + const std::string& attribute) { + auto vector_table = find_vector_table(impl_->schema.get(), collection, attribute); + auto sql = "SELECT id, " + attribute + " FROM " + vector_table + " ORDER BY id, vector_index"; + auto result = execute(sql); + + std::vector> vectors; + int64_t current_id = -1; + + for (size_t i = 0; i < result.row_count(); ++i) { + auto id = result[i].get_int(0); + auto val = result[i].get_double(1); + + if (!id) + continue; + + if (*id != current_id) { + vectors.emplace_back(); + current_id = *id; + } + + if (val) { + vectors.back().push_back(*val); + } + } + return vectors; +} + +std::vector> Database::read_vector_strings(const std::string& collection, + const std::string& attribute) { + auto vector_table = find_vector_table(impl_->schema.get(), collection, attribute); + auto sql = "SELECT id, " + attribute + " FROM " + vector_table + " ORDER BY id, vector_index"; + auto result = execute(sql); + + std::vector> vectors; + int64_t current_id = -1; + + for (size_t i = 0; i < result.row_count(); ++i) { + auto id = result[i].get_int(0); + auto val = result[i].get_string(1); + + if (!id) + continue; + + if (*id != current_id) { + vectors.emplace_back(); + current_id = *id; + } + + if (val) { + vectors.back().push_back(*val); + } + } + return vectors; +} + } // namespace psr From cb77151eef559a64390ab8b877d28f82cae9b691 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 13:45:12 -0300 Subject: [PATCH 02/19] [skip ci] update --- bindings/dart/lib/src/database.dart | 132 ++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/bindings/dart/lib/src/database.dart b/bindings/dart/lib/src/database.dart index 7ad635b..6756f63 100644 --- a/bindings/dart/lib/src/database.dart +++ b/bindings/dart/lib/src/database.dart @@ -185,6 +185,138 @@ class Database { } } + /// Reads all int vectors for a vector attribute from a collection. + List> readVectorInts(String collection, String attribute) { + _ensureNotClosed(); + + final arena = Arena(); + try { + final outVectors = arena>>(); + final outSizes = arena>(); + final outCount = arena(); + + final err = bindings.psr_database_read_vector_ints( + _ptr, + collection.toNativeUtf8(allocator: arena).cast(), + attribute.toNativeUtf8(allocator: arena).cast(), + outVectors, + outSizes, + outCount, + ); + + if (err != psr_error_t.PSR_OK) { + throw DatabaseException.fromError(err, "Failed to read vector ints from '$collection.$attribute'"); + } + + final count = outCount.value; + if (count == 0 || outVectors.value == nullptr) { + return []; + } + + final result = >[]; + for (var i = 0; i < count; i++) { + final size = outSizes.value[i]; + if (size == 0 || outVectors.value[i] == nullptr) { + result.add([]); + } else { + result.add(List.generate(size, (j) => outVectors.value[i][j])); + } + } + bindings.psr_free_int_vectors(outVectors.value, outSizes.value, count); + return result; + } finally { + arena.releaseAll(); + } + } + + /// Reads all double vectors for a vector attribute from a collection. + List> readVectorDoubles(String collection, String attribute) { + _ensureNotClosed(); + + final arena = Arena(); + try { + final outVectors = arena>>(); + final outSizes = arena>(); + final outCount = arena(); + + final err = bindings.psr_database_read_vector_doubles( + _ptr, + collection.toNativeUtf8(allocator: arena).cast(), + attribute.toNativeUtf8(allocator: arena).cast(), + outVectors, + outSizes, + outCount, + ); + + if (err != psr_error_t.PSR_OK) { + throw DatabaseException.fromError(err, "Failed to read vector doubles from '$collection.$attribute'"); + } + + final count = outCount.value; + if (count == 0 || outVectors.value == nullptr) { + return []; + } + + final result = >[]; + for (var i = 0; i < count; i++) { + final size = outSizes.value[i]; + if (size == 0 || outVectors.value[i] == nullptr) { + result.add([]); + } else { + result.add(List.generate(size, (j) => outVectors.value[i][j])); + } + } + bindings.psr_free_double_vectors(outVectors.value, outSizes.value, count); + return result; + } finally { + arena.releaseAll(); + } + } + + /// Reads all string vectors for a vector attribute from a collection. + List> readVectorStrings(String collection, String attribute) { + _ensureNotClosed(); + + final arena = Arena(); + try { + final outVectors = arena>>>(); + final outSizes = arena>(); + final outCount = arena(); + + final err = bindings.psr_database_read_vector_strings( + _ptr, + collection.toNativeUtf8(allocator: arena).cast(), + attribute.toNativeUtf8(allocator: arena).cast(), + outVectors, + outSizes, + outCount, + ); + + if (err != psr_error_t.PSR_OK) { + throw DatabaseException.fromError(err, "Failed to read vector strings from '$collection.$attribute'"); + } + + final count = outCount.value; + if (count == 0 || outVectors.value == nullptr) { + return []; + } + + final result = >[]; + for (var i = 0; i < count; i++) { + final size = outSizes.value[i]; + if (size == 0 || outVectors.value[i] == nullptr) { + result.add([]); + } else { + result.add(List.generate(size, (j) => outVectors.value[i][j].cast().toDartString())); + } + } + bindings.psr_free_string_vectors(outVectors.value, outSizes.value, count); + return result; + } finally { + arena.releaseAll(); + } + } + /// Closes the database and frees native resources. void close() { if (_isClosed) return; From 181c18e545a79a3131fbaef80beef41eac14ee5d Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 13:55:05 -0300 Subject: [PATCH 03/19] [skip ci] update --- src/c_api_database.cpp | 143 +++++++++++++++++------------------------ 1 file changed, 60 insertions(+), 83 deletions(-) diff --git a/src/c_api_database.cpp b/src/c_api_database.cpp index a6bc9cf..39f4098 100644 --- a/src/c_api_database.cpp +++ b/src/c_api_database.cpp @@ -38,6 +38,58 @@ psr::DatabaseOptions to_cpp_options(const psr_database_options_t* options) { return cpp_options; } +// Helper template for reading numeric scalars +template +psr_error_t read_scalars_impl(const std::vector& values, T** out_values, size_t* out_count) { + *out_count = values.size(); + if (values.empty()) { + *out_values = nullptr; + return PSR_OK; + } + *out_values = new T[values.size()]; + std::copy(values.begin(), values.end(), *out_values); + return PSR_OK; +} + +// Helper template for reading numeric vectors +template +psr_error_t read_vectors_impl(const std::vector>& vectors, + T*** out_vectors, + size_t** out_sizes, + size_t* out_count) { + *out_count = vectors.size(); + if (vectors.empty()) { + *out_vectors = nullptr; + *out_sizes = nullptr; + return PSR_OK; + } + *out_vectors = new T*[vectors.size()]; + *out_sizes = new size_t[vectors.size()]; + for (size_t i = 0; i < vectors.size(); ++i) { + (*out_sizes)[i] = vectors[i].size(); + if (vectors[i].empty()) { + (*out_vectors)[i] = nullptr; + } else { + (*out_vectors)[i] = new T[vectors[i].size()]; + std::copy(vectors[i].begin(), vectors[i].end(), (*out_vectors)[i]); + } + } + return PSR_OK; +} + +// Helper template for freeing numeric vectors +template +void free_vectors_impl(T** vectors, size_t* sizes, size_t count) { + (void)sizes; // unused for numeric types + if (!vectors) + return; + for (size_t i = 0; i < count; ++i) { + delete[] vectors[i]; + } + delete[] vectors; + delete[] sizes; +} + } // namespace struct psr_database { @@ -213,17 +265,8 @@ PSR_C_API psr_error_t psr_database_read_scalar_ints(psr_database_t* db, if (!db || !collection || !attribute || !out_values || !out_count) { return PSR_ERROR_INVALID_ARGUMENT; } - try { - auto values = db->db.read_scalar_ints(collection, attribute); - *out_count = values.size(); - if (values.empty()) { - *out_values = nullptr; - return PSR_OK; - } - *out_values = new int64_t[values.size()]; - std::copy(values.begin(), values.end(), *out_values); - return PSR_OK; + return read_scalars_impl(db->db.read_scalar_ints(collection, attribute), out_values, out_count); } catch (const std::exception&) { return PSR_ERROR_DATABASE; } @@ -237,17 +280,8 @@ PSR_C_API psr_error_t psr_database_read_scalar_doubles(psr_database_t* db, if (!db || !collection || !attribute || !out_values || !out_count) { return PSR_ERROR_INVALID_ARGUMENT; } - try { - auto values = db->db.read_scalar_doubles(collection, attribute); - *out_count = values.size(); - if (values.empty()) { - *out_values = nullptr; - return PSR_OK; - } - *out_values = new double[values.size()]; - std::copy(values.begin(), values.end(), *out_values); - return PSR_OK; + return read_scalars_impl(db->db.read_scalar_doubles(collection, attribute), out_values, out_count); } catch (const std::exception&) { return PSR_ERROR_DATABASE; } @@ -281,13 +315,9 @@ PSR_C_API psr_error_t psr_database_read_scalar_strings(psr_database_t* db, } } -PSR_C_API void psr_free_int_array(int64_t* values) { - delete[] values; -} +PSR_C_API void psr_free_int_array(int64_t* values) { delete[] values; } -PSR_C_API void psr_free_double_array(double* values) { - delete[] values; -} +PSR_C_API void psr_free_double_array(double* values) { delete[] values; } PSR_C_API void psr_free_string_array(char** values, size_t count) { if (!values) { @@ -308,27 +338,8 @@ PSR_C_API psr_error_t psr_database_read_vector_ints(psr_database_t* db, if (!db || !collection || !attribute || !out_vectors || !out_sizes || !out_count) { return PSR_ERROR_INVALID_ARGUMENT; } - try { - auto vectors = db->db.read_vector_ints(collection, attribute); - *out_count = vectors.size(); - if (vectors.empty()) { - *out_vectors = nullptr; - *out_sizes = nullptr; - return PSR_OK; - } - *out_vectors = new int64_t*[vectors.size()]; - *out_sizes = new size_t[vectors.size()]; - for (size_t i = 0; i < vectors.size(); ++i) { - (*out_sizes)[i] = vectors[i].size(); - if (vectors[i].empty()) { - (*out_vectors)[i] = nullptr; - } else { - (*out_vectors)[i] = new int64_t[vectors[i].size()]; - std::copy(vectors[i].begin(), vectors[i].end(), (*out_vectors)[i]); - } - } - return PSR_OK; + return read_vectors_impl(db->db.read_vector_ints(collection, attribute), out_vectors, out_sizes, out_count); } catch (const std::exception&) { return PSR_ERROR_DATABASE; } @@ -343,27 +354,8 @@ PSR_C_API psr_error_t psr_database_read_vector_doubles(psr_database_t* db, if (!db || !collection || !attribute || !out_vectors || !out_sizes || !out_count) { return PSR_ERROR_INVALID_ARGUMENT; } - try { - auto vectors = db->db.read_vector_doubles(collection, attribute); - *out_count = vectors.size(); - if (vectors.empty()) { - *out_vectors = nullptr; - *out_sizes = nullptr; - return PSR_OK; - } - *out_vectors = new double*[vectors.size()]; - *out_sizes = new size_t[vectors.size()]; - for (size_t i = 0; i < vectors.size(); ++i) { - (*out_sizes)[i] = vectors[i].size(); - if (vectors[i].empty()) { - (*out_vectors)[i] = nullptr; - } else { - (*out_vectors)[i] = new double[vectors[i].size()]; - std::copy(vectors[i].begin(), vectors[i].end(), (*out_vectors)[i]); - } - } - return PSR_OK; + return read_vectors_impl(db->db.read_vector_doubles(collection, attribute), out_vectors, out_sizes, out_count); } catch (const std::exception&) { return PSR_ERROR_DATABASE; } @@ -378,7 +370,6 @@ PSR_C_API psr_error_t psr_database_read_vector_strings(psr_database_t* db, if (!db || !collection || !attribute || !out_vectors || !out_sizes || !out_count) { return PSR_ERROR_INVALID_ARGUMENT; } - try { auto vectors = db->db.read_vector_strings(collection, attribute); *out_count = vectors.size(); @@ -409,25 +400,11 @@ PSR_C_API psr_error_t psr_database_read_vector_strings(psr_database_t* db, } PSR_C_API void psr_free_int_vectors(int64_t** vectors, size_t* sizes, size_t count) { - if (!vectors) { - return; - } - for (size_t i = 0; i < count; ++i) { - delete[] vectors[i]; - } - delete[] vectors; - delete[] sizes; + free_vectors_impl(vectors, sizes, count); } PSR_C_API void psr_free_double_vectors(double** vectors, size_t* sizes, size_t count) { - if (!vectors) { - return; - } - for (size_t i = 0; i < count; ++i) { - delete[] vectors[i]; - } - delete[] vectors; - delete[] sizes; + free_vectors_impl(vectors, sizes, count); } PSR_C_API void psr_free_string_vectors(char*** vectors, size_t* sizes, size_t count) { From c8c2bd08106ce6996d9ed12e7b584dee30d38e38 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 13:56:59 -0300 Subject: [PATCH 04/19] [skip ci] update --- bindings/julia/generator/generator.jl | 5 ++++- bindings/julia/generator/prologue.jl | 21 ++++++++++++++++++++- bindings/julia/src/c_api.jl | 24 ++++++++++++------------ bindings/julia/test/fixture.jl | 2 +- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/bindings/julia/generator/generator.jl b/bindings/julia/generator/generator.jl index ba9a765..e9f81ff 100644 --- a/bindings/julia/generator/generator.jl +++ b/bindings/julia/generator/generator.jl @@ -12,7 +12,10 @@ include_dir = joinpath(database_dir, "include", "psr", "c") Libdl.dlopen(joinpath(database_dir, "build", "bin", "libpsr_database_c.dll")) -headers = [joinpath(include_dir, header) for header in readdir(include_dir) if endswith(header, ".h") && header != "platform.h"] +headers = [ + joinpath(include_dir, header) for + header in readdir(include_dir) if endswith(header, ".h") && header != "platform.h" +] args = get_default_args() options = load_options(joinpath(@__DIR__, "generator.toml")) ctx = create_context(headers, args, options) diff --git a/bindings/julia/generator/prologue.jl b/bindings/julia/generator/prologue.jl index fd43b29..4088ee4 100644 --- a/bindings/julia/generator/prologue.jl +++ b/bindings/julia/generator/prologue.jl @@ -3,4 +3,23 @@ using CEnum using Libdl -const libpsr_database_c = joinpath(@__DIR__, "..", "..", "..", "build", "bin", "libpsr_database_c.dll") +function library_name() + if Sys.iswindows() + return "libpsr_database_c.dll" + elseif Sys.isapple() + return "libpsr_database_c.dylib" + else + return "libpsr_database_c.so" + end +end + +# On Windows, DLLs go to bin/; on Linux/macOS, shared libs go to lib/ +function library_dir() + if Sys.iswindows() + return "bin" + else + return "lib" + end +end + +const libpsr_database_c = joinpath(@__DIR__, "..", "..", "..", "build", library_dir(), library_name()) diff --git a/bindings/julia/src/c_api.jl b/bindings/julia/src/c_api.jl index b06f182..a712651 100644 --- a/bindings/julia/src/c_api.jl +++ b/bindings/julia/src/c_api.jl @@ -134,18 +134,6 @@ function psr_database_read_scalar_strings(db, collection, attribute, out_values, @ccall libpsr_database_c.psr_database_read_scalar_strings(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, out_values::Ptr{Ptr{Ptr{Cchar}}}, out_count::Ptr{Csize_t})::psr_error_t end -function psr_free_int_array(values) - @ccall libpsr_database_c.psr_free_int_array(values::Ptr{Int64})::Cvoid -end - -function psr_free_double_array(values) - @ccall libpsr_database_c.psr_free_double_array(values::Ptr{Cdouble})::Cvoid -end - -function psr_free_string_array(values, count) - @ccall libpsr_database_c.psr_free_string_array(values::Ptr{Ptr{Cchar}}, count::Csize_t)::Cvoid -end - function psr_database_read_vector_ints(db, collection, attribute, out_vectors, out_sizes, out_count) @ccall libpsr_database_c.psr_database_read_vector_ints(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, out_vectors::Ptr{Ptr{Ptr{Int64}}}, out_sizes::Ptr{Ptr{Csize_t}}, out_count::Ptr{Csize_t})::psr_error_t end @@ -158,6 +146,18 @@ function psr_database_read_vector_strings(db, collection, attribute, out_vectors @ccall libpsr_database_c.psr_database_read_vector_strings(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, out_vectors::Ptr{Ptr{Ptr{Ptr{Cchar}}}}, out_sizes::Ptr{Ptr{Csize_t}}, out_count::Ptr{Csize_t})::psr_error_t end +function psr_free_int_array(values) + @ccall libpsr_database_c.psr_free_int_array(values::Ptr{Int64})::Cvoid +end + +function psr_free_double_array(values) + @ccall libpsr_database_c.psr_free_double_array(values::Ptr{Cdouble})::Cvoid +end + +function psr_free_string_array(values, count) + @ccall libpsr_database_c.psr_free_string_array(values::Ptr{Ptr{Cchar}}, count::Csize_t)::Cvoid +end + function psr_free_int_vectors(vectors, sizes, count) @ccall libpsr_database_c.psr_free_int_vectors(vectors::Ptr{Ptr{Int64}}, sizes::Ptr{Csize_t}, count::Csize_t)::Cvoid end diff --git a/bindings/julia/test/fixture.jl b/bindings/julia/test/fixture.jl index 03f4d3e..eec48f2 100644 --- a/bindings/julia/test/fixture.jl +++ b/bindings/julia/test/fixture.jl @@ -1,3 +1,3 @@ function tests_path() return joinpath(@__DIR__, "..", "..", "..", "tests") -end \ No newline at end of file +end From 0cfea97e0d7455f323eb02fdc235235b6d8f3b6f Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 13:57:44 -0300 Subject: [PATCH 05/19] update --- bindings/julia/src/c_api.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bindings/julia/src/c_api.jl b/bindings/julia/src/c_api.jl index a712651..ebb26d0 100644 --- a/bindings/julia/src/c_api.jl +++ b/bindings/julia/src/c_api.jl @@ -5,7 +5,7 @@ module C using CEnum using Libdl -function _library_name() +function library_name() if Sys.iswindows() return "libpsr_database_c.dll" elseif Sys.isapple() @@ -15,8 +15,8 @@ function _library_name() end end -function _library_dir() - # On Windows, DLLs go to bin/; on Linux/macOS, shared libs go to lib/ +# On Windows, DLLs go to bin/; on Linux/macOS, shared libs go to lib/ +function library_dir() if Sys.iswindows() return "bin" else @@ -24,7 +24,7 @@ function _library_dir() end end -const libpsr_database_c = joinpath(@__DIR__, "..", "..", "..", "build", _library_dir(), _library_name()) +const libpsr_database_c = joinpath(@__DIR__, "..", "..", "..", "build", library_dir(), library_name()) @cenum psr_error_t::Int32 begin From d5e39d6f673ec2b5666ffda41d2d2ca315013bc2 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 14:05:00 -0300 Subject: [PATCH 06/19] update --- bindings/dart/test/read_test.dart | 102 ++++++++++++++++++++++++++++++ bindings/julia/test/test_read.jl | 60 ++++++++++++++++++ 2 files changed, 162 insertions(+) diff --git a/bindings/dart/test/read_test.dart b/bindings/dart/test/read_test.dart index 8fb07fe..0d368e3 100644 --- a/bindings/dart/test/read_test.dart +++ b/bindings/dart/test/read_test.dart @@ -143,4 +143,106 @@ void main() { } }); }); + + group('Read Vector Attributes', () { + test('reads int vectors from Collection', () { + final db = Database.fromSchema( + ':memory:', + path.join(testsPath, 'schemas', 'valid', 'collections.sql'), + ); + try { + db.createElement('Configuration', {'label': 'Test Config'}); + db.createElement('Collection', { + 'label': 'Item 1', + 'value_int': [1, 2, 3], + }); + db.createElement('Collection', { + 'label': 'Item 2', + 'value_int': [10, 20], + }); + + expect( + db.readVectorInts('Collection', 'value_int'), + equals([ + [1, 2, 3], + [10, 20], + ]), + ); + } finally { + db.close(); + } + }); + + test('reads double vectors from Collection', () { + final db = Database.fromSchema( + ':memory:', + path.join(testsPath, 'schemas', 'valid', 'collections.sql'), + ); + try { + db.createElement('Configuration', {'label': 'Test Config'}); + db.createElement('Collection', { + 'label': 'Item 1', + 'value_float': [1.5, 2.5, 3.5], + }); + db.createElement('Collection', { + 'label': 'Item 2', + 'value_float': [10.5, 20.5], + }); + + expect( + db.readVectorDoubles('Collection', 'value_float'), + equals([ + [1.5, 2.5, 3.5], + [10.5, 20.5], + ]), + ); + } finally { + db.close(); + } + }); + }); + + group('Read Vector Empty Result', () { + test('returns empty list when no elements', () { + final db = Database.fromSchema( + ':memory:', + path.join(testsPath, 'schemas', 'valid', 'collections.sql'), + ); + try { + db.createElement('Configuration', {'label': 'Test Config'}); + + expect(db.readVectorInts('Collection', 'value_int'), isEmpty); + expect(db.readVectorDoubles('Collection', 'value_float'), isEmpty); + } finally { + db.close(); + } + }); + }); + + group('Read Vector With Empty Vectors', () { + test('handles elements with and without vectors', () { + final db = Database.fromSchema( + ':memory:', + path.join(testsPath, 'schemas', 'valid', 'collections.sql'), + ); + try { + db.createElement('Configuration', {'label': 'Test Config'}); + db.createElement('Collection', { + 'label': 'Item 1', + 'value_int': [1, 2, 3], + }); + db.createElement('Collection', { + 'label': 'Item 2', + // No vector data + }); + + final result = db.readVectorInts('Collection', 'value_int'); + expect(result.length, equals(2)); + expect(result[0], equals([1, 2, 3])); + expect(result[1], isEmpty); + } finally { + db.close(); + } + }); + }); } diff --git a/bindings/julia/test/test_read.jl b/bindings/julia/test/test_read.jl index f3fe766..6fe1b64 100644 --- a/bindings/julia/test/test_read.jl +++ b/bindings/julia/test/test_read.jl @@ -68,4 +68,64 @@ end PSRDatabase.close!(db) end +@testset "Read Vector Attributes" begin + path_schema = joinpath(tests_path(), "schemas", "valid", "collections.sql") + db = PSRDatabase.create_empty_db_from_schema(":memory:", path_schema; force = true) + + PSRDatabase.create_element!(db, "Configuration"; label = "Test Config") + + PSRDatabase.create_element!(db, "Collection"; + label = "Item 1", + value_int = [1, 2, 3], + value_float = [1.5, 2.5, 3.5], + ) + PSRDatabase.create_element!(db, "Collection"; + label = "Item 2", + value_int = [10, 20], + value_float = [10.5, 20.5], + ) + + @test PSRDatabase.read_vector_ints(db, "Collection", "value_int") == [[1, 2, 3], [10, 20]] + @test PSRDatabase.read_vector_doubles(db, "Collection", "value_float") == [[1.5, 2.5, 3.5], [10.5, 20.5]] + + PSRDatabase.close!(db) +end + +@testset "Read Vector Empty Result" begin + path_schema = joinpath(tests_path(), "schemas", "valid", "collections.sql") + db = PSRDatabase.create_empty_db_from_schema(":memory:", path_schema; force = true) + + PSRDatabase.create_element!(db, "Configuration"; label = "Test Config") + + # No Collection elements created + @test PSRDatabase.read_vector_ints(db, "Collection", "value_int") == Vector{Int64}[] + @test PSRDatabase.read_vector_doubles(db, "Collection", "value_float") == Vector{Float64}[] + + PSRDatabase.close!(db) +end + +@testset "Read Vector With Empty Vectors" begin + path_schema = joinpath(tests_path(), "schemas", "valid", "collections.sql") + db = PSRDatabase.create_empty_db_from_schema(":memory:", path_schema; force = true) + + PSRDatabase.create_element!(db, "Configuration"; label = "Test Config") + + # Create element with vectors + PSRDatabase.create_element!(db, "Collection"; + label = "Item 1", + value_int = [1, 2, 3], + ) + # Create element without vectors (empty) + PSRDatabase.create_element!(db, "Collection"; + label = "Item 2", + ) + + result = PSRDatabase.read_vector_ints(db, "Collection", "value_int") + @test length(result) == 2 + @test result[1] == [1, 2, 3] + @test result[2] == Int64[] + + PSRDatabase.close!(db) +end + end From d62996deab74fa1934d0580e31b78efd6f9069b6 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 14:08:13 -0300 Subject: [PATCH 07/19] update --- bindings/dart/test/read_test.dart | 11 ++++++++--- bindings/julia/test/test_read.jl | 12 +++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/bindings/dart/test/read_test.dart b/bindings/dart/test/read_test.dart index 0d368e3..61858c7 100644 --- a/bindings/dart/test/read_test.dart +++ b/bindings/dart/test/read_test.dart @@ -219,8 +219,8 @@ void main() { }); }); - group('Read Vector With Empty Vectors', () { - test('handles elements with and without vectors', () { + group('Read Vector Only Returns Elements With Data', () { + test('only returns vectors for elements with data', () { final db = Database.fromSchema( ':memory:', path.join(testsPath, 'schemas', 'valid', 'collections.sql'), @@ -235,11 +235,16 @@ void main() { 'label': 'Item 2', // No vector data }); + db.createElement('Collection', { + 'label': 'Item 3', + 'value_int': [4, 5], + }); + // Only elements with vector data are returned final result = db.readVectorInts('Collection', 'value_int'); expect(result.length, equals(2)); expect(result[0], equals([1, 2, 3])); - expect(result[1], isEmpty); + expect(result[1], equals([4, 5])); } finally { db.close(); } diff --git a/bindings/julia/test/test_read.jl b/bindings/julia/test/test_read.jl index 6fe1b64..5ae0820 100644 --- a/bindings/julia/test/test_read.jl +++ b/bindings/julia/test/test_read.jl @@ -104,7 +104,7 @@ end PSRDatabase.close!(db) end -@testset "Read Vector With Empty Vectors" begin +@testset "Read Vector Only Returns Elements With Data" begin path_schema = joinpath(tests_path(), "schemas", "valid", "collections.sql") db = PSRDatabase.create_empty_db_from_schema(":memory:", path_schema; force = true) @@ -115,15 +115,21 @@ end label = "Item 1", value_int = [1, 2, 3], ) - # Create element without vectors (empty) + # Create element without vectors (no vector data inserted) PSRDatabase.create_element!(db, "Collection"; label = "Item 2", ) + # Create another element with vectors + PSRDatabase.create_element!(db, "Collection"; + label = "Item 3", + value_int = [4, 5], + ) + # Only elements with vector data are returned result = PSRDatabase.read_vector_ints(db, "Collection", "value_int") @test length(result) == 2 @test result[1] == [1, 2, 3] - @test result[2] == Int64[] + @test result[2] == [4, 5] PSRDatabase.close!(db) end From 9cd80d5b8818a10fb76ee996795f950330953684 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 14:17:03 -0300 Subject: [PATCH 08/19] update --- include/psr/c/database.h | 30 ++-- include/psr/database.h | 8 +- src/c_api_database.cpp | 44 ++--- src/database.cpp | 10 +- tests/test_c_api_database.cpp | 295 ++++++++++++++++++++++++++++++++++ tests/test_database.cpp | 162 +++++++++++++++++++ 6 files changed, 503 insertions(+), 46 deletions(-) diff --git a/include/psr/c/database.h b/include/psr/c/database.h index d6b5cb4..5fc143c 100644 --- a/include/psr/c/database.h +++ b/include/psr/c/database.h @@ -73,25 +73,25 @@ PSR_C_API psr_error_t psr_database_read_scalar_strings(psr_database_t* db, // Read vector attributes PSR_C_API psr_error_t psr_database_read_vector_ints(psr_database_t* db, - const char* collection, - const char* attribute, - int64_t*** out_vectors, - size_t** out_sizes, - size_t* out_count); + const char* collection, + const char* attribute, + int64_t*** out_vectors, + size_t** out_sizes, + size_t* out_count); PSR_C_API psr_error_t psr_database_read_vector_doubles(psr_database_t* db, - const char* collection, - const char* attribute, - double*** out_vectors, - size_t** out_sizes, - size_t* out_count); + const char* collection, + const char* attribute, + double*** out_vectors, + size_t** out_sizes, + size_t* out_count); PSR_C_API psr_error_t psr_database_read_vector_strings(psr_database_t* db, - const char* collection, - const char* attribute, - char**** out_vectors, - size_t** out_sizes, - size_t* out_count); + const char* collection, + const char* attribute, + char**** out_vectors, + size_t** out_sizes, + size_t* out_count); // Memory cleanup for read results PSR_C_API void psr_free_int_array(int64_t* values); diff --git a/include/psr/database.h b/include/psr/database.h index 8ecde3c..70ef43f 100644 --- a/include/psr/database.h +++ b/include/psr/database.h @@ -56,12 +56,10 @@ class PSR_API Database { std::vector read_scalar_strings(const std::string& collection, const std::string& attribute); // Read vector attributes - std::vector> read_vector_ints(const std::string& collection, - const std::string& attribute); - std::vector> read_vector_doubles(const std::string& collection, - const std::string& attribute); + std::vector> read_vector_ints(const std::string& collection, const std::string& attribute); + std::vector> read_vector_doubles(const std::string& collection, const std::string& attribute); std::vector> read_vector_strings(const std::string& collection, - const std::string& attribute); + const std::string& attribute); // Transaction management void begin_transaction(); diff --git a/src/c_api_database.cpp b/src/c_api_database.cpp index 39f4098..04cc0eb 100644 --- a/src/c_api_database.cpp +++ b/src/c_api_database.cpp @@ -53,10 +53,8 @@ psr_error_t read_scalars_impl(const std::vector& values, T** out_values, size // Helper template for reading numeric vectors template -psr_error_t read_vectors_impl(const std::vector>& vectors, - T*** out_vectors, - size_t** out_sizes, - size_t* out_count) { +psr_error_t +read_vectors_impl(const std::vector>& vectors, T*** out_vectors, size_t** out_sizes, size_t* out_count) { *out_count = vectors.size(); if (vectors.empty()) { *out_vectors = nullptr; @@ -315,9 +313,13 @@ PSR_C_API psr_error_t psr_database_read_scalar_strings(psr_database_t* db, } } -PSR_C_API void psr_free_int_array(int64_t* values) { delete[] values; } +PSR_C_API void psr_free_int_array(int64_t* values) { + delete[] values; +} -PSR_C_API void psr_free_double_array(double* values) { delete[] values; } +PSR_C_API void psr_free_double_array(double* values) { + delete[] values; +} PSR_C_API void psr_free_string_array(char** values, size_t count) { if (!values) { @@ -330,11 +332,11 @@ PSR_C_API void psr_free_string_array(char** values, size_t count) { } PSR_C_API psr_error_t psr_database_read_vector_ints(psr_database_t* db, - const char* collection, - const char* attribute, - int64_t*** out_vectors, - size_t** out_sizes, - size_t* out_count) { + const char* collection, + const char* attribute, + int64_t*** out_vectors, + size_t** out_sizes, + size_t* out_count) { if (!db || !collection || !attribute || !out_vectors || !out_sizes || !out_count) { return PSR_ERROR_INVALID_ARGUMENT; } @@ -346,11 +348,11 @@ PSR_C_API psr_error_t psr_database_read_vector_ints(psr_database_t* db, } PSR_C_API psr_error_t psr_database_read_vector_doubles(psr_database_t* db, - const char* collection, - const char* attribute, - double*** out_vectors, - size_t** out_sizes, - size_t* out_count) { + const char* collection, + const char* attribute, + double*** out_vectors, + size_t** out_sizes, + size_t* out_count) { if (!db || !collection || !attribute || !out_vectors || !out_sizes || !out_count) { return PSR_ERROR_INVALID_ARGUMENT; } @@ -362,11 +364,11 @@ PSR_C_API psr_error_t psr_database_read_vector_doubles(psr_database_t* db, } PSR_C_API psr_error_t psr_database_read_vector_strings(psr_database_t* db, - const char* collection, - const char* attribute, - char**** out_vectors, - size_t** out_sizes, - size_t* out_count) { + const char* collection, + const char* attribute, + char**** out_vectors, + size_t** out_sizes, + size_t* out_count) { if (!db || !collection || !attribute || !out_vectors || !out_sizes || !out_count) { return PSR_ERROR_INVALID_ARGUMENT; } diff --git a/src/database.cpp b/src/database.cpp index 65411e9..c39841b 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -658,8 +658,8 @@ std::vector Database::read_scalar_strings(const std::string& collec } // Helper to find vector table for an attribute -static std::string find_vector_table(const Schema* schema, const std::string& collection, - const std::string& attribute) { +static std::string +find_vector_table(const Schema* schema, const std::string& collection, const std::string& attribute) { // First try: Collection_vector_attribute auto vector_table = Schema::vector_table_name(collection, attribute); if (schema->has_table(vector_table)) { @@ -683,7 +683,7 @@ static std::string find_vector_table(const Schema* schema, const std::string& co } std::vector> Database::read_vector_ints(const std::string& collection, - const std::string& attribute) { + const std::string& attribute) { auto vector_table = find_vector_table(impl_->schema.get(), collection, attribute); auto sql = "SELECT id, " + attribute + " FROM " + vector_table + " ORDER BY id, vector_index"; auto result = execute(sql); @@ -711,7 +711,7 @@ std::vector> Database::read_vector_ints(const std::string& } std::vector> Database::read_vector_doubles(const std::string& collection, - const std::string& attribute) { + const std::string& attribute) { auto vector_table = find_vector_table(impl_->schema.get(), collection, attribute); auto sql = "SELECT id, " + attribute + " FROM " + vector_table + " ORDER BY id, vector_index"; auto result = execute(sql); @@ -739,7 +739,7 @@ std::vector> Database::read_vector_doubles(const std::string } std::vector> Database::read_vector_strings(const std::string& collection, - const std::string& attribute) { + const std::string& attribute) { auto vector_table = find_vector_table(impl_->schema.get(), collection, attribute); auto sql = "SELECT id, " + attribute + " FROM " + vector_table + " ORDER BY id, vector_index"; auto result = execute(sql); diff --git a/tests/test_c_api_database.cpp b/tests/test_c_api_database.cpp index 86b6095..553ac1e 100644 --- a/tests/test_c_api_database.cpp +++ b/tests/test_c_api_database.cpp @@ -258,3 +258,298 @@ TEST_F(DatabaseFixture, CreateElementNullElement) { psr_database_close(db); } + +TEST_F(DatabaseFixture, ReadScalarInts) { + auto options = psr_database_options_default(); + options.console_level = PSR_LOG_OFF; + auto db = psr_database_from_schema(":memory:", schema_path("schemas/valid/basic.sql").c_str(), &options); + ASSERT_NE(db, nullptr); + + auto e1 = psr_element_create(); + psr_element_set_string(e1, "label", "Config 1"); + psr_element_set_int(e1, "integer_attribute", 42); + psr_database_create_element(db, "Configuration", e1); + psr_element_destroy(e1); + + auto e2 = psr_element_create(); + psr_element_set_string(e2, "label", "Config 2"); + psr_element_set_int(e2, "integer_attribute", 100); + psr_database_create_element(db, "Configuration", e2); + psr_element_destroy(e2); + + int64_t* values = nullptr; + size_t count = 0; + auto err = psr_database_read_scalar_ints(db, "Configuration", "integer_attribute", &values, &count); + + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(count, 2); + EXPECT_EQ(values[0], 42); + EXPECT_EQ(values[1], 100); + + psr_free_int_array(values); + psr_database_close(db); +} + +TEST_F(DatabaseFixture, ReadScalarDoubles) { + auto options = psr_database_options_default(); + options.console_level = PSR_LOG_OFF; + auto db = psr_database_from_schema(":memory:", schema_path("schemas/valid/basic.sql").c_str(), &options); + ASSERT_NE(db, nullptr); + + auto e1 = psr_element_create(); + psr_element_set_string(e1, "label", "Config 1"); + psr_element_set_double(e1, "float_attribute", 3.14); + psr_database_create_element(db, "Configuration", e1); + psr_element_destroy(e1); + + auto e2 = psr_element_create(); + psr_element_set_string(e2, "label", "Config 2"); + psr_element_set_double(e2, "float_attribute", 2.71); + psr_database_create_element(db, "Configuration", e2); + psr_element_destroy(e2); + + double* values = nullptr; + size_t count = 0; + auto err = psr_database_read_scalar_doubles(db, "Configuration", "float_attribute", &values, &count); + + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(count, 2); + EXPECT_DOUBLE_EQ(values[0], 3.14); + EXPECT_DOUBLE_EQ(values[1], 2.71); + + psr_free_double_array(values); + psr_database_close(db); +} + +TEST_F(DatabaseFixture, ReadScalarStrings) { + auto options = psr_database_options_default(); + options.console_level = PSR_LOG_OFF; + auto db = psr_database_from_schema(":memory:", schema_path("schemas/valid/basic.sql").c_str(), &options); + ASSERT_NE(db, nullptr); + + auto e1 = psr_element_create(); + psr_element_set_string(e1, "label", "Config 1"); + psr_element_set_string(e1, "string_attribute", "hello"); + psr_database_create_element(db, "Configuration", e1); + psr_element_destroy(e1); + + auto e2 = psr_element_create(); + psr_element_set_string(e2, "label", "Config 2"); + psr_element_set_string(e2, "string_attribute", "world"); + psr_database_create_element(db, "Configuration", e2); + psr_element_destroy(e2); + + char** values = nullptr; + size_t count = 0; + auto err = psr_database_read_scalar_strings(db, "Configuration", "string_attribute", &values, &count); + + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(count, 2); + EXPECT_STREQ(values[0], "hello"); + EXPECT_STREQ(values[1], "world"); + + psr_free_string_array(values, count); + psr_database_close(db); +} + +TEST_F(DatabaseFixture, ReadScalarEmpty) { + auto options = psr_database_options_default(); + options.console_level = PSR_LOG_OFF; + auto db = psr_database_from_schema(":memory:", schema_path("schemas/valid/collections.sql").c_str(), &options); + ASSERT_NE(db, nullptr); + + auto config = psr_element_create(); + psr_element_set_string(config, "label", "Test Config"); + psr_database_create_element(db, "Configuration", config); + psr_element_destroy(config); + + int64_t* int_values = nullptr; + size_t int_count = 0; + auto err = psr_database_read_scalar_ints(db, "Collection", "some_integer", &int_values, &int_count); + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(int_count, 0); + EXPECT_EQ(int_values, nullptr); + + double* double_values = nullptr; + size_t double_count = 0; + err = psr_database_read_scalar_doubles(db, "Collection", "some_float", &double_values, &double_count); + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(double_count, 0); + EXPECT_EQ(double_values, nullptr); + + psr_database_close(db); +} + +TEST_F(DatabaseFixture, ReadVectorInts) { + auto options = psr_database_options_default(); + options.console_level = PSR_LOG_OFF; + auto db = psr_database_from_schema(":memory:", schema_path("schemas/valid/collections.sql").c_str(), &options); + ASSERT_NE(db, nullptr); + + auto config = psr_element_create(); + psr_element_set_string(config, "label", "Test Config"); + psr_database_create_element(db, "Configuration", config); + psr_element_destroy(config); + + auto e1 = psr_element_create(); + psr_element_set_string(e1, "label", "Item 1"); + int64_t values1[] = {1, 2, 3}; + psr_element_set_array_int(e1, "value_int", values1, 3); + psr_database_create_element(db, "Collection", e1); + psr_element_destroy(e1); + + auto e2 = psr_element_create(); + psr_element_set_string(e2, "label", "Item 2"); + int64_t values2[] = {10, 20}; + psr_element_set_array_int(e2, "value_int", values2, 2); + psr_database_create_element(db, "Collection", e2); + psr_element_destroy(e2); + + int64_t** vectors = nullptr; + size_t* sizes = nullptr; + size_t count = 0; + auto err = psr_database_read_vector_ints(db, "Collection", "value_int", &vectors, &sizes, &count); + + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(count, 2); + EXPECT_EQ(sizes[0], 3); + EXPECT_EQ(sizes[1], 2); + EXPECT_EQ(vectors[0][0], 1); + EXPECT_EQ(vectors[0][1], 2); + EXPECT_EQ(vectors[0][2], 3); + EXPECT_EQ(vectors[1][0], 10); + EXPECT_EQ(vectors[1][1], 20); + + psr_free_int_vectors(vectors, sizes, count); + psr_database_close(db); +} + +TEST_F(DatabaseFixture, ReadVectorDoubles) { + auto options = psr_database_options_default(); + options.console_level = PSR_LOG_OFF; + auto db = psr_database_from_schema(":memory:", schema_path("schemas/valid/collections.sql").c_str(), &options); + ASSERT_NE(db, nullptr); + + auto config = psr_element_create(); + psr_element_set_string(config, "label", "Test Config"); + psr_database_create_element(db, "Configuration", config); + psr_element_destroy(config); + + auto e1 = psr_element_create(); + psr_element_set_string(e1, "label", "Item 1"); + double values1[] = {1.5, 2.5, 3.5}; + psr_element_set_array_double(e1, "value_float", values1, 3); + psr_database_create_element(db, "Collection", e1); + psr_element_destroy(e1); + + auto e2 = psr_element_create(); + psr_element_set_string(e2, "label", "Item 2"); + double values2[] = {10.5, 20.5}; + psr_element_set_array_double(e2, "value_float", values2, 2); + psr_database_create_element(db, "Collection", e2); + psr_element_destroy(e2); + + double** vectors = nullptr; + size_t* sizes = nullptr; + size_t count = 0; + auto err = psr_database_read_vector_doubles(db, "Collection", "value_float", &vectors, &sizes, &count); + + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(count, 2); + EXPECT_EQ(sizes[0], 3); + EXPECT_EQ(sizes[1], 2); + EXPECT_DOUBLE_EQ(vectors[0][0], 1.5); + EXPECT_DOUBLE_EQ(vectors[0][1], 2.5); + EXPECT_DOUBLE_EQ(vectors[0][2], 3.5); + EXPECT_DOUBLE_EQ(vectors[1][0], 10.5); + EXPECT_DOUBLE_EQ(vectors[1][1], 20.5); + + psr_free_double_vectors(vectors, sizes, count); + psr_database_close(db); +} + +TEST_F(DatabaseFixture, ReadVectorEmpty) { + auto options = psr_database_options_default(); + options.console_level = PSR_LOG_OFF; + auto db = psr_database_from_schema(":memory:", schema_path("schemas/valid/collections.sql").c_str(), &options); + ASSERT_NE(db, nullptr); + + auto config = psr_element_create(); + psr_element_set_string(config, "label", "Test Config"); + psr_database_create_element(db, "Configuration", config); + psr_element_destroy(config); + + int64_t** int_vectors = nullptr; + size_t* int_sizes = nullptr; + size_t int_count = 0; + auto err = psr_database_read_vector_ints(db, "Collection", "value_int", &int_vectors, &int_sizes, &int_count); + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(int_count, 0); + EXPECT_EQ(int_vectors, nullptr); + EXPECT_EQ(int_sizes, nullptr); + + double** double_vectors = nullptr; + size_t* double_sizes = nullptr; + size_t double_count = 0; + err = psr_database_read_vector_doubles( + db, "Collection", "value_float", &double_vectors, &double_sizes, &double_count); + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(double_count, 0); + EXPECT_EQ(double_vectors, nullptr); + EXPECT_EQ(double_sizes, nullptr); + + psr_database_close(db); +} + +TEST_F(DatabaseFixture, ReadVectorOnlyReturnsElementsWithData) { + auto options = psr_database_options_default(); + options.console_level = PSR_LOG_OFF; + auto db = psr_database_from_schema(":memory:", schema_path("schemas/valid/collections.sql").c_str(), &options); + ASSERT_NE(db, nullptr); + + auto config = psr_element_create(); + psr_element_set_string(config, "label", "Test Config"); + psr_database_create_element(db, "Configuration", config); + psr_element_destroy(config); + + // Element with vector data + auto e1 = psr_element_create(); + psr_element_set_string(e1, "label", "Item 1"); + int64_t values1[] = {1, 2, 3}; + psr_element_set_array_int(e1, "value_int", values1, 3); + psr_database_create_element(db, "Collection", e1); + psr_element_destroy(e1); + + // Element without vector data + auto e2 = psr_element_create(); + psr_element_set_string(e2, "label", "Item 2"); + psr_database_create_element(db, "Collection", e2); + psr_element_destroy(e2); + + // Another element with vector data + auto e3 = psr_element_create(); + psr_element_set_string(e3, "label", "Item 3"); + int64_t values3[] = {4, 5}; + psr_element_set_array_int(e3, "value_int", values3, 2); + psr_database_create_element(db, "Collection", e3); + psr_element_destroy(e3); + + int64_t** vectors = nullptr; + size_t* sizes = nullptr; + size_t count = 0; + auto err = psr_database_read_vector_ints(db, "Collection", "value_int", &vectors, &sizes, &count); + + // Only elements with vector data are returned + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(count, 2); + EXPECT_EQ(sizes[0], 3); + EXPECT_EQ(sizes[1], 2); + EXPECT_EQ(vectors[0][0], 1); + EXPECT_EQ(vectors[0][1], 2); + EXPECT_EQ(vectors[0][2], 3); + EXPECT_EQ(vectors[1][0], 4); + EXPECT_EQ(vectors[1][1], 5); + + psr_free_int_vectors(vectors, sizes, count); + psr_database_close(db); +} diff --git a/tests/test_database.cpp b/tests/test_database.cpp index e6ce0b2..e34d956 100644 --- a/tests/test_database.cpp +++ b/tests/test_database.cpp @@ -220,3 +220,165 @@ TEST_F(DatabaseFixture, CurrentVersion) { psr::Database db(":memory:", {.console_level = psr::LogLevel::off}); EXPECT_EQ(db.current_version(), 0); } + +TEST_F(DatabaseFixture, ReadScalarInts) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/basic.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element e1; + e1.set("label", std::string("Config 1")).set("integer_attribute", int64_t{42}); + db.create_element("Configuration", e1); + + psr::Element e2; + e2.set("label", std::string("Config 2")).set("integer_attribute", int64_t{100}); + db.create_element("Configuration", e2); + + auto values = db.read_scalar_ints("Configuration", "integer_attribute"); + EXPECT_EQ(values.size(), 2); + EXPECT_EQ(values[0], 42); + EXPECT_EQ(values[1], 100); +} + +TEST_F(DatabaseFixture, ReadScalarDoubles) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/basic.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element e1; + e1.set("label", std::string("Config 1")).set("float_attribute", 3.14); + db.create_element("Configuration", e1); + + psr::Element e2; + e2.set("label", std::string("Config 2")).set("float_attribute", 2.71); + db.create_element("Configuration", e2); + + auto values = db.read_scalar_doubles("Configuration", "float_attribute"); + EXPECT_EQ(values.size(), 2); + EXPECT_DOUBLE_EQ(values[0], 3.14); + EXPECT_DOUBLE_EQ(values[1], 2.71); +} + +TEST_F(DatabaseFixture, ReadScalarStrings) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/basic.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element e1; + e1.set("label", std::string("Config 1")).set("string_attribute", std::string("hello")); + db.create_element("Configuration", e1); + + psr::Element e2; + e2.set("label", std::string("Config 2")).set("string_attribute", std::string("world")); + db.create_element("Configuration", e2); + + auto values = db.read_scalar_strings("Configuration", "string_attribute"); + EXPECT_EQ(values.size(), 2); + EXPECT_EQ(values[0], "hello"); + EXPECT_EQ(values[1], "world"); +} + +TEST_F(DatabaseFixture, ReadScalarEmpty) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Test Config")); + db.create_element("Configuration", config); + + // No Collection elements created + auto ints = db.read_scalar_ints("Collection", "some_integer"); + auto doubles = db.read_scalar_doubles("Collection", "some_float"); + auto strings = db.read_scalar_strings("Collection", "label"); + + EXPECT_TRUE(ints.empty()); + EXPECT_TRUE(doubles.empty()); + EXPECT_TRUE(strings.empty()); +} + +TEST_F(DatabaseFixture, ReadVectorInts) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Test Config")); + db.create_element("Configuration", config); + + psr::Element e1; + e1.set("label", std::string("Item 1")).set("value_int", std::vector{1, 2, 3}); + db.create_element("Collection", e1); + + psr::Element e2; + e2.set("label", std::string("Item 2")).set("value_int", std::vector{10, 20}); + db.create_element("Collection", e2); + + auto vectors = db.read_vector_ints("Collection", "value_int"); + EXPECT_EQ(vectors.size(), 2); + EXPECT_EQ(vectors[0], (std::vector{1, 2, 3})); + EXPECT_EQ(vectors[1], (std::vector{10, 20})); +} + +TEST_F(DatabaseFixture, ReadVectorDoubles) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Test Config")); + db.create_element("Configuration", config); + + psr::Element e1; + e1.set("label", std::string("Item 1")).set("value_float", std::vector{1.5, 2.5, 3.5}); + db.create_element("Collection", e1); + + psr::Element e2; + e2.set("label", std::string("Item 2")).set("value_float", std::vector{10.5, 20.5}); + db.create_element("Collection", e2); + + auto vectors = db.read_vector_doubles("Collection", "value_float"); + EXPECT_EQ(vectors.size(), 2); + EXPECT_EQ(vectors[0], (std::vector{1.5, 2.5, 3.5})); + EXPECT_EQ(vectors[1], (std::vector{10.5, 20.5})); +} + +TEST_F(DatabaseFixture, ReadVectorEmpty) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Test Config")); + db.create_element("Configuration", config); + + // No Collection elements created + auto int_vectors = db.read_vector_ints("Collection", "value_int"); + auto double_vectors = db.read_vector_doubles("Collection", "value_float"); + + EXPECT_TRUE(int_vectors.empty()); + EXPECT_TRUE(double_vectors.empty()); +} + +TEST_F(DatabaseFixture, ReadVectorOnlyReturnsElementsWithData) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Test Config")); + db.create_element("Configuration", config); + + // Element with vector data + psr::Element e1; + e1.set("label", std::string("Item 1")).set("value_int", std::vector{1, 2, 3}); + db.create_element("Collection", e1); + + // Element without vector data + psr::Element e2; + e2.set("label", std::string("Item 2")); + db.create_element("Collection", e2); + + // Another element with vector data + psr::Element e3; + e3.set("label", std::string("Item 3")).set("value_int", std::vector{4, 5}); + db.create_element("Collection", e3); + + // Only elements with vector data are returned + auto vectors = db.read_vector_ints("Collection", "value_int"); + EXPECT_EQ(vectors.size(), 2); + EXPECT_EQ(vectors[0], (std::vector{1, 2, 3})); + EXPECT_EQ(vectors[1], (std::vector{4, 5})); +} From 68ddfdebf9c333d8c2c289f0e222141fe2752105 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 15:04:39 -0300 Subject: [PATCH 09/19] update --- include/psr/column_type.h | 10 +++------- src/database.cpp | 11 +++++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/include/psr/column_type.h b/include/psr/column_type.h index ee3f203..8c7fa00 100644 --- a/include/psr/column_type.h +++ b/include/psr/column_type.h @@ -6,17 +6,15 @@ namespace psr { -enum class ColumnType { Integer, Real, Text, Blob }; +enum class ColumnType { Integer, Real, Text }; inline ColumnType column_type_from_string(const std::string& type_str) { if (type_str == "INTEGER") return ColumnType::Integer; - if (type_str == "REAL") + else if (type_str == "REAL") return ColumnType::Real; - if (type_str == "TEXT") + else if (type_str == "TEXT") return ColumnType::Text; - if (type_str == "BLOB") - return ColumnType::Blob; throw std::runtime_error("Unknown column type: " + type_str); } @@ -28,8 +26,6 @@ inline const char* column_type_to_string(ColumnType type) { return "REAL"; case ColumnType::Text: return "TEXT"; - case ColumnType::Blob: - return "BLOB"; } return "UNKNOWN"; } diff --git a/src/database.cpp b/src/database.cpp index c39841b..c5b0e12 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -189,7 +189,7 @@ Result Database::execute(const std::string& sql, const std::vector& param values.reserve(col_count); for (int i = 0; i < col_count; ++i) { - int type = sqlite3_column_type(stmt, i); + auto type = sqlite3_column_type(stmt, i); switch (type) { case SQLITE_INTEGER: values.emplace_back(sqlite3_column_int64(stmt, i)); @@ -201,12 +201,15 @@ Result Database::execute(const std::string& sql, const std::vector& param const char* text = reinterpret_cast(sqlite3_column_text(stmt, i)); values.emplace_back(std::string(text ? text : "")); break; - } - case SQLITE_BLOB: + } case SQLITE_NULL: - default: values.emplace_back(nullptr); break; + case SQLITE_BLOB: + throw std::runtime_error("Blob not implemented"); + default: + throw std::runtime_error("Type not implemented"); + break; } } rows.emplace_back(std::move(values)); From 40fce614ed6754ab7c90ff2dbd52b4b4b9ad3696 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 19:44:50 -0300 Subject: [PATCH 10/19] update --- src/database.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database.cpp b/src/database.cpp index c5b0e12..05e9fdc 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -201,7 +201,7 @@ Result Database::execute(const std::string& sql, const std::vector& param const char* text = reinterpret_cast(sqlite3_column_text(stmt, i)); values.emplace_back(std::string(text ? text : "")); break; - } + } case SQLITE_NULL: values.emplace_back(nullptr); break; From 813e832ea8bfea4232460a41cd9546367171eaaa Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 20:02:33 -0300 Subject: [PATCH 11/19] update --- include/psr/c/database.h | 22 ++++++++ include/psr/database.h | 5 ++ src/c_api_database.cpp | 72 ++++++++++++++++++++++++++ src/database.cpp | 107 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+) diff --git a/include/psr/c/database.h b/include/psr/c/database.h index 5fc143c..eca733c 100644 --- a/include/psr/c/database.h +++ b/include/psr/c/database.h @@ -93,6 +93,28 @@ PSR_C_API psr_error_t psr_database_read_vector_strings(psr_database_t* db, size_t** out_sizes, size_t* out_count); +// Read set attributes (same structure as vectors, uses same free functions) +PSR_C_API psr_error_t psr_database_read_set_ints(psr_database_t* db, + const char* collection, + const char* attribute, + int64_t*** out_sets, + size_t** out_sizes, + size_t* out_count); + +PSR_C_API psr_error_t psr_database_read_set_doubles(psr_database_t* db, + const char* collection, + const char* attribute, + double*** out_sets, + size_t** out_sizes, + size_t* out_count); + +PSR_C_API psr_error_t psr_database_read_set_strings(psr_database_t* db, + const char* collection, + const char* attribute, + char**** out_sets, + size_t** out_sizes, + size_t* out_count); + // Memory cleanup for read results PSR_C_API void psr_free_int_array(int64_t* values); PSR_C_API void psr_free_double_array(double* values); diff --git a/include/psr/database.h b/include/psr/database.h index 70ef43f..3f8426d 100644 --- a/include/psr/database.h +++ b/include/psr/database.h @@ -61,6 +61,11 @@ class PSR_API Database { std::vector> read_vector_strings(const std::string& collection, const std::string& attribute); + // Read set attributes + std::vector> read_set_ints(const std::string& collection, const std::string& attribute); + std::vector> read_set_doubles(const std::string& collection, const std::string& attribute); + std::vector> read_set_strings(const std::string& collection, const std::string& attribute); + // Transaction management void begin_transaction(); void commit(); diff --git a/src/c_api_database.cpp b/src/c_api_database.cpp index 04cc0eb..3495a71 100644 --- a/src/c_api_database.cpp +++ b/src/c_api_database.cpp @@ -425,4 +425,76 @@ PSR_C_API void psr_free_string_vectors(char*** vectors, size_t* sizes, size_t co delete[] sizes; } +// Set read functions (reuse vector helpers since sets have same return structure) + +PSR_C_API psr_error_t psr_database_read_set_ints(psr_database_t* db, + const char* collection, + const char* attribute, + int64_t*** out_sets, + size_t** out_sizes, + size_t* out_count) { + if (!db || !collection || !attribute || !out_sets || !out_sizes || !out_count) { + return PSR_ERROR_INVALID_ARGUMENT; + } + try { + return read_vectors_impl(db->db.read_set_ints(collection, attribute), out_sets, out_sizes, out_count); + } catch (const std::exception&) { + return PSR_ERROR_DATABASE; + } +} + +PSR_C_API psr_error_t psr_database_read_set_doubles(psr_database_t* db, + const char* collection, + const char* attribute, + double*** out_sets, + size_t** out_sizes, + size_t* out_count) { + if (!db || !collection || !attribute || !out_sets || !out_sizes || !out_count) { + return PSR_ERROR_INVALID_ARGUMENT; + } + try { + return read_vectors_impl(db->db.read_set_doubles(collection, attribute), out_sets, out_sizes, out_count); + } catch (const std::exception&) { + return PSR_ERROR_DATABASE; + } +} + +PSR_C_API psr_error_t psr_database_read_set_strings(psr_database_t* db, + const char* collection, + const char* attribute, + char**** out_sets, + size_t** out_sizes, + size_t* out_count) { + if (!db || !collection || !attribute || !out_sets || !out_sizes || !out_count) { + return PSR_ERROR_INVALID_ARGUMENT; + } + try { + auto sets = db->db.read_set_strings(collection, attribute); + *out_count = sets.size(); + if (sets.empty()) { + *out_sets = nullptr; + *out_sizes = nullptr; + return PSR_OK; + } + *out_sets = new char**[sets.size()]; + *out_sizes = new size_t[sets.size()]; + for (size_t i = 0; i < sets.size(); ++i) { + (*out_sizes)[i] = sets[i].size(); + if (sets[i].empty()) { + (*out_sets)[i] = nullptr; + } else { + (*out_sets)[i] = new char*[sets[i].size()]; + for (size_t j = 0; j < sets[i].size(); ++j) { + (*out_sets)[i][j] = new char[sets[i][j].size() + 1]; + std::copy(sets[i][j].begin(), sets[i][j].end(), (*out_sets)[i][j]); + (*out_sets)[i][j][sets[i][j].size()] = '\0'; + } + } + } + return PSR_OK; + } catch (const std::exception&) { + return PSR_ERROR_DATABASE; + } +} + } // extern "C" diff --git a/src/database.cpp b/src/database.cpp index 05e9fdc..d01dcb1 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -769,4 +769,111 @@ std::vector> Database::read_vector_strings(const std::s return vectors; } +// Helper to find set table for an attribute +static std::string find_set_table(const Schema* schema, const std::string& collection, const std::string& attribute) { + // First try: Collection_set_attribute + auto set_table = Schema::set_table_name(collection, attribute); + if (schema->has_table(set_table)) { + return set_table; + } + + // Second try: search all set tables for the collection + for (const auto& table_name : schema->table_names()) { + if (!schema->is_set_table(table_name)) + continue; + if (schema->get_parent_collection(table_name) != collection) + continue; + + const auto* table_def = schema->get_table(table_name); + if (table_def && table_def->has_column(attribute)) { + return table_name; + } + } + + throw std::runtime_error("Set attribute '" + attribute + "' not found for collection '" + collection + "'"); +} + +std::vector> Database::read_set_ints(const std::string& collection, const std::string& attribute) { + auto set_table = find_set_table(impl_->schema.get(), collection, attribute); + auto sql = "SELECT id, " + attribute + " FROM " + set_table + " ORDER BY id"; + auto result = execute(sql); + + std::vector> sets; + int64_t current_id = -1; + + for (size_t i = 0; i < result.row_count(); ++i) { + auto id = result[i].get_int(0); + auto val = result[i].get_int(1); + + if (!id) + continue; + + if (*id != current_id) { + sets.emplace_back(); + current_id = *id; + } + + if (val) { + sets.back().push_back(*val); + } + } + return sets; +} + +std::vector> Database::read_set_doubles(const std::string& collection, + const std::string& attribute) { + auto set_table = find_set_table(impl_->schema.get(), collection, attribute); + auto sql = "SELECT id, " + attribute + " FROM " + set_table + " ORDER BY id"; + auto result = execute(sql); + + std::vector> sets; + int64_t current_id = -1; + + for (size_t i = 0; i < result.row_count(); ++i) { + auto id = result[i].get_int(0); + auto val = result[i].get_double(1); + + if (!id) + continue; + + if (*id != current_id) { + sets.emplace_back(); + current_id = *id; + } + + if (val) { + sets.back().push_back(*val); + } + } + return sets; +} + +std::vector> Database::read_set_strings(const std::string& collection, + const std::string& attribute) { + auto set_table = find_set_table(impl_->schema.get(), collection, attribute); + auto sql = "SELECT id, " + attribute + " FROM " + set_table + " ORDER BY id"; + auto result = execute(sql); + + std::vector> sets; + int64_t current_id = -1; + + for (size_t i = 0; i < result.row_count(); ++i) { + auto id = result[i].get_int(0); + auto val = result[i].get_string(1); + + if (!id) + continue; + + if (*id != current_id) { + sets.emplace_back(); + current_id = *id; + } + + if (val) { + sets.back().push_back(*val); + } + } + return sets; +} + } // namespace psr From 4ea498dc8280a3c37f4f8a6bb1e243cdf7b76c33 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 20:03:25 -0300 Subject: [PATCH 12/19] update --- bindings/julia/src/c_api.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bindings/julia/src/c_api.jl b/bindings/julia/src/c_api.jl index ebb26d0..c47b9cc 100644 --- a/bindings/julia/src/c_api.jl +++ b/bindings/julia/src/c_api.jl @@ -146,6 +146,18 @@ function psr_database_read_vector_strings(db, collection, attribute, out_vectors @ccall libpsr_database_c.psr_database_read_vector_strings(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, out_vectors::Ptr{Ptr{Ptr{Ptr{Cchar}}}}, out_sizes::Ptr{Ptr{Csize_t}}, out_count::Ptr{Csize_t})::psr_error_t end +function psr_database_read_set_ints(db, collection, attribute, out_sets, out_sizes, out_count) + @ccall libpsr_database_c.psr_database_read_set_ints(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, out_sets::Ptr{Ptr{Ptr{Int64}}}, out_sizes::Ptr{Ptr{Csize_t}}, out_count::Ptr{Csize_t})::psr_error_t +end + +function psr_database_read_set_doubles(db, collection, attribute, out_sets, out_sizes, out_count) + @ccall libpsr_database_c.psr_database_read_set_doubles(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, out_sets::Ptr{Ptr{Ptr{Cdouble}}}, out_sizes::Ptr{Ptr{Csize_t}}, out_count::Ptr{Csize_t})::psr_error_t +end + +function psr_database_read_set_strings(db, collection, attribute, out_sets, out_sizes, out_count) + @ccall libpsr_database_c.psr_database_read_set_strings(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, out_sets::Ptr{Ptr{Ptr{Ptr{Cchar}}}}, out_sizes::Ptr{Ptr{Csize_t}}, out_count::Ptr{Csize_t})::psr_error_t +end + function psr_free_int_array(values) @ccall libpsr_database_c.psr_free_int_array(values::Ptr{Int64})::Cvoid end From 3c315a28036dfd4765c77bb0ca5cc4f71ac37979 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 20:04:34 -0300 Subject: [PATCH 13/19] update --- bindings/julia/src/database.jl | 88 ++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/bindings/julia/src/database.jl b/bindings/julia/src/database.jl index 0640d66..de2657c 100644 --- a/bindings/julia/src/database.jl +++ b/bindings/julia/src/database.jl @@ -177,3 +177,91 @@ function read_vector_strings(db::Database, collection::String, attribute::String C.psr_free_string_vectors(out_vectors[], out_sizes[], count) return result end + +function read_set_ints(db::Database, collection::String, attribute::String) + out_sets = Ref{Ptr{Ptr{Int64}}}(C_NULL) + out_sizes = Ref{Ptr{Csize_t}}(C_NULL) + out_count = Ref{Csize_t}(0) + + err = C.psr_database_read_set_ints(db.ptr, collection, attribute, out_sets, out_sizes, out_count) + if err != C.PSR_OK + throw(DatabaseException("Failed to read set ints from '$collection.$attribute'")) + end + + count = out_count[] + if count == 0 || out_sets[] == C_NULL + return Vector{Int64}[] + end + + sets_ptr = unsafe_wrap(Array, out_sets[], count) + sizes_ptr = unsafe_wrap(Array, out_sizes[], count) + result = Vector{Int64}[] + for i in 1:count + if sets_ptr[i] == C_NULL || sizes_ptr[i] == 0 + push!(result, Int64[]) + else + push!(result, copy(unsafe_wrap(Array, sets_ptr[i], sizes_ptr[i]))) + end + end + C.psr_free_int_vectors(out_sets[], out_sizes[], count) + return result +end + +function read_set_doubles(db::Database, collection::String, attribute::String) + out_sets = Ref{Ptr{Ptr{Cdouble}}}(C_NULL) + out_sizes = Ref{Ptr{Csize_t}}(C_NULL) + out_count = Ref{Csize_t}(0) + + err = C.psr_database_read_set_doubles(db.ptr, collection, attribute, out_sets, out_sizes, out_count) + if err != C.PSR_OK + throw(DatabaseException("Failed to read set doubles from '$collection.$attribute'")) + end + + count = out_count[] + if count == 0 || out_sets[] == C_NULL + return Vector{Float64}[] + end + + sets_ptr = unsafe_wrap(Array, out_sets[], count) + sizes_ptr = unsafe_wrap(Array, out_sizes[], count) + result = Vector{Float64}[] + for i in 1:count + if sets_ptr[i] == C_NULL || sizes_ptr[i] == 0 + push!(result, Float64[]) + else + push!(result, copy(unsafe_wrap(Array, sets_ptr[i], sizes_ptr[i]))) + end + end + C.psr_free_double_vectors(out_sets[], out_sizes[], count) + return result +end + +function read_set_strings(db::Database, collection::String, attribute::String) + out_sets = Ref{Ptr{Ptr{Ptr{Cchar}}}}(C_NULL) + out_sizes = Ref{Ptr{Csize_t}}(C_NULL) + out_count = Ref{Csize_t}(0) + + err = C.psr_database_read_set_strings(db.ptr, collection, attribute, out_sets, out_sizes, out_count) + if err != C.PSR_OK + throw(DatabaseException("Failed to read set strings from '$collection.$attribute'")) + end + + count = out_count[] + if count == 0 || out_sets[] == C_NULL + return Vector{String}[] + end + + sets_ptr = unsafe_wrap(Array, out_sets[], count) + sizes_ptr = unsafe_wrap(Array, out_sizes[], count) + result = Vector{String}[] + for i in 1:count + if sets_ptr[i] == C_NULL || sizes_ptr[i] == 0 + push!(result, String[]) + else + str_ptrs = unsafe_wrap(Array, sets_ptr[i], sizes_ptr[i]) + push!(result, [unsafe_string(ptr) for ptr in str_ptrs]) + end + end + C.psr_free_string_vectors(out_sets[], out_sizes[], count) + return result +end From 51a153a47a8853030aac528937d1c09f4c36e5a3 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 20:13:54 -0300 Subject: [PATCH 14/19] update --- bindings/dart/lib/src/database.dart | 132 ++++++++++++++++++++++++ bindings/dart/lib/src/ffi/bindings.dart | 111 ++++++++++++++++++++ bindings/dart/test/read_test.dart | 74 +++++++++++++ bindings/julia/test/test_read.jl | 64 ++++++++++++ tests/test_c_api_database.cpp | 122 ++++++++++++++++++++++ tests/test_database.cpp | 68 ++++++++++++ 6 files changed, 571 insertions(+) diff --git a/bindings/dart/lib/src/database.dart b/bindings/dart/lib/src/database.dart index 6756f63..5f5141e 100644 --- a/bindings/dart/lib/src/database.dart +++ b/bindings/dart/lib/src/database.dart @@ -317,6 +317,138 @@ class Database { } } + /// Reads all int sets for a set attribute from a collection. + List> readSetInts(String collection, String attribute) { + _ensureNotClosed(); + + final arena = Arena(); + try { + final outSets = arena>>(); + final outSizes = arena>(); + final outCount = arena(); + + final err = bindings.psr_database_read_set_ints( + _ptr, + collection.toNativeUtf8(allocator: arena).cast(), + attribute.toNativeUtf8(allocator: arena).cast(), + outSets, + outSizes, + outCount, + ); + + if (err != psr_error_t.PSR_OK) { + throw DatabaseException.fromError(err, "Failed to read set ints from '$collection.$attribute'"); + } + + final count = outCount.value; + if (count == 0 || outSets.value == nullptr) { + return []; + } + + final result = >[]; + for (var i = 0; i < count; i++) { + final size = outSizes.value[i]; + if (size == 0 || outSets.value[i] == nullptr) { + result.add([]); + } else { + result.add(List.generate(size, (j) => outSets.value[i][j])); + } + } + bindings.psr_free_int_vectors(outSets.value, outSizes.value, count); + return result; + } finally { + arena.releaseAll(); + } + } + + /// Reads all double sets for a set attribute from a collection. + List> readSetDoubles(String collection, String attribute) { + _ensureNotClosed(); + + final arena = Arena(); + try { + final outSets = arena>>(); + final outSizes = arena>(); + final outCount = arena(); + + final err = bindings.psr_database_read_set_doubles( + _ptr, + collection.toNativeUtf8(allocator: arena).cast(), + attribute.toNativeUtf8(allocator: arena).cast(), + outSets, + outSizes, + outCount, + ); + + if (err != psr_error_t.PSR_OK) { + throw DatabaseException.fromError(err, "Failed to read set doubles from '$collection.$attribute'"); + } + + final count = outCount.value; + if (count == 0 || outSets.value == nullptr) { + return []; + } + + final result = >[]; + for (var i = 0; i < count; i++) { + final size = outSizes.value[i]; + if (size == 0 || outSets.value[i] == nullptr) { + result.add([]); + } else { + result.add(List.generate(size, (j) => outSets.value[i][j])); + } + } + bindings.psr_free_double_vectors(outSets.value, outSizes.value, count); + return result; + } finally { + arena.releaseAll(); + } + } + + /// Reads all string sets for a set attribute from a collection. + List> readSetStrings(String collection, String attribute) { + _ensureNotClosed(); + + final arena = Arena(); + try { + final outSets = arena>>>(); + final outSizes = arena>(); + final outCount = arena(); + + final err = bindings.psr_database_read_set_strings( + _ptr, + collection.toNativeUtf8(allocator: arena).cast(), + attribute.toNativeUtf8(allocator: arena).cast(), + outSets, + outSizes, + outCount, + ); + + if (err != psr_error_t.PSR_OK) { + throw DatabaseException.fromError(err, "Failed to read set strings from '$collection.$attribute'"); + } + + final count = outCount.value; + if (count == 0 || outSets.value == nullptr) { + return []; + } + + final result = >[]; + for (var i = 0; i < count; i++) { + final size = outSizes.value[i]; + if (size == 0 || outSets.value[i] == nullptr) { + result.add([]); + } else { + result.add(List.generate(size, (j) => outSets.value[i][j].cast().toDartString())); + } + } + bindings.psr_free_string_vectors(outSets.value, outSizes.value, count); + return result; + } finally { + arena.releaseAll(); + } + } + /// Closes the database and frees native resources. void close() { if (_isClosed) return; diff --git a/bindings/dart/lib/src/ffi/bindings.dart b/bindings/dart/lib/src/ffi/bindings.dart index 91d30ed..adcd2ea 100644 --- a/bindings/dart/lib/src/ffi/bindings.dart +++ b/bindings/dart/lib/src/ffi/bindings.dart @@ -484,6 +484,117 @@ class PsrDatabaseBindings { ffi.Pointer>, ffi.Pointer)>(); + int psr_database_read_set_ints( + ffi.Pointer db, + ffi.Pointer collection, + ffi.Pointer attribute, + ffi.Pointer>> out_sets, + ffi.Pointer> out_sizes, + ffi.Pointer out_count, + ) { + return _psr_database_read_set_ints( + db, + collection, + attribute, + out_sets, + out_sizes, + out_count, + ); + } + + late final _psr_database_read_set_intsPtr = _lookup< + ffi.NativeFunction< + ffi.Int32 Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>, + ffi.Pointer>, + ffi.Pointer)>>('psr_database_read_set_ints'); + late final _psr_database_read_set_ints = + _psr_database_read_set_intsPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>, + ffi.Pointer>, + ffi.Pointer)>(); + + int psr_database_read_set_doubles( + ffi.Pointer db, + ffi.Pointer collection, + ffi.Pointer attribute, + ffi.Pointer>> out_sets, + ffi.Pointer> out_sizes, + ffi.Pointer out_count, + ) { + return _psr_database_read_set_doubles( + db, + collection, + attribute, + out_sets, + out_sizes, + out_count, + ); + } + + late final _psr_database_read_set_doublesPtr = _lookup< + ffi.NativeFunction< + ffi.Int32 Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>, + ffi.Pointer>, + ffi.Pointer)>>('psr_database_read_set_doubles'); + late final _psr_database_read_set_doubles = + _psr_database_read_set_doublesPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>, + ffi.Pointer>, + ffi.Pointer)>(); + + int psr_database_read_set_strings( + ffi.Pointer db, + ffi.Pointer collection, + ffi.Pointer attribute, + ffi.Pointer>>> out_sets, + ffi.Pointer> out_sizes, + ffi.Pointer out_count, + ) { + return _psr_database_read_set_strings( + db, + collection, + attribute, + out_sets, + out_sizes, + out_count, + ); + } + + late final _psr_database_read_set_stringsPtr = _lookup< + ffi.NativeFunction< + ffi.Int32 Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>>, + ffi.Pointer>, + ffi.Pointer)>>('psr_database_read_set_strings'); + late final _psr_database_read_set_strings = + _psr_database_read_set_stringsPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>>, + ffi.Pointer>, + ffi.Pointer)>(); + void psr_free_int_array( ffi.Pointer values, ) { diff --git a/bindings/dart/test/read_test.dart b/bindings/dart/test/read_test.dart index 61858c7..67abec0 100644 --- a/bindings/dart/test/read_test.dart +++ b/bindings/dart/test/read_test.dart @@ -250,4 +250,78 @@ void main() { } }); }); + + group('Read Set Attributes', () { + test('reads string sets from Collection', () { + final db = Database.fromSchema( + ':memory:', + path.join(testsPath, 'schemas', 'valid', 'collections.sql'), + ); + try { + db.createElement('Configuration', {'label': 'Test Config'}); + db.createElement('Collection', { + 'label': 'Item 1', + 'tag': ['important', 'urgent'], + }); + db.createElement('Collection', { + 'label': 'Item 2', + 'tag': ['review'], + }); + + final result = db.readSetStrings('Collection', 'tag'); + expect(result.length, equals(2)); + // Sets are unordered, so sort before comparison + expect(result[0]..sort(), equals(['important', 'urgent'])); + expect(result[1], equals(['review'])); + } finally { + db.close(); + } + }); + }); + + group('Read Set Empty Result', () { + test('returns empty list when no elements', () { + final db = Database.fromSchema( + ':memory:', + path.join(testsPath, 'schemas', 'valid', 'collections.sql'), + ); + try { + db.createElement('Configuration', {'label': 'Test Config'}); + + expect(db.readSetStrings('Collection', 'tag'), isEmpty); + } finally { + db.close(); + } + }); + }); + + group('Read Set Only Returns Elements With Data', () { + test('only returns sets for elements with data', () { + final db = Database.fromSchema( + ':memory:', + path.join(testsPath, 'schemas', 'valid', 'collections.sql'), + ); + try { + db.createElement('Configuration', {'label': 'Test Config'}); + db.createElement('Collection', { + 'label': 'Item 1', + 'tag': ['important'], + }); + db.createElement('Collection', { + 'label': 'Item 2', + // No set data + }); + db.createElement('Collection', { + 'label': 'Item 3', + 'tag': ['urgent', 'review'], + }); + + // Only elements with set data are returned + final result = db.readSetStrings('Collection', 'tag'); + expect(result.length, equals(2)); + } finally { + db.close(); + } + }); + }); } diff --git a/bindings/julia/test/test_read.jl b/bindings/julia/test/test_read.jl index 5ae0820..425e14c 100644 --- a/bindings/julia/test/test_read.jl +++ b/bindings/julia/test/test_read.jl @@ -134,4 +134,68 @@ end PSRDatabase.close!(db) end +@testset "Read Set Attributes" begin + path_schema = joinpath(tests_path(), "schemas", "valid", "collections.sql") + db = PSRDatabase.create_empty_db_from_schema(":memory:", path_schema; force = true) + + PSRDatabase.create_element!(db, "Configuration"; label = "Test Config") + + PSRDatabase.create_element!(db, "Collection"; + label = "Item 1", + tag = ["important", "urgent"], + ) + PSRDatabase.create_element!(db, "Collection"; + label = "Item 2", + tag = ["review"], + ) + + result = PSRDatabase.read_set_strings(db, "Collection", "tag") + @test length(result) == 2 + # Sets are unordered, so sort before comparison + @test sort(result[1]) == ["important", "urgent"] + @test result[2] == ["review"] + + PSRDatabase.close!(db) +end + +@testset "Read Set Empty Result" begin + path_schema = joinpath(tests_path(), "schemas", "valid", "collections.sql") + db = PSRDatabase.create_empty_db_from_schema(":memory:", path_schema; force = true) + + PSRDatabase.create_element!(db, "Configuration"; label = "Test Config") + + # No Collection elements created + @test PSRDatabase.read_set_strings(db, "Collection", "tag") == Vector{String}[] + + PSRDatabase.close!(db) +end + +@testset "Read Set Only Returns Elements With Data" begin + path_schema = joinpath(tests_path(), "schemas", "valid", "collections.sql") + db = PSRDatabase.create_empty_db_from_schema(":memory:", path_schema; force = true) + + PSRDatabase.create_element!(db, "Configuration"; label = "Test Config") + + # Create element with set data + PSRDatabase.create_element!(db, "Collection"; + label = "Item 1", + tag = ["important"], + ) + # Create element without set data + PSRDatabase.create_element!(db, "Collection"; + label = "Item 2", + ) + # Create another element with set data + PSRDatabase.create_element!(db, "Collection"; + label = "Item 3", + tag = ["urgent", "review"], + ) + + # Only elements with set data are returned + result = PSRDatabase.read_set_strings(db, "Collection", "tag") + @test length(result) == 2 + + PSRDatabase.close!(db) +end + end diff --git a/tests/test_c_api_database.cpp b/tests/test_c_api_database.cpp index 553ac1e..77dd0dd 100644 --- a/tests/test_c_api_database.cpp +++ b/tests/test_c_api_database.cpp @@ -553,3 +553,125 @@ TEST_F(DatabaseFixture, ReadVectorOnlyReturnsElementsWithData) { psr_free_int_vectors(vectors, sizes, count); psr_database_close(db); } + +TEST_F(DatabaseFixture, ReadSetStrings) { + auto options = psr_database_options_default(); + options.console_level = PSR_LOG_OFF; + auto db = psr_database_from_schema(":memory:", schema_path("schemas/valid/collections.sql").c_str(), &options); + ASSERT_NE(db, nullptr); + + auto config = psr_element_create(); + psr_element_set_string(config, "label", "Test Config"); + psr_database_create_element(db, "Configuration", config); + psr_element_destroy(config); + + auto e1 = psr_element_create(); + psr_element_set_string(e1, "label", "Item 1"); + const char* tags1[] = {"important", "urgent"}; + psr_element_set_array_string(e1, "tag", tags1, 2); + psr_database_create_element(db, "Collection", e1); + psr_element_destroy(e1); + + auto e2 = psr_element_create(); + psr_element_set_string(e2, "label", "Item 2"); + const char* tags2[] = {"review"}; + psr_element_set_array_string(e2, "tag", tags2, 1); + psr_database_create_element(db, "Collection", e2); + psr_element_destroy(e2); + + char*** sets = nullptr; + size_t* sizes = nullptr; + size_t count = 0; + auto err = psr_database_read_set_strings(db, "Collection", "tag", &sets, &sizes, &count); + + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(count, 2); + EXPECT_EQ(sizes[0], 2); + EXPECT_EQ(sizes[1], 1); + + // Sets are unordered, so just check values exist + std::vector set1_values; + for (size_t i = 0; i < sizes[0]; i++) { + set1_values.push_back(sets[0][i]); + } + std::sort(set1_values.begin(), set1_values.end()); + EXPECT_EQ(set1_values[0], "important"); + EXPECT_EQ(set1_values[1], "urgent"); + + EXPECT_STREQ(sets[1][0], "review"); + + psr_free_string_vectors(sets, sizes, count); + psr_database_close(db); +} + +TEST_F(DatabaseFixture, ReadSetEmpty) { + auto options = psr_database_options_default(); + options.console_level = PSR_LOG_OFF; + auto db = psr_database_from_schema(":memory:", schema_path("schemas/valid/collections.sql").c_str(), &options); + ASSERT_NE(db, nullptr); + + auto config = psr_element_create(); + psr_element_set_string(config, "label", "Test Config"); + psr_database_create_element(db, "Configuration", config); + psr_element_destroy(config); + + char*** sets = nullptr; + size_t* sizes = nullptr; + size_t count = 0; + auto err = psr_database_read_set_strings(db, "Collection", "tag", &sets, &sizes, &count); + + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(count, 0); + EXPECT_EQ(sets, nullptr); + EXPECT_EQ(sizes, nullptr); + + psr_database_close(db); +} + +TEST_F(DatabaseFixture, ReadSetOnlyReturnsElementsWithData) { + auto options = psr_database_options_default(); + options.console_level = PSR_LOG_OFF; + auto db = psr_database_from_schema(":memory:", schema_path("schemas/valid/collections.sql").c_str(), &options); + ASSERT_NE(db, nullptr); + + auto config = psr_element_create(); + psr_element_set_string(config, "label", "Test Config"); + psr_database_create_element(db, "Configuration", config); + psr_element_destroy(config); + + // Element with set data + auto e1 = psr_element_create(); + psr_element_set_string(e1, "label", "Item 1"); + const char* tags1[] = {"important"}; + psr_element_set_array_string(e1, "tag", tags1, 1); + psr_database_create_element(db, "Collection", e1); + psr_element_destroy(e1); + + // Element without set data + auto e2 = psr_element_create(); + psr_element_set_string(e2, "label", "Item 2"); + psr_database_create_element(db, "Collection", e2); + psr_element_destroy(e2); + + // Another element with set data + auto e3 = psr_element_create(); + psr_element_set_string(e3, "label", "Item 3"); + const char* tags3[] = {"urgent", "review"}; + psr_element_set_array_string(e3, "tag", tags3, 2); + psr_database_create_element(db, "Collection", e3); + psr_element_destroy(e3); + + char*** sets = nullptr; + size_t* sizes = nullptr; + size_t count = 0; + auto err = psr_database_read_set_strings(db, "Collection", "tag", &sets, &sizes, &count); + + // Only elements with set data are returned + EXPECT_EQ(err, PSR_OK); + EXPECT_EQ(count, 2); + EXPECT_EQ(sizes[0], 1); + EXPECT_EQ(sizes[1], 2); + + psr_free_string_vectors(sets, sizes, count); + psr_database_close(db); +} diff --git a/tests/test_database.cpp b/tests/test_database.cpp index e34d956..fee2de3 100644 --- a/tests/test_database.cpp +++ b/tests/test_database.cpp @@ -382,3 +382,71 @@ TEST_F(DatabaseFixture, ReadVectorOnlyReturnsElementsWithData) { EXPECT_EQ(vectors[0], (std::vector{1, 2, 3})); EXPECT_EQ(vectors[1], (std::vector{4, 5})); } + +TEST_F(DatabaseFixture, ReadSetStrings) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Test Config")); + db.create_element("Configuration", config); + + psr::Element e1; + e1.set("label", std::string("Item 1")).set("tag", std::vector{"important", "urgent"}); + db.create_element("Collection", e1); + + psr::Element e2; + e2.set("label", std::string("Item 2")).set("tag", std::vector{"review"}); + db.create_element("Collection", e2); + + auto sets = db.read_set_strings("Collection", "tag"); + EXPECT_EQ(sets.size(), 2); + // Sets are unordered, so sort before comparison + auto set1 = sets[0]; + auto set2 = sets[1]; + std::sort(set1.begin(), set1.end()); + std::sort(set2.begin(), set2.end()); + EXPECT_EQ(set1, (std::vector{"important", "urgent"})); + EXPECT_EQ(set2, (std::vector{"review"})); +} + +TEST_F(DatabaseFixture, ReadSetEmpty) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Test Config")); + db.create_element("Configuration", config); + + // No Collection elements created + auto sets = db.read_set_strings("Collection", "tag"); + EXPECT_TRUE(sets.empty()); +} + +TEST_F(DatabaseFixture, ReadSetOnlyReturnsElementsWithData) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Test Config")); + db.create_element("Configuration", config); + + // Element with set data + psr::Element e1; + e1.set("label", std::string("Item 1")).set("tag", std::vector{"important"}); + db.create_element("Collection", e1); + + // Element without set data + psr::Element e2; + e2.set("label", std::string("Item 2")); + db.create_element("Collection", e2); + + // Another element with set data + psr::Element e3; + e3.set("label", std::string("Item 3")).set("tag", std::vector{"urgent", "review"}); + db.create_element("Collection", e3); + + // Only elements with set data are returned + auto sets = db.read_set_strings("Collection", "tag"); + EXPECT_EQ(sets.size(), 2); +} From f8ad87374bce09802998b21a0c5ab1b6cc250cf4 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 20:23:42 -0300 Subject: [PATCH 15/19] update --- include/psr/c/database.h | 30 +++++++++++++++--------------- src/c_api_database.cpp | 30 +++++++++++++++--------------- src/database.cpp | 4 ++-- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/include/psr/c/database.h b/include/psr/c/database.h index eca733c..a684cca 100644 --- a/include/psr/c/database.h +++ b/include/psr/c/database.h @@ -95,25 +95,25 @@ PSR_C_API psr_error_t psr_database_read_vector_strings(psr_database_t* db, // Read set attributes (same structure as vectors, uses same free functions) PSR_C_API psr_error_t psr_database_read_set_ints(psr_database_t* db, - const char* collection, - const char* attribute, - int64_t*** out_sets, - size_t** out_sizes, - size_t* out_count); + const char* collection, + const char* attribute, + int64_t*** out_sets, + size_t** out_sizes, + size_t* out_count); PSR_C_API psr_error_t psr_database_read_set_doubles(psr_database_t* db, - const char* collection, - const char* attribute, - double*** out_sets, - size_t** out_sizes, - size_t* out_count); + const char* collection, + const char* attribute, + double*** out_sets, + size_t** out_sizes, + size_t* out_count); PSR_C_API psr_error_t psr_database_read_set_strings(psr_database_t* db, - const char* collection, - const char* attribute, - char**** out_sets, - size_t** out_sizes, - size_t* out_count); + const char* collection, + const char* attribute, + char**** out_sets, + size_t** out_sizes, + size_t* out_count); // Memory cleanup for read results PSR_C_API void psr_free_int_array(int64_t* values); diff --git a/src/c_api_database.cpp b/src/c_api_database.cpp index 3495a71..0d39b25 100644 --- a/src/c_api_database.cpp +++ b/src/c_api_database.cpp @@ -428,11 +428,11 @@ PSR_C_API void psr_free_string_vectors(char*** vectors, size_t* sizes, size_t co // Set read functions (reuse vector helpers since sets have same return structure) PSR_C_API psr_error_t psr_database_read_set_ints(psr_database_t* db, - const char* collection, - const char* attribute, - int64_t*** out_sets, - size_t** out_sizes, - size_t* out_count) { + const char* collection, + const char* attribute, + int64_t*** out_sets, + size_t** out_sizes, + size_t* out_count) { if (!db || !collection || !attribute || !out_sets || !out_sizes || !out_count) { return PSR_ERROR_INVALID_ARGUMENT; } @@ -444,11 +444,11 @@ PSR_C_API psr_error_t psr_database_read_set_ints(psr_database_t* db, } PSR_C_API psr_error_t psr_database_read_set_doubles(psr_database_t* db, - const char* collection, - const char* attribute, - double*** out_sets, - size_t** out_sizes, - size_t* out_count) { + const char* collection, + const char* attribute, + double*** out_sets, + size_t** out_sizes, + size_t* out_count) { if (!db || !collection || !attribute || !out_sets || !out_sizes || !out_count) { return PSR_ERROR_INVALID_ARGUMENT; } @@ -460,11 +460,11 @@ PSR_C_API psr_error_t psr_database_read_set_doubles(psr_database_t* db, } PSR_C_API psr_error_t psr_database_read_set_strings(psr_database_t* db, - const char* collection, - const char* attribute, - char**** out_sets, - size_t** out_sizes, - size_t* out_count) { + const char* collection, + const char* attribute, + char**** out_sets, + size_t** out_sizes, + size_t* out_count) { if (!db || !collection || !attribute || !out_sets || !out_sizes || !out_count) { return PSR_ERROR_INVALID_ARGUMENT; } diff --git a/src/database.cpp b/src/database.cpp index d01dcb1..246e601 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -821,7 +821,7 @@ std::vector> Database::read_set_ints(const std::string& col } std::vector> Database::read_set_doubles(const std::string& collection, - const std::string& attribute) { + const std::string& attribute) { auto set_table = find_set_table(impl_->schema.get(), collection, attribute); auto sql = "SELECT id, " + attribute + " FROM " + set_table + " ORDER BY id"; auto result = execute(sql); @@ -849,7 +849,7 @@ std::vector> Database::read_set_doubles(const std::string& c } std::vector> Database::read_set_strings(const std::string& collection, - const std::string& attribute) { + const std::string& attribute) { auto set_table = find_set_table(impl_->schema.get(), collection, attribute); auto sql = "SELECT id, " + attribute + " FROM " + set_table + " ORDER BY id"; auto result = execute(sql); From c2966e6004cd7d5c4c4fa504f524ce65d78e1f44 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 20:27:23 -0300 Subject: [PATCH 16/19] update --- include/psr/schema.h | 4 ++++ src/schema.cpp | 46 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/include/psr/schema.h b/include/psr/schema.h index 9a81793..046343a 100644 --- a/include/psr/schema.h +++ b/include/psr/schema.h @@ -68,6 +68,10 @@ class PSR_API Schema { bool is_time_series_table(const std::string& table) const; std::string get_parent_collection(const std::string& table) const; + // Find table for attribute (throws if not found) + std::string find_vector_table(const std::string& collection, const std::string& attribute) const; + std::string find_set_table(const std::string& collection, const std::string& attribute) const; + // All tables/collections std::vector table_names() const; std::vector collection_names() const; diff --git a/src/schema.cpp b/src/schema.cpp index 2cf34a4..617e021 100644 --- a/src/schema.cpp +++ b/src/schema.cpp @@ -96,6 +96,52 @@ std::string Schema::get_parent_collection(const std::string& table) const { return ""; } +std::string Schema::find_vector_table(const std::string& collection, const std::string& attribute) const { + // First try: Collection_vector_attribute + auto vt = vector_table_name(collection, attribute); + if (has_table(vt)) { + return vt; + } + + // Second try: search all vector tables for the collection + for (const auto& table_name : table_names()) { + if (!is_vector_table(table_name)) + continue; + if (get_parent_collection(table_name) != collection) + continue; + + const auto* table_def = get_table(table_name); + if (table_def && table_def->has_column(attribute)) { + return table_name; + } + } + + throw std::runtime_error("Vector attribute '" + attribute + "' not found for collection '" + collection + "'"); +} + +std::string Schema::find_set_table(const std::string& collection, const std::string& attribute) const { + // First try: Collection_set_attribute + auto st = set_table_name(collection, attribute); + if (has_table(st)) { + return st; + } + + // Second try: search all set tables for the collection + for (const auto& table_name : table_names()) { + if (!is_set_table(table_name)) + continue; + if (get_parent_collection(table_name) != collection) + continue; + + const auto* table_def = get_table(table_name); + if (table_def && table_def->has_column(attribute)) { + return table_name; + } + } + + throw std::runtime_error("Set attribute '" + attribute + "' not found for collection '" + collection + "'"); +} + std::vector Schema::table_names() const { std::vector names; names.reserve(tables_.size()); From 6eb89fd9c5e482af562b62dd01b21ef79da0a136 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 20:27:52 -0300 Subject: [PATCH 17/19] update --- src/database.cpp | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/src/database.cpp b/src/database.cpp index 246e601..6a8a5c7 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -660,34 +660,9 @@ std::vector Database::read_scalar_strings(const std::string& collec return values; } -// Helper to find vector table for an attribute -static std::string -find_vector_table(const Schema* schema, const std::string& collection, const std::string& attribute) { - // First try: Collection_vector_attribute - auto vector_table = Schema::vector_table_name(collection, attribute); - if (schema->has_table(vector_table)) { - return vector_table; - } - - // Second try: search all vector tables for the collection - for (const auto& table_name : schema->table_names()) { - if (!schema->is_vector_table(table_name)) - continue; - if (schema->get_parent_collection(table_name) != collection) - continue; - - const auto* table_def = schema->get_table(table_name); - if (table_def && table_def->has_column(attribute)) { - return table_name; - } - } - - throw std::runtime_error("Vector attribute '" + attribute + "' not found for collection '" + collection + "'"); -} - std::vector> Database::read_vector_ints(const std::string& collection, const std::string& attribute) { - auto vector_table = find_vector_table(impl_->schema.get(), collection, attribute); + auto vector_table = impl_->schema->find_vector_table(collection, attribute); auto sql = "SELECT id, " + attribute + " FROM " + vector_table + " ORDER BY id, vector_index"; auto result = execute(sql); @@ -715,7 +690,7 @@ std::vector> Database::read_vector_ints(const std::string& std::vector> Database::read_vector_doubles(const std::string& collection, const std::string& attribute) { - auto vector_table = find_vector_table(impl_->schema.get(), collection, attribute); + auto vector_table = impl_->schema->find_vector_table(collection, attribute); auto sql = "SELECT id, " + attribute + " FROM " + vector_table + " ORDER BY id, vector_index"; auto result = execute(sql); @@ -743,7 +718,7 @@ std::vector> Database::read_vector_doubles(const std::string std::vector> Database::read_vector_strings(const std::string& collection, const std::string& attribute) { - auto vector_table = find_vector_table(impl_->schema.get(), collection, attribute); + auto vector_table = impl_->schema->find_vector_table(collection, attribute); auto sql = "SELECT id, " + attribute + " FROM " + vector_table + " ORDER BY id, vector_index"; auto result = execute(sql); From e2d78f66987b7845bd67336f2dd1ca42f1a05f06 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 20:28:28 -0300 Subject: [PATCH 18/19] update --- src/database.cpp | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/database.cpp b/src/database.cpp index 6a8a5c7..5a3d5bc 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -744,32 +744,8 @@ std::vector> Database::read_vector_strings(const std::s return vectors; } -// Helper to find set table for an attribute -static std::string find_set_table(const Schema* schema, const std::string& collection, const std::string& attribute) { - // First try: Collection_set_attribute - auto set_table = Schema::set_table_name(collection, attribute); - if (schema->has_table(set_table)) { - return set_table; - } - - // Second try: search all set tables for the collection - for (const auto& table_name : schema->table_names()) { - if (!schema->is_set_table(table_name)) - continue; - if (schema->get_parent_collection(table_name) != collection) - continue; - - const auto* table_def = schema->get_table(table_name); - if (table_def && table_def->has_column(attribute)) { - return table_name; - } - } - - throw std::runtime_error("Set attribute '" + attribute + "' not found for collection '" + collection + "'"); -} - std::vector> Database::read_set_ints(const std::string& collection, const std::string& attribute) { - auto set_table = find_set_table(impl_->schema.get(), collection, attribute); + auto set_table = impl_->schema->find_set_table(collection, attribute); auto sql = "SELECT id, " + attribute + " FROM " + set_table + " ORDER BY id"; auto result = execute(sql); From 4c9601f76dbbdbc8e5595a47d58820f1914a2b26 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Thu, 15 Jan 2026 20:28:56 -0300 Subject: [PATCH 19/19] update --- src/database.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database.cpp b/src/database.cpp index 5a3d5bc..53cefde 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -773,7 +773,7 @@ std::vector> Database::read_set_ints(const std::string& col std::vector> Database::read_set_doubles(const std::string& collection, const std::string& attribute) { - auto set_table = find_set_table(impl_->schema.get(), collection, attribute); + auto set_table = impl_->schema->find_set_table(collection, attribute); auto sql = "SELECT id, " + attribute + " FROM " + set_table + " ORDER BY id"; auto result = execute(sql); @@ -801,7 +801,7 @@ std::vector> Database::read_set_doubles(const std::string& c std::vector> Database::read_set_strings(const std::string& collection, const std::string& attribute) { - auto set_table = find_set_table(impl_->schema.get(), collection, attribute); + auto set_table = impl_->schema->find_set_table(collection, attribute); auto sql = "SELECT id, " + attribute + " FROM " + set_table + " ORDER BY id"; auto result = execute(sql);