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/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 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 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/include/psr/c/database.h b/include/psr/c/database.h index 5fc143c..a684cca 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/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/c_api_database.cpp b/src/c_api_database.cpp index 04cc0eb..0d39b25 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..53cefde 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); @@ -769,4 +744,87 @@ std::vector> Database::read_vector_strings(const std::s return vectors; } +std::vector> Database::read_set_ints(const std::string& collection, const std::string& 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); + + 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 = impl_->schema->find_set_table(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 = impl_->schema->find_set_table(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 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()); 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); +}