jsql-neo 2.1.2 → 2.1.3

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/lib/btree.js CHANGED
@@ -322,6 +322,52 @@ class BTree {
322
322
  }
323
323
  }
324
324
  }
325
+
326
+ /**
327
+ * 批量加载:从已排序的 [key, rowIndex] 数组底部向上构建
328
+ * O(n) 时间,适合大数据量批量构建
329
+ */
330
+ bulkLoad(sortedPairs) {
331
+ if (sortedPairs.length === 0) { this.clear(); return; }
332
+ const order = this._order;
333
+ // 第一层:构建叶子节点
334
+ const leaves = [];
335
+ let i = 0;
336
+ while (i < sortedPairs.length) {
337
+ const node = new BTreeNode(true);
338
+ const end = Math.min(i + order - 1, sortedPairs.length);
339
+ for (; i < end; i++) {
340
+ const [key, val] = sortedPairs[i];
341
+ node.keys.push(key);
342
+ node.values.push(Array.isArray(val) ? val : [val]);
343
+ }
344
+ leaves.push(node);
345
+ }
346
+ for (let i = 0; i < leaves.length - 1; i++) leaves[i].next = leaves[i + 1];
347
+ if (leaves.length === 1) { this._root = leaves[0]; this._size = sortedPairs.length; return; }
348
+ // 逐层向上构建内部节点
349
+ let currentLevel = leaves;
350
+ while (currentLevel.length > 1) {
351
+ const parents = [];
352
+ let j = 0;
353
+ while (j < currentLevel.length) {
354
+ const node = new BTreeNode(false);
355
+ const end = Math.min(j + order, currentLevel.length);
356
+ for (; j < end; j++) {
357
+ if (node.children.length > 0) {
358
+ const child = currentLevel[j];
359
+ node.keys.push(child.keys[0]);
360
+ node.values.push(child.values[0]);
361
+ }
362
+ node.children.push(currentLevel[j]);
363
+ }
364
+ parents.push(node);
365
+ }
366
+ currentLevel = parents;
367
+ }
368
+ this._root = currentLevel[0];
369
+ this._size = sortedPairs.length;
370
+ }
325
371
  }
326
372
 
327
373
  module.exports = BTree;
package/lib/table.js CHANGED
@@ -26,6 +26,7 @@ class Table {
26
26
  };
27
27
  this._softDelete = false;
28
28
  this._computedFields = {};
29
+ this._btreesDirty = false;
29
30
 
30
31
  this._primaryKey = null;
31
32
  this._autoIncrementField = null;
@@ -68,7 +69,10 @@ class Table {
68
69
  this._softDelete = true;
69
70
  }
70
71
 
71
- // 自动为 primaryKey unique 字段建 B-Tree 索引
72
+ this._cachedSchemaFields = Object.keys(schema).filter(f => f !== '_softDelete');
73
+ this._cachedDateFields = Object.keys(this._dateFields);
74
+ this._cachedUniqueFields = this._cachedSchemaFields.filter(f => schema[f].unique);
75
+ this._cachedRequiredFields = this._cachedSchemaFields.filter(f => schema[f].required);
72
76
  this._autoCreateBTrees();
73
77
  }
74
78
 
