@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
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 集合类 - 管理文档集合
|
|
3
|
+
* 支持 async/await 异步操作
|
|
4
|
+
* 支持 JSONB 二进制存储模式
|
|
5
|
+
* 支持 Schema 验证
|
|
6
|
+
* 支持内存缓存优化
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { generateId, deepClone, getNestedValue } from './Utils.js';
|
|
13
|
+
import { Cursor } from './Cursor.js';
|
|
14
|
+
import { matchQuery, applyUpdate } from './Operators.js';
|
|
15
|
+
import { CollectionNotFoundError, DocumentNotFoundError, ValidationError } from './errors.js';
|
|
16
|
+
import type { Database } from './Database.js';
|
|
17
|
+
import type { Schema } from './Schema.js';
|
|
18
|
+
import type { SQLExecutionResult } from './SQLExecutor.js';
|
|
19
|
+
import { executeSQL } from './SQLExecutor.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 集合统计信息接口
|
|
23
|
+
*/
|
|
24
|
+
interface CollectionStats {
|
|
25
|
+
ns: string;
|
|
26
|
+
count: number;
|
|
27
|
+
size: number;
|
|
28
|
+
avgObjSize: number;
|
|
29
|
+
indexes: number;
|
|
30
|
+
jsonb: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 索引信息接口
|
|
35
|
+
*/
|
|
36
|
+
export interface IndexInfo {
|
|
37
|
+
key: Record<string, number>;
|
|
38
|
+
name: string;
|
|
39
|
+
unique: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 集合数据接口
|
|
44
|
+
*/
|
|
45
|
+
interface CollectionData {
|
|
46
|
+
_meta: {
|
|
47
|
+
name: string;
|
|
48
|
+
count: number;
|
|
49
|
+
indexes: IndexInfo[];
|
|
50
|
+
};
|
|
51
|
+
_documents: Array<Record<string, unknown>>;
|
|
52
|
+
_indexes: Record<string, Record<string, string[]>>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Collection 类
|
|
57
|
+
* 表示一个文档集合,提供 CRUD 操作
|
|
58
|
+
*/
|
|
59
|
+
export class Collection {
|
|
60
|
+
db: Database;
|
|
61
|
+
name: string;
|
|
62
|
+
private _filePath: string;
|
|
63
|
+
_data: CollectionData | null;
|
|
64
|
+
private _lock: boolean | null;
|
|
65
|
+
jsonb: boolean;
|
|
66
|
+
private _schema: Schema | null;
|
|
67
|
+
_validateOnInsert: boolean;
|
|
68
|
+
_validateOnUpdate: boolean;
|
|
69
|
+
|
|
70
|
+
// 内存缓存优化
|
|
71
|
+
_cache: CollectionData | null;
|
|
72
|
+
_cacheTime: number;
|
|
73
|
+
private _cacheTTL: number;
|
|
74
|
+
private _dirty: boolean;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param db - 数据库实例
|
|
78
|
+
* @param name - 集合名称
|
|
79
|
+
*/
|
|
80
|
+
constructor(db: Database, name: string) {
|
|
81
|
+
this.db = db;
|
|
82
|
+
this.name = name;
|
|
83
|
+
this._filePath = join(db.dbPath, `${name}.json`);
|
|
84
|
+
this._data = null;
|
|
85
|
+
this._lock = null;
|
|
86
|
+
this.jsonb = db.options.jsonb;
|
|
87
|
+
this._schema = null;
|
|
88
|
+
this._validateOnInsert = true;
|
|
89
|
+
this._validateOnUpdate = false;
|
|
90
|
+
|
|
91
|
+
// 内存缓存优化
|
|
92
|
+
this._cache = null;
|
|
93
|
+
this._cacheTime = 0;
|
|
94
|
+
this._cacheTTL = db.options.cacheTTL || 5000;
|
|
95
|
+
this._dirty = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 设置 Schema
|
|
100
|
+
*/
|
|
101
|
+
setSchema(schema: Schema, options: { validateOnInsert?: boolean; validateOnUpdate?: boolean } = {}): Collection {
|
|
102
|
+
this._schema = schema;
|
|
103
|
+
this._validateOnInsert = options.validateOnInsert !== false;
|
|
104
|
+
this._validateOnUpdate = options.validateOnUpdate || false;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 获取 Schema
|
|
110
|
+
*/
|
|
111
|
+
getSchema(): Schema | null {
|
|
112
|
+
return this._schema;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 验证文档
|
|
117
|
+
*/
|
|
118
|
+
_validate(doc: Record<string, unknown>): void {
|
|
119
|
+
if (!this._schema) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = this._schema.validate(doc);
|
|
124
|
+
if (!result.valid) {
|
|
125
|
+
throw new ValidationError(`验证失败:${result.errors!.map(e => `${e.field}: ${e.message}`).join(', ')}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 加载集合数据(带缓存)
|
|
131
|
+
*/
|
|
132
|
+
async _load(): Promise<void> {
|
|
133
|
+
if (this._data !== null) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 检查内存缓存
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
if (this._cache && (now - this._cacheTime) < this._cacheTTL) {
|
|
140
|
+
this._data = this._cache;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 等待锁
|
|
145
|
+
await this._acquireLock();
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
if (!existsSync(this._filePath)) {
|
|
149
|
+
// 如果集合文件不存在,自动创建
|
|
150
|
+
this._data = {
|
|
151
|
+
_meta: { name: this.name, count: 0, indexes: [] },
|
|
152
|
+
_documents: [],
|
|
153
|
+
_indexes: {}
|
|
154
|
+
};
|
|
155
|
+
await this._save();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// JSONB 模式:读取二进制文件
|
|
160
|
+
if (this.jsonb) {
|
|
161
|
+
const buffer = await readFile(this._filePath);
|
|
162
|
+
this._data = this._decodeJsonb(buffer);
|
|
163
|
+
} else {
|
|
164
|
+
// 普通模式:读取文本文件
|
|
165
|
+
const content = await readFile(this._filePath, 'utf-8');
|
|
166
|
+
this._data = JSON.parse(content) as CollectionData;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 确保数据结构完整
|
|
170
|
+
if (!this._data._meta) {
|
|
171
|
+
this._data._meta = { name: this.name, count: 0, indexes: [] };
|
|
172
|
+
}
|
|
173
|
+
if (!this._data._documents) {
|
|
174
|
+
this._data._documents = [];
|
|
175
|
+
}
|
|
176
|
+
if (!this._data._indexes) {
|
|
177
|
+
this._data._indexes = {};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 更新缓存
|
|
181
|
+
this._cache = this._data;
|
|
182
|
+
this._cacheTime = now;
|
|
183
|
+
} finally {
|
|
184
|
+
this._releaseLock();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 保存集合数据(带缓存失效)
|
|
190
|
+
*/
|
|
191
|
+
async _save(): Promise<void> {
|
|
192
|
+
if (this._data === null) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 标记为脏数据
|
|
197
|
+
this._dirty = true;
|
|
198
|
+
|
|
199
|
+
// 更新元数据
|
|
200
|
+
this._data._meta.count = this._data._documents.length;
|
|
201
|
+
|
|
202
|
+
// 失效缓存
|
|
203
|
+
this._cache = null;
|
|
204
|
+
this._cacheTime = 0;
|
|
205
|
+
|
|
206
|
+
// JSONB 模式:写入二进制文件
|
|
207
|
+
if (this.jsonb) {
|
|
208
|
+
const buffer = this._encodeJsonb();
|
|
209
|
+
await writeFile(this._filePath, buffer);
|
|
210
|
+
} else {
|
|
211
|
+
// 普通模式:写入格式化的 JSON 文本
|
|
212
|
+
const content = JSON.stringify(this._data, null, 2);
|
|
213
|
+
await writeFile(this._filePath, content, 'utf-8');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* JSONB 编码 - 将数据编码为二进制 Buffer
|
|
219
|
+
*/
|
|
220
|
+
_encodeJsonb(): Buffer {
|
|
221
|
+
const json = JSON.stringify(this._data);
|
|
222
|
+
const jsonBuffer = Buffer.from(json, 'utf-8');
|
|
223
|
+
|
|
224
|
+
// 创建包含长度前缀的 Buffer
|
|
225
|
+
// 4 字节 (uint32) 长度 + JSON 数据
|
|
226
|
+
const lengthBuffer = Buffer.alloc(4);
|
|
227
|
+
lengthBuffer.writeUInt32BE(jsonBuffer.length, 0);
|
|
228
|
+
|
|
229
|
+
return Buffer.concat([lengthBuffer, jsonBuffer]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* JSONB 解码 - 将二进制 Buffer 解码为数据
|
|
234
|
+
*/
|
|
235
|
+
_decodeJsonb(buffer: Buffer): CollectionData {
|
|
236
|
+
try {
|
|
237
|
+
// 读取长度前缀
|
|
238
|
+
const length = buffer.readUInt32BE(0);
|
|
239
|
+
|
|
240
|
+
// 验证长度
|
|
241
|
+
if (length !== buffer.length - 4) {
|
|
242
|
+
throw new Error('JSONB 长度不匹配');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 读取 JSON 数据
|
|
246
|
+
const jsonBuffer = buffer.subarray(4);
|
|
247
|
+
const json = jsonBuffer.toString('utf-8');
|
|
248
|
+
return JSON.parse(json) as CollectionData;
|
|
249
|
+
} catch (e) {
|
|
250
|
+
// 如果不是有效的 JSONB 格式,尝试直接 JSON 解析(向后兼容)
|
|
251
|
+
try {
|
|
252
|
+
const json = buffer.toString('utf-8');
|
|
253
|
+
return JSON.parse(json) as CollectionData;
|
|
254
|
+
} catch (e2) {
|
|
255
|
+
throw new Error(`无法解析数据:${(e as Error).message}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 获取锁
|
|
262
|
+
*/
|
|
263
|
+
async _acquireLock(): Promise<void> {
|
|
264
|
+
while (this._lock) {
|
|
265
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
266
|
+
}
|
|
267
|
+
this._lock = true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 释放锁
|
|
272
|
+
*/
|
|
273
|
+
_releaseLock(): void {
|
|
274
|
+
this._lock = null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 获取所有文档
|
|
279
|
+
*/
|
|
280
|
+
async _getDocuments(): Promise<Array<Record<string, unknown>>> {
|
|
281
|
+
await this._load();
|
|
282
|
+
return this._data!._documents;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 插入单个文档
|
|
287
|
+
*/
|
|
288
|
+
async insertOne(doc: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
289
|
+
await this._load();
|
|
290
|
+
|
|
291
|
+
// Schema 验证
|
|
292
|
+
if (this._validateOnInsert) {
|
|
293
|
+
this._validate(doc);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 创建文档副本并添加 _id
|
|
297
|
+
const newDoc: Record<string, unknown> = {
|
|
298
|
+
...deepClone(doc),
|
|
299
|
+
_id: (doc as Record<string, unknown>)._id || generateId(),
|
|
300
|
+
createdAt: new Date().toISOString()
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
this._data!._documents.push(newDoc);
|
|
304
|
+
await this._save();
|
|
305
|
+
|
|
306
|
+
return deepClone(newDoc);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 插入多个文档(优化版 - 单次写入)
|
|
311
|
+
*/
|
|
312
|
+
async insertMany(
|
|
313
|
+
docs: Array<Record<string, unknown>>,
|
|
314
|
+
options: Record<string, unknown> = {}
|
|
315
|
+
): Promise<{
|
|
316
|
+
acknowledged: boolean;
|
|
317
|
+
insertedCount: number;
|
|
318
|
+
insertedIds: Record<number, string>;
|
|
319
|
+
}> {
|
|
320
|
+
if (!Array.isArray(docs)) {
|
|
321
|
+
throw new Error('insertMany 需要数组参数');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
await this._load();
|
|
325
|
+
|
|
326
|
+
// 批量验证
|
|
327
|
+
if (this._validateOnInsert) {
|
|
328
|
+
for (const doc of docs) {
|
|
329
|
+
this._validate(doc);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 批量创建文档(带 _id 和 createdAt)
|
|
334
|
+
const now = new Date().toISOString();
|
|
335
|
+
const insertedDocs = docs.map(doc => ({
|
|
336
|
+
...deepClone(doc),
|
|
337
|
+
_id: doc._id || generateId(),
|
|
338
|
+
createdAt: doc.createdAt || now
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
// 单次批量添加
|
|
342
|
+
this._data!._documents.push(...insertedDocs);
|
|
343
|
+
|
|
344
|
+
// 更新索引(如果有)
|
|
345
|
+
if (this._data!._meta.indexes?.length > 0) {
|
|
346
|
+
await this._updateIndexes(insertedDocs, 'insert');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 单次保存
|
|
350
|
+
await this._save();
|
|
351
|
+
|
|
352
|
+
// 失效缓存
|
|
353
|
+
this._cache = null;
|
|
354
|
+
this._cacheTime = 0;
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
acknowledged: true,
|
|
358
|
+
insertedCount: insertedDocs.length,
|
|
359
|
+
insertedIds: insertedDocs.reduce((acc: Record<number, string>, doc, i) => {
|
|
360
|
+
acc[i] = doc._id as string;
|
|
361
|
+
return acc;
|
|
362
|
+
}, {})
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 更新索引
|
|
368
|
+
*/
|
|
369
|
+
private async _updateIndexes(
|
|
370
|
+
docs: Array<Record<string, unknown>>,
|
|
371
|
+
operation: 'insert' | 'update' | 'delete'
|
|
372
|
+
): Promise<void> {
|
|
373
|
+
// 简化版本:重建所有索引
|
|
374
|
+
for (const index of this._data!._meta.indexes) {
|
|
375
|
+
await this._buildIndex(index);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 查询文档
|
|
381
|
+
*/
|
|
382
|
+
find(query: Record<string, unknown> = {}, options: Record<string, unknown> = {}): Cursor {
|
|
383
|
+
return new Cursor(this, query, options);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* 查询单个文档
|
|
388
|
+
*/
|
|
389
|
+
async findOne(query: Record<string, unknown> = {}, options: Record<string, unknown> = {}): Promise<Record<string, unknown> | null> {
|
|
390
|
+
return await new Cursor(this, query, options).first();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 更新单个文档
|
|
395
|
+
*/
|
|
396
|
+
async updateOne(
|
|
397
|
+
query: Record<string, unknown>,
|
|
398
|
+
update: Record<string, unknown>,
|
|
399
|
+
options: { upsert?: boolean } = {}
|
|
400
|
+
): Promise<{
|
|
401
|
+
acknowledged: boolean;
|
|
402
|
+
matchedCount: number;
|
|
403
|
+
modifiedCount: number;
|
|
404
|
+
upsertedId?: string;
|
|
405
|
+
}> {
|
|
406
|
+
await this._load();
|
|
407
|
+
|
|
408
|
+
const docIndex = this._data!._documents.findIndex(doc => matchQuery(doc, query));
|
|
409
|
+
|
|
410
|
+
if (docIndex === -1) {
|
|
411
|
+
if (options.upsert) {
|
|
412
|
+
// 插入新文档
|
|
413
|
+
const newDoc: Record<string, unknown> = {
|
|
414
|
+
_id: generateId(),
|
|
415
|
+
...deepClone(query),
|
|
416
|
+
...deepClone(update)
|
|
417
|
+
};
|
|
418
|
+
this._data!._documents.push(newDoc);
|
|
419
|
+
await this._save();
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
acknowledged: true,
|
|
423
|
+
matchedCount: 0,
|
|
424
|
+
modifiedCount: 0,
|
|
425
|
+
upsertedId: newDoc._id as string
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
acknowledged: true,
|
|
431
|
+
matchedCount: 0,
|
|
432
|
+
modifiedCount: 0
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 应用更新
|
|
437
|
+
const updatedDoc = applyUpdate(this._data!._documents[docIndex], update) as Record<string, unknown>;
|
|
438
|
+
updatedDoc.updatedAt = new Date().toISOString();
|
|
439
|
+
this._data!._documents[docIndex] = updatedDoc;
|
|
440
|
+
|
|
441
|
+
await this._save();
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
acknowledged: true,
|
|
445
|
+
matchedCount: 1,
|
|
446
|
+
modifiedCount: 1
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* 更新多个文档
|
|
452
|
+
*/
|
|
453
|
+
async updateMany(
|
|
454
|
+
query: Record<string, unknown>,
|
|
455
|
+
update: Record<string, unknown>,
|
|
456
|
+
options: { upsert?: boolean } = {}
|
|
457
|
+
): Promise<{
|
|
458
|
+
acknowledged: boolean;
|
|
459
|
+
matchedCount: number;
|
|
460
|
+
modifiedCount: number;
|
|
461
|
+
upsertedId?: string;
|
|
462
|
+
}> {
|
|
463
|
+
await this._load();
|
|
464
|
+
|
|
465
|
+
let matchedCount = 0;
|
|
466
|
+
let modifiedCount = 0;
|
|
467
|
+
|
|
468
|
+
for (let i = 0; i < this._data!._documents.length; i++) {
|
|
469
|
+
const doc = this._data!._documents[i];
|
|
470
|
+
|
|
471
|
+
if (matchQuery(doc, query)) {
|
|
472
|
+
matchedCount++;
|
|
473
|
+
const updatedDoc = applyUpdate(doc, update) as Record<string, unknown>;
|
|
474
|
+
updatedDoc.updatedAt = new Date().toISOString();
|
|
475
|
+
|
|
476
|
+
if (JSON.stringify(doc) !== JSON.stringify(updatedDoc)) {
|
|
477
|
+
modifiedCount++;
|
|
478
|
+
this._data!._documents[i] = updatedDoc;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (matchedCount === 0 && options.upsert) {
|
|
484
|
+
// 插入新文档
|
|
485
|
+
const newDoc: Record<string, unknown> = {
|
|
486
|
+
_id: generateId(),
|
|
487
|
+
...deepClone(query),
|
|
488
|
+
...deepClone(update)
|
|
489
|
+
};
|
|
490
|
+
this._data!._documents.push(newDoc);
|
|
491
|
+
await this._save();
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
acknowledged: true,
|
|
495
|
+
matchedCount: 0,
|
|
496
|
+
modifiedCount: 0,
|
|
497
|
+
upsertedId: newDoc._id as string
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
await this._save();
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
acknowledged: true,
|
|
505
|
+
matchedCount,
|
|
506
|
+
modifiedCount
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* 替换单个文档
|
|
512
|
+
*/
|
|
513
|
+
async replaceOne(
|
|
514
|
+
query: Record<string, unknown>,
|
|
515
|
+
doc: Record<string, unknown>,
|
|
516
|
+
options: { upsert?: boolean } = {}
|
|
517
|
+
): Promise<{
|
|
518
|
+
acknowledged: boolean;
|
|
519
|
+
matchedCount: number;
|
|
520
|
+
modifiedCount: number;
|
|
521
|
+
upsertedId?: string;
|
|
522
|
+
}> {
|
|
523
|
+
await this._load();
|
|
524
|
+
|
|
525
|
+
const docIndex = this._data!._documents.findIndex(d => matchQuery(d, query));
|
|
526
|
+
|
|
527
|
+
if (docIndex === -1) {
|
|
528
|
+
if (options.upsert) {
|
|
529
|
+
const newDoc: Record<string, unknown> = {
|
|
530
|
+
...deepClone(doc),
|
|
531
|
+
_id: generateId(),
|
|
532
|
+
createdAt: new Date().toISOString()
|
|
533
|
+
};
|
|
534
|
+
this._data!._documents.push(newDoc);
|
|
535
|
+
await this._save();
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
acknowledged: true,
|
|
539
|
+
matchedCount: 0,
|
|
540
|
+
modifiedCount: 0,
|
|
541
|
+
upsertedId: newDoc._id as string
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
acknowledged: true,
|
|
547
|
+
matchedCount: 0,
|
|
548
|
+
modifiedCount: 0
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 保留 _id,替换其他字段
|
|
553
|
+
const oldId = this._data!._documents[docIndex]._id;
|
|
554
|
+
const newDoc: Record<string, unknown> = {
|
|
555
|
+
...deepClone(doc),
|
|
556
|
+
_id: oldId,
|
|
557
|
+
updatedAt: new Date().toISOString()
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
this._data!._documents[docIndex] = newDoc;
|
|
561
|
+
await this._save();
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
acknowledged: true,
|
|
565
|
+
matchedCount: 1,
|
|
566
|
+
modifiedCount: 1
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* 删除单个文档
|
|
572
|
+
*/
|
|
573
|
+
async deleteOne(query: Record<string, unknown>): Promise<{ acknowledged: boolean; deletedCount: number }> {
|
|
574
|
+
await this._load();
|
|
575
|
+
|
|
576
|
+
const docIndex = this._data!._documents.findIndex(doc => matchQuery(doc, query));
|
|
577
|
+
|
|
578
|
+
if (docIndex === -1) {
|
|
579
|
+
return {
|
|
580
|
+
acknowledged: true,
|
|
581
|
+
deletedCount: 0
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
this._data!._documents.splice(docIndex, 1);
|
|
586
|
+
await this._save();
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
acknowledged: true,
|
|
590
|
+
deletedCount: 1
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* 删除多个文档
|
|
596
|
+
*/
|
|
597
|
+
async deleteMany(query: Record<string, unknown>): Promise<{ acknowledged: boolean; deletedCount: number }> {
|
|
598
|
+
await this._load();
|
|
599
|
+
|
|
600
|
+
const initialCount = this._data!._documents.length;
|
|
601
|
+
this._data!._documents = this._data!._documents.filter(doc => !matchQuery(doc, query));
|
|
602
|
+
const deletedCount = initialCount - this._data!._documents.length;
|
|
603
|
+
|
|
604
|
+
await this._save();
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
acknowledged: true,
|
|
608
|
+
deletedCount
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* 计数文档
|
|
614
|
+
*/
|
|
615
|
+
async countDocuments(query: Record<string, unknown> = {}): Promise<number> {
|
|
616
|
+
return await this.find(query).count();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* 获取不同值
|
|
621
|
+
*/
|
|
622
|
+
async distinct(key: string, query: Record<string, unknown> = {}): Promise<unknown[]> {
|
|
623
|
+
const docs = await this.find(query).toArray();
|
|
624
|
+
const values = new Set<unknown>();
|
|
625
|
+
|
|
626
|
+
for (const doc of docs) {
|
|
627
|
+
const value = getNestedValue(doc, key);
|
|
628
|
+
if (value !== undefined) {
|
|
629
|
+
values.add(value);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return Array.from(values);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* 聚合查询
|
|
638
|
+
*/
|
|
639
|
+
async aggregate(pipeline: Array<Record<string, unknown>>): Promise<Array<Record<string, unknown>>> {
|
|
640
|
+
await this._load();
|
|
641
|
+
|
|
642
|
+
let results = this._data!._documents.map(doc => deepClone(doc));
|
|
643
|
+
|
|
644
|
+
for (const stage of pipeline) {
|
|
645
|
+
const stageName = Object.keys(stage)[0];
|
|
646
|
+
const stageValue = stage[stageName];
|
|
647
|
+
|
|
648
|
+
results = this._applyAggregationStage(results, stageName, stageValue);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return results;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* 应用聚合阶段
|
|
656
|
+
*/
|
|
657
|
+
private _applyAggregationStage(
|
|
658
|
+
docs: Array<Record<string, unknown>>,
|
|
659
|
+
stageName: string,
|
|
660
|
+
stageValue: unknown
|
|
661
|
+
): Array<Record<string, unknown>> {
|
|
662
|
+
switch (stageName) {
|
|
663
|
+
case '$match':
|
|
664
|
+
return docs.filter(doc => matchQuery(doc, stageValue as Record<string, unknown>));
|
|
665
|
+
|
|
666
|
+
case '$project':
|
|
667
|
+
return docs.map(doc => {
|
|
668
|
+
const result: Record<string, unknown> = {};
|
|
669
|
+
for (const [key, value] of Object.entries(stageValue as Record<string, unknown>)) {
|
|
670
|
+
if (typeof value === 'string' && value.startsWith('$')) {
|
|
671
|
+
result[key] = getNestedValue(doc, value.substring(1));
|
|
672
|
+
} else if (value === 0 || value === false) {
|
|
673
|
+
// 排除字段
|
|
674
|
+
} else {
|
|
675
|
+
result[key] = getNestedValue(doc, key);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if ((stageValue as Record<string, unknown>)._id !== undefined) {
|
|
679
|
+
result._id = (stageValue as Record<string, unknown>)._id === 0 ? undefined : doc._id;
|
|
680
|
+
} else {
|
|
681
|
+
result._id = doc._id;
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
case '$group':
|
|
687
|
+
return this._applyGroup(docs, stageValue as Record<string, unknown>);
|
|
688
|
+
|
|
689
|
+
case '$sort':
|
|
690
|
+
return docs.sort((a, b) => {
|
|
691
|
+
for (const [key, direction] of Object.entries(stageValue as Record<string, number>)) {
|
|
692
|
+
const aVal = getNestedValue(a, key);
|
|
693
|
+
const bVal = getNestedValue(b, key);
|
|
694
|
+
if (aVal < bVal) return -1 * direction;
|
|
695
|
+
if (aVal > bVal) return 1 * direction;
|
|
696
|
+
}
|
|
697
|
+
return 0;
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
case '$limit':
|
|
701
|
+
return docs.slice(0, stageValue as number);
|
|
702
|
+
|
|
703
|
+
case '$skip':
|
|
704
|
+
return docs.slice(stageValue as number);
|
|
705
|
+
|
|
706
|
+
case '$count':
|
|
707
|
+
return [{ [stageValue as string]: docs.length }];
|
|
708
|
+
|
|
709
|
+
default:
|
|
710
|
+
console.warn(`未实现的聚合阶段:${stageName}`);
|
|
711
|
+
return docs;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* 应用分组
|
|
717
|
+
*/
|
|
718
|
+
private _applyGroup(
|
|
719
|
+
docs: Array<Record<string, unknown>>,
|
|
720
|
+
stageValue: Record<string, unknown>
|
|
721
|
+
): Array<Record<string, unknown>> {
|
|
722
|
+
const groups = new Map<unknown, Array<Record<string, unknown>>>();
|
|
723
|
+
|
|
724
|
+
for (const doc of docs) {
|
|
725
|
+
let key: unknown;
|
|
726
|
+
if (stageValue._id === null) {
|
|
727
|
+
key = null;
|
|
728
|
+
} else if (typeof stageValue._id === 'string') {
|
|
729
|
+
key = getNestedValue(doc, stageValue._id.substring(1));
|
|
730
|
+
} else {
|
|
731
|
+
key = JSON.stringify(stageValue._id);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (!groups.has(key)) {
|
|
735
|
+
groups.set(key, []);
|
|
736
|
+
}
|
|
737
|
+
groups.get(key)!.push(doc);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return Array.from(groups.entries()).map(([key, groupDocs]) => {
|
|
741
|
+
const result: Record<string, unknown> = { _id: key };
|
|
742
|
+
|
|
743
|
+
for (const [aggKey, aggExpr] of Object.entries(stageValue)) {
|
|
744
|
+
if (aggKey === '_id') continue;
|
|
745
|
+
|
|
746
|
+
if (typeof aggExpr === 'object' && aggExpr !== null) {
|
|
747
|
+
const aggOp = Object.keys(aggExpr)[0];
|
|
748
|
+
const aggField = aggExpr[aggOp] as string;
|
|
749
|
+
|
|
750
|
+
if (aggOp === '$sum') {
|
|
751
|
+
result[aggKey] = groupDocs.reduce((sum, d) => {
|
|
752
|
+
const val = getNestedValue(d, aggField.substring(1)) as number || 0;
|
|
753
|
+
return sum + val;
|
|
754
|
+
}, 0);
|
|
755
|
+
} else if (aggOp === '$avg') {
|
|
756
|
+
result[aggKey] = groupDocs.reduce((sum, d) => {
|
|
757
|
+
const val = getNestedValue(d, aggField.substring(1)) as number || 0;
|
|
758
|
+
return sum + val;
|
|
759
|
+
}, 0) / groupDocs.length;
|
|
760
|
+
} else if (aggOp === '$count') {
|
|
761
|
+
result[aggKey] = groupDocs.length;
|
|
762
|
+
} else if (aggOp === '$min') {
|
|
763
|
+
result[aggKey] = Math.min(...groupDocs.map(d => getNestedValue(d, aggField.substring(1)) as number || 0));
|
|
764
|
+
} else if (aggOp === '$max') {
|
|
765
|
+
result[aggKey] = Math.max(...groupDocs.map(d => getNestedValue(d, aggField.substring(1)) as number || 0));
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return result;
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* 创建索引
|
|
776
|
+
*/
|
|
777
|
+
async createIndex(keys: Record<string, number>, options: { unique?: boolean } = {}): Promise<IndexInfo> {
|
|
778
|
+
await this._load();
|
|
779
|
+
|
|
780
|
+
const indexName = Object.entries(keys)
|
|
781
|
+
.map(([k, v]) => `${k}_${v}`)
|
|
782
|
+
.join('_');
|
|
783
|
+
|
|
784
|
+
// 检查索引是否已存在
|
|
785
|
+
const existingIndex = this._data!._meta.indexes?.find(i => i.name === indexName);
|
|
786
|
+
if (existingIndex) {
|
|
787
|
+
return existingIndex;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// 创建索引
|
|
791
|
+
const index: IndexInfo = {
|
|
792
|
+
key: keys,
|
|
793
|
+
name: indexName,
|
|
794
|
+
unique: options.unique || false
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
if (!this._data!._meta.indexes) {
|
|
798
|
+
this._data!._meta.indexes = [];
|
|
799
|
+
}
|
|
800
|
+
this._data!._meta.indexes.push(index);
|
|
801
|
+
|
|
802
|
+
// 构建索引数据
|
|
803
|
+
await this._buildIndex(index);
|
|
804
|
+
|
|
805
|
+
await this._save();
|
|
806
|
+
|
|
807
|
+
return index;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* 构建索引数据
|
|
812
|
+
*/
|
|
813
|
+
private async _buildIndex(index: IndexInfo): Promise<void> {
|
|
814
|
+
const indexKeys = Object.keys(index.key);
|
|
815
|
+
const indexData: Record<string, string[]> = {};
|
|
816
|
+
|
|
817
|
+
for (const doc of this._data!._documents) {
|
|
818
|
+
for (const key of indexKeys) {
|
|
819
|
+
const value = getNestedValue(doc, key);
|
|
820
|
+
if (value !== undefined) {
|
|
821
|
+
const keyStr = String(value);
|
|
822
|
+
if (!indexData[keyStr]) {
|
|
823
|
+
indexData[keyStr] = [];
|
|
824
|
+
}
|
|
825
|
+
indexData[keyStr].push(doc._id as string);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (!this._data!._indexes) {
|
|
831
|
+
this._data!._indexes = {};
|
|
832
|
+
}
|
|
833
|
+
this._data!._indexes[index.name] = indexData;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* 删除索引
|
|
838
|
+
*/
|
|
839
|
+
async dropIndex(name: string): Promise<{ acknowledged: boolean }> {
|
|
840
|
+
await this._load();
|
|
841
|
+
|
|
842
|
+
if (!this._data!._meta.indexes) {
|
|
843
|
+
return { acknowledged: false };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const initialLength = this._data!._meta.indexes.length;
|
|
847
|
+
this._data!._meta.indexes = this._data!._meta.indexes.filter(i => i.name !== name);
|
|
848
|
+
|
|
849
|
+
if (this._data!._meta.indexes.length === initialLength) {
|
|
850
|
+
return { acknowledged: false };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
await this._save();
|
|
854
|
+
|
|
855
|
+
return { acknowledged: true };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* 列出所有索引
|
|
860
|
+
*/
|
|
861
|
+
async listIndexes(): Promise<IndexInfo[]> {
|
|
862
|
+
await this._load();
|
|
863
|
+
return this._data!._meta.indexes || [];
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* 获取集合统计信息
|
|
868
|
+
*/
|
|
869
|
+
async stats(): Promise<CollectionStats> {
|
|
870
|
+
await this._load();
|
|
871
|
+
|
|
872
|
+
const size = this.jsonb
|
|
873
|
+
? this._encodeJsonb().length
|
|
874
|
+
: JSON.stringify(this._data).length;
|
|
875
|
+
|
|
876
|
+
return {
|
|
877
|
+
ns: this.name,
|
|
878
|
+
count: this._data!._documents.length,
|
|
879
|
+
size,
|
|
880
|
+
avgObjSize: this._data!._documents.length > 0
|
|
881
|
+
? size / this._data!._documents.length
|
|
882
|
+
: 0,
|
|
883
|
+
indexes: this._data!._meta.indexes?.length || 0,
|
|
884
|
+
jsonb: this.jsonb
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* 执行 SQL 语句
|
|
890
|
+
* @param sql - SQL 语句
|
|
891
|
+
* @returns SQL 执行结果
|
|
892
|
+
*/
|
|
893
|
+
async sql(sqlStr: string): Promise<SQLExecutionResult> {
|
|
894
|
+
return await executeSQL(this, sqlStr);
|
|
895
|
+
}
|
|
896
|
+
}
|