anigo
v0.1.11
Published
A lightweight, local / embedded NoSQL database.
Downloads
1,851
Maintainers
Readme
Installing Anigo
$ npm install -g anigoQuick Start
import { Anigo } from 'anigo'
const db = Anigo.connect({ path: './app.db' })
const users = db.collection('users')
const id = users.insertOne({ name: 'Alice', age: 30, email: '[email protected]' })
// id is a UUID v7 string
const found = users.findOne({ _id: id })
// { _id: '...', name: 'Alice', age: 30, email: '[email protected]' }
const results = users.find({ age: { $gte: 25 } })
.sort({ name: 1 })
.skip(0)
.limit(20)
.toArray()
db.close()Features
- MongoDB-compatible CRUD API — all synchronous
- Query operators:
$gt,$gte,$lt,$lte,$ne,$in,$nin,$exists,$regex - Logical operators:
$and,$or,$not - Update operators:
$set,$unset,$inc,$push,$pull,$addToSet - Compound indexes with direction and uniqueness
- Aggregation pipeline:
$match,$sort,$skip,$limit,$project,$count,$group - Dot-notation for nested field queries
- Vector search — semantic similarity via local HuggingFace ONNX embeddings (async, no HTTP calls)
- UUID v7 (time-sortable, no dependencies)
- Page-level encryption (AES-256-CBC, ChaCha20, SQLCipher, etc.)
- WAL mode, 64 MB cache, optimized defaults
Table of Contents
- Connection
- Collections
- CRUD
- Query Operators
- Cursor
- Indexes
- Aggregation Pipeline
- Vector Search
- Encryption
- Backup & Optimize
- CLI
- Error Handling
- API Reference
Connection
import { Anigo } from 'anigo'
const db = Anigo.connect({ path: './data.db' })The path is a file path where the SQLite database will be stored. The file is created on first write.
| Option | Type | Description |
|--------|------|-------------|
| path | string | Path to the SQLite database file |
| key | string | Passphrase for AES-256-CBC encryption |
| cipher | { key, algorithm } | Advanced cipher configuration |
| vector | VectorOptions | Vector search configuration (model, dimensions, autoEmbed) |
isClosed
db.isClosed // booleanMethods
query
const rows = db.query('SELECT id, doc FROM "users"')Runs a raw SQL query and returns all matching rows. Useful for inspecting internal tables (_vec_config, _vec_pending, etc.).
exec
db.exec('INSERT INTO "users_vec_content_vx" (rowid, embedding) VALUES (?, ?)')Runs a raw SQL statement that does not return rows (INSERT, UPDATE, DELETE, DDL).
close
db.close()Closes the database. All subsequent operations throw.
flushVectorIndex
await db.flushVectorIndex()Processes all pending vector embedding entries. Required when autoEmbed is false.
optimize
db.optimize()Runs PRAGMA optimize. Call periodically or during maintenance windows.
backup
db.backup('./backup.db')Creates a point-in-time snapshot via VACUUM INTO. The backup is a fully functional database file.
listCollections
const names = db.listCollections()
// ['users', 'products']Returns the names of all user-defined collections. Excludes internal tables.
Collections
Collections are created lazily — the table is created on first write:
const users = db.collection('users')
users.insertOne({ name: 'Alice', age: 30, tags: ['admin'] })
const result = users.findOne({ name: 'Alice' })Collections are cached per connection. Calling db.collection('users') twice returns the same instance.
CRUD
Insert
// Auto-generated UUID v7 _id
const id = users.insertOne({ name: 'Alice', age: 30 })
// Custom _id
const id = users.insertOne({ _id: 'user-1', name: 'Bob' })
// Bulk insert
const ids = users.insertMany([
{ name: 'Charlie', age: 25 },
{ name: 'Diana', age: 28 },
])insertOnereturns the document's _id (UUID v7 string). The generated_idis also set on the input document object.- Duplicate
_idvalues throwDuplicateKeyError. insertManywraps all inserts in a transaction.- If vector search is configured and the document contains a vector-indexed field, the field is enqueued for embedding.
Find
// Find by _id (uses indexed column)
const user = users.findOne({ _id: id })
// Find by field
const user = users.findOne({ email: '[email protected]' })
// Find with operators
const results = users.find({ age: { $gte: 18, $lte: 65 } }).toArray()
// Find all
const all = users.find({}).toArray()findOnereturnsnullwhen no document matches.findreturns aCursor— no query executes until.toArray(),.first(), or.forEach()is called.
Update
// $set — set field values
users.updateOne({ _id: id }, { $set: { age: 31 } })
// $inc — increment numeric fields
users.updateOne({ _id: id }, { $inc: { loginCount: 1 } })
// $unset — remove fields
users.updateOne({ _id: id }, { $unset: { tempField: '' } })
// $push — append to array
users.updateOne({ _id: id }, { $push: { tags: 'premium' } })
// $pull — remove from array by value
users.updateOne({ _id: id }, { $pull: { tags: 'inactive' } })
// $addToSet — add to array if not present
users.updateOne({ _id: id }, { $addToSet: { tags: 'vip' } })
// Update many
users.updateMany({ role: 'guest' }, { $set: { role: 'user' } })Returns { matched, modified }:
matched— number of documents matching the filtermodified— number of documents actually changed
$push, $pull, and $addToSet use a read-modify-write pattern (read doc into JS, mutate, write back). $set, $unset, and $inc compile to SQL json_set()/json_remove() and run server-side.
Delete
// Delete one
users.deleteOne({ _id: id })
// Delete many
users.deleteMany({ status: 'archived' })Returns { deleted } — the number of documents removed.
Count
// Total documents
const total = users.countDocuments()
// With filter
const active = users.countDocuments({ active: true })Query Operators
Comparison
{ field: value } // Equality
{ field: { $gt: value } } // Greater than
{ field: { $gte: value } } // Greater than or equal
{ field: { $lt: value } } // Less than
{ field: { $lte: value } } // Less than or equal
{ field: { $ne: value } } // Not equal
{ field: { $in: [a, b] } } // In array
{ field: { $nin: [a, b] } } // Not in array
{ field: { $exists: true } } // Field exists / does not exist
{ field: { $regex: 'pat' } } // Regex match (JavaScript RegExp)Logical
{ $and: [filter1, filter2] }
{ $or: [filter1, filter2] }
{ $not: filter }Nested Fields
Use dot notation for nested fields:
{ 'address.city': 'New York' }
{ 'address.coordinates.lat': { $gt: 40 } }_id
The _id field is special — it maps to the indexed id column:
{ _id: 'abc-123' } // Direct PK lookup
{ _id: { $in: ['a', 'b'] } } // IN query on PKCursor
The Cursor is a lazy, chainable iterator returned by find() and aggregate().
const cursor = users.find({ age: { $gte: 25 } })
.sort({ name: 1 }) // ORDER BY in SQL
.skip(10) // OFFSET in SQL
.limit(5) // LIMIT in SQL
.project({ name: 1 }) // field projection in JS
// Terminal methods execute the query:
const results = cursor.toArray() // T[]
const first = cursor.first() // T | null
cursor.forEach(doc => { ... }) // voidSQL Push Optimization
When sort(), skip(), or limit() are chained on a find() cursor, Anigo pushes these clauses directly into the SQL query sent to SQLite. This means:
ORDER BYruns in SQLite — can use indexes for sortingLIMIT/OFFSETrun in SQLite — only fetches needed rows from diskfind({}).sort({ name: 1 }).limit(10).toArray()fetches exactly 10 rows
Without sort/skip/limit, the cursor falls back to fetching all matching rows and applying operations in memory (including project()).
Projection
// Inclusion — only specified fields (plus _id by default)
cursor.project({ name: 1, age: 1 })
// Exclusion — all fields except specified
cursor.project({ _id: 0, password: 0, __v: 0 })Projection is always applied in JavaScript (not pushed to SQL).
Indexes
Indexes are backed by SQLite indexes on json_extract() expressions.
Single-Field Index
users.createIndex({ email: 1 })
// SQL: CREATE INDEX "idx_users_email_1" ON "users" (json_extract(doc, '$.email') ASC)Compound Index
A single index covering multiple fields. Supports queries on leading fields (index prefix matching):
users.createIndex({ lastName: 1, firstName: -1 })
// SQL: CREATE INDEX "idx_users_lastName_1_firstName_-1" ON "users"
// (json_extract(doc, '$.lastName') ASC, json_extract(doc, '$.firstName') DESC)This index accelerates:
find({ lastName: 'Smith' })— prefix match on leading fieldfind({ lastName: 'Smith', firstName: 'John' })— full matchsort({ lastName: 1, firstName: -1 })— sort direction matches indexsort({ lastName: 1 })— prefix sort
Unique Index
users.createIndex({ email: 1 }, { unique: true })
// SQL: CREATE UNIQUE INDEX "idx_users_email_1" ON "users" (json_extract(doc, '$.email') ASC)Inserts or updates that would create a duplicate value throw an error. Unique constraint violations surface as typed errors (extend AnigoError).
Managing Indexes
// Create idempotently (IF NOT EXISTS)
users.createIndex({ name: 1 })
// Alias
users.ensureIndex({ email: 1 }, { unique: true })
// Drop
users.dropIndex({ name: 1 })
// List all indexes
const indexes = users.listIndexes()
// [
// { name: 'idx_users_email_1', unique: true, columns: [...] },
// { name: 'idx_users_lastName_1_firstName_-1', unique: false, columns: [...] },
// ]Naming Convention
Indexes are named automatically: idx_{collection}_{field1}_{dir1}_{field2}_{dir2}...
createIndex({ age: 1 })?idx_users_age_1createIndex({ lastName: 1, firstName: -1 })?idx_users_lastName_1_firstName_-1
Notes
_idalready has a primary key index —createIndex({ _id: 1 })is silently skipped.- Indexes on
$regexqueries are not used (uses JavaScriptRegExp). - The
CursorSQL push optimization allows SQLite's query planner to use indexes forORDER BY+LIMIT.
Aggregation Pipeline
const results = users.aggregate([
{ $match: { active: true } },
{ $sort: { age: -1 } },
{ $skip: 0 },
{ $limit: 10 },
{ $project: { name: 1, age: 1 } },
]).toArray()Supported Stages
| Stage | Description |
|-------|-------------|
| $match | Filter documents (same operators as find) |
| $sort | Sort by field: 1 (ASC) or -1 (DESC) |
| $skip | Skip N documents |
| $limit | Limit to N documents |
| $project | Include/exclude fields |
| $count | Count and output as document |
| $group | Group with accumulators |
$group
users.aggregate([
{ $group: {
_id: '$category',
total: { $sum: 1 },
avgPrice: { $avg: '$price' },
minPrice: { $min: '$price' },
maxPrice: { $max: '$price' },
}},
]).toArray()Supported accumulators: $sum, $avg, $min, $max, $first, $last.
Pipeline Optimization
Pipelines consisting only of $match, $sort, $skip, and $limit are collapsed into a single SQL query — sorting and pagination happen in SQLite, indexes apply.
Pipelines containing $project, $count, or $group execute entirely in memory (all matching documents are loaded into JavaScript).
Vector Search
Vector search enables semantic similarity queries. Field values are embedded into float vectors via a local HuggingFace ONNX model; documents closest to a prompt vector are returned ranked by distance. Embedding runs via ONNX Runtime — no HTTP calls.
Vector search operations are async (embedding inference runs asynchronously). CRUD operations remain synchronous.
const db = Anigo.connect({
path: './app.db',
vector: {
model: 'onnx-community/bge-m3-onnx',
dimensions: 1024,
autoEmbed: true,
},
})
const coll = db.collection<{ content: string; title: string }>('docs')
// Insert is sync; enqueues the document for embedding
coll.insertOne({ title: 'machine learning', content: 'neural networks' })
// Create vector indexes
coll.createVectorIndex('content')
coll.createVectorIndex('title')
// Flush pending embeddings (await required)
await db.flushVectorIndex()Key Concepts
| Concept | Description |
|---------|-------------|
| Vector index | A vec0 virtual table per (collection, field). Created via createVectorIndex(field). |
| Pending queue | Table _vec_pending records documents needing embedding after insert/update when autoEmbed=false. |
| autoEmbed | When true (default), a background drain loop processes pending entries. When false, call flushVectorIndex() manually. |
| Collection API | createVectorIndex(), dropVectorIndex(), listVectorIndexes(), flushPending(), search() all live on Collection — no need to access an internal vectorManager property. |
Creating a Vector Index
coll.createVectorIndex('content')createVectorIndexaccepts only the field name. Dimension and distance are inferred from the globalVectorOptionsconfig.- The method is synchronous — it creates the
vec0virtual table and enqueues existing documents for embedding. - Supported distance metrics:
cosine(default),l2,inner_product.
Listing Vector Indexes
const indexes = coll.listVectorIndexes()
// [
// { field: 'content', dimensions: 1024, distance: 'cosine', pendingCount: 0 },
// { field: 'title', dimensions: 1024, distance: 'cosine', pendingCount: 0 },
// ]Dropping a Vector Index
coll.dropVectorIndex('content')Removes the vec0 virtual table and its config entry.
Searching
// By raw vector array (no embedding needed)
const docs = await coll.search([0.1, 0.2, ...], { limit: 10 })
// By natural language prompt (embeds the prompt locally, then searches)
const docs = await coll.search('what is machine learning?', { limit: 10 })
// Cross-collection search
const results = await db.search('semantic query')
// Returns { collection, doc, score, field }[]coll.search()merges results across all vector indexes on that collection, taking the minimum distance per document. ReturnsWithId<T>[].db.search()searches all vector-indexed collections. ReturnsVectorSearchResult[]withcollection,doc,score, andfield.- Both methods are
async— useawait.
Flushing Pending Entries
await db.flushVectorIndex()Processes all pending embedding jobs. Required when autoEmbed is false.
Error Handling
| Scenario | Behavior |
|----------|----------|
| Model load failure | Throws at connect (eager) or first use (lazy) |
| Embedding dimension mismatch | Throws at createVectorIndex |
| search(string) with no embedder | Throws InvalidOperationError |
| Operation after close() | Throws InvalidOperationError |
Detailed documentation: docs/VECTOR.md
Encryption
Databases can be encrypted at rest by passing a key option:
const db = Anigo.connect({ path: './secure.db', key: 'passphrase' })This uses SQLite's page-level AES-256-CBC cipher. The API is identical — everything is transparent.
For advanced cipher configurations:
const db = Anigo.connect({
path: './secure.db',
cipher: { key: 'passphrase', algorithm: 'chacha20' },
})Available algorithms: aes256cbc, aes128cbc, chacha20, sqlcipher, rc4, ascon128, aegis.
Note: AES-256-GCM is not supported.
Backup & Optimize
// Point-in-time backup (VACUUM INTO)
db.backup('./snapshot.db')
// Optimize for performance (PRAGMA optimize)
db.optimize()CLI
Anigo ships with a command-line interface. Install it globally:
npm install -g anigoThen use the anigo command in one of three modes:
REPL Mode
Start an interactive shell with auto-await, dot-commands, and tab completion:
anigo path/to/db.dbSupports db.collectionName shorthand (via Proxy), bare commands (show collections, exit, quit), and the it variable:
> db.users.insertOne({ name: 'Alice', age: 30 })
'...uuid...'
> db.users.find({ age: { $gte: 25 } }).toArray()
[ { _id: '...', name: 'Alice', age: 30 } ]
> it
[ { _id: '...', name: 'Alice', age: 30 } ]Expression Mode
Evaluate a single expression in a VM sandbox:
anigo path/to/db.db 'db.users.find({}).toArray()'The sandbox provides access to db, console, JSON, Math, Buffer, Date, Map, Set, RegExp, setTimeout, and other standard globals. require, process, and import() are blocked.
Script Mode
Run a JavaScript file with access to the same sandbox:
anigo path/to/db.db --file ./scripts/migrate.jsFlags
| Flag | Description |
|------|-------------|
| --file | Run a script file instead of a single expression |
| --json | Output results as JSON (both REPL and expression modes) |
| --key | Database encryption passphrase |
| --cipher-key | Advanced cipher key |
| --cipher-algorithm | Cipher algorithm (e.g., chacha20, sqlcipher) |
| --vector-model | Embedding model name |
| --vector-dimensions | Embedding vector dimensions |
| --vector-autoembed | Enable/disable autoEmbed |
Environment Variables
All flags can be set via environment variables:
ANIGO_KEY— encryption passphraseANIGO_CIPHER_KEY,ANIGO_CIPHER_ALGORITHM— cipher configANIGO_VECTOR_MODEL,ANIGO_VECTOR_DIMENSIONS,ANIGO_VECTOR_AUTOEMBED— vector config
Flags take precedence over environment variables.
Error Handling
All errors are typed subclasses of AnigoError:
| Error | When |
|-------|------|
| DuplicateKeyError | Insert/update violates unique constraint or duplicate _id |
| InvalidFilterError | Malformed query filter |
| InvalidUpdateError | Malformed update document |
| InvalidOperationError | Operation not supported (including after close()) |
import { DuplicateKeyError } from 'anigo'
try {
users.insertOne({ _id: 'dup-id' })
} catch (err) {
if (err instanceof DuplicateKeyError) {
console.log('Duplicate:', err.details)
}
}API Reference
Detailed API documentation in the docs/ directory:
- Full API Reference — complete method signatures
- Query Operators — filter syntax reference
- Update Operators — update syntax reference
- Indexes — index usage and optimization guide
- Aggregation Pipeline — pipeline stage reference
- Vector Search — vector index, embedding, and search internals
License
MIT
