jsql-neo 1.3.0 → 2.0.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 +514 -0
- package/index.js +6 -3
- package/lib/btree.js +326 -0
- package/lib/database.js +286 -43
- package/lib/date-types.js +183 -0
- package/lib/errors.js +84 -0
- package/lib/query.js +251 -70
- package/lib/table.js +281 -175
- package/package.json +7 -3
package/lib/table.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// © Vexify 2026 All Rights Reserved.
|
|
2
2
|
/**
|
|
3
|
-
* JSQL Table — 表操作引擎
|
|
4
|
-
*
|
|
3
|
+
* JSQL Table — 表操作引擎 v2.0
|
|
4
|
+
* B-Tree 自动索引、日期类型、错误码、外键 CASCADE、长度/精度校验
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const Query = require('./query');
|
|
8
|
+
const BTree = require('./btree');
|
|
9
|
+
const { validateDateType, now } = require('./date-types');
|
|
10
|
+
const { createError } = require('./errors');
|
|
8
11
|
|
|
9
12
|
class Table {
|
|
10
13
|
constructor(name, schema, db) {
|
|
@@ -12,7 +15,8 @@ class Table {
|
|
|
12
15
|
this._schema = schema;
|
|
13
16
|
this._db = db;
|
|
14
17
|
this._rows = [];
|
|
15
|
-
this._indexes = {};
|
|
18
|
+
this._indexes = {}; // 旧版 hash 索引(兼容)
|
|
19
|
+
this._btrees = {}; // B-Tree 索引 { fieldName: BTree }
|
|
16
20
|
this._autoIncrement = 0;
|
|
17
21
|
this._dirty = false;
|
|
18
22
|
this._hooks = {
|
|
@@ -20,15 +24,15 @@ class Table {
|
|
|
20
24
|
beforeUpdate: [], afterUpdate: [],
|
|
21
25
|
beforeRemove: [], afterRemove: []
|
|
22
26
|
};
|
|
23
|
-
this._softDelete = false;
|
|
24
|
-
this._computedFields = {};
|
|
27
|
+
this._softDelete = false;
|
|
28
|
+
this._computedFields = {};
|
|
25
29
|
|
|
26
|
-
// 解析列定义
|
|
27
30
|
this._primaryKey = null;
|
|
28
31
|
this._autoIncrementField = null;
|
|
29
32
|
this._foreignKeys = [];
|
|
30
33
|
this._checkConstraints = [];
|
|
31
34
|
this._defaults = {};
|
|
35
|
+
this._dateFields = {}; // { fieldName: 'date'|'datetime'|'timestamp' }
|
|
32
36
|
|
|
33
37
|
for (const [field, def] of Object.entries(schema)) {
|
|
34
38
|
if (def.primaryKey) this._primaryKey = field;
|
|
@@ -46,17 +50,35 @@ class Table {
|
|
|
46
50
|
this._foreignKeys.push({
|
|
47
51
|
field,
|
|
48
52
|
table: def.foreignKey.table,
|
|
49
|
-
|
|
53
|
+
refField: def.foreignKey.field || 'id',
|
|
54
|
+
onDelete: def.foreignKey.onDelete || 'restrict',
|
|
55
|
+
onUpdate: def.foreignKey.onUpdate || 'restrict'
|
|
50
56
|
});
|
|
51
57
|
}
|
|
52
58
|
if (def.computed) {
|
|
53
59
|
this._computedFields[field] = def.computed;
|
|
54
60
|
}
|
|
61
|
+
// 日期类型
|
|
62
|
+
if (['date', 'datetime', 'timestamp', 'time'].includes(def.type)) {
|
|
63
|
+
this._dateFields[field] = def.type;
|
|
64
|
+
}
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
if (schema._softDelete) {
|
|
58
68
|
this._softDelete = true;
|
|
59
69
|
}
|
|
70
|
+
|
|
71
|
+
// 自动为 primaryKey 和 unique 字段建 B-Tree 索引
|
|
72
|
+
this._autoCreateBTrees();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_autoCreateBTrees() {
|
|
76
|
+
for (const [field, def] of Object.entries(this._schema)) {
|
|
77
|
+
if (field === '_softDelete') continue;
|
|
78
|
+
if (def.primaryKey || def.unique) {
|
|
79
|
+
this._btrees[field] = new BTree(64, true);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
60
82
|
}
|
|
61
83
|
|
|
62
84
|
// ============================================================
|
|
@@ -68,39 +90,11 @@ class Table {
|
|
|
68
90
|
data = hook({ ...data }) || data;
|
|
69
91
|
}
|
|
70
92
|
|
|
71
|
-
// 填充默认值
|
|
72
93
|
data = this._applyDefaults(data);
|
|
73
|
-
|
|
74
|
-
// 计算字段
|
|
75
94
|
data = this._applyComputed(data);
|
|
95
|
+
data = this._validateDataTypes(data);
|
|
96
|
+
this._validateConstraints(data, -1);
|
|
76
97
|
|
|
77
|
-
// 校验必填
|
|
78
|
-
for (const [field, def] of Object.entries(this._schema)) {
|
|
79
|
-
if (field === '_softDelete') continue;
|
|
80
|
-
if (def.required && (data[field] === undefined || data[field] === null)) {
|
|
81
|
-
throw new Error(`Field '${field}' is required in table '${this._name}'`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// CHECK 约束
|
|
86
|
-
for (const { field, fn } of this._checkConstraints) {
|
|
87
|
-
if (data[field] !== undefined && !fn(data[field], data)) {
|
|
88
|
-
throw new Error(`CHECK constraint failed on '${this._name}.${field}'`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// 唯一约束
|
|
93
|
-
for (const [field, def] of Object.entries(this._schema)) {
|
|
94
|
-
if (def.unique && data[field] !== undefined) {
|
|
95
|
-
for (const row of this._rows) {
|
|
96
|
-
if (row[field] === data[field]) {
|
|
97
|
-
throw new Error(`Duplicate value for unique field '${field}': ${data[field]}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 自增
|
|
104
98
|
if (this._autoIncrementField && data[this._autoIncrementField] === undefined) {
|
|
105
99
|
this._autoIncrement++;
|
|
106
100
|
data[this._autoIncrementField] = this._autoIncrement;
|
|
@@ -110,14 +104,13 @@ class Table {
|
|
|
110
104
|
|
|
111
105
|
const rowIndex = this._rows.length;
|
|
112
106
|
this._rows.push({ ...data });
|
|
113
|
-
this.
|
|
107
|
+
this._addToIndexes(rowIndex, data);
|
|
114
108
|
this._dirty = true;
|
|
115
109
|
this._db._markDirty();
|
|
116
110
|
|
|
117
111
|
for (const hook of this._hooks.afterInsert) {
|
|
118
112
|
hook(data);
|
|
119
113
|
}
|
|
120
|
-
|
|
121
114
|
this._db._emitChange('insert', this._name, data);
|
|
122
115
|
|
|
123
116
|
return data;
|
|
@@ -127,10 +120,6 @@ class Table {
|
|
|
127
120
|
return items.map(item => this.insert(item));
|
|
128
121
|
}
|
|
129
122
|
|
|
130
|
-
/**
|
|
131
|
-
* UPSERT — 有则更新,无则插入
|
|
132
|
-
* 按主键或唯一字段判断
|
|
133
|
-
*/
|
|
134
123
|
upsert(data) {
|
|
135
124
|
if (this._primaryKey && data[this._primaryKey] !== undefined) {
|
|
136
125
|
const existing = this.findById(data[this._primaryKey]);
|
|
@@ -143,13 +132,13 @@ class Table {
|
|
|
143
132
|
}
|
|
144
133
|
|
|
145
134
|
// ============================================================
|
|
146
|
-
//
|
|
135
|
+
// 查询(支持 B-Tree 加速)
|
|
147
136
|
// ============================================================
|
|
148
137
|
|
|
149
138
|
find(query = {}) {
|
|
150
139
|
let rows = this._getVisibleRows();
|
|
151
140
|
if (Object.keys(query).length > 0) {
|
|
152
|
-
rows = this.
|
|
141
|
+
rows = this._applyFilterOptimized(rows, query);
|
|
153
142
|
}
|
|
154
143
|
return rows.map(r => ({ ...r }));
|
|
155
144
|
}
|
|
@@ -160,7 +149,16 @@ class Table {
|
|
|
160
149
|
}
|
|
161
150
|
|
|
162
151
|
findById(id) {
|
|
163
|
-
if (!this._primaryKey) throw
|
|
152
|
+
if (!this._primaryKey) throw createError('ER_PARSE_ERROR', 'Table has no primary key');
|
|
153
|
+
// B-Tree 加速
|
|
154
|
+
if (this._btrees[this._primaryKey]) {
|
|
155
|
+
const indices = this._btrees[this._primaryKey].search(id);
|
|
156
|
+
if (indices.length > 0) {
|
|
157
|
+
const row = this._rows[indices[0]];
|
|
158
|
+
if (row && !row._deleted) return { ...row };
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
164
162
|
return this.findOne({ [this._primaryKey]: id });
|
|
165
163
|
}
|
|
166
164
|
|
|
@@ -170,7 +168,7 @@ class Table {
|
|
|
170
168
|
|
|
171
169
|
count(query = {}) {
|
|
172
170
|
if (Object.keys(query).length === 0) return this._getVisibleRows().length;
|
|
173
|
-
return this.
|
|
171
|
+
return this._applyFilterOptimized(this._getVisibleRows(), query).length;
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
where(conditions) {
|
|
@@ -186,7 +184,8 @@ class Table {
|
|
|
186
184
|
updates = hook(query, { ...updates }) || updates;
|
|
187
185
|
}
|
|
188
186
|
|
|
189
|
-
|
|
187
|
+
updates = this._validateDataTypes(updates, true);
|
|
188
|
+
const matched = this._applyFilterOptimized(this._getVisibleRows(), query);
|
|
190
189
|
let count = 0;
|
|
191
190
|
|
|
192
191
|
for (const row of matched) {
|
|
@@ -194,29 +193,12 @@ class Table {
|
|
|
194
193
|
if (index === -1) continue;
|
|
195
194
|
|
|
196
195
|
const oldData = { ...this._rows[index] };
|
|
197
|
-
|
|
198
|
-
// 唯一约束
|
|
199
|
-
for (const [field, def] of Object.entries(this._schema)) {
|
|
200
|
-
if (def.unique && updates[field] !== undefined) {
|
|
201
|
-
for (let i = 0; i < this._rows.length; i++) {
|
|
202
|
-
if (i !== index && this._rows[i][field] === updates[field]) {
|
|
203
|
-
throw new Error(`Duplicate value for unique field '${field}': ${updates[field]}`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// CHECK 约束
|
|
210
196
|
const merged = { ...this._rows[index], ...updates };
|
|
211
|
-
|
|
212
|
-
if (merged[field] !== undefined && !fn(merged[field], merged)) {
|
|
213
|
-
throw new Error(`CHECK constraint failed on '${this._name}.${field}'`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
197
|
+
this._validateConstraints(merged, index);
|
|
216
198
|
|
|
217
|
-
Object.assign(this._rows[index], updates);
|
|
218
199
|
this._removeFromIndexes(index, oldData);
|
|
219
|
-
|
|
200
|
+
Object.assign(this._rows[index], updates);
|
|
201
|
+
this._addToIndexes(index, this._rows[index]);
|
|
220
202
|
count++;
|
|
221
203
|
}
|
|
222
204
|
|
|
@@ -232,12 +214,12 @@ class Table {
|
|
|
232
214
|
}
|
|
233
215
|
|
|
234
216
|
updateById(id, updates) {
|
|
235
|
-
if (!this._primaryKey) throw
|
|
217
|
+
if (!this._primaryKey) throw createError('ER_PARSE_ERROR', 'Table has no primary key');
|
|
236
218
|
return this.update({ [this._primaryKey]: id }, updates);
|
|
237
219
|
}
|
|
238
220
|
|
|
239
221
|
// ============================================================
|
|
240
|
-
// 删除
|
|
222
|
+
// 删除 & 外键 CASCADE
|
|
241
223
|
// ============================================================
|
|
242
224
|
|
|
243
225
|
remove(query = {}) {
|
|
@@ -249,22 +231,15 @@ class Table {
|
|
|
249
231
|
return this._truncate();
|
|
250
232
|
}
|
|
251
233
|
|
|
252
|
-
const toRemove = this.
|
|
234
|
+
const toRemove = this._applyFilterOptimized(this._getVisibleRows(), query);
|
|
253
235
|
let count = 0;
|
|
254
236
|
|
|
255
237
|
for (const row of toRemove) {
|
|
256
238
|
const index = this._rows.indexOf(row);
|
|
257
239
|
if (index === -1) continue;
|
|
258
240
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
if (fk.onDelete === 'cascade') {
|
|
262
|
-
const refTable = this._db._tables[fk.table];
|
|
263
|
-
if (refTable) {
|
|
264
|
-
refTable.remove({ [fk.field]: row[this._primaryKey] });
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
241
|
+
// 外键 CASCADE / SET NULL:查找所有引用此表的子表
|
|
242
|
+
this._cascadeForeignKeys(row);
|
|
268
243
|
|
|
269
244
|
if (this._softDelete) {
|
|
270
245
|
this._rows[index]._deleted = true;
|
|
@@ -272,7 +247,7 @@ class Table {
|
|
|
272
247
|
} else {
|
|
273
248
|
this._removeFromIndexes(index, row);
|
|
274
249
|
this._rows.splice(index, 1);
|
|
275
|
-
this.
|
|
250
|
+
this._adjustBTreeAfterRemove(index);
|
|
276
251
|
}
|
|
277
252
|
count++;
|
|
278
253
|
}
|
|
@@ -289,24 +264,17 @@ class Table {
|
|
|
289
264
|
}
|
|
290
265
|
|
|
291
266
|
removeById(id) {
|
|
292
|
-
if (!this._primaryKey) throw
|
|
267
|
+
if (!this._primaryKey) throw createError('ER_PARSE_ERROR', 'Table has no primary key');
|
|
293
268
|
return this.remove({ [this._primaryKey]: id });
|
|
294
269
|
}
|
|
295
270
|
|
|
296
|
-
/**
|
|
297
|
-
* 软删除 — 标记为已删除,可恢复
|
|
298
|
-
*/
|
|
299
271
|
removeSoft(query = {}) {
|
|
300
272
|
this._softDelete = true;
|
|
301
|
-
|
|
302
|
-
return result;
|
|
273
|
+
return this.remove(query);
|
|
303
274
|
}
|
|
304
275
|
|
|
305
|
-
/**
|
|
306
|
-
* 恢复软删除的行
|
|
307
|
-
*/
|
|
308
276
|
restore(query = {}) {
|
|
309
|
-
if (!this._softDelete) throw
|
|
277
|
+
if (!this._softDelete) throw createError('ER_NOT_SUPPORTED', 'Soft delete is not enabled');
|
|
310
278
|
const matched = this._applyFilter(this._rows, { ...query, _deleted: true });
|
|
311
279
|
for (const row of matched) {
|
|
312
280
|
const index = this._rows.indexOf(row);
|
|
@@ -318,9 +286,6 @@ class Table {
|
|
|
318
286
|
return matched.length;
|
|
319
287
|
}
|
|
320
288
|
|
|
321
|
-
/**
|
|
322
|
-
* 清空表(保留结构)
|
|
323
|
-
*/
|
|
324
289
|
truncate() {
|
|
325
290
|
return this._truncate();
|
|
326
291
|
}
|
|
@@ -328,6 +293,9 @@ class Table {
|
|
|
328
293
|
_truncate() {
|
|
329
294
|
const count = this._rows.length;
|
|
330
295
|
this._rows = [];
|
|
296
|
+
for (const key of Object.keys(this._btrees)) {
|
|
297
|
+
this._btrees[key].clear();
|
|
298
|
+
}
|
|
331
299
|
this._indexes = {};
|
|
332
300
|
this._autoIncrement = 0;
|
|
333
301
|
this._dirty = true;
|
|
@@ -340,11 +308,10 @@ class Table {
|
|
|
340
308
|
// ============================================================
|
|
341
309
|
|
|
342
310
|
addColumn(field, definition) {
|
|
343
|
-
if (this._schema[field]) throw
|
|
311
|
+
if (this._schema[field]) throw createError('ER_DUP_FIELDNAME', field);
|
|
344
312
|
this._schema[field] = definition;
|
|
345
313
|
if (definition.default !== undefined) {
|
|
346
314
|
this._defaults[field] = definition.default;
|
|
347
|
-
// 填充现有行的默认值
|
|
348
315
|
for (const row of this._rows) {
|
|
349
316
|
if (row[field] === undefined) {
|
|
350
317
|
row[field] = typeof definition.default === 'function' ? definition.default() : definition.default;
|
|
@@ -356,40 +323,57 @@ class Table {
|
|
|
356
323
|
this._foreignKeys.push({
|
|
357
324
|
field,
|
|
358
325
|
table: definition.foreignKey.table,
|
|
359
|
-
|
|
326
|
+
refField: definition.foreignKey.field || 'id',
|
|
327
|
+
onDelete: definition.foreignKey.onDelete || 'restrict',
|
|
328
|
+
onUpdate: definition.foreignKey.onUpdate || 'restrict'
|
|
360
329
|
});
|
|
361
330
|
}
|
|
362
331
|
if (definition.computed) this._computedFields[field] = definition.computed;
|
|
332
|
+
if (['date', 'datetime', 'timestamp', 'time'].includes(definition.type)) {
|
|
333
|
+
this._dateFields[field] = definition.type;
|
|
334
|
+
}
|
|
335
|
+
if (definition.primaryKey || definition.unique) {
|
|
336
|
+
this._btrees[field] = new BTree(64, true);
|
|
337
|
+
this._rebuildBTree(field);
|
|
338
|
+
}
|
|
363
339
|
this._dirty = true;
|
|
364
340
|
this._db._markDirty();
|
|
365
341
|
return this;
|
|
366
342
|
}
|
|
367
343
|
|
|
368
344
|
dropColumn(field) {
|
|
369
|
-
if (!this._schema[field]) throw
|
|
345
|
+
if (!this._schema[field]) throw createError('ER_CANT_DROP_FIELD', field);
|
|
370
346
|
delete this._schema[field];
|
|
371
347
|
delete this._defaults[field];
|
|
348
|
+
delete this._btrees[field];
|
|
349
|
+
delete this._indexes[field];
|
|
372
350
|
this._checkConstraints = this._checkConstraints.filter(c => c.field !== field);
|
|
373
351
|
this._foreignKeys = this._foreignKeys.filter(fk => fk.field !== field);
|
|
374
352
|
delete this._computedFields[field];
|
|
375
|
-
|
|
353
|
+
delete this._dateFields[field];
|
|
376
354
|
for (const row of this._rows) {
|
|
377
355
|
delete row[field];
|
|
378
356
|
}
|
|
357
|
+
if (this._primaryKey === field) this._primaryKey = null;
|
|
358
|
+
if (this._autoIncrementField === field) this._autoIncrementField = null;
|
|
379
359
|
this._dirty = true;
|
|
380
360
|
this._db._markDirty();
|
|
381
361
|
return this;
|
|
382
362
|
}
|
|
383
363
|
|
|
384
364
|
renameColumn(oldName, newName) {
|
|
385
|
-
if (!this._schema[oldName]) throw
|
|
386
|
-
if (this._schema[newName]) throw
|
|
365
|
+
if (!this._schema[oldName]) throw createError('ER_BAD_FIELD_ERROR', oldName, this._name);
|
|
366
|
+
if (this._schema[newName]) throw createError('ER_DUP_FIELDNAME', newName);
|
|
387
367
|
this._schema[newName] = this._schema[oldName];
|
|
388
368
|
delete this._schema[oldName];
|
|
389
369
|
if (this._defaults[oldName] !== undefined) {
|
|
390
370
|
this._defaults[newName] = this._defaults[oldName];
|
|
391
371
|
delete this._defaults[oldName];
|
|
392
372
|
}
|
|
373
|
+
if (this._btrees[oldName]) {
|
|
374
|
+
this._btrees[newName] = this._btrees[oldName];
|
|
375
|
+
delete this._btrees[oldName];
|
|
376
|
+
}
|
|
393
377
|
for (const row of this._rows) {
|
|
394
378
|
row[newName] = row[oldName];
|
|
395
379
|
delete row[oldName];
|
|
@@ -402,40 +386,35 @@ class Table {
|
|
|
402
386
|
}
|
|
403
387
|
|
|
404
388
|
modifyColumn(field, definition) {
|
|
405
|
-
if (!this._schema[field]) throw
|
|
389
|
+
if (!this._schema[field]) throw createError('ER_BAD_FIELD_ERROR', field, this._name);
|
|
406
390
|
this._schema[field] = { ...this._schema[field], ...definition };
|
|
407
391
|
if (definition.default !== undefined) this._defaults[field] = definition.default;
|
|
408
392
|
this._checkConstraints = this._checkConstraints.filter(c => c.field !== field);
|
|
409
393
|
if (definition.check) this._checkConstraints.push({ field, fn: definition.check });
|
|
394
|
+
if (definition.primaryKey || definition.unique) {
|
|
395
|
+
if (!this._btrees[field]) this._btrees[field] = new BTree(64, true);
|
|
396
|
+
this._rebuildBTree(field);
|
|
397
|
+
}
|
|
410
398
|
this._dirty = true;
|
|
411
399
|
this._db._markDirty();
|
|
412
400
|
return this;
|
|
413
401
|
}
|
|
414
402
|
|
|
415
|
-
/**
|
|
416
|
-
* 获取表结构
|
|
417
|
-
*/
|
|
418
403
|
describe() {
|
|
419
404
|
return {
|
|
420
405
|
name: this._name,
|
|
421
406
|
columns: { ...this._schema },
|
|
422
407
|
primaryKey: this._primaryKey,
|
|
423
408
|
rowCount: this._getVisibleRows().length,
|
|
424
|
-
|
|
409
|
+
btreeIndexes: Object.keys(this._btrees),
|
|
410
|
+
hashIndexes: Object.keys(this._indexes),
|
|
425
411
|
softDelete: this._softDelete,
|
|
426
412
|
foreignKeys: this._foreignKeys.map(fk => ({ field: fk.field, table: fk.table }))
|
|
427
413
|
};
|
|
428
414
|
}
|
|
429
415
|
|
|
430
|
-
// ============================================================
|
|
431
|
-
// SELECT INTO
|
|
432
|
-
// ============================================================
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* 从查询结果创建新表
|
|
436
|
-
*/
|
|
437
416
|
selectInto(newTableName, newSchema, query = {}) {
|
|
438
|
-
const rows = this.
|
|
417
|
+
const rows = this._applyFilterOptimized(this._getVisibleRows(), query);
|
|
439
418
|
this._db.createTable(newTableName, newSchema);
|
|
440
419
|
const newTable = this._db._tables[newTableName];
|
|
441
420
|
for (const row of rows) {
|
|
@@ -444,21 +423,10 @@ class Table {
|
|
|
444
423
|
return newTable;
|
|
445
424
|
}
|
|
446
425
|
|
|
447
|
-
// ============================================================
|
|
448
|
-
// 全文搜索评分
|
|
449
|
-
// ============================================================
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* 全文搜索并返回带相关性评分的排序结果
|
|
453
|
-
* @param {string} field - 搜索字段
|
|
454
|
-
* @param {string|string[]} terms - 搜索词
|
|
455
|
-
* @returns {Array} 按 _score 降序排列的结果
|
|
456
|
-
*/
|
|
457
426
|
textSearch(field, terms) {
|
|
458
427
|
const searchTerms = Array.isArray(terms) ? terms : terms.split(/\s+/);
|
|
459
428
|
const rows = this._getVisibleRows();
|
|
460
429
|
const scored = [];
|
|
461
|
-
|
|
462
430
|
for (const row of rows) {
|
|
463
431
|
const value = String(row[field] || '').toLowerCase();
|
|
464
432
|
let score = 0;
|
|
@@ -466,17 +434,12 @@ class Table {
|
|
|
466
434
|
const t = term.toLowerCase();
|
|
467
435
|
const idx = value.indexOf(t);
|
|
468
436
|
if (idx !== -1) {
|
|
469
|
-
score += 10
|
|
470
|
-
|
|
471
|
-
if (value === t) score += 20; // 完全匹配加更多
|
|
472
|
-
score += (value.length - idx) / value.length * 5; // 靠前加权
|
|
437
|
+
score += 10 + (idx === 0 ? 5 : 0) + (value === t ? 20 : 0)
|
|
438
|
+
+ (value.length - idx) / value.length * 5;
|
|
473
439
|
}
|
|
474
440
|
}
|
|
475
|
-
if (score > 0) {
|
|
476
|
-
scored.push({ ...row, _score: score });
|
|
477
|
-
}
|
|
441
|
+
if (score > 0) scored.push({ ...row, _score: score });
|
|
478
442
|
}
|
|
479
|
-
|
|
480
443
|
return scored.sort((a, b) => b._score - a._score);
|
|
481
444
|
}
|
|
482
445
|
|
|
@@ -500,6 +463,34 @@ class Table {
|
|
|
500
463
|
|
|
501
464
|
dropIndex(field) {
|
|
502
465
|
delete this._indexes[field];
|
|
466
|
+
delete this._btrees[field];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* 创建 B-Tree 索引(O(log n) 查询)
|
|
471
|
+
*/
|
|
472
|
+
createBTreeIndex(field, unique = false) {
|
|
473
|
+
this._btrees[field] = new BTree(64, unique);
|
|
474
|
+
this._rebuildBTree(field);
|
|
475
|
+
return this._btrees[field];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
_rebuildBTree(field) {
|
|
479
|
+
if (this._btrees[field]) {
|
|
480
|
+
this._btrees[field].clear();
|
|
481
|
+
for (let i = 0; i < this._rows.length; i++) {
|
|
482
|
+
const val = this._rows[i][field];
|
|
483
|
+
if (val !== undefined && val !== null) {
|
|
484
|
+
this._btrees[field].insert(val, i);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
_rebuildAllBTrees() {
|
|
491
|
+
for (const field of Object.keys(this._btrees)) {
|
|
492
|
+
this._rebuildBTree(field);
|
|
493
|
+
}
|
|
503
494
|
}
|
|
504
495
|
|
|
505
496
|
// ============================================================
|
|
@@ -521,9 +512,41 @@ class Table {
|
|
|
521
512
|
}
|
|
522
513
|
|
|
523
514
|
// ============================================================
|
|
524
|
-
// 内部:
|
|
515
|
+
// 内部: 过滤器(B-Tree 加速)
|
|
525
516
|
// ============================================================
|
|
526
517
|
|
|
518
|
+
_applyFilterOptimized(rows, query) {
|
|
519
|
+
// 尝试用 B-Tree 加速
|
|
520
|
+
for (const [field, condition] of Object.entries(query)) {
|
|
521
|
+
if (field === '$or' || field === '$and' || field === '$not') continue;
|
|
522
|
+
// 等值查询用 B-Tree
|
|
523
|
+
if (typeof condition !== 'object' || condition === null || Array.isArray(condition) || condition instanceof RegExp) {
|
|
524
|
+
if (this._btrees[field]) {
|
|
525
|
+
const indices = this._btrees[field].search(condition);
|
|
526
|
+
if (indices.length > 0) {
|
|
527
|
+
const subset = indices.map(i => this._rows[i]).filter(r => r);
|
|
528
|
+
return this._applyFilter(subset, query);
|
|
529
|
+
}
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// 范围查询用 B-Tree
|
|
534
|
+
if (condition && typeof condition === 'object') {
|
|
535
|
+
if (condition.$gt !== undefined && condition.$lt !== undefined && this._btrees[field]) {
|
|
536
|
+
const indices = this._btrees[field].range(condition.$gt, condition.$lt);
|
|
537
|
+
const subset = indices.map(i => this._rows[i]).filter(r => r);
|
|
538
|
+
return this._applyFilter(subset, query);
|
|
539
|
+
}
|
|
540
|
+
if (condition.$gte !== undefined && condition.$lte !== undefined && this._btrees[field]) {
|
|
541
|
+
const indices = this._btrees[field].range(condition.$gte, condition.$lte);
|
|
542
|
+
const subset = indices.map(i => this._rows[i]).filter(r => r);
|
|
543
|
+
return this._applyFilter(subset, query);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return this._applyFilter(rows, query);
|
|
548
|
+
}
|
|
549
|
+
|
|
527
550
|
_applyFilter(rows, query) {
|
|
528
551
|
const result = [];
|
|
529
552
|
for (let i = 0; i < rows.length; i++) {
|
|
@@ -536,7 +559,6 @@ class Table {
|
|
|
536
559
|
|
|
537
560
|
_matchRow(row, query) {
|
|
538
561
|
for (const [field, condition] of Object.entries(query)) {
|
|
539
|
-
// 顶层 $or / $and / $not
|
|
540
562
|
if (field === '$or') {
|
|
541
563
|
if (!Array.isArray(condition) || !condition.some(sub => this._matchRow(row, sub))) return false;
|
|
542
564
|
continue;
|
|
@@ -550,21 +572,16 @@ class Table {
|
|
|
550
572
|
continue;
|
|
551
573
|
}
|
|
552
574
|
|
|
553
|
-
// JSON 路径: 'profile.city' 或 'data.profile.city'
|
|
554
575
|
const value = this._resolvePath(row, field);
|
|
555
576
|
|
|
556
577
|
if (condition !== null && typeof condition === 'object' && !Array.isArray(condition) && !(condition instanceof RegExp)) {
|
|
557
|
-
// 检查是否是操作符
|
|
558
578
|
const keys = Object.keys(condition);
|
|
559
579
|
const isOperator = keys.every(k => k.startsWith('$'));
|
|
560
580
|
if (isOperator && keys.length > 0) {
|
|
561
581
|
for (const [op, target] of Object.entries(condition)) {
|
|
562
|
-
if (!this._matchOperator(value, op, target))
|
|
563
|
-
return false;
|
|
564
|
-
}
|
|
582
|
+
if (!this._matchOperator(value, op, target)) return false;
|
|
565
583
|
}
|
|
566
584
|
} else {
|
|
567
|
-
// 普通对象:递归匹配子文档
|
|
568
585
|
if (value !== null && typeof value === 'object') {
|
|
569
586
|
if (!this._matchRow(value, condition)) return false;
|
|
570
587
|
} else {
|
|
@@ -590,15 +607,13 @@ class Table {
|
|
|
590
607
|
case '$nin': return Array.isArray(target) && !target.includes(value);
|
|
591
608
|
case '$like':
|
|
592
609
|
if (typeof value !== 'string' || typeof target !== 'string') return false;
|
|
593
|
-
|
|
594
|
-
return new RegExp('^' + likeRegex + '$', 'i').test(value);
|
|
610
|
+
return new RegExp('^' + target.replace(/%/g, '.*').replace(/_/g, '.') + '$', 'i').test(value);
|
|
595
611
|
case '$regex':
|
|
596
612
|
return new RegExp(target).test(String(value));
|
|
597
613
|
case '$exists':
|
|
598
614
|
return target ? value !== undefined : value === undefined;
|
|
599
615
|
case '$between':
|
|
600
|
-
return Array.isArray(target) && target.length === 2
|
|
601
|
-
&& value >= target[0] && value <= target[1];
|
|
616
|
+
return Array.isArray(target) && target.length === 2 && value >= target[0] && value <= target[1];
|
|
602
617
|
case '$and':
|
|
603
618
|
return Array.isArray(target) && target.every(sub => this._matchRow({ _val: value }, { _val: sub }));
|
|
604
619
|
case '$or':
|
|
@@ -613,8 +628,7 @@ class Table {
|
|
|
613
628
|
return Array.isArray(value) && value.length === target;
|
|
614
629
|
case '$elemMatch':
|
|
615
630
|
return Array.isArray(value) && value.some(el =>
|
|
616
|
-
typeof el === 'object' ? this._matchRow(el, target) : el === target
|
|
617
|
-
);
|
|
631
|
+
typeof el === 'object' ? this._matchRow(el, target) : el === target);
|
|
618
632
|
case '$isNull':
|
|
619
633
|
return target ? value === null || value === undefined : value !== null && value !== undefined;
|
|
620
634
|
case '$startsWith':
|
|
@@ -622,18 +636,14 @@ class Table {
|
|
|
622
636
|
case '$endsWith':
|
|
623
637
|
return typeof value === 'string' && value.endsWith(target);
|
|
624
638
|
case '$search':
|
|
625
|
-
// 全文搜索(简单实现)
|
|
626
639
|
if (typeof value !== 'string') return false;
|
|
627
640
|
const terms = Array.isArray(target) ? target : [target];
|
|
628
641
|
return terms.every(t => value.toLowerCase().includes(t.toLowerCase()));
|
|
629
642
|
case '$textSearch':
|
|
630
|
-
// 高级全文搜索,带相关性评分(返回 true 即可,评分在 _textScore 中)
|
|
631
643
|
if (typeof value !== 'string') return false;
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
return searchTerms.some(t => lower.includes(t.toLowerCase()));
|
|
644
|
+
const sTerms = Array.isArray(target) ? target : target.split(/\s+/);
|
|
645
|
+
return sTerms.some(t => value.toLowerCase().includes(t.toLowerCase()));
|
|
635
646
|
case '$inSub':
|
|
636
|
-
// 子查询:$inSub: { table: otherTable, field: 'col', query: {} }
|
|
637
647
|
if (!target || !target.table) return false;
|
|
638
648
|
const subRows = target.table._applyFilter(target.table._rows, target.query || {});
|
|
639
649
|
return subRows.some(r => r[target.field] === value);
|
|
@@ -642,7 +652,6 @@ class Table {
|
|
|
642
652
|
const subRows2 = target.table._applyFilter(target.table._rows, target.query || {});
|
|
643
653
|
return !subRows2.some(r => r[target.field] === value);
|
|
644
654
|
case '$expr':
|
|
645
|
-
// 表达式:$expr: (value, row) => boolean
|
|
646
655
|
if (typeof target === 'function') return target(value);
|
|
647
656
|
return false;
|
|
648
657
|
default:
|
|
@@ -650,9 +659,6 @@ class Table {
|
|
|
650
659
|
}
|
|
651
660
|
}
|
|
652
661
|
|
|
653
|
-
/**
|
|
654
|
-
* 解析 JSON 路径,如 'profile.city' -> row.profile.city
|
|
655
|
-
*/
|
|
656
662
|
_resolvePath(row, field) {
|
|
657
663
|
if (field.includes('.') && !field.startsWith('$')) {
|
|
658
664
|
const parts = field.split('.');
|
|
@@ -666,6 +672,102 @@ class Table {
|
|
|
666
672
|
return row[field];
|
|
667
673
|
}
|
|
668
674
|
|
|
675
|
+
// ============================================================
|
|
676
|
+
// 内部: 约束校验
|
|
677
|
+
// ============================================================
|
|
678
|
+
|
|
679
|
+
_validateConstraints(data, excludeIndex) {
|
|
680
|
+
for (const [field, def] of Object.entries(this._schema)) {
|
|
681
|
+
if (field === '_softDelete') continue;
|
|
682
|
+
|
|
683
|
+
// NOT NULL
|
|
684
|
+
if (def.required && (data[field] === undefined || data[field] === null)) {
|
|
685
|
+
throw createError('ER_BAD_NULL_ERROR', field);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// UNIQUE
|
|
689
|
+
if (def.unique && data[field] !== undefined) {
|
|
690
|
+
for (let i = 0; i < this._rows.length; i++) {
|
|
691
|
+
if (i === excludeIndex) continue;
|
|
692
|
+
if (this._rows[i][field] === data[field]) {
|
|
693
|
+
throw createError('ER_DUP_ENTRY', String(data[field]), field);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// CHECK
|
|
699
|
+
for (const { field: cf, fn } of this._checkConstraints) {
|
|
700
|
+
if (cf === field && data[field] !== undefined && !fn(data[field], data)) {
|
|
701
|
+
throw createError('ER_CHECK_CONSTRAINT', this._name + '.' + field);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// 长度限制
|
|
706
|
+
if (def.length && typeof data[field] === 'string' && data[field].length > def.length) {
|
|
707
|
+
throw createError('ER_DATA_TOO_LONG', field);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// 数值范围
|
|
711
|
+
if (def.type === 'integer' && data[field] !== undefined && data[field] !== null) {
|
|
712
|
+
const v = Number(data[field]);
|
|
713
|
+
if (def.min !== undefined && v < def.min) throw createError('ER_OUT_OF_RANGE', field);
|
|
714
|
+
if (def.max !== undefined && v > def.max) throw createError('ER_OUT_OF_RANGE', field);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// FOREIGN KEY 校验
|
|
719
|
+
for (const fk of this._foreignKeys) {
|
|
720
|
+
if (data[fk.field] !== undefined && data[fk.field] !== null) {
|
|
721
|
+
const refTable = this._resolveTable(fk.table);
|
|
722
|
+
if (refTable) {
|
|
723
|
+
const exists = refTable.findOne({ [fk.refField]: data[fk.field] });
|
|
724
|
+
if (!exists) {
|
|
725
|
+
throw createError('ER_NO_REFERENCED_ROW');
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
_resolveTable(tableRef) {
|
|
733
|
+
if (typeof tableRef === 'string') {
|
|
734
|
+
return this._db._tables[tableRef] || this._db._attached[tableRef]?._tables[tableRef];
|
|
735
|
+
}
|
|
736
|
+
return tableRef;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* 级联外键操作:当删除当前表的行时,查找所有引用此表的子表
|
|
741
|
+
* 并执行 CASCADE 或 SET NULL
|
|
742
|
+
*/
|
|
743
|
+
_cascadeForeignKeys(row) {
|
|
744
|
+
for (const [, table] of Object.entries(this._db._tables)) {
|
|
745
|
+
if (table._name === this._name) continue;
|
|
746
|
+
for (const fk of table._foreignKeys) {
|
|
747
|
+
const refTable = table._resolveTable(fk.table);
|
|
748
|
+
if (!refTable || refTable._name !== this._name) continue;
|
|
749
|
+
|
|
750
|
+
// 检查子表行是否引用当前行
|
|
751
|
+
if (fk.onDelete === 'cascade') {
|
|
752
|
+
table.remove({ [fk.field]: row[fk.refField] });
|
|
753
|
+
} else if (fk.onDelete === 'set null') {
|
|
754
|
+
table.update({ [fk.field]: row[fk.refField] }, { [fk.field]: null });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
_validateDataTypes(data, partial = false) {
|
|
761
|
+
const result = { ...data };
|
|
762
|
+
for (const [field, type] of Object.entries(this._dateFields)) {
|
|
763
|
+
if (partial && data[field] === undefined) continue;
|
|
764
|
+
if (data[field] !== undefined && data[field] !== null) {
|
|
765
|
+
result[field] = validateDateType(data[field], type, field);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return result;
|
|
769
|
+
}
|
|
770
|
+
|
|
669
771
|
// ============================================================
|
|
670
772
|
// 内部: 辅助
|
|
671
773
|
// ============================================================
|
|
@@ -674,7 +776,9 @@ class Table {
|
|
|
674
776
|
const result = { ...data };
|
|
675
777
|
for (const [field, defaultValue] of Object.entries(this._defaults)) {
|
|
676
778
|
if (result[field] === undefined) {
|
|
677
|
-
result[field] = typeof defaultValue === 'function'
|
|
779
|
+
result[field] = typeof defaultValue === 'function'
|
|
780
|
+
? defaultValue()
|
|
781
|
+
: (defaultValue === 'CURRENT_TIMESTAMP' ? now() : defaultValue);
|
|
678
782
|
}
|
|
679
783
|
}
|
|
680
784
|
return result;
|
|
@@ -688,13 +792,11 @@ class Table {
|
|
|
688
792
|
}
|
|
689
793
|
|
|
690
794
|
_getVisibleRows() {
|
|
691
|
-
if (this._softDelete)
|
|
692
|
-
return this._rows.filter(r => !r._deleted);
|
|
693
|
-
}
|
|
795
|
+
if (this._softDelete) return this._rows.filter(r => !r._deleted);
|
|
694
796
|
return this._rows;
|
|
695
797
|
}
|
|
696
798
|
|
|
697
|
-
|
|
799
|
+
_addToIndexes(rowIndex, data) {
|
|
698
800
|
for (const field of Object.keys(this._indexes)) {
|
|
699
801
|
const value = data[field];
|
|
700
802
|
if (value !== undefined) {
|
|
@@ -704,6 +806,12 @@ class Table {
|
|
|
704
806
|
this._indexes[field].get(value).push(rowIndex);
|
|
705
807
|
}
|
|
706
808
|
}
|
|
809
|
+
for (const [field, tree] of Object.entries(this._btrees)) {
|
|
810
|
+
const value = data[field];
|
|
811
|
+
if (value !== undefined && value !== null) {
|
|
812
|
+
tree.insert(value, rowIndex);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
707
815
|
}
|
|
708
816
|
|
|
709
817
|
_removeFromIndexes(rowIndex, data) {
|
|
@@ -718,21 +826,18 @@ class Table {
|
|
|
718
826
|
}
|
|
719
827
|
}
|
|
720
828
|
}
|
|
829
|
+
for (const [field, tree] of Object.entries(this._btrees)) {
|
|
830
|
+
const value = data[field];
|
|
831
|
+
if (value !== undefined && value !== null) {
|
|
832
|
+
tree.remove(value, rowIndex);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
721
835
|
}
|
|
722
836
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const newPositions = positions
|
|
728
|
-
.filter(p => p !== removedIndex)
|
|
729
|
-
.map(p => p > removedIndex ? p - 1 : p);
|
|
730
|
-
if (newPositions.length === 0) {
|
|
731
|
-
idx.delete(key);
|
|
732
|
-
} else {
|
|
733
|
-
idx.set(key, newPositions);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
837
|
+
_adjustBTreeAfterRemove(removedIndex) {
|
|
838
|
+
// 重建受影响的 B-Tree
|
|
839
|
+
for (const field of Object.keys(this._btrees)) {
|
|
840
|
+
this._rebuildBTree(field);
|
|
736
841
|
}
|
|
737
842
|
}
|
|
738
843
|
|
|
@@ -746,6 +851,7 @@ class Table {
|
|
|
746
851
|
}
|
|
747
852
|
}
|
|
748
853
|
}
|
|
854
|
+
this._rebuildAllBTrees();
|
|
749
855
|
for (const field of Object.keys(this._indexes)) {
|
|
750
856
|
this.createIndex(field);
|
|
751
857
|
}
|