deepbase-sqlite 3.4.12 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,7 +27,8 @@ import SqliteDriver from 'deepbase-sqlite';
27
27
 
28
28
  const db = new DeepBase(new SqliteDriver({
29
29
  path: './data',
30
- name: 'mydb'
30
+ name: 'mydb',
31
+ pragma: 'balanced' // default — omit for same result
31
32
  }));
32
33
 
33
34
  await db.connect();
@@ -42,6 +43,7 @@ const alice = await db.get('users', 'alice');
42
43
  new SqliteDriver({
43
44
  path: './data', // Directory to store database files
44
45
  name: 'default', // Database filename (without .db)
46
+ pragma: 'balanced', // Performance profile: 'none' | 'safe' | 'balanced' | 'fast'
45
47
  nidAlphabet: 'ABC...', // Alphabet for ID generation
46
48
  nidLength: 10 // Length of generated IDs
47
49
  })
@@ -84,15 +86,56 @@ SQLite provides:
84
86
  - **Isolation**: Concurrent operations don't interfere
85
87
  - **Durability**: Committed data persists even after crashes
86
88
 
89
+ ## Pragma Modes
90
+
91
+ `SqliteDriver` ships with four configurable performance profiles via the `pragma` option:
92
+
93
+ | Mode | `synchronous` | `cache_size` | `mmap_size` | `WITHOUT ROWID` | Use case |
94
+ |------|--------------|-------------|-------------|-----------------|----------|
95
+ | **none** | — | — | — | No | Backward-compatible with databases created by older versions |
96
+ | **safe** | FULL | 2 MB | off | Yes | Durability first, WAL + full fsync |
97
+ | **balanced** *(default)* | NORMAL | 8 MB | 256 MB | Yes | Best mix of speed and safety for most apps |
98
+ | **fast** | OFF | 16 MB | 256 MB | Yes | Maximum throughput — data may be lost on OS crash |
99
+
100
+ All WAL modes use `journal_mode=WAL`, `temp_store=MEMORY`, and `busy_timeout=5000ms`.
101
+
102
+ ```javascript
103
+ // Backward-compatible (no PRAGMAs, no WITHOUT ROWID)
104
+ new SqliteDriver({ name: 'mydb', pragma: 'none' })
105
+
106
+ // Maximum durability
107
+ new SqliteDriver({ name: 'mydb', pragma: 'safe' })
108
+
109
+ // Recommended (default)
110
+ new SqliteDriver({ name: 'mydb', pragma: 'balanced' })
111
+
112
+ // Maximum throughput
113
+ new SqliteDriver({ name: 'mydb', pragma: 'fast' })
114
+ ```
115
+
116
+ `SqliteFastDriver` is kept as a named alias for backward compatibility:
117
+
118
+ ```javascript
119
+ import { SqliteFastDriver } from 'deepbase-sqlite';
120
+ // SqliteFastDriver === SqliteDriver (same class, same options)
121
+ ```
122
+
87
123
  ## Database Structure
88
124
 
89
125
  Data is stored in a simple key-value table:
90
126
 
91
127
  ```sql
128
+ -- pragma: 'none' (legacy-compatible)
92
129
  CREATE TABLE deepbase (
93
130
  key TEXT PRIMARY KEY,
94
131
  value TEXT NOT NULL
95
132
  )
133
+
134
+ -- pragma: 'safe' | 'balanced' | 'fast' (optimized)
135
+ CREATE TABLE deepbase (
136
+ key TEXT PRIMARY KEY,
137
+ value TEXT NOT NULL
138
+ ) WITHOUT ROWID
96
139
  ```
97
140
 
98
141
  Example data:
@@ -167,6 +210,27 @@ data/
167
210
  | Reliability | 💪 Very High | ⚠️ File corruption risk |
168
211
  | Debugging | 🔧 SQL tools | 👁️ Easy to inspect |
169
212
 
