Skip to content

Commit 6755b55

Browse files
authored
fix(rustffi): complete transaction and response parity (#5102)
## Summary - route collection deletion and active-version changes through the real Rust transaction instead of applying them after commit - preserve Rust index lifecycle metadata and collection names in list responses - preserve nullable datetime array elements using the Go client’s typed optional representation - align document-add and filter mutations with the current Go integration-test API - refresh the generated C header and remove the obsolete manual mutation-event helper This is the Go integration-test adapter companion to [sourcenetwork/defradb.rs#1261](sourcenetwork/defradb.rs#1261). It targets `jack/ffi-rust-compat` because that branch currently carries the Rust FFI client. ## Verification - all 4 transactional collection-version tests from defradb.rs#1249 - all 11 index lifecycle/query tests from defradb.rs#1255 - all 3 nullable datetime and mixed-delete-ID tests from defradb.rs#1256 - focused nullable datetime conversion unit test - project `golangci-lint` configuration for `tests/clients/rustffi`: 0 issues - generated `defra.h` matches `cbindgen` output from defradb.rs#1261 - `git diff --check`
1 parent a6a2a31 commit 6755b55

4 files changed

Lines changed: 419 additions & 327 deletions

File tree

tests/clients/rustffi/defra.go

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,72 @@ func (t *Transaction) Mutate(mutation string) (*QueryResult, error) {
658658
return t.Query(mutation)
659659
}
660660

661+
// DeleteCollections deletes collections within the transaction.
662+
func (t *Transaction) DeleteCollections(identityDID string, targets []string, activeOnly bool) error {
663+
cTxnID := C.CString(t.id)
664+
defer C.free(unsafe.Pointer(cTxnID))
665+
666+
var cIdentityDID *C.char
667+
if identityDID != "" {
668+
cIdentityDID = C.CString(identityDID)
669+
defer C.free(unsafe.Pointer(cIdentityDID))
670+
}
671+
672+
targetsJSON, err := json.Marshal(targets)
673+
if err != nil {
674+
return fmt.Errorf("ffi: failed to marshal collection targets: %w", err)
675+
}
676+
cTargets := C.CString(string(targetsJSON))
677+
defer C.free(unsafe.Pointer(cTargets))
678+
679+
result := C.delete_collections_in_txn(
680+
t.node.ptr,
681+
cTxnID,
682+
cIdentityDID,
683+
cTargets,
684+
C.bool(activeOnly),
685+
)
686+
if result.status != 0 {
687+
err := C.GoString(result.error)
688+
C.defra_free_string(result.error)
689+
return mapFFIError("delete_collections_in_txn", err)
690+
}
691+
692+
C.defra_free_string(result.value)
693+
return nil
694+
}
695+
696+
// SetCollectionActive updates a collection version within the transaction.
697+
func (t *Transaction) SetCollectionActive(identityDID string, versionID string, isActive bool) error {
698+
cTxnID := C.CString(t.id)
699+
defer C.free(unsafe.Pointer(cTxnID))
700+
701+
var cIdentityDID *C.char
702+
if identityDID != "" {
703+
cIdentityDID = C.CString(identityDID)
704+
defer C.free(unsafe.Pointer(cIdentityDID))
705+
}
706+
707+
cVersionID := C.CString(versionID)
708+
defer C.free(unsafe.Pointer(cVersionID))
709+
710+
result := C.set_collection_active_in_txn(
711+
t.node.ptr,
712+
cTxnID,
713+
cIdentityDID,
714+
cVersionID,
715+
C.bool(isActive),
716+
)
717+
if result.status != 0 {
718+
err := C.GoString(result.error)
719+
C.defra_free_string(result.error)
720+
return mapFFIError("set_collection_active_in_txn", err)
721+
}
722+
723+
C.defra_free_string(result.value)
724+
return nil
725+
}
726+
661727
// ============================================================================
662728
// Collection Functions
663729
// ============================================================================
@@ -1147,6 +1213,12 @@ type IndexDescription struct {
11471213
Unique bool `json:"Unique,omitempty"`
11481214
}
11491215

1216+
type IndexResult struct {
1217+
IndexDescription
1218+
CollectionName string `json:"CollectionName"`
1219+
Execution client.ActionExecution `json:"Execution"`
1220+
}
1221+
11501222
// CreateIndex creates a new index on a collection.
11511223
// Returns the created index description with assigned ID.
11521224
func (n *Node) CreateIndex(identityDID string, collectionName string, indexName string, fields []IndexField, unique bool) (*IndexDescription, error) {
@@ -1221,7 +1293,7 @@ func (n *Node) DropIndex(identityDID string, collectionName string, indexName st
12211293
}
12221294

12231295
// GetIndexes returns all indexes for a collection.
1224-
func (n *Node) GetIndexes(identityDID string, collectionName string) ([]IndexDescription, error) {
1296+
func (n *Node) GetIndexes(identityDID string, collectionName string) ([]IndexResult, error) {
12251297
var cIdentityDID *C.char
12261298
if identityDID != "" {
12271299
cIdentityDID = C.CString(identityDID)
@@ -1242,7 +1314,7 @@ func (n *Node) GetIndexes(identityDID string, collectionName string) ([]IndexDes
12421314
value := C.GoString(result.value)
12431315
C.defra_free_string(result.value)
12441316

1245-
var indexes []IndexDescription
1317+
var indexes []IndexResult
12461318
if err := json.Unmarshal([]byte(value), &indexes); err != nil {
12471319
return nil, fmt.Errorf("ffi: failed to parse indexes: %w", err)
12481320
}
@@ -1251,7 +1323,7 @@ func (n *Node) GetIndexes(identityDID string, collectionName string) ([]IndexDes
12511323
}
12521324

12531325
// GetAllIndexes returns all indexes across all collections.
1254-
func (n *Node) GetAllIndexes(identityDID string) (map[string][]IndexDescription, error) {
1326+
func (n *Node) GetAllIndexes(identityDID string) (map[string][]IndexResult, error) {
12551327
var cIdentityDID *C.char
12561328
if identityDID != "" {
12571329
cIdentityDID = C.CString(identityDID)
@@ -1269,7 +1341,7 @@ func (n *Node) GetAllIndexes(identityDID string) (map[string][]IndexDescription,
12691341
value := C.GoString(result.value)
12701342
C.defra_free_string(result.value)
12711343

1272-
var indexes map[string][]IndexDescription
1344+
var indexes map[string][]IndexResult
12731345
if err := json.Unmarshal([]byte(value), &indexes); err != nil {
12741346
return nil, fmt.Errorf("ffi: failed to parse indexes: %w", err)
12751347
}

tests/clients/rustffi/defra.h

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,12 @@ typedef struct NodeInitOptions {
118118
int enable_signing;
119119
/*
120120
Optional: signing key type string (e.g. "secp256k1", "secp256r1", "ed25519").
121-
Null to auto-generate secp256k1.
121+
Only used when signing_private_key is provided.
122122
*/
123123
const char *signing_key_type;
124124
/*
125-
Optional: raw private key bytes for signing.
126-
Null to auto-generate.
125+
Optional: raw private key bytes used for the node identity.
126+
Block signing remains controlled by enable_signing.
127127
*/
128128
const uint8_t *signing_private_key;
129129
/*
@@ -587,6 +587,15 @@ struct FfiResult delete_nac_actor_relationship(uintptr_t node_ptr,
587587
const char *relation,
588588
const char *target_did);
589589

590+
/*
591+
List actions that are in progress or ended with an error.
592+
593+
# Safety
594+
595+
`identity_did` must be null or a valid null-terminated UTF-8 string.
596+
*/
597+
struct FfiResult list_actions(uintptr_t node_ptr, const char *identity_did);
598+
590599
/*
591600
Export the database to a JSON file.
592601
@@ -1014,6 +1023,31 @@ struct FfiResult gc_downsample_histories(uintptr_t node_ptr, const char *options
10141023
*/
10151024
struct FfiResult delete_collection(uintptr_t node_ptr, const char *identity_did, const char *name);
10161025

1026+
/*
1027+
Delete one or more collections by name.
1028+
1029+
# Safety
1030+
1031+
`names_json` must be a valid null-terminated UTF-8 JSON array of strings.
1032+
*/
1033+
struct FfiResult delete_collections(uintptr_t node_ptr,
1034+
const char *identity_did,
1035+
const char *names_json,
1036+
bool active_only);
1037+
1038+
/*
1039+
Delete collections or collection versions within an existing transaction.
1040+
1041+
# Safety
1042+
1043+
`txn_id` and `targets_json` must be valid null-terminated UTF-8 strings.
1044+
*/
1045+
struct FfiResult delete_collections_in_txn(uintptr_t node_ptr,
1046+
const char *txn_id,
1047+
const char *identity_did,
1048+
const char *targets_json,
1049+
bool active_only);
1050+
10171051
/*
10181052
Set the active collection version.
10191053
@@ -1038,6 +1072,19 @@ struct FfiResult set_active_collection_version(uintptr_t node_ptr,
10381072
const char *identity_did,
10391073
const char *version_id);
10401074

1075+
/*
1076+
Set a collection version's active state within an existing transaction.
1077+
1078+
# Safety
1079+
1080+
`txn_id` and `version_id` must be valid null-terminated UTF-8 strings.
1081+
*/
1082+
struct FfiResult set_collection_active_in_txn(uintptr_t node_ptr,
1083+
const char *txn_id,
1084+
const char *identity_did,
1085+
const char *version_id,
1086+
bool is_active);
1087+
10411088
/*
10421089
Patch a collection's schema using JSON patch operations.
10431090
@@ -1692,6 +1739,24 @@ struct FfiResult exec_request(uintptr_t node_ptr,
16921739
const char *variables,
16931740
const char *batch_session_id);
16941741

1742+
/*
1743+
Execute a GraphQL query or mutation with a request-scoped signing override.
1744+
1745+
`signing_override` accepts `-1` for the node default, `0` to disable signing,
1746+
and `1` to enable signing.
1747+
1748+
# Safety
1749+
1750+
All string pointers must be either null or valid null-terminated UTF-8 strings.
1751+
*/
1752+
struct FfiResult exec_request_with_signing(uintptr_t node_ptr,
1753+
const char *identity_did,
1754+
const char *request_query,
1755+
const char *operation_name,
1756+
const char *variables,
1757+
const char *batch_session_id,
1758+
int signing_override);
1759+
16951760
/*
16961761
Add a schema to the database.
16971762
@@ -1885,6 +1950,25 @@ struct FfiResult exec_request_in_txn(uintptr_t node_ptr,
18851950
const char *variables,
18861951
const char *batch_session_id);
18871952

1953+
/*
1954+
Execute a GraphQL query or mutation within a transaction with a signing override.
1955+
1956+
`signing_override` accepts `-1` for the node default, `0` to disable signing,
1957+
and `1` to enable signing.
1958+
1959+
# Safety
1960+
1961+
All string pointers must be either null or valid null-terminated UTF-8 strings.
1962+
*/
1963+
struct FfiResult exec_request_in_txn_with_signing(uintptr_t node_ptr,
1964+
const char *txn_id,
1965+
const char *identity_did,
1966+
const char *request_query,
1967+
const char *operation_name,
1968+
const char *variables,
1969+
const char *batch_session_id,
1970+
int signing_override);
1971+
18881972
/*
18891973
Begin a new transaction.
18901974
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2026 Democratized Data Foundation
2+
//
3+
// This file is part of the DefraDB test suite.
4+
//
5+
// The DefraDB test suite is licensed under either:
6+
//
7+
// (1) GNU Affero General Public License v3
8+
// (2) Business Source License 1.1
9+
//
10+
// See tests/LICENSE for details.
11+
12+
//go:build rust_ffi
13+
14+
package rustffi
15+
16+
import (
17+
"testing"
18+
"time"
19+
20+
"github.com/stretchr/testify/require"
21+
22+
"github.com/sourcenetwork/defradb/client"
23+
"github.com/sourcenetwork/immutable"
24+
)
25+
26+
func TestNormalizeCollectionDateTimesPreservesNullableElements(t *testing.T) {
27+
first := time.Date(2026, time.July, 30, 12, 0, 0, 0, time.UTC)
28+
second := first.Add(time.Hour)
29+
value := map[string]any{
30+
"times": []any{first, nil, second},
31+
}
32+
version := client.CollectionVersion{
33+
Fields: []client.CollectionFieldDescription{{
34+
Name: "times",
35+
Kind: client.FieldKind_NILLABLE_DATETIME_ARRAY,
36+
}},
37+
}
38+
39+
normalizeCollectionDateTimes(value, version)
40+
41+
require.Equal(t, []immutable.Option[time.Time]{
42+
immutable.Some(first),
43+
immutable.None[time.Time](),
44+
immutable.Some(second),
45+
}, value["times"])
46+
}

0 commit comments

Comments
 (0)