Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions bindings/dart/lib/src/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,73 @@ 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 scalar relation values (target labels) for a FK attribute.
/// Returns null for elements with no relation set.
List<String?> readScalarRelation(String collection, String attribute) {
_ensureNotClosed();

final arena = Arena();
try {
final outValues = arena<Pointer<Pointer<Char>>>();
final outCount = arena<Size>();

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 = <String?>[];
for (var i = 0; i < count; i++) {
final ptr = outValues.value[i];
if (ptr == nullptr) {
result.add(null);
} else {
final s = ptr.cast<Utf8>().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<int> readScalarIntegers(String collection, String attribute) {
_ensureNotClosed();
Expand Down
66 changes: 66 additions & 0 deletions bindings/dart/lib/src/ffi/bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,72 @@ class PsrDatabaseBindings {
int Function(ffi.Pointer<psr_database_t>, ffi.Pointer<ffi.Char>,
ffi.Pointer<psr_element_t>)>();

int psr_database_set_scalar_relation(
ffi.Pointer<psr_database_t> db,
ffi.Pointer<ffi.Char> collection,
ffi.Pointer<ffi.Char> attribute,
ffi.Pointer<ffi.Char> from_label,
ffi.Pointer<ffi.Char> 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<psr_database_t>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>)>>('psr_database_set_scalar_relation');
late final _psr_database_set_scalar_relation =
_psr_database_set_scalar_relationPtr.asFunction<
int Function(
ffi.Pointer<psr_database_t>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>)>();

int psr_database_read_scalar_relation(
ffi.Pointer<psr_database_t> db,
ffi.Pointer<ffi.Char> collection,
ffi.Pointer<ffi.Char> attribute,
ffi.Pointer<ffi.Pointer<ffi.Pointer<ffi.Char>>> out_values,
ffi.Pointer<ffi.Size> 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<psr_database_t>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Pointer<ffi.Pointer<ffi.Char>>>,
ffi.Pointer<ffi.Size>)>>('psr_database_read_scalar_relation');
late final _psr_database_read_scalar_relation =
_psr_database_read_scalar_relationPtr.asFunction<
int Function(
ffi.Pointer<psr_database_t>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Pointer<ffi.Pointer<ffi.Char>>>,
ffi.Pointer<ffi.Size>)>();

int psr_database_read_scalar_integers(
ffi.Pointer<psr_database_t> db,
ffi.Pointer<ffi.Char> collection,
Expand Down
75 changes: 75 additions & 0 deletions bindings/dart/test/read_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});
});
}
8 changes: 8 additions & 0 deletions bindings/julia/src/c_api.jl
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ 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_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
Expand Down
42 changes: 42 additions & 0 deletions bindings/julia/src/database.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,48 @@ 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_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)
Expand Down
64 changes: 64 additions & 0 deletions bindings/julia/test/test_read.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions include/psr/c/database.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ 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);

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,
Expand Down
8 changes: 8 additions & 0 deletions include/psr/database.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ 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);

std::vector<std::string> read_scalar_relation(const std::string& collection, const std::string& attribute);

// Read scalar attributes
std::vector<int64_t> read_scalar_integers(const std::string& collection, const std::string& attribute);
std::vector<double> read_scalar_doubles(const std::string& collection, const std::string& attribute);
Expand Down
Loading
Loading