213
+ ## Benchmark — pragma modes
214
+
215
+ Median of 3 runs, 1 000 iterations per operation. `balanced` vs `none`:
216
+
217
+ ```
218
+ Operation none safe balanced fast Bal vs None
219
+ ─────────────────────────────────────────────────────────────────────────
220
+ Sequential Write 4,525 17,296 84,701 98,238 +1772%
221
+ Sequential Read 20,705 22,871 23,405 23,040 +13%
222
+ Update 2,689 10,666 17,764 18,857 +561%
223
+ Increment 4,291 14,129 25,734 26,364 +500%
224
+ Delete 3,806 12,010 20,884 21,074 +449%
225
+ Batch Write 3,813 17,763 87,209 83,045 +2187%
226
+ Concurrent Write 4,449 22,196 72,613 102,458 +1532%
227
+ Deep Write (5-lvl) 4,311 26,740 53,957 91,312 +1152%
228
+ Obj Expansion 1,997 365 34,633 47,318 +1634%
229
+ Session Lifecycle 572 2,465 3,700 3,792 +546%
230
+ ```
231
+
232
+ Disk usage is **29 % smaller** after compaction compared to `none`. All correctness checks pass on every mode.
233
+
170
234
  ## Best Practices
171
235
 
172
236
  ### Use Transactions for Bulk Operations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepbase-sqlite",
3
- "version": "3.4.12",
3
+ "version": "3.5.1",
4
4
  "description": "⚡ DeepBase SQLite - SQLite database driver",
5
5
  "type": "module",
6
6
  "main": "src/index.cjs",
@@ -17,7 +17,7 @@
17
17
  "better-sqlite3": "^11.8.1"
18
18
  },
19
19
  "peerDependencies": {
20
- "deepbase": "^3.4.12"
20
+ "deepbase": "^3.5.1"
21
21
  },
