deepbase-sqlite 3.8.2 → 3.8.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepbase-sqlite",
3
- "version": "3.8.2",
3
+ "version": "3.8.6",
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.8.2"
20
+ "deepbase": "^3.8.6"
21
21
  },
22
22
  "devDependencies": {
23
23
  "mocha": "^10.8.2"
@@ -72,17 +72,19 @@ export class SqliteDriver extends DeepBaseDriver {
72
72
  `);
73
73
  this.delStmt = this.db.prepare('DELETE FROM deepbase WHERE key = ?');
74
74
  this.getAllStmt = this.db.prepare('SELECT key, value FROM deepbase ORDER BY seq, key');
75
- this.getKeysLikeStmt = this.db.prepare(
76
- "SELECT key, value FROM deepbase WHERE key LIKE ? ESCAPE '!' ORDER BY seq, key",
75
+ this.getChildrenStmt = this.db.prepare(
76
+ 'SELECT key, value FROM deepbase WHERE key >= ? AND key < ? ORDER BY seq, key',
77
77
  );
78
78
  this.getFirstChildKeyStmt = this.db.prepare(
79
- "SELECT key FROM deepbase WHERE key LIKE ? ESCAPE '!' ORDER BY seq, key LIMIT 1",
79
+ 'SELECT key FROM deepbase WHERE key >= ? AND key < ? ORDER BY seq, key LIMIT 1',
80
80
  );
81
81
  this.getLastChildKeyStmt = this.db.prepare(
82
- "SELECT key FROM deepbase WHERE key LIKE ? ESCAPE '!' ORDER BY seq DESC, key DESC LIMIT 1",
82
+ 'SELECT key FROM deepbase WHERE key >= ? AND key < ? ORDER BY seq DESC, key DESC LIMIT 1',
83
83
  );
84
- this.delChildrenStmt = this.db.prepare("DELETE FROM deepbase WHERE key LIKE ? ESCAPE '!'");
85
- this.hasChildrenStmt = this.db.prepare("SELECT 1 FROM deepbase WHERE key LIKE ? ESCAPE '!' LIMIT 1");
84
+ this.getFirstKeyStmt = this.db.prepare('SELECT key FROM deepbase ORDER BY seq, key LIMIT 1');
85
+ this.getLastKeyStmt = this.db.prepare('SELECT key FROM deepbase ORDER BY seq DESC, key DESC LIMIT 1');
86
+ this.delChildrenStmt = this.db.prepare('DELETE FROM deepbase WHERE key >= ? AND key < ?');
87
+ this.hasChildrenStmt = this.db.prepare('SELECT 1 FROM deepbase WHERE key >= ? AND key < ? LIMIT 1');
86
88
 
87
89
  const setTxn = this.db.transaction((key, jsonValue, keys) => {
88
90
  this._expandParentObjects(keys);
@@ -90,10 +92,10 @@ export class SqliteDriver extends DeepBaseDriver {
90
92
  });
91
93
  this._setTxn = (...args) => setTxn.immediate(...args);
92
94
 
93
- const delTxn = this.db.transaction((key, likePattern, keys) => {
95
+ const delTxn = this.db.transaction((key, childLowerBound, childUpperBound, keys) => {
94
96
  this._expandParentObjects(keys);
95
97
  this.delStmt.run(key);
96
- this.delChildrenStmt.run(likePattern);
98
+ this.delChildrenStmt.run(childLowerBound, childUpperBound);
97
99
  });
98
100
  this._delTxn = (...args) => delTxn.immediate(...args);
99
101
 
@@ -209,17 +211,17 @@ export class SqliteDriver extends DeepBaseDriver {
209
211
 
210
212
  const key = this._pathToKey(args);
211
213
  const row = this.getStmt.get(key);
212
- const likePattern = this._likePrefix(key);
214
+ const childRange = this._childRange(key);
213
215
 
214
216
  if (row) {
215
- if (this.hasChildrenStmt.get(likePattern)) {
216
- return this._buildObjectFromChildren(key, likePattern);
217
+ if (this.hasChildrenStmt.get(...childRange)) {
218
+ return this._buildObjectFromChildren(key, childRange);
217
219
  }
218
220
  return JSON.parse(row.value);
219
221
  }
220
222
 
221
- if (this.hasChildrenStmt.get(likePattern)) {
222
- return this._buildObjectFromChildren(key, likePattern);
223
+ if (this.hasChildrenStmt.get(...childRange)) {
224
+ return this._buildObjectFromChildren(key, childRange);
223
225
  }
224
226
 
225
227
  return this._getFromParent(args);
@@ -249,7 +251,7 @@ export class SqliteDriver extends DeepBaseDriver {
249
251
  }
250
252
 
251
253
  _replaceRow(key, jsonValue) {
252
- this.delChildrenStmt.run(this._likePrefix(key));
254
+ this.delChildrenStmt.run(...this._childRange(key));
253
255
  this.setStmt.run(key, jsonValue);
254
256
  }
255
257
 
@@ -261,9 +263,9 @@ export class SqliteDriver extends DeepBaseDriver {
261
263
  }
262
264
 
263
265
  const key = this._pathToKey(keys);
264
- const likePattern = this._likePrefix(key);
266
+ const childRange = this._childRange(key);
265
267
  return this._runWrite(() => {
266
- this._delTxn(key, likePattern, keys);
268
+ this._delTxn(key, ...childRange, keys);
267
269
  });
268
270
  }
269
271
 
@@ -322,8 +324,10 @@ export class SqliteDriver extends DeepBaseDriver {
322
324
  }
323
325
 
324
326
  _findBoundaryNestedKey(path, fromEnd) {
325
- const likePattern = path.length === 0 ? '%' : this._likePrefix(this._pathToKey(path));
326
- const row = (fromEnd ? this.getLastChildKeyStmt : this.getFirstChildKeyStmt).get(likePattern);
327
+ const row = path.length === 0
328
+ ? (fromEnd ? this.getLastKeyStmt : this.getFirstKeyStmt).get()
329
+ : (fromEnd ? this.getLastChildKeyStmt : this.getFirstChildKeyStmt)
330
+ .get(...this._childRange(this._pathToKey(path)));
327
331
  if (!row) return undefined;
328
332
 
329
333
  const fullPath = this._keyToPath(row.key);
@@ -343,8 +347,8 @@ export class SqliteDriver extends DeepBaseDriver {
343
347
  return result;
344
348
  }
345
349
 
346
- _buildObjectFromChildren(parentKey, likePattern) {
347
- const rows = this.getKeysLikeStmt.all(likePattern || (parentKey ? this._likePrefix(parentKey) : '%'));
350
+ _buildObjectFromChildren(parentKey, childRange) {
351
+ const rows = this.getChildrenStmt.all(...childRange);
348
352
  const result = {};
349
353
  const parentPathLen = parentKey ? this._keyToPath(parentKey).length : 0;
350
354
 
@@ -368,12 +372,11 @@ export class SqliteDriver extends DeepBaseDriver {
368
372
  );
369
373
  }
370
374
 
371
- _escapeLikePattern(str) {
372
- return str.replace(/[!%_]/g, '!$&');
373
- }
374
-
375
- _likePrefix(key) {
376
- return this._escapeLikePattern(key) + '.%';
375
+ _childRange(key) {
376
+ // Stored descendants start with `${key}.`. Since '/' is the next ASCII
377
+ // character after '.', this half-open range matches exactly that prefix
378
+ // while allowing SQLite to seek through the primary-key index.
379
+ return [`${key}.`, `${key}/`];
377
380
  }
378
381
 
379
382
  _setNestedValue(obj, path, value) {
@@ -92,6 +92,23 @@ describe('SqliteDriver multi-process safety', function () {
92
92
  await driver.disconnect();
93
93
  });
94
94
 
95
+ it('uses the primary-key index to find descendant rows', async function () {
96
+ const driver = new SqliteDriver({
97
+ name: 'descendant-query-plan',
98
+ path: testDataPath,
99
+ });
100
+ await driver.connect();
101
+
102
+ const plan = driver.db
103
+ .prepare(`EXPLAIN QUERY PLAN ${driver.delChildrenStmt.source}`)
104
+ .all('parent.', 'parent/');
105
+ const details = plan.map(step => step.detail).join('\n');
106
+
107
+ assert.match(details, /SEARCH deepbase USING PRIMARY KEY/);
108
+ assert.doesNotMatch(details, /SCAN deepbase/);
109
+ await driver.disconnect();
110
+ });
111
+
95
112
  it('adds a missing seq column without rewriting legacy rows', async function () {
96
113
  const name = 'legacy-no-seq';
97
114
  const fileName = path.join(testDataPath, `${name}.db`);
package/test/test.js CHANGED
@@ -554,7 +554,7 @@ for (const pragma of PRAGMA_MODES) {
554
554
  });
555
555
  });
556
556
 
557
- describe('Keys with Underscores (SQL LIKE safety)', function () {
557
+ describe('Special Key Isolation', function () {
558
558
  it('should not cross-match keys with underscores', async function () {
559
559
  await db.set('chat_1', 'msg', 'hello');
560
560
  await db.set('chatX1', 'msg', 'world');
@@ -585,6 +585,19 @@ for (const pragma of PRAGMA_MODES) {
585
585
  assert.deepStrictEqual(await db.get('progress', '100%'), { done: true });
586
586
  assert.deepStrictEqual(await db.get('progress', 'abc'), { done: false });
587
587
  });
588
+
589
+ it('should isolate range boundaries for slashes and backslashes', async function () {
590
+ await db.set('route/path', 'child', 1);
591
+ await db.set('route/path0', 'child', 2);
592
+ await db.set('route\\path', 'child', 3);
593
+
594
+ await db.set('route/path', { replaced: true });
595
+ await db.del('route\\path');
596
+
597
+ assert.deepStrictEqual(await db.get('route/path'), { replaced: true });
598
+ assert.deepStrictEqual(await db.get('route/path0'), { child: 2 });
599
+ assert.strictEqual(await db.get('route\\path'), null);
600
+ });
588
601
  });
589
602
 
590
603
  describe('Storybot-like Structure', function () {