daxiapi-cli 2.6.0 → 2.7.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/README.md +73 -0
- package/bin/index.js +1 -0
- package/commands/sql.js +66 -0
- package/lib/api.js +6 -0
- package/lib/sql-parser.js +348 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,10 +91,34 @@ daxiapi sector gn
|
|
|
91
91
|
|
|
92
92
|
# 热门概念板块(东方财富数据源)
|
|
93
93
|
daxiapi sector gn --type dfcf
|
|
94
|
+
|
|
95
|
+
# 行业板块详情(通过板块ID查询)
|
|
96
|
+
daxiapi sector bk_info --code BK0428
|
|
97
|
+
|
|
98
|
+
# 行业板块详情(通过板块名称查询,支持模糊匹配)
|
|
99
|
+
daxiapi sector bk_info --name 工程建筑
|
|
100
|
+
|
|
101
|
+
# 行业板块详情(指定数据源)
|
|
102
|
+
daxiapi sector bk_info --type ths --name 工程建筑
|
|
103
|
+
|
|
104
|
+
# 概念板块详情(通过板块ID查询)
|
|
105
|
+
daxiapi sector gn_info --code GN123
|
|
106
|
+
|
|
107
|
+
# 概念板块详情(通过板块名称查询,支持模糊匹配)
|
|
108
|
+
daxiapi sector gn_info --name 人工智能
|
|
109
|
+
|
|
110
|
+
# 概念板块详情(指定数据源)
|
|
111
|
+
daxiapi sector gn_info --type ths --name 人工智能
|
|
94
112
|
```
|
|
95
113
|
|
|
96
114
|
**sector stocks 排序字段**:`cs`(CS强度)、`zdf`(涨跌幅)、`sm`(市值)、`cg`(成交额)、`cr`(换手率)、`sctr`(SCTR排名)
|
|
97
115
|
|
|
116
|
+
**bk_info/gn_info 说明**:
|
|
117
|
+
- `bk_info`:查询行业板块详情,返回CS强度、多日涨跌幅、市场宽度、主力资金净流入等数据
|
|
118
|
+
- `gn_info`:查询概念板块详情,返回CS强度、多日涨跌幅、涨幅7%以上股票数、突破箱体股票数等数据
|
|
119
|
+
- 支持通过 `--code`(板块ID)或 `--name`(板块名称,模糊匹配)查询
|
|
120
|
+
- `--type` 可选数据源:`dfcf`(东方财富)或 `ths`(同花顺)
|
|
121
|
+
|
|
98
122
|
---
|
|
99
123
|
|
|
100
124
|
### 个股数据
|
|
@@ -164,6 +188,55 @@ daxiapi stock capital-flow 600031 --days 10
|
|
|
164
188
|
|
|
165
189
|
---
|
|
166
190
|
|
|
191
|
+
### SQL筛选
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# 基础查询(查询指定日期的股票)
|
|
195
|
+
daxiapi sql "date='2026-06-17' LIMIT 10"
|
|
196
|
+
|
|
197
|
+
# 强势股筛选(RPS>70)
|
|
198
|
+
daxiapi sql "date='2026-06-17' AND rps_score>70 ORDER BY rps_score DESC LIMIT 20"
|
|
199
|
+
|
|
200
|
+
# 多条件组合(RPS>70、SCTR>60、CS>0)
|
|
201
|
+
daxiapi sql "date='2026-06-17' AND rps_score>70 AND sctr>60 AND cs>0 ORDER BY rps_score DESC LIMIT 15"
|
|
202
|
+
|
|
203
|
+
# 技术形态筛选(VCP形态)
|
|
204
|
+
daxiapi sql "date='2026-06-17' AND isVCP=1 ORDER BY rps_score DESC LIMIT 20"
|
|
205
|
+
|
|
206
|
+
# 区间范围查询(CS在0-15之间)
|
|
207
|
+
daxiapi sql "date='2026-06-17' AND cs in [0, 15] ORDER BY cs DESC LIMIT 20"
|
|
208
|
+
|
|
209
|
+
# 板块筛选(银行板块)
|
|
210
|
+
daxiapi sql "date='2026-06-17' AND bkName='银行' AND rps_score>70 ORDER BY rps_score DESC LIMIT 20"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**SQL语法说明**:
|
|
214
|
+
|
|
215
|
+
- **必须包含 `date` 条件**:所有查询必须指定日期,格式为 `date='YYYY-MM-DD'`
|
|
216
|
+
- **日期限制**:只支持查询最近10天内的数据
|
|
217
|
+
- **支持的运算符**:`=`, `!=`, `<>`, `>`, `<`, `>=`, `<=`, `in [...]`, `in (...)`
|
|
218
|
+
- **逻辑运算符**:`AND`, `OR`,支持括号嵌套
|
|
219
|
+
- **排序**:`ORDER BY field ASC/DESC`,支持多字段排序
|
|
220
|
+
- **数量限制**:`LIMIT N`,限制返回数量
|
|
221
|
+
|
|
222
|
+
**支持的查询字段**:
|
|
223
|
+
|
|
224
|
+
| 字段类别 | 字段名 | 说明 |
|
|
225
|
+
|---------|--------|------|
|
|
226
|
+
| 日期 | `date` | 查询日期(必填) |
|
|
227
|
+
| 基本信息 | `stockId`, `name` | 股票代码、名称 |
|
|
228
|
+
| 涨跌幅 | `zdf`, `zdf_5d`, `zdf_10d`, `zdf_20d` | 当日、5日、10日、20日涨跌幅 |
|
|
229
|
+
| 强度指标 | `cs`, `rps_score`, `sctr`, `sm`, `ml` | CS强度、RPS、SCTR、SM、ML |
|
|
230
|
+
| 技术形态 | `isVCP`, `isSOS`, `isSpring`, `isNewHigh` | VCP、SOS、Spring、新高形态 |
|
|
231
|
+
| 价格 | `close`, `open`, `high`, `low` | 收盘价、开盘价、最高价、最低价 |
|
|
232
|
+
| 成交量 | `vol`, `amount` | 成交量、成交额 |
|
|
233
|
+
| 市值 | `shizhi` | 总市值(亿元) |
|
|
234
|
+
| 板块 | `bkName`, `bkCode` | 板块名称、板块代码 |
|
|
235
|
+
|
|
236
|
+
**更多示例**:参考 [xiapi-stock-sql-screener Skill](../skills/xiapi-stock-sql-screener/SKILL.md)
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
167
240
|
### 涨跌停
|
|
168
241
|
|
|
169
242
|
```bash
|
package/bin/index.js
CHANGED
|
@@ -12,6 +12,7 @@ program
|
|
|
12
12
|
require('../commands/config')(program);
|
|
13
13
|
require('../commands/market')(program);
|
|
14
14
|
require('../commands/sector')(program);
|
|
15
|
+
require('../commands/sql')(program);
|
|
15
16
|
require('../commands/stock')(program);
|
|
16
17
|
require('../commands/kline')(program);
|
|
17
18
|
require('../commands/zdt')(program);
|
package/commands/sql.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const config = require('../lib/config');
|
|
2
|
+
const api = require('../lib/api');
|
|
3
|
+
const {handleError, createParameterError} = require('../lib/error');
|
|
4
|
+
const {output} = require('../lib/output');
|
|
5
|
+
const {parseSql} = require('../lib/sql-parser');
|
|
6
|
+
|
|
7
|
+
module.exports = function (program) {
|
|
8
|
+
const sqlCmd = program
|
|
9
|
+
.command('sql')
|
|
10
|
+
.description(
|
|
11
|
+
'使用 SQL 条件筛选股票,支持自定义条件组合、排序和数量限制。' +
|
|
12
|
+
'支持等于、大于、小于、区间范围、IN枚举、字段间比较等多种条件写法,支持 AND/OR 逻辑组合和括号嵌套。' +
|
|
13
|
+
'返回股票代码、名称、涨跌幅、CS强度、RPS相对强度、SCTR技术排名、所属板块、概念等详细数据。' +
|
|
14
|
+
'可用于量化筛选、技术形态选股和自定义策略分析。'
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
sqlCmd
|
|
18
|
+
.argument('<condition>', 'SQL WHERE 条件(支持 ORDER BY 和 LIMIT)')
|
|
19
|
+
.action(async condition => {
|
|
20
|
+
try {
|
|
21
|
+
const token = config.getToken();
|
|
22
|
+
if (!token) {
|
|
23
|
+
const error = new Error('未配置 API Token');
|
|
24
|
+
error.response = {status: 401};
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!condition) {
|
|
29
|
+
throw createParameterError(
|
|
30
|
+
'参数无效',
|
|
31
|
+
["参数 'condition' 不能为空"],
|
|
32
|
+
[
|
|
33
|
+
'daxiapi sql "date=\'2026-06-17\' AND isVCP=1"',
|
|
34
|
+
'daxiapi sql "date=\'2026-06-17\' AND rps_score>70 ORDER BY rps_score DESC LIMIT 20"'
|
|
35
|
+
]
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 解析 SQL 条件
|
|
40
|
+
const parsed = parseSql(condition);
|
|
41
|
+
if (parsed.error) {
|
|
42
|
+
throw createParameterError(
|
|
43
|
+
'SQL 解析失败',
|
|
44
|
+
[parsed.error],
|
|
45
|
+
[
|
|
46
|
+
'daxiapi sql "date=\'2026-06-17\' AND isVCP=1"',
|
|
47
|
+
'daxiapi sql "date=\'2026-06-17\' AND cs in [0, 15] AND rps_score>70 ORDER BY rps_score DESC LIMIT 10"'
|
|
48
|
+
]
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 调用服务端接口
|
|
53
|
+
const data = await api.querySqlStocks(
|
|
54
|
+
token,
|
|
55
|
+
parsed.conditions,
|
|
56
|
+
parsed.orderBy,
|
|
57
|
+
parsed.limit
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
output(data);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
handleError(error);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
};
|
package/lib/api.js
CHANGED
|
@@ -124,6 +124,11 @@ async function queryStockData(token, q, type = 'stock') {
|
|
|
124
124
|
return post(client, '/query_stock_data', {q, type});
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
async function querySqlStocks(token, conditions, orderBy, limit) {
|
|
128
|
+
const client = createClient(token);
|
|
129
|
+
return post(client, '/sql_query', { conditions, orderBy, limit });
|
|
130
|
+
}
|
|
131
|
+
|
|
127
132
|
|
|
128
133
|
|
|
129
134
|
async function getCapitalFlow(code, days =10) {
|
|
@@ -509,6 +514,7 @@ module.exports = {
|
|
|
509
514
|
getZdtPool,
|
|
510
515
|
getSecId,
|
|
511
516
|
queryStockData,
|
|
517
|
+
querySqlStocks,
|
|
512
518
|
getPatternStocks,
|
|
513
519
|
getDividendScore,
|
|
514
520
|
getStockRank,
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
class Tokenizer {
|
|
2
|
+
constructor(text) {
|
|
3
|
+
this.text = text;
|
|
4
|
+
this.pos = 0;
|
|
5
|
+
this.tokens = [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
tokenize() {
|
|
9
|
+
while (this.pos < this.text.length) {
|
|
10
|
+
this.skipWhitespace();
|
|
11
|
+
if (this.pos >= this.text.length) break;
|
|
12
|
+
|
|
13
|
+
const char = this.text[this.pos];
|
|
14
|
+
|
|
15
|
+
if (char === '(') {
|
|
16
|
+
this.tokens.push({type: 'LPAREN', value: '('});
|
|
17
|
+
this.pos++;
|
|
18
|
+
} else if (char === ')') {
|
|
19
|
+
this.tokens.push({type: 'RPAREN', value: ')'});
|
|
20
|
+
this.pos++;
|
|
21
|
+
} else if (char === '[') {
|
|
22
|
+
this.tokens.push({type: 'LBRACKET', value: '['});
|
|
23
|
+
this.pos++;
|
|
24
|
+
} else if (char === ']') {
|
|
25
|
+
this.tokens.push({type: 'RBRACKET', value: ']'});
|
|
26
|
+
this.pos++;
|
|
27
|
+
} else if (char === ',') {
|
|
28
|
+
this.tokens.push({type: 'COMMA', value: ','});
|
|
29
|
+
this.pos++;
|
|
30
|
+
} else if ('=!<>'.includes(char)) {
|
|
31
|
+
this.readOperator();
|
|
32
|
+
} else if (char === "'" || char === '"') {
|
|
33
|
+
this.readString(char);
|
|
34
|
+
} else if (/\d/.test(char)) {
|
|
35
|
+
this.readNumber();
|
|
36
|
+
} else if (/[a-zA-Z_]/.test(char)) {
|
|
37
|
+
this.readWord();
|
|
38
|
+
} else {
|
|
39
|
+
throw new Error(`未知字符: ${char} at position ${this.pos}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return this.tokens;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
skipWhitespace() {
|
|
46
|
+
while (this.pos < this.text.length && /\s/.test(this.text[this.pos])) {
|
|
47
|
+
this.pos++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
readOperator() {
|
|
52
|
+
const start = this.pos;
|
|
53
|
+
const char = this.text[this.pos];
|
|
54
|
+
|
|
55
|
+
if (char === '!' && this.text[this.pos + 1] === '=') {
|
|
56
|
+
this.tokens.push({type: 'OP', value: '!='});
|
|
57
|
+
this.pos += 2;
|
|
58
|
+
} else if (char === '>' && this.text[this.pos + 1] === '=') {
|
|
59
|
+
this.tokens.push({type: 'OP', value: '>='});
|
|
60
|
+
this.pos += 2;
|
|
61
|
+
} else if (char === '<' && this.text[this.pos + 1] === '=') {
|
|
62
|
+
this.tokens.push({type: 'OP', value: '<='});
|
|
63
|
+
this.pos += 2;
|
|
64
|
+
} else if ('=!<>'.includes(char)) {
|
|
65
|
+
this.tokens.push({type: 'OP', value: char});
|
|
66
|
+
this.pos++;
|
|
67
|
+
} else {
|
|
68
|
+
throw new Error(`无效操作符 at position ${start}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
readString(quote) {
|
|
73
|
+
this.pos++;
|
|
74
|
+
const start = this.pos;
|
|
75
|
+
while (this.pos < this.text.length && this.text[this.pos] !== quote) {
|
|
76
|
+
this.pos++;
|
|
77
|
+
}
|
|
78
|
+
if (this.pos >= this.text.length) {
|
|
79
|
+
throw new Error(`未闭合的字符串 at position ${start - 1}`);
|
|
80
|
+
}
|
|
81
|
+
const value = this.text.substring(start, this.pos);
|
|
82
|
+
this.tokens.push({type: 'STRING', value});
|
|
83
|
+
this.pos++;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
readNumber() {
|
|
87
|
+
const start = this.pos;
|
|
88
|
+
while (this.pos < this.text.length && /[\d.]/.test(this.text[this.pos])) {
|
|
89
|
+
this.pos++;
|
|
90
|
+
}
|
|
91
|
+
const value = parseFloat(this.text.substring(start, this.pos));
|
|
92
|
+
this.tokens.push({type: 'NUMBER', value});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
readWord() {
|
|
96
|
+
const start = this.pos;
|
|
97
|
+
while (this.pos < this.text.length && /[a-zA-Z0-9_]/.test(this.text[this.pos])) {
|
|
98
|
+
this.pos++;
|
|
99
|
+
}
|
|
100
|
+
const value = this.text.substring(start, this.pos);
|
|
101
|
+
|
|
102
|
+
const upper = value.toUpperCase();
|
|
103
|
+
if (upper === 'AND') {
|
|
104
|
+
this.tokens.push({type: 'AND', value: upper});
|
|
105
|
+
} else if (upper === 'OR') {
|
|
106
|
+
this.tokens.push({type: 'OR', value: upper});
|
|
107
|
+
} else if (upper === 'IN') {
|
|
108
|
+
this.tokens.push({type: 'IN', value: upper});
|
|
109
|
+
} else if (upper === 'BETWEEN') {
|
|
110
|
+
this.tokens.push({type: 'BETWEEN', value: upper});
|
|
111
|
+
} else {
|
|
112
|
+
this.tokens.push({type: 'IDENT', value});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
class Parser {
|
|
118
|
+
constructor(tokens, fieldMap = {}) {
|
|
119
|
+
this.tokens = tokens;
|
|
120
|
+
this.pos = 0;
|
|
121
|
+
this.fieldMap = fieldMap;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
parse() {
|
|
125
|
+
if (this.tokens.length === 0) {
|
|
126
|
+
return {error: '空条件'};
|
|
127
|
+
}
|
|
128
|
+
const result = this.parseExpression();
|
|
129
|
+
if (this.pos < this.tokens.length) {
|
|
130
|
+
return {error: `未解析的 token: ${JSON.stringify(this.tokens[this.pos])}`};
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
parseExpression() {
|
|
136
|
+
return this.parseOr();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
parseOr() {
|
|
140
|
+
let left = this.parseAnd();
|
|
141
|
+
|
|
142
|
+
while (this.match('OR')) {
|
|
143
|
+
const right = this.parseAnd();
|
|
144
|
+
if (left.type === 'or') {
|
|
145
|
+
left.conditions.push(right);
|
|
146
|
+
} else {
|
|
147
|
+
left = {type: 'or', conditions: [left, right]};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return left;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
parseAnd() {
|
|
155
|
+
let left = this.parseFactor();
|
|
156
|
+
|
|
157
|
+
while (this.match('AND')) {
|
|
158
|
+
const right = this.parseFactor();
|
|
159
|
+
if (left.type === 'and') {
|
|
160
|
+
left.conditions.push(right);
|
|
161
|
+
} else {
|
|
162
|
+
left = {type: 'and', conditions: [left, right]};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return left;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
parseFactor() {
|
|
170
|
+
if (this.match('LPAREN')) {
|
|
171
|
+
const expr = this.parseExpression();
|
|
172
|
+
if (!this.match('RPAREN')) {
|
|
173
|
+
return {error: '缺少右括号 )'};
|
|
174
|
+
}
|
|
175
|
+
return expr;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return this.parseCondition();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
parseCondition() {
|
|
182
|
+
const fieldToken = this.expect('IDENT');
|
|
183
|
+
if (fieldToken.error) return fieldToken;
|
|
184
|
+
|
|
185
|
+
const field = fieldToken.value;
|
|
186
|
+
|
|
187
|
+
if (this.match('IN')) {
|
|
188
|
+
if (this.match('LPAREN')) {
|
|
189
|
+
const values = [];
|
|
190
|
+
do {
|
|
191
|
+
const valueToken = this.tokens[this.pos];
|
|
192
|
+
if (valueToken.type === 'STRING' || valueToken.type === 'NUMBER') {
|
|
193
|
+
values.push(valueToken.value);
|
|
194
|
+
this.pos++;
|
|
195
|
+
} else if (valueToken.type === 'IDENT') {
|
|
196
|
+
values.push(valueToken.value);
|
|
197
|
+
this.pos++;
|
|
198
|
+
} else {
|
|
199
|
+
return {error: 'IN 列表中无效的值'};
|
|
200
|
+
}
|
|
201
|
+
} while (this.match('COMMA'));
|
|
202
|
+
|
|
203
|
+
if (!this.match('RPAREN')) {
|
|
204
|
+
return {error: 'IN 列表缺少 )'};
|
|
205
|
+
}
|
|
206
|
+
return {type: 'condition', field, op: 'IN', value: values};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (this.match('LBRACKET')) {
|
|
210
|
+
const minToken = this.tokens[this.pos];
|
|
211
|
+
if (minToken.type !== 'NUMBER' && minToken.type !== 'IDENT') {
|
|
212
|
+
return {error: '区间下限无效'};
|
|
213
|
+
}
|
|
214
|
+
const min = minToken.value;
|
|
215
|
+
this.pos++;
|
|
216
|
+
|
|
217
|
+
if (!this.match('COMMA')) {
|
|
218
|
+
return {error: '区间缺少逗号'};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const maxToken = this.tokens[this.pos];
|
|
222
|
+
if (maxToken.type !== 'NUMBER' && maxToken.type !== 'IDENT') {
|
|
223
|
+
return {error: '区间上限无效'};
|
|
224
|
+
}
|
|
225
|
+
const max = maxToken.value;
|
|
226
|
+
this.pos++;
|
|
227
|
+
|
|
228
|
+
if (!this.match('RBRACKET')) {
|
|
229
|
+
return {error: '区间缺少 ]'};
|
|
230
|
+
}
|
|
231
|
+
return {type: 'condition', field, op: 'BETWEEN', value: [min, max]};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {error: 'IN 后缺少 ( 或 ['};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const opToken = this.expect('OP');
|
|
238
|
+
if (opToken.error) return opToken;
|
|
239
|
+
|
|
240
|
+
const op = opToken.value;
|
|
241
|
+
const valueToken = this.tokens[this.pos];
|
|
242
|
+
|
|
243
|
+
if (!valueToken) {
|
|
244
|
+
return {error: '缺少值'};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (valueToken.type === 'STRING') {
|
|
248
|
+
this.pos++;
|
|
249
|
+
return {type: 'condition', field, op, value: valueToken.value};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (valueToken.type === 'NUMBER') {
|
|
253
|
+
this.pos++;
|
|
254
|
+
return {type: 'condition', field, op, value: valueToken.value};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (valueToken.type === 'IDENT') {
|
|
258
|
+
this.pos++;
|
|
259
|
+
if (this.fieldMap[valueToken.value]) {
|
|
260
|
+
return {type: 'condition', field, op: 'FIELD_CMP', cmpOp: op, value: valueToken.value};
|
|
261
|
+
}
|
|
262
|
+
return {type: 'condition', field, op, value: valueToken.value};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {error: `无效的值: ${JSON.stringify(valueToken)}`};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
match(type) {
|
|
269
|
+
if (this.pos < this.tokens.length && this.tokens[this.pos].type === type) {
|
|
270
|
+
this.pos++;
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
expect(type) {
|
|
277
|
+
if (this.pos < this.tokens.length && this.tokens[this.pos].type === type) {
|
|
278
|
+
const token = this.tokens[this.pos];
|
|
279
|
+
this.pos++;
|
|
280
|
+
return token;
|
|
281
|
+
}
|
|
282
|
+
return {error: `期望 ${type},但得到 ${this.tokens[this.pos] ? this.tokens[this.pos].type : 'EOF'}`};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parseSql(text, fieldMap = {}) {
|
|
287
|
+
text = text.trim();
|
|
288
|
+
if (!text) return {error: '请输入查询条件'};
|
|
289
|
+
|
|
290
|
+
text = text.replace(/^\s*SELECT\s+.*?\s+FROM\s+stocks\s*/i, '');
|
|
291
|
+
text = text.replace(/^\s*SELECT\s+\*\s+FROM\s+stocks\s*/i, '');
|
|
292
|
+
text = text.replace(/^\s*FROM\s+stocks\s*/i, '');
|
|
293
|
+
|
|
294
|
+
let whereText = '';
|
|
295
|
+
let orderByText = '';
|
|
296
|
+
let limitText = '';
|
|
297
|
+
|
|
298
|
+
const limitMatch = text.match(/\bLIMIT\s+(\d+)\s*$/i);
|
|
299
|
+
if (limitMatch) {
|
|
300
|
+
limitText = limitMatch[1];
|
|
301
|
+
text = text.substring(0, limitMatch.index).trim();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const orderMatch = text.match(/\bORDER\s+BY\s+(\w+)(?:\s+(ASC|DESC))?\s*$/i);
|
|
305
|
+
if (orderMatch) {
|
|
306
|
+
orderByText = orderMatch[0];
|
|
307
|
+
text = text.substring(0, orderMatch.index).trim();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const whereMatch = text.match(/^\s*WHERE\s+/i);
|
|
311
|
+
if (whereMatch) {
|
|
312
|
+
whereText = text.substring(whereMatch[0].length).trim();
|
|
313
|
+
} else {
|
|
314
|
+
whereText = text;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!whereText) return {error: '请输入 WHERE 条件'};
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const tokenizer = new Tokenizer(whereText);
|
|
321
|
+
const tokens = tokenizer.tokenize();
|
|
322
|
+
const parser = new Parser(tokens, fieldMap);
|
|
323
|
+
const parseResult = parser.parse();
|
|
324
|
+
|
|
325
|
+
if (parseResult.error) return parseResult;
|
|
326
|
+
|
|
327
|
+
const result = {conditions: parseResult};
|
|
328
|
+
|
|
329
|
+
if (orderMatch) {
|
|
330
|
+
result.orderBy = {
|
|
331
|
+
field: orderMatch[1],
|
|
332
|
+
dir: (orderMatch[2] || 'DESC').toUpperCase()
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (limitText) {
|
|
337
|
+
const n = parseInt(limitText, 10);
|
|
338
|
+
if (n < 1 || n > 100) return {error: 'LIMIT 必须在 1~100 之间'};
|
|
339
|
+
result.limit = n;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return result;
|
|
343
|
+
} catch (err) {
|
|
344
|
+
return {error: err.message};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports = {Tokenizer, Parser, parseSql};
|