jsql-neo 1.0.0 → 1.1.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/lib/database.js CHANGED
@@ -28,6 +28,7 @@ class Database {
28
28
  this._dirty = false;
29
29
  this._autoSaveTimer = null;
30
30
  this._loading = false; // 加载中,不触发自动保存
31
+ this._transaction = null; // 事务快照 { tables, autoIncrements }
31
32
 
32
33
  // 从文件加载
33
34
  if (!this._memoryMode && fs.existsSync(this._filePath)) {
@@ -96,6 +97,103 @@ class Database {
96
97
  return Object.keys(this._tables);
97
98
  }
98
99
 
100
+ // ============================================================
101
+ // 事务
102
+ // ============================================================
103
+
104
+ /**
105
+ * 开始事务
106
+ * 保存当前数据快照,后续操作可回滚
107
+ */
108
+ begin() {
109
+ if (this._transaction) {
110
+ throw new Error('Transaction already in progress');
111
+ }
112
+ this._transaction = {
113
+ rows: {},
114
+ autoIncrements: {}
115
+ };
116
+ for (const [name, table] of Object.entries(this._tables)) {
117
+ this._transaction.rows[name] = table._rows.map(r => ({ ...r }));
118
+ this._transaction.autoIncrements[name] = table._autoIncrement;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * 提交事务
124
+ */
125
+ commit() {
126
+ if (!this._transaction) {
127
+ throw new Error('No transaction in progress');
128
+ }
129
+ this._transaction = null;
130
+ this._markDirty();
131
+ }
132
+
133
+ /**
134
+ * 回滚事务
135
+ */
136
+ rollback() {
137
+ if (!this._transaction) {
138
+ throw new Error('No transaction in progress');
139
+ }
140
+ for (const [name, table] of Object.entries(this._tables)) {
141
+ if (this._transaction.rows[name]) {
142
+ table._rows = this._transaction.rows[name];
143
+ table._autoIncrement = this._transaction.autoIncrements[name];
144
+ // 重建索引
145
+ for (const field of Object.keys(table._indexes)) {
146
+ table.createIndex(field);
147
+ }
148
+ }
149
+ }
150
+ this._transaction = null;
151
+ }
152
+
153
+ /**
154
+ * 是否在事务中
155
+ */
156
+ inTransaction() {
157
+ return !!this._transaction;
158
+ }
159
+
160
+ // ============================================================
161
+ // 备份与恢复
162
+ // ============================================================
163
+
164
+ /**
165
+ * 备份到文件
166
+ */
167
+ backup(filePath) {
168
+ const data = this.export();
169
+ const json = this._options.pretty
170
+ ? JSON.stringify(data, null, 2)
171
+ : JSON.stringify(data);
172
+ const dir = path.dirname(filePath);
173
+ if (!fs.existsSync(dir)) {
174
+ fs.mkdirSync(dir, { recursive: true });
175
+ }
176
+ fs.writeFileSync(filePath, json, 'utf8');
177
+ return filePath;
178
+ }
179
+
180
+ /**
181
+ * 从文件恢复(覆盖当前数据)
182
+ */
183
+ restore(filePath) {
184
+ if (!fs.existsSync(filePath)) {
185
+ throw new Error(`Backup file not found: ${filePath}`);
186
+ }
187
+ const json = fs.readFileSync(filePath, 'utf8');
188
+ const data = JSON.parse(json);
189
+ for (const [name, rows] of Object.entries(data)) {
190
+ if (this._tables[name]) {
191
+ this._tables[name]._loadRows(rows);
192
+ }
193
+ }
194
+ this._markDirty();
195
+ }
196
+
99
197
  // ============================================================
100
198
  // 持久化
101
199
  // ============================================================
package/lib/table.js CHANGED
@@ -13,6 +13,14 @@ class Table {
13
13
  this._indexes = {}; // 字段名 -> Map<value, [rowIndex, ...]>
14
14
  this._autoIncrement = 0; // 自增计数器
15
15
  this._dirty = false; // 是否有未保存的修改
16
+ this._hooks = { // 事件钩子
17
+ beforeInsert: [],
18
+ afterInsert: [],
19
+ beforeUpdate: [],
20
+ afterUpdate: [],
21
+ beforeRemove: [],
22
+ afterRemove: []
23
+ };
16
24
 
17
25
  // 解析主键和自增字段
18
26
  this._primaryKey = null;
@@ -31,6 +39,11 @@ class Table {
31
39
  // ============================================================
32
40
 
33
41
  insert(data) {
42
+ // 触发 beforeInsert 钩子
43
+ for (const hook of this._hooks.beforeInsert) {
44
+ data = hook({ ...data }) || data;
45
+ }
46
+
34
47
  // 校验必填字段
35
48
  for (const [field, def] of Object.entries(this._schema)) {
36
49
  if (def.required && (data[field] === undefined || data[field] === null)) {
@@ -67,6 +80,12 @@ class Table {
67
80
 
68
81
  this._dirty = true;
69
82
  this._db._markDirty();
83
+
84
+ // 触发 afterInsert 钩子
85
+ for (const hook of this._hooks.afterInsert) {
86
+ hook(data);
87
+ }
88
+
70
89
  return data;
71
90
  }
72
91
 
@@ -139,6 +158,11 @@ class Table {
139
158
  // ============================================================
140
159
 
141
160
  update(query, updates) {
161
+ // 触发 beforeUpdate 钩子
162
+ for (const hook of this._hooks.beforeUpdate) {
163
+ updates = hook(query, { ...updates }) || updates;
164
+ }
165
+
142
166
  const matched = this._applyFilter(this._rows, query);
143
167
  let count = 0;
144
168
 
@@ -172,6 +196,11 @@ class Table {
172
196
  if (count > 0) {
173
197
  this._dirty = true;
174
198
  this._db._markDirty();
199
+
200
+ // 触发 afterUpdate 钩子
201
+ for (const hook of this._hooks.afterUpdate) {
202
+ hook(query, updates, count);
203
+ }
175
204
  }
176
205
  return count;
177
206
  }
@@ -188,6 +217,12 @@ class Table {
188
217
  // ============================================================
189
218
 
190
219
  remove(query = {}) {
220
+ // 触发 beforeRemove 钩子
221
+ for (const hook of this._hooks.beforeRemove) {
222
+ const result = hook(query);
223
+ if (result === false) return 0; // 钩子取消删除
224
+ }
225
+
191
226
  if (Object.keys(query).length === 0) {
192
227
  // 删除全部
193
228
  const count = this._rows.length;
@@ -225,6 +260,11 @@ class Table {
225
260
  if (toRemove.length > 0) {
226
261
  this._dirty = true;
227
262
  this._db._markDirty();
263
+
264
+ // 触发 afterRemove 钩子
265
+ for (const hook of this._hooks.afterRemove) {
266
+ hook(query, toRemove.length);
267
+ }
228
268
  }
229
269
  return toRemove.length;
230
270
  }
@@ -258,6 +298,35 @@ class Table {
258
298
  delete this._indexes[field];
259
299
  }
260
300
 
301
+ // ============================================================
302
+ // 事件钩子
303
+ // ============================================================
304
+
305
+ /**
306
+ * 注册事件钩子
307
+ * @param {string} event - 'beforeInsert' | 'afterInsert' | 'beforeUpdate' | 'afterUpdate' | 'beforeRemove' | 'afterRemove'
308
+ * @param {function} callback
309
+ * - beforeInsert(data) => 返回修改后的 data
310
+ * - afterInsert(data)
311
+ * - beforeUpdate(query, updates) => 返回修改后的 updates
312
+ * - afterUpdate(query, updates, count)
313
+ * - beforeRemove(query) => 返回 false 阻止删除
314
+ * - afterRemove(query, count)
315
+ */
316
+ on(event, callback) {
317
+ if (this._hooks[event]) {
318
+ this._hooks[event].push(callback);
319
+ }
320
+ return this;
321
+ }
322
+
323
+ off(event, callback) {
324
+ if (this._hooks[event]) {
325
+ this._hooks[event] = this._hooks[event].filter(cb => cb !== callback);
326
+ }
327
+ return this;
328
+ }
329
+
261
330
  // ============================================================
262
331
  // 内部方法
263
332
  // ============================================================
@@ -386,6 +455,7 @@ class Query {
386
455
  this._limitCount = null;
387
456
  this._offsetCount = 0;
388
457
  this._selectFields = null;
458
+ this._groupBy = null;
389
459
  }
390
460
 
391
461
  orderBy(field, dir = 'asc') {
@@ -409,6 +479,11 @@ class Query {
409
479
  return this;
410
480
  }
411
481
 
482
+ groupBy(field) {
483
+ this._groupBy = field;
484
+ return this;
485
+ }
486
+
412
487
  get() {
413
488
  let rows = this._table._applyFilter(this._table._rows, this._conditions);
414
489
 
@@ -456,6 +531,72 @@ class Query {
456
531
  return rows.length;
457
532
  }
458
533
 
534
+ sum(field) {
535
+ const rows = this._applyGroupedRows();
536
+ return rows.reduce((acc, r) => acc + (Number(r[field]) || 0), 0);
537
+ }
538
+
539
+ avg(field) {
540
+ const rows = this._applyGroupedRows();
541
+ if (rows.length === 0) return 0;
542
+ return this.sum(field) / rows.length;
543
+ }
544
+
545
+ min(field) {
546
+ const rows = this._applyGroupedRows();
547
+ if (rows.length === 0) return null;
548
+ return rows.reduce((min, r) => {
549
+ const v = Number(r[field]);
550
+ return (min === null || v < min) ? v : min;
551
+ }, null);
552
+ }
553
+
554
+ max(field) {
555
+ const rows = this._applyGroupedRows();
556
+ if (rows.length === 0) return null;
557
+ return rows.reduce((max, r) => {
558
+ const v = Number(r[field]);
559
+ return (max === null || v > max) ? v : max;
560
+ }, null);
561
+ }
562
+
563
+ /**
564
+ * 获取分组统计结果
565
+ * @returns {Array<{ _group: value, _count, _sum, _avg, _min, _max }>}
566
+ */
567
+ groupStats(field) {
568
+ if (!this._groupBy) throw new Error('groupStats() requires groupBy() to be called first');
569
+ const rows = this._table._applyFilter(this._table._rows, this._conditions);
570
+ const groups = {};
571
+
572
+ for (const row of rows) {
573
+ const key = String(row[this._groupBy] ?? '__null__');
574
+ if (!groups[key]) {
575
+ groups[key] = { _group: row[this._groupBy], _count: 0, _sum: 0, _min: null, _max: null, _rows: [] };
576
+ }
577
+ const g = groups[key];
578
+ g._count++;
579
+ const v = Number(row[field]) || 0;
580
+ g._sum += v;
581
+ g._min = g._min === null ? v : Math.min(g._min, v);
582
+ g._max = g._max === null ? v : Math.max(g._max, v);
583
+ g._rows.push(row);
584
+ }
585
+
586
+ const result = Object.values(groups);
587
+ result.forEach(g => {
588
+ g._avg = g._count > 0 ? g._sum / g._count : 0;
589
+ delete g._rows;
590
+ });
591
+
592
+ return result;
593
+ }
594
+
595
+ _applyGroupedRows() {
596
+ let rows = this._table._applyFilter(this._table._rows, this._conditions);
597
+ return rows;
598
+ }
599
+
459
600
  update(updates) {
460
601
  return this._table.update(this._conditions, updates);
461
602
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "JSQL — Pure JavaScript embedded database. SQL-like API, zero native dependencies, file-based or in-memory storage.",
5
5
  "main": "index.js",
6
6
  "files": [