22
22
  "scripts": {
23
23
  "test": "mocha test/test.js"
@@ -3,54 +3,119 @@ import Database from 'better-sqlite3';
3
3
  import fs from 'fs';
4
4
  import * as pathModule from 'path';
5
5
 
6
+ const PRAGMA = {
7
+ none: null,
8
+ safe: {
9
+ journal_mode: 'WAL',
10
+ synchronous: 'FULL',
11
+ temp_store: 'MEMORY',
12
+ cache_size: -2000,
13
+ busy_timeout: 5000,
14
+ mmap_size: 0,
15
+ },
16
+ balanced: {
17
+ journal_mode: 'WAL',
18
+ synchronous: 'NORMAL',
19
+ temp_store: 'MEMORY',
20
+ cache_size: -8000,
21
+ busy_timeout: 5000,
22
+ mmap_size: 268435456,
23
+ },
24
+ fast: {
25
+ journal_mode: 'WAL',
26
+ synchronous: 'OFF',
27
+ temp_store: 'MEMORY',
28
+ cache_size: -16000,
29
+ busy_timeout: 5000,
30
+ mmap_size: 268435456,
31
+ },
32
+ };
33
+
6
34
  export class SqliteDriver extends DeepBaseDriver {
7
35
  static _instances = {};
8
-
9
- constructor({name, path, ...opts} = {}) {
36
+
37
+ constructor({ name, path, pragma, ...opts } = {}) {
10
38
  super(opts);
11
-
12
- this.name = name || "default";
39
+
40
+ this.name = name || 'default';
13
41
  this.path = path || new URL('../../../db', import.meta.url).pathname;
14
-
42
+ this.pragma = pragma || 'balanced';
43
+
15
44
  this.path = pathModule.resolve(this.path);
16
45
  this.fileName = pathModule.join(this.path, `${this.name}.db`);
17
-
18
- // Singleton pattern per file
46
+
19
47
  if (SqliteDriver._instances[this.fileName]) {
20
48
  return SqliteDriver._instances[this.fileName];
21
49
  }
22
-
50
+
23
51
  this.db = null;
24
52
  SqliteDriver._instances[this.fileName] = this;
25
53
  }
26
-
54
+
27
55
  async connect() {
28
56
  if (this._connected) return;
29
-
57
+
30
58
  if (!fs.existsSync(this.path)) {
31
59
  fs.mkdirSync(this.path, { recursive: true });
32
60
  }
33
-
61
+
34
62
  this.db = new Database(this.fileName);
35
-
36
- // Create table if it doesn't exist
63
+
64
+ const cfg = PRAGMA[this.pragma];
65
+ if (cfg) {
66
+ this.db.pragma(`journal_mode = ${cfg.journal_mode}`);
67
+ this.db.pragma(`synchronous = ${cfg.synchronous}`);
68
+ this.db.pragma(`temp_store = ${cfg.temp_store}`);
69
+ this.db.pragma(`cache_size = ${cfg.cache_size}`);
70
+ this.db.pragma(`busy_timeout = ${cfg.busy_timeout}`);
71
+ this.db.pragma(`mmap_size = ${cfg.mmap_size}`);
72
+ }
73
+
74
+ const withoutRowid = cfg ? ' WITHOUT ROWID' : '';
37
75
  this.db.exec(`
38
76
  CREATE TABLE IF NOT EXISTS deepbase (
39
77
  key TEXT PRIMARY KEY,
40
78
  value TEXT NOT NULL
41
- )
79
+ )${withoutRowid}
42
80
  `);
43
-
44
- // Prepare statements for better performance
81
+
45
82
  this.getStmt = this.db.prepare('SELECT value FROM deepbase WHERE key = ?');
46
83
  this.setStmt = this.db.prepare('INSERT OR REPLACE INTO deepbase (key, value) VALUES (?, ?)');
47
84
  this.delStmt = this.db.prepare('DELETE FROM deepbase WHERE key = ?');
48
85
  this.getAllStmt = this.db.prepare('SELECT key, value FROM deepbase');
49
- this.getKeysLikeStmt = this.db.prepare('SELECT key, value FROM deepbase WHERE key LIKE ? ESCAPE \'!\'');
50
-
86
+ this.getKeysLikeStmt = this.db.prepare("SELECT key, value FROM deepbase WHERE key LIKE ? ESCAPE '!'");
87
+ this.delChildrenStmt = this.db.prepare("DELETE FROM deepbase WHERE key LIKE ? ESCAPE '!'");
88
+ this.hasChildrenStmt = this.db.prepare("SELECT 1 FROM deepbase WHERE key LIKE ? ESCAPE '!' LIMIT 1");
89
+
90
+ this._setTxn = this.db.transaction((key, jsonValue, keys) => {
91
+ this._expandParentObjects(keys);
92
+ this.setStmt.run(key, jsonValue);
93
+ });
94
+
95
+ this._delTxn = this.db.transaction((key, likePattern) => {
96
+ this.delStmt.run(key);
97
+ this.delChildrenStmt.run(likePattern);
98
+ });
99
+
100
+ this._updTxn = this.db.transaction((keys, func) => {
101
+ const currentValue = this._getSync(keys);
102
+ const newValue = func(currentValue);
103
+ const key = this._pathToKey(keys);
104
+ this._expandParentObjects(keys);
105
+ this.setStmt.run(key, JSON.stringify(newValue));
106
+ return keys;
107
+ });
108
+
109
+ this._setRootTxn = this.db.transaction((entries) => {
110
+ this.db.exec('DELETE FROM deepbase');
111
+ for (const [key, value] of entries) {
112
+ this.setStmt.run(key, JSON.stringify(value));
113
+ }
114
+ });
115
+
51
116
  this._connected = true;
52
117
  }
53
-
118
+
54
119
  async disconnect() {
55
120
  if (this.db) {
56
121
  this.db.close();
@@ -58,178 +123,119 @@ export class SqliteDriver extends DeepBaseDriver {
58
123
  }
59
124
  this._connected = false;
60
125
  }
61
-
126
+
62
127
  async get(...args) {
63
128
  return this._getSync(args);
64
129
  }
65
-
130
+
66
131
  _getSync(args) {
67
132
  if (args.length === 0) {
68
- // Get root object
69
133
  return this._getRootObject();
70
134
  }
71
-
135
+
72
136
  const key = this._pathToKey(args);
73
137
  const row = this.getStmt.get(key);
74
-
75
- // Check if there are child keys (nested properties)
76
- const children = this.getKeysLikeStmt.all(this._likePrefix(key));
77
-
78
- // If there are children, build object from them
79
- if (children.length > 0) {
80
- return this._buildObjectFromChildren(key);
81
- }
82
-
83
- // If direct key exists and no children, return it
138
+ const likePattern = this._likePrefix(key);
139
+
84
140
  if (row) {
141
+ if (this.hasChildrenStmt.get(likePattern)) {
142
+ return this._buildObjectFromChildren(key, likePattern);
143
+ }
85
144
  return JSON.parse(row.value);
86
145
  }
87
-
88
- // Check if we need to look into a parent object
89
- // For example: if we're looking for 'users.abc.name' but only 'users.abc' exists as a JSON object
90
- const parentPath = this._findParentWithValue(args);
91
- if (parentPath) {
92
- const parentKey = this._pathToKey(parentPath);
93
- const parentRow = this.getStmt.get(parentKey);
94
- if (parentRow) {
95
- const parentValue = JSON.parse(parentRow.value);
96
- const relativePath = args.slice(parentPath.length);
97
- return this._getFromObject(parentValue, relativePath);
98
- }
146
+
147
+ if (this.hasChildrenStmt.get(likePattern)) {
148
+ return this._buildObjectFromChildren(key, likePattern);
99
149
  }
100
-
101
- return null;
150
+
151
+ return this._getFromParent(args);
102
152
  }
103
-
153
+
104
154
  async set(...args) {
105
155
  if (args.length === 0) {
106
156
  throw new Error('set() requires at least one argument');
107
157
  }
108
-
158
+
109
159
  if (args.length === 1) {
110
- // Setting root object
111
160
  await this._setRootObject(args[0]);
112
161
  return [];
113
162
  }
114
-
163
+
115
164
  return this._setSync(args);
116
165
  }
117
-
166
+
118
167
  _setSync(args) {
119
168
  const keys = args.slice(0, -1);
120
169
  const value = args[args.length - 1];
121
170
  const key = this._pathToKey(keys);
122
-
123
- // Use transaction to make expansion and set atomic
124
- const transaction = this.db.transaction(() => {
125
- // Check if any parent path exists as an object that needs to be expanded
126
- this._expandParentObjects(keys);
127
-
128
- this.setStmt.run(key, JSON.stringify(value));
129
- });
130
-
131
- transaction();
171
+ this._setTxn(key, JSON.stringify(value), keys);
132
172
  return keys;
133
173
  }
134
-
174
+
135
175
  async del(...keys) {
136
176
  if (keys.length === 0) {
137
- // Delete everything
138
177
  this.db.exec('DELETE FROM deepbase');
139
178
  return;
140
179
  }
141
-
180
+
142
181
  const key = this._pathToKey(keys);
143
-
144
- // Use transaction to make deletion atomic
145
- const transaction = this.db.transaction(() => {
146
- // Delete the key itself
147
- this.delStmt.run(key);
148
-
149
- // Delete all children
150
- this.db.prepare('DELETE FROM deepbase WHERE key LIKE ? ESCAPE \'!\'').run(this._likePrefix(key));
151
- });
152
-
153
- transaction();
182
+ this._delTxn(key, this._likePrefix(key));
154
183
  }
155
-
184
+
156
185
  async inc(...args) {
157
186
  const i = args.pop();
158
187
  return this.upd(...args, n => n + i);
159
188
  }
160
-
189
+
161
190
  async dec(...args) {
162
191
  const i = args.pop();
163
192
  return this.upd(...args, n => n - i);
164
193
  }
165
-
194
+
166
195
  async add(...keys) {
167
196
  const value = keys.pop();
168
197
  const id = this.nanoid();
169
198
  await this.set(...[...keys, id], value);
170
199
  return [...keys, id];
171
200
  }
172
-
201
+
173
202
  async upd(...args) {
174
203
  const func = args.pop();
175
204
  const keys = args;
176
-
177
- // Use transaction to make get+set atomic
178
- const transaction = this.db.transaction(() => {
179
- const currentValue = this._getSync(keys);
180
- const newValue = func(currentValue);
181
- this._setSync([...keys, newValue]);
182
- return keys;
183
- });
184
-
185
- return transaction();
205
+ return this._updTxn(keys, func);
186
206
  }
187
-
207
+
188
208
  _getRootObject() {
189
209
  const rows = this.getAllStmt.all();
190
210
  const result = {};
191
-
192
211
  for (const row of rows) {
193
212
  const path = this._keyToPath(row.key);
194
213
  const value = JSON.parse(row.value);
195
214
  this._setNestedValue(result, path, value);
196
215
  }
197
-
198
216
  return result;
199
217
  }
200
-
218
+
201
219
  async _setRootObject(obj) {
202
- // Clear existing data
203
- this.db.exec('DELETE FROM deepbase');
204
-
205
- // Flatten and insert
206
220
  const entries = this._flattenObject(obj);
207
- const insertMany = this.db.transaction((entries) => {
208
- for (const [key, value] of entries) {
209
- this.setStmt.run(key, JSON.stringify(value));
210
- }
211
- });
212
-
213
- insertMany(entries);
221
+ this._setRootTxn(entries);
214
222
  }
215
-
216
- _buildObjectFromChildren(parentKey) {
217
- const rows = this.getKeysLikeStmt.all(parentKey ? this._likePrefix(parentKey) : '%');
223
+
224
+ _buildObjectFromChildren(parentKey, likePattern) {
225
+ const rows = this.getKeysLikeStmt.all(likePattern || (parentKey ? this._likePrefix(parentKey) : '%'));
218
226
  const result = {};
219
-
227
+ const parentPathLen = parentKey ? this._keyToPath(parentKey).length : 0;
228
+
220
229
  for (const row of rows) {
221
230
  const fullPath = this._keyToPath(row.key);
222
- const relativePath = parentKey
223
- ? fullPath.slice(this._keyToPath(parentKey).length)
224
- : fullPath;
225
-
231
+ const relativePath = fullPath.slice(parentPathLen);
226
232
  const value = JSON.parse(row.value);
227
233
  this._setNestedValue(result, relativePath, value);
228
234
  }
229
-
235
+
230
236
  return result;
231
237
  }
232
-
238
+
233
239
  _escapeLikePattern(str) {
234
240
  return str.replace(/[!%_]/g, '!$&');
235
241
  }
@@ -240,85 +246,76 @@ export class SqliteDriver extends DeepBaseDriver {
240
246
 
241
247
  _setNestedValue(obj, path, value) {
242
248
  if (path.length === 0) return;
243
-
249
+
244
250
  if (path.length === 1) {
245
251
  obj[path[0]] = value;
246
252
  return;
247
253
  }
248
-
254
+
249
255
  const key = path[0];
250
- if (!obj.hasOwnProperty(key) || typeof obj[key] !== "object") {
256
+ if (!obj.hasOwnProperty(key) || typeof obj[key] !== 'object') {
251
257
  obj[key] = {};
252
258
  }
253
-
259
+
254
260
  this._setNestedValue(obj[key], path.slice(1), value);
255
261
  }
256
-
262
+
257
263
  _flattenObject(obj, prefix = '') {
258
264
  const entries = [];
259
-
265
+
260
266
  for (const [key, value] of Object.entries(obj)) {
261
267
  const escapedKey = this._escapeDots(String(key));
262
268
  const fullKey = prefix ? `${prefix}.${escapedKey}` : escapedKey;
263
-
269
+
264
270
  if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
265
271
  entries.push(...this._flattenObject(value, fullKey));
266
272
  } else {
267
273
  entries.push([fullKey, value]);
268
274
  }
269
275
  }
270
-
276
+
271
277
  return entries;
272
278
  }
273
-
274
- _findParentWithValue(path) {
275
- // Try to find a parent path that has a stored value
276
- // For example: if looking for ['users', 'abc', 'name']
277
- // and 'users.abc' exists as a stored JSON object, return ['users', 'abc']
279
+
280
+ _getFromParent(path) {
278
281
  for (let i = path.length - 1; i > 0; i--) {
279
282
  const parentPath = path.slice(0, i);
280
283
  const parentKey = this._pathToKey(parentPath);
281
284
  const row = this.getStmt.get(parentKey);
282
285
  if (row) {
283
- const value = JSON.parse(row.value);
284
- // Only return if it's an object (not a primitive)
285
- if (value !== null && typeof value === 'object') {
286
- return parentPath;
286
+ const parentValue = JSON.parse(row.value);
287
+ if (parentValue !== null && typeof parentValue === 'object') {
288
+ return this._getFromObject(parentValue, path.slice(i));
287
289
  }
288
290
  }
289
291
  }
290
292
  return null;
291
293
  }
292
-
294
+
293
295
  _getFromObject(obj, path) {
294
296
  if (path.length === 0) return obj;
295
297
  if (path.length === 1) {
296
298
  return obj === null || obj[path[0]] === undefined ? null : obj[path[0]];
297
299
  }
298
-
300
+
299
301
  const key = path[0];
300
302
  if (!obj.hasOwnProperty(key)) return null;
301
-
303
+
302
304
  return this._getFromObject(obj[key], path.slice(1));
303
305
  }
304
-
306
+
305
307
  _expandParentObjects(path) {
306
- // Check each parent level (not including the path itself) to see if it exists as an object that needs expanding
307
- // For example, if path is ['config', 'theme'], check if 'config' exists as an object
308
308
  for (let i = 1; i < path.length; i++) {
309
309
  const parentPath = path.slice(0, i);
310
310
  const parentKey = this._pathToKey(parentPath);
311
311
  const parentRow = this.getStmt.get(parentKey);
312
-
312
+
313
313
  if (parentRow) {
314
314
  const parentValue = JSON.parse(parentRow.value);
315
-
316
- // If it's an object (not array or primitive), expand it
315
+
317
316
  if (parentValue !== null && typeof parentValue === 'object' && !Array.isArray(parentValue)) {
318
- // Delete the parent key
319
317
  this.delStmt.run(parentKey);
320
-
321
- // Insert all properties as individual keys
318
+
322
319
  const entries = this._flattenObject(parentValue, parentKey);
323
320
  for (const [key, value] of entries) {
324
321
  this.setStmt.run(key, JSON.stringify(value));
@@ -330,5 +327,3 @@ export class SqliteDriver extends DeepBaseDriver {
330
327
  }
331
328
 
332
329
  export default SqliteDriver;
333
-
334
-
package/src/index.d.ts CHANGED
@@ -3,6 +3,7 @@ import { DeepBaseDriver, DeepBaseDriverOptions } from 'deepbase';
3
3
  export interface SqliteDriverOptions extends DeepBaseDriverOptions {
4
4
  name?: string;
5
5
  path?: string;
6
+ pragma?: 'none' | 'safe' | 'balanced' | 'fast';
6
7
  }
7
8
 
8
9
  export class SqliteDriver extends DeepBaseDriver {
@@ -11,6 +12,8 @@ export class SqliteDriver extends DeepBaseDriver {
11
12
  name: string;
12
13
  path: string;
13
14
  fileName: string;
15
+ pragma: string;
14
16
  }
15
17
 
18
+ export { SqliteDriver as SqliteFastDriver };
16
19
  export default SqliteDriver;
package/src/index.js CHANGED
@@ -1,4 +1,3 @@
1
1
  export { SqliteDriver } from './SqliteDriver.js';
2
+ export { SqliteDriver as SqliteFastDriver } from './SqliteDriver.js';
2
3
  export { SqliteDriver as default } from './SqliteDriver.js';
3
-
4
-