@@ -119,35 +123,73 @@ class Table {
119
123
  insertMany(items) {
120
124
  if (items.length === 0) return [];
121
125
  for (const hook of this._hooks.beforeInsert) {
122
- items = items.map(data => hook({ ...data }) || data);
126
+ const mapped = new Array(items.length);
127
+ for (let i = 0; i < items.length; i++) mapped[i] = hook({ ...items[i] }) || items[i];
128
+ items = mapped;
123
129
  }
130
+ const N = items.length;
124
131
  const uniqueTracker = {};
125
- for (const [field, def] of Object.entries(this._schema)) {
126
- if (def.unique) uniqueTracker[field] = new Set();
127
- }
128
- const results = [];
129
- for (const item of items) {
130
- let data = this._applyDefaults(item);
131
- data = this._applyComputed(data);
132
- data = this._validateDataTypes(data);
133
- this._validateConstraints(data, -1, uniqueTracker);
134
- if (this._autoIncrementField && data[this._autoIncrementField] === undefined) {
132
+ for (const field of this._cachedUniqueFields) uniqueTracker[field] = new Set();
133
+ const dateFields = this._cachedDateFields;
134
+ const hasDateFields = dateFields.length > 0;
135
+ const autoIncField = this._autoIncrementField;
136
+ const defaults = this._defaults;
137
+ const computedFields = this._computedFields;
138
+ const schema = this._schema;
139
+ const checkConstraints = this._checkConstraints;
140
+ const foreignKeys = this._foreignKeys;
141
+ for (let i = 0; i < N; i++) {
142
+ let data = items[i];
143
+ for (const [f, dv] of Object.entries(defaults)) {
144
+ if (data[f] === undefined) data[f] = typeof dv === 'function' ? dv() : dv === 'CURRENT_TIMESTAMP' ? now() : dv;
145
+ }
146
+ for (const f of Object.keys(computedFields)) data[f] = computedFields[f](data);
147
+ if (hasDateFields) {
148
+ for (const f of dateFields) {
149
+ if (data[f] !== undefined && data[f] !== null) data[f] = validateDateType(data[f], this._dateFields[f], f);
150
+ }
151
+ }
152
+ for (const field of this._cachedSchemaFields) {
153
+ const def = schema[field];
154
+ if (def.required && (data[field] === undefined || data[field] === null)) throw createError('ER_BAD_NULL_ERROR', field);
155
+ if (def.unique && data[field] !== undefined) {
156
+ if (uniqueTracker[field].has(data[field])) throw createError('ER_DUP_ENTRY', String(data[field]), field);
157
+ uniqueTracker[field].add(data[field]);
158
+ }
159
+ if (def.length && typeof data[field] === 'string' && data[field].length > def.length) throw createError('ER_DATA_TOO_LONG', field);
160
+ if (def.type === 'integer' || def.type === 'number') {
161
+ const v = def.type === 'integer' ? Number(data[field]) : data[field];
162
+ if (data[field] !== undefined && data[field] !== null && def.min !== undefined && v < def.min) throw createError('ER_OUT_OF_RANGE', field);
163
+ if (data[field] !== undefined && data[field] !== null && def.max !== undefined && v > def.max) throw createError('ER_OUT_OF_RANGE', field);
164
+ }
165
+ if (def.check && data[field] !== undefined && !def.check(data[field], data)) throw createError('ER_CHECK_CONSTRAINT', this._name + '.' + field);
166
+ }
167
+ for (const fk of foreignKeys) {
168
+ if (data[fk.field] !== undefined && data[fk.field] !== null) {
169
+ const refTable = this._resolveTable(fk.table);
170
+ if (refTable) {
171
+ const exists = refTable.findOne({ [fk.refField]: data[fk.field] });
172
+ if (!exists) throw createError('ER_NO_REFERENCED_ROW');
173
+ }
174
+ }
175
+ }
176
+ if (autoIncField && data[autoIncField] === undefined) {
135
177
  this._autoIncrement++;
136
- data[this._autoIncrementField] = this._autoIncrement;
137
- } else if (this._autoIncrementField && data[this._autoIncrementField] > this._autoIncrement) {
138
- this._autoIncrement = data[this._autoIncrementField];
178
+ data[autoIncField] = this._autoIncrement;
179
+ } else if (autoIncField && data[autoIncField] > this._autoIncrement) {
180
+ this._autoIncrement = data[autoIncField];
139
181
  }
140
- this._rows.push({ ...data });
141
- results.push(data);
182
+ this._rows.push(Object.assign({}, data));
183
+ items[i] = data;
142
184
  }
143
- this._rebuildAllBTrees();
185
+ this._btreesDirty = true;
144
186
  this._dirty = true;
145
187
  this._db._markDirty();
146
188
  for (const hook of this._hooks.afterInsert) {
147
- for (const data of results) hook(data);
189
+ for (let i = 0; i < N; i++) hook(items[i]);
148
190
  }
149
- for (const data of results) this._db._emitChange('insert', this._name, data);
150
- return results;
191
+ this._db._emitChange('insertMany', this._name, { count: N });
192
+ return items;
151
193
  }
152
194
 
153
195
  upsert(data) {
@@ -180,7 +222,7 @@ class Table {
180
222
 
181
223
  findById(id) {
182
224
  if (!this._primaryKey) throw createError('ER_PARSE_ERROR', 'Table has no primary key');
183
- // B-Tree 加速
225
+ this._ensureBTrees();
184
226
  if (this._btrees[this._primaryKey]) {
185
227
  const indices = this._btrees[this._primaryKey].search(id);
186
228
  if (indices.length > 0) {
@@ -517,6 +559,21 @@ class Table {
517
559
  }
518
560
  }
519
561
 
562
+ _ensureBTrees() {
563
+ if (!this._btreesDirty) return;
564
+ this._btreesDirty = false;
565
+ for (const field of Object.keys(this._btrees)) {
566
+ this._btrees[field].clear();
567
+ }
568
+ for (let i = 0; i < this._rows.length; i++) {
569
+ const row = this._rows[i];
570
+ for (const [field, tree] of Object.entries(this._btrees)) {
571
+ const v = row[field];
572
+ if (v !== undefined && v !== null) tree.insert(v, i);
573
+ }
574
+ }
575
+ }
576
+
520
577
  _rebuildAllBTrees() {
521
578
  for (const field of Object.keys(this._btrees)) {
522
579
  this._rebuildBTree(field);
@@ -546,6 +603,7 @@ class Table {
546
603
  // ============================================================
547
604
 
548
605
  _applyFilterOptimized(rows, query) {
606
+ this._ensureBTrees();
549
607
  // 尝试用 B-Tree 加速
550
608
  for (const [field, condition] of Object.entries(query)) {
551
609
  if (field === '$or' || field === '$and' || field === '$not') continue;
@@ -843,6 +901,7 @@ class Table {
843
901
  }
844
902
 
845
903
  _addToIndexes(rowIndex, data) {
904
+ this._ensureBTrees();
846
905
  for (const field of Object.keys(this._indexes)) {
847
906
  const value = data[field];
848
907
  if (value !== undefined) {
@@ -861,6 +920,7 @@ class Table {
861
920
  }
862
921
 
863
922
  _removeFromIndexes(rowIndex, data) {
923
+ this._ensureBTrees();
864
924
  for (const field of Object.keys(this._indexes)) {
865
925
  const value = data[field];
866
926
  if (value !== undefined && this._indexes[field].has(value)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "JSQL-NEO — Pure JavaScript embedded database with B-Tree indexes, WAL, hash JOIN, Redis-compatible Cache, and MySQL-style error codes",
5
5
  "main": "index.js",
6
6
  "files": [