gitoriaLog in with ident

mpackdb

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitcde73eb4cde73eb4release 1.0.6caramboleyocde73eb4/README.md

15.2 KB

  1. # MPackDB
  2. A fast, local, append-only JSON database with [MessagePack](https://msgpack.org/) serialization for Node.js.
  3. ## Features
  4. - **🚀 High Performance** - Append-only writes with MessagePack binary serialization
  5. - **📇 Flexible Indexing** - Optional numeric and lexical indexes for fast queries
  6. - **🔒 Concurrent Access** - File-based locking for safe multi-process access
  7. - **💾 Auto-Persistence** - Configurable automatic index persistence
  8. - **🗜️ Auto-Compaction** - Removes deleted records on startup
  9. - **🎯 Simple API** - Intuitive CRUD operations with async/await
  10. - **📦 Zero Dependencies** - Only requires `msgpackr`
  11. ## Installation
  12. ```bash
  13. npm install mpackdb
  14. ```
  15. ## Quick Start
  16. ```javascript
  17. import MPackDB from 'mpackdb';
  18. // Create a database with auto-increment numeric primary key
  19. const db = new MPackDB('data/users', {
  20. primaryKey: '*id', // * prefix = numeric auto-increment
  21. indexes: ['email', '*age'] // Index email (lexical) and age (numeric)
  22. });
  23. // Insert records
  24. await db.insert({ name: 'Alice', email: '[email protected]', age: 30 });
  25. await db.insert({ name: 'Bob', email: '[email protected]', age: 25 });
  26. // Find all records
  27. for await (const user of db.find()) {
  28. console.log(user);
  29. }
  30. // Find by primary key
  31. const users = await db.find(0); // Returns array with Alice
  32. // Query with function
  33. for await (const user of db.find(r => r.age > 28)) {
  34. console.log(user.name); // Alice
  35. }
  36. // Update records
  37. await db.update(0, { age: 31 });
  38. await db.update(r => r.age < 26, { status: 'junior' });
  39. // Delete records
  40. await db.delete(r => r.age < 18);
  41. // Close database (persists all changes)
  42. await db.close();
  43. ```
  44. ## API Reference
  45. ### Constructor
  46. ```javascript
  47. new MPackDB(dbFile, options)
  48. ```
  49. **Parameters:**
  50. - `dbFile` (string): Path to database file (without extension)
  51. - `options` (object):
  52. - `primaryKey` (string): Primary key field name with optional prefix:
  53. - `*field` - Numeric auto-increment (e.g., `*id`)
  54. - `@field` - UUID (e.g., `@uuid`)
  55. - `field` - String (e.g., `username`)
  56. - `primaryKeyType` (PrimaryKeyType): Explicit type override
  57. - `indexes` (string[]): Fields to index. Prefix options:
  58. - `*field` - Numeric index
  59. - `@field` - UUID index (lexical)
  60. - `!field` - Unique index (rejects duplicates)
  61. - `!*field` - Unique numeric index
  62. - `field` - Lexical index (default)
  63. - `debug` (boolean): Enable debug logging (default: `false`)
  64. - `compact` (boolean): Run compaction on init (default: `true`, set `false` for read-only / secondary instances)
  65. - `indexPersistInterval` (number): Auto-persist interval in ms (default: `60000`, `0` to disable)
  66. - `indexPersistThreshold` (number): Auto-persist after N changes (default: `1000`)
  67. **Examples:**
  68. functional style:
  69. ```javascript
  70. import MPackDB from '@worldapi.org/mpackdb';
  71. const db = new MPackDB('data/products', {
  72. primaryKey: '*id',
  73. indexes: ['category', '*price', '@sku'],
  74. indexPersistThreshold: 100
  75. });
  76. ```
  77. OOP style:
  78. ```javascript
  79. import { MPackDB, Model, PrimaryKeyType } from 'mpackdb';
  80. export class Product extends Model {
  81. id = 0;
  82. name = '';
  83. price = 0;
  84. category = '';
  85. sku = '';
  86. }
  87. export class Products extends MPackDB {
  88. _classToUse = Product;
  89. _primaryKey = 'id';
  90. _primaryKeyType = PrimaryKeyType.UUID;
  91. _indexes = ['category', '*price', '@sku'];
  92. }
  93. export default products = new Products('data/products');
  94. ```
  95. ### insert(record, options)
  96. Insert a new record into the database.
  97. ```javascript
  98. await db.insert({ name: 'Alice', age: 30 });
  99. // Returns: { id: 0, name: 'Alice', age: 30 }
  100. ```
  101. **Parameters:**
  102. - `record` (object): The record to insert
  103. - `options` (object):
  104. - `skipPrimaryKey` (boolean): Don't auto-generate primary key
  105. **Returns:** Promise<Object> - The inserted record with primary key
  106. ### find(query, options)
  107. Find records in the database. Returns an async iterable Cursor.
  108. The Cursor is both an async iterable (for streaming with `for await`) and a thenable (awaiting it returns an array).
  109. ```javascript
  110. // Find all (streaming)
  111. for await (const record of db.find()) {
  112. console.log(record);
  113. }
  114. // Find all (array)
  115. const all = await db.find();
  116. // Find by primary key
  117. const users = await db.find(0);
  118. // Find with filter function (stream)
  119. for await (const user of db.find(r => r.age > 30)) {
  120. console.log(user.name);
  121. }
  122. // Find with single index — only reads records from the index range
  123. for await (const user of db.find(r => r.age <= 50, {
  124. index: { field: 'age', from: 30, to: 50 }
  125. })) {
  126. console.log(user.name);
  127. }
  128. // Find with index (descending)
  129. for await (const user of db.find(null, {
  130. index: { field: 'age', from: 50, direction: 'desc' }
  131. })) {
  132. if (user.age < 18) break;
  133. console.log(user.name);
  134. }
  135. // Find with index intersection — intersects offset sets, then streams only matches
  136. const results = await db.find(null, {
  137. index: [
  138. { field: 'x', from: 0, to: 100 },
  139. { field: 'status', value: 'active' }
  140. ]
  141. });
  142. ```
  143. **Parameters:**
  144. - `query` (undefined|string|number|Function):
  145. - `undefined/null` - Returns all records
  146. - `string/number` - Primary key value
  147. - `Function` - Filter function `(record) => boolean`
  148. - `options` (object):
  149. - `mode` (string): Return mode - `'record'`, `'raw'`, or `'mixed'`
  150. - `index` (object|array): Index hint(s) to narrow disk reads:
  151. - `field` (string): Indexed field name
  152. - `value` (any): Exact match lookup
  153. - `from` (any): Start key (inclusive) for range scan
  154. - `to` (any): End key (inclusive) for range scan
  155. - `direction` ('asc'|'desc'): Scan direction (default: 'asc')
  156. When `index` is an array, offset sets from each index are intersected before streaming records from disk.
  157. **Returns:** Cursor (async iterable + thenable)
  158. ### update(query, data, options)
  159. Update records matching a query.
  160. ```javascript
  161. // Update by primary key
  162. await db.update(0, { age: 31 });
  163. // Update with query function
  164. await db.update(r => r.age > 30, { status: 'senior' });
  165. // Update with callback
  166. await db.update(r => r.age > 30, r => ({ ...r, age: r.age + 1 }));
  167. ```
  168. **Parameters:**
  169. - `query` (string|Function): Primary key or query function
  170. - `data` (object|Function): Data to update or callback function
  171. - `options` (object):
  172. - `upsert` (boolean): Insert if no records match
  173. - `index` (object|array): Index hint(s), same as `find()`
  174. **Returns:** Promise<Object[]> - Array of updated records
  175. ### upsert(query, data, options)
  176. Update records or insert if not found.
  177. ```javascript
  178. await db.upsert(r => r.email === '[email protected]', {
  179. name: 'Alice',
  180. email: '[email protected]',
  181. age: 30
  182. });
  183. ```
  184. **Parameters:**
  185. - `query` (string|Function): Primary key or query function
  186. - `data` (object|Function): Data to update/insert or callback function
  187. - `options` (object):
  188. - `index` (object|array): Index hint(s), same as `find()`
  189. ### delete(query, options)
  190. Delete records matching a query.
  191. ```javascript
  192. // Delete by primary key
  193. await db.delete(0);
  194. // Delete with query function
  195. await db.delete(r => r.age < 18);
  196. // Delete with index hint (avoids full scan)
  197. await db.delete(r => r.status === 'inactive', {
  198. index: { field: 'status', value: 'inactive' }
  199. });
  200. ```
  201. **Parameters:**
  202. - `query` (string|Function): Primary key or query function
  203. - `options` (object):
  204. - `index` (object|array): Index hint(s), same as `find()`
  205. **Returns:** Promise<Object[]> - Array of deleted records
  206. ### compact()
  207. Compact the database by removing deleted records. This rewrites the data file without tombstones.
  208. ```javascript
  209. await db.compact();
  210. ```
  211. **Note:** Compaction happens automatically on database initialization.
  212. ### boundingBox(corners, filter)
  213. Find records within a bounding box defined by 4 corner coordinates. Requires numeric indexes on `x` and `y` fields. Corners can be in any order — min/max are extracted automatically.
  214. ```javascript
  215. const db = new MPackDB('data/sectors', {
  216. primaryKey: '*id',
  217. indexes: ['*x', '*y']
  218. });
  219. // Find all sectors in a bounding box
  220. const sectors = await db.boundingBox([
  221. { x: -5, y: 5 },
  222. { x: 5, y: 5 },
  223. { x: 5, y: -5 },
  224. { x: -5, y: -5 }
  225. ]);
  226. // Streaming
  227. for await (const sector of db.boundingBox([
  228. { x: 0, y: 0 }, { x: 100, y: 0 },
  229. { x: 100, y: 100 }, { x: 0, y: 100 }
  230. ])) {
  231. console.log(sector);
  232. }
  233. // With additional filter
  234. const active = await db.boundingBox([
  235. { x: -5, y: 5 }, { x: 5, y: 5 },
  236. { x: 5, y: -5 }, { x: -5, y: -5 }
  237. ], s => s.status === 'active');
  238. ```
  239. **Parameters:**
  240. - `corners` (Array<{x: number, y: number}>): 4 corner coordinates
  241. - `filter` (Function): Optional additional filter function
  242. **Returns:** Cursor (async iterable + thenable)
  243. ### withLock(callback)
  244. Execute a callback while holding the database lock. The lock is re-entrant: `find`, `insert`, `delete`, `update` called inside the callback reuse the same lock instead of deadlocking.
  245. Use this for compound operations that must be atomic, e.g. find-then-insert.
  246. ```javascript
  247. const user = await db.withLock(async () => {
  248. const [existing] = await db.find(u => u.email === email);
  249. if (existing) return existing;
  250. return db.insert({ email, name });
  251. });
  252. ```
  253. **Parameters:**
  254. - `callback` (Function): Async function to execute under lock
  255. **Returns:** Promise<any> - The return value of the callback
  256. ### close()
  257. Close the database and persist all pending changes. Should be called before process exit.
  258. ```javascript
  259. await db.close();
  260. ```
  261. ## Primary Key Types
  262. MPackDB supports three primary key types:
  263. ### Numeric (Auto-increment)
  264. ```javascript
  265. const db = new MPackDB('data/users', {
  266. primaryKey: '*id' // * prefix
  267. });
  268. await db.insert({ name: 'Alice' });
  269. // { id: 0, name: 'Alice' }
  270. ```
  271. ### UUID
  272. ```javascript
  273. const db = new MPackDB('data/sessions', {
  274. primaryKey: '@sessionId' // @ prefix
  275. });
  276. await db.insert({ data: 'session data' });
  277. // { sessionId: 'lz7gdcwh9x4r', data: 'session data' }
  278. ```
  279. ### String
  280. ```javascript
  281. const db = new MPackDB('data/users', {
  282. primaryKey: 'username' // No prefix
  283. });
  284. await db.insert({ username: 'alice', name: 'Alice' });
  285. // { username: 'alice', name: 'Alice' }
  286. ```
  287. ## Indexes
  288. Indexes dramatically improve query performance for large datasets.
  289. ### Index Types
  290. - **Lexical** (default): String sorting, good for text fields
  291. - **Numeric**: Number sorting, good for integers/floats
  292. - **UUID**: Treated as lexical (string)
  293. - **Unique**: Rejects duplicate values on insert (any type)
  294. ### Creating Indexes
  295. ```javascript
  296. const db = new MPackDB('data/products', {
  297. primaryKey: '*id',
  298. indexes: [
  299. 'category', // Lexical index
  300. '*price', // Numeric index
  301. '*stock', // Numeric index
  302. '@sku', // UUID index (lexical)
  303. '!email' // Unique lexical index
  304. ]
  305. });
  306. ```
  307. ### Unique Indexes
  308. Unique indexes prevent duplicate values. Primary keys are always unique by default. Use the `!` prefix to make secondary indexes unique:
  309. ```javascript
  310. const db = new MPackDB('data/users', {
  311. primaryKey: '*id',
  312. indexes: ['!email', '!username', '*age']
  313. });
  314. await db.insert({ email: '[email protected]', username: 'alice', age: 30 }); // ok
  315. await db.insert({ email: '[email protected]', username: 'bob', age: 25 });
  316. // throws: Error { code: 'DUPLICATE_KEY', field: 'email', value: '[email protected]' }
  317. ```
  318. Combine `!` with type prefixes: `!*field` for unique numeric, `!@field` for unique UUID.
  319. Uniqueness is enforced atomically under the write lock, so concurrent inserts cannot create duplicates.
  320. ### Index Persistence
  321. Indexes are automatically persisted based on:
  322. - **Threshold**: After N changes (default: 1000)
  323. - **Interval**: Every N milliseconds (default: 60000)
  324. - **On close**: When `db.close()` is called
  325. ```javascript
  326. const db = new MPackDB('data/users', {
  327. primaryKey: '*id',
  328. indexes: ['email'],
  329. indexPersistThreshold: 100, // Persist after 100 changes
  330. indexPersistInterval: 30000 // Persist every 30 seconds
  331. });
  332. ```
  333. ## File Structure
  334. MPackDB creates the following files:
  335. ```
  336. data/
  337. users.mpack # Main data file (MessagePack binary)
  338. users.meta.json # Metadata (nextId, deleted offsets)
  339. users.id.txt # Primary key index
  340. users.email.txt # Email field index
  341. users.age.txt # Age field index
  342. users.lock # Lock file (temporary)
  343. ```
  344. ## Concurrency
  345. MPackDB uses file-based locking to ensure safe concurrent access:
  346. ```javascript
  347. // Process 1
  348. const db1 = new MPackDB('data/users', { primaryKey: '*id' });
  349. await db1.insert({ name: 'Alice' });
  350. // Process 2 (waits for lock)
  351. const db2 = new MPackDB('data/users', { primaryKey: '*id' });
  352. await db2.insert({ name: 'Bob' });
  353. ```
  354. ### Multi-process reads
  355. MPackDB automatically re-reads `meta.json` from disk before every read operation (`refresh()`). This means a second process (e.g. an admin UI) opening the same database files will always see the latest state, including records deleted by the main application. Without this, the in-memory `_meta.deleted` array would go stale and deleted records would reappear in query results.
  356. You can also call `db.refresh()` manually if needed.
  357. **Important:** Secondary/read-only instances should disable compaction with `compact: false`. Compaction replaces the data file via `rename()`, which invalidates the write stream of any other instance pointing at the old file inode.
  358. ```javascript
  359. // Main app — owns the data, compacts on startup (default)
  360. const db = new MPackDB('data/users', { primaryKey: '*id' });
  361. // Admin UI or secondary reader — no compaction
  362. const admin = new MPackDB('data/users', { primaryKey: '*id', compact: false });
  363. ```
  364. ## Performance Tips
  365. 1. **Use indexes** for frequently queried fields
  366. 2. **Adjust persist thresholds** based on your write patterns
  367. 3. **Call `compact()`** periodically if you have many deletes
  368. 4. **Use numeric indexes** for number fields
  369. 5. **Batch operations** when possible
  370. ## Examples
  371. ### User Management System
  372. ```javascript
  373. import MPackDB from '@worldapi.org/mpackdb';
  374. const users = new MPackDB('data/users', {
  375. primaryKey: '*id',
  376. indexes: ['email', '*age', 'role']
  377. });
  378. // Register user
  379. await users.insert({
  380. email: '[email protected]',
  381. name: 'Alice',
  382. age: 30,
  383. role: 'admin'
  384. });
  385. // Find by email
  386. for await (const user of users.find(u => u.email === '[email protected]')) {
  387. console.log('Found user:', user.name);
  388. }
  389. // Get all admins
  390. for await (const admin of users.find(u => u.role === 'admin')) {
  391. console.log('Admin:', admin.name);
  392. }
  393. // Update age
  394. await users.update(u => u.email === '[email protected]', { age: 31 });
  395. // Delete inactive users
  396. await users.delete(u => u.lastLogin < Date.now() - 90 * 24 * 60 * 60 * 1000);
  397. await users.close();
  398. ```
  399. ### Product Catalog
  400. ```javascript
  401. const products = new MPackDB('data/products', {
  402. primaryKey: '*id',
  403. indexes: ['category', '*price', '@sku']
  404. });
  405. // Add products
  406. await products.insert({ sku: 'ABC-123', name: 'Laptop', category: 'Electronics', price: 999 });
  407. await products.insert({ sku: 'DEF-456', name: 'Mouse', category: 'Electronics', price: 29 });
  408. // Find by category
  409. for await (const product of products.find(p => p.category === 'Electronics')) {
  410. console.log(product.name, product.price);
  411. }
  412. // Find products under $50
  413. for await (const product of products.find(p => p.price < 50)) {
  414. console.log('Affordable:', product.name);
  415. }
  416. // Update price
  417. await products.update(p => p.sku === 'ABC-123', { price: 899 });
  418. await products.close();
  419. ```
  420. ## License
  421. MIT
  422. ## Contributing
  423. Contributions are welcome! Please open an issue or submit a pull request.
  424. ## Related Projects
  425. - [**BsonDB**](https://bsondb.gitoria.worldapi.org) - Similar database using BSON serialization

Branches

Latest commits

  • cde73eb4release 1.0.6caramboleyo
  • d01dda02add index hints, intersection, boundingBox; remove findByIndexcaramboleyo
  • b8ffc1a0release 1.0.5caramboleyo
  • d47876a1reimplemented lost features like indexed find and more testscaramboleyo
  • 7f08da9afixed insert ignoring model definitioncaramboleyo
  • 705774a9added flush before findcaramboleyo
  • b4db6391initial commitcaramboleyo