jsql-neo 1.2.0 → 1.3.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/index.js CHANGED
@@ -1,22 +1,11 @@
1
1
  // © Vexify 2026 All Rights Reserved.
2
2
  /**
3
- * JSQL — Pure JavaScript Embedded Database
3
+ * JSQL-NEO — Pure JavaScript Embedded Database
4
+ * v1.3.0 — 窗口函数、视图、触发器、全文搜索、CSV、stats、多数据库
4
5
  *
5
6
  * @example
6
7
  * const jsql = require('jsql-neo');
7
8
  * const db = new jsql.Database('mydb.json');
8
- *
9
- * db.createTable('users', {
10
- * id: { type: 'integer', primaryKey: true, autoIncrement: true },
11
- * name: { type: 'string', required: true },
12
- * age: { type: 'integer' }
13
- * });
14
- *
15
- * db.users.insert({ name: 'Alice', age: 25 });
16
- * db.users.insert({ name: 'Bob', age: 30 });
17
- *
18
- * const adults = db.users.find({ age: { $gte: 18 } });
19
- * const sorted = db.users.where({ age: { $gte: 18 } }).orderBy('age', 'desc').limit(10).get();
20
9
  */
21
10
 
22
11
  module.exports = {
package/lib/database.js CHANGED
@@ -39,6 +39,9 @@ class Database {
39
39
  this._plugins = [];
40
40
  this._migrations = [];
41
41
  this._observers = []; // 响应式观察者
42
+ this._views = {}; // 视图 { name: { query, table } }
43
+ this._triggers = {}; // 触发器 { name: { event, table, callback } }
44
+ this._attached = {}; // 附加数据库 { name: db }
42
45
  this._changeStream = null; // 变更流
43
46
 
44
47
  // 从文件加载
@@ -90,6 +93,310 @@ class Database {
90
93
  return Object.keys(this._tables);
91
94
  }
92
95
 
96
+ // ============================================================
97
+ // 视图
98
+ // ============================================================
99
+
100
+ /**
101
+ * 创建视图(虚拟表)
102
+ * @param {string} name - 视图名
103
+ * @param {function} queryFn - (db) => Query 实例
104
+ * @example
105
+ * db.createView('adults', db => db.users.where({ age: { $gte: 18 } }).select(['name', 'age']));
106
+ */
107
+ createView(name, queryFn) {
108
+ if (this._views[name]) throw new Error(`View '${name}' already exists`);
109
+ this._views[name] = { queryFn };
110
+
111
+ // 代理到 db 上
112
+ Object.defineProperty(this, name, {
113
+ get: () => {
114
+ const q = queryFn(this);
115
+ return {
116
+ get: () => q.get(),
117
+ first: () => q.first(),
118
+ count: () => q.count(),
119
+ sum: (f) => q.sum(f),
120
+ avg: (f) => q.avg(f),
121
+ min: (f) => q.min(f),
122
+ max: (f) => q.max(f),
123
+ _query: q
124
+ };
125
+ },
126
+ enumerable: true,
127
+ configurable: true
128
+ });
129
+ }
130
+
131
+ dropView(name) {
132
+ if (!this._views[name]) throw new Error(`View '${name}' does not exist`);
133
+ delete this._views[name];
134
+ delete this[name];
135
+ }
136
+
137
+ getViews() {
138
+ return Object.keys(this._views);
139
+ }
140
+
141
+ // ============================================================
142
+ // 触发器
143
+ // ============================================================
144
+
145
+ /**
146
+ * 创建触发器
147
+ * @param {string} name - 触发器名
148
+ * @param {object} options - { event: 'insert'|'update'|'remove', table: 'tableName', timing: 'before'|'after' }
149
+ * @param {function} callback - 触发时执行的回调
150
+ */
151
+ createTrigger(name, options, callback) {
152
+ if (this._triggers[name]) throw new Error(`Trigger '${name}' already exists`);
153
+ const table = this._tables[options.table];
154
+ if (!table) throw new Error(`Table '${options.table}' does not exist`);
155
+
156
+ const hookName = options.timing + options.event.charAt(0).toUpperCase() + options.event.slice(1);
157
+ this._triggers[name] = { event: options.event, table: options.table, callback, hookName };
158
+ table.on(hookName, callback);
159
+ }
160
+
161
+ dropTrigger(name) {
162
+ const trigger = this._triggers[name];
163
+ if (!trigger) throw new Error(`Trigger '${name}' does not exist`);
164
+ const table = this._tables[trigger.table];
165
+ if (table) table.off(trigger.hookName, trigger.callback);
166
+ delete this._triggers[name];
167
+ }
168
+
169
+ getTriggers() {
170
+ return Object.keys(this._triggers).map(name => ({
171
+ name,
172
+ event: this._triggers[name].event,
173
+ table: this._triggers[name].table
174
+ }));
175
+ }
176
+
177
+ // ============================================================
178
+ // CSV 导入/导出
179
+ // ============================================================
180
+
181
+ /**
182
+ * 从 CSV 文件导入数据到表
183
+ * @param {string} filePath - CSV 文件路径
184
+ * @param {string} tableName - 目标表名
185
+ * @param {object} options - { delimiter: ',', hasHeader: true, skipRows: 0, mapping: { csvCol: schemaCol } }
186
+ */
187
+ importCSV(filePath, tableName, options = {}) {
188
+ const table = this._tables[tableName];
189
+ if (!table) throw new Error(`Table '${tableName}' does not exist`);
190
+
191
+ const delimiter = options.delimiter || ',';
192
+ const hasHeader = options.hasHeader !== false;
193
+ const skipRows = options.skipRows || 0;
194
+ const mapping = options.mapping || null;
195
+
196
+ const content = fs.readFileSync(filePath, 'utf8');
197
+ const lines = content.split(/\r?\n/).filter(l => l.trim());
198
+
199
+ let headers = [];
200
+ let startRow = 0;
201
+
202
+ if (hasHeader) {
203
+ headers = this._parseCSVLine(lines[0], delimiter);
204
+ startRow = 1 + skipRows;
205
+ } else {
206
+ startRow = skipRows;
207
+ }
208
+
209
+ let count = 0;
210
+ for (let i = startRow; i < lines.length; i++) {
211
+ const values = this._parseCSVLine(lines[i], delimiter);
212
+ if (values.length === 0) continue;
213
+
214
+ const data = {};
215
+ if (mapping) {
216
+ for (const [csvCol, schemaCol] of Object.entries(mapping)) {
217
+ const idx = hasHeader ? headers.indexOf(csvCol) : parseInt(csvCol);
218
+ if (idx >= 0 && idx < values.length) {
219
+ data[schemaCol] = this._castValue(values[idx], table._schema[schemaCol]);
220
+ }
221
+ }
222
+ } else if (hasHeader) {
223
+ for (let j = 0; j < headers.length; j++) {
224
+ if (j < values.length && table._schema[headers[j]]) {
225
+ data[headers[j]] = this._castValue(values[j], table._schema[headers[j]]);
226
+ }
227
+ }
228
+ }
229
+
230
+ if (Object.keys(data).length > 0) {
231
+ table.insert(data);
232
+ count++;
233
+ }
234
+ }
235
+
236
+ return count;
237
+ }
238
+
239
+ /**
240
+ * 导出表数据到 CSV 文件
241
+ * @param {string} filePath - 输出文件路径
242
+ * @param {string} tableName - 表名
243
+ * @param {object} query - 筛选条件(可选)
244
+ * @param {object} options - { delimiter: ',', fields: ['col1','col2'] }
245
+ */
246
+ exportCSV(filePath, tableName, query = {}, options = {}) {
247
+ const table = this._tables[tableName];
248
+ if (!table) throw new Error(`Table '${tableName}' does not exist`);
249
+
250
+ const delimiter = options.delimiter || ',';
251
+ const rows = table._applyFilter(table._rows, query);
252
+ const fields = options.fields || Object.keys(table._schema).filter(f => f !== '_softDelete');
253
+
254
+ const lines = [];
255
+ // 表头
256
+ lines.push(fields.map(f => this._csvEscape(f, delimiter)).join(delimiter));
257
+
258
+ // 数据行
259
+ for (const row of rows) {
260
+ lines.push(fields.map(f => {
261
+ const val = row[f];
262
+ return val === null || val === undefined ? '' : this._csvEscape(String(val), delimiter);
263
+ }).join(delimiter));
264
+ }
265
+
266
+ const dir = path.dirname(filePath);
267
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
268
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
269
+ return lines.length - 1; // 返回数据行数
270
+ }
271
+
272
+ _parseCSVLine(line, delimiter) {
273
+ const result = [];
274
+ let current = '';
275
+ let inQuotes = false;
276
+
277
+ for (let i = 0; i < line.length; i++) {
278
+ const ch = line[i];
279
+ if (inQuotes) {
280
+ if (ch === '"') {
281
+ if (i + 1 < line.length && line[i + 1] === '"') {
282
+ current += '"';
283
+ i++;
284
+ } else {
285
+ inQuotes = false;
286
+ }
287
+ } else {
288
+ current += ch;
289
+ }
290
+ } else {
291
+ if (ch === '"') {
292
+ inQuotes = true;
293
+ } else if (ch === delimiter) {
294
+ result.push(current.trim());
295
+ current = '';
296
+ } else {
297
+ current += ch;
298
+ }
299
+ }
300
+ }
301
+ result.push(current.trim());
302
+ return result;
303
+ }
304
+
305
+ _csvEscape(value, delimiter) {
306
+ if (value.includes(delimiter) || value.includes('"') || value.includes('\n')) {
307
+ return '"' + value.replace(/"/g, '""') + '"';
308
+ }
309
+ return value;
310
+ }
311
+
312
+ _castValue(value, schemaDef) {
313
+ if (!schemaDef || !value) return value;
314
+ switch (schemaDef.type) {
315
+ case 'integer': return parseInt(value, 10) || 0;
316
+ case 'number': return parseFloat(value) || 0;
317
+ case 'boolean': return value.toLowerCase() === 'true' || value === '1';
318
+ case 'array': try { return JSON.parse(value); } catch(e) { return [value]; }
319
+ case 'object': try { return JSON.parse(value); } catch(e) { return {}; }
320
+ default: return value;
321
+ }
322
+ }
323
+
324
+ // ============================================================
325
+ // 数据库统计
326
+ // ============================================================
327
+
328
+ /**
329
+ * 获取数据库统计信息
330
+ */
331
+ stats() {
332
+ const tableStats = {};
333
+ let totalRows = 0;
334
+ let totalIndexes = 0;
335
+
336
+ for (const [name, table] of Object.entries(this._tables)) {
337
+ if (name === '__meta__') continue;
338
+ const rowCount = table._rows.length;
339
+ const indexCount = Object.keys(table._indexes).length;
340
+ const sizeEstimate = JSON.stringify(table._rows).length;
341
+ tableStats[name] = {
342
+ rowCount,
343
+ indexCount,
344
+ sizeEstimate,
345
+ primaryKey: table._primaryKey,
346
+ columns: Object.keys(table._schema).length,
347
+ softDelete: table._softDelete,
348
+ foreignKeys: table._foreignKeys.length,
349
+ hooks: Object.values(table._hooks).reduce((s, a) => s + a.length, 0)
350
+ };
351
+ totalRows += rowCount;
352
+ totalIndexes += indexCount;
353
+ }
354
+
355
+ return {
356
+ tables: Object.keys(this._tables).filter(t => t !== '__meta__').length,
357
+ views: Object.keys(this._views).length,
358
+ triggers: Object.keys(this._triggers).length,
359
+ migrations: this._migrations.length,
360
+ plugins: this._plugins.length,
361
+ totalRows,
362
+ totalIndexes,
363
+ memoryMode: this._memoryMode,
364
+ versioning: this._versioning,
365
+ encrypted: !!this._encryptKey,
366
+ tableStats
367
+ };
368
+ }
369
+
370
+ // ============================================================
371
+ // 多数据库
372
+ // ============================================================
373
+
374
+ /**
375
+ * 附加另一个数据库(跨库查询)
376
+ * @param {string} name - 别名
377
+ * @param {Database} db - 另一个 Database 实例
378
+ */
379
+ attach(name, db) {
380
+ if (this._attached[name]) throw new Error(`Database '${name}' already attached`);
381
+ this._attached[name] = db;
382
+
383
+ Object.defineProperty(this, name, {
384
+ get: () => db,
385
+ enumerable: true,
386
+ configurable: true
387
+ });
388
+ }
389
+
390
+ detach(name) {
391
+ if (!this._attached[name]) throw new Error(`Database '${name}' is not attached`);
392
+ delete this._attached[name];
393
+ delete this[name];
394
+ }
395
+
396
+ getAttached() {
397
+ return Object.keys(this._attached);
398
+ }
399
+
93
400
  // ============================================================
94
401
  // 事务
95
402
  // ============================================================
package/lib/query.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // © Vexify 2026 All Rights Reserved.
2
2
  /**
3
3
  * JSQL Query — 链式查询构建器
4
- * 支持 JOIN、DISTINCT、HAVING、UNION、多字段排序、高级操作符、分页、EXPLAIN
4
+ * 支持 JOIN、DISTINCT、HAVING、UNION、窗口函数、缓存、CASE WHEN、toSQL
5
5
  */
6
6
 
7
7
  class Query {
@@ -19,6 +19,9 @@ class Query {
19
19
  this._joins = [];
20
20
  this._union = null;
21
21
  this._explain = false;
22
+ this._window = null;
23
+ this._cache = null; // { ttl, result, timestamp }
24
+ this._caseExpr = null; // [{ when, then }, ...] + else
22
25
  }
23
26
 
24
27
  // ============================================================
@@ -57,24 +60,12 @@ class Query {
57
60
  return this;
58
61
  }
59
62
 
60
- /**
61
- * 便捷分页
62
- * @param {number} page - 页码(从 1 开始)
63
- * @param {number} perPage - 每页数量
64
- * @returns {{ rows, total, page, perPage, totalPages }}
65
- */
66
63
  paginate(page = 1, perPage = 20) {
67
64
  this._limitCount = perPage;
68
65
  this._offsetCount = (page - 1) * perPage;
69
66
  const total = this._countRaw();
70
67
  const rows = this.get();
71
- return {
72
- rows,
73
- total,
74
- page,
75
- perPage,
76
- totalPages: Math.ceil(total / perPage)
77
- };
68
+ return { rows, total, page, perPage, totalPages: Math.ceil(total / perPage) };
78
69
  }
79
70
 
80
71
  // ============================================================
@@ -92,6 +83,31 @@ class Query {
92
83
  return this;
93
84
  }
94
85
 
86
+ // ============================================================
87
+ // CASE WHEN
88
+ // ============================================================
89
+
90
+ /**
91
+ * CASE WHEN 表达式
92
+ * @example
93
+ * db.users.where({}).case([
94
+ * { when: { age: { $lt: 18 } }, then: 'minor' },
95
+ * { when: { age: { $lt: 60 } }, then: 'adult' }
96
+ * ], 'senior').as('ageGroup').get()
97
+ */
98
+ case(whenClauses, elseExpr) {
99
+ this._caseExpr = { clauses: whenClauses, else: elseExpr };
100
+ return this;
101
+ }
102
+
103
+ /**
104
+ * 给 CASE WHEN 结果命名
105
+ */
106
+ as(fieldName) {
107
+ this._caseAs = fieldName;
108
+ return this;
109
+ }
110
+
95
111
  // ============================================================
96
112
  // 分组 & 聚合后过滤
97
113
  // ============================================================
@@ -107,16 +123,25 @@ class Query {
107
123
  }
108
124
 
109
125
  // ============================================================
110
- // JOIN
126
+ // 窗口函数
111
127
  // ============================================================
112
128
 
113
129
  /**
114
- * INNER JOIN
115
- * @param {Table} otherTable - JOIN 的表
116
- * @param {string} localField - 本地字段
117
- * @param {string} foreignField - 对方字段
118
- * @param {string} as - 别名
130
+ * 窗口函数
131
+ * @param {string} fn - 'rowNumber' | 'rank' | 'denseRank'
132
+ * @param {string} field - 排序字段
133
+ * @param {string} partitionBy - 分区字段
134
+ * @param {string} dir - 'asc' | 'desc'
119
135
  */
136
+ window(fn, field, partitionBy, dir = 'asc') {
137
+ this._window = { fn, field, partitionBy, dir };
138
+ return this;
139
+ }
140
+
141
+ // ============================================================
142
+ // JOIN
143
+ // ============================================================
144
+
120
145
  join(otherTable, localField, foreignField, as) {
121
146
  this._joins.push({ type: 'inner', table: otherTable, localField, foreignField, as });
122
147
  return this;
@@ -150,6 +175,19 @@ class Query {
150
175
  return this;
151
176
  }
152
177
 
178
+ // ============================================================
179
+ // 缓存
180
+ // ============================================================
181
+
182
+ /**
183
+ * 缓存查询结果
184
+ * @param {number} ttlMs - 缓存有效期(毫秒),默认 60000
185
+ */
186
+ cache(ttlMs = 60000) {
187
+ this._cache = { ttl: ttlMs, result: null, timestamp: 0 };
188
+ return this;
189
+ }
190
+
153
191
  // ============================================================
154
192
  // 执行
155
193
  // ============================================================
@@ -157,6 +195,11 @@ class Query {
157
195
  get() {
158
196
  if (this._explain) return this._getExplain();
159
197
 
198
+ // 缓存命中
199
+ if (this._cache && this._cache.result && (Date.now() - this._cache.timestamp < this._cache.ttl)) {
200
+ return this._cache.result;
201
+ }
202
+
160
203
  let rows = this._table._applyFilter(this._table._rows, this._conditions);
161
204
 
162
205
  // JOIN
@@ -180,12 +223,17 @@ class Query {
180
223
  rows = this._table._applyFilter(rows, this._having);
181
224
  }
182
225
 
226
+ // 窗口函数
227
+ if (this._window) {
228
+ rows = this._applyWindow(rows);
229
+ }
230
+
183
231
  // DISTINCT
184
232
  if (this._distinct && this._distinctField) {
185
233
  const seen = new Set();
186
234
  rows = rows.filter(r => {
187
235
  const val = r[this._distinctField];
188
- if (val === undefined) return false; // 跳过 undefined
236
+ if (val === undefined) return false;
189
237
  const key = String(val);
190
238
  if (seen.has(key)) return false;
191
239
  seen.add(key);
@@ -206,6 +254,23 @@ class Query {
206
254
  rows = rows.slice(0, this._limitCount);
207
255
  }
208
256
 
257
+ // CASE WHEN
258
+ if (this._caseExpr) {
259
+ rows = rows.map(r => {
260
+ const row = { ...r };
261
+ let val = this._caseExpr.else;
262
+ for (const clause of this._caseExpr.clauses) {
263
+ if (this._table._matchRow(row, clause.when)) {
264
+ val = clause.then;
265
+ break;
266
+ }
267
+ }
268
+ const asField = this._caseAs || '_case';
269
+ row[asField] = val;
270
+ return row;
271
+ });
272
+ }
273
+
209
274
  // 字段选择
210
275
  rows = rows.map(r => ({ ...r }));
211
276
  if (this._selectFields) {
@@ -218,6 +283,12 @@ class Query {
218
283
  });
219
284
  }
220
285
 
286
+ // 缓存存储
287
+ if (this._cache) {
288
+ this._cache.result = rows;
289
+ this._cache.timestamp = Date.now();
290
+ }
291
+
221
292
  return rows;
222
293
  }
223
294
 
@@ -260,7 +331,7 @@ class Query {
260
331
  }
261
332
 
262
333
  groupStats(field) {
263
- if (!this._groupBy) throw new Error('groupStats() requires groupBy() to be called first');
334
+ if (!this._groupBy) throw new Error('groupStats() requires groupBy()');
264
335
  const rows = this._table._applyFilter(this._table._rows, this._conditions);
265
336
  const groups = {};
266
337
 
@@ -283,7 +354,6 @@ class Query {
283
354
  g._avg = g._count > 0 ? g._sum / g._count : 0;
284
355
  delete g._rows;
285
356
  });
286
-
287
357
  return result;
288
358
  }
289
359
 
@@ -295,6 +365,57 @@ class Query {
295
365
  return this._table.remove(this._conditions);
296
366
  }
297
367
 
368
+ /**
369
+ * 无效缓存
370
+ */
371
+ invalidateCache() {
372
+ if (this._cache) {
373
+ this._cache.result = null;
374
+ this._cache.timestamp = 0;
375
+ }
376
+ return this;
377
+ }
378
+
379
+ // ============================================================
380
+ // toSQL
381
+ // ============================================================
382
+
383
+ /**
384
+ * 导出为类 SQL 字符串(调试用)
385
+ */
386
+ toSQL() {
387
+ const parts = ['SELECT'];
388
+ if (this._selectFields) {
389
+ parts.push(this._selectFields.join(', '));
390
+ } else {
391
+ parts.push('*');
392
+ }
393
+ parts.push('FROM', this._table._name);
394
+
395
+ if (Object.keys(this._conditions).length > 0) {
396
+ parts.push('WHERE', JSON.stringify(this._conditions));
397
+ }
398
+
399
+ if (this._groupBy) {
400
+ parts.push('GROUP BY', this._groupBy);
401
+ if (this._having) parts.push('HAVING', JSON.stringify(this._having));
402
+ }
403
+
404
+ if (this._orderBy.length > 0) {
405
+ parts.push('ORDER BY', this._orderBy.map(o => `${o.field} ${o.dir}`).join(', '));
406
+ }
407
+
408
+ if (this._limitCount !== null) {
409
+ parts.push('LIMIT', String(this._limitCount));
410
+ }
411
+
412
+ if (this._offsetCount > 0) {
413
+ parts.push('OFFSET', String(this._offsetCount));
414
+ }
415
+
416
+ return parts.join(' ');
417
+ }
418
+
298
419
  // ============================================================
299
420
  // 内部
300
421
  // ============================================================
@@ -319,6 +440,68 @@ class Query {
319
440
  });
320
441
  }
321
442
 
443
+ _applyWindow(rows) {
444
+ const { fn, field, partitionBy, dir } = this._window;
445
+
446
+ if (partitionBy) {
447
+ // 分组
448
+ const groups = {};
449
+ for (const row of rows) {
450
+ const key = String(row[partitionBy] ?? '__null__');
451
+ if (!groups[key]) groups[key] = [];
452
+ groups[key].push(row);
453
+ }
454
+
455
+ const result = [];
456
+ for (const group of Object.values(groups)) {
457
+ this._applyWindowToGroup(group, fn, field, dir);
458
+ result.push(...group);
459
+ }
460
+ return result;
461
+ }
462
+
463
+ this._applyWindowToGroup(rows, fn, field, dir);
464
+ return rows;
465
+ }
466
+
467
+ _applyWindowToGroup(rows, fn, field, dir) {
468
+ // 按字段排序
469
+ if (field) {
470
+ rows.sort((a, b) => {
471
+ const va = a[field];
472
+ const vb = b[field];
473
+ if (va < vb) return dir === 'asc' ? -1 : 1;
474
+ if (va > vb) return dir === 'asc' ? 1 : -1;
475
+ return 0;
476
+ });
477
+ }
478
+
479
+ const colName = fn === 'rowNumber' ? '_row_number' : fn === 'rank' ? '_rank' : '_dense_rank';
480
+ let rank = 1;
481
+ let denseRank = 1;
482
+ let prevVal = null;
483
+
484
+ for (let i = 0; i < rows.length; i++) {
485
+ const curVal = field ? rows[i][field] : null;
486
+
487
+ if (fn === 'rowNumber') {
488
+ rows[i][colName] = i + 1;
489
+ } else if (fn === 'rank') {
490
+ if (i > 0 && curVal !== prevVal) {
491
+ rank = i + 1;
492
+ }
493
+ rows[i][colName] = rank;
494
+ } else if (fn === 'denseRank') {
495
+ if (i > 0 && curVal !== prevVal) {
496
+ denseRank++;
497
+ }
498
+ rows[i][colName] = denseRank;
499
+ }
500
+
501
+ prevVal = curVal;
502
+ }
503
+ }
504
+
322
505
  _applyJoin(localRows, join) {
323
506
  const result = [];
324
507
  const foreignRows = join.table._rows;
@@ -346,7 +529,6 @@ class Query {
346
529
  }
347
530
  }
348
531
  } else {
349
- // inner join
350
532
  for (const lr of localRows) {
351
533
  for (const fr of foreignRows) {
352
534
  if (fr[join.foreignField] === lr[join.localField]) {
@@ -367,10 +549,9 @@ class Query {
367
549
  return merged;
368
550
  }
369
551
 
370
- _nullRow(table, isLocal) {
552
+ _nullRow(table) {
371
553
  const nulls = {};
372
- const schema = table._schema;
373
- for (const field of Object.keys(schema)) {
554
+ for (const field of Object.keys(table._schema)) {
374
555
  nulls[field] = null;
375
556
  }
376
557
  return nulls;
@@ -389,7 +570,6 @@ class Query {
389
570
 
390
571
  const result = [];
391
572
  for (const g of Object.values(groups)) {
392
- // 使用第一行作为基础,并添加 _group 和 _count
393
573
  const base = { ...g._rows[0] };
394
574
  base._group = g._group;
395
575
  base._count = g._count;
@@ -412,13 +592,13 @@ class Query {
412
592
  joins: this._joins.length,
413
593
  groupBy: this._groupBy,
414
594
  having: this._having,
595
+ window: this._window ? this._window.fn : null,
415
596
  orderBy: this._orderBy.map(o => `${o.field} ${o.dir}`),
416
597
  limit: this._limitCount,
417
598
  offset: this._offsetCount,
418
599
  estimatedCost: filtered.length * (1 + this._joins.length)
419
600
  };
420
601
 
421
- // 检查索引命中
422
602
  for (const [field] of Object.entries(this._conditions)) {
423
603
  if (this._table._indexes[field]) {
424
604
  plan.hasIndex = true;
package/lib/table.js CHANGED
@@ -118,6 +118,8 @@ class Table {
118
118
  hook(data);
119
119
  }
120
120
 
121
+ this._db._emitChange('insert', this._name, data);
122
+
121
123
  return data;
122
124
  }
123
125
 
@@ -224,6 +226,7 @@ class Table {
224
226
  for (const hook of this._hooks.afterUpdate) {
225
227
  hook(query, updates, count);
226
228
  }
229
+ this._db._emitChange('update', this._name, { query, updates, count });
227
230
  }
228
231
  return count;
229
232
  }
@@ -280,6 +283,7 @@ class Table {
280
283
  for (const hook of this._hooks.afterRemove) {
281
284
  hook(query, count);
282
285
  }
286
+ this._db._emitChange('remove', this._name, { query, count });
283
287
  }
284
288
  return count;
285
289
  }
@@ -338,7 +342,15 @@ class Table {
338
342
  addColumn(field, definition) {
339
343
  if (this._schema[field]) throw new Error(`Column '${field}' already exists`);
340
344
  this._schema[field] = definition;
341
- if (definition.default !== undefined) this._defaults[field] = definition.default;
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
+ }
342
354
  if (definition.check) this._checkConstraints.push({ field, fn: definition.check });
343
355
  if (definition.foreignKey) {
344
356
  this._foreignKeys.push({
@@ -415,6 +427,59 @@ class Table {
415
427
  };
416
428
  }
417
429
 
430
+ // ============================================================
431
+ // SELECT INTO
432
+ // ============================================================
433
+
434
+ /**
435
+ * 从查询结果创建新表
436
+ */
437
+ selectInto(newTableName, newSchema, query = {}) {
438
+ const rows = this._applyFilter(this._getVisibleRows(), query);
439
+ this._db.createTable(newTableName, newSchema);
440
+ const newTable = this._db._tables[newTableName];
441
+ for (const row of rows) {
442
+ newTable.insert(row);
443
+ }
444
+ return newTable;
445
+ }
446
+
447
+ // ============================================================
448
+ // 全文搜索评分
449
+ // ============================================================
450
+
451
+ /**
452
+ * 全文搜索并返回带相关性评分的排序结果
453
+ * @param {string} field - 搜索字段
454
+ * @param {string|string[]} terms - 搜索词
455
+ * @returns {Array} 按 _score 降序排列的结果
456
+ */
457
+ textSearch(field, terms) {
458
+ const searchTerms = Array.isArray(terms) ? terms : terms.split(/\s+/);
459
+ const rows = this._getVisibleRows();
460
+ const scored = [];
461
+
462
+ for (const row of rows) {
463
+ const value = String(row[field] || '').toLowerCase();
464
+ let score = 0;
465
+ for (const term of searchTerms) {
466
+ const t = term.toLowerCase();
467
+ const idx = value.indexOf(t);
468
+ if (idx !== -1) {
469
+ score += 10; // 基础命中
470
+ if (idx === 0) score += 5; // 开头匹配加分
471
+ if (value === t) score += 20; // 完全匹配加更多
472
+ score += (value.length - idx) / value.length * 5; // 靠前加权
473
+ }
474
+ }
475
+ if (score > 0) {
476
+ scored.push({ ...row, _score: score });
477
+ }
478
+ }
479
+
480
+ return scored.sort((a, b) => b._score - a._score);
481
+ }
482
+
418
483
  // ============================================================
419
484
  // 索引
420
485
  // ============================================================
@@ -471,6 +536,20 @@ class Table {
471
536
 
472
537
  _matchRow(row, query) {
473
538
  for (const [field, condition] of Object.entries(query)) {
539
+ // 顶层 $or / $and / $not
540
+ if (field === '$or') {
541
+ if (!Array.isArray(condition) || !condition.some(sub => this._matchRow(row, sub))) return false;
542
+ continue;
543
+ }
544
+ if (field === '$and') {
545
+ if (!Array.isArray(condition) || !condition.every(sub => this._matchRow(row, sub))) return false;
546
+ continue;
547
+ }
548
+ if (field === '$not') {
549
+ if (this._matchRow(row, condition)) return false;
550
+ continue;
551
+ }
552
+
474
553
  // JSON 路径: 'profile.city' 或 'data.profile.city'
475
554
  const value = this._resolvePath(row, field);
476
555
 
@@ -511,7 +590,8 @@ class Table {
511
590
  case '$nin': return Array.isArray(target) && !target.includes(value);
512
591
  case '$like':
513
592
  if (typeof value !== 'string' || typeof target !== 'string') return false;
514
- return new RegExp(target.replace(/%/g, '.*').replace(/_/g, '.'), 'i').test(value);
593
+ const likeRegex = target.replace(/%/g, '.*').replace(/_/g, '.');
594
+ return new RegExp('^' + likeRegex + '$', 'i').test(value);
515
595
  case '$regex':
516
596
  return new RegExp(target).test(String(value));
517
597
  case '$exists':
@@ -546,6 +626,25 @@ class Table {
546
626
  if (typeof value !== 'string') return false;
547
627
  const terms = Array.isArray(target) ? target : [target];
548
628
  return terms.every(t => value.toLowerCase().includes(t.toLowerCase()));
629
+ case '$textSearch':
630
+ // 高级全文搜索,带相关性评分(返回 true 即可,评分在 _textScore 中)
631
+ if (typeof value !== 'string') return false;
632
+ const searchTerms = Array.isArray(target) ? target : target.split(/\s+/);
633
+ const lower = value.toLowerCase();
634
+ return searchTerms.some(t => lower.includes(t.toLowerCase()));
635
+ case '$inSub':
636
+ // 子查询:$inSub: { table: otherTable, field: 'col', query: {} }
637
+ if (!target || !target.table) return false;
638
+ const subRows = target.table._applyFilter(target.table._rows, target.query || {});
639
+ return subRows.some(r => r[target.field] === value);
640
+ case '$notInSub':
641
+ if (!target || !target.table) return false;
642
+ const subRows2 = target.table._applyFilter(target.table._rows, target.query || {});
643
+ return !subRows2.some(r => r[target.field] === value);
644
+ case '$expr':
645
+ // 表达式:$expr: (value, row) => boolean
646
+ if (typeof target === 'function') return target(value);
647
+ return false;
549
648
  default:
550
649
  return false;
551
650
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "1.2.0",
3
+ "version": "1.3.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": [