deepbase-sqlite 3.4.11 → 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.11",
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.11"
20
+ "deepbase": "^3.5.1"
21
21
  },
22
22
  "scripts": {
23
23
  "test": "mocha test/test.js"
@@ -3,230 +3,239 @@ 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() {
56
+ if (this._connected) return;
57
+
28
58
  if (!fs.existsSync(this.path)) {
29
59
  fs.mkdirSync(this.path, { recursive: true });
30
60
  }
31
-
61
+
32
62
  this.db = new Database(this.fileName);
33
-
34
- // 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' : '';
35
75
  this.db.exec(`
36
76
  CREATE TABLE IF NOT EXISTS deepbase (
37
77
  key TEXT PRIMARY KEY,
38
78
  value TEXT NOT NULL
39
- )
79
+ )${withoutRowid}
40
80
  `);
41
-
42
- // Prepare statements for better performance
81
+
43
82
  this.getStmt = this.db.prepare('SELECT value FROM deepbase WHERE key = ?');
44
83
  this.setStmt = this.db.prepare('INSERT OR REPLACE INTO deepbase (key, value) VALUES (?, ?)');
45
84
  this.delStmt = this.db.prepare('DELETE FROM deepbase WHERE key = ?');
46
85
  this.getAllStmt = this.db.prepare('SELECT key, value FROM deepbase');
47
- this.getKeysLikeStmt = this.db.prepare('SELECT key, value FROM deepbase WHERE key LIKE ? ESCAPE \'!\'');
48
-
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
+
49
116
  this._connected = true;
50
117
  }
51
-
118
+
52
119
  async disconnect() {
53
120
  if (this.db) {
54
121
  this.db.close();
55
122
  this.db = null;
56
123
  }
124
+ this._connected = false;
57
125
  }
58
-
126
+
59
127
  async get(...args) {
60
128
  return this._getSync(args);
61
129
  }
62
-
130
+
63
131
  _getSync(args) {
64
132
  if (args.length === 0) {
65
- // Get root object
66
133
  return this._getRootObject();
67
134
  }
68
-
135
+
69
136
  const key = this._pathToKey(args);
70
137
  const row = this.getStmt.get(key);
71
-
72
- // Check if there are child keys (nested properties)
73
- const children = this.getKeysLikeStmt.all(this._likePrefix(key));
74
-
75
- // If there are children, build object from them
76
- if (children.length > 0) {
77
- return this._buildObjectFromChildren(key);
78
- }
79
-
80
- // If direct key exists and no children, return it
138
+ const likePattern = this._likePrefix(key);
139
+
81
140
  if (row) {
141
+ if (this.hasChildrenStmt.get(likePattern)) {
142
+ return this._buildObjectFromChildren(key, likePattern);
143
+ }
82
144
  return JSON.parse(row.value);
83
145
  }
84
-
85
- // Check if we need to look into a parent object
86
- // For example: if we're looking for 'users.abc.name' but only 'users.abc' exists as a JSON object
87
- const parentPath = this._findParentWithValue(args);
88
- if (parentPath) {
89
- const parentKey = this._pathToKey(parentPath);
90
- const parentRow = this.getStmt.get(parentKey);
91
- if (parentRow) {
92
- const parentValue = JSON.parse(parentRow.value);
93
- const relativePath = args.slice(parentPath.length);
94
- return this._getFromObject(parentValue, relativePath);
95
- }
146
+
147
+ if (this.hasChildrenStmt.get(likePattern)) {
148
+ return this._buildObjectFromChildren(key, likePattern);
96
149
  }
97
-
98
- return null;
150
+
151
+ return this._getFromParent(args);
99
152
  }
100
-
153
+
101
154
  async set(...args) {
102
155
  if (args.length === 0) {
103
156
  throw new Error('set() requires at least one argument');
104
157
  }
105
-
158
+
106
159
  if (args.length === 1) {
107
- // Setting root object
108
160
  await this._setRootObject(args[0]);
109
161
  return [];
110
162
  }
111
-
163
+
112
164
  return this._setSync(args);
113
165
  }
114
-
166
+
115
167
  _setSync(args) {
116
168
  const keys = args.slice(0, -1);
117
169
  const value = args[args.length - 1];
118
170
  const key = this._pathToKey(keys);
119
-
120
- // Use transaction to make expansion and set atomic
121
- const transaction = this.db.transaction(() => {
122
- // Check if any parent path exists as an object that needs to be expanded
123
- this._expandParentObjects(keys);
124
-
125
- this.setStmt.run(key, JSON.stringify(value));
126
- });
127
-
128
- transaction();
171
+ this._setTxn(key, JSON.stringify(value), keys);
129
172
  return keys;
130
173
  }
131
-
174
+
132
175
  async del(...keys) {
133
176
  if (keys.length === 0) {
134
- // Delete everything
135
177
  this.db.exec('DELETE FROM deepbase');
136
178
  return;
137
179
  }
138
-
180
+
139
181
  const key = this._pathToKey(keys);
140
-
141
- // Use transaction to make deletion atomic
142
- const transaction = this.db.transaction(() => {
143
- // Delete the key itself
144
- this.delStmt.run(key);
145
-
146
- // Delete all children
147
- this.db.prepare('DELETE FROM deepbase WHERE key LIKE ? ESCAPE \'!\'').run(this._likePrefix(key));
148
- });
149
-
150
- transaction();
182
+ this._delTxn(key, this._likePrefix(key));
151
183
  }
152
-
184
+
153
185
  async inc(...args) {
154
186
  const i = args.pop();
155
187
  return this.upd(...args, n => n + i);
156
188
  }
157
-
189
+
158
190
  async dec(...args) {
159
191
  const i = args.pop();
160
192
  return this.upd(...args, n => n - i);
161
193
  }
162
-
194
+
163
195
  async add(...keys) {
164
196
  const value = keys.pop();
165
197
  const id = this.nanoid();
166
198
  await this.set(...[...keys, id], value);
167
199
  return [...keys, id];
168
200
  }
169
-
201
+
170
202
  async upd(...args) {
171
203
  const func = args.pop();
172
204
  const keys = args;
173
-
174
- // Use transaction to make get+set atomic
175
- const transaction = this.db.transaction(() => {
176
- const currentValue = this._getSync(keys);
177
- const newValue = func(currentValue);
178
- this._setSync([...keys, newValue]);
179
- return keys;
180
- });
181
-
182
- return transaction();
205
+ return this._updTxn(keys, func);
183
206
  }
184
-
207
+
185
208
  _getRootObject() {
186
209
  const rows = this.getAllStmt.all();
187
210
  const result = {};
188
-
189
211
  for (const row of rows) {
190
212
  const path = this._keyToPath(row.key);
191
213
  const value = JSON.parse(row.value);
192
214
  this._setNestedValue(result, path, value);
193
215
  }
194
-
195
216
  return result;
196
217
  }
197
-
218
+
198
219
  async _setRootObject(obj) {
199
- // Clear existing data
200
- this.db.exec('DELETE FROM deepbase');
201
-
202
- // Flatten and insert
203
220
  const entries = this._flattenObject(obj);
204
- const insertMany = this.db.transaction((entries) => {
205
- for (const [key, value] of entries) {
206
- this.setStmt.run(key, JSON.stringify(value));
207
- }
208
- });
209
-
210
- insertMany(entries);
221
+ this._setRootTxn(entries);
211
222
  }
212
-
213
- _buildObjectFromChildren(parentKey) {
214
- 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) : '%'));
215
226
  const result = {};
