gitoriaLog in with ident

mpackdb

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitd47876a1d47876a1reimplemented lost features like indexed find and more testscaramboleyod47876a1/README.md

13.5 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. ```javascript
  109. // Find all
  110. for await (const record of db.find()) {
  111. console.log(record);
  112. }
  113. // Find by primary key
  114. const users = await db.find(0);
  115. // Find with query function
  116. for await (const user of db.find(r => r.age > 30)) {
  117. console.log(user.name);
  118. }
  119. ```
  120. **Parameters:**
  121. - `query` (undefined|string|Function):
  122. - `undefined/null` - Returns all records
  123. - `string` - Primary key value
  124. - `Function` - Query function `(record) => boolean`
  125. - `options` (object):
  126. - `mode` (string): Return mode - `'record'`, `'raw'`, or `'mixed'`
  127. **Returns:** Cursor (async iterable)
  128. ### update(query, data, options)
  129. Update records matching a query.
  130. ```javascript
  131. // Update by primary key
  132. await db.update(0, { age: 31 });
  133. // Update with query function
  134. await db.update(r => r.age > 30, { status: 'senior' });
  135. // Update with callback
  136. await db.update(r => r.age > 30, r => ({ ...r, age: r.age + 1 }));
  137. ```
  138. **Parameters:**
  139. - `query` (string|Function): Primary key or query function
  140. - `data` (object|Function): Data to update or callback function
  141. - `options` (object):
  142. - `upsert` (boolean): Insert if no records match
  143. **Returns:** Promise<Object[]> - Array of updated records
  144. ### upsert(query, data)
  145. Update records or insert if not found.
  146. ```javascript
  147. await db.upsert(r => r.email === '[email protected]', {
  148. name: 'Alice',
  149. email: '[email protected]',
  150. age: 30
  151. });
  152. ```
  153. ### delete(query, callback)
  154. Delete records matching a query.
  155. ```javascript
  156. // Delete by primary key
  157. await db.delete(0);
  158. // Delete with query function
  159. await db.delete(r => r.age < 18);
  160. // Delete with callback
  161. await db.delete(r => r.age < 18, async (record) => {
  162. console.log('Deleting:', record.name);
  163. return record;
  164. });
  165. ```
  166. **Parameters:**
  167. - `query` (string|Function): Primary key or query function
  168. - `callback` (Function): Optional callback for each deleted record
  169. **Returns:** Promise<Object[]> - Array of deleted records
  170. ### compact()
  171. Compact the database by removing deleted records. This rewrites the data file without tombstones.
  172. ```javascript
  173. await db.compact();
  174. ```
  175. **Note:** Compaction happens automatically on database initialization.
  176. ### findByIndex(field, options)
  177. Walk an index in sorted order. Returns an async iterable Cursor. Streams block by block from disk — does not load the full index into memory.
  178. ```javascript
  179. // All users sorted by age
  180. for await (const user of db.findByIndex('age')) {
  181. console.log(user.name, user.age);
  182. }
  183. // Age 25+, first 10
  184. for await (const user of db.findByIndex('age', { from: 25, limit: 10 })) {
  185. console.log(user.name);
  186. }
  187. // Age 18+, only admins, max 5
  188. const admins = await db.findByIndex('age', {
  189. from: 18,
  190. filter: u => u.role === 'admin',
  191. limit: 5
  192. });
  193. ```
  194. **Parameters:**
  195. - `field` (string): Indexed field name
  196. - `options` (object):
  197. - `from` (any): Start key (inclusive), uses binary search to skip ahead
  198. - `limit` (number): Max records to return
  199. - `filter` (Function): Filter applied per record during iteration
  200. **Returns:** Cursor (async iterable)
  201. ### withLock(callback)
  202. 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.
  203. Use this for compound operations that must be atomic, e.g. find-then-insert.
  204. ```javascript
  205. const user = await db.withLock(async () => {
  206. const [existing] = await db.find(u => u.email === email);
  207. if (existing) return existing;
  208. return db.insert({ email, name });
  209. });
  210. ```
  211. **Parameters:**
  212. - `callback` (Function): Async function to execute under lock
  213. **Returns:** Promise<any> - The return value of the callback
  214. ### close()
  215. Close the database and persist all pending changes. Should be called before process exit.
  216. ```javascript
  217. await db.close();
  218. ```
  219. ## Primary Key Types
  220. MPackDB supports three primary key types:
  221. ### Numeric (Auto-increment)
  222. ```javascript
  223. const db = new MPackDB('data/users', {
  224. primaryKey: '*id' // * prefix
  225. });
  226. await db.insert({ name: 'Alice' });
  227. // { id: 0, name: 'Alice' }
  228. ```
  229. ### UUID
  230. ```javascript
  231. const db = new MPackDB('data/sessions', {
  232. primaryKey: '@sessionId' // @ prefix
  233. });
  234. await db.insert({ data: 'session data' });
  235. // { sessionId: 'lz7gdcwh9x4r', data: 'session data' }
  236. ```
  237. ### String
  238. ```javascript
  239. const db = new MPackDB('data/users', {
  240. primaryKey: 'username' // No prefix
  241. });
  242. await db.insert({ username: 'alice', name: 'Alice' });
  243. // { username: 'alice', name: 'Alice' }
  244. ```
  245. ## Indexes
  246. Indexes dramatically improve query performance for large datasets.
  247. ### Index Types
  248. - **Lexical** (default): String sorting, good for text fields
  249. - **Numeric**: Number sorting, good for integers/floats
  250. - **UUID**: Treated as lexical (string)
  251. - **Unique**: Rejects duplicate values on insert (any type)
  252. ### Creating Indexes
  253. ```javascript
  254. const db = new MPackDB('data/products', {
  255. primaryKey: '*id',
  256. indexes: [
  257. 'category', // Lexical index
  258. '*price', // Numeric index
  259. '*stock', // Numeric index
  260. '@sku', // UUID index (lexical)
  261. '!email' // Unique lexical index
  262. ]
  263. });
  264. ```
  265. ### Unique Indexes
  266. Unique indexes prevent duplicate values. Primary keys are always unique by default. Use the `!` prefix to make secondary indexes unique:
  267. ```javascript
  268. const db = new MPackDB('data/users', {
  269. primaryKey: '*id',
  270. indexes: ['!email', '!username', '*age']
  271. });
  272. await db.insert({ email: '[email protected]', username: 'alice', age: 30 }); // ok
  273. await db.insert({ email: '[email protected]', username: 'bob', age: 25 });
  274. // throws: Error { code: 'DUPLICATE_KEY', field: 'email', value: '[email protected]' }
  275. ```
  276. Combine `!` with type prefixes: `!*field` for unique numeric, `!@field` for unique UUID.
  277. Uniqueness is enforced atomically under the write lock, so concurrent inserts cannot create duplicates.
  278. ### Index Persistence
  279. Indexes are automatically persisted based on:
  280. - **Threshold**: After N changes (default: 1000)
  281. - **Interval**: Every N milliseconds (default: 60000)
  282. - **On close**: When `db.close()` is called
  283. ```javascript
  284. const db = new MPackDB('data/users', {
  285. primaryKey: '*id',
  286. indexes: ['email'],
  287. indexPersistThreshold: 100, // Persist after 100 changes
  288. indexPersistInterval: 30000 // Persist every 30 seconds
  289. });
  290. ```
  291. ## File Structure
  292. MPackDB creates the following files:
  293. ```
  294. data/
  295. users.mpack # Main data file (MessagePack binary)
  296. users.meta.json # Metadata (nextId, deleted offsets)
  297. users.id.txt # Primary key index
  298. users.email.txt # Email field index
  299. users.age.txt # Age field index
  300. users.lock # Lock file (temporary)
  301. ```
  302. ## Concurrency
  303. MPackDB uses file-based locking to ensure safe concurrent access:
  304. ```javascript
  305. // Process 1
  306. const db1 = new MPackDB('data/users', { primaryKey: '*id' });
  307. await db1.insert({ name: 'Alice' });
  308. // Process 2 (waits for lock)
  309. const db2 = new MPackDB('data/users', { primaryKey: '*id' });
  310. await db2.insert({ name: 'Bob' });
  311. ```
  312. ### Multi-process reads
  313. 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.
  314. You can also call `db.refresh()` manually if needed.
  315. **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.
  316. ```javascript
  317. // Main app — owns the data, compacts on startup (default)
  318. const db = new MPackDB('data/users', { primaryKey: '*id' });
  319. // Admin UI or secondary reader — no compaction
  320. const admin = new MPackDB('data/users', { primaryKey: '*id', compact: false });
  321. ```
  322. ## Performance Tips
  323. 1. **Use indexes** for frequently queried fields
  324. 2. **Adjust persist thresholds** based on your write patterns
  325. 3. **Call `compact()`** periodically if you have many deletes
  326. 4. **Use numeric indexes** for number fields
  327. 5. **Batch operations** when possible
  328. ## Examples
  329. ### User Management System
  330. ```javascript
  331. import MPackDB from '@worldapi.org/mpackdb';
  332. const users = new MPackDB('data/users', {
  333. primaryKey: '*id',
  334. indexes: ['email', '*age', 'role']
  335. });
  336. // Register user
  337. await users.insert({
  338. email: '[email protected]',
  339. name: 'Alice',
  340. age: 30,
  341. role: 'admin'
  342. });
  343. // Find by email
  344. for await (const user of users.find(u => u.email === '[email protected]')) {
  345. console.log('Found user:', user.name);
  346. }
  347. // Get all admins
  348. for await (const admin of users.find(u => u.role === 'admin')) {
  349. console.log('Admin:', admin.name);
  350. }
  351. // Update age
  352. await users.update(u => u.email === '[email protected]', { age: 31 });
  353. // Delete inactive users
  354. await users.delete(u => u.lastLogin < Date.now() - 90 * 24 * 60 * 60 * 1000);
  355. await users.close();
  356. ```
  357. ### Product Catalog
  358. ```javascript
  359. const products = new MPackDB('data/products', {
  360. primaryKey: '*id',
  361. indexes: ['category', '*price', '@sku']
  362. });
  363. // Add products
  364. await products.insert({ sku: 'ABC-123', name: 'Laptop', category: 'Electronics', price: 999 });
  365. await products.insert({ sku: 'DEF-456', name: 'Mouse', category: 'Electronics', price: 29 });
  366. // Find by category
  367. for await (const product of products.find(p => p.category === 'Electronics')) {
  368. console.log(product.name, product.price);
  369. }
  370. // Find products under $50
  371. for await (const product of products.find(p => p.price < 50)) {
  372. console.log('Affordable:', product.name);
  373. }
  374. // Update price
  375. await products.update(p => p.sku === 'ABC-123', { price: 899 });
  376. await products.close();
  377. ```
  378. ## License
  379. MIT
  380. ## Contributing
  381. Contributions are welcome! Please open an issue or submit a pull request.
  382. ## Related Projects
  383. - [**BsonDB**](https://bsondb.gitoria.worldapi.org) - Similar database using BSON serialization

Branches

Latest commits

  • d47876a1reimplemented lost features like indexed find and more testscaramboleyo
  • 7f08da9afixed insert ignoring model definitioncaramboleyo
  • 705774a9added flush before findcaramboleyo
  • b4db6391initial commitcaramboleyo