jsql-neo 1.2.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/index.js +7 -15
- package/lib/btree.js +326 -0
- package/lib/database.js +572 -22
- package/lib/date-types.js +183 -0
- package/lib/errors.js +84 -0
- package/lib/query.js +412 -51
- package/lib/table.js +320 -142
- 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,31 +386,63 @@ 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
|
|
|
416
|
+
selectInto(newTableName, newSchema, query = {}) {
|
|
417
|
+
const rows = this._applyFilterOptimized(this._getVisibleRows(), query);
|
|
418
|
+
this._db.createTable(newTableName, newSchema);
|
|
419
|
+
const newTable = this._db._tables[newTableName];
|
|
420
|
+
for (const row of rows) {
|
|
421
|
+
newTable.insert(row);
|
|
422
|
+
}
|
|
423
|
+
return newTable;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
textSearch(field, terms) {
|
|
427
|
+
const searchTerms = Array.isArray(terms) ? terms : terms.split(/\s+/);
|
|
428
|
+
const rows = this._getVisibleRows();
|
|
429
|
+
const scored = [];
|
|
430
|
+
for (const row of rows) {
|
|
431
|
+
const value = String(row[field] || '').toLowerCase();
|
|
432
|
+
let score = 0;
|
|
433
|
+
for (const term of searchTerms) {
|
|
434
|
+
const t = term.toLowerCase();
|
|
435
|
+
const idx = value.indexOf(t);
|
|
436
|
+
if (idx !== -1) {
|
|
437
|
+
score += 10 + (idx === 0 ? 5 : 0) + (value === t ? 20 : 0)
|
|
438
|
+
+ (value.length - idx) / value.length * 5;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (score > 0) scored.push({ ...row, _score: score });
|
|
442
|
+
}
|
|
443
|
+
return scored.sort((a, b) => b._score - a._score);
|
|
444
|
+
}
|
|
445
|
+
|
|
430
446
|
// ============================================================
|
|
431
447
|
// 索引
|
|
432
448
|
// ============================================================
|
|
@@ -447,6 +463,34 @@ class Table {
|
|
|
447
463
|
|
|
448
464
|
dropIndex(field) {
|
|
449
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
|
+
}
|
|
450
494
|
}
|
|
451
495
|
|
|
452
496
|
// ============================================================
|
|
@@ -468,9 +512,41 @@ class Table {
|
|
|
468
512
|
}
|
|
469
513
|
|
|
470
514
|
// ============================================================
|
|
471
|
-
// 内部:
|
|
515
|
+
// 内部: 过滤器(B-Tree 加速)
|
|
472
516
|
// ============================================================
|
|
473
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
|
+
|
|
474
550
|
_applyFilter(rows, query) {
|
|
475
551
|
const result = [];
|
|
476
552
|
for (let i = 0; i < rows.length; i++) {
|
|
@@ -483,7 +559,6 @@ class Table {
|
|
|
483
559
|
|
|
484
560
|
_matchRow(row, query) {
|
|
485
561
|
for (const [field, condition] of Object.entries(query)) {
|
|
486
|
-
// 顶层 $or / $and / $not
|
|
487
562
|
if (field === '$or') {
|
|
488
563
|
if (!Array.isArray(condition) || !condition.some(sub => this._matchRow(row, sub))) return false;
|
|
489
564
|
continue;
|
|
@@ -497,21 +572,16 @@ class Table {
|
|
|
497
572
|
continue;
|
|
498
573
|
}
|
|
499
574
|
|
|
500
|
-
// JSON 路径: 'profile.city' 或 'data.profile.city'
|
|
501
575
|
const value = this._resolvePath(row, field);
|
|
502
576
|
|
|
503
577
|
if (condition !== null && typeof condition === 'object' && !Array.isArray(condition) && !(condition instanceof RegExp)) {
|
|
504
|
-
// 检查是否是操作符
|
|
505
578
|
const keys = Object.keys(condition);
|
|
506
579
|
const isOperator = keys.every(k => k.startsWith('$'));
|
|
507
580
|
if (isOperator && keys.length > 0) {
|
|
508
581
|
for (const [op, target] of Object.entries(condition)) {
|
|
509
|
-
if (!this._matchOperator(value, op, target))
|
|
510
|
-
return false;
|
|
511
|
-
}
|
|
582
|
+
if (!this._matchOperator(value, op, target)) return false;
|
|
512
583
|
}
|
|
513
584
|
} else {
|
|
514
|
-
// 普通对象:递归匹配子文档
|
|
515
585
|
if (value !== null && typeof value === 'object') {
|
|
516
586
|
if (!this._matchRow(value, condition)) return false;
|
|
517
587
|
} else {
|
|
@@ -537,15 +607,13 @@ class Table {
|
|
|
537
607
|
case '$nin': return Array.isArray(target) && !target.includes(value);
|
|
538
608
|
case '$like':
|
|
539
609
|
if (typeof value !== 'string' || typeof target !== 'string') return false;
|
|
540
|
-
|
|
541
|
-
return new RegExp('^' + likeRegex + '$', 'i').test(value);
|
|
610
|
+
return new RegExp('^' + target.replace(/%/g, '.*').replace(/_/g, '.') + '$', 'i').test(value);
|
|
542
611
|
case '$regex':
|
|
543
612
|
return new RegExp(target).test(String(value));
|
|
544
613
|
case '$exists':
|
|
545
614
|
return target ? value !== undefined : value === undefined;
|
|
546
615
|
case '$between':
|
|
547
|
-
return Array.isArray(target) && target.length === 2
|
|
548
|
-
&& value >= target[0] && value <= target[1];
|
|
616
|
+
return Array.isArray(target) && target.length === 2 && value >= target[0] && value <= target[1];
|
|
549
617
|
case '$and':
|
|
550
618
|
return Array.isArray(target) && target.every(sub => this._matchRow({ _val: value }, { _val: sub }));
|
|
551
619
|
case '$or':
|
|
@@ -560,8 +628,7 @@ class Table {
|
|
|
560
628
|
return Array.isArray(value) && value.length === target;
|
|
561
629
|
case '$elemMatch':
|
|
562
630
|
return Array.isArray(value) && value.some(el =>
|
|
563
|
-
typeof el === 'object' ? this._matchRow(el, target) : el === target
|
|
564
|
-
);
|
|
631
|
+
typeof el === 'object' ? this._matchRow(el, target) : el === target);
|
|
565
632
|
case '$isNull':
|
|
566
633
|
return target ? value === null || value === undefined : value !== null && value !== undefined;
|
|
567
634
|
case '$startsWith':
|
|
@@ -569,18 +636,29 @@ class Table {
|
|
|
569
636
|
case '$endsWith':
|
|
570
637
|
return typeof value === 'string' && value.endsWith(target);
|
|
571
638
|
case '$search':
|
|
572
|
-
// 全文搜索(简单实现)
|
|
573
639
|
if (typeof value !== 'string') return false;
|
|
574
640
|
const terms = Array.isArray(target) ? target : [target];
|
|
575
641
|
return terms.every(t => value.toLowerCase().includes(t.toLowerCase()));
|
|
642
|
+
case '$textSearch':
|
|
643
|
+
if (typeof value !== 'string') return false;
|
|
644
|
+
const sTerms = Array.isArray(target) ? target : target.split(/\s+/);
|
|
645
|
+
return sTerms.some(t => value.toLowerCase().includes(t.toLowerCase()));
|
|
646
|
+
case '$inSub':
|
|
647
|
+
if (!target || !target.table) return false;
|
|
648
|
+
const subRows = target.table._applyFilter(target.table._rows, target.query || {});
|
|
649
|
+
return subRows.some(r => r[target.field] === value);
|
|
650
|
+
case '$notInSub':
|
|
651
|
+
if (!target || !target.table) return false;
|
|
652
|
+
const subRows2 = target.table._applyFilter(target.table._rows, target.query || {});
|
|
653
|
+
return !subRows2.some(r => r[target.field] === value);
|
|
654
|
+
case '$expr':
|
|
655
|
+
if (typeof target === 'function') return target(value);
|
|
656
|
+
return false;
|
|
576
657
|
default:
|
|
577
658
|
return false;
|
|
578
659
|
}
|
|
579
660
|
}
|
|
580
661
|
|
|
581
|
-
/**
|
|
582
|
-
* 解析 JSON 路径,如 'profile.city' -> row.profile.city
|
|
583
|
-
*/
|
|
584
662
|
_resolvePath(row, field) {
|
|
585
663
|
if (field.includes('.') && !field.startsWith('$')) {
|
|
586
664
|
const parts = field.split('.');
|
|
@@ -594,6 +672,102 @@ class Table {
|
|
|
594
672
|
return row[field];
|
|
595
673
|
}
|
|
596
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
|
+
|
|
597
771
|
// ============================================================
|
|
598
772
|
// 内部: 辅助
|
|
599
773
|
// ============================================================
|
|
@@ -602,7 +776,9 @@ class Table {
|
|
|
602
776
|
const result = { ...data };
|
|
603
777
|
for (const [field, defaultValue] of Object.entries(this._defaults)) {
|
|
604
778
|
if (result[field] === undefined) {
|
|
605
|
-
result[field] = typeof defaultValue === 'function'
|
|
779
|
+
result[field] = typeof defaultValue === 'function'
|
|
780
|
+
? defaultValue()
|
|
781
|
+
: (defaultValue === 'CURRENT_TIMESTAMP' ? now() : defaultValue);
|
|
606
782
|
}
|
|
607
783
|
}
|
|
608
784
|
return result;
|
|
@@ -616,13 +792,11 @@ class Table {
|
|
|
616
792
|
}
|
|
617
793
|
|
|
618
794
|
_getVisibleRows() {
|
|
619
|
-
if (this._softDelete)
|
|
620
|
-
return this._rows.filter(r => !r._deleted);
|
|
621
|
-
}
|
|
795
|
+
if (this._softDelete) return this._rows.filter(r => !r._deleted);
|
|
622
796
|
return this._rows;
|
|
623
797
|
}
|
|
624
798
|
|
|
625
|
-
|
|
799
|
+
_addToIndexes(rowIndex, data) {
|
|
626
800
|
for (const field of Object.keys(this._indexes)) {
|
|
627
801
|
const value = data[field];
|
|
628
802
|
if (value !== undefined) {
|
|
@@ -632,6 +806,12 @@ class Table {
|
|
|
632
806
|
this._indexes[field].get(value).push(rowIndex);
|
|
633
807
|
}
|
|
634
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
|
+
}
|
|
635
815
|
}
|
|
636
816
|
|
|
637
817
|
_removeFromIndexes(rowIndex, data) {
|
|
@@ -646,21 +826,18 @@ class Table {
|
|
|
646
826
|
}
|
|
647
827
|
}
|
|
648
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
|
+
}
|
|
649
835
|
}
|
|
650
836
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const newPositions = positions
|
|
656
|
-
.filter(p => p !== removedIndex)
|
|
657
|
-
.map(p => p > removedIndex ? p - 1 : p);
|
|
658
|
-
if (newPositions.length === 0) {
|
|
659
|
-
idx.delete(key);
|
|
660
|
-
} else {
|
|
661
|
-
idx.set(key, newPositions);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
837
|
+
_adjustBTreeAfterRemove(removedIndex) {
|
|
838
|
+
// 重建受影响的 B-Tree
|
|
839
|
+
for (const field of Object.keys(this._btrees)) {
|
|
840
|
+
this._rebuildBTree(field);
|
|
664
841
|
}
|
|
665
842
|
}
|
|
666
843
|
|
|
@@ -674,6 +851,7 @@ class Table {
|
|
|
674
851
|
}
|
|
675
852
|
}
|
|
676
853
|
}
|
|
854
|
+
this._rebuildAllBTrees();
|
|
677
855
|
for (const field of Object.keys(this._indexes)) {
|
|
678
856
|
this.createIndex(field);
|
|
679
857
|
}
|