sekejap 0.19.4
sekejap: ^0.19.4 copied to clipboard
Embedded graph-first multi-model database (SQL + graph + vector + spatial). dart:ffi bindings over the sekejap C ABI, for Dart and Flutter.
sekejap for Dart #
Embedded graph-first, multi-model database — SQL + graph traversal + vector
- spatial in one native library, running in your process. No server, no second process.
The binding is dart:ffi over the sekejap C ABI
(docs/dist/C_ABI.md, header
dist/ffi/include/sekejap.h): one Dart class
per C handle and one method per C function. Pure Dart — it works the same in a
Dart CLI, in a server and in a Flutter app.
- PostgreSQL-like SQL —
CREATE TABLE,CREATE INDEX,INSERT,SELECT … WHERE,$1parameters. - Documents — a row is addressed by collection and key; a document is a
Dart
Map<String, Object?>and comes back as one, with_keyon it. - Graph — typed edges with properties, and the rows one hop away.
- Prepared statements — parse once, rebind many.
- Transactions — many writes under one durability barrier.
- Service mode — one writer, parallel readers, a change feed, a statement timeout and a cancel.
Install #
dart pub add sekejap # Dart CLI or server
flutter pub add sekejap # Flutter app
The package builds no native code. It loads a libsekejap that is already on
the machine.
Getting libsekejap #
libsekejap is the sekejap-capi crate (dist/ffi). Three ways to have one:
| where from | how |
|---|---|
| a release | libsekejap-<platform>.tar.gz is attached to every GitHub release (lib/ + include/) |
| from source | cargo build --release -p sekejap-capi in the sekejap repository |
| a package manager | cd dist/ffi && make install PREFIX=~/.local puts the library and sekejap.pc where pkg-config finds them |
How the wrapper finds it #
In this order; the first that loads wins.
useSekejapLibrary(path)— called before the first database call.SEKEJAP_LIBRARY— the full path to the file. What the tests use.- The running process — when it already carries
sekejap_version: a Flutter app whose plugin bundled the library, or a host that linkedlibsekejap.a. - The platform's plain library name —
libsekejap.dylib,libsekejap.so,sekejap.dll. The dynamic loader resolves it againstDYLD_LIBRARY_PATH/LD_LIBRARY_PATH, the macOS app bundle, the AndroidjniLibsdirectory, or the directory of the executable.
Nothing found is a SekejapLibraryNotFound naming every candidate it tried.
In Flutter, put the platform's libsekejap where the platform already
looks — android/app/src/main/jniLibs/<abi>/libsekejap.so, the macOS/iOS
app's Frameworks, beside the Windows or Linux executable — and step 4 finds
it with no configuration. Nothing in this package downloads or compiles a
native library at build time.
Use #
import 'package:sekejap/sekejap.dart';
void main() {
final db = Db.open('/tmp/demo');
db.createCollection('dish', const [
FieldSpec('name', FieldKind.text),
FieldSpec('price', FieldKind.integer),
]);
// A Tier-1 predicate is answered index-side, so a field a WHERE names needs
// an index; without one the query is REFUSED rather than quietly scanned.
db.execute('CREATE INDEX dish_price ON dish USING btree(price)');
db.put('dish', 'd1',
{'_key': 'd1', 'name': 'Fried Rice', 'price': 45000});
db.putMany('dish', {
'd2': {'name': 'Garden Salad', 'price': 38000},
'd3': {'name': 'Grilled Skewers', 'price': 52000},
});
print(db.get('dish', 'd1')); // {_key: d1, name: …, price: …}
print(db.query(r'SELECT name FROM dish WHERE price < $1', [40000]));
db.close();
}
example/sekejap_example.dart is the same tour over every call.
A walk, one page at a time #
final walk = db.scan('dish', pageRows: 256);
for (final row in walk.rows()) {
print(row['_key']);
}
walk.close();
db.stream(sql, params, pageRows) is the same shape over a statement's
answer. What paging buys is a bounded string per call and the freedom to stop
reading; the answer itself is assembled when the walk opens.
Prepared statements #
final byCeiling = db.prepare(r'SELECT name FROM dish WHERE price < $1');
print(byCeiling.rebindable); // Rebindable.unbound, before any bind
print(byCeiling.query([20000]));
print(byCeiling.rebindable); // Rebindable.yes — a rebind compiles nothing
byCeiling.close();
A WRITING statement answers Rebindable.no, because a write folds its
document at compile. The call says so rather than pretending otherwise.
Graph #
db.link('place', 'stall', 'serves', 'dish', 'd1', properties: {'since': 1998});
final served = db.neighbours('place', 'stall', edgeType: 'serves');
final back = db.neighbours('dish', 'd1',
edgeType: 'serves', direction: SekejapDirection.incoming);
Both endpoints must exist: a missing one is SekejapStatus.unknownRow, never
a dangling identity. A neighbour answer is complete or refused under a bound
of 256 edges; a walk deeper than one hop is SQL's GRAPH_TABLE.
Transactions #
final tx = db.transaction();
try {
tx.put('dish', 'd4', {'name': 'Chicken Soup', 'price': 41000});
tx.execute(r'DELETE FROM dish WHERE _key = $1', ['d3']);
tx.commit();
} catch (_) {
tx.rollbackIfOpen();
rethrow;
}
Every call outside a transaction commits before it returns: durability per
call. A transaction is the other bargain — many writes, one barrier — and the
two are the whole story. commit and rollback both free the handle;
db.close() rolls back a transaction that is still open, because a close is
not a commit.
Errors #
Both channels of the ABI reach Dart. A failure is a SekejapException
carrying the closed SekejapStatus and the sentence the engine wrote:
try {
db.query('SELECT FROM WHERE');
} on SekejapException catch (e) {
print(e.status); // SekejapStatus.invalid
print(e.message); // the message sekejap_last_error wrote on this thread
}
A miss is not a failure: db.get answers null for no such row,
db.describe answers null for no such collection, and Scan.nextPage
answers null at the end of the walk.
A refusal is never an empty answer. Db.openMemory, db.trimMemory,
db.compact and db.show keep their names and always throw
SekejapStatus.refused with the reason — sekejap has no atomic underneath any
of them, and an emulation would be a fake.
Service mode #
final service = Db.openService('/tmp/demo');
final subscription = service.subscribe();
service.put('dish', 'd9', {'name': 'Beef Stew', 'price': 65000});
final event = service.nextChange(subscription, timeoutMs: 2000);
service.statementTimeoutMs(5000);
service.unsubscribe(subscription);
Every call in this family is refused by name on a handle that was not opened
with Db.openService: single mode has no writer to time out, no interrupt and
no change feed.
Threading #
sekejap::Db is Send + Sync, so one Db MAY be used from several isolates
and each reads its own error slot back. The derived handles — Scan,
Statement, Transaction — are each used from one isolate at a time, and
all of them are freed before the Db they borrow (db.close() does that for
any that are still open).
Every call is synchronous: a dart:ffi call blocks its isolate. Put long
scans and large statements on their own isolate rather than on the one serving
a UI.
API mapping #
One Dart call per C function, all 60 of them. docs/dist/C_ABI.md is the
contract; lib/src/bindings.dart is the only file that names a C symbol.
| C | Dart |
|---|---|
sekejap_open |
Db.open(path) |
sekejap_open_with_config |
Db.openWithConfig(path, StoreConfig(…)) |
sekejap_open_service |
Db.openService(path) |
sekejap_close |
db.close() |
sekejap_version |
Db.version |
sekejap_format_version |
Db.formatVersion |
sekejap_last_error |
Db.lastError |
sekejap_last_error_code |
Db.lastErrorCode |
sekejap_string_free |
inside the wrapper, on every returned string |
sekejap_put |
db.put(collection, key, document) |
sekejap_put_many |
db.putMany(collection, rows) |
sekejap_get |
db.get(collection, key) |
sekejap_exists |
db.exists(collection, key) |
sekejap_delete |
db.delete(collection, key) |
sekejap_scan_open / _next / _close |
db.scan(…) → Scan.nextPage() / pages() / rows() / close() |
sekejap_execute |
db.execute(sql, params) |
sekejap_query |
db.query(sql, params) |
sekejap_explain |
db.explain(sql, params) |
sekejap_prepare |
db.prepare(sql) |
sekejap_stmt_query |
statement.query(params) |
sekejap_stmt_execute |
statement.execute(params) |
sekejap_stmt_rebindable |
statement.rebindable |
sekejap_stmt_free |
statement.close() |
sekejap_query_open / _next / _close |
db.stream(…) → the same Scan |
sekejap_link / sekejap_link_with |
db.link(…, properties: …) |
sekejap_unlink |
db.unlink(…) |
sekejap_neighbours |
db.neighbours(…) → List<Neighbour> |
sekejap_create_collection |
db.createCollection(name, fields) |
sekejap_drop_collection |
db.dropCollection(name) |
sekejap_collections |
db.collections() |
sekejap_describe |
db.describe(name) → CollectionDescription? |
sekejap_count_rows |
db.countRows(name) |
sekejap_scan_count_rows |
db.scanCountRows(name) |
sekejap_scan_count_edges |
db.scanCountEdges() |
sekejap_tx_begin |
db.transaction() |
sekejap_tx_put / _delete / _link / _execute |
tx.put / delete / link / execute |
sekejap_tx_commit / _rollback |
tx.commit() / tx.rollback() |
sekejap_checkpoint |
db.checkpoint() — false is DEFERRED, not failed |
sekejap_publish |
db.publish() |
sekejap_storage |
db.storage() → StorageBytes |
sekejap_statement_timeout_ms |
db.statementTimeoutMs(ms) |
sekejap_cancel |
db.cancel() |
sekejap_clear_interrupt |
db.clearInterrupt() |
sekejap_subscribe |
db.subscribe() |
sekejap_next_change |
db.nextChange(id, timeoutMs: …) → ChangeEvent? |
sekejap_unsubscribe |
db.unsubscribe(id) |
sekejap_open_memory |
Db.openMemory() — always refuses |
sekejap_trim_memory |
db.trimMemory() — always refuses |
sekejap_compact |
db.compact() — always refuses |
sekejap_show |
db.show(statement) — always refuses |
Gaps #
ChangeEvent.collectionIds, ChangeEvent.edgeTypeIds and
ChangedKey.collectionId are NUMERIC ids, because that is what
sekejap_next_change emits. The C ABI has no call that turns one into a name
(sekejap_collections answers names with no ids beside them), so a listener
that wants to act on a name cannot get one from the feed alone.
Tests #
tool/test.sh /path/to/libsekejap.dylib
which is dart pub get, dart analyze and dart test with
SEKEJAP_LIBRARY set. Every test opens a real database in a temporary
directory and compares against an oracle computed in the test process.
Licence #
MIT OR Apache-2.0, as the rest of sekejap.