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 +2 -13
- package/lib/database.js +307 -0
- package/lib/query.js +209 -29
- package/lib/table.js +101 -2
- package/package.json +1 -1
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
|
|
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
|
-
//
|
|
126
|
+
// 窗口函数
|
|
111
127
|
// ============================================================
|
|
112
128
|
|
|
113
129
|
/**
|
|
114
|
-
*
|
|
115
|
-
* @param {
|
|
116
|
-
* @param {string}
|
|
117
|
-
* @param {string}
|
|
118
|
-
* @param {string}
|
|
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;
|
|
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()
|
|
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
|
|
552
|
+
_nullRow(table) {
|
|
371
553
|
const nulls = {};
|
|
372
|
-
const
|
|
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)
|
|
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
|
-
|
|
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
|
}
|