jsql-neo 2.1.1 → 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;
@@ -715,13 +773,22 @@ class Table {
715
773
  throw createError('ER_BAD_NULL_ERROR', field);
716
774
  }
717
775
 
718
- // UNIQUE
776
+ // UNIQUE(B-Tree 优化 O(log n))
719
777
  if (def.unique && data[field] !== undefined) {
720
778
  if (uniqueTracker && uniqueTracker[field]) {
721
779
  if (uniqueTracker[field].has(data[field])) {
722
780
  throw createError('ER_DUP_ENTRY', String(data[field]), field);
723
781
  }
724
782
  uniqueTracker[field].add(data[field]);
783
+ } else if (this._btrees[field]) {
784
+ const found = this._btrees[field].search(data[field]);
785
+ if (found.length > 0) {
786
+ for (const idx of found) {
787
+ if (idx !== excludeIndex && this._rows[idx] && this._rows[idx][field] === data[field]) {
788
+ throw createError('ER_DUP_ENTRY', String(data[field]), field);
789
+ }
790
+ }
791
+ }
725
792
  } else {
726
793
  for (let i = 0; i < this._rows.length; i++) {
727
794
  if (i === excludeIndex) continue;
@@ -834,6 +901,7 @@ class Table {
834
901
  }
835
902
 
836
903
  _addToIndexes(rowIndex, data) {
904
+ this._ensureBTrees();
837
905
  for (const field of Object.keys(this._indexes)) {
838
906
  const value = data[field];
839
907
  if (value !== undefined) {
@@ -852,6 +920,7 @@ class Table {
852
920
  }
853
921
 
854
922
  _removeFromIndexes(rowIndex, data) {
923
+ this._ensureBTrees();
855
924
  for (const field of Object.keys(this._indexes)) {
856
925
  const value = data[field];
857
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.1",
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": [