NoSqueel Dart FFI Bindings
Dart FFI bindings for the NoSqueel embedded database. The package wraps the C library with idiomatic Dart classes for entities, queries, transactions, and optional encryption.
What this package provides
- FFI bindings — generated from
nosqueel.hviaffigen NSDatabase— open, close, get, put, wipe, shared multi-isolate handlesNSEntity/NSKey— create records with typed propertiesNSQuery— filtered and ordered queries withFilter,AndFilter, andOrFilterNSTransaction— optimistic transactions with commit and rollback- Encryption — optional AES-256-GCM at rest via
encryptionKey:on open
The native library must be built separately and placed where the loader can find it (see Building).
Prerequisites
- Dart SDK 3.0 or later
- CMake 3.28 or later
- A C compiler with C23 support
- POSIX threads (provided by the system toolchain)
Linux example:
sudo apt-get install cmake build-essential
Building
Option A: build script (recommended)
From bindings/dart:
chmod +x build.sh
./build.sh
This will:
- Configure and build
libnosqueelvia CMake - Copy the shared library into
lib/(e.g.lib/libnosqueel.so) - Regenerate
lib/impl/nosqueel.g.dartwithdart run tool/ffigen.dart
On Windows, use build.bat instead.
Option B: repository root build
From the repo root:
cmake -S . -B build
cmake --build build
cp build/libnosqueel.so bindings/dart/lib/libnosqueel.so # Linux
dart run bindings/dart/tool/ffigen.dart
Running tests
Tests load the native library from lib/libnosqueel.so (or platform equivalent). Build the library first, then:
cd bindings/dart
dart pub get
dart test
Adding to your project
dependencies:
nosqueel:
path: path/to/bindings/dart
dart pub get
import 'package:nosqueel/nosqueel.dart';
Quick start
import 'package:nosqueel/nosqueel.dart';
void main() {
final db = NSDatabase.open('/path/to/app.nosqueel');
final key = NSKey.integer('users', 1);
final user = NSEntity(key);
user.setProperty('name', 'Ada');
user.setProperty('score', 42);
db.put(user);
final loaded = db.get(key);
print(loaded?.getProperty<String>('name')); // Ada
db.close();
}
API overview
Opening and closing
// Plain database
final db = NSDatabase.open('/path/to/data.nosqueel');
// Encrypted database (AES-256-GCM, passphrase hashed with SHA-256)
final secure = NSDatabase.open(
'/path/to/secure.nosqueel',
encryptionKey: 'my-secret-passphrase',
);
db.wipe(); // clear all data, keep file open
db.close(); // release shared handle; pair each open with one close
NSDatabase.open uses the native ns_database_acquire() registry. Multiple isolates can open the same path and receive the same underlying handle. Each open must be matched with close.
Wrong encryption keys fail to open (they do not return an empty database).
Keys
final intKey = NSKey.integer('products', 42);
final nameKey = NSKey.name('users', 'alice');
Tables are created implicitly when the first entity is stored. Table and property names follow the limits documented in the root README.
Entities and properties
Properties are set and read with a single typed API:
entity.setProperty('name', 'Alice'); // String
entity.setProperty('age', 28); // int
entity.setProperty('rating', 4.5); // double
entity.setProperty('active', true); // bool
entity.setProperty('notes', null); // null
entity.setProperty('tags', <int>[1, 2]); // indexed list (empty list: <int>[])
entity.setProperty('body', NSText('…')); // large opaque payload (up to 1 MiB)
final name = entity.getProperty<String>('name');
final age = entity.getProperty<int>('age');
final tags = entity.getProperty<List<int>>('tags');
final body = entity.getProperty<NSText>('body');
final kind = entity.propertyType('name'); // NSPropertyType
final tagKind = entity.listElementType('tags'); // NSPropertyType.int
Supported property types: int, double, bool, String, typed List values (List<int>, List<double>, List<bool>, List<String>), null, and NSText. An empty typed list is distinct from null. Indexed String values are capped at maxIndexableStringBytes UTF-8 bytes; use NSText (or NSText.bytes) for larger opaque payloads up to maxTextBytes. NSText properties are not indexed and cannot be used in query filters.
NSDatabase.get returns an owned NSEntity?. Query results return borrowed entities that are only valid while the NSQueryResults object is alive.
Queries
final query = db.query('items', orderBy: ['-score', 'name']);
query.addFilter(Filter('score', Op.greaterThanOrEqual, 10));
query.addFilter(Filter('active', Op.equals, true));
final results = query.fetch(limit: 20, offset: 0);
for (final entity in results.entities) {
print(entity.getProperty<int>('score'));
}
results.dispose(); // optional: release native memory early
final total = query.count();
Filter composition:
- Consecutive
addFilter(Filter(...))calls are AND-ed on one branch addFilter(AndFilter([...]))adds one OR branch with AND-ed filtersaddFilter(OrFilter([...]))adds separate OR branches
Available operators (Op): equals, contains, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, overlap, isEmpty.
List properties support contains/equals (value in list), overlap (pass a typed list as the filter value), and isEmpty (pass true or false). Other operators on list properties match nothing.
If a filtered or ordered property lacks a suitable index, fetch() or count() throws NSMissingIndexException. Indexes are created automatically when data is written or when a query succeeds.
limit and offset are applied in Dart after the native query runs.
Transactions
final txn = db.beginTransaction();
final entity = txn.get(NSKey.integer('items', 1));
if (entity != null) {
entity.setProperty('score', 200);
txn.put(entity);
}
txn.commit(); // or txn.rollback()
- Read-your-writes —
txn.getsees uncommitted puts and deletes in the same transaction - Optimistic locking — updating or deleting an existing entity requires a prior
txn.getin that transaction; commit fails if another writer changed a read key - Queries inside transactions —
txn.query()runs against committed data only; uncommitted writes are not visible to transactional queries - Deletes —
txn.delete(key)is available; there is no non-transactionaldb.delete()wrapper yet
Multiple transactions may be active on the same database.
Encryption
Pass encryptionKey when opening:
final db = NSDatabase.open(path, encryptionKey: 'strong-passphrase');
The binding:
- Derives a 32-byte AES-256 key with SHA-256 from the UTF-8 passphrase
- Registers per-isolate
NativeCallableencrypt/decrypt callbacks - Passes key and callbacks to
ns_database_acquire()on every open
Each isolate that opens an encrypted database must do so with the correct key. Callbacks are bound to the isolate's thread in native code.
Limitations:
- Protects data at rest on disk, not in memory while the database is open
- SHA-256 key derivation has no salt or memory-hard stretching — use a strong passphrase
- The 32-byte key size is required by AES-256, not an arbitrary cap
- See the root README encryption section for the full threat model
Package layout
lib/
nosqueel.dart # Public exports
libnosqueel.so # Built native library (not in git)
impl/
bindings.dart # Library loader + re-exports generated bindings
nosqueel.g.dart # Generated FFI bindings (ffigen)
ns_database.dart
ns_entity.dart
ns_key.dart
ns_query.dart
ns_transaction.dart
ns_filter.dart
ns_crypto.dart # AES-GCM callbacks for encryption
tool/
ffigen.dart # ffigen configuration
test/ # Integration tests (require built lib/)
Development
Regenerate FFI bindings
After changing nosqueel.h:
cd bindings/dart
dart run tool/ffigen.dart
Rebuild native library after C changes
./build.sh
Or manually:
cmake --build build
cp build/lib/libnosqueel.so lib/
Troubleshooting
Cannot load library / ns_database_acquire failed
- Build the native library:
./build.sh - Confirm the file exists:
ls -la lib/libnosqueel.so - For tests, ensure you run from
bindings/dartso the default search path resolves
NSMissingIndexException
The query filters or orders on a property that does not yet have an index. Store at least one entity with that property, or simplify the query.
Encryption open failures
- Encrypted files require the same
encryptionKeyused at creation - Wrong keys fail at open time with
StateError - Opening without a key on an encrypted file also fails
CMake or compiler errors
See docs/BUILD.md for additional build notes.
Platform support
| Platform | Status |
|---|---|
| Linux | Flutter FFI plugin + CLI tests via prebuilt lib/libnosqueel.so |
| macOS | Flutter FFI plugin (nosqueel.framework) |
| Windows | Flutter FFI plugin (nosqueel.dll) |
| Android | Flutter FFI plugin (libnosqueel.so) |
| iOS | Flutter FFI plugin (nosqueel.framework) |
| Web | Not supported (dart:ffi unavailable) |
This package is a Flutter FFI plugin. In a Flutter app, add it as a dependency and run flutter pub get; Flutter builds and bundles the native library automatically.
For CLI / unit tests on desktop, build the library with ./build.sh (or build.bat) so tests can load lib/libnosqueel.so before falling back to the bundled plugin name.
cd bindings/dart
flutter pub get # or dart pub get when Flutter SDK is on PATH
./build.sh
dart test
Mobile-specific notes:
Limitations
- No
NSDatabase.delete()— useNSTransaction.delete()inside a transaction - Query
limit/offsetare Dart-side only - Transactional queries do not see uncommitted writes
- Key-typed properties are not readable from Dart yet (
NSPropertyType.key) - Some C APIs (direct
ns_database_open, memory databases, table iteration) are not wrapped - Mobile packaging is documented separately and is more involved than desktop FFI
Further reading
- Root NoSqueel README — data model, query rules, transaction semantics, encryption design
- docs/BUILD.md — detailed build troubleshooting
Libraries
- impl/bindings
- impl/nosqueel.g
- impl/ns_change
- impl/ns_change_hub
- impl/ns_crypto
- impl/ns_database
- impl/ns_entity
- impl/ns_filter
- impl/ns_key
- impl/ns_query
- impl/ns_query_matcher
- impl/ns_text
- impl/ns_transaction
- nosqueel
- Dart bindings for the NoSqueel embedded database.