From f4802944bd54c61fdd04261e42963667de54b28e Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Fri, 16 Jan 2026 00:22:17 -0300 Subject: [PATCH 01/12] update --- include/psr/c/database.h | 7 +++++++ include/psr/database.h | 6 ++++++ src/c_api_database.cpp | 16 +++++++++++++++ src/database.cpp | 43 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/include/psr/c/database.h b/include/psr/c/database.h index 1e4773b..81c6f08 100644 --- a/include/psr/c/database.h +++ b/include/psr/c/database.h @@ -45,6 +45,13 @@ PSR_C_API int64_t psr_database_current_version(psr_database_t* db); typedef struct psr_element psr_element_t; PSR_C_API int64_t psr_database_create_element(psr_database_t* db, const char* collection, psr_element_t* element); +// Relation operations +PSR_C_API psr_error_t psr_database_set_scalar_relation(psr_database_t* db, + const char* collection, + const char* attribute, + const char* from_label, + const char* to_label); + // Read scalar attributes PSR_C_API psr_error_t psr_database_read_scalar_integers(psr_database_t* db, const char* collection, diff --git a/include/psr/database.h b/include/psr/database.h index b7ccbc5..27061da 100644 --- a/include/psr/database.h +++ b/include/psr/database.h @@ -46,6 +46,12 @@ class PSR_API Database { // Element operations int64_t create_element(const std::string& collection, const Element& element); + // Relation operations + void set_scalar_relation(const std::string& collection, + const std::string& attribute, + const std::string& from_label, + const std::string& to_label); + // Read scalar attributes std::vector read_scalar_integers(const std::string& collection, const std::string& attribute); std::vector read_scalar_doubles(const std::string& collection, const std::string& attribute); diff --git a/src/c_api_database.cpp b/src/c_api_database.cpp index 0696413..c8b33b1 100644 --- a/src/c_api_database.cpp +++ b/src/c_api_database.cpp @@ -177,6 +177,22 @@ PSR_C_API int64_t psr_database_create_element(psr_database_t* db, const char* co } } +PSR_C_API psr_error_t psr_database_set_scalar_relation(psr_database_t* db, + const char* collection, + const char* attribute, + const char* from_label, + const char* to_label) { + if (!db || !collection || !attribute || !from_label || !to_label) { + return PSR_ERROR_INVALID_ARGUMENT; + } + try { + db->db.set_scalar_relation(collection, attribute, from_label, to_label); + return PSR_OK; + } catch (const std::exception&) { + return PSR_ERROR_DATABASE; + } +} + PSR_C_API psr_database_t* psr_database_from_schema(const char* db_path, const char* schema_path, const psr_database_options_t* options) { if (!db_path || !schema_path) { diff --git a/src/database.cpp b/src/database.cpp index ca44949..befcc35 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -615,6 +615,49 @@ int64_t Database::create_element(const std::string& collection, const Element& e return element_id; } +void Database::set_scalar_relation(const std::string& collection, + const std::string& attribute, + const std::string& from_label, + const std::string& to_label) { + impl_->logger->debug("Setting relation {}.{} from '{}' to '{}'", collection, attribute, from_label, to_label); + + if (!impl_->schema) { + throw std::runtime_error("Cannot set relation: no schema loaded"); + } + + const auto* table_def = impl_->schema->get_table(collection); + if (!table_def) { + throw std::runtime_error("Collection not found in schema: " + collection); + } + + // Find the foreign key with the given attribute name + std::string to_table; + for (const auto& fk : table_def->foreign_keys) { + if (fk.from_column == attribute) { + to_table = fk.to_table; + break; + } + } + + if (to_table.empty()) { + throw std::runtime_error("Attribute '" + attribute + "' is not a foreign key in collection '" + collection + "'"); + } + + // Look up the target ID by label + auto lookup_sql = "SELECT id FROM " + to_table + " WHERE label = ?"; + auto lookup_result = execute(lookup_sql, {to_label}); + if (lookup_result.empty() || !lookup_result[0].get_int(0)) { + throw std::runtime_error("Target element with label '" + to_label + "' not found in '" + to_table + "'"); + } + auto to_id = lookup_result[0].get_int(0).value(); + + // Update the source element + auto update_sql = "UPDATE " + collection + " SET " + attribute + " = ? WHERE label = ?"; + execute(update_sql, {to_id, from_label}); + + impl_->logger->info("Set relation {}.{} for '{}' to '{}' (id: {})", collection, attribute, from_label, to_label, to_id); +} + std::vector Database::read_scalar_integers(const std::string& collection, const std::string& attribute) { auto sql = "SELECT " + attribute + " FROM " + collection; auto result = execute(sql); From 46e150d6a12694b156bd6b3b52843e5690a1e5c2 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Fri, 16 Jan 2026 00:22:47 -0300 Subject: [PATCH 02/12] update --- bindings/julia/src/c_api.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bindings/julia/src/c_api.jl b/bindings/julia/src/c_api.jl index 8f6735b..9108810 100644 --- a/bindings/julia/src/c_api.jl +++ b/bindings/julia/src/c_api.jl @@ -102,6 +102,10 @@ function psr_database_create_element(db, collection, element) @ccall libpsr_database_c.psr_database_create_element(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, element::Ptr{psr_element_t})::Int64 end +function psr_database_set_scalar_relation(db, collection, attribute, from_label, to_label) + @ccall libpsr_database_c.psr_database_set_scalar_relation(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, from_label::Ptr{Cchar}, to_label::Ptr{Cchar})::psr_error_t +end + function psr_database_read_scalar_integers(db, collection, attribute, out_values, out_count) @ccall libpsr_database_c.psr_database_read_scalar_integers(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, out_values::Ptr{Ptr{Int64}}, out_count::Ptr{Csize_t})::psr_error_t end From def3be15d30c793a9fb9ccf8f551f627bc781ae1 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Fri, 16 Jan 2026 00:25:47 -0300 Subject: [PATCH 03/12] update --- bindings/dart/lib/src/database.dart | 23 +++++++++++++++++ bindings/dart/lib/src/ffi/bindings.dart | 33 +++++++++++++++++++++++++ bindings/julia/src/database.jl | 9 +++++++ 3 files changed, 65 insertions(+) diff --git a/bindings/dart/lib/src/database.dart b/bindings/dart/lib/src/database.dart index ee86325..58a70ba 100644 --- a/bindings/dart/lib/src/database.dart +++ b/bindings/dart/lib/src/database.dart @@ -83,6 +83,29 @@ class Database { } } + /// Sets a scalar relation (foreign key) between two elements by their labels. + void setScalarRelation(String collection, String attribute, + String fromLabel, String toLabel) { + _ensureNotClosed(); + + final arena = Arena(); + try { + final err = bindings.psr_database_set_scalar_relation( + _ptr, + collection.toNativeUtf8(allocator: arena).cast(), + attribute.toNativeUtf8(allocator: arena).cast(), + fromLabel.toNativeUtf8(allocator: arena).cast(), + toLabel.toNativeUtf8(allocator: arena).cast(), + ); + + if (err != psr_error_t.PSR_OK) { + throw DatabaseException.fromError(err, "Failed to set scalar relation '$attribute' in '$collection'"); + } + } finally { + arena.releaseAll(); + } + } + /// Reads all integer values for a scalar attribute from a collection. List readScalarIntegers(String collection, String attribute) { _ensureNotClosed(); diff --git a/bindings/dart/lib/src/ffi/bindings.dart b/bindings/dart/lib/src/ffi/bindings.dart index 016edd0..10fb407 100644 --- a/bindings/dart/lib/src/ffi/bindings.dart +++ b/bindings/dart/lib/src/ffi/bindings.dart @@ -197,6 +197,39 @@ class PsrDatabaseBindings { int Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>(); + int psr_database_set_scalar_relation( + ffi.Pointer db, + ffi.Pointer collection, + ffi.Pointer attribute, + ffi.Pointer from_label, + ffi.Pointer to_label, + ) { + return _psr_database_set_scalar_relation( + db, + collection, + attribute, + from_label, + to_label, + ); + } + + late final _psr_database_set_scalar_relationPtr = _lookup< + ffi.NativeFunction< + ffi.Int32 Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('psr_database_set_scalar_relation'); + late final _psr_database_set_scalar_relation = + _psr_database_set_scalar_relationPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>(); + int psr_database_read_scalar_integers( ffi.Pointer db, ffi.Pointer collection, diff --git a/bindings/julia/src/database.jl b/bindings/julia/src/database.jl index abfc666..ecfb916 100644 --- a/bindings/julia/src/database.jl +++ b/bindings/julia/src/database.jl @@ -41,6 +41,15 @@ function close!(db::Database) return nothing end +function set_scalar_relation!(db::Database, collection::String, attribute::String, + from_label::String, to_label::String) + err = C.psr_database_set_scalar_relation(db.ptr, collection, attribute, from_label, to_label) + if err != C.PSR_OK + throw(DatabaseException("Failed to set scalar relation '$attribute' in '$collection'")) + end + return nothing +end + function read_scalar_integers(db::Database, collection::String, attribute::String) out_values = Ref{Ptr{Int64}}(C_NULL) out_count = Ref{Csize_t}(0) From 534bb95be8b0a9de42adf313f0bf128874080206 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Fri, 16 Jan 2026 00:28:53 -0300 Subject: [PATCH 04/12] update --- tests/test_database.cpp | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_database.cpp b/tests/test_database.cpp index 2971b9c..0d4b4cd 100644 --- a/tests/test_database.cpp +++ b/tests/test_database.cpp @@ -450,3 +450,52 @@ TEST_F(DatabaseFixture, ReadSetOnlyReturnsElementsWithData) { auto sets = db.read_set_strings("Collection", "tag"); EXPECT_EQ(sets.size(), 2); } + +// ============================================================================ +// set_scalar_relation tests +// ============================================================================ + +TEST_F(DatabaseFixture, SetScalarRelation) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/relations.sql"), {.console_level = psr::LogLevel::off}); + + // Create parent + psr::Element parent; + parent.set("label", std::string("Parent 1")); + db.create_element("Parent", parent); + + // Create child without relation + psr::Element child; + child.set("label", std::string("Child 1")); + db.create_element("Child", child); + + // Set the relation + db.set_scalar_relation("Child", "parent_id", "Child 1", "Parent 1"); + + // Verify the relation was set + auto result = db.execute("SELECT parent_id FROM Child WHERE label = ?", {std::string("Child 1")}); + EXPECT_EQ(result.row_count(), 1); + EXPECT_EQ(result[0].get_int(0).value(), 1); // Parent 1 has id = 1 +} + +TEST_F(DatabaseFixture, SetScalarRelationSelfReference) { + auto db = psr::Database::from_schema( + ":memory:", schema_path("schemas/valid/relations.sql"), {.console_level = psr::LogLevel::off}); + + // Create two children + psr::Element child1; + child1.set("label", std::string("Child 1")); + db.create_element("Child", child1); + + psr::Element child2; + child2.set("label", std::string("Child 2")); + db.create_element("Child", child2); + + // Set self-referential relation (sibling) + db.set_scalar_relation("Child", "sibling_id", "Child 1", "Child 2"); + + // Verify the relation was set + auto result = db.execute("SELECT sibling_id FROM Child WHERE label = ?", {std::string("Child 1")}); + EXPECT_EQ(result.row_count(), 1); + EXPECT_EQ(result[0].get_int(0).value(), 2); // Child 2 has id = 2 +} From aed3755ebeb01687ccd723d0296b1c777d7529de Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Fri, 16 Jan 2026 00:31:09 -0300 Subject: [PATCH 05/12] update --- include/psr/c/database.h | 8 ++++---- src/c_api_database.cpp | 8 ++++---- src/database.cpp | 6 ++++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/include/psr/c/database.h b/include/psr/c/database.h index 81c6f08..d9de74d 100644 --- a/include/psr/c/database.h +++ b/include/psr/c/database.h @@ -47,10 +47,10 @@ PSR_C_API int64_t psr_database_create_element(psr_database_t* db, const char* co // Relation operations PSR_C_API psr_error_t psr_database_set_scalar_relation(psr_database_t* db, - const char* collection, - const char* attribute, - const char* from_label, - const char* to_label); + const char* collection, + const char* attribute, + const char* from_label, + const char* to_label); // Read scalar attributes PSR_C_API psr_error_t psr_database_read_scalar_integers(psr_database_t* db, diff --git a/src/c_api_database.cpp b/src/c_api_database.cpp index c8b33b1..35882f6 100644 --- a/src/c_api_database.cpp +++ b/src/c_api_database.cpp @@ -178,10 +178,10 @@ PSR_C_API int64_t psr_database_create_element(psr_database_t* db, const char* co } PSR_C_API psr_error_t psr_database_set_scalar_relation(psr_database_t* db, - const char* collection, - const char* attribute, - const char* from_label, - const char* to_label) { + const char* collection, + const char* attribute, + const char* from_label, + const char* to_label) { if (!db || !collection || !attribute || !from_label || !to_label) { return PSR_ERROR_INVALID_ARGUMENT; } diff --git a/src/database.cpp b/src/database.cpp index befcc35..ad596f3 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -640,7 +640,8 @@ void Database::set_scalar_relation(const std::string& collection, } if (to_table.empty()) { - throw std::runtime_error("Attribute '" + attribute + "' is not a foreign key in collection '" + collection + "'"); + throw std::runtime_error("Attribute '" + attribute + "' is not a foreign key in collection '" + collection + + "'"); } // Look up the target ID by label @@ -655,7 +656,8 @@ void Database::set_scalar_relation(const std::string& collection, auto update_sql = "UPDATE " + collection + " SET " + attribute + " = ? WHERE label = ?"; execute(update_sql, {to_id, from_label}); - impl_->logger->info("Set relation {}.{} for '{}' to '{}' (id: {})", collection, attribute, from_label, to_label, to_id); + impl_->logger->info( + "Set relation {}.{} for '{}' to '{}' (id: {})", collection, attribute, from_label, to_label, to_id); } std::vector Database::read_scalar_integers(const std::string& collection, const std::string& attribute) { From d7ff9615edaf5aa205b7d698686fe620d5a5c200 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Fri, 16 Jan 2026 00:38:32 -0300 Subject: [PATCH 06/12] update --- bindings/julia/src/c_api.jl | 4 ++++ bindings/julia/src/database.jl | 28 ++++++++++++++++++++++++++ include/psr/c/database.h | 6 ++++++ include/psr/database.h | 3 +++ src/c_api_database.cpp | 27 +++++++++++++++++++++++++ src/database.cpp | 36 ++++++++++++++++++++++++++++++++++ 6 files changed, 104 insertions(+) diff --git a/bindings/julia/src/c_api.jl b/bindings/julia/src/c_api.jl index 9108810..f37eeeb 100644 --- a/bindings/julia/src/c_api.jl +++ b/bindings/julia/src/c_api.jl @@ -106,6 +106,10 @@ function psr_database_set_scalar_relation(db, collection, attribute, from_label, @ccall libpsr_database_c.psr_database_set_scalar_relation(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, from_label::Ptr{Cchar}, to_label::Ptr{Cchar})::psr_error_t end +function psr_database_read_scalar_relation(db, collection, attribute, out_values, out_count) + @ccall libpsr_database_c.psr_database_read_scalar_relation(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_scalar_integers(db, collection, attribute, out_values, out_count) @ccall libpsr_database_c.psr_database_read_scalar_integers(db::Ptr{psr_database_t}, collection::Ptr{Cchar}, attribute::Ptr{Cchar}, out_values::Ptr{Ptr{Int64}}, out_count::Ptr{Csize_t})::psr_error_t end diff --git a/bindings/julia/src/database.jl b/bindings/julia/src/database.jl index ecfb916..df210c7 100644 --- a/bindings/julia/src/database.jl +++ b/bindings/julia/src/database.jl @@ -50,6 +50,34 @@ function set_scalar_relation!(db::Database, collection::String, attribute::Strin return nothing end +function read_scalar_relation(db::Database, collection::String, attribute::String) + out_values = Ref{Ptr{Ptr{Cchar}}}(C_NULL) + out_count = Ref{Csize_t}(0) + + err = C.psr_database_read_scalar_relation(db.ptr, collection, attribute, out_values, out_count) + if err != C.PSR_OK + throw(DatabaseException("Failed to read scalar relation '$attribute' from '$collection'")) + end + + count = out_count[] + if count == 0 || out_values[] == C_NULL + return Union{String, Nothing}[] + end + + ptrs = unsafe_wrap(Array, out_values[], count) + result = Union{String, Nothing}[] + for ptr in ptrs + if ptr == C_NULL + push!(result, nothing) + else + s = unsafe_string(ptr) + push!(result, isempty(s) ? nothing : s) + end + end + C.psr_free_string_array(out_values[], count) + return result +end + function read_scalar_integers(db::Database, collection::String, attribute::String) out_values = Ref{Ptr{Int64}}(C_NULL) out_count = Ref{Csize_t}(0) diff --git a/include/psr/c/database.h b/include/psr/c/database.h index d9de74d..e230d15 100644 --- a/include/psr/c/database.h +++ b/include/psr/c/database.h @@ -52,6 +52,12 @@ PSR_C_API psr_error_t psr_database_set_scalar_relation(psr_database_t* db, const char* from_label, const char* to_label); +PSR_C_API psr_error_t psr_database_read_scalar_relation(psr_database_t* db, + const char* collection, + const char* attribute, + char*** out_values, + size_t* out_count); + // Read scalar attributes PSR_C_API psr_error_t psr_database_read_scalar_integers(psr_database_t* db, const char* collection, diff --git a/include/psr/database.h b/include/psr/database.h index 27061da..cf0defa 100644 --- a/include/psr/database.h +++ b/include/psr/database.h @@ -52,6 +52,9 @@ class PSR_API Database { const std::string& from_label, const std::string& to_label); + std::vector read_scalar_relation(const std::string& collection, + const std::string& attribute); + // Read scalar attributes std::vector read_scalar_integers(const std::string& collection, const std::string& attribute); std::vector read_scalar_doubles(const std::string& collection, const std::string& attribute); diff --git a/src/c_api_database.cpp b/src/c_api_database.cpp index 35882f6..4e50f25 100644 --- a/src/c_api_database.cpp +++ b/src/c_api_database.cpp @@ -193,6 +193,33 @@ PSR_C_API psr_error_t psr_database_set_scalar_relation(psr_database_t* db, } } +PSR_C_API psr_error_t psr_database_read_scalar_relation(psr_database_t* db, + const char* collection, + const char* attribute, + char*** out_values, + size_t* out_count) { + if (!db || !collection || !attribute || !out_values || !out_count) { + return PSR_ERROR_INVALID_ARGUMENT; + } + try { + auto values = db->db.read_scalar_relation(collection, attribute); + *out_count = values.size(); + if (values.empty()) { + *out_values = nullptr; + return PSR_OK; + } + *out_values = new char*[values.size()]; + for (size_t i = 0; i < values.size(); ++i) { + (*out_values)[i] = new char[values[i].size() + 1]; + std::copy(values[i].begin(), values[i].end(), (*out_values)[i]); + (*out_values)[i][values[i].size()] = '\0'; + } + return PSR_OK; + } catch (const std::exception&) { + return PSR_ERROR_DATABASE; + } +} + PSR_C_API psr_database_t* psr_database_from_schema(const char* db_path, const char* schema_path, const psr_database_options_t* options) { if (!db_path || !schema_path) { diff --git a/src/database.cpp b/src/database.cpp index ad596f3..3755fe6 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -660,6 +660,42 @@ void Database::set_scalar_relation(const std::string& collection, "Set relation {}.{} for '{}' to '{}' (id: {})", collection, attribute, from_label, to_label, to_id); } +std::vector Database::read_scalar_relation(const std::string& collection, const std::string& attribute) { + if (!impl_->schema) { + throw std::runtime_error("Cannot read relation: no schema loaded"); + } + + const auto* table_def = impl_->schema->get_table(collection); + if (!table_def) { + throw std::runtime_error("Collection not found in schema: " + collection); + } + + // Find the foreign key with the given attribute name + std::string to_table; + for (const auto& fk : table_def->foreign_keys) { + if (fk.from_column == attribute) { + to_table = fk.to_table; + break; + } + } + + if (to_table.empty()) { + throw std::runtime_error("Attribute '" + attribute + "' is not a foreign key in collection '" + collection + "'"); + } + + // LEFT JOIN to get target labels (NULL for unset relations) + auto sql = "SELECT t.label FROM " + collection + " c LEFT JOIN " + to_table + " t ON c." + attribute + " = t.id"; + auto result = execute(sql); + + std::vector labels; + labels.reserve(result.row_count()); + for (size_t i = 0; i < result.row_count(); ++i) { + auto val = result[i].get_string(0); + labels.push_back(val.value_or("")); + } + return labels; +} + std::vector Database::read_scalar_integers(const std::string& collection, const std::string& attribute) { auto sql = "SELECT " + attribute + " FROM " + collection; auto result = execute(sql); From ddc7d4674e028fa6b102013f8ca6d789cd073039 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Fri, 16 Jan 2026 00:41:05 -0300 Subject: [PATCH 07/12] update --- bindings/julia/test/test_read.jl | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/bindings/julia/test/test_read.jl b/bindings/julia/test/test_read.jl index 4903def..3042990 100644 --- a/bindings/julia/test/test_read.jl +++ b/bindings/julia/test/test_read.jl @@ -198,4 +198,68 @@ end PSRDatabase.close!(db) end +@testset "Set and Read Scalar Relations" begin + path_schema = joinpath(tests_path(), "schemas", "valid", "relations.sql") + db = PSRDatabase.from_schema(":memory:", path_schema; force = true) + + PSRDatabase.create_element!(db, "Configuration"; label = "Test Config") + + # Create parents + PSRDatabase.create_element!(db, "Parent"; label = "Parent 1") + PSRDatabase.create_element!(db, "Parent"; label = "Parent 2") + + # Create children + PSRDatabase.create_element!(db, "Child"; label = "Child 1") + PSRDatabase.create_element!(db, "Child"; label = "Child 2") + PSRDatabase.create_element!(db, "Child"; label = "Child 3") + + # Set relations + PSRDatabase.set_scalar_relation!(db, "Child", "parent_id", "Child 1", "Parent 1") + PSRDatabase.set_scalar_relation!(db, "Child", "parent_id", "Child 3", "Parent 2") + + # Read relations + labels = PSRDatabase.read_scalar_relation(db, "Child", "parent_id") + @test length(labels) == 3 + @test labels[1] == "Parent 1" + @test labels[2] === nothing # Child 2 has no parent + @test labels[3] == "Parent 2" + + PSRDatabase.close!(db) +end + +@testset "Read Scalar Relations Self-Reference" begin + path_schema = joinpath(tests_path(), "schemas", "valid", "relations.sql") + db = PSRDatabase.from_schema(":memory:", path_schema; force = true) + + PSRDatabase.create_element!(db, "Configuration"; label = "Test Config") + + # Create children (Child references itself via sibling_id) + PSRDatabase.create_element!(db, "Child"; label = "Child 1") + PSRDatabase.create_element!(db, "Child"; label = "Child 2") + + # Set sibling relation (self-reference) + PSRDatabase.set_scalar_relation!(db, "Child", "sibling_id", "Child 1", "Child 2") + + # Read sibling relations + labels = PSRDatabase.read_scalar_relation(db, "Child", "sibling_id") + @test length(labels) == 2 + @test labels[1] == "Child 2" # Child 1's sibling is Child 2 + @test labels[2] === nothing # Child 2 has no sibling set + + PSRDatabase.close!(db) +end + +@testset "Read Scalar Relations Empty Result" begin + path_schema = joinpath(tests_path(), "schemas", "valid", "relations.sql") + db = PSRDatabase.from_schema(":memory:", path_schema; force = true) + + PSRDatabase.create_element!(db, "Configuration"; label = "Test Config") + + # No Child elements created + labels = PSRDatabase.read_scalar_relation(db, "Child", "parent_id") + @test labels == Union{String, Nothing}[] + + PSRDatabase.close!(db) +end + end From c4d613b8d393b75e3563265a7c7f1f5164f6fd6b Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Fri, 16 Jan 2026 00:42:15 -0300 Subject: [PATCH 08/12] update --- bindings/dart/lib/src/database.dart | 44 +++++++++++++++++++++++++ bindings/dart/lib/src/ffi/bindings.dart | 33 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/bindings/dart/lib/src/database.dart b/bindings/dart/lib/src/database.dart index 58a70ba..cc51138 100644 --- a/bindings/dart/lib/src/database.dart +++ b/bindings/dart/lib/src/database.dart @@ -106,6 +106,50 @@ class Database { } } + /// Reads scalar relation values (target labels) for a FK attribute. + /// Returns null for elements with no relation set. + List readScalarRelation(String collection, String attribute) { + _ensureNotClosed(); + + final arena = Arena(); + try { + final outValues = arena>>(); + final outCount = arena(); + + final err = bindings.psr_database_read_scalar_relation( + _ptr, + collection.toNativeUtf8(allocator: arena).cast(), + attribute.toNativeUtf8(allocator: arena).cast(), + outValues, + outCount, + ); + + if (err != psr_error_t.PSR_OK) { + throw DatabaseException.fromError(err, "Failed to read scalar relation '$attribute' from '$collection'"); + } + + final count = outCount.value; + if (count == 0 || outValues.value == nullptr) { + return []; + } + + final result = []; + for (var i = 0; i < count; i++) { + final ptr = outValues.value[i]; + if (ptr == nullptr) { + result.add(null); + } else { + final s = ptr.cast().toDartString(); + result.add(s.isEmpty ? null : s); + } + } + bindings.psr_free_string_array(outValues.value, count); + return result; + } finally { + arena.releaseAll(); + } + } + /// Reads all integer values for a scalar attribute from a collection. List readScalarIntegers(String collection, String attribute) { _ensureNotClosed(); diff --git a/bindings/dart/lib/src/ffi/bindings.dart b/bindings/dart/lib/src/ffi/bindings.dart index 10fb407..5b483d6 100644 --- a/bindings/dart/lib/src/ffi/bindings.dart +++ b/bindings/dart/lib/src/ffi/bindings.dart @@ -230,6 +230,39 @@ class PsrDatabaseBindings { ffi.Pointer, ffi.Pointer)>(); + int psr_database_read_scalar_relation( + ffi.Pointer db, + ffi.Pointer collection, + ffi.Pointer attribute, + ffi.Pointer>> out_values, + ffi.Pointer out_count, + ) { + return _psr_database_read_scalar_relation( + db, + collection, + attribute, + out_values, + out_count, + ); + } + + late final _psr_database_read_scalar_relationPtr = _lookup< + ffi.NativeFunction< + ffi.Int32 Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>, + ffi.Pointer)>>('psr_database_read_scalar_relation'); + late final _psr_database_read_scalar_relation = + _psr_database_read_scalar_relationPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>>, + ffi.Pointer)>(); + int psr_database_read_scalar_integers( ffi.Pointer db, ffi.Pointer collection, From a0967dd25fdad9fa73dfb1f7d00c2f2e69028bab Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Fri, 16 Jan 2026 00:43:04 -0300 Subject: [PATCH 09/12] update --- bindings/dart/test/read_test.dart | 75 +++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/bindings/dart/test/read_test.dart b/bindings/dart/test/read_test.dart index a1c91ea..64be0a7 100644 --- a/bindings/dart/test/read_test.dart +++ b/bindings/dart/test/read_test.dart @@ -324,4 +324,79 @@ void main() { } }); }); + + group('Scalar Relations', () { + test('set and read scalar relation', () { + final db = Database.fromSchema( + ':memory:', + path.join(testsPath, 'schemas', 'valid', 'relations.sql'), + ); + try { + db.createElement('Configuration', {'label': 'Test Config'}); + + // Create parents + db.createElement('Parent', {'label': 'Parent 1'}); + db.createElement('Parent', {'label': 'Parent 2'}); + + // Create children + db.createElement('Child', {'label': 'Child 1'}); + db.createElement('Child', {'label': 'Child 2'}); + db.createElement('Child', {'label': 'Child 3'}); + + // Set relations + db.setScalarRelation('Child', 'parent_id', 'Child 1', 'Parent 1'); + db.setScalarRelation('Child', 'parent_id', 'Child 3', 'Parent 2'); + + // Read relations + final labels = db.readScalarRelation('Child', 'parent_id'); + expect(labels.length, equals(3)); + expect(labels[0], equals('Parent 1')); + expect(labels[1], isNull); // Child 2 has no parent + expect(labels[2], equals('Parent 2')); + } finally { + db.close(); + } + }); + + test('read scalar relation self-reference', () { + final db = Database.fromSchema( + ':memory:', + path.join(testsPath, 'schemas', 'valid', 'relations.sql'), + ); + try { + db.createElement('Configuration', {'label': 'Test Config'}); + + // Create children (Child references itself via sibling_id) + db.createElement('Child', {'label': 'Child 1'}); + db.createElement('Child', {'label': 'Child 2'}); + + // Set sibling relation (self-reference) + db.setScalarRelation('Child', 'sibling_id', 'Child 1', 'Child 2'); + + // Read sibling relations + final labels = db.readScalarRelation('Child', 'sibling_id'); + expect(labels.length, equals(2)); + expect(labels[0], equals('Child 2')); // Child 1's sibling is Child 2 + expect(labels[1], isNull); // Child 2 has no sibling set + } finally { + db.close(); + } + }); + + test('read scalar relation empty result', () { + final db = Database.fromSchema( + ':memory:', + path.join(testsPath, 'schemas', 'valid', 'relations.sql'), + ); + try { + db.createElement('Configuration', {'label': 'Test Config'}); + + // No Child elements created + final labels = db.readScalarRelation('Child', 'parent_id'); + expect(labels, isEmpty); + } finally { + db.close(); + } + }); + }); } From 366a9907a863b53dc5f4a109df0a3f22f57214b1 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Fri, 16 Jan 2026 00:45:33 -0300 Subject: [PATCH 10/12] update --- include/psr/c/database.h | 8 ++++---- include/psr/database.h | 3 +-- src/c_api_database.cpp | 8 ++++---- src/database.cpp | 3 ++- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/include/psr/c/database.h b/include/psr/c/database.h index e230d15..a5c1c2d 100644 --- a/include/psr/c/database.h +++ b/include/psr/c/database.h @@ -53,10 +53,10 @@ PSR_C_API psr_error_t psr_database_set_scalar_relation(psr_database_t* db, const char* to_label); PSR_C_API psr_error_t psr_database_read_scalar_relation(psr_database_t* db, - const char* collection, - const char* attribute, - char*** out_values, - size_t* out_count); + const char* collection, + const char* attribute, + char*** out_values, + size_t* out_count); // Read scalar attributes PSR_C_API psr_error_t psr_database_read_scalar_integers(psr_database_t* db, diff --git a/include/psr/database.h b/include/psr/database.h index cf0defa..60791e4 100644 --- a/include/psr/database.h +++ b/include/psr/database.h @@ -52,8 +52,7 @@ class PSR_API Database { const std::string& from_label, const std::string& to_label); - std::vector read_scalar_relation(const std::string& collection, - const std::string& attribute); + std::vector read_scalar_relation(const std::string& collection, const std::string& attribute); // Read scalar attributes std::vector read_scalar_integers(const std::string& collection, const std::string& attribute); diff --git a/src/c_api_database.cpp b/src/c_api_database.cpp index 4e50f25..05acc0d 100644 --- a/src/c_api_database.cpp +++ b/src/c_api_database.cpp @@ -194,10 +194,10 @@ PSR_C_API psr_error_t psr_database_set_scalar_relation(psr_database_t* db, } PSR_C_API psr_error_t psr_database_read_scalar_relation(psr_database_t* db, - const char* collection, - const char* attribute, - char*** out_values, - size_t* out_count) { + const char* collection, + const char* attribute, + char*** out_values, + size_t* out_count) { if (!db || !collection || !attribute || !out_values || !out_count) { return PSR_ERROR_INVALID_ARGUMENT; } diff --git a/src/database.cpp b/src/database.cpp index 3755fe6..7eaa0ac 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -680,7 +680,8 @@ std::vector Database::read_scalar_relation(const std::string& colle } if (to_table.empty()) { - throw std::runtime_error("Attribute '" + attribute + "' is not a foreign key in collection '" + collection + "'"); + throw std::runtime_error("Attribute '" + attribute + "' is not a foreign key in collection '" + collection + + "'"); } // LEFT JOIN to get target labels (NULL for unset relations) From c19a85f49973ae6446bc11537e1c5a461498f78b Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Fri, 16 Jan 2026 00:46:32 -0300 Subject: [PATCH 11/12] update --- bindings/julia/src/database.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/julia/src/database.jl b/bindings/julia/src/database.jl index df210c7..19e4134 100644 --- a/bindings/julia/src/database.jl +++ b/bindings/julia/src/database.jl @@ -42,7 +42,7 @@ function close!(db::Database) end function set_scalar_relation!(db::Database, collection::String, attribute::String, - from_label::String, to_label::String) + from_label::String, to_label::String) err = C.psr_database_set_scalar_relation(db.ptr, collection, attribute, from_label, to_label) if err != C.PSR_OK throw(DatabaseException("Failed to set scalar relation '$attribute' in '$collection'")) From ce246d7b2afbb426157e8f732bd24b21e4dcafa6 Mon Sep 17 00:00:00 2001 From: raphasampaio Date: Fri, 16 Jan 2026 00:47:42 -0300 Subject: [PATCH 12/12] update --- bindings/julia/src/database.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bindings/julia/src/database.jl b/bindings/julia/src/database.jl index 19e4134..aac4d26 100644 --- a/bindings/julia/src/database.jl +++ b/bindings/julia/src/database.jl @@ -41,8 +41,13 @@ function close!(db::Database) return nothing end -function set_scalar_relation!(db::Database, collection::String, attribute::String, - from_label::String, to_label::String) +function set_scalar_relation!( + db::Database, + collection::String, + attribute::String, + from_label::String, + to_label::String, +) err = C.psr_database_set_scalar_relation(db.ptr, collection, attribute, from_label, to_label) if err != C.PSR_OK throw(DatabaseException("Failed to set scalar relation '$attribute' in '$collection'"))