216
-
227
+ const parentPathLen = parentKey ? this._keyToPath(parentKey).length : 0;
228
+
217
229
  for (const row of rows) {
218
230
  const fullPath = this._keyToPath(row.key);
219
- const relativePath = parentKey
220
- ? fullPath.slice(this._keyToPath(parentKey).length)
221
- : fullPath;
222
-
231
+ const relativePath = fullPath.slice(parentPathLen);
223
232
  const value = JSON.parse(row.value);
224
233
  this._setNestedValue(result, relativePath, value);
225
234
  }
226
-
235
+
227
236
  return result;
228
237
  }
229
-
238
+
230
239
  _escapeLikePattern(str) {
231
240
  return str.replace(/[!%_]/g, '!$&');
232
241
  }
@@ -237,85 +246,76 @@ export class SqliteDriver extends DeepBaseDriver {
237
246
 
238
247
  _setNestedValue(obj, path, value) {
239
248
  if (path.length === 0) return;
240
-
249
+
241
250
  if (path.length === 1) {
242
251
  obj[path[0]] = value;
243
252
  return;
244
253
  }
245
-
254
+
246
255
  const key = path[0];
247
- if (!obj.hasOwnProperty(key) || typeof obj[key] !== "object") {
256
+ if (!obj.hasOwnProperty(key) || typeof obj[key] !== 'object') {
248
257
  obj[key] = {};
249
258
  }
250
-
259
+
251
260
  this._setNestedValue(obj[key], path.slice(1), value);
252
261
  }
253
-
262
+
254
263
  _flattenObject(obj, prefix = '') {
255
264
  const entries = [];
256
-
265
+
257
266
  for (const [key, value] of Object.entries(obj)) {
258
267
  const escapedKey = this._escapeDots(String(key));
259
268
  const fullKey = prefix ? `${prefix}.${escapedKey}` : escapedKey;
260
-
269
+
261
270
  if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
262
271
  entries.push(...this._flattenObject(value, fullKey));
263
272
  } else {
264
273
  entries.push([fullKey, value]);
265
274
  }
266
275
  }
267
-
276
+
268
277
  return entries;
269
278
  }
270
-
271
- _findParentWithValue(path) {
272
- // Try to find a parent path that has a stored value
273
- // For example: if looking for ['users', 'abc', 'name']
274
- // and 'users.abc' exists as a stored JSON object, return ['users', 'abc']
279
+
280
+ _getFromParent(path) {
275
281
  for (let i = path.length - 1; i > 0; i--) {
276
282
  const parentPath = path.slice(0, i);
277
283
  const parentKey = this._pathToKey(parentPath);
278
284
  const row = this.getStmt.get(parentKey);
279
285
  if (row) {
280
- const value = JSON.parse(row.value);
281
- // Only return if it's an object (not a primitive)
282
- if (value !== null && typeof value === 'object') {
283
- return parentPath;
286
+ const parentValue = JSON.parse(row.value);
287
+ if (parentValue !== null && typeof parentValue === 'object') {
288
+ return this._getFromObject(parentValue, path.slice(i));
284
289
  }
285
290
  }
286
291
  }
287
292
  return null;
288
293
  }
289
-
294
+
290
295
  _getFromObject(obj, path) {
291
296
  if (path.length === 0) return obj;
292
297
  if (path.length === 1) {
293
298
  return obj === null || obj[path[0]] === undefined ? null : obj[path[0]];
294
299
  }
295
-
300
+
296
301
  const key = path[0];
297
302
  if (!obj.hasOwnProperty(key)) return null;
298
-
303
+
299
304
  return this._getFromObject(obj[key], path.slice(1));
300
305
  }
301
-
306
+
302
307
  _expandParentObjects(path) {
303
- // Check each parent level (not including the path itself) to see if it exists as an object that needs expanding
304
- // For example, if path is ['config', 'theme'], check if 'config' exists as an object
305
308
  for (let i = 1; i < path.length; i++) {
306
309
  const parentPath = path.slice(0, i);
307
310
  const parentKey = this._pathToKey(parentPath);
308
311
  const parentRow = this.getStmt.get(parentKey);
309
-
312
+
310
313
  if (parentRow) {
311
314
  const parentValue = JSON.parse(parentRow.value);
312
-
313
- // If it's an object (not array or primitive), expand it
315
+
314
316
  if (parentValue !== null && typeof parentValue === 'object' && !Array.isArray(parentValue)) {
315
- // Delete the parent key
316
317
  this.delStmt.run(parentKey);
317
-
318
- // Insert all properties as individual keys
318
+
319
319
  const entries = this._flattenObject(parentValue, parentKey);
320
320
  for (const [key, value] of entries) {
321
321
  this.setStmt.run(key, JSON.stringify(value));
@@ -327,5 +327,3 @@ export class SqliteDriver extends DeepBaseDriver {
327
327
  }
328
328
 
329
329
  export default SqliteDriver;
330
-
331
-
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
-