@yanit/jsondb 0.1.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 +903 -0
- package/dist/bin/cli-export.d.ts +7 -0
- package/dist/bin/cli-export.d.ts.map +1 -0
- package/dist/bin/cli-export.js +318 -0
- package/dist/bin/cli-export.js.map +1 -0
- package/dist/bin/cli-import.d.ts +7 -0
- package/dist/bin/cli-import.d.ts.map +1 -0
- package/dist/bin/cli-import.js +298 -0
- package/dist/bin/cli-import.js.map +1 -0
- package/dist/bin/server.d.ts +7 -0
- package/dist/bin/server.d.ts.map +1 -0
- package/dist/bin/server.js +92 -0
- package/dist/bin/server.js.map +1 -0
- package/dist/examples/sql-example.d.ts +7 -0
- package/dist/examples/sql-example.d.ts.map +1 -0
- package/dist/examples/sql-example.js +131 -0
- package/dist/examples/sql-example.js.map +1 -0
- package/dist/src/BulkOp.d.ts +74 -0
- package/dist/src/BulkOp.d.ts.map +1 -0
- package/dist/src/BulkOp.js +143 -0
- package/dist/src/BulkOp.js.map +1 -0
- package/dist/src/Collection.d.ts +232 -0
- package/dist/src/Collection.d.ts.map +1 -0
- package/dist/src/Collection.js +705 -0
- package/dist/src/Collection.js.map +1 -0
- package/dist/src/Cursor.d.ts +94 -0
- package/dist/src/Cursor.d.ts.map +1 -0
- package/dist/src/Cursor.js +259 -0
- package/dist/src/Cursor.js.map +1 -0
- package/dist/src/Database.d.ts +98 -0
- package/dist/src/Database.d.ts.map +1 -0
- package/dist/src/Database.js +198 -0
- package/dist/src/Database.js.map +1 -0
- package/dist/src/Operators.d.ts +73 -0
- package/dist/src/Operators.d.ts.map +1 -0
- package/dist/src/Operators.js +339 -0
- package/dist/src/Operators.js.map +1 -0
- package/dist/src/QueryCache.d.ts +87 -0
- package/dist/src/QueryCache.d.ts.map +1 -0
- package/dist/src/QueryCache.js +155 -0
- package/dist/src/QueryCache.js.map +1 -0
- package/dist/src/SQLExecutor.d.ts +60 -0
- package/dist/src/SQLExecutor.d.ts.map +1 -0
- package/dist/src/SQLExecutor.js +317 -0
- package/dist/src/SQLExecutor.js.map +1 -0
- package/dist/src/SQLParser.d.ts +181 -0
- package/dist/src/SQLParser.d.ts.map +1 -0
- package/dist/src/SQLParser.js +640 -0
- package/dist/src/SQLParser.js.map +1 -0
- package/dist/src/Schema.d.ts +92 -0
- package/dist/src/Schema.d.ts.map +1 -0
- package/dist/src/Schema.js +253 -0
- package/dist/src/Schema.js.map +1 -0
- package/dist/src/Transaction.d.ts +118 -0
- package/dist/src/Transaction.d.ts.map +1 -0
- package/dist/src/Transaction.js +233 -0
- package/dist/src/Transaction.js.map +1 -0
- package/dist/src/Utils.d.ts +68 -0
- package/dist/src/Utils.d.ts.map +1 -0
- package/dist/src/Utils.js +187 -0
- package/dist/src/Utils.js.map +1 -0
- package/dist/src/errors.d.ts +58 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +85 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +39 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +44 -0
- package/dist/src/index.js.map +1 -0
- package/dist/test/basic.test.d.ts +5 -0
- package/dist/test/basic.test.d.ts.map +1 -0
- package/dist/test/basic.test.js +283 -0
- package/dist/test/basic.test.js.map +1 -0
- package/dist/test/index.test.d.ts +5 -0
- package/dist/test/index.test.d.ts.map +1 -0
- package/dist/test/index.test.js +126 -0
- package/dist/test/index.test.js.map +1 -0
- package/dist/test/jsonb.test.d.ts +5 -0
- package/dist/test/jsonb.test.d.ts.map +1 -0
- package/dist/test/jsonb.test.js +165 -0
- package/dist/test/jsonb.test.js.map +1 -0
- package/dist/test/optimization.test.d.ts +6 -0
- package/dist/test/optimization.test.d.ts.map +1 -0
- package/dist/test/optimization.test.js +196 -0
- package/dist/test/optimization.test.js.map +1 -0
- package/dist/test/schema.test.d.ts +5 -0
- package/dist/test/schema.test.d.ts.map +1 -0
- package/dist/test/schema.test.js +197 -0
- package/dist/test/schema.test.js.map +1 -0
- package/dist/test/sql.test.d.ts +7 -0
- package/dist/test/sql.test.d.ts.map +1 -0
- package/dist/test/sql.test.js +21 -0
- package/dist/test/sql.test.js.map +1 -0
- package/package.json +73 -0
- package/src/BulkOp.js +181 -0
- package/src/BulkOp.ts +191 -0
- package/src/Collection.js +843 -0
- package/src/Collection.ts +896 -0
- package/src/Cursor.js +315 -0
- package/src/Cursor.ts +319 -0
- package/src/Database.js +244 -0
- package/src/Database.ts +268 -0
- package/src/Operators.js +382 -0
- package/src/Operators.ts +375 -0
- package/src/QueryCache.js +190 -0
- package/src/QueryCache.ts +208 -0
- package/src/SQLExecutor.ts +391 -0
- package/src/SQLParser.ts +814 -0
- package/src/Schema.js +292 -0
- package/src/Schema.ts +317 -0
- package/src/Transaction.js +291 -0
- package/src/Transaction.ts +313 -0
- package/src/Utils.js +205 -0
- package/src/Utils.ts +205 -0
- package/src/errors.js +93 -0
- package/src/errors.ts +93 -0
- package/src/index.js +90 -0
- package/src/index.ts +106 -0
package/src/Cursor.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 游标类 - 用于处理查询结果
|
|
3
|
+
* 支持 async/await 异步操作
|
|
4
|
+
* 支持索引查询优化
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { matchQuery } from './Operators.js';
|
|
8
|
+
import { deepClone, createSortFunction, projectDocument, getNestedValue } from './Utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cursor 类
|
|
12
|
+
* 提供链式查询接口和结果迭代
|
|
13
|
+
*/
|
|
14
|
+
export class Cursor {
|
|
15
|
+
/**
|
|
16
|
+
* @param {Collection} collection - 集合实例
|
|
17
|
+
* @param {Object} query - 查询条件
|
|
18
|
+
* @param {Object} options - 查询选项
|
|
19
|
+
*/
|
|
20
|
+
constructor(collection, query = {}, options = {}) {
|
|
21
|
+
this.collection = collection;
|
|
22
|
+
this.query = query;
|
|
23
|
+
this.options = { ...options };
|
|
24
|
+
this._executed = false;
|
|
25
|
+
this._results = null;
|
|
26
|
+
this._currentIndex = 0;
|
|
27
|
+
this._useIndex = options.useIndex !== false; // 默认使用索引
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 指定使用哪个索引
|
|
32
|
+
* @param {string} indexName - 索引名称
|
|
33
|
+
* @returns {Cursor}
|
|
34
|
+
*/
|
|
35
|
+
hint(indexName) {
|
|
36
|
+
this.options.hint = indexName;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 禁用索引
|
|
42
|
+
* @returns {Cursor}
|
|
43
|
+
*/
|
|
44
|
+
noIndex() {
|
|
45
|
+
this._useIndex = false;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 排序
|
|
51
|
+
* @param {Object} sortSpec - 排序规范,如 { age: -1, name: 1 }
|
|
52
|
+
* @returns {Cursor} 返回自身以支持链式调用
|
|
53
|
+
*/
|
|
54
|
+
sort(sortSpec) {
|
|
55
|
+
this.options.sort = sortSpec;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 跳过指定数量的文档
|
|
61
|
+
* @param {number} count - 跳过数量
|
|
62
|
+
* @returns {Cursor}
|
|
63
|
+
*/
|
|
64
|
+
skip(count) {
|
|
65
|
+
this.options.skip = count;
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 限制返回文档数量
|
|
71
|
+
* @param {number} count - 限制数量
|
|
72
|
+
* @returns {Cursor}
|
|
73
|
+
*/
|
|
74
|
+
limit(count) {
|
|
75
|
+
this.options.limit = count;
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 投影字段
|
|
81
|
+
* @param {Object} projection - 投影规范
|
|
82
|
+
* @returns {Cursor}
|
|
83
|
+
*/
|
|
84
|
+
project(projection) {
|
|
85
|
+
this.options.projection = projection;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 使用索引查找匹配的文档 IDs
|
|
91
|
+
* @private
|
|
92
|
+
* @returns {Set|null} 匹配的 ID 集合,如果无法使用索引则返回 null
|
|
93
|
+
*/
|
|
94
|
+
async _findWithIndex() {
|
|
95
|
+
if (!this._useIndex) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const indexes = await this.collection.listIndexes();
|
|
100
|
+
if (indexes.length === 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 查找可以使用的索引
|
|
105
|
+
const queryKeys = Object.keys(this.query).filter(k => !k.startsWith('$'));
|
|
106
|
+
if (queryKeys.length === 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 查找匹配的索引
|
|
111
|
+
let matchingIndex = null;
|
|
112
|
+
|
|
113
|
+
// 如果指定了 hint,使用指定的索引
|
|
114
|
+
if (this.options.hint) {
|
|
115
|
+
matchingIndex = indexes.find(i => i.name === this.options.hint);
|
|
116
|
+
} else {
|
|
117
|
+
// 否则查找最佳匹配
|
|
118
|
+
for (const index of indexes) {
|
|
119
|
+
const indexKeys = Object.keys(index.key);
|
|
120
|
+
// 检查索引是否覆盖查询字段
|
|
121
|
+
if (indexKeys.some(key => queryKeys.includes(key))) {
|
|
122
|
+
matchingIndex = index;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!matchingIndex) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 使用索引查找
|
|
133
|
+
return await this._queryIndex(matchingIndex);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 查询索引
|
|
138
|
+
* @private
|
|
139
|
+
* @param {Object} index - 索引对象
|
|
140
|
+
* @returns {Set|null} 匹配的 ID 集合
|
|
141
|
+
*/
|
|
142
|
+
async _queryIndex(index) {
|
|
143
|
+
const indexKeys = Object.keys(index.key);
|
|
144
|
+
const matchedIds = new Set();
|
|
145
|
+
let hasMatch = false;
|
|
146
|
+
|
|
147
|
+
for (const key of indexKeys) {
|
|
148
|
+
if (this.query[key] !== undefined) {
|
|
149
|
+
hasMatch = true;
|
|
150
|
+
const queryValue = this.query[key];
|
|
151
|
+
|
|
152
|
+
// 获取索引数据
|
|
153
|
+
const indexData = this.collection._data._indexes?.[index.name] || {};
|
|
154
|
+
|
|
155
|
+
// 简单相等查询
|
|
156
|
+
if (typeof queryValue !== 'object' || queryValue === null) {
|
|
157
|
+
const keyStr = String(queryValue);
|
|
158
|
+
if (indexData[keyStr]) {
|
|
159
|
+
indexData[keyStr].forEach(id => matchedIds.add(id));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// 操作符查询($eq, $in 等)
|
|
163
|
+
else if (queryValue.$eq !== undefined) {
|
|
164
|
+
const keyStr = String(queryValue.$eq);
|
|
165
|
+
if (indexData[keyStr]) {
|
|
166
|
+
indexData[keyStr].forEach(id => matchedIds.add(id));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (queryValue.$in !== undefined && Array.isArray(queryValue.$in)) {
|
|
170
|
+
queryValue.$in.forEach(val => {
|
|
171
|
+
const keyStr = String(val);
|
|
172
|
+
if (indexData[keyStr]) {
|
|
173
|
+
indexData[keyStr].forEach(id => matchedIds.add(id));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return hasMatch ? matchedIds : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 执行查询并获取结果
|
|
185
|
+
* @private
|
|
186
|
+
* @returns {Promise<Array>} 文档数组
|
|
187
|
+
*/
|
|
188
|
+
async _execute() {
|
|
189
|
+
if (this._executed) {
|
|
190
|
+
return this._results;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 尝试使用索引
|
|
194
|
+
const indexedIds = await this._findWithIndex();
|
|
195
|
+
|
|
196
|
+
let results;
|
|
197
|
+
|
|
198
|
+
if (indexedIds !== null && indexedIds.size > 0) {
|
|
199
|
+
// 使用索引优化查询
|
|
200
|
+
const allDocs = await this.collection._getDocuments();
|
|
201
|
+
const docMap = new Map(allDocs.map(doc => [doc._id, doc]));
|
|
202
|
+
|
|
203
|
+
results = [];
|
|
204
|
+
for (const id of indexedIds) {
|
|
205
|
+
const doc = docMap.get(id);
|
|
206
|
+
if (doc && matchQuery(doc, this.query)) {
|
|
207
|
+
results.push(doc);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(`[索引优化] 从 ${allDocs.length} 条记录中快速定位到 ${indexedIds.size} 条`);
|
|
212
|
+
} else {
|
|
213
|
+
// 全表扫描
|
|
214
|
+
results = await this.collection._getDocuments();
|
|
215
|
+
results = results.filter(doc => matchQuery(doc, this.query));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 排序
|
|
219
|
+
if (this.options.sort) {
|
|
220
|
+
const sortFn = createSortFunction(this.options.sort);
|
|
221
|
+
results.sort(sortFn);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 跳过
|
|
225
|
+
if (this.options.skip) {
|
|
226
|
+
results = results.slice(this.options.skip);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 限制
|
|
230
|
+
if (this.options.limit) {
|
|
231
|
+
results = results.slice(0, this.options.limit);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 投影
|
|
235
|
+
if (this.options.projection) {
|
|
236
|
+
results = results.map(doc => projectDocument(doc, this.options.projection));
|
|
237
|
+
} else {
|
|
238
|
+
// 深拷贝,避免修改原数据
|
|
239
|
+
results = results.map(doc => deepClone(doc));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this._results = results;
|
|
243
|
+
this._executed = true;
|
|
244
|
+
|
|
245
|
+
return this._results;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 获取下一个文档
|
|
250
|
+
* @returns {Promise<Object|null>} 文档或 null
|
|
251
|
+
*/
|
|
252
|
+
async next() {
|
|
253
|
+
const results = await this._execute();
|
|
254
|
+
|
|
255
|
+
if (this._currentIndex < results.length) {
|
|
256
|
+
return results[this._currentIndex++];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 转换为数组
|
|
264
|
+
* @returns {Promise<Array>} 文档数组
|
|
265
|
+
*/
|
|
266
|
+
async toArray() {
|
|
267
|
+
return await this._execute();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 遍历每个文档(异步)
|
|
272
|
+
* @param {Function} callback - 异步回调函数
|
|
273
|
+
* @returns {Promise<void>}
|
|
274
|
+
*/
|
|
275
|
+
async forEach(callback) {
|
|
276
|
+
const results = await this._execute();
|
|
277
|
+
for (let i = 0; i < results.length; i++) {
|
|
278
|
+
await callback(results[i], i);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 获取第一个文档
|
|
284
|
+
* @returns {Promise<Object|null>}
|
|
285
|
+
*/
|
|
286
|
+
async first() {
|
|
287
|
+
const results = await this._execute();
|
|
288
|
+
return results.length > 0 ? results[0] : null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 获取文档数量
|
|
293
|
+
* @returns {Promise<number>}
|
|
294
|
+
*/
|
|
295
|
+
async count() {
|
|
296
|
+
const results = await this._execute();
|
|
297
|
+
return results.length;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* [Symbol.asyncIterator] 支持 for await...of 循环
|
|
302
|
+
* @returns {AsyncIterator}
|
|
303
|
+
*/
|
|
304
|
+
[Symbol.asyncIterator]() {
|
|
305
|
+
return {
|
|
306
|
+
next: async () => {
|
|
307
|
+
const doc = await this.next();
|
|
308
|
+
return {
|
|
309
|
+
value: doc,
|
|
310
|
+
done: doc === null
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
package/src/Cursor.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 游标类 - 用于处理查询结果
|
|
3
|
+
* 支持 async/await 异步操作
|
|
4
|
+
* 支持索引查询优化
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { matchQuery } from './Operators.js';
|
|
8
|
+
import { deepClone, createSortFunction, projectDocument, getNestedValue } from './Utils.js';
|
|
9
|
+
import type { Collection } from './Collection.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 查询选项接口
|
|
13
|
+
*/
|
|
14
|
+
interface CursorOptions {
|
|
15
|
+
sort?: Record<string, number>;
|
|
16
|
+
skip?: number;
|
|
17
|
+
limit?: number;
|
|
18
|
+
projection?: Record<string, unknown>;
|
|
19
|
+
hint?: string;
|
|
20
|
+
useIndex?: boolean;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 索引接口
|
|
26
|
+
*/
|
|
27
|
+
interface Index {
|
|
28
|
+
name: string;
|
|
29
|
+
key: Record<string, number>;
|
|
30
|
+
unique?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cursor 类
|
|
35
|
+
* 提供链式查询接口和结果迭代
|
|
36
|
+
*/
|
|
37
|
+
export class Cursor {
|
|
38
|
+
collection: Collection;
|
|
39
|
+
query: Record<string, unknown>;
|
|
40
|
+
options: CursorOptions;
|
|
41
|
+
private _executed: boolean;
|
|
42
|
+
private _results: Array<Record<string, unknown>> | null;
|
|
43
|
+
private _currentIndex: number;
|
|
44
|
+
private _useIndex: boolean;
|
|
45
|
+
|
|
46
|
+
constructor(collection: Collection, query: Record<string, unknown> = {}, options: CursorOptions = {}) {
|
|
47
|
+
this.collection = collection;
|
|
48
|
+
this.query = query;
|
|
49
|
+
this.options = { ...options };
|
|
50
|
+
this._executed = false;
|
|
51
|
+
this._results = null;
|
|
52
|
+
this._currentIndex = 0;
|
|
53
|
+
this._useIndex = options.useIndex !== false; // 默认使用索引
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 指定使用哪个索引
|
|
58
|
+
*/
|
|
59
|
+
hint(indexName: string): Cursor {
|
|
60
|
+
this.options.hint = indexName;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 禁用索引
|
|
66
|
+
*/
|
|
67
|
+
noIndex(): Cursor {
|
|
68
|
+
this._useIndex = false;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 排序
|
|
74
|
+
*/
|
|
75
|
+
sort(sortSpec: Record<string, number>): Cursor {
|
|
76
|
+
this.options.sort = sortSpec;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 跳过指定数量的文档
|
|
82
|
+
*/
|
|
83
|
+
skip(count: number): Cursor {
|
|
84
|
+
this.options.skip = count;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 限制返回文档数量
|
|
90
|
+
*/
|
|
91
|
+
limit(count: number): Cursor {
|
|
92
|
+
this.options.limit = count;
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 投影字段
|
|
98
|
+
*/
|
|
99
|
+
project(projection: Record<string, unknown>): Cursor {
|
|
100
|
+
this.options.projection = projection;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 使用索引查找匹配的文档 IDs
|
|
106
|
+
*/
|
|
107
|
+
private async _findWithIndex(): Promise<Set<string> | null> {
|
|
108
|
+
if (!this._useIndex) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const indexes = await this.collection.listIndexes();
|
|
113
|
+
if (indexes.length === 0) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 查找可以使用的索引
|
|
118
|
+
const queryKeys = Object.keys(this.query).filter(k => !k.startsWith('$'));
|
|
119
|
+
if (queryKeys.length === 0) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 查找匹配的索引
|
|
124
|
+
let matchingIndex: Index | null = null;
|
|
125
|
+
|
|
126
|
+
// 如果指定了 hint,使用指定的索引
|
|
127
|
+
if (this.options.hint) {
|
|
128
|
+
matchingIndex = indexes.find(i => i.name === this.options.hint) || null;
|
|
129
|
+
} else {
|
|
130
|
+
// 否则查找最佳匹配
|
|
131
|
+
for (const index of indexes) {
|
|
132
|
+
const indexKeys = Object.keys(index.key);
|
|
133
|
+
// 检查索引是否覆盖查询字段
|
|
134
|
+
if (indexKeys.some(key => queryKeys.includes(key))) {
|
|
135
|
+
matchingIndex = index;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!matchingIndex) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 使用索引查找
|
|
146
|
+
return await this._queryIndex(matchingIndex);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 查询索引
|
|
151
|
+
*/
|
|
152
|
+
private async _queryIndex(index: Index): Promise<Set<string> | null> {
|
|
153
|
+
const indexKeys = Object.keys(index.key);
|
|
154
|
+
const matchedIds = new Set<string>();
|
|
155
|
+
let hasMatch = false;
|
|
156
|
+
|
|
157
|
+
for (const key of indexKeys) {
|
|
158
|
+
if (this.query[key] !== undefined) {
|
|
159
|
+
hasMatch = true;
|
|
160
|
+
const queryValue = this.query[key];
|
|
161
|
+
|
|
162
|
+
// 获取索引数据
|
|
163
|
+
const indexData = this.collection._data._indexes?.[index.name] || {};
|
|
164
|
+
|
|
165
|
+
// 简单相等查询
|
|
166
|
+
if (typeof queryValue !== 'object' || queryValue === null) {
|
|
167
|
+
const keyStr = String(queryValue);
|
|
168
|
+
if (indexData[keyStr]) {
|
|
169
|
+
indexData[keyStr].forEach((id: string) => matchedIds.add(id));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// 操作符查询($eq, $in 等)
|
|
173
|
+
else {
|
|
174
|
+
const qv = queryValue as Record<string, unknown>;
|
|
175
|
+
if (qv.$eq !== undefined) {
|
|
176
|
+
const keyStr = String(qv.$eq);
|
|
177
|
+
if (indexData[keyStr]) {
|
|
178
|
+
indexData[keyStr].forEach((id: string) => matchedIds.add(id));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (qv.$in !== undefined && Array.isArray(qv.$in)) {
|
|
182
|
+
qv.$in.forEach((val: unknown) => {
|
|
183
|
+
const keyStr = String(val);
|
|
184
|
+
if (indexData[keyStr]) {
|
|
185
|
+
indexData[keyStr].forEach((id: string) => matchedIds.add(id));
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return hasMatch ? matchedIds : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 执行查询并获取结果
|
|
198
|
+
*/
|
|
199
|
+
private async _execute(): Promise<Array<Record<string, unknown>>> {
|
|
200
|
+
if (this._executed) {
|
|
201
|
+
return this._results!;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 尝试使用索引
|
|
205
|
+
const indexedIds = await this._findWithIndex();
|
|
206
|
+
|
|
207
|
+
let results: Array<Record<string, unknown>>;
|
|
208
|
+
|
|
209
|
+
if (indexedIds !== null && indexedIds.size > 0) {
|
|
210
|
+
// 使用索引优化查询
|
|
211
|
+
const allDocs = await this.collection._getDocuments();
|
|
212
|
+
const docMap = new Map(allDocs.map(doc => [doc._id as string, doc]));
|
|
213
|
+
|
|
214
|
+
results = [];
|
|
215
|
+
for (const id of indexedIds) {
|
|
216
|
+
const doc = docMap.get(id);
|
|
217
|
+
if (doc && matchQuery(doc, this.query)) {
|
|
218
|
+
results.push(doc);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(`[索引优化] 从 ${allDocs.length} 条记录中快速定位到 ${indexedIds.size} 条`);
|
|
223
|
+
} else {
|
|
224
|
+
// 全表扫描
|
|
225
|
+
results = await this.collection._getDocuments();
|
|
226
|
+
results = results.filter(doc => matchQuery(doc, this.query));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 排序
|
|
230
|
+
if (this.options.sort) {
|
|
231
|
+
const sortFn = createSortFunction(this.options.sort);
|
|
232
|
+
results.sort(sortFn);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 跳过
|
|
236
|
+
if (this.options.skip) {
|
|
237
|
+
results = results.slice(this.options.skip);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 限制
|
|
241
|
+
if (this.options.limit) {
|
|
242
|
+
results = results.slice(0, this.options.limit);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 投影
|
|
246
|
+
if (this.options.projection) {
|
|
247
|
+
results = results.map(doc => projectDocument(doc, this.options.projection!));
|
|
248
|
+
} else {
|
|
249
|
+
// 深拷贝,避免修改原数据
|
|
250
|
+
results = results.map(doc => deepClone(doc));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this._results = results;
|
|
254
|
+
this._executed = true;
|
|
255
|
+
|
|
256
|
+
return this._results;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 获取下一个文档
|
|
261
|
+
*/
|
|
262
|
+
async next(): Promise<Record<string, unknown> | null> {
|
|
263
|
+
const results = await this._execute();
|
|
264
|
+
|
|
265
|
+
if (this._currentIndex < results.length) {
|
|
266
|
+
return results[this._currentIndex++];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 转换为数组
|
|
274
|
+
*/
|
|
275
|
+
async toArray(): Promise<Array<Record<string, unknown>>> {
|
|
276
|
+
return await this._execute();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 遍历每个文档(异步)
|
|
281
|
+
*/
|
|
282
|
+
async forEach(callback: (doc: Record<string, unknown>, index: number) => Promise<void>): Promise<void> {
|
|
283
|
+
const results = await this._execute();
|
|
284
|
+
for (let i = 0; i < results.length; i++) {
|
|
285
|
+
await callback(results[i], i);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 获取第一个文档
|
|
291
|
+
*/
|
|
292
|
+
async first(): Promise<Record<string, unknown> | null> {
|
|
293
|
+
const results = await this._execute();
|
|
294
|
+
return results.length > 0 ? results[0] : null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 获取文档数量
|
|
299
|
+
*/
|
|
300
|
+
async count(): Promise<number> {
|
|
301
|
+
const results = await this._execute();
|
|
302
|
+
return results.length;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* [Symbol.asyncIterator] 支持 for await...of 循环
|
|
307
|
+
*/
|
|
308
|
+
[Symbol.asyncIterator](): AsyncIterator<Record<string, unknown>> {
|
|
309
|
+
return {
|
|
310
|
+
next: async () => {
|
|
311
|
+
const doc = await this.next();
|
|
312
|
+
return {
|
|
313
|
+
value: doc,
|
|
314
|
+
done: doc === null
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|