Guides
Transactions
Atomic writes, snapshot isolation, conflicts, and bulk load
A transaction groups writes so they commit together or not at all. The high-level API wraps this in transaction() and batch(); the low-level Database API exposes begin, commit, and rollback directly.
High-level transactions (Kite)
The high-level Kite API supports explicit transactions for batching multiple operations into a single commit. When the callback completes, the transaction commits; on error it rolls back.
import { kite } from '@kitedb/core';const db = await kite('./my.kitedb', { nodes: [User], edges: [follows] });await db.transaction(async (ctx) => { const alice = ctx.insert('user').values('alice', { name: 'Alice' }).returning(); const bob = ctx.insert('user').values('bob', { name: 'Bob' }).returning(); ctx.link(alice.id, 'follows', bob.id, { since: 2024 });});Batch operations
A batch runs its operations in a single transaction, so it is atomic and pays the commit cost once. Use it for ingestion, such as indexing a codebase.
// Batch with builder/executor operations (sync)db.batch([ db.insert('user').values('alice', { name: 'Alice' }), db.insert('user').values('bob', { name: 'Bob' }), () => db.link(aliceId, 'follows', bobId, { since: 2024 }),]);Isolation and conflicts
Transactions are snapshot-isolated (MVCC, on by default since 0.3.0): a transaction reads the database as of its begin, plus its own writes, and never sees a later commit. Reads outside a transaction see the latest committed state.
Write transactions on different threads run at the same time. A commit fails with a conflict if a key the transaction read or wrote was changed by a commit since the transaction began. Nothing from the failed transaction is applied and the transaction has ended, so run it again. Rust returns KiteError::Conflict, Python raises ConflictError, and JavaScript throws an Error whose message contains conflict on keys. Writers that touch different nodes and edges don't conflict. In Node.js, JS on the main thread runs one transaction at a time per handle, so its commits can only conflict with work on other threads, such as importFromJsonAsync().
function isConflict(e: unknown): boolean { return e instanceof Error && e.message.includes('conflict on keys');}for (let attempt = 1; ; attempt++) { try { db.transaction((ctx) => { const update = ctx.update('user', 'alice'); update.set('status', 'active'); update.execute(); }); break; } catch (e) { // Nothing was applied; run the transaction again if (!isConflict(e) || attempt === 5) throw e; }}Opening with mvcc: false (Rust .mvcc(false), Python OpenOptions(mvcc=False)) is deprecated and will be removed in a later release. In that mode write transactions run one at a time, a second writer's begin waits for the first to finish, and transactions read the latest committed state instead of a snapshot. See Concurrency.
Bulk load (max throughput)
A bulk-load transaction is the fastest way to load data, with MVCC or without it. It runs alone among writers: it waits for open write transactions to finish, and write transactions that begin while it is open wait for it. Readers never wait for it. Reads outside a transaction see the whole load once it commits; a read transaction that began before the commit does not see it. If read transactions are open when it commits, the commit records version history for them, which takes time proportional to the size of the load. Use it for one-shot ingest or ETL jobs.
import { Database } from '@kitedb/core';const db = Database.open('./my.kitedb');db.beginBulk();const nodeIds = db.createNodesBatch(keys); // keys: Array<string | null>db.addEdgesBatch(edges); // edges: { src, etype, dst }[]db.addEdgesWithPropsBatch(edgesWithProps);db.commit();Choose a write path
| Goal | Recommended API |
|---|---|
| Fastest ingest | beginBulk() + batch APIs |
| Atomic writes | batch() / transaction() |
| Several writer threads | A transaction per thread, syncMode: 'Normal'; concurrent commits are written together. Retry commits that fail with a conflict |
Current limitations
- Traversal and path queries read the committed view. If you need to traverse newly written edges, commit first.
- JavaScript
batch()is synchronous; avoid async work inside a batch (do async work first, then batch the writes).
Basic transactions
import { Database, PropValueType } from '@kitedb/core';const db = Database.open('./my.kitedb');// Begin a read-write transactiondb.begin();try { // All operations are part of the transaction const nodeId = db.createNode('user:alice'); db.setNodePropByName(nodeId, 'name', { propType: PropValueType.String, stringValue: 'Alice', }); // Commit the transaction db.commit();} catch (e) { // Rollback on error db.rollback(); throw e;}Read-only transactions
A read-only transaction reads one snapshot, as of its begin, for as long as it is open. Keep it short: while any transaction is open, commits record version history for it, and garbage collection can't remove that history until it ends.
// Begin a read-only transactiondb.begin(true);const node = db.get_node_by_key('user:alice');const props = node !== null ? db.get_node_props(node) : null;// Read-only transactions still need to be endeddb.commit(); // or db.rollback() - same effect for read-onlyTransaction status
// Check if there's an active transactionif (db.hasTransaction()) { console.log('Transaction is active');}// The Kite high-level API auto-manages transactions// and also supports explicit db.transaction()/db.batch()Next steps
- High-level API: the full transaction API
- MVCC and transactions: how transactions are isolated