@worldapi.org/mpackdb
Version:
A simple, local, binary json (using MessagePack) database with binary search index.
428 lines (314 loc) • 10.3 kB
Markdown
# MPackDB
A fast, local, append-only JSON database with [MessagePack](https://msgpack.org/) serialization for Node.js.
## Features
- **🚀 High Performance** - Append-only writes with MessagePack binary serialization
- **📇 Flexible Indexing** - Optional numeric and lexical indexes for fast queries
- **🔒 Concurrent Access** - File-based locking for safe multi-process access
- **💾 Auto-Persistence** - Configurable automatic index persistence
- **🗜️ Auto-Compaction** - Removes deleted records on startup
- **🎯 Simple API** - Intuitive CRUD operations with async/await
- **📦 Zero Dependencies** - Only requires `msgpackr`
## Installation
```bash
npm install @worldapi.org/mpackdb
```
## Quick Start
```javascript
import MPackDB from '@worldapi.org/mpackdb';
// Create a database with auto-increment numeric primary key
const db = new MPackDB('data/users', {
primaryKey: '*id', // * prefix = numeric auto-increment
indexes: ['email', '*age'] // Index email (lexical) and age (numeric)
});
// Insert records
await db.insert({ name: 'Alice', email: 'alice@example.com', age: 30 });
await db.insert({ name: 'Bob', email: 'bob@example.com', age: 25 });
// Find all records
for await (const user of db.find()) {
console.log(user);
}
// Find by primary key
const users = await db.find(0); // Returns array with Alice
// Query with function
for await (const user of db.find(r => r.age > 28)) {
console.log(user.name); // Alice
}
// Update records
await db.update(0, { age: 31 });
await db.update(r => r.age < 26, { status: 'junior' });
// Delete records
await db.delete(r => r.age < 18);
// Close database (persists all changes)
await db.close();
```
## API Reference
### Constructor
```javascript
new MPackDB(dbFile, options)
```
**Parameters:**
- `dbFile` (string): Path to database file (without extension)
- `options` (object):
- `primaryKey` (string): Primary key field name with optional prefix:
- `*field` - Numeric auto-increment (e.g., `*id`)
- `@field` - UUID (e.g., `@uuid`)
- `field` - String (e.g., `username`)
- `primaryKeyType` (PrimaryKeyType): Explicit type override
- `indexes` (string[]): Fields to index (use prefixes like primary key)
- `debug` (boolean): Enable debug logging (default: `false`)
- `indexPersistInterval` (number): Auto-persist interval in ms (default: `60000`, `0` to disable)
- `indexPersistThreshold` (number): Auto-persist after N changes (default: `1000`)
**Examples:**
functional style:
```javascript
import MPackDB from '@worldapi.org/mpackdb';
const db = new MPackDB('data/products', {
primaryKey: '*id',
indexes: ['category', '*price', '@sku'],
indexPersistThreshold: 100
});
```
OOP style:
```javascript
import { MPackDB, Model, PrimaryKeyType } from '@worldapi.org/mpackdb';
class Product extends Model {
id = 0;
name = '';
price = 0;
category = '';
sku = '';
}
class Products extends MPackDB {
_classToUse = Product;
_primaryKey = 'id';
_primaryKeyType = PrimaryKeyType.UUID;
_indexes = ['category', '*price', '@sku'];
}
export default products = new Products('data/products');
```
### insert(record, options)
Insert a new record into the database.
```javascript
await db.insert({ name: 'Alice', age: 30 });
// Returns: { id: 0, name: 'Alice', age: 30 }
```
**Parameters:**
- `record` (object): The record to insert
- `options` (object):
- `skipPrimaryKey` (boolean): Don't auto-generate primary key
**Returns:** Promise<Object> - The inserted record with primary key
### find(query, options)
Find records in the database. Returns an async iterable Cursor.
```javascript
// Find all
for await (const record of db.find()) {
console.log(record);
}
// Find by primary key
const users = await db.find(0);
// Find with query function
for await (const user of db.find(r => r.age > 30)) {
console.log(user.name);
}
```
**Parameters:**
- `query` (undefined|string|Function):
- `undefined/null` - Returns all records
- `string` - Primary key value
- `Function` - Query function `(record) => boolean`
- `options` (object):
- `mode` (string): Return mode - `'record'`, `'raw'`, or `'mixed'`
**Returns:** Cursor (async iterable)
### update(query, data, options)
Update records matching a query.
```javascript
// Update by primary key
await db.update(0, { age: 31 });
// Update with query function
await db.update(r => r.age > 30, { status: 'senior' });
// Update with callback
await db.update(r => r.age > 30, r => ({ ...r, age: r.age + 1 }));
```
**Parameters:**
- `query` (string|Function): Primary key or query function
- `data` (object|Function): Data to update or callback function
- `options` (object):
- `upsert` (boolean): Insert if no records match
**Returns:** Promise<Object[]> - Array of updated records
### upsert(query, data)
Update records or insert if not found.
```javascript
await db.upsert(r => r.email === 'alice@example.com', {
name: 'Alice',
email: 'alice@example.com',
age: 30
});
```
### delete(query, callback)
Delete records matching a query.
```javascript
// Delete by primary key
await db.delete(0);
// Delete with query function
await db.delete(r => r.age < 18);
// Delete with callback
await db.delete(r => r.age < 18, async (record) => {
console.log('Deleting:', record.name);
return record;
});
```
**Parameters:**
- `query` (string|Function): Primary key or query function
- `callback` (Function): Optional callback for each deleted record
**Returns:** Promise<Object[]> - Array of deleted records
### compact()
Compact the database by removing deleted records. This rewrites the data file without tombstones.
```javascript
await db.compact();
```
**Note:** Compaction happens automatically on database initialization.
### close()
Close the database and persist all pending changes. Should be called before process exit.
```javascript
await db.close();
```
## Primary Key Types
MPackDB supports three primary key types:
### Numeric (Auto-increment)
```javascript
const db = new MPackDB('data/users', {
primaryKey: '*id' // * prefix
});
await db.insert({ name: 'Alice' });
// { id: 0, name: 'Alice' }
```
### UUID
```javascript
const db = new MPackDB('data/sessions', {
primaryKey: '@sessionId' // @ prefix
});
await db.insert({ data: 'session data' });
// { sessionId: 'a1b2c3d4-...', data: 'session data' }
```
### String
```javascript
const db = new MPackDB('data/users', {
primaryKey: 'username' // No prefix
});
await db.insert({ username: 'alice', name: 'Alice' });
// { username: 'alice', name: 'Alice' }
```
## Indexes
Indexes dramatically improve query performance for large datasets.
### Index Types
- **Lexical** (default): String sorting, good for text fields
- **Numeric**: Number sorting, good for integers/floats
- **UUID**: Treated as lexical (string)
### Creating Indexes
```javascript
const db = new MPackDB('data/products', {
primaryKey: '*id',
indexes: [
'category', // Lexical index
'*price', // Numeric index
'*stock', // Numeric index
'@sku' // UUID index (lexical)
]
});
```
### Index Persistence
Indexes are automatically persisted based on:
- **Threshold**: After N changes (default: 1000)
- **Interval**: Every N milliseconds (default: 60000)
- **On close**: When `db.close()` is called
```javascript
const db = new MPackDB('data/users', {
primaryKey: '*id',
indexes: ['email'],
indexPersistThreshold: 100, // Persist after 100 changes
indexPersistInterval: 30000 // Persist every 30 seconds
});
```
## File Structure
MPackDB creates the following files:
```
data/
users.mpack # Main data file (MessagePack binary)
users.meta.json # Metadata (nextId, deleted offsets)
users.id.txt # Primary key index
users.email.txt # Email field index
users.age.txt # Age field index
users.lock # Lock file (temporary)
```
## Concurrency
MPackDB uses file-based locking to ensure safe concurrent access:
```javascript
// Process 1
const db1 = new MPackDB('data/users', { primaryKey: '*id' });
await db1.insert({ name: 'Alice' });
// Process 2 (waits for lock)
const db2 = new MPackDB('data/users', { primaryKey: '*id' });
await db2.insert({ name: 'Bob' });
```
## Performance Tips
1. **Use indexes** for frequently queried fields
2. **Adjust persist thresholds** based on your write patterns
3. **Call `compact()`** periodically if you have many deletes
4. **Use numeric indexes** for number fields
5. **Batch operations** when possible
## Examples
### User Management System
```javascript
import MPackDB from '@worldapi.org/mpackdb';
const users = new MPackDB('data/users', {
primaryKey: '*id',
indexes: ['email', '*age', 'role']
});
// Register user
await users.insert({
email: 'alice@example.com',
name: 'Alice',
age: 30,
role: 'admin'
});
// Find by email
for await (const user of users.find(u => u.email === 'alice@example.com')) {
console.log('Found user:', user.name);
}
// Get all admins
for await (const admin of users.find(u => u.role === 'admin')) {
console.log('Admin:', admin.name);
}
// Update age
await users.update(u => u.email === 'alice@example.com', { age: 31 });
// Delete inactive users
await users.delete(u => u.lastLogin < Date.now() - 90 * 24 * 60 * 60 * 1000);
await users.close();
```
### Product Catalog
```javascript
const products = new MPackDB('data/products', {
primaryKey: '*id',
indexes: ['category', '*price', '@sku']
});
// Add products
await products.insert({ sku: 'ABC-123', name: 'Laptop', category: 'Electronics', price: 999 });
await products.insert({ sku: 'DEF-456', name: 'Mouse', category: 'Electronics', price: 29 });
// Find by category
for await (const product of products.find(p => p.category === 'Electronics')) {
console.log(product.name, product.price);
}
// Find products under $50
for await (const product of products.find(p => p.price < 50)) {
console.log('Affordable:', product.name);
}
// Update price
await products.update(p => p.sku === 'ABC-123', { price: 899 });
await products.close();
```
## License
MIT
## Contributing
Contributions are welcome! Please open an issue or submit a pull request.
## Related Projects
- [**BsonDB**](https://bsondb.gitoria.worldapi.org) - Similar database using BSON serialization