@tuckertucker/koji-db
v0.5.1
Published
Kōji - Hybrid SQL + Recursive + Vector Database
Maintainers
Readme
Koji Node.js Bindings
Node.js bindings for Koji, the hybrid SQL + Recursive + Vector database.
Installation
npm install @tuckertucker/koji-dbQuick Start
import { openMemory } from '@tuckertucker/koji-db';
// Create in-memory database
const db = await openMemory();
// Create table
await db.syncSchema({
users: { columns: { id: { type: 'integer' }, name: { type: 'text' } } }
});
// Insert data
await db.insert('users', [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]);
// Query
const result = await db.query('SELECT * FROM users');
console.log(result.rows);
await db.close();Features
- Relational queries via SQL
- Recursive CTEs for graph traversal
- Vector similarity search with automatic indexing
- JSON serialization for seamless JavaScript integration
Configuration
Auto-Sync (Durability by Default)
Koji automatically syncs data to disk after each mutation operation by default. This ensures your data is durable and survives process crashes.
import { open } from '@tuckertucker/koji-db';
// Default behavior: autoSync enabled
const db = await open('./data');
await db.syncSchema({
users: { columns: { id: { type: 'integer' }, name: { type: 'text' } } }
});
await db.insert('users', [{ id: 1, name: 'Alice' }]);
// ^ Data is immediately durable - survives crash
await db.update('users', { name: 'Bob' }, 'id = 1');
// ^ Update is immediately durable
await db.delete('users', 'id = 1');
// ^ Delete is immediately durableDisabling Auto-Sync for Batch Operations
For high-throughput batch operations, disable auto-sync and manually sync at the end:
import { open } from '@tuckertucker/koji-db';
// Disable auto-sync for batch insert
const db = await open('./data', { autoSync: false });
await db.syncSchema({
events: { columns: { id: { type: 'integer' }, data: { type: 'text' } } }
});
// Insert 10,000 rows without per-row sync overhead
for (const batch of dataBatches) {
await db.insert('events', batch);
}
// Single sync at the end
await db.sync();Performance Considerations
| Mode | Use Case | Tradeoff |
|------|----------|----------|
| autoSync: true (default) | OLTP, multi-process apps | Durable but slower for bulk ops |
| autoSync: false | Batch ingestion, ETL | Faster but requires manual sync |
When to disable auto-sync:
- Bulk data imports (1000+ rows)
- ETL pipelines
- Single-writer scenarios with explicit checkpoints
When to keep auto-sync enabled (default):
- Multi-process applications
- User-facing operations
- Any operation where data loss is unacceptable
In-Memory Databases
Auto-sync also applies to in-memory databases, though it's less critical since there's no disk persistence:
import { openMemory } from '@tuckertucker/koji-db';
// Auto-sync enabled (default)
const db = await openMemory();
// Auto-sync disabled
const db2 = await openMemory({ autoSync: false });API Reference
open(path, options?)
Opens a database at the specified path.
Parameters:
path: string- Path to database directoryoptions?: OpenOptions- Configuration options
Returns: Promise<Database>
Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| autoSync | boolean | true | Sync to disk after mutations |
Example:
// Default (durable)
const db = await open('./data');
// Batch mode (manual sync)
const db2 = await open('./data', { autoSync: false });openMemory(options?)
Opens an in-memory database.
Parameters:
options?: OpenOptions- Configuration options
Returns: Promise<Database>
Example:
const db = await openMemory();
const db2 = await openMemory({ autoSync: false });OpenOptions
Configuration options for database opening.
interface OpenOptions {
/**
* Automatically sync after mutations.
* @default true
*/
autoSync?: boolean;
}Database Methods
| Method | Description |
|--------|-------------|
| syncSchema(schema, options?) | Sync database schema to match target definition |
| query(sql, params?) | Execute query and return results |
| insert(table, data) | Insert JSON objects into table |
| scan(table) | Read all rows from table |
| delete(table, filter?) | Delete rows matching filter |
| update(table, assignments, filter?) | Update rows matching filter |
| sync() | Force sync to disk |
| close() | Close database connection |
| listTables() | List all table names |
| getSchema(table) | Get table schema |
| version(table) | Get table version number |
| compact(table, options?) | Compact table fragments |
| cleanupVersions(table, options?) | Remove old versions |
| vacuum(table, options?) | Full table maintenance |
| vectorSearch(table, column, query, k, filter?) | k-NN similarity search |
| hybridSearch(table, column, query, k, filter) | Vector + SQL hybrid search |
| batchVectorSearch(table, column, queries, k, filter?) | Batch k-NN searches |
| createVectorIndex(table, column, options) | Create HNSW/IVF-PQ vector index |
| listIndexes(table) | List vector indexes on table |
| indexExists(table, indexName) | Check if index exists |
| dropIndex(table, indexName) | Drop vector index (no-op in Lance 0.22) |
Migration Guide
Migrating from pre-autoSync Versions
If you're upgrading from a version without autoSync, your existing code will continue to work without changes.
Before (manual sync required):
await db.insert('users', [{ id: 1 }]);
await db.sync(); // Required for durabilityAfter (auto-sync by default):
await db.insert('users', [{ id: 1 }]);
// sync() is called automatically - data is durableYour existing sync() calls are now harmless no-ops. You can:
- Leave them in place (safest, no code changes)
- Remove them gradually (cleaner code)
- Add
{ autoSync: false }to batch operations that have sync() at the end
If you have batch operations with explicit sync:
// Before: works but inefficient with autoSync=true
for (const row of data) {
await db.insert('table', [row]);
}
await db.sync();
// After: explicitly disable autoSync for batches
const db = await open('./data', { autoSync: false });
for (const row of data) {
await db.insert('table', [row]);
}
await db.sync(); // Single sync at endDocumentation
See the full documentation.
