sorodb 0.0.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/CONTRIBUTING.md +33 -0
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/docs/explanation/architecture.md +34 -0
- package/docs/getting-started.md +68 -0
- package/docs/guides/indexes-and-sorting.md +70 -0
- package/docs/guides/pagination.md +47 -0
- package/docs/guides/schema-upgrades.md +67 -0
- package/docs/guides/transactions.md +38 -0
- package/docs/reference/api.md +90 -0
- package/docs/reference/schema-and-query.md +87 -0
- package/examples/basic.js +26 -0
- package/package.json +45 -0
- package/src/collection.js +90 -0
- package/src/encoding.js +200 -0
- package/src/errors.js +43 -0
- package/src/index-methods.js +131 -0
- package/src/index.js +16 -0
- package/src/indexes.js +32 -0
- package/src/query.js +493 -0
- package/src/schema.js +247 -0
- package/src/storage.js +334 -0
- package/src/types.js +29 -0
- package/types/collection.d.ts +20 -0
- package/types/encoding.d.ts +32 -0
- package/types/errors.d.ts +26 -0
- package/types/index-methods.d.ts +16 -0
- package/types/index.d.ts +6 -0
- package/types/indexes.d.ts +4 -0
- package/types/query.d.ts +49 -0
- package/types/schema.d.ts +8 -0
- package/types/storage.d.ts +58 -0
- package/types/types.d.ts +160 -0
package/src/query.js
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { CursorError, QueryError } from "./errors.js";
|
|
4
|
+
import { decodeData, docKey, docPrefix, getPath, indexPrefix, stable } from "./encoding.js";
|
|
5
|
+
import { indexMethod } from "./index-methods.js";
|
|
6
|
+
import { scan } from "./storage.js";
|
|
7
|
+
|
|
8
|
+
/** @param {string} path */
|
|
9
|
+
const checkPath = (path) => {
|
|
10
|
+
if (typeof path !== "string" || !/^[A-Za-z][A-Za-z0-9_.]*$/.test(path) || path.includes("..")) {
|
|
11
|
+
throw new QueryError("Field path must be a dotted name");
|
|
12
|
+
}
|
|
13
|
+
return path;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** @returns {import("./types.js").WhereBuilder} */
|
|
17
|
+
function whereBuilder() {
|
|
18
|
+
/** @param {import("./types.js").Predicate["op"]} op */
|
|
19
|
+
const comparison = (op) => {
|
|
20
|
+
/** @param {string} path @param {unknown} value */
|
|
21
|
+
return (path, value) => ({ op, path: checkPath(path), value });
|
|
22
|
+
};
|
|
23
|
+
return Object.freeze({
|
|
24
|
+
eq: comparison("eq"),
|
|
25
|
+
ne: comparison("ne"),
|
|
26
|
+
gt: comparison("gt"),
|
|
27
|
+
gte: comparison("gte"),
|
|
28
|
+
lt: comparison("lt"),
|
|
29
|
+
lte: comparison("lte"),
|
|
30
|
+
in: comparison("in"),
|
|
31
|
+
contains: comparison("contains"),
|
|
32
|
+
startsWith: comparison("startsWith"),
|
|
33
|
+
exists: (path, value = true) => ({
|
|
34
|
+
op: "exists",
|
|
35
|
+
path: checkPath(path),
|
|
36
|
+
value: value === true,
|
|
37
|
+
}),
|
|
38
|
+
and: (...conditions) => ({ op: "and", conditions }),
|
|
39
|
+
or: (...conditions) => ({ op: "or", conditions }),
|
|
40
|
+
not: (condition) => ({ op: "not", condition }),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {((builder: import("./types.js").OrderBuilder) => unknown) | undefined} callback @returns {import("./types.js").IndexField[]} */
|
|
45
|
+
function compileOrder(callback) {
|
|
46
|
+
if (callback === undefined) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
if (typeof callback !== "function") {
|
|
50
|
+
throw new QueryError("orderBy must be a callback");
|
|
51
|
+
}
|
|
52
|
+
/** @type {import("./types.js").IndexField[]} */
|
|
53
|
+
const fields = [];
|
|
54
|
+
/** @type {import("./types.js").OrderBuilder} */
|
|
55
|
+
const builder = {
|
|
56
|
+
asc(path) {
|
|
57
|
+
fields.push({ name: checkPath(path), direction: "asc" });
|
|
58
|
+
return builder;
|
|
59
|
+
},
|
|
60
|
+
desc(path) {
|
|
61
|
+
fields.push({ name: checkPath(path), direction: "desc" });
|
|
62
|
+
return builder;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
callback(builder);
|
|
66
|
+
if (!fields.length || new Set(fields.map((field) => field.name)).size !== fields.length) {
|
|
67
|
+
throw new QueryError("orderBy must name distinct fields");
|
|
68
|
+
}
|
|
69
|
+
return fields;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @param {((builder: import("./types.js").WhereBuilder) => import("./types.js").WhereNode) | undefined} callback @returns {import("./types.js").WhereNode | null} */
|
|
73
|
+
function compileWhere(callback) {
|
|
74
|
+
if (callback === undefined) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
if (typeof callback !== "function") {
|
|
78
|
+
throw new QueryError("where must be a callback");
|
|
79
|
+
}
|
|
80
|
+
const ast = callback(whereBuilder());
|
|
81
|
+
/** @param {any} node */
|
|
82
|
+
function check(node) {
|
|
83
|
+
if (!node || typeof node !== "object" || typeof node.op !== "string") {
|
|
84
|
+
throw new QueryError("where must return a builder expression");
|
|
85
|
+
}
|
|
86
|
+
if (node.op === "and" || node.op === "or") {
|
|
87
|
+
if (!Array.isArray(node.conditions) || !node.conditions.length) {
|
|
88
|
+
throw new QueryError(`${node.op} needs conditions`);
|
|
89
|
+
}
|
|
90
|
+
node.conditions.forEach(check);
|
|
91
|
+
} else if (node.op === "not") {
|
|
92
|
+
check(node.condition);
|
|
93
|
+
} else if (
|
|
94
|
+
!["eq", "ne", "gt", "gte", "lt", "lte", "in", "contains", "startsWith", "exists"].includes(
|
|
95
|
+
node.op,
|
|
96
|
+
)
|
|
97
|
+
) {
|
|
98
|
+
throw new QueryError(`Unknown where operator ${node.op}`);
|
|
99
|
+
}
|
|
100
|
+
if (node.op === "in" && !Array.isArray(node.value)) {
|
|
101
|
+
throw new QueryError("in requires an array");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
check(ast);
|
|
105
|
+
return ast;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @param {unknown} a @param {unknown} b */
|
|
109
|
+
function same(a, b) {
|
|
110
|
+
return stable(a) === stable(b);
|
|
111
|
+
}
|
|
112
|
+
/** @param {unknown} a @param {unknown} b */
|
|
113
|
+
function compare(a, b) {
|
|
114
|
+
if (a instanceof Date && b instanceof Date) {
|
|
115
|
+
return a.getTime() - b.getTime();
|
|
116
|
+
}
|
|
117
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
118
|
+
if (a < b) {
|
|
119
|
+
return -1;
|
|
120
|
+
}
|
|
121
|
+
if (a > b) {
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
127
|
+
if (a < b) {
|
|
128
|
+
return -1;
|
|
129
|
+
}
|
|
130
|
+
if (a > b) {
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
if (typeof a === "bigint" && typeof b === "bigint") {
|
|
136
|
+
if (a < b) {
|
|
137
|
+
return -1;
|
|
138
|
+
}
|
|
139
|
+
if (a > b) {
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
if (a instanceof Uint8Array && b instanceof Uint8Array) {
|
|
145
|
+
return Buffer.compare(a, b);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @param {import("./types.js").WhereNode | null} node @param {import("./types.js").Document} doc @returns {boolean} */
|
|
151
|
+
function matches(node, doc) {
|
|
152
|
+
if (!node) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (node.op === "and") {
|
|
156
|
+
return node.conditions.every((child) => matches(child, doc));
|
|
157
|
+
}
|
|
158
|
+
if (node.op === "or") {
|
|
159
|
+
return node.conditions.some((child) => matches(child, doc));
|
|
160
|
+
}
|
|
161
|
+
if (node.op === "not") {
|
|
162
|
+
return !matches(node.condition, doc);
|
|
163
|
+
}
|
|
164
|
+
const value = getPath(doc, node.path);
|
|
165
|
+
switch (node.op) {
|
|
166
|
+
case "eq":
|
|
167
|
+
return same(value, node.value);
|
|
168
|
+
case "ne":
|
|
169
|
+
return !same(value, node.value);
|
|
170
|
+
case "gt": {
|
|
171
|
+
const result = compare(value, node.value);
|
|
172
|
+
return result !== null && result > 0;
|
|
173
|
+
}
|
|
174
|
+
case "gte": {
|
|
175
|
+
const result = compare(value, node.value);
|
|
176
|
+
return result !== null && result >= 0;
|
|
177
|
+
}
|
|
178
|
+
case "lt": {
|
|
179
|
+
const result = compare(value, node.value);
|
|
180
|
+
return result !== null && result < 0;
|
|
181
|
+
}
|
|
182
|
+
case "lte": {
|
|
183
|
+
const result = compare(value, node.value);
|
|
184
|
+
return result !== null && result <= 0;
|
|
185
|
+
}
|
|
186
|
+
case "in":
|
|
187
|
+
return Array.isArray(node.value) && node.value.some((item) => same(value, item));
|
|
188
|
+
case "contains":
|
|
189
|
+
if (typeof value === "string" && typeof node.value === "string") {
|
|
190
|
+
return value.includes(node.value);
|
|
191
|
+
}
|
|
192
|
+
return Array.isArray(value) && value.some((item) => same(item, node.value));
|
|
193
|
+
case "startsWith":
|
|
194
|
+
return (
|
|
195
|
+
typeof value === "string" && typeof node.value === "string" && value.startsWith(node.value)
|
|
196
|
+
);
|
|
197
|
+
case "exists":
|
|
198
|
+
return (value !== undefined) === node.value;
|
|
199
|
+
default:
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @param {import("./types.js").Document} doc @param {string[] | undefined} fields @returns {import("./types.js").Document} */
|
|
205
|
+
function select(doc, fields) {
|
|
206
|
+
if (!fields) {
|
|
207
|
+
return doc;
|
|
208
|
+
}
|
|
209
|
+
/** @type {import("./types.js").Document} */
|
|
210
|
+
const selected = { id: doc.id };
|
|
211
|
+
for (const path of fields) {
|
|
212
|
+
const value = getPath(doc, path);
|
|
213
|
+
if (value === undefined) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const names = path.split(".");
|
|
217
|
+
/** @type {Record<string, any>} */
|
|
218
|
+
let target = selected;
|
|
219
|
+
for (let i = 0; i < names.length - 1; i++) {
|
|
220
|
+
target = target[names[i]] ??= {};
|
|
221
|
+
}
|
|
222
|
+
target[names[names.length - 1]] = value;
|
|
223
|
+
}
|
|
224
|
+
return selected;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** @param {import("./types.js").WhereNode | null} node @returns {import("./types.js").Predicate | null} */
|
|
228
|
+
function firstEquality(node) {
|
|
229
|
+
if (!node) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (node.op === "eq") {
|
|
233
|
+
return node;
|
|
234
|
+
}
|
|
235
|
+
if (node.op === "and") {
|
|
236
|
+
return node.conditions.map(firstEquality).find(Boolean) ?? null;
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** @param {import("./types.js").WhereNode | null} node @param {string} path @returns {import("./types.js").Predicate | null} */
|
|
242
|
+
function equalityFor(node, path) {
|
|
243
|
+
if (!node) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
if (node.op === "eq" && node.path === path) {
|
|
247
|
+
return node;
|
|
248
|
+
}
|
|
249
|
+
if (node.op === "and") {
|
|
250
|
+
return node.conditions.map((child) => equalityFor(child, path)).find(Boolean) ?? null;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** @param {string} id @param {Buffer} last @param {string} fingerprint */
|
|
256
|
+
function makeCursor(id, last, fingerprint) {
|
|
257
|
+
return Buffer.from(JSON.stringify({ id, last: last.toString("base64"), fingerprint })).toString(
|
|
258
|
+
"base64url",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** @param {string} token @returns {import("./types.js").Cursor} */
|
|
263
|
+
function readCursor(token) {
|
|
264
|
+
try {
|
|
265
|
+
const value = JSON.parse(Buffer.from(token, "base64url").toString("utf8"));
|
|
266
|
+
if (
|
|
267
|
+
typeof value.id !== "string" ||
|
|
268
|
+
typeof value.last !== "string" ||
|
|
269
|
+
typeof value.fingerprint !== "string"
|
|
270
|
+
) {
|
|
271
|
+
throw new Error();
|
|
272
|
+
}
|
|
273
|
+
return { ...value, last: Buffer.from(value.last, "base64") };
|
|
274
|
+
} catch {
|
|
275
|
+
throw new CursorError("Invalid cursor");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export class Query {
|
|
280
|
+
/** @param {import("./collection.js").Collection} collection @param {import("./types.js").QueryOptions} options */
|
|
281
|
+
constructor(collection, options) {
|
|
282
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
283
|
+
throw new QueryError("filter options must be an object");
|
|
284
|
+
}
|
|
285
|
+
this.collection = collection;
|
|
286
|
+
this.ast = compileWhere(options.where);
|
|
287
|
+
this.order = compileOrder(options.orderBy);
|
|
288
|
+
this.select = options.select;
|
|
289
|
+
if (
|
|
290
|
+
this.select !== undefined &&
|
|
291
|
+
(!Array.isArray(this.select) || !this.select.every((path) => typeof path === "string"))
|
|
292
|
+
) {
|
|
293
|
+
throw new QueryError("select must be an array of field paths");
|
|
294
|
+
}
|
|
295
|
+
this.select?.forEach(checkPath);
|
|
296
|
+
if (
|
|
297
|
+
options.limit !== undefined &&
|
|
298
|
+
(!Number.isSafeInteger(options.limit) || options.limit < 1)
|
|
299
|
+
) {
|
|
300
|
+
throw new QueryError("limit must be a positive integer");
|
|
301
|
+
}
|
|
302
|
+
this.limit = options.limit;
|
|
303
|
+
this.cursor = options.cursor;
|
|
304
|
+
if (this.cursor !== undefined && typeof this.cursor !== "string") {
|
|
305
|
+
throw new CursorError("cursor must be a string");
|
|
306
|
+
}
|
|
307
|
+
this.fingerprint = createHash("sha256")
|
|
308
|
+
.update(
|
|
309
|
+
stable({
|
|
310
|
+
table: collection.table.name,
|
|
311
|
+
ast: this.ast,
|
|
312
|
+
order: this.order,
|
|
313
|
+
select: this.select ?? null,
|
|
314
|
+
}),
|
|
315
|
+
)
|
|
316
|
+
.digest("hex");
|
|
317
|
+
this.plan = this._plan();
|
|
318
|
+
this.sessionId = null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
_plan() {
|
|
322
|
+
const table = this.collection.table;
|
|
323
|
+
if (this.order.length) {
|
|
324
|
+
if (
|
|
325
|
+
this.order.length === 1 &&
|
|
326
|
+
this.order[0].name === "id" &&
|
|
327
|
+
this.order[0].direction === "asc"
|
|
328
|
+
) {
|
|
329
|
+
return { prefix: docPrefix(table.name), index: null };
|
|
330
|
+
}
|
|
331
|
+
const index = table.indexes.find((candidate) =>
|
|
332
|
+
indexMethod(candidate.method).supportsOrder(candidate, this.order),
|
|
333
|
+
);
|
|
334
|
+
if (!index) {
|
|
335
|
+
throw new QueryError("orderBy requires a matching schema index");
|
|
336
|
+
}
|
|
337
|
+
return { prefix: indexPrefix(table.name, index.name), index };
|
|
338
|
+
}
|
|
339
|
+
const idEquality = equalityFor(this.ast, "id");
|
|
340
|
+
if (
|
|
341
|
+
idEquality &&
|
|
342
|
+
(typeof idEquality.value === "string" || typeof idEquality.value === "number")
|
|
343
|
+
) {
|
|
344
|
+
return { key: docKey(table.name, idEquality.value), index: null };
|
|
345
|
+
}
|
|
346
|
+
const equality = firstEquality(this.ast);
|
|
347
|
+
if (equality) {
|
|
348
|
+
for (const index of table.indexes) {
|
|
349
|
+
const prefix = indexMethod(index.method).equalityPrefix(
|
|
350
|
+
table.name,
|
|
351
|
+
index,
|
|
352
|
+
equality.path,
|
|
353
|
+
equality.value,
|
|
354
|
+
);
|
|
355
|
+
if (prefix) {
|
|
356
|
+
return { prefix, index };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { prefix: docPrefix(table.name), index: null };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** @param {import("./types.js").Reader} reader @param {Uint8Array} [after] */
|
|
364
|
+
async *_rows(reader, after) {
|
|
365
|
+
if (this.plan.key) {
|
|
366
|
+
if (after) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const bytes = await reader.get(this.plan.key);
|
|
370
|
+
if (bytes) {
|
|
371
|
+
const doc = decodeData(bytes);
|
|
372
|
+
if (matches(this.ast, doc)) {
|
|
373
|
+
yield { key: this.plan.key, value: select(doc, this.select) };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
for await (const row of scan(reader, this.plan.prefix, after)) {
|
|
379
|
+
let bytes;
|
|
380
|
+
if (this.plan.index) {
|
|
381
|
+
bytes = await reader.get(row.value);
|
|
382
|
+
} else {
|
|
383
|
+
bytes = row.value;
|
|
384
|
+
}
|
|
385
|
+
if (!bytes) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const doc = decodeData(bytes);
|
|
389
|
+
if (matches(this.ast, doc)) {
|
|
390
|
+
yield { key: row.key, value: select(doc, this.select) };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async page() {
|
|
396
|
+
if (this.collection.transaction) {
|
|
397
|
+
throw new QueryError("page() is unavailable inside a transaction; use async iteration");
|
|
398
|
+
}
|
|
399
|
+
const db = this.collection.db;
|
|
400
|
+
const pageSize = this.limit ?? 100;
|
|
401
|
+
let id, session, after;
|
|
402
|
+
if (this.cursor) {
|
|
403
|
+
const cursor = readCursor(this.cursor);
|
|
404
|
+
if (cursor.fingerprint !== this.fingerprint) {
|
|
405
|
+
throw new CursorError("Cursor belongs to a different query");
|
|
406
|
+
}
|
|
407
|
+
id = cursor.id;
|
|
408
|
+
after = cursor.last;
|
|
409
|
+
session = db._getSession(id, this.fingerprint);
|
|
410
|
+
} else {
|
|
411
|
+
({ id, session } = await db._newSession(this.fingerprint));
|
|
412
|
+
}
|
|
413
|
+
this.sessionId = id;
|
|
414
|
+
db._holdSession(session);
|
|
415
|
+
const items = [];
|
|
416
|
+
let last;
|
|
417
|
+
let more = false;
|
|
418
|
+
try {
|
|
419
|
+
for await (const row of this._rows(session.snapshot, after)) {
|
|
420
|
+
if (items.length === pageSize) {
|
|
421
|
+
more = true;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
items.push(row.value);
|
|
425
|
+
last = row.key;
|
|
426
|
+
}
|
|
427
|
+
if (!more) {
|
|
428
|
+
db._releaseSession(id);
|
|
429
|
+
this.sessionId = null;
|
|
430
|
+
}
|
|
431
|
+
let nextCursor = null;
|
|
432
|
+
if (more && last) {
|
|
433
|
+
nextCursor = makeCursor(id, last, this.fingerprint);
|
|
434
|
+
}
|
|
435
|
+
return { items, nextCursor };
|
|
436
|
+
} catch (error) {
|
|
437
|
+
db._releaseSession(id);
|
|
438
|
+
this.sessionId = null;
|
|
439
|
+
throw error;
|
|
440
|
+
} finally {
|
|
441
|
+
db._idleSession(id);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async *[Symbol.asyncIterator]() {
|
|
446
|
+
const db = this.collection.db;
|
|
447
|
+
await db._ready();
|
|
448
|
+
if (!db.native) {
|
|
449
|
+
throw new QueryError("Database is not open");
|
|
450
|
+
}
|
|
451
|
+
let reader,
|
|
452
|
+
id = null,
|
|
453
|
+
after;
|
|
454
|
+
if (this.cursor) {
|
|
455
|
+
if (this.collection.transaction) {
|
|
456
|
+
throw new QueryError("Cursors are unavailable inside a transaction");
|
|
457
|
+
}
|
|
458
|
+
const cursor = readCursor(this.cursor);
|
|
459
|
+
if (cursor.fingerprint !== this.fingerprint) {
|
|
460
|
+
throw new CursorError("Cursor belongs to a different query");
|
|
461
|
+
}
|
|
462
|
+
id = cursor.id;
|
|
463
|
+
after = cursor.last;
|
|
464
|
+
const session = db._getSession(id, this.fingerprint);
|
|
465
|
+
db._holdSession(session);
|
|
466
|
+
reader = session.snapshot;
|
|
467
|
+
} else {
|
|
468
|
+
reader = this.collection.transaction ?? (await db.native.snapshot());
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
let count = 0;
|
|
472
|
+
for await (const row of this._rows(reader, after)) {
|
|
473
|
+
yield row.value;
|
|
474
|
+
if (++count === this.limit) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} finally {
|
|
479
|
+
if (id) {
|
|
480
|
+
db._releaseSession(id);
|
|
481
|
+
} else if (!this.collection.transaction) {
|
|
482
|
+
reader.dispose();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
close() {
|
|
488
|
+
if (this.sessionId) {
|
|
489
|
+
this.collection.db._releaseSession(this.sessionId);
|
|
490
|
+
}
|
|
491
|
+
this.sessionId = null;
|
|
492
|
+
}
|
|
493
|
+
}
|