jsql-neo 1.1.0 → 1.2.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 -1
- package/lib/database.js +258 -98
- package/lib/query.js +435 -0
- package/lib/table.js +350 -299
- package/package.json +1 -1
package/lib/query.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// © Vexify 2026 All Rights Reserved.
|
|
2
|
+
/**
|
|
3
|
+
* JSQL Query — 链式查询构建器
|
|
4
|
+
* 支持 JOIN、DISTINCT、HAVING、UNION、多字段排序、高级操作符、分页、EXPLAIN
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class Query {
|
|
8
|
+
constructor(table, conditions = {}) {
|
|
9
|
+
this._table = table;
|
|
10
|
+
this._conditions = conditions;
|
|
11
|
+
this._orderBy = [];
|
|
12
|
+
this._limitCount = null;
|
|
13
|
+
this._offsetCount = 0;
|
|
14
|
+
this._selectFields = null;
|
|
15
|
+
this._groupBy = null;
|
|
16
|
+
this._having = null;
|
|
17
|
+
this._distinct = false;
|
|
18
|
+
this._distinctField = null;
|
|
19
|
+
this._joins = [];
|
|
20
|
+
this._union = null;
|
|
21
|
+
this._explain = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ============================================================
|
|
25
|
+
// 条件
|
|
26
|
+
// ============================================================
|
|
27
|
+
|
|
28
|
+
where(conditions) {
|
|
29
|
+
Object.assign(this._conditions, conditions);
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================================
|
|
34
|
+
// 排序
|
|
35
|
+
// ============================================================
|
|
36
|
+
|
|
37
|
+
orderBy(field, dir = 'asc') {
|
|
38
|
+
this._orderBy.push({ field, dir: dir.toLowerCase() });
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
orderByDesc(field) {
|
|
43
|
+
return this.orderBy(field, 'desc');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================
|
|
47
|
+
// 分页
|
|
48
|
+
// ============================================================
|
|
49
|
+
|
|
50
|
+
limit(n) {
|
|
51
|
+
this._limitCount = n;
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
offset(n) {
|
|
56
|
+
this._offsetCount = n;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 便捷分页
|
|
62
|
+
* @param {number} page - 页码(从 1 开始)
|
|
63
|
+
* @param {number} perPage - 每页数量
|
|
64
|
+
* @returns {{ rows, total, page, perPage, totalPages }}
|
|
65
|
+
*/
|
|
66
|
+
paginate(page = 1, perPage = 20) {
|
|
67
|
+
this._limitCount = perPage;
|
|
68
|
+
this._offsetCount = (page - 1) * perPage;
|
|
69
|
+
const total = this._countRaw();
|
|
70
|
+
const rows = this.get();
|
|
71
|
+
return {
|
|
72
|
+
rows,
|
|
73
|
+
total,
|
|
74
|
+
page,
|
|
75
|
+
perPage,
|
|
76
|
+
totalPages: Math.ceil(total / perPage)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================
|
|
81
|
+
// 字段选择
|
|
82
|
+
// ============================================================
|
|
83
|
+
|
|
84
|
+
select(fields) {
|
|
85
|
+
this._selectFields = Array.isArray(fields) ? fields : [fields];
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
distinct(field) {
|
|
90
|
+
this._distinct = true;
|
|
91
|
+
this._distinctField = field;
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================
|
|
96
|
+
// 分组 & 聚合后过滤
|
|
97
|
+
// ============================================================
|
|
98
|
+
|
|
99
|
+
groupBy(field) {
|
|
100
|
+
this._groupBy = field;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
having(conditions) {
|
|
105
|
+
this._having = conditions;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================================================
|
|
110
|
+
// JOIN
|
|
111
|
+
// ============================================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* INNER JOIN
|
|
115
|
+
* @param {Table} otherTable - 要 JOIN 的表
|
|
116
|
+
* @param {string} localField - 本地字段
|
|
117
|
+
* @param {string} foreignField - 对方字段
|
|
118
|
+
* @param {string} as - 别名
|
|
119
|
+
*/
|
|
120
|
+
join(otherTable, localField, foreignField, as) {
|
|
121
|
+
this._joins.push({ type: 'inner', table: otherTable, localField, foreignField, as });
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
leftJoin(otherTable, localField, foreignField, as) {
|
|
126
|
+
this._joins.push({ type: 'left', table: otherTable, localField, foreignField, as });
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
rightJoin(otherTable, localField, foreignField, as) {
|
|
131
|
+
this._joins.push({ type: 'right', table: otherTable, localField, foreignField, as });
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================
|
|
136
|
+
// UNION
|
|
137
|
+
// ============================================================
|
|
138
|
+
|
|
139
|
+
union(query) {
|
|
140
|
+
this._union = query;
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============================================================
|
|
145
|
+
// EXPLAIN
|
|
146
|
+
// ============================================================
|
|
147
|
+
|
|
148
|
+
explain() {
|
|
149
|
+
this._explain = true;
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================
|
|
154
|
+
// 执行
|
|
155
|
+
// ============================================================
|
|
156
|
+
|
|
157
|
+
get() {
|
|
158
|
+
if (this._explain) return this._getExplain();
|
|
159
|
+
|
|
160
|
+
let rows = this._table._applyFilter(this._table._rows, this._conditions);
|
|
161
|
+
|
|
162
|
+
// JOIN
|
|
163
|
+
for (const join of this._joins) {
|
|
164
|
+
rows = this._applyJoin(rows, join);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// UNION
|
|
168
|
+
if (this._union) {
|
|
169
|
+
const unionRows = this._union.get();
|
|
170
|
+
rows = [...rows, ...unionRows];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// GROUP BY
|
|
174
|
+
if (this._groupBy) {
|
|
175
|
+
rows = this._applyGroupBy(rows);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// HAVING
|
|
179
|
+
if (this._having) {
|
|
180
|
+
rows = this._table._applyFilter(rows, this._having);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// DISTINCT
|
|
184
|
+
if (this._distinct && this._distinctField) {
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
rows = rows.filter(r => {
|
|
187
|
+
const val = r[this._distinctField];
|
|
188
|
+
if (val === undefined) return false; // 跳过 undefined
|
|
189
|
+
const key = String(val);
|
|
190
|
+
if (seen.has(key)) return false;
|
|
191
|
+
seen.add(key);
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 排序
|
|
197
|
+
if (this._orderBy.length > 0) {
|
|
198
|
+
rows = this._applyOrderBy(rows);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 分页
|
|
202
|
+
if (this._offsetCount > 0) {
|
|
203
|
+
rows = rows.slice(this._offsetCount);
|
|
204
|
+
}
|
|
205
|
+
if (this._limitCount !== null) {
|
|
206
|
+
rows = rows.slice(0, this._limitCount);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 字段选择
|
|
210
|
+
rows = rows.map(r => ({ ...r }));
|
|
211
|
+
if (this._selectFields) {
|
|
212
|
+
rows = rows.map(r => {
|
|
213
|
+
const obj = {};
|
|
214
|
+
for (const f of this._selectFields) {
|
|
215
|
+
obj[f] = r[f];
|
|
216
|
+
}
|
|
217
|
+
return obj;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return rows;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
first() {
|
|
225
|
+
const rows = this.limit(1).get();
|
|
226
|
+
return rows.length > 0 ? rows[0] : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
count() {
|
|
230
|
+
return this._countRaw();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
sum(field) {
|
|
234
|
+
const rows = this._getFilteredRows();
|
|
235
|
+
return rows.reduce((acc, r) => acc + (Number(r[field]) || 0), 0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
avg(field) {
|
|
239
|
+
const rows = this._getFilteredRows();
|
|
240
|
+
if (rows.length === 0) return 0;
|
|
241
|
+
return this.sum(field) / rows.length;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
min(field) {
|
|
245
|
+
const rows = this._getFilteredRows();
|
|
246
|
+
if (rows.length === 0) return null;
|
|
247
|
+
return rows.reduce((min, r) => {
|
|
248
|
+
const v = Number(r[field]);
|
|
249
|
+
return (min === null || v < min) ? v : min;
|
|
250
|
+
}, null);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
max(field) {
|
|
254
|
+
const rows = this._getFilteredRows();
|
|
255
|
+
if (rows.length === 0) return null;
|
|
256
|
+
return rows.reduce((max, r) => {
|
|
257
|
+
const v = Number(r[field]);
|
|
258
|
+
return (max === null || v > max) ? v : max;
|
|
259
|
+
}, null);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
groupStats(field) {
|
|
263
|
+
if (!this._groupBy) throw new Error('groupStats() requires groupBy() to be called first');
|
|
264
|
+
const rows = this._table._applyFilter(this._table._rows, this._conditions);
|
|
265
|
+
const groups = {};
|
|
266
|
+
|
|
267
|
+
for (const row of rows) {
|
|
268
|
+
const key = String(row[this._groupBy] ?? '__null__');
|
|
269
|
+
if (!groups[key]) {
|
|
270
|
+
groups[key] = { _group: row[this._groupBy], _count: 0, _sum: 0, _min: null, _max: null, _rows: [] };
|
|
271
|
+
}
|
|
272
|
+
const g = groups[key];
|
|
273
|
+
g._count++;
|
|
274
|
+
const v = Number(row[field]) || 0;
|
|
275
|
+
g._sum += v;
|
|
276
|
+
g._min = g._min === null ? v : Math.min(g._min, v);
|
|
277
|
+
g._max = g._max === null ? v : Math.max(g._max, v);
|
|
278
|
+
g._rows.push(row);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const result = Object.values(groups);
|
|
282
|
+
result.forEach(g => {
|
|
283
|
+
g._avg = g._count > 0 ? g._sum / g._count : 0;
|
|
284
|
+
delete g._rows;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
update(updates) {
|
|
291
|
+
return this._table.update(this._conditions, updates);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
remove() {
|
|
295
|
+
return this._table.remove(this._conditions);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ============================================================
|
|
299
|
+
// 内部
|
|
300
|
+
// ============================================================
|
|
301
|
+
|
|
302
|
+
_getFilteredRows() {
|
|
303
|
+
return this._table._applyFilter(this._table._rows, this._conditions);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
_countRaw() {
|
|
307
|
+
return this._table._applyFilter(this._table._rows, this._conditions).length;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
_applyOrderBy(rows) {
|
|
311
|
+
return [...rows].sort((a, b) => {
|
|
312
|
+
for (const { field, dir } of this._orderBy) {
|
|
313
|
+
const va = a[field];
|
|
314
|
+
const vb = b[field];
|
|
315
|
+
if (va < vb) return dir === 'asc' ? -1 : 1;
|
|
316
|
+
if (va > vb) return dir === 'asc' ? 1 : -1;
|
|
317
|
+
}
|
|
318
|
+
return 0;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_applyJoin(localRows, join) {
|
|
323
|
+
const result = [];
|
|
324
|
+
const foreignRows = join.table._rows;
|
|
325
|
+
|
|
326
|
+
if (join.type === 'left') {
|
|
327
|
+
for (const lr of localRows) {
|
|
328
|
+
const matches = foreignRows.filter(fr => fr[join.foreignField] === lr[join.localField]);
|
|
329
|
+
if (matches.length === 0) {
|
|
330
|
+
result.push({ ...lr, ...this._nullRow(join.table) });
|
|
331
|
+
} else {
|
|
332
|
+
for (const fr of matches) {
|
|
333
|
+
result.push(this._mergeJoinRow(lr, fr, join));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} else if (join.type === 'right') {
|
|
338
|
+
for (const fr of foreignRows) {
|
|
339
|
+
const matches = localRows.filter(lr => lr[join.localField] === fr[join.foreignField]);
|
|
340
|
+
if (matches.length === 0) {
|
|
341
|
+
result.push({ ...fr, ...this._nullRow(join.table, true) });
|
|
342
|
+
} else {
|
|
343
|
+
for (const lr of matches) {
|
|
344
|
+
result.push(this._mergeJoinRow(lr, fr, join));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
// inner join
|
|
350
|
+
for (const lr of localRows) {
|
|
351
|
+
for (const fr of foreignRows) {
|
|
352
|
+
if (fr[join.foreignField] === lr[join.localField]) {
|
|
353
|
+
result.push(this._mergeJoinRow(lr, fr, join));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
_mergeJoinRow(lr, fr, join) {
|
|
362
|
+
const merged = { ...lr };
|
|
363
|
+
const prefix = join.as ? join.as + '_' : '';
|
|
364
|
+
for (const [key, value] of Object.entries(fr)) {
|
|
365
|
+
merged[prefix + key] = value;
|
|
366
|
+
}
|
|
367
|
+
return merged;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
_nullRow(table, isLocal) {
|
|
371
|
+
const nulls = {};
|
|
372
|
+
const schema = table._schema;
|
|
373
|
+
for (const field of Object.keys(schema)) {
|
|
374
|
+
nulls[field] = null;
|
|
375
|
+
}
|
|
376
|
+
return nulls;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
_applyGroupBy(rows) {
|
|
380
|
+
const groups = {};
|
|
381
|
+
for (const row of rows) {
|
|
382
|
+
const key = String(row[this._groupBy] ?? '__null__');
|
|
383
|
+
if (!groups[key]) {
|
|
384
|
+
groups[key] = { _group: row[this._groupBy], _rows: [], _count: 0 };
|
|
385
|
+
}
|
|
386
|
+
groups[key]._rows.push(row);
|
|
387
|
+
groups[key]._count++;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const result = [];
|
|
391
|
+
for (const g of Object.values(groups)) {
|
|
392
|
+
// 使用第一行作为基础,并添加 _group 和 _count
|
|
393
|
+
const base = { ...g._rows[0] };
|
|
394
|
+
base._group = g._group;
|
|
395
|
+
base._count = g._count;
|
|
396
|
+
result.push(base);
|
|
397
|
+
}
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
_getExplain() {
|
|
402
|
+
const totalRows = this._table._rows.length;
|
|
403
|
+
const filtered = this._table._applyFilter(this._table._rows, this._conditions);
|
|
404
|
+
const plan = {
|
|
405
|
+
table: this._table._name,
|
|
406
|
+
totalRows,
|
|
407
|
+
conditions: this._conditions,
|
|
408
|
+
filteredRows: filtered.length,
|
|
409
|
+
selectivity: totalRows > 0 ? (filtered.length / totalRows * 100).toFixed(1) + '%' : '0%',
|
|
410
|
+
hasIndex: false,
|
|
411
|
+
indexUsed: null,
|
|
412
|
+
joins: this._joins.length,
|
|
413
|
+
groupBy: this._groupBy,
|
|
414
|
+
having: this._having,
|
|
415
|
+
orderBy: this._orderBy.map(o => `${o.field} ${o.dir}`),
|
|
416
|
+
limit: this._limitCount,
|
|
417
|
+
offset: this._offsetCount,
|
|
418
|
+
estimatedCost: filtered.length * (1 + this._joins.length)
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// 检查索引命中
|
|
422
|
+
for (const [field] of Object.entries(this._conditions)) {
|
|
423
|
+
if (this._table._indexes[field]) {
|
|
424
|
+
plan.hasIndex = true;
|
|
425
|
+
plan.indexUsed = field;
|
|
426
|
+
plan.estimatedCost = Math.floor(plan.estimatedCost * 0.1);
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return plan;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
module.exports = Query;
|