deepbase-sqlite 3.10.0 → 3.11.0

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
@@ -62,6 +62,8 @@ new SqliteDriver({
62
62
  baseDelayMs: 25, // Exponential backoff base
63
63
  maxDelayMs: 250 // Backoff cap
64
64
  },
65
+ queryWindowMaxRecords: 1000, // query() records a native window may reach
66
+ queryWindowMaxProbes: 12000, // query() lookups the order guard may spend
65
67
  nidAlphabet: 'ABC...', // Alphabet for ID generation
66
68
  nidLength: 10 // Length of generated IDs
67
69
  })
@@ -143,6 +145,35 @@ Efficiently stores nested objects using a key-value schema:
143
145
 
144
146
  Each row also stores a database-assigned `seq` so reads that rebuild objects use `ORDER BY seq, key`. That matches JavaScript insertion order for sibling keys and keeps `shift()` / `pop()` aligned with `JsonDriver`. For legacy databases, the driver only adds the missing column and index; it does not renumber existing rows. Historical ties remain deterministic through the `key` fallback order.
145
147
 
148
+ ### Querying
149
+
150
+ `db.query(...path)` evaluates `where` / `orderBy` / `skip` / `take` / `select` in memory and never pushes filters into SQLite. Terminals (`toArray`, `first`, `count`, `any`) return records shaped `{ id, value }`, where `id` is the property name and `value` the stored value:
151
+
152
+ ```javascript
153
+ const adults = await db.query('users').where('age', '>=', 18).orderBy('age').toArray();
154
+ // [{ id: 'alice', value: { name: 'Alice', age: 30 } }, ...]
155
+ ```
156
+
157
+ #### Native pagination window
158
+
159
+ When the chain *starts* with `skip()`/`take()`, the driver answers that window from the key index and rebuilds only the records the window keeps, instead of materialising the whole collection. `first()` and `any()` use the same path because they are a single-record window.
160
+
161
+ ```javascript
162
+ // Reads the key index plus 10 records, not the 20,000 records in the collection.
163
+ const page = await db.query('users').skip(100).take(10).toArray();
164
+
165
+ // Filters and ordering still run in memory, so a leading where()/orderBy()
166
+ // falls back to reading the collection.
167
+ const adults = await db.query('users').where('age', '>=', 18).take(10).toArray();
168
+ ```
169
+
170
+ Two guards keep the native window cheaper than reading the collection, both configurable on the driver:
171
+
172
+ - `queryWindowMaxRecords` (default `1000`): how far `offset + limit` may reach before the driver reads the collection instead.
173
+ - `queryWindowMaxProbes` (default `12000`): how many key-index lookups the order guard may spend. The guard exists because stored keys escape `.` and `\` and expand records into `<key>.<field>` rows, so a plain key scan can disagree with the record order for ids such as `u1` and `u1!`; when it cannot prove the order, the driver falls back to the generic path.
174
+
175
+ Set either budget to `0` to always use the generic path. Measurements on 20,000 records (80,000 rows, 4 fields each): `first()` 71 ms → 0.04 ms, `take(10)` 71 ms → 0.14 ms, `skip(100).take(10)` 69 ms → 0.8 ms, `take(1000)` 70 ms → 13 ms.
176
+
146
177
  ### ACID Compliance
147
178
 
148
179
  SQLite provides:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepbase-sqlite",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
4
4
  "description": "⚡ DeepBase SQLite - SQLite database driver",
5
5
  "type": "module",
6
6
  "main": "src/index.cjs",
@@ -17,10 +17,7 @@
17
17
  "better-sqlite3": "^11.8.1"
18
18
  },
19
19
  "peerDependencies": {
20
- "deepbase": "^3.10.0"
21
- },
22
- "scripts": {
23
- "test": "mocha test/test.js test/test-multiprocess.js"
20
+ "deepbase": "^3.11.0"
24
21
  },
25
22
  "devDependencies": {
26
23
  "mocha": "^10.8.2"
@@ -44,5 +41,8 @@
44
41
  "bugs": {
45
42
  "url": "https://github.com/clasen/DeepBase/issues"
46
43
  },
47
- "homepage": "https://github.com/clasen/DeepBase/tree/main/packages/driver-sqlite"
48
- }
44
+ "homepage": "https://github.com/clasen/DeepBase/tree/main/packages/driver-sqlite",
45
+ "scripts": {
46
+ "test": "mocha test/test.js test/test-multiprocess.js"
47
+ }
48
+ }
@@ -1,4 +1,4 @@
1
- import { DeepBaseDriver } from 'deepbase';
1
+ import { DeepBaseDriver, evaluateQuery, removeKey, resolveQueryWindow } from 'deepbase';
2
2
  import Database from 'better-sqlite3';
3
3
  import fs from 'fs';
4
4
  import * as pathModule from 'path';
@@ -8,20 +8,37 @@ import { backupFrom, checkIntegrityAt, checkpoint, vacuum } from './maintenance.
8
8
  import { ensureSchema } from './schema.js';
9
9
 
10
10
  export class SqliteDriver extends DeepBaseDriver {
11
- constructor({ name, path, pragma, busyTimeoutMs, busyRetry, ...opts } = {}) {
11
+ constructor({
12
+ name,
13
+ path,
14
+ pragma,
15
+ busyTimeoutMs,
16
+ busyRetry,
17
+ queryWindowMaxRecords,
18
+ queryWindowMaxProbes,
19
+ ...opts
20
+ } = {}) {
12
21
  super(opts);
13
22
 
14
23
  if (typeof path !== 'string' || path.trim() === '' || !pathModule.isAbsolute(path)) {
15
24
  throw new TypeError('SqliteDriver requires an absolute "path" option.');
16
25
  }
17
26
 
18
- const config = resolveSqliteConfig({ pragma, busyTimeoutMs, busyRetry });
27
+ const config = resolveSqliteConfig({
28
+ pragma,
29
+ busyTimeoutMs,
30
+ busyRetry,
31
+ queryWindowMaxRecords,
32
+ queryWindowMaxProbes,
33
+ });
19
34
  this.name = name || 'default';
20
35
  this.path = path;
21
36
  this.pragma = config.pragma;
22
37
  this.pragmaConfig = config.pragmaConfig;
23
38
  this.busyTimeoutMs = config.busyTimeoutMs;
24
39
  this.busyRetry = config.busyRetry;
40
+ this.queryWindowMaxRecords = config.queryWindowMaxRecords;
41
+ this.queryWindowMaxProbes = config.queryWindowMaxProbes;
25
42
 
26
43
  this.path = pathModule.resolve(this.path);
27
44
  this.fileName = pathModule.join(this.path, `${this.name}.db`);
@@ -89,6 +106,9 @@ export class SqliteDriver extends DeepBaseDriver {
89
106
  this.getLastKeyStmt = this.db.prepare('SELECT key FROM deepbase ORDER BY seq DESC, key DESC LIMIT 1');
90
107
  this.delChildrenStmt = this.db.prepare('DELETE FROM deepbase WHERE key >= ? AND key < ?');
91
108
  this.hasChildrenStmt = this.db.prepare('SELECT 1 FROM deepbase WHERE key >= ? AND key < ? LIMIT 1');
109
+ this.hasRangeStmt = this.db.prepare('SELECT 1 FROM deepbase WHERE key >= ? AND key < ? LIMIT 1');
110
+ this.getChildKeysStmt = this.db.prepare('SELECT key FROM deepbase WHERE key >= ? AND key < ? ORDER BY key');
111
+ this.getAllKeysStmt = this.db.prepare('SELECT key FROM deepbase ORDER BY key');
92
112
 
93
113
  const setTxn = this.db.transaction((key, jsonValue, keys) => {
94
114
  this._expandParentObjects(keys);
@@ -208,6 +228,86 @@ export class SqliteDriver extends DeepBaseDriver {
208
228
  return this._getSync(args);
209
229
  }
210
230
 
231
+ async query(path, steps) {
232
+ await this.connect();
233
+ const window = resolveQueryWindow(steps);
234
+ if (window) {
235
+ const records = this._readQueryWindow(path, window);
236
+ if (records) return evaluateQuery(records, window.rest, { path });
237
+ }
238
+ return evaluateQuery(this._getSync(path), steps, { path });
239
+ }
240
+
241
+ /**
242
+ * Answer a leading skip()/take() window from the key index, so only the kept
243
+ * records are rebuilt. Returns null when the window cannot be proven
244
+ * equivalent to reading the collection in record order.
245
+ * @param {Array<string|number>} path - Path to the queried object
246
+ * @param {{offset: number, limit: number|null}} window - Resolved window
247
+ * @returns {object|null} Records keyed by id, or null for the generic path
248
+ */
249
+ _readQueryWindow(path, { offset, limit }) {
250
+ const needed = limit === null ? Infinity : offset + limit;
251
+ if (needed === 0 || needed > this.queryWindowMaxRecords) return null;
252
+
253
+ const ids = [];
254
+ const seen = new Set();
255
+ let probes = 0;
256
+ let previous;
257
+
258
+ const scan = path.length === 0
259
+ ? this.getAllKeysStmt.iterate()
260
+ : this.getChildKeysStmt.iterate(...this._childRange(this._pathToKey(path)));
261
+
262
+ for (const row of scan) {
263
+ const id = this._keyToPath(row.key)[path.length];
264
+ if (id === undefined || id === previous) continue;
265
+ // Record ids must sit in one run of rows, in ascending key order, for the
266
+ // key index order to match the record order evaluateQuery() establishes.
267
+ if (seen.has(id)) return null;
268
+ // One probe per id character, plus the children probe below, keeps the
269
+ // guard cheaper than reading the collection for any window we accept.
270
+ probes += id.length + 1;
271
+ if (probes > this.queryWindowMaxProbes) return null;
272
+ // The first id sizes the whole window: long ids cost a probe each, so a
273
+ // window that cannot fit the budget falls back before probing further.
274
+ if (ids.length === 0 && needed * probes > this.queryWindowMaxProbes) return null;
275
+ if (this._isWindowEdgeUnsafe(path, id)) return null;
276
+ seen.add(id);
277
+ previous = id;
278
+ ids.push(id);
279
+ if (ids.length >= needed) break;
280
+ }
281
+
282
+ // No rows means either a missing path or a value that is not an object;
283
+ // both keep the generic path so it can report the same error.
284
+ if (ids.length === 0) return null;
285
+
286
+ const selected = limit === null ? ids.slice(offset) : ids.slice(offset, offset + limit);
287
+ const records = {};
288
+ for (const id of selected) {
289
+ records[id] = this._getSync([...path, id]);
290
+ }
291
+ return records;
292
+ }
293
+
294
+ /**
295
+ * Stored keys escape "." and "\\", and records expand into "<key>.<field>"
296
+ * rows, so a key-order scan can disagree with the record order evaluateQuery()
297
+ * uses. Probe the two shapes that can produce that disagreement.
298
+ * @param {Array<string|number>} path - Path to the queried object
299
+ * @param {string} id - Record id to check
300
+ * @returns {boolean} True when a native window could skip an earlier record
301
+ */
302
+ _isWindowEdgeUnsafe(path, id) {
303
+ for (let index = 0; index < id.length; index++) {
304
+ const prefix = this._pathToKey([...path, id.slice(0, index)]);
305
+ if (this.hasRangeStmt.get(`${prefix}\\`, `${prefix}]`)) return true;
306
+ if (id[index] < '.' && this.hasChildrenStmt.get(...this._childRange(prefix))) return true;
307
+ }
308
+ return false;
309
+ }
310
+
211
311
  _getSync(args) {
212
312
  if (args.length === 0) {
213
313
  return this._getRootObject();
@@ -269,10 +369,33 @@ export class SqliteDriver extends DeepBaseDriver {
269
369
  const key = this._pathToKey(keys);
270
370
  const childRange = this._childRange(key);
271
371
  return this._runWrite(() => {
372
+ if (this._delArrayElement(keys)) return;
272
373
  this._delTxn(key, ...childRange, keys);
273
374
  });
274
375
  }
275
376
 
377
+ /**
378
+ * A stored array lives in one row, so deleting one of its indexes has to
379
+ * splice the array and rewrite that row instead of deleting a child row that
380
+ * does not exist. Returns true when the array handled the delete.
381
+ * @param {Array<string|number>} keys - Path to the parent plus the index
382
+ * @returns {boolean} True when an array element was removed
383
+ */
384
+ _delArrayElement(keys) {
385
+ if (keys.length < 2) return false;
386
+
387
+ const parentPath = keys.slice(0, -1);
388
+ const parent = this._getSync(parentPath);
389
+ if (!Array.isArray(parent)) return false;
390
+
391
+ const index = keys[keys.length - 1];
392
+ // removeKey() splices real indexes and ignores everything else.
393
+ if (!removeKey(parent, index)) return false;
394
+
395
+ this._setTxn(this._pathToKey(parentPath), JSON.stringify(parent), keys);
396
+ return true;
397
+ }
398
+
276
399
  async inc(...args) {
277
400
  const i = args.pop();
278
401
  return this.upd(...args, n => n + i);
package/src/config.js CHANGED
@@ -26,6 +26,11 @@ const PRAGMA_PROFILES = Object.freeze({
26
26
  export const SQLITE_CONFIG = Object.freeze({
27
27
  defaultPragma: 'balanced',
28
28
  busyTimeoutMs: 5000,
29
+ // query() answers a leading skip()/take() window from the key index. Both
30
+ // budgets keep that scan cheaper than reading the whole collection: how far
31
+ // the window may reach, and how many order probes it may spend.
32
+ queryWindowMaxRecords: 1000,
33
+ queryWindowMaxProbes: 12000,
29
34
  busyRetry: Object.freeze({
30
35
  maxAttempts: 2,
31
36
  baseDelayMs: 25,
@@ -40,7 +45,13 @@ function assertNonNegativeInteger(name, value) {
40
45
  }
41
46
  }
42
47
 
43
- export function resolveSqliteConfig({ pragma, busyTimeoutMs, busyRetry } = {}) {
48
+ export function resolveSqliteConfig({
49
+ pragma,
50
+ busyTimeoutMs,
51
+ busyRetry,
52
+ queryWindowMaxRecords,
53
+ queryWindowMaxProbes,
54
+ } = {}) {
44
55
  const resolvedPragma = pragma ?? SQLITE_CONFIG.defaultPragma;
45
56
  if (!Object.prototype.hasOwnProperty.call(SQLITE_CONFIG.pragmaProfiles, resolvedPragma)) {
46
57
  throw new TypeError(`pragma must be one of: ${Object.keys(SQLITE_CONFIG.pragmaProfiles).join(', ')}`);
@@ -68,10 +79,18 @@ export function resolveSqliteConfig({ pragma, busyTimeoutMs, busyRetry } = {}) {
68
79
  throw new TypeError('busyRetry.maxDelayMs must be greater than or equal to busyRetry.baseDelayMs');
69
80
  }
70
81
 
82
+ const resolvedQueryWindowMaxRecords = queryWindowMaxRecords ?? SQLITE_CONFIG.queryWindowMaxRecords;
83
+ assertNonNegativeInteger('queryWindowMaxRecords', resolvedQueryWindowMaxRecords);
84
+
85
+ const resolvedQueryWindowMaxProbes = queryWindowMaxProbes ?? SQLITE_CONFIG.queryWindowMaxProbes;
86
+ assertNonNegativeInteger('queryWindowMaxProbes', resolvedQueryWindowMaxProbes);
87
+
71
88
  return {
72
89
  pragma: resolvedPragma,
73
90
  pragmaConfig: SQLITE_CONFIG.pragmaProfiles[resolvedPragma],
74
91
  busyTimeoutMs: resolvedBusyTimeoutMs,
75
92
  busyRetry: resolvedBusyRetry,
93
+ queryWindowMaxRecords: resolvedQueryWindowMaxRecords,
94
+ queryWindowMaxProbes: resolvedQueryWindowMaxProbes,
76
95
  };
77
96
  }
package/src/index.d.ts CHANGED
@@ -24,6 +24,13 @@ export interface SqliteDriverOptions extends DeepBaseDriverOptions {
24
24
  pragma?: 'none' | 'safe' | 'balanced' | 'fast';
25
25
  busyTimeoutMs?: number;
26
26
  busyRetry?: SqliteBusyRetryOptions;
27
+ /**
28
+ * Records a native `query()` window may reach, counting `offset + limit`.
29
+ * Beyond it the driver reads the collection and evaluates in memory.
30
+ */
31
+ queryWindowMaxRecords?: number;
32
+ /** Key-index lookups the `query()` order guard may spend before falling back. */
33
+ queryWindowMaxProbes?: number;
27
34
  }
28
35
 
29
36
  export class SqliteDriver extends DeepBaseDriver {
@@ -35,6 +42,8 @@ export class SqliteDriver extends DeepBaseDriver {
35
42
  pragma: string;
36
43
  busyTimeoutMs: number;
37
44
  busyRetry: Required<SqliteBusyRetryOptions>;
45
+ queryWindowMaxRecords: number;
46
+ queryWindowMaxProbes: number;
38
47
 
39
48
  /** Opens the existing database read-only, runs `PRAGMA integrity_check`,
40
49
  * then closes it. Does not connect or mutate the driver database. */
package/test/test.js CHANGED
@@ -4,6 +4,8 @@ import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { DeepBase } from '../../core/src/index.js';
7
+ import { arrayScenarios } from '../../core/test/array-scenario.js';
8
+ import { seedQueryFixture, queryScenarios } from '../../core/test/query-scenario.js';
7
9
  import { SqliteDriver } from '../src/SqliteDriver.js';
8
10
 
9
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -22,6 +24,21 @@ describe('SqliteDriver configuration', function () {
22
24
  /requires an absolute "path" option/,
23
25
  );
24
26
  });
27
+
28
+ it('should reject invalid query window budgets', function () {
29
+ const options = { name: 'config', path: testDataPath };
30
+ assert.throws(
31
+ () => new SqliteDriver({ ...options, queryWindowMaxRecords: -1 }),
32
+ /queryWindowMaxRecords must be a non-negative integer/,
33
+ );
34
+ assert.throws(
35
+ () => new SqliteDriver({ ...options, queryWindowMaxProbes: 1.5 }),
36
+ /queryWindowMaxProbes must be a non-negative integer/,
37
+ );
38
+ const driver = new SqliteDriver(options);
39
+ assert.strictEqual(driver.queryWindowMaxRecords, 1000);
40
+ assert.strictEqual(driver.queryWindowMaxProbes, 12000);
41
+ });
25
42
  });
26
43
 
27
44
  for (const pragma of PRAGMA_MODES) {
@@ -200,6 +217,14 @@ for (const pragma of PRAGMA_MODES) {
200
217
  });
201
218
  });
202
219
 
220
+ describe('del()/pop()/shift() sobre arrays', function () {
221
+ for (const scenario of arrayScenarios) {
222
+ it(scenario.title, async function () {
223
+ await scenario.run(db);
224
+ });
225
+ }
226
+ });
227
+
203
228
  describe('Add Operation', function () {
204
229
  it('should add item with auto-generated ID', async function () {
205
230
  const p = await db.add('users', { name: 'Charlie' });
@@ -858,6 +883,164 @@ for (const pragma of PRAGMA_MODES) {
858
883
  });
859
884
  }
860
885
  });
886
+
887
+ describe('query()', function () {
888
+ beforeEach(async function () {
889
+ await seedQueryFixture(db);
890
+ });
891
+
892
+ for (const scenario of queryScenarios) {
893
+ it(scenario.title, async function () {
894
+ await scenario.run(db);
895
+ });
896
+ }
897
+ });
898
+
899
+ describe('query() native window', function () {
900
+ // Same file, same data, but without the key-index window: the reference
901
+ // always reads the collection and evaluates every step in memory.
902
+ let reference;
903
+
904
+ beforeEach(async function () {
905
+ reference = new DeepBase(new SqliteDriver({
906
+ name: `test-${pragma}-${testCounter}`,
907
+ path: testDataPath,
908
+ pragma,
909
+ queryWindowMaxRecords: 0,
910
+ }));
911
+ await reference.connect();
912
+ });
913
+
914
+ afterEach(async function () {
915
+ await reference.disconnect();
916
+ });
917
+
918
+ // One row per field, so records live in fragmented rows.
919
+ async function seedRecords(ids) {
920
+ await db.del();
921
+ for (const [index, id] of ids.entries()) {
922
+ await db.set('users', id, 'name', `name ${id}`);
923
+ await db.set('users', id, 'age', index);
924
+ await db.set('users', id, 'address', 'city', 'Rosario');
925
+ }
926
+ }
927
+
928
+ const countRecords = length =>
929
+ Array.from({ length }, (unused, index) => `u${String(index).padStart(3, '0')}`);
930
+
931
+ // Record ids that make the key index order disagree with the record order.
932
+ const ID_SETS = [
933
+ ['u000', 'u001', 'u002', 'u003', 'u004', 'u005'],
934
+ ['u1', 'u1!', 'u2'],
935
+ ['.hidden', '0alpha', 'z'],
936
+ ['a.b', 'a0', 'a1'],
937
+ ['x', 'x\\y', 'y'],
938
+ ['', 'a', 'b'],
939
+ ];
940
+ const CHAINS = [
941
+ query => query.take(1),
942
+ query => query.take(2),
943
+ query => query.skip(1).take(2),
944
+ query => query.take(2).skip(1),
945
+ query => query.skip(3),
946
+ query => query.take(0),
947
+ query => query.take(2).where('age', '>', 0),
948
+ ];
949
+
950
+ function instrument(driver) {
951
+ const probe = { rows: 0, checks: 0 };
952
+ const all = driver.getChildrenStmt.all.bind(driver.getChildrenStmt);
953
+ driver.getChildrenStmt.all = (...args) => {
954
+ const rows = all(...args);
955
+ probe.rows += rows.length;
956
+ return rows;
957
+ };
958
+ const range = driver.hasRangeStmt.get.bind(driver.hasRangeStmt);
959
+ driver.hasRangeStmt.get = (...args) => {
960
+ probe.checks++;
961
+ return range(...args);
962
+ };
963
+ return probe;
964
+ }
965
+
966
+ it('matches the generic path for windows and pathological ids', async function () {
967
+ for (const ids of ID_SETS) {
968
+ await seedRecords(ids);
969
+ for (const chain of CHAINS) {
970
+ for (const terminal of ['toArray', 'first', 'count', 'any']) {
971
+ const native = await chain(db.query('users'))[terminal]();
972
+ const generic = await chain(reference.query('users'))[terminal]();
973
+ assert.deepStrictEqual(native, generic, `${JSON.stringify(ids)} / ${terminal}`);
974
+ }
975
+ }
976
+ }
977
+ });
978
+
979
+ it('keeps the record order for ids that break the key order', async function () {
980
+ await seedRecords(['u1', 'u1!', 'u2']);
981
+ assert.deepStrictEqual(
982
+ (await db.query('users').take(1).toArray()).map(record => record.id),
983
+ ['u1'],
984
+ );
985
+ assert.deepStrictEqual(
986
+ (await db.query('users').skip(1).toArray()).map(record => record.id),
987
+ ['u1!', 'u2'],
988
+ );
989
+
990
+ await seedRecords(['.hidden', '0alpha', 'z']);
991
+ assert.deepStrictEqual(
992
+ (await db.query('users').take(1).toArray()).map(record => record.id),
993
+ ['.hidden'],
994
+ );
995
+ });
996
+
997
+ it('reads only the records the window keeps', async function () {
998
+ await seedRecords(countRecords(60));
999
+
1000
+ const nativeProbe = instrument(db.getDriver(0));
1001
+ const native = await db.query('users').take(5).toArray();
1002
+ const genericProbe = instrument(reference.getDriver(0));
1003
+ const generic = await reference.query('users').take(5).toArray();
1004
+
1005
+ assert.deepStrictEqual(native, generic);
1006
+ assert.strictEqual(native.length, 5);
1007
+ assert.ok(nativeProbe.checks > 0, 'the window guard did not run');
1008
+ assert.ok(nativeProbe.rows <= 20, `native window read ${nativeProbe.rows} child rows`);
1009
+ assert.ok(genericProbe.rows >= 120, `generic path read only ${genericProbe.rows} child rows`);
1010
+ });
1011
+
1012
+ it('answers first() and any() from the window', async function () {
1013
+ await seedRecords(countRecords(60));
1014
+
1015
+ const probe = instrument(db.getDriver(0));
1016
+ assert.deepStrictEqual(await db.query('users').first(), {
1017
+ id: 'u000',
1018
+ value: { name: 'name u000', age: 0, address: { city: 'Rosario' } },
1019
+ });
1020
+ assert.strictEqual(await db.query('users').any(), true);
1021
+ assert.ok(probe.checks > 0, 'first()/any() did not use the window');
1022
+ assert.ok(probe.rows <= 8, `first()/any() read ${probe.rows} child rows`);
1023
+ });
1024
+
1025
+ it('leaves chains that another step opens on the generic path', async function () {
1026
+ await seedRecords(countRecords(20));
1027
+
1028
+ const probe = instrument(db.getDriver(0));
1029
+ const filtered = await db.query('users').where('age', '=', 7).take(2).toArray();
1030
+ assert.strictEqual(filtered.length, 1);
1031
+ assert.strictEqual(probe.checks, 0);
1032
+
1033
+ const ordered = await db.query('users').orderBy('age').take(2).toArray();
1034
+ assert.strictEqual(ordered.length, 2);
1035
+ assert.strictEqual(probe.checks, 0);
1036
+
1037
+ const deep = await db.query('users').skip(500).take(2).toArray();
1038
+ assert.deepStrictEqual(deep, []);
1039
+ // A skip past the end still answers from the key index, without
1040
+ // rebuilding the records it drops.
1041
+ assert.ok(probe.checks > 0, 'skip past the end did not use the window');
1042
+ });
1043
+ });
861
1044
  });
862
1045
  }
863
1046