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; 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/dart/test/read_test.dart b/bindings/dart/test/read_test.dart index 8fb07fe..61858c7 100644 --- a/bindings/dart/test/read_test.dart +++ b/bindings/dart/test/read_test.dart @@ -143,4 +143,111 @@ 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 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'), + ); + 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 + }); + 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], equals([4, 5])); + } finally { + db.close(); + } + }); + }); } 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 34c3051..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 @@ -134,6 +134,18 @@ 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_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_array(values) @ccall libpsr_database_c.psr_free_int_array(values::Ptr{Int64})::Cvoid end @@ -146,6 +158,18 @@ 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 + +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/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 diff --git a/bindings/julia/test/test_read.jl b/bindings/julia/test/test_read.jl index f3fe766..5ae0820 100644 --- a/bindings/julia/test/test_read.jl +++ b/bindings/julia/test/test_read.jl @@ -68,4 +68,70 @@ 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 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 vectors + PSRDatabase.create_element!(db, "Collection"; + label = "Item 1", + value_int = [1, 2, 3], + ) + # 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] == [4, 5] + + PSRDatabase.close!(db) +end + end diff --git a/include/psr/c/database.h b/include/psr/c/database.h index 095588b..5fc143c 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/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/include/psr/database.h b/include/psr/database.h index 29c87d7..70ef43f 100644 --- a/include/psr/database.h +++ b/include/psr/database.h @@ -55,6 +55,12 @@ 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..04cc0eb 100644 --- a/src/c_api_database.cpp +++ b/src/c_api_database.cpp @@ -38,6 +38,56 @@ 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 +263,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 +278,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; } @@ -299,4 +331,98 @@ 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 { + 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; + } +} + +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 { + 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; + } +} + +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) { + free_vectors_impl(vectors, sizes, count); +} + +PSR_C_API void psr_free_double_vectors(double** vectors, size_t* sizes, size_t count) { + free_vectors_impl(vectors, sizes, count); +} + +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..05e9fdc 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)); @@ -202,11 +202,14 @@ Result Database::execute(const std::string& sql, const std::vector& param 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)); @@ -657,4 +660,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 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})); +}