jsql-neo 2.0.1 → 2.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/index.js +2 -1
- package/lib/btree.js +2 -1
- package/lib/cache.js +328 -0
- package/lib/database.js +3 -3
- package/lib/query.js +2 -0
- package/lib/table.js +42 -5
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// © Vexify 2026 All Rights Reserved.
|
|
2
2
|
/**
|
|
3
3
|
* JSQL-NEO — Pure JavaScript Embedded Database
|
|
4
|
-
* v2.
|
|
4
|
+
* v2.1.0 — B-Tree 索引、WAL、哈希 JOIN、Redis 兼容 Cache、事务隔离
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
7
|
* const jsql = require('jsql-neo');
|
|
@@ -13,6 +13,7 @@ module.exports = {
|
|
|
13
13
|
Table: require('./lib/table'),
|
|
14
14
|
Query: require('./lib/query'),
|
|
15
15
|
BTree: require('./lib/btree'),
|
|
16
|
+
Cache: require('./lib/cache'),
|
|
16
17
|
JSQL_Error: require('./lib/errors').JSQL_Error,
|
|
17
18
|
ErrorCodes: require('./lib/errors').ErrorCodes
|
|
18
19
|
};
|
package/lib/btree.js
CHANGED
|
@@ -241,8 +241,9 @@ class BTree {
|
|
|
241
241
|
return true;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
const origKey = node.keys[index];
|
|
244
245
|
this._mergeChildren(node, index);
|
|
245
|
-
return this._removeFromNode(node.children[index],
|
|
246
|
+
return this._removeFromNode(node.children[index], origKey, rowIndex);
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
_getMax(node) {
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class Cache {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this._data = new Map();
|
|
7
|
+
this._expires = new Map();
|
|
8
|
+
this._ttlTimer = null;
|
|
9
|
+
this._db = 0;
|
|
10
|
+
this._dbs = [this._data];
|
|
11
|
+
this._dbsExpires = [this._expires];
|
|
12
|
+
this._persistPath = options.path || null;
|
|
13
|
+
this._saveInterval = options.saveInterval || 0;
|
|
14
|
+
this._dirty = false;
|
|
15
|
+
if (this._persistPath) this._load();
|
|
16
|
+
if (this._saveInterval > 0) this._startAutoSave();
|
|
17
|
+
this._startTTLCleaner();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
set(key, value) { this._getData().set(String(key), String(value)); this._dirty = true; return 'OK'; }
|
|
21
|
+
get(key) {
|
|
22
|
+
const k = String(key);
|
|
23
|
+
if (this._isExpired(k)) return null;
|
|
24
|
+
const val = this._getData().get(k);
|
|
25
|
+
return val !== undefined ? val : null;
|
|
26
|
+
}
|
|
27
|
+
del(...keys) {
|
|
28
|
+
let count = 0;
|
|
29
|
+
for (const key of keys) { const k = String(key); if (this._getData().delete(k)) count++; this._getExpires().delete(k); }
|
|
30
|
+
if (count > 0) this._dirty = true;
|
|
31
|
+
return count;
|
|
32
|
+
}
|
|
33
|
+
exists(...keys) {
|
|
34
|
+
let count = 0;
|
|
35
|
+
for (const key of keys) { const k = String(key); if (!this._isExpired(k) && this._getData().has(k)) count++; }
|
|
36
|
+
return count;
|
|
37
|
+
}
|
|
38
|
+
keys(pattern = '*') {
|
|
39
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
40
|
+
const result = [];
|
|
41
|
+
for (const key of this._getData().keys()) { if (!this._isExpired(key) && regex.test(key)) result.push(key); }
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
type(key) {
|
|
45
|
+
const k = String(key);
|
|
46
|
+
if (this._isExpired(k)) return 'none';
|
|
47
|
+
const val = this._getData().get(k);
|
|
48
|
+
if (val === undefined) return 'none';
|
|
49
|
+
if (typeof val === 'string') return 'string';
|
|
50
|
+
if (Array.isArray(val)) return val._type === 'hash' ? 'hash' : 'list';
|
|
51
|
+
if (val instanceof Set) return 'set';
|
|
52
|
+
return 'string';
|
|
53
|
+
}
|
|
54
|
+
expire(key, seconds) {
|
|
55
|
+
const k = String(key);
|
|
56
|
+
if (!this._getData().has(k)) return 0;
|
|
57
|
+
this._getExpires().set(k, Date.now() + seconds * 1000);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
ttl(key) {
|
|
61
|
+
const k = String(key);
|
|
62
|
+
if (!this._getData().has(k)) return -2;
|
|
63
|
+
const expiry = this._getExpires().get(k);
|
|
64
|
+
if (expiry === undefined) return -1;
|
|
65
|
+
const remaining = Math.ceil((expiry - Date.now()) / 1000);
|
|
66
|
+
return remaining > 0 ? remaining : -2;
|
|
67
|
+
}
|
|
68
|
+
persist(key) { return this._getExpires().delete(String(key)) ? 1 : 0; }
|
|
69
|
+
incr(key) { return this._incrby(key, 1); }
|
|
70
|
+
decr(key) { return this._incrby(key, -1); }
|
|
71
|
+
incrby(key, increment) { return this._incrby(key, increment); }
|
|
72
|
+
decrby(key, decrement) { return this._incrby(key, -decrement); }
|
|
73
|
+
_incrby(key, delta) {
|
|
74
|
+
const k = String(key);
|
|
75
|
+
const cur = this._getData().get(k);
|
|
76
|
+
const val = (cur !== undefined ? Number(cur) : 0) + delta;
|
|
77
|
+
if (!Number.isFinite(val)) throw new Error('value is not an integer or out of range');
|
|
78
|
+
this._getData().set(k, String(val));
|
|
79
|
+
this._dirty = true;
|
|
80
|
+
return val;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
hset(key, field, value) {
|
|
84
|
+
const k = String(key);
|
|
85
|
+
let hash = this._getData().get(k);
|
|
86
|
+
if (!hash || hash._type !== 'hash') { hash = { _type: 'hash' }; this._getData().set(k, hash); }
|
|
87
|
+
const existed = hash[field] !== undefined;
|
|
88
|
+
hash[field] = String(value);
|
|
89
|
+
this._dirty = true;
|
|
90
|
+
return existed ? 0 : 1;
|
|
91
|
+
}
|
|
92
|
+
hget(key, field) {
|
|
93
|
+
const k = String(key);
|
|
94
|
+
if (this._isExpired(k)) return null;
|
|
95
|
+
const hash = this._getData().get(k);
|
|
96
|
+
if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return null;
|
|
97
|
+
return hash[field] !== undefined ? hash[field] : null;
|
|
98
|
+
}
|
|
99
|
+
hgetall(key) {
|
|
100
|
+
const k = String(key);
|
|
101
|
+
if (this._isExpired(k)) return null;
|
|
102
|
+
const hash = this._getData().get(k);
|
|
103
|
+
if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return null;
|
|
104
|
+
const result = {};
|
|
105
|
+
for (const [f, v] of Object.entries(hash)) { if (f !== '_type') result[f] = v; }
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
hdel(key, ...fields) {
|
|
109
|
+
const k = String(key);
|
|
110
|
+
const hash = this._getData().get(k);
|
|
111
|
+
if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return 0;
|
|
112
|
+
let count = 0;
|
|
113
|
+
for (const field of fields) { if (delete hash[field]) count++; }
|
|
114
|
+
if (count > 0) this._dirty = true;
|
|
115
|
+
return count;
|
|
116
|
+
}
|
|
117
|
+
hkeys(key) {
|
|
118
|
+
const k = String(key);
|
|
119
|
+
if (this._isExpired(k)) return [];
|
|
120
|
+
const hash = this._getData().get(k);
|
|
121
|
+
if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return [];
|
|
122
|
+
return Object.keys(hash).filter(f => f !== '_type');
|
|
123
|
+
}
|
|
124
|
+
hlen(key) {
|
|
125
|
+
const k = String(key);
|
|
126
|
+
if (this._isExpired(k)) return 0;
|
|
127
|
+
const hash = this._getData().get(k);
|
|
128
|
+
if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return 0;
|
|
129
|
+
return Object.keys(hash).filter(f => f !== '_type').length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lpush(key, ...values) {
|
|
133
|
+
const k = String(key);
|
|
134
|
+
let list = this._getData().get(k);
|
|
135
|
+
if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
|
|
136
|
+
list.unshift(...values.map(String).reverse());
|
|
137
|
+
this._dirty = true;
|
|
138
|
+
return list.length;
|
|
139
|
+
}
|
|
140
|
+
rpush(key, ...values) {
|
|
141
|
+
const k = String(key);
|
|
142
|
+
let list = this._getData().get(k);
|
|
143
|
+
if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
|
|
144
|
+
list.push(...values.map(String));
|
|
145
|
+
this._dirty = true;
|
|
146
|
+
return list.length;
|
|
147
|
+
}
|
|
148
|
+
lpop(key) {
|
|
149
|
+
const k = String(key);
|
|
150
|
+
const list = this._getData().get(k);
|
|
151
|
+
if (!list || !Array.isArray(list) || list._type === 'hash') return null;
|
|
152
|
+
if (list.length === 0) return null;
|
|
153
|
+
this._dirty = true;
|
|
154
|
+
return list.shift();
|
|
155
|
+
}
|
|
156
|
+
rpop(key) {
|
|
157
|
+
const k = String(key);
|
|
158
|
+
const list = this._getData().get(k);
|
|
159
|
+
if (!list || !Array.isArray(list) || list._type === 'hash') return null;
|
|
160
|
+
if (list.length === 0) return null;
|
|
161
|
+
this._dirty = true;
|
|
162
|
+
return list.pop();
|
|
163
|
+
}
|
|
164
|
+
llen(key) {
|
|
165
|
+
const k = String(key);
|
|
166
|
+
if (this._isExpired(k)) return 0;
|
|
167
|
+
const list = this._getData().get(k);
|
|
168
|
+
if (!list || !Array.isArray(list) || list._type === 'hash') return 0;
|
|
169
|
+
return list.length;
|
|
170
|
+
}
|
|
171
|
+
lrange(key, start, stop) {
|
|
172
|
+
const k = String(key);
|
|
173
|
+
if (this._isExpired(k)) return [];
|
|
174
|
+
const list = this._getData().get(k);
|
|
175
|
+
if (!list || !Array.isArray(list) || list._type === 'hash') return [];
|
|
176
|
+
if (start < 0) start = Math.max(list.length + start, 0);
|
|
177
|
+
if (stop < 0) stop = list.length + stop;
|
|
178
|
+
return list.slice(start, stop + 1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
sadd(key, ...members) {
|
|
182
|
+
const k = String(key);
|
|
183
|
+
let set = this._getData().get(k);
|
|
184
|
+
if (!set || !(set instanceof Set)) { set = new Set(); this._getData().set(k, set); }
|
|
185
|
+
let count = 0;
|
|
186
|
+
for (const m of members) { if (!set.has(String(m))) { set.add(String(m)); count++; } }
|
|
187
|
+
if (count > 0) this._dirty = true;
|
|
188
|
+
return count;
|
|
189
|
+
}
|
|
190
|
+
srem(key, ...members) {
|
|
191
|
+
const k = String(key);
|
|
192
|
+
const set = this._getData().get(k);
|
|
193
|
+
if (!set || !(set instanceof Set)) return 0;
|
|
194
|
+
let count = 0;
|
|
195
|
+
for (const m of members) { if (set.delete(String(m))) count++; }
|
|
196
|
+
if (count > 0) this._dirty = true;
|
|
197
|
+
return count;
|
|
198
|
+
}
|
|
199
|
+
smembers(key) {
|
|
200
|
+
const k = String(key);
|
|
201
|
+
if (this._isExpired(k)) return [];
|
|
202
|
+
const set = this._getData().get(k);
|
|
203
|
+
if (!set || !(set instanceof Set)) return [];
|
|
204
|
+
return [...set];
|
|
205
|
+
}
|
|
206
|
+
sismember(key, member) {
|
|
207
|
+
const k = String(key);
|
|
208
|
+
if (this._isExpired(k)) return 0;
|
|
209
|
+
const set = this._getData().get(k);
|
|
210
|
+
if (!set || !(set instanceof Set)) return 0;
|
|
211
|
+
return set.has(String(member)) ? 1 : 0;
|
|
212
|
+
}
|
|
213
|
+
scard(key) {
|
|
214
|
+
const k = String(key);
|
|
215
|
+
if (this._isExpired(k)) return 0;
|
|
216
|
+
const set = this._getData().get(k);
|
|
217
|
+
if (!set || !(set instanceof Set)) return 0;
|
|
218
|
+
return set.size;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
flushall() {
|
|
222
|
+
for (let i = 0; i < this._dbs.length; i++) { if (this._dbs[i]) this._dbs[i].clear(); if (this._dbsExpires[i]) this._dbsExpires[i].clear(); }
|
|
223
|
+
this._data = this._dbs[0] = new Map();
|
|
224
|
+
this._expires = this._dbsExpires[0] = new Map();
|
|
225
|
+
this._db = 0;
|
|
226
|
+
this._dirty = true;
|
|
227
|
+
return 'OK';
|
|
228
|
+
}
|
|
229
|
+
flushdb() { this._getData().clear(); this._getExpires().clear(); this._dirty = true; return 'OK'; }
|
|
230
|
+
dbsize() {
|
|
231
|
+
let count = 0;
|
|
232
|
+
for (const key of this._getData().keys()) { if (!this._isExpired(key)) count++; }
|
|
233
|
+
return count;
|
|
234
|
+
}
|
|
235
|
+
select(index) {
|
|
236
|
+
const idx = Number(index);
|
|
237
|
+
if (idx < 0 || idx > 15) throw new Error('DB index out of range');
|
|
238
|
+
this._db = idx;
|
|
239
|
+
if (!this._dbs[idx]) { this._dbs[idx] = new Map(); this._dbsExpires[idx] = new Map(); }
|
|
240
|
+
this._data = this._dbs[idx];
|
|
241
|
+
this._expires = this._dbsExpires[idx];
|
|
242
|
+
return 'OK';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
save() {
|
|
246
|
+
if (!this._persistPath) return;
|
|
247
|
+
const dump = { version: 1, databases: [] };
|
|
248
|
+
for (let i = 0; i < this._dbs.length; i++) {
|
|
249
|
+
const db = this._dbs[i];
|
|
250
|
+
if (!db || db.size === 0) continue;
|
|
251
|
+
const data = {};
|
|
252
|
+
for (const [key, val] of db) {
|
|
253
|
+
const exp = this._dbsExpires[i];
|
|
254
|
+
if (exp && exp.has(key) && exp.get(key) <= Date.now()) continue;
|
|
255
|
+
if (val instanceof Set) data[key] = { _t: 'set', _v: [...val] };
|
|
256
|
+
else if (Array.isArray(val)) data[key] = { _t: 'list', _v: val };
|
|
257
|
+
else if (typeof val === 'object' && val._type === 'hash') {
|
|
258
|
+
const clean = {}; for (const [f, v] of Object.entries(val)) { if (f !== '_type') clean[f] = v; }
|
|
259
|
+
data[key] = { _t: 'hash', _v: clean };
|
|
260
|
+
} else data[key] = { _t: 'string', _v: val };
|
|
261
|
+
}
|
|
262
|
+
const expires = {};
|
|
263
|
+
const expMap = this._dbsExpires[i];
|
|
264
|
+
if (expMap) { for (const [key, ttl] of expMap) { if (ttl > Date.now()) expires[key] = ttl; } }
|
|
265
|
+
if (Object.keys(data).length > 0 || Object.keys(expires).length > 0) dump.databases.push({ index: i, data, expires });
|
|
266
|
+
}
|
|
267
|
+
const dir = path.dirname(this._persistPath);
|
|
268
|
+
if (dir) fs.mkdirSync(dir, { recursive: true });
|
|
269
|
+
fs.writeFileSync(this._persistPath + '.tmp', JSON.stringify(dump), 'utf8');
|
|
270
|
+
fs.renameSync(this._persistPath + '.tmp', this._persistPath);
|
|
271
|
+
this._dirty = false;
|
|
272
|
+
return 'OK';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_load() {
|
|
276
|
+
try {
|
|
277
|
+
if (!fs.existsSync(this._persistPath)) return;
|
|
278
|
+
const dump = JSON.parse(fs.readFileSync(this._persistPath, 'utf8'));
|
|
279
|
+
if (dump.version !== 1) return;
|
|
280
|
+
for (const db of dump.databases || []) {
|
|
281
|
+
const idx = db.index;
|
|
282
|
+
if (!this._dbs[idx]) { this._dbs[idx] = new Map(); this._dbsExpires[idx] = new Map(); }
|
|
283
|
+
const data = this._dbs[idx], expires = this._dbsExpires[idx];
|
|
284
|
+
for (const [key, entry] of Object.entries(db.data || {})) {
|
|
285
|
+
if (entry._t === 'set') data.set(key, new Set(entry._v));
|
|
286
|
+
else if (entry._t === 'list') data.set(key, entry._v);
|
|
287
|
+
else if (entry._t === 'hash') data.set(key, { _type: 'hash', ...entry._v });
|
|
288
|
+
else data.set(key, entry._v);
|
|
289
|
+
}
|
|
290
|
+
for (const [key, ttl] of Object.entries(db.expires || {})) { if (ttl > Date.now()) expires.set(key, ttl); }
|
|
291
|
+
}
|
|
292
|
+
this._data = this._dbs[this._db] || new Map();
|
|
293
|
+
this._expires = this._dbsExpires[this._db] || new Map();
|
|
294
|
+
} catch (_) { /* ignore */ }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_startAutoSave() { this._saveTimer = setInterval(() => { if (this._dirty) this.save(); }, this._saveInterval); }
|
|
298
|
+
_startTTLCleaner() {
|
|
299
|
+
this._ttlTimer = setInterval(() => {
|
|
300
|
+
for (let i = 0; i < this._dbs.length; i++) {
|
|
301
|
+
const exp = this._dbsExpires[i]; if (!exp) continue;
|
|
302
|
+
const now = Date.now();
|
|
303
|
+
for (const [key, ttl] of exp) { if (ttl <= now) { if (this._dbs[i]) this._dbs[i].delete(key); exp.delete(key); } }
|
|
304
|
+
}
|
|
305
|
+
}, 1000);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
close() { if (this._persistPath && this._dirty) this.save(); if (this._saveTimer) clearInterval(this._saveTimer); if (this._ttlTimer) clearInterval(this._ttlTimer); }
|
|
309
|
+
_isExpired(key) {
|
|
310
|
+
const expiry = this._getExpires().get(key);
|
|
311
|
+
if (expiry !== undefined && expiry <= Date.now()) { this._getData().delete(key); this._getExpires().delete(key); return true; }
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
_getData() { return this._data; }
|
|
315
|
+
_getExpires() { return this._expires; }
|
|
316
|
+
|
|
317
|
+
static redisPlugin(db, options = {}) {
|
|
318
|
+
const cache = new Cache(options);
|
|
319
|
+
db._cache = cache;
|
|
320
|
+
const redisMethods = ['set','get','del','exists','keys','type','expire','ttl','persist','incr','decr','incrby','decrby','hset','hget','hgetall','hdel','hkeys','hlen','lpush','rpush','lpop','rpop','llen','lrange','sadd','srem','smembers','sismember','scard','flushall','flushdb','dbsize','select'];
|
|
321
|
+
for (const m of redisMethods) {
|
|
322
|
+
db[m] = cache[m].bind(cache);
|
|
323
|
+
}
|
|
324
|
+
return cache;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = Cache;
|
package/lib/database.js
CHANGED
|
@@ -21,7 +21,7 @@ class Database {
|
|
|
21
21
|
* @param {boolean} options.versioning - 是否启用版本历史(默认 false)
|
|
22
22
|
* @param {boolean} options.wal - 是否启用 WAL 模式(默认 false,内存模式自动禁用)
|
|
23
23
|
* @param {string} options.isolationLevel - 事务隔离级别 'READ_COMMITTED' | 'REPEATABLE_READ'(默认 'READ_COMMITTED')
|
|
24
|
-
* @param {number} options.slowQueryThreshold - 慢查询阈值 ms(默认 100,0 禁用)
|
|
24
|
+
* @param {number} options.slowQueryThreshold - 慢查询阈值 ms(默认 100,0 记录全部,<0 禁用)
|
|
25
25
|
* @param {boolean} options.fileLock - 是否启用文件锁(默认 false)
|
|
26
26
|
*/
|
|
27
27
|
constructor(filePath, options = {}) {
|
|
@@ -514,8 +514,8 @@ class Database {
|
|
|
514
514
|
* 记录慢查询
|
|
515
515
|
*/
|
|
516
516
|
_logSlowQuery(sql, durationMs, rowsReturned) {
|
|
517
|
-
if (this._slowQueryThreshold
|
|
518
|
-
if (durationMs < this._slowQueryThreshold) return;
|
|
517
|
+
if (this._slowQueryThreshold < 0) return;
|
|
518
|
+
if (this._slowQueryThreshold > 0 && durationMs < this._slowQueryThreshold) return;
|
|
519
519
|
|
|
520
520
|
this._slowQueries.push({
|
|
521
521
|
sql,
|
package/lib/query.js
CHANGED
|
@@ -223,6 +223,7 @@ class Query {
|
|
|
223
223
|
// ============================================================
|
|
224
224
|
|
|
225
225
|
get() {
|
|
226
|
+
const startTime = Date.now();
|
|
226
227
|
if (this._explain) return this._getExplain();
|
|
227
228
|
|
|
228
229
|
// 缓存命中
|
|
@@ -319,6 +320,7 @@ class Query {
|
|
|
319
320
|
this._cache.timestamp = Date.now();
|
|
320
321
|
}
|
|
321
322
|
|
|
323
|
+
this._table._db._logSlowQuery(`get ${this._table._name}`, Date.now() - startTime, rows.length);
|
|
322
324
|
return rows;
|
|
323
325
|
}
|
|
324
326
|
|
package/lib/table.js
CHANGED
|
@@ -117,7 +117,37 @@ class Table {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
insertMany(items) {
|
|
120
|
-
|
|
120
|
+
if (items.length === 0) return [];
|
|
121
|
+
for (const hook of this._hooks.beforeInsert) {
|
|
122
|
+
items = items.map(data => hook({ ...data }) || data);
|
|
123
|
+
}
|
|
124
|
+
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) {
|
|
135
|
+
this._autoIncrement++;
|
|
136
|
+
data[this._autoIncrementField] = this._autoIncrement;
|
|
137
|
+
} else if (this._autoIncrementField && data[this._autoIncrementField] > this._autoIncrement) {
|
|
138
|
+
this._autoIncrement = data[this._autoIncrementField];
|
|
139
|
+
}
|
|
140
|
+
this._rows.push({ ...data });
|
|
141
|
+
results.push(data);
|
|
142
|
+
}
|
|
143
|
+
this._rebuildAllBTrees();
|
|
144
|
+
this._dirty = true;
|
|
145
|
+
this._db._markDirty();
|
|
146
|
+
for (const hook of this._hooks.afterInsert) {
|
|
147
|
+
for (const data of results) hook(data);
|
|
148
|
+
}
|
|
149
|
+
for (const data of results) this._db._emitChange('insert', this._name, data);
|
|
150
|
+
return results;
|
|
121
151
|
}
|
|
122
152
|
|
|
123
153
|
upsert(data) {
|
|
@@ -676,7 +706,7 @@ class Table {
|
|
|
676
706
|
// 内部: 约束校验
|
|
677
707
|
// ============================================================
|
|
678
708
|
|
|
679
|
-
_validateConstraints(data, excludeIndex) {
|
|
709
|
+
_validateConstraints(data, excludeIndex, uniqueTracker) {
|
|
680
710
|
for (const [field, def] of Object.entries(this._schema)) {
|
|
681
711
|
if (field === '_softDelete') continue;
|
|
682
712
|
|
|
@@ -687,11 +717,18 @@ class Table {
|
|
|
687
717
|
|
|
688
718
|
// UNIQUE
|
|
689
719
|
if (def.unique && data[field] !== undefined) {
|
|
690
|
-
|
|
691
|
-
if (
|
|
692
|
-
if (this._rows[i][field] === data[field]) {
|
|
720
|
+
if (uniqueTracker && uniqueTracker[field]) {
|
|
721
|
+
if (uniqueTracker[field].has(data[field])) {
|
|
693
722
|
throw createError('ER_DUP_ENTRY', String(data[field]), field);
|
|
694
723
|
}
|
|
724
|
+
uniqueTracker[field].add(data[field]);
|
|
725
|
+
} else {
|
|
726
|
+
for (let i = 0; i < this._rows.length; i++) {
|
|
727
|
+
if (i === excludeIndex) continue;
|
|
728
|
+
if (this._rows[i][field] === data[field]) {
|
|
729
|
+
throw createError('ER_DUP_ENTRY', String(data[field]), field);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
695
732
|
}
|
|
696
733
|
}
|
|
697
734
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "JSQL-NEO — Pure JavaScript embedded database with B-Tree indexes, WAL, hash JOIN, and MySQL-style error codes",
|
|
3
|
+
"version": "2.1.1",
|
|
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": [
|
|
7
7
|
"index.js",
|