jsql-neo 1.1.0 → 1.2.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/lib/table.js CHANGED
@@ -1,36 +1,61 @@
1
1
  // © Vexify 2026 All Rights Reserved.
2
2
  /**
3
3
  * JSQL Table — 表操作引擎
4
- * 每张表是一个独立的数据集合,支持 CRUD、查询、索引
4
+ * 支持 DEFAULT、CHECK、外键、UPSERT、TRUNCATE、ALTER TABLE、计算字段、JSON 路径、软删除
5
5
  */
6
6
 
7
+ const Query = require('./query');
8
+
7
9
  class Table {
8
10
  constructor(name, schema, db) {
9
11
  this._name = name;
10
12
  this._schema = schema;
11
13
  this._db = db;
12
- this._rows = []; // 数据行
13
- this._indexes = {}; // 字段名 -> Map<value, [rowIndex, ...]>
14
- this._autoIncrement = 0; // 自增计数器
15
- this._dirty = false; // 是否有未保存的修改
16
- this._hooks = { // 事件钩子
17
- beforeInsert: [],
18
- afterInsert: [],
19
- beforeUpdate: [],
20
- afterUpdate: [],
21
- beforeRemove: [],
22
- afterRemove: []
14
+ this._rows = [];
15
+ this._indexes = {};
16
+ this._autoIncrement = 0;
17
+ this._dirty = false;
18
+ this._hooks = {
19
+ beforeInsert: [], afterInsert: [],
20
+ beforeUpdate: [], afterUpdate: [],
21
+ beforeRemove: [], afterRemove: []
23
22
  };
23
+ this._softDelete = false; // 是否启用软删除
24
+ this._computedFields = {}; // 计算字段
24
25
 
25
- // 解析主键和自增字段
26
+ // 解析列定义
26
27
  this._primaryKey = null;
27
28
  this._autoIncrementField = null;
29
+ this._foreignKeys = [];
30
+ this._checkConstraints = [];
31
+ this._defaults = {};
32
+
28
33
  for (const [field, def] of Object.entries(schema)) {
29
34
  if (def.primaryKey) this._primaryKey = field;
30
35
  if (def.autoIncrement) {
31
36
  this._autoIncrementField = field;
32
37
  if (def.primaryKey) this._primaryKey = field;
33
38
  }
39
+ if (def.default !== undefined) {
40
+ this._defaults[field] = def.default;
41
+ }
42
+ if (def.check) {
43
+ this._checkConstraints.push({ field, fn: def.check });
44
+ }
45
+ if (def.foreignKey) {
46
+ this._foreignKeys.push({
47
+ field,
48
+ table: def.foreignKey.table,
49
+ onDelete: def.foreignKey.onDelete || 'restrict'
50
+ });
51
+ }
52
+ if (def.computed) {
53
+ this._computedFields[field] = def.computed;
54
+ }
55
+ }
56
+
57
+ if (schema._softDelete) {
58
+ this._softDelete = true;
34
59
  }
35
60
  }
36
61
 
@@ -39,19 +64,32 @@ class Table {
39
64
  // ============================================================
40
65
 
41
66
  insert(data) {
42
- // 触发 beforeInsert 钩子
43
67
  for (const hook of this._hooks.beforeInsert) {
44
68
  data = hook({ ...data }) || data;
45
69
  }
46
70
 
47
- // 校验必填字段
71
+ // 填充默认值
72
+ data = this._applyDefaults(data);
73
+
74
+ // 计算字段
75
+ data = this._applyComputed(data);
76
+
77
+ // 校验必填
48
78
  for (const [field, def] of Object.entries(this._schema)) {
79
+ if (field === '_softDelete') continue;
49
80
  if (def.required && (data[field] === undefined || data[field] === null)) {
50
81
  throw new Error(`Field '${field}' is required in table '${this._name}'`);
51
82
  }
52
83
  }
53
84
 
54
- // 唯一约束检查
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
+ // 唯一约束
55
93
  for (const [field, def] of Object.entries(this._schema)) {
56
94
  if (def.unique && data[field] !== undefined) {
57
95
  for (const row of this._rows) {
@@ -63,29 +101,25 @@ class Table {
63
101
  }
64
102
 
65
103
  // 自增
66
- if (this._autoIncrementField) {
67
- if (data[this._autoIncrementField] === undefined) {
68
- this._autoIncrement++;
69
- data[this._autoIncrementField] = this._autoIncrement;
70
- } else if (data[this._autoIncrementField] > this._autoIncrement) {
71
- this._autoIncrement = data[this._autoIncrementField];
72
- }
104
+ if (this._autoIncrementField && data[this._autoIncrementField] === undefined) {
105
+ this._autoIncrement++;
106
+ data[this._autoIncrementField] = this._autoIncrement;
107
+ } else if (this._autoIncrementField && data[this._autoIncrementField] > this._autoIncrement) {
108
+ this._autoIncrement = data[this._autoIncrementField];
73
109
  }
74
110
 
75
111
  const rowIndex = this._rows.length;
76
112
  this._rows.push({ ...data });
77
-
78
- // 更新索引
79
113
  this._updateIndexes(rowIndex, data);
80
-
81
114
  this._dirty = true;
82
115
  this._db._markDirty();
83
116
 
84
- // 触发 afterInsert 钩子
85
117
  for (const hook of this._hooks.afterInsert) {
86
118
  hook(data);
87
119
  }
88
120
 
121
+ this._db._emitChange('insert', this._name, data);
122
+
89
123
  return data;
90
124
  }
91
125
 
@@ -93,62 +127,52 @@ class Table {
93
127
  return items.map(item => this.insert(item));
94
128
  }
95
129
 
130
+ /**
131
+ * UPSERT — 有则更新,无则插入
132
+ * 按主键或唯一字段判断
133
+ */
134
+ upsert(data) {
135
+ if (this._primaryKey && data[this._primaryKey] !== undefined) {
136
+ const existing = this.findById(data[this._primaryKey]);
137
+ if (existing) {
138
+ this.updateById(data[this._primaryKey], data);
139
+ return this.findById(data[this._primaryKey]);
140
+ }
141
+ }
142
+ return this.insert(data);
143
+ }
144
+
96
145
  // ============================================================
97
146
  // 查询
98
147
  // ============================================================
99
148
 
100
- /**
101
- * 查找所有匹配的行
102
- */
103
149
  find(query = {}) {
104
- let rows = this._rows;
105
-
150
+ let rows = this._getVisibleRows();
106
151
  if (Object.keys(query).length > 0) {
107
152
  rows = this._applyFilter(rows, query);
108
153
  }
109
-
110
154
  return rows.map(r => ({ ...r }));
111
155
  }
112
156
 
113
- /**
114
- * 查找第一条匹配的行
115
- */
116
157
  findOne(query = {}) {
117
158
  const rows = this.find(query);
118
159
  return rows.length > 0 ? rows[0] : null;
119
160
  }
120
161
 
121
- /**
122
- * 按主键查找
123
- */
124
162
  findById(id) {
125
- if (!this._primaryKey) {
126
- throw new Error(`Table '${this._name}' has no primary key`);
127
- }
163
+ if (!this._primaryKey) throw new Error(`Table '${this._name}' has no primary key`);
128
164
  return this.findOne({ [this._primaryKey]: id });
129
165
  }
130
166
 
131
- /**
132
- * 获取所有行
133
- */
134
167
  findAll() {
135
- return this._rows.map(r => ({ ...r }));
168
+ return this._getVisibleRows().map(r => ({ ...r }));
136
169
  }
137
170
 
138
- /**
139
- * 计数
140
- */
141
171
  count(query = {}) {
142
- if (Object.keys(query).length === 0) {
143
- return this._rows.length;
144
- }
145
- return this._applyFilter(this._rows, query).length;
172
+ if (Object.keys(query).length === 0) return this._getVisibleRows().length;
173
+ return this._applyFilter(this._getVisibleRows(), query).length;
146
174
  }
147
175
 
148
- // ============================================================
149
- // 链式查询
150
- // ============================================================
151
-
152
176
  where(conditions) {
153
177
  return new Query(this, conditions);
154
178
  }
@@ -158,12 +182,11 @@ class Table {
158
182
  // ============================================================
159
183
 
160
184
  update(query, updates) {
161
- // 触发 beforeUpdate 钩子
162
185
  for (const hook of this._hooks.beforeUpdate) {
163
186
  updates = hook(query, { ...updates }) || updates;
164
187
  }
165
188
 
166
- const matched = this._applyFilter(this._rows, query);
189
+ const matched = this._applyFilter(this._getVisibleRows(), query);
167
190
  let count = 0;
168
191
 
169
192
  for (const row of matched) {
@@ -172,7 +195,7 @@ class Table {
172
195
 
173
196
  const oldData = { ...this._rows[index] };
174
197
 
175
- // 唯一约束检查
198
+ // 唯一约束
176
199
  for (const [field, def] of Object.entries(this._schema)) {
177
200
  if (def.unique && updates[field] !== undefined) {
178
201
  for (let i = 0; i < this._rows.length; i++) {
@@ -183,32 +206,33 @@ class Table {
183
206
  }
184
207
  }
185
208
 
186
- // 更新
187
- Object.assign(this._rows[index], updates);
209
+ // CHECK 约束
210
+ const merged = { ...this._rows[index], ...updates };
211
+ for (const { field, fn } of this._checkConstraints) {
212
+ if (merged[field] !== undefined && !fn(merged[field], merged)) {
213
+ throw new Error(`CHECK constraint failed on '${this._name}.${field}'`);
214
+ }
215
+ }
188
216
 
189
- // 更新索引
217
+ Object.assign(this._rows[index], updates);
190
218
  this._removeFromIndexes(index, oldData);
191
219
  this._updateIndexes(index, this._rows[index]);
192
-
193
220
  count++;
194
221
  }
195
222
 
196
223
  if (count > 0) {
197
224
  this._dirty = true;
198
225
  this._db._markDirty();
199
-
200
- // 触发 afterUpdate 钩子
201
226
  for (const hook of this._hooks.afterUpdate) {
202
227
  hook(query, updates, count);
203
228
  }
229
+ this._db._emitChange('update', this._name, { query, updates, count });
204
230
  }
205
231
  return count;
206
232
  }
207
233
 
208
234
  updateById(id, updates) {
209
- if (!this._primaryKey) {
210
- throw new Error(`Table '${this._name}' has no primary key`);
211
- }
235
+ if (!this._primaryKey) throw new Error(`Table '${this._name}' has no primary key`);
212
236
  return this.update({ [this._primaryKey]: id }, updates);
213
237
  }
214
238
 
@@ -217,65 +241,192 @@ class Table {
217
241
  // ============================================================
218
242
 
219
243
  remove(query = {}) {
220
- // 触发 beforeRemove 钩子
221
244
  for (const hook of this._hooks.beforeRemove) {
222
- const result = hook(query);
223
- if (result === false) return 0; // 钩子取消删除
245
+ if (hook(query) === false) return 0;
224
246
  }
225
247
 
226
248
  if (Object.keys(query).length === 0) {
227
- // 删除全部
228
- const count = this._rows.length;
229
- this._rows = [];
230
- this._indexes = {};
231
- this._autoIncrement = 0;
232
- this._dirty = true;
233
- this._db._markDirty();
234
- return count;
249
+ return this._truncate();
235
250
  }
236
251
 
237
- const toRemove = this._applyFilter(this._rows, query);
252
+ const toRemove = this._applyFilter(this._getVisibleRows(), query);
253
+ let count = 0;
254
+
238
255
  for (const row of toRemove) {
239
256
  const index = this._rows.indexOf(row);
240
- if (index !== -1) {
241
- this._removeFromIndexes(index, row);
242
- this._rows.splice(index, 1);
243
- // 调整后续索引
244
- for (const idxName of Object.keys(this._indexes)) {
245
- const idx = this._indexes[idxName];
246
- for (const [key, positions] of idx) {
247
- const newPositions = positions
248
- .filter(p => p !== index)
249
- .map(p => p > index ? p - 1 : p);
250
- if (newPositions.length === 0) {
251
- idx.delete(key);
252
- } else {
253
- idx.set(key, newPositions);
254
- }
257
+ if (index === -1) continue;
258
+
259
+ // 外键级联删除
260
+ for (const fk of this._foreignKeys) {
261
+ if (fk.onDelete === 'cascade') {
262
+ const refTable = this._db._tables[fk.table];
263
+ if (refTable) {
264
+ refTable.remove({ [fk.field]: row[this._primaryKey] });
255
265
  }
256
266
  }
257
267
  }
268
+
269
+ if (this._softDelete) {
270
+ this._rows[index]._deleted = true;
271
+ this._rows[index]._deletedAt = Date.now();
272
+ } else {
273
+ this._removeFromIndexes(index, row);
274
+ this._rows.splice(index, 1);
275
+ this._adjustIndexesAfterRemove(index);
276
+ }
277
+ count++;
258
278
  }
259
279
 
260
- if (toRemove.length > 0) {
280
+ if (count > 0) {
261
281
  this._dirty = true;
262
282
  this._db._markDirty();
263
-
264
- // 触发 afterRemove 钩子
265
283
  for (const hook of this._hooks.afterRemove) {
266
- hook(query, toRemove.length);
284
+ hook(query, count);
267
285
  }
286
+ this._db._emitChange('remove', this._name, { query, count });
268
287
  }
269
- return toRemove.length;
288
+ return count;
270
289
  }
271
290
 
272
291
  removeById(id) {
273
- if (!this._primaryKey) {
274
- throw new Error(`Table '${this._name}' has no primary key`);
275
- }
292
+ if (!this._primaryKey) throw new Error(`Table '${this._name}' has no primary key`);
276
293
  return this.remove({ [this._primaryKey]: id });
277
294
  }
278
295
 
296
+ /**
297
+ * 软删除 — 标记为已删除,可恢复
298
+ */
299
+ removeSoft(query = {}) {
300
+ this._softDelete = true;
301
+ const result = this.remove(query);
302
+ return result;
303
+ }
304
+
305
+ /**
306
+ * 恢复软删除的行
307
+ */
308
+ restore(query = {}) {
309
+ if (!this._softDelete) throw new Error('Soft delete is not enabled');
310
+ const matched = this._applyFilter(this._rows, { ...query, _deleted: true });
311
+ for (const row of matched) {
312
+ const index = this._rows.indexOf(row);
313
+ if (index !== -1) {
314
+ delete this._rows[index]._deleted;
315
+ delete this._rows[index]._deletedAt;
316
+ }
317
+ }
318
+ return matched.length;
319
+ }
320
+
321
+ /**
322
+ * 清空表(保留结构)
323
+ */
324
+ truncate() {
325
+ return this._truncate();
326
+ }
327
+
328
+ _truncate() {
329
+ const count = this._rows.length;
330
+ this._rows = [];
331
+ this._indexes = {};
332
+ this._autoIncrement = 0;
333
+ this._dirty = true;
334
+ this._db._markDirty();
335
+ return count;
336
+ }
337
+
338
+ // ============================================================
339
+ // ALTER TABLE
340
+ // ============================================================
341
+
342
+ addColumn(field, definition) {
343
+ if (this._schema[field]) throw new Error(`Column '${field}' already exists`);
344
+ this._schema[field] = definition;
345
+ if (definition.default !== undefined) {
346
+ this._defaults[field] = definition.default;
347
+ // 填充现有行的默认值
348
+ for (const row of this._rows) {
349
+ if (row[field] === undefined) {
350
+ row[field] = typeof definition.default === 'function' ? definition.default() : definition.default;
351
+ }
352
+ }
353
+ }
354
+ if (definition.check) this._checkConstraints.push({ field, fn: definition.check });
355
+ if (definition.foreignKey) {
356
+ this._foreignKeys.push({
357
+ field,
358
+ table: definition.foreignKey.table,
359
+ onDelete: definition.foreignKey.onDelete || 'restrict'
360
+ });
361
+ }
362
+ if (definition.computed) this._computedFields[field] = definition.computed;
363
+ this._dirty = true;
364
+ this._db._markDirty();
365
+ return this;
366
+ }
367
+
368
+ dropColumn(field) {
369
+ if (!this._schema[field]) throw new Error(`Column '${field}' does not exist`);
370
+ delete this._schema[field];
371
+ delete this._defaults[field];
372
+ this._checkConstraints = this._checkConstraints.filter(c => c.field !== field);
373
+ this._foreignKeys = this._foreignKeys.filter(fk => fk.field !== field);
374
+ delete this._computedFields[field];
375
+ // 移除所有行中的该字段
376
+ for (const row of this._rows) {
377
+ delete row[field];
378
+ }
379
+ this._dirty = true;
380
+ this._db._markDirty();
381
+ return this;
382
+ }
383
+
384
+ renameColumn(oldName, newName) {
385
+ if (!this._schema[oldName]) throw new Error(`Column '${oldName}' does not exist`);
386
+ if (this._schema[newName]) throw new Error(`Column '${newName}' already exists`);
387
+ this._schema[newName] = this._schema[oldName];
388
+ delete this._schema[oldName];
389
+ if (this._defaults[oldName] !== undefined) {
390
+ this._defaults[newName] = this._defaults[oldName];
391
+ delete this._defaults[oldName];
392
+ }
393
+ for (const row of this._rows) {
394
+ row[newName] = row[oldName];
395
+ delete row[oldName];
396
+ }
397
+ if (this._primaryKey === oldName) this._primaryKey = newName;
398
+ if (this._autoIncrementField === oldName) this._autoIncrementField = newName;
399
+ this._dirty = true;
400
+ this._db._markDirty();
401
+ return this;
402
+ }
403
+
404
+ modifyColumn(field, definition) {
405
+ if (!this._schema[field]) throw new Error(`Column '${field}' does not exist`);
406
+ this._schema[field] = { ...this._schema[field], ...definition };
407
+ if (definition.default !== undefined) this._defaults[field] = definition.default;
408
+ this._checkConstraints = this._checkConstraints.filter(c => c.field !== field);
409
+ if (definition.check) this._checkConstraints.push({ field, fn: definition.check });
410
+ this._dirty = true;
411
+ this._db._markDirty();
412
+ return this;
413
+ }
414
+
415
+ /**
416
+ * 获取表结构
417
+ */
418
+ describe() {
419
+ return {
420
+ name: this._name,
421
+ columns: { ...this._schema },
422
+ primaryKey: this._primaryKey,
423
+ rowCount: this._getVisibleRows().length,
424
+ indexCount: Object.keys(this._indexes).length,
425
+ softDelete: this._softDelete,
426
+ foreignKeys: this._foreignKeys.map(fk => ({ field: fk.field, table: fk.table }))
427
+ };
428
+ }
429
+
279
430
  // ============================================================
280
431
  // 索引
281
432
  // ============================================================
@@ -302,17 +453,6 @@ class Table {
302
453
  // 事件钩子
303
454
  // ============================================================
304
455
 
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
456
  on(event, callback) {
317
457
  if (this._hooks[event]) {
318
458
  this._hooks[event].push(callback);
@@ -328,7 +468,7 @@ class Table {
328
468
  }
329
469
 
330
470
  // ============================================================
331
- // 内部方法
471
+ // 内部: 过滤器
332
472
  // ============================================================
333
473
 
334
474
  _applyFilter(rows, query) {
@@ -343,20 +483,43 @@ class Table {
343
483
 
344
484
  _matchRow(row, query) {
345
485
  for (const [field, condition] of Object.entries(query)) {
346
- const value = row[field];
486
+ // 顶层 $or / $and / $not
487
+ if (field === '$or') {
488
+ if (!Array.isArray(condition) || !condition.some(sub => this._matchRow(row, sub))) return false;
489
+ continue;
490
+ }
491
+ if (field === '$and') {
492
+ if (!Array.isArray(condition) || !condition.every(sub => this._matchRow(row, sub))) return false;
493
+ continue;
494
+ }
495
+ if (field === '$not') {
496
+ if (this._matchRow(row, condition)) return false;
497
+ continue;
498
+ }
347
499
 
348
- // 操作符对象: { $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $like, $regex }
349
- if (condition !== null && typeof condition === 'object' && !Array.isArray(condition)) {
350
- for (const [op, target] of Object.entries(condition)) {
351
- if (!this._matchOperator(value, op, target)) {
500
+ // JSON 路径: 'profile.city' 'data.profile.city'
501
+ const value = this._resolvePath(row, field);
502
+
503
+ if (condition !== null && typeof condition === 'object' && !Array.isArray(condition) && !(condition instanceof RegExp)) {
504
+ // 检查是否是操作符
505
+ const keys = Object.keys(condition);
506
+ const isOperator = keys.every(k => k.startsWith('$'));
507
+ if (isOperator && keys.length > 0) {
508
+ for (const [op, target] of Object.entries(condition)) {
509
+ if (!this._matchOperator(value, op, target)) {
510
+ return false;
511
+ }
512
+ }
513
+ } else {
514
+ // 普通对象:递归匹配子文档
515
+ if (value !== null && typeof value === 'object') {
516
+ if (!this._matchRow(value, condition)) return false;
517
+ } else {
352
518
  return false;
353
519
  }
354
520
  }
355
521
  } else {
356
- // 简单等值匹配
357
- if (value !== condition) {
358
- return false;
359
- }
522
+ if (value !== condition) return false;
360
523
  }
361
524
  }
362
525
  return true;
@@ -374,8 +537,8 @@ class Table {
374
537
  case '$nin': return Array.isArray(target) && !target.includes(value);
375
538
  case '$like':
376
539
  if (typeof value !== 'string' || typeof target !== 'string') return false;
377
- const regex = new RegExp(target.replace(/%/g, '.*').replace(/_/g, '.'), 'i');
378
- return regex.test(value);
540
+ const likeRegex = target.replace(/%/g, '.*').replace(/_/g, '.');
541
+ return new RegExp('^' + likeRegex + '$', 'i').test(value);
379
542
  case '$regex':
380
543
  return new RegExp(target).test(String(value));
381
544
  case '$exists':
@@ -383,13 +546,82 @@ class Table {
383
546
  case '$between':
384
547
  return Array.isArray(target) && target.length === 2
385
548
  && value >= target[0] && value <= target[1];
549
+ case '$and':
550
+ return Array.isArray(target) && target.every(sub => this._matchRow({ _val: value }, { _val: sub }));
386
551
  case '$or':
387
- return Array.isArray(target) && target.some(sub => this._matchRow({ [op]: value }, { [op]: sub }));
552
+ return Array.isArray(target) && target.some(sub => this._matchRow({ _val: value }, { _val: sub }));
553
+ case '$not':
554
+ return !this._matchRow({ _val: value }, { _val: target });
555
+ case '$contains':
556
+ return Array.isArray(value) && value.includes(target);
557
+ case '$type':
558
+ return typeof value === target;
559
+ case '$size':
560
+ return Array.isArray(value) && value.length === target;
561
+ case '$elemMatch':
562
+ return Array.isArray(value) && value.some(el =>
563
+ typeof el === 'object' ? this._matchRow(el, target) : el === target
564
+ );
565
+ case '$isNull':
566
+ return target ? value === null || value === undefined : value !== null && value !== undefined;
567
+ case '$startsWith':
568
+ return typeof value === 'string' && value.startsWith(target);
569
+ case '$endsWith':
570
+ return typeof value === 'string' && value.endsWith(target);
571
+ case '$search':
572
+ // 全文搜索(简单实现)
573
+ if (typeof value !== 'string') return false;
574
+ const terms = Array.isArray(target) ? target : [target];
575
+ return terms.every(t => value.toLowerCase().includes(t.toLowerCase()));
388
576
  default:
389
577
  return false;
390
578
  }
391
579
  }
392
580
 
581
+ /**
582
+ * 解析 JSON 路径,如 'profile.city' -> row.profile.city
583
+ */
584
+ _resolvePath(row, field) {
585
+ if (field.includes('.') && !field.startsWith('$')) {
586
+ const parts = field.split('.');
587
+ let value = row;
588
+ for (const part of parts) {
589
+ if (value === null || value === undefined) return undefined;
590
+ value = value[part];
591
+ }
592
+ return value;
593
+ }
594
+ return row[field];
595
+ }
596
+
597
+ // ============================================================
598
+ // 内部: 辅助
599
+ // ============================================================
600
+
601
+ _applyDefaults(data) {
602
+ const result = { ...data };
603
+ for (const [field, defaultValue] of Object.entries(this._defaults)) {
604
+ if (result[field] === undefined) {
605
+ result[field] = typeof defaultValue === 'function' ? defaultValue() : defaultValue;
606
+ }
607
+ }
608
+ return result;
609
+ }
610
+
611
+ _applyComputed(data) {
612
+ for (const [field, fn] of Object.entries(this._computedFields)) {
613
+ data[field] = fn(data);
614
+ }
615
+ return data;
616
+ }
617
+
618
+ _getVisibleRows() {
619
+ if (this._softDelete) {
620
+ return this._rows.filter(r => !r._deleted);
621
+ }
622
+ return this._rows;
623
+ }
624
+
393
625
  _updateIndexes(rowIndex, data) {
394
626
  for (const field of Object.keys(this._indexes)) {
395
627
  const value = data[field];
@@ -416,9 +648,24 @@ class Table {
416
648
  }
417
649
  }
418
650
 
651
+ _adjustIndexesAfterRemove(removedIndex) {
652
+ for (const idxName of Object.keys(this._indexes)) {
653
+ const idx = this._indexes[idxName];
654
+ for (const [key, positions] of idx) {
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
+ }
664
+ }
665
+ }
666
+
419
667
  _loadRows(rows) {
420
668
  this._rows = rows;
421
- // 重建自增
422
669
  if (this._autoIncrementField) {
423
670
  for (const row of rows) {
424
671
  const val = row[this._autoIncrementField];
@@ -427,183 +674,14 @@ class Table {
427
674
  }
428
675
  }
429
676
  }
430
- // 重建所有索引
431
677
  for (const field of Object.keys(this._indexes)) {
432
678
  this.createIndex(field);
433
679
  }
434
680
  }
435
681
 
436
- // ============================================================
437
- // 导出
438
- // ============================================================
439
-
440
682
  toJSON() {
441
683
  return this._rows;
442
684
  }
443
685
  }
444
686
 
445
- // ============================================================
446
- // 链式查询构建器
447
- // ============================================================
448
-
449
- class Query {
450
- constructor(table, conditions) {
451
- this._table = table;
452
- this._conditions = conditions;
453
- this._orderBy = null;
454
- this._orderDir = 'asc';
455
- this._limitCount = null;
456
- this._offsetCount = 0;
457
- this._selectFields = null;
458
- this._groupBy = null;
459
- }
460
-
461
- orderBy(field, dir = 'asc') {
462
- this._orderBy = field;
463
- this._orderDir = dir;
464
- return this;
465
- }
466
-
467
- limit(n) {
468
- this._limitCount = n;
469
- return this;
470
- }
471
-
472
- offset(n) {
473
- this._offsetCount = n;
474
- return this;
475
- }
476
-
477
- select(fields) {
478
- this._selectFields = Array.isArray(fields) ? fields : [fields];
479
- return this;
480
- }
481
-
482
- groupBy(field) {
483
- this._groupBy = field;
484
- return this;
485
- }
486
-
487
- get() {
488
- let rows = this._table._applyFilter(this._table._rows, this._conditions);
489
-
490
- // 排序
491
- if (this._orderBy) {
492
- rows = rows.sort((a, b) => {
493
- const va = a[this._orderBy];
494
- const vb = b[this._orderBy];
495
- if (va < vb) return this._orderDir === 'asc' ? -1 : 1;
496
- if (va > vb) return this._orderDir === 'asc' ? 1 : -1;
497
- return 0;
498
- });
499
- }
500
-
501
- // 分页
502
- if (this._offsetCount > 0) {
503
- rows = rows.slice(this._offsetCount);
504
- }
505
- if (this._limitCount !== null) {
506
- rows = rows.slice(0, this._limitCount);
507
- }
508
-
509
- // 字段选择
510
- rows = rows.map(r => ({ ...r }));
511
- if (this._selectFields) {
512
- rows = rows.map(r => {
513
- const obj = {};
514
- for (const f of this._selectFields) {
515
- obj[f] = r[f];
516
- }
517
- return obj;
518
- });
519
- }
520
-
521
- return rows;
522
- }
523
-
524
- first() {
525
- const rows = this.limit(1).get();
526
- return rows.length > 0 ? rows[0] : null;
527
- }
528
-
529
- count() {
530
- const rows = this._table._applyFilter(this._table._rows, this._conditions);
531
- return rows.length;
532
- }
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
-
600
- update(updates) {
601
- return this._table.update(this._conditions, updates);
602
- }
603
-
604
- remove() {
605
- return this._table.remove(this._conditions);
606
- }
607
- }
608
-
609
687
  module.exports = Table;