| name | chiffondb-dart |
| description | Work on chiffondb-dart, the Dart/Flutter FFI bindings for ChiffonDB (a Rust embedded property graph DB). Use this skill whenever editing this package: regenerating flutter_rust_bridge code, debugging native-library loading, running the FFI-backed test suite, building the example with build_runner, or cutting a release. Covers the non-obvious rules that prevent breaking the generated bindings, the native library hook, and CI. |
chiffondb-dart
Dart/Flutter FFI bindings for ChiffonDB, a lightweight embedded property
graph database written in Rust. This package wraps the Rust chiffondb-ffi
crate via flutter_rust_bridge (frb) 2.12.0 and ships a code generator
(chiffondb_generator, separate package) for @NodeType/@EdgeType schema
classes.
The package is FFI-heavy: most of the friction is not in the Dart API but in
code generation, native library loading, and test/CI environment
variables. This skill captures the rules that are easy to get wrong.
Repository layout
lib/chiffondb.dart Public barrel (exports below)
lib/src/init.dart ChiffonDb.init() + native lib loader (HAND-WRITTEN)
lib/src/annotations.dart @NodeType / @EdgeType / @Id (HAND-WRITTEN)
lib/src/generated/ frb output — DO NOT EDIT BY HAND
hook/build.dart Native asset build hook (download/lipo) (HAND-WRITTEN)
test/property_test.dart kiri_check property tests
test/stateful_test.dart kiri_check stateful model test
test/traversal_search_test.dart raw command-JSON traversal semantics
example/social_graph.dart @NodeType demo; *.g.dart + *.chiffondb_store.dart are generated
.github/workflows/ci.yml analyze + 3-OS test matrix
The Rust source lives in a sibling repository, conventionally at
../chiffondb/ (the Rust workspace). lib/src/init.dart and hook/build.dart
both probe several relative paths to find a locally built dylib there.
Golden rules
- Never hand-edit anything under
lib/src/generated/. Every file there
starts with // This file is automatically generated. To change the Dart
surface of the FFI, change the Rust chiffondb-ffi crate and regenerate
(see below). The frb version is pinned to 2.12.0 — keep the codegen tool
and the flutter_rust_bridge dependency on the same version.
- A new FFI feature does NOT always need regeneration. Traversal and
Cypher queries are sent as raw command JSON through
Connection.executeTraversal / executeCypher. Features that only change
command-JSON semantics (e.g. any_key_contains recursion, {"path":[...]}
nested keys — see test/traversal_search_test.dart) require no FFI
signature change and no regeneration. Add a test that builds the JSON
directly rather than touching the bindings.
dart test must run serially. dart_test.yaml pins concurrency: 1 to
avoid concurrent native-library initialization. If you invoke tests
manually, pass --concurrency=1.
- The native library load path is fragile and mirrored in two files.
lib/src/init.dart (_libraryCandidates) and hook/build.dart
(_findLocalLib / _resolveTarget) must stay in sync on the dylib naming,
the aarch64/x86_64 arch strings, and the target triples. Editing one
almost always means editing the other.
Code generation (flutter_rust_bridge)
The generated bindings come from the Rust chiffondb-ffi crate. Regenerate
only when the Rust FFI signatures change.
There is no flutter_rust_bridge.yaml in this repo, so generate must be
given its inputs/outputs on the command line. The critical part is the
dual --rust-input: the FFI surface (Connection) lives in the
chiffondb-core crate and chiffondb-ffi only re-exports it
(pub use chiffondb_core::api;). If you pass crate::api alone, frb cannot
follow the re-export into the dependency crate and silently drops every
Connection method — api.dart is left untouched and frb_generated.dart
shrinks by thousands of lines. You must scan both modules:
cd ../chiffondb/chiffondb/chiffondb-ffi
flutter_rust_bridge_codegen generate \
--rust-input "crate::api,chiffondb-core::api" \
--rust-root "$PWD" \
--rust-output "$PWD/src/frb_generated.rs" \
--dart-output "<path>/chiffondb-dart/lib/src/generated" \
--dart-root "<path>/chiffondb-dart"
After regenerating, the diff should touch only lib/src/generated/**
(frb_generated.dart, frb_generated.io.dart, frb_generated.web.dart,
generated/third_party/chiffondb_core/api.dart) on the Dart side and
chiffondb-ffi/src/frb_generated.rs on the Rust side. Then:
(cd ../chiffondb/chiffondb && cargo build -p chiffondb-ffi)
dart format .
CHIFFONDB_HOOK_SKIP=1 dart analyze --fatal-infos
Notes:
- The Rust workspace must be reachable (default
../chiffondb/).
- frb resolves
dart/flutter via FVM and prints
"Installing Flutter version via FVM… Done!" — this is normal, not a failure.
frb ignores opaque types/methods it can't expose; the header comments in
api.dart list what was ignored (e.g. Transaction, NodeResult). This is
expected, not a bug. Connection is auto-treated as an opaque handle
(RustAutoOpaqueInner<Connection>); no #[frb(opaque)] is needed in the
pure-Rust core as long as the dual --rust-input above is used.
- A fuller walkthrough lives in
doc/regenerating-bindings.md.
The chiffondb_generator code generator (build_runner)
@NodeType / @EdgeType / @Id annotations (in lib/src/annotations.dart)
are consumed by the separate chiffondb_generator package via
build_runner, producing *.g.dart and *.chiffondb_store.dart parts (see
example/social_graph.dart). To regenerate the example/store code:
CHIFFONDB_HOOK_SKIP=1 dart run build_runner build --delete-conflicting-outputs
CHIFFONDB_HOOK_SKIP=1 skips the native build hook so build_runner does not try
to download/build the dylib (it doesn't need FFI just to generate source).
Native library loading — how it works
ChiffonDb.init() must be called once before any Connection API. It loads the
dylib explicitly (frb uses DynamicLibrary.lookup, so the native_assets
mapping is not auto-resolved). Load order, by platform:
CHIFFONDB_CORE_LIB env var (explicit absolute path) — wins if set.
- Local dev builds in the sibling Rust repo:
../chiffondb/.../target/{debug,release}/.
- Hook output:
.dart_tool/lib/libchiffondb_ffi.{dylib,so} / chiffondb_ffi.dll.
- Flutter-bundled framework:
chiffondb_ffi.framework/chiffondb_ffi
(no arch suffix — produced by flutter run/flutter build).
- Arch-suffixed framework:
chiffondb_ffi-<arch>-apple-darwin.framework/...
(GitHub Releases naming).
Common failure: "Failed to load the chiffondb_ffi native library". Causes:
the dylib wasn't built/downloaded, or a naming mismatch between init.dart and
how the platform actually bundles it. The non-suffixed .framework path was
added precisely because flutter build macos names the framework after the
CodeAsset with no arch suffix — keep that candidate.
The build hook (hook/build.dart)
Runs at dart pub get / build time. It either copies a locally built
dylib (search order in _findLocalLib) or downloads the prebuilt zip from
github.com/szktty/chiffondb/releases/download/v<version>/.... On macOS/iOS it
thins the universal binary with lipo (Dart's native_assets bundler
requires a single-arch dylib).
The version constant in hook/build.dart (currently 0.1.0) must point at a
ChiffonDB GitHub Release that actually has the platform zips
(libchiffondb_ffi-macos-universal.zip, -linux-x86_64.zip,
chiffondb_ffi-windows-x86_64.zip). Bump it in lockstep with releases.
Environment variables (build/test control)
| Variable | Effect |
|---|
CHIFFONDB_CORE_LIB | Absolute path to a prebuilt dylib; highest-priority load source. |
CHIFFONDB_CORE_ROOT | Rust workspace root; hook searches $root/target/{release,debug}/. |
CHIFFONDB_USE_RELEASE=1 | Hook skips local search and always downloads from GitHub Releases (used by CI tests). |
CHIFFONDB_HOOK_SKIP=1 | Skip the native hook entirely (no FFI). Use for dart analyze, build_runner, or pure-source work. |
Rule of thumb:
- Editing source / analyzing / build_runner, no FFI calls → prefix with
CHIFFONDB_HOOK_SKIP=1.
- Running tests against the published binary → prefix with
CHIFFONDB_USE_RELEASE=1.
- Running tests against a local Rust build → leave both unset (hook finds the sibling
target/), or set CHIFFONDB_CORE_LIB.
Testing
The suite uses package:test plus kiri_check (property-based + stateful
model testing):
property_test.dart — schema/CRUD invariants.
stateful_test.dart — random command sequences vs. an in-memory GraphState model.
traversal_search_test.dart — command-JSON query semantics (no FFI change).
Run (serial is mandatory):
CHIFFONDB_USE_RELEASE=1 dart test --concurrency=1
dart test --concurrency=1
Every test that touches FFI calls await ChiffonDb.init() in setUpAll.
Before committing / pre-PR checklist
CI (.github/workflows/ci.yml) runs three gates. Match them locally:
dart format --output=none --set-exit-if-changed .
CHIFFONDB_HOOK_SKIP=1 dart analyze --fatal-infos
CHIFFONDB_USE_RELEASE=1 dart test --concurrency=1
Also:
- If you regenerated frb code, confirm the diff is only under
lib/src/generated/**.
- Update
CHANGELOG.md under ## Unreleased describing the change, and note
explicitly whether it involved an FFI/binding change or was
command-JSON only (this distinction matters to consumers).
Releasing
- Keep the compatibility table in
README.md (Dart version ↔ Rust ChiffonDB
version) accurate.
- Bump
version: in pubspec.yaml, the version constant in
hook/build.dart, and the ## Unreleased → versioned heading in
CHANGELOG.md together.
- Ensure the matching ChiffonDB GitHub Release exists with all platform zips
before publishing, or the hook download will fail for users.
- Native library naming in release zips must match
_resolveTarget
(hook/build.dart) and the _libraryCandidates framework names
(init.dart).
When to use the sibling Rust repo
The Rust crate is expected at ../chiffondb/ (or ../../chiffondb/). When a
change needs Rust-side work (new FFI method, changed query semantics), build it
there first:
cargo build -p chiffondb-ffi
cargo test && cargo clippy && cargo fmt
Then regenerate bindings on the Dart side (flutter_rust_bridge_codegen generate) and the local target/ build will be picked up by the hook/loader
automatically.