mpackdb
All repositories: gitoria
10.0 KB
# MPackDBA 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```bashnpm install mpackdb```## Quick Start```javascriptimport MPackDB from 'mpackdb';// Create a database with auto-increment numeric primary keyconst db = new MPackDB('data/users', {primaryKey: '*id', // * prefix = numeric auto-incrementindexes: ['email', '*age'] // Index email (lexical) and age (numeric)});// Insert recordsawait db.insert({ name: 'Alice', email: '[email protected]', age: 30 });await db.insert({ name: 'Bob', email: '[email protected]', age: 25 });// Find all recordsfor await (const user of db.find()) {console.log(user);}// Find by primary keyconst users = await db.find(0); // Returns array with Alice// Query with functionfor await (const user of db.find(r => r.age > 28)) {console.log(user.name); // Alice}// Update recordsawait db.update(0, { age: 31 });await db.update(r => r.age < 26, { status: 'junior' });// Delete recordsawait db.delete(r => r.age < 18);// Close database (persists all changes)await db.close();```## API Reference### Constructor```javascriptnew 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:```javascriptimport MPackDB from '@worldapi.org/mpackdb';const db = new MPackDB('data/products', {primaryKey: '*id',indexes: ['category', '*price', '@sku'],indexPersistThreshold: 100});```OOP style:```javascriptimport { MPackDB, Model, PrimaryKeyType } from 'mpackdb';export class Product extends Model {id = 0;name = '';price = 0;category = '';sku = '';}export 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.```javascriptawait 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 allfor await (const record of db.find()) {console.log(record);}// Find by primary keyconst users = await db.find(0);// Find with query functionfor 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 keyawait db.update(0, { age: 31 });// Update with query functionawait db.update(r => r.age > 30, { status: 'senior' });// Update with callbackawait 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.```javascriptawait db.upsert(r => r.email === '[email protected]', {name: 'Alice',email: '[email protected]',age: 30});```### delete(query, callback)Delete records matching a query.```javascript// Delete by primary keyawait db.delete(0);// Delete with query functionawait db.delete(r => r.age < 18);// Delete with callbackawait 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.```javascriptawait db.compact();```**Note:** Compaction happens automatically on database initialization.### close()Close the database and persist all pending changes. Should be called before process exit.```javascriptawait db.close();```## Primary Key TypesMPackDB supports three primary key types:### Numeric (Auto-increment)```javascriptconst db = new MPackDB('data/users', {primaryKey: '*id' // * prefix});await db.insert({ name: 'Alice' });// { id: 0, name: 'Alice' }```### UUID```javascriptconst db = new MPackDB('data/sessions', {primaryKey: '@sessionId' // @ prefix});await db.insert({ data: 'session data' });// { sessionId: 'lz7gdcwh9x4r', data: 'session data' }```### String```javascriptconst db = new MPackDB('data/users', {primaryKey: 'username' // No prefix});await db.insert({ username: 'alice', name: 'Alice' });// { username: 'alice', name: 'Alice' }```## IndexesIndexes 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```javascriptconst db = new MPackDB('data/products', {primaryKey: '*id',indexes: ['category', // Lexical index'*price', // Numeric index'*stock', // Numeric index'@sku' // UUID index (lexical)]});```### Index PersistenceIndexes are automatically persisted based on:- **Threshold**: After N changes (default: 1000)- **Interval**: Every N milliseconds (default: 60000)- **On close**: When `db.close()` is called```javascriptconst db = new MPackDB('data/users', {primaryKey: '*id',indexes: ['email'],indexPersistThreshold: 100, // Persist after 100 changesindexPersistInterval: 30000 // Persist every 30 seconds});```## File StructureMPackDB creates the following files:```data/users.mpack # Main data file (MessagePack binary)users.meta.json # Metadata (nextId, deleted offsets)users.id.txt # Primary key indexusers.email.txt # Email field indexusers.age.txt # Age field indexusers.lock # Lock file (temporary)```## ConcurrencyMPackDB uses file-based locking to ensure safe concurrent access:```javascript// Process 1const 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 Tips1. **Use indexes** for frequently queried fields2. **Adjust persist thresholds** based on your write patterns3. **Call `compact()`** periodically if you have many deletes4. **Use numeric indexes** for number fields5. **Batch operations** when possible## Examples### User Management System```javascriptimport MPackDB from '@worldapi.org/mpackdb';const users = new MPackDB('data/users', {primaryKey: '*id',indexes: ['email', '*age', 'role']});// Register userawait users.insert({email: '[email protected]',name: 'Alice',age: 30,role: 'admin'});// Find by emailfor await (const user of users.find(u => u.email === '[email protected]')) {console.log('Found user:', user.name);}// Get all adminsfor await (const admin of users.find(u => u.role === 'admin')) {console.log('Admin:', admin.name);}// Update ageawait users.update(u => u.email === '[email protected]', { age: 31 });// Delete inactive usersawait users.delete(u => u.lastLogin < Date.now() - 90 * 24 * 60 * 60 * 1000);await users.close();```### Product Catalog```javascriptconst products = new MPackDB('data/products', {primaryKey: '*id',indexes: ['category', '*price', '@sku']});// Add productsawait 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 categoryfor await (const product of products.find(p => p.category === 'Electronics')) {console.log(product.name, product.price);}// Find products under $50for await (const product of products.find(p => p.price < 50)) {console.log('Affordable:', product.name);}// Update priceawait products.update(p => p.sku === 'ABC-123', { price: 899 });await products.close();```## LicenseMIT## ContributingContributions are welcome! Please open an issue or submit a pull request.## Related Projects- [**BsonDB**](https://bsondb.gitoria.worldapi.org) - Similar database using BSON serialization
Branches
- mastermain branch
Latest commits
- b4db6391initial commitcaramboleyo