@cicctencent/json-orm 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -0
- package/dist/cjs/index.d.ts +25 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +66 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/json-backend-fs.d.ts +26 -0
- package/dist/cjs/json-backend-fs.d.ts.map +1 -0
- package/dist/cjs/json-backend-fs.js +278 -0
- package/dist/cjs/json-backend-fs.js.map +1 -0
- package/dist/cjs/json-collection.d.ts +70 -0
- package/dist/cjs/json-collection.d.ts.map +1 -0
- package/dist/cjs/json-collection.js +385 -0
- package/dist/cjs/json-collection.js.map +1 -0
- package/dist/cjs/json-config.d.ts +87 -0
- package/dist/cjs/json-config.d.ts.map +1 -0
- package/dist/cjs/json-config.js +77 -0
- package/dist/cjs/json-config.js.map +1 -0
- package/dist/cjs/json-datasource.d.ts +35 -0
- package/dist/cjs/json-datasource.d.ts.map +1 -0
- package/dist/cjs/json-datasource.js +131 -0
- package/dist/cjs/json-datasource.js.map +1 -0
- package/dist/cjs/json-entity-meta.d.ts +42 -0
- package/dist/cjs/json-entity-meta.d.ts.map +1 -0
- package/dist/cjs/json-entity-meta.js +128 -0
- package/dist/cjs/json-entity-meta.js.map +1 -0
- package/dist/cjs/json-query-builder.d.ts +83 -0
- package/dist/cjs/json-query-builder.d.ts.map +1 -0
- package/dist/cjs/json-query-builder.js +477 -0
- package/dist/cjs/json-query-builder.js.map +1 -0
- package/dist/cjs/json-repository.d.ts +59 -0
- package/dist/cjs/json-repository.d.ts.map +1 -0
- package/dist/cjs/json-repository.js +309 -0
- package/dist/cjs/json-repository.js.map +1 -0
- package/dist/cjs/json-sql-expr.d.ts +107 -0
- package/dist/cjs/json-sql-expr.d.ts.map +1 -0
- package/dist/cjs/json-sql-expr.js +857 -0
- package/dist/cjs/json-sql-expr.js.map +1 -0
- package/dist/cjs/json-sql-select.d.ts +9 -0
- package/dist/cjs/json-sql-select.d.ts.map +1 -0
- package/dist/cjs/json-sql-select.js +434 -0
- package/dist/cjs/json-sql-select.js.map +1 -0
- package/dist/cjs/json-sql-shared.d.ts +16 -0
- package/dist/cjs/json-sql-shared.d.ts.map +1 -0
- package/dist/cjs/json-sql-shared.js +12 -0
- package/dist/cjs/json-sql-shared.js.map +1 -0
- package/dist/cjs/json-sql.d.ts +8 -0
- package/dist/cjs/json-sql.d.ts.map +1 -0
- package/dist/cjs/json-sql.js +322 -0
- package/dist/cjs/json-sql.js.map +1 -0
- package/dist/cjs/json-where.d.ts +54 -0
- package/dist/cjs/json-where.d.ts.map +1 -0
- package/dist/cjs/json-where.js +352 -0
- package/dist/cjs/json-where.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/esm/index.d.ts +25 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/json-backend-fs.d.ts +26 -0
- package/dist/esm/json-backend-fs.d.ts.map +1 -0
- package/dist/esm/json-backend-fs.js +241 -0
- package/dist/esm/json-backend-fs.js.map +1 -0
- package/dist/esm/json-collection.d.ts +70 -0
- package/dist/esm/json-collection.d.ts.map +1 -0
- package/dist/esm/json-collection.js +346 -0
- package/dist/esm/json-collection.js.map +1 -0
- package/dist/esm/json-config.d.ts +87 -0
- package/dist/esm/json-config.d.ts.map +1 -0
- package/dist/esm/json-config.js +69 -0
- package/dist/esm/json-config.js.map +1 -0
- package/dist/esm/json-datasource.d.ts +35 -0
- package/dist/esm/json-datasource.d.ts.map +1 -0
- package/dist/esm/json-datasource.js +123 -0
- package/dist/esm/json-datasource.js.map +1 -0
- package/dist/esm/json-entity-meta.d.ts +42 -0
- package/dist/esm/json-entity-meta.d.ts.map +1 -0
- package/dist/esm/json-entity-meta.js +120 -0
- package/dist/esm/json-entity-meta.js.map +1 -0
- package/dist/esm/json-query-builder.d.ts +83 -0
- package/dist/esm/json-query-builder.d.ts.map +1 -0
- package/dist/esm/json-query-builder.js +473 -0
- package/dist/esm/json-query-builder.js.map +1 -0
- package/dist/esm/json-repository.d.ts +59 -0
- package/dist/esm/json-repository.d.ts.map +1 -0
- package/dist/esm/json-repository.js +304 -0
- package/dist/esm/json-repository.js.map +1 -0
- package/dist/esm/json-sql-expr.d.ts +107 -0
- package/dist/esm/json-sql-expr.d.ts.map +1 -0
- package/dist/esm/json-sql-expr.js +847 -0
- package/dist/esm/json-sql-expr.js.map +1 -0
- package/dist/esm/json-sql-select.d.ts +9 -0
- package/dist/esm/json-sql-select.d.ts.map +1 -0
- package/dist/esm/json-sql-select.js +429 -0
- package/dist/esm/json-sql-select.js.map +1 -0
- package/dist/esm/json-sql-shared.d.ts +16 -0
- package/dist/esm/json-sql-shared.d.ts.map +1 -0
- package/dist/esm/json-sql-shared.js +9 -0
- package/dist/esm/json-sql-shared.js.map +1 -0
- package/dist/esm/json-sql.d.ts +8 -0
- package/dist/esm/json-sql.d.ts.map +1 -0
- package/dist/esm/json-sql.js +319 -0
- package/dist/esm/json-sql.js.map +1 -0
- package/dist/esm/json-where.d.ts +54 -0
- package/dist/esm/json-where.d.ts.map +1 -0
- package/dist/esm/json-where.js +342 -0
- package/dist/esm/json-where.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/examples/01-basic-entity.ts +89 -0
- package/examples/02-query-builder.ts +114 -0
- package/examples/03-raw-sql.ts +105 -0
- package/examples/04-non-entity-table.ts +70 -0
- package/examples/05-custom-backend.ts +145 -0
- package/examples/06-find-operators.ts +75 -0
- package/examples/README.md +73 -0
- package/examples/tsconfig.json +20 -0
- package/package.json +53 -0
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AGG_FUNCS = void 0;
|
|
4
|
+
exports.hasAggregate = hasAggregate;
|
|
5
|
+
exports.truthy = truthy;
|
|
6
|
+
exports.evalExpr = evalExpr;
|
|
7
|
+
exports.toDate = toDate;
|
|
8
|
+
exports.mysqlDatetime = mysqlDatetime;
|
|
9
|
+
exports.jsonPathGet = jsonPathGet;
|
|
10
|
+
exports.jsonUnquote = jsonUnquote;
|
|
11
|
+
exports.parseExpr = parseExpr;
|
|
12
|
+
/**
|
|
13
|
+
* JSON 回退存储:SQL 表达式引擎(json-fallback-storage 规格)
|
|
14
|
+
*
|
|
15
|
+
* 供 json-sql.ts 共用的表达式 AST、递归下降解析与求值:
|
|
16
|
+
* - 优先级:OR → AND → NOT → 比较(= != < > <= >= IS [NOT] NULL、
|
|
17
|
+
* [NOT] IN、[NOT] LIKE、[NOT] BETWEEN)→ 加减(含 `NOW() ± INTERVAL n UNIT`)
|
|
18
|
+
* → 乘除模 → 一元 → 后缀(`col->'$.path'` / `->>`)
|
|
19
|
+
* - CASE [operand] WHEN ... THEN ... ELSE ... END、CAST(x AS type)
|
|
20
|
+
* - MySQL 函数:COALESCE/NULLIF/IF/IFNULL、JSON_EXTRACT/JSON_UNQUOTE、
|
|
21
|
+
* NOW/UNIX_TIMESTAMP/FROM_UNIXTIME/DATE_FORMAT/TIMESTAMPDIFF/DATEDIFF、
|
|
22
|
+
* ABS/ROUND/FLOOR/CEIL、CONCAT/LOWER/UPPER/LENGTH/SUBSTRING/TRIM、DATABASE
|
|
23
|
+
* - 聚合(SUM/COUNT/AVG/MIN/MAX,含 DISTINCT)在分组上下文中求值
|
|
24
|
+
*
|
|
25
|
+
* 未覆盖语法显式抛错(fail-loud)。
|
|
26
|
+
*/
|
|
27
|
+
const json_where_ts_1 = require("./json-where.js");
|
|
28
|
+
exports.AGG_FUNCS = new Set(['SUM', 'COUNT', 'AVG', 'MIN', 'MAX']);
|
|
29
|
+
/** 表达式中是否含聚合函数 */
|
|
30
|
+
function hasAggregate(e) {
|
|
31
|
+
switch (e.kind) {
|
|
32
|
+
case 'func':
|
|
33
|
+
return exports.AGG_FUNCS.has(e.name) || e.args.some(hasAggregate);
|
|
34
|
+
case 'binary':
|
|
35
|
+
return hasAggregate(e.left) || hasAggregate(e.right);
|
|
36
|
+
case 'unary':
|
|
37
|
+
return hasAggregate(e.operand);
|
|
38
|
+
case 'case':
|
|
39
|
+
return ((e.operand ? hasAggregate(e.operand) : false) ||
|
|
40
|
+
e.whens.some(w => hasAggregate(w.when) || hasAggregate(w.then)) ||
|
|
41
|
+
(e.else ? hasAggregate(e.else) : false));
|
|
42
|
+
case 'cast':
|
|
43
|
+
return hasAggregate(e.operand);
|
|
44
|
+
case 'jsonPath':
|
|
45
|
+
return hasAggregate(e.source);
|
|
46
|
+
case 'interval':
|
|
47
|
+
return hasAggregate(e.value);
|
|
48
|
+
default:
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// ---------------- 解析 ----------------
|
|
53
|
+
function parseExpr(c) {
|
|
54
|
+
let left = parseAnd(c);
|
|
55
|
+
while (c.isKeyword('OR')) {
|
|
56
|
+
c.next();
|
|
57
|
+
left = { kind: 'binary', op: 'OR', left, right: parseAnd(c) };
|
|
58
|
+
}
|
|
59
|
+
return left;
|
|
60
|
+
}
|
|
61
|
+
function parseAnd(c) {
|
|
62
|
+
let left = parseNot(c);
|
|
63
|
+
while (c.isKeyword('AND')) {
|
|
64
|
+
c.next();
|
|
65
|
+
left = { kind: 'binary', op: 'AND', left, right: parseNot(c) };
|
|
66
|
+
}
|
|
67
|
+
return left;
|
|
68
|
+
}
|
|
69
|
+
function parseNot(c) {
|
|
70
|
+
if (c.isKeyword('NOT')) {
|
|
71
|
+
c.next();
|
|
72
|
+
return { kind: 'unary', op: 'NOT', operand: parseNot(c) };
|
|
73
|
+
}
|
|
74
|
+
return parseComparison(c);
|
|
75
|
+
}
|
|
76
|
+
const CMP_OPS = ['=', '!=', '<>', '<', '>', '<=', '>='];
|
|
77
|
+
function parseComparison(c) {
|
|
78
|
+
const left = parseAdditive(c);
|
|
79
|
+
if (c.isKeyword('IS')) {
|
|
80
|
+
c.next();
|
|
81
|
+
let negated = false;
|
|
82
|
+
if (c.isKeyword('NOT')) {
|
|
83
|
+
c.next();
|
|
84
|
+
negated = true;
|
|
85
|
+
}
|
|
86
|
+
c.expectKeyword('NULL');
|
|
87
|
+
return { kind: 'unary', op: negated ? 'IS NOT NULL' : 'IS NULL', operand: left };
|
|
88
|
+
}
|
|
89
|
+
let negated = false;
|
|
90
|
+
if (c.isKeyword('NOT')) {
|
|
91
|
+
c.next();
|
|
92
|
+
negated = true;
|
|
93
|
+
}
|
|
94
|
+
if (c.isKeyword('IN')) {
|
|
95
|
+
c.next();
|
|
96
|
+
c.expect('lparen');
|
|
97
|
+
const items = [];
|
|
98
|
+
if (c.peek().type !== 'rparen') {
|
|
99
|
+
items.push(parseExpr(c));
|
|
100
|
+
while (c.peek().type === 'comma') {
|
|
101
|
+
c.next();
|
|
102
|
+
items.push(parseExpr(c));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
c.expect('rparen');
|
|
106
|
+
return { kind: 'func', name: negated ? 'NOT IN' : 'IN', args: [left, ...items] };
|
|
107
|
+
}
|
|
108
|
+
if (c.isKeyword('LIKE')) {
|
|
109
|
+
c.next();
|
|
110
|
+
return { kind: 'binary', op: negated ? 'NOT LIKE' : 'LIKE', left, right: parseAdditive(c) };
|
|
111
|
+
}
|
|
112
|
+
// BETWEEN 分支须在 negated 抛错之前,否则 NOT BETWEEN 永不生效
|
|
113
|
+
if (c.isKeyword('BETWEEN')) {
|
|
114
|
+
c.next();
|
|
115
|
+
const low = parseAdditive(c);
|
|
116
|
+
c.expectKeyword('AND');
|
|
117
|
+
const high = parseAdditive(c);
|
|
118
|
+
return { kind: 'func', name: negated ? 'NOT BETWEEN' : 'BETWEEN', args: [left, low, high] };
|
|
119
|
+
}
|
|
120
|
+
if (negated) {
|
|
121
|
+
throw new Error('[json-storage] SQL 解析失败:NOT 后仅支持 IN/LIKE/BETWEEN/IS NULL');
|
|
122
|
+
}
|
|
123
|
+
const t = c.peek();
|
|
124
|
+
if (t.type === 'op' && CMP_OPS.includes(t.value)) {
|
|
125
|
+
c.next();
|
|
126
|
+
const right = parseAdditive(c);
|
|
127
|
+
return { kind: 'binary', op: t.value === '<>' ? '!=' : t.value, left, right };
|
|
128
|
+
}
|
|
129
|
+
return left;
|
|
130
|
+
}
|
|
131
|
+
const INTERVAL_UNITS = new Set(['SECOND', 'MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'QUARTER', 'YEAR']);
|
|
132
|
+
function parseAdditive(c) {
|
|
133
|
+
let left = parseMultiplicative(c);
|
|
134
|
+
while (c.peek().type === 'op' && (c.peek().value === '+' || c.peek().value === '-')) {
|
|
135
|
+
const op = c.next().value;
|
|
136
|
+
if (c.isKeyword('INTERVAL')) {
|
|
137
|
+
c.next();
|
|
138
|
+
const value = parseUnary(c);
|
|
139
|
+
const unitTok = c.peek();
|
|
140
|
+
if (unitTok.type !== 'ident' || !INTERVAL_UNITS.has(unitTok.value.toUpperCase())) {
|
|
141
|
+
throw new Error(`[json-storage] 不支持的 INTERVAL 单位:${unitTok.value}`);
|
|
142
|
+
}
|
|
143
|
+
c.next();
|
|
144
|
+
left = { kind: 'binary', op, left, right: { kind: 'interval', value, unit: unitTok.value.toUpperCase() } };
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
left = { kind: 'binary', op, left, right: parseMultiplicative(c) };
|
|
148
|
+
}
|
|
149
|
+
return left;
|
|
150
|
+
}
|
|
151
|
+
function parseMultiplicative(c) {
|
|
152
|
+
let left = parseUnary(c);
|
|
153
|
+
while (c.peek().type === 'op' && ['*', '/', '%'].includes(c.peek().value)) {
|
|
154
|
+
const op = c.next().value;
|
|
155
|
+
left = { kind: 'binary', op, left, right: parseUnary(c) };
|
|
156
|
+
}
|
|
157
|
+
return left;
|
|
158
|
+
}
|
|
159
|
+
function parseUnary(c) {
|
|
160
|
+
const t = c.peek();
|
|
161
|
+
if (t.type === 'op' && t.value === '-') {
|
|
162
|
+
c.next();
|
|
163
|
+
return { kind: 'unary', op: '-', operand: parseUnary(c) };
|
|
164
|
+
}
|
|
165
|
+
if (t.type === 'op' && t.value === '+') {
|
|
166
|
+
c.next();
|
|
167
|
+
return parseUnary(c);
|
|
168
|
+
}
|
|
169
|
+
return parsePostfix(c);
|
|
170
|
+
}
|
|
171
|
+
function parsePostfix(c) {
|
|
172
|
+
let expr = parsePrimaryExpr(c);
|
|
173
|
+
while (c.peek().type === 'op' && (c.peek().value === '->' || c.peek().value === '->>')) {
|
|
174
|
+
const op = c.next().value;
|
|
175
|
+
const path = c.expect('string');
|
|
176
|
+
expr = { kind: 'jsonPath', source: expr, path: path.value, unquote: op === '->>' };
|
|
177
|
+
}
|
|
178
|
+
return expr;
|
|
179
|
+
}
|
|
180
|
+
function parsePrimaryExpr(c) {
|
|
181
|
+
const t = c.peek();
|
|
182
|
+
if (t.type === 'number') {
|
|
183
|
+
c.next();
|
|
184
|
+
return { kind: 'literal', value: Number(t.value) };
|
|
185
|
+
}
|
|
186
|
+
if (t.type === 'string') {
|
|
187
|
+
c.next();
|
|
188
|
+
return { kind: 'literal', value: t.value };
|
|
189
|
+
}
|
|
190
|
+
if (t.type === 'param') {
|
|
191
|
+
c.next();
|
|
192
|
+
return { kind: 'param', name: t.value };
|
|
193
|
+
}
|
|
194
|
+
if (t.type === 'qmark') {
|
|
195
|
+
c.next();
|
|
196
|
+
return { kind: 'param', name: '?' };
|
|
197
|
+
}
|
|
198
|
+
if (t.type === 'op' && t.value === '*') {
|
|
199
|
+
c.next();
|
|
200
|
+
return { kind: 'star' };
|
|
201
|
+
}
|
|
202
|
+
if (t.type === 'lparen') {
|
|
203
|
+
c.next();
|
|
204
|
+
const inner = parseExpr(c);
|
|
205
|
+
c.expect('rparen');
|
|
206
|
+
return inner;
|
|
207
|
+
}
|
|
208
|
+
if (t.type === 'ident') {
|
|
209
|
+
const upper = t.value.toUpperCase();
|
|
210
|
+
if (upper === 'NULL') {
|
|
211
|
+
c.next();
|
|
212
|
+
return { kind: 'literal', value: null };
|
|
213
|
+
}
|
|
214
|
+
if (upper === 'TRUE' || upper === 'FALSE') {
|
|
215
|
+
c.next();
|
|
216
|
+
return { kind: 'literal', value: upper === 'TRUE' };
|
|
217
|
+
}
|
|
218
|
+
if (upper === 'CASE') {
|
|
219
|
+
return parseCase(c);
|
|
220
|
+
}
|
|
221
|
+
if (upper === 'CAST') {
|
|
222
|
+
return parseCast(c);
|
|
223
|
+
}
|
|
224
|
+
c.next();
|
|
225
|
+
if (c.peek().type === 'lparen') {
|
|
226
|
+
c.next();
|
|
227
|
+
const name = upper;
|
|
228
|
+
if (name === 'VALUES') {
|
|
229
|
+
const col = c.expect('ident');
|
|
230
|
+
c.expect('rparen');
|
|
231
|
+
return { kind: 'valuesCol', column: col.value };
|
|
232
|
+
}
|
|
233
|
+
let distinct = false;
|
|
234
|
+
let star = false;
|
|
235
|
+
const args = [];
|
|
236
|
+
if (c.peek().type === 'op' && c.peek().value === '*') {
|
|
237
|
+
c.next();
|
|
238
|
+
star = true;
|
|
239
|
+
}
|
|
240
|
+
else if (c.peek().type !== 'rparen') {
|
|
241
|
+
if (c.isKeyword('DISTINCT')) {
|
|
242
|
+
c.next();
|
|
243
|
+
distinct = true;
|
|
244
|
+
}
|
|
245
|
+
args.push(parseExpr(c));
|
|
246
|
+
while (c.peek().type === 'comma') {
|
|
247
|
+
c.next();
|
|
248
|
+
args.push(parseExpr(c));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
c.expect('rparen');
|
|
252
|
+
return { kind: 'func', name, args, distinct, star };
|
|
253
|
+
}
|
|
254
|
+
if (c.peek().type === 'dot') {
|
|
255
|
+
c.next();
|
|
256
|
+
if (c.peek().type === 'op' && c.peek().value === '*') {
|
|
257
|
+
c.next();
|
|
258
|
+
return { kind: 'star', alias: t.value };
|
|
259
|
+
}
|
|
260
|
+
const col = c.expect('ident');
|
|
261
|
+
return { kind: 'column', alias: t.value, name: col.value };
|
|
262
|
+
}
|
|
263
|
+
return { kind: 'column', name: t.value };
|
|
264
|
+
}
|
|
265
|
+
throw new Error(`[json-storage] SQL 表达式解析失败:不支持的 token(${t.type} "${t.value}")`);
|
|
266
|
+
}
|
|
267
|
+
function parseCase(c) {
|
|
268
|
+
c.expectKeyword('CASE');
|
|
269
|
+
let operand;
|
|
270
|
+
if (!c.isKeyword('WHEN')) {
|
|
271
|
+
operand = parseExpr(c);
|
|
272
|
+
}
|
|
273
|
+
const whens = [];
|
|
274
|
+
while (c.isKeyword('WHEN')) {
|
|
275
|
+
c.next();
|
|
276
|
+
const when = parseExpr(c);
|
|
277
|
+
c.expectKeyword('THEN');
|
|
278
|
+
const then = parseExpr(c);
|
|
279
|
+
whens.push({ when, then });
|
|
280
|
+
}
|
|
281
|
+
if (whens.length === 0) {
|
|
282
|
+
throw new Error('[json-storage] CASE 表达式缺少 WHEN 分支');
|
|
283
|
+
}
|
|
284
|
+
let elseExpr;
|
|
285
|
+
if (c.isKeyword('ELSE')) {
|
|
286
|
+
c.next();
|
|
287
|
+
elseExpr = parseExpr(c);
|
|
288
|
+
}
|
|
289
|
+
c.expectKeyword('END');
|
|
290
|
+
return { kind: 'case', operand, whens, else: elseExpr };
|
|
291
|
+
}
|
|
292
|
+
function parseCast(c) {
|
|
293
|
+
c.expectKeyword('CAST');
|
|
294
|
+
c.expect('lparen');
|
|
295
|
+
const operand = parseExpr(c);
|
|
296
|
+
c.expectKeyword('AS');
|
|
297
|
+
const typeTok = c.expect('ident');
|
|
298
|
+
let type = typeTok.value.toUpperCase();
|
|
299
|
+
if (c.peek().type === 'lparen') {
|
|
300
|
+
c.next();
|
|
301
|
+
while (c.peek().type !== 'rparen')
|
|
302
|
+
c.next();
|
|
303
|
+
c.expect('rparen');
|
|
304
|
+
type = `${type}(...)`;
|
|
305
|
+
}
|
|
306
|
+
c.expect('rparen');
|
|
307
|
+
return { kind: 'cast', operand, type };
|
|
308
|
+
}
|
|
309
|
+
// ---------------- 求值 ----------------
|
|
310
|
+
function truthy(v) {
|
|
311
|
+
if (v === null || v === undefined)
|
|
312
|
+
return false;
|
|
313
|
+
if (typeof v === 'boolean')
|
|
314
|
+
return v;
|
|
315
|
+
if (typeof v === 'number')
|
|
316
|
+
return v !== 0;
|
|
317
|
+
if (typeof v === 'string') {
|
|
318
|
+
const n = Number(v);
|
|
319
|
+
return !Number.isNaN(n) && n !== 0;
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
function resolveColumnValue(name, alias, ctx) {
|
|
324
|
+
if (!alias && ctx.outer && name in ctx.outer)
|
|
325
|
+
return ctx.outer[name];
|
|
326
|
+
const candidates = alias ? ctx.aliases.filter(a => a.alias === alias) : ctx.aliases;
|
|
327
|
+
for (const a of candidates) {
|
|
328
|
+
const row = ctx.row[a.alias];
|
|
329
|
+
if (!row)
|
|
330
|
+
continue;
|
|
331
|
+
const prop = a.meta?.columnByDbName.get(name) ?? name;
|
|
332
|
+
if (prop in row)
|
|
333
|
+
return row[prop];
|
|
334
|
+
if (name in row)
|
|
335
|
+
return row[name];
|
|
336
|
+
}
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
function evalParam(name, ctx) {
|
|
340
|
+
if (name === '?') {
|
|
341
|
+
const v = ctx.positional[ctx.pos.i];
|
|
342
|
+
ctx.pos.i += 1;
|
|
343
|
+
return v;
|
|
344
|
+
}
|
|
345
|
+
const key = name.replace(/^\.\.\./, '');
|
|
346
|
+
if (!(key in ctx.named)) {
|
|
347
|
+
throw new Error(`[json-storage] 缺少 SQL 参数 :${key}`);
|
|
348
|
+
}
|
|
349
|
+
return ctx.named[key];
|
|
350
|
+
}
|
|
351
|
+
function evalExpr(e, ctx) {
|
|
352
|
+
switch (e.kind) {
|
|
353
|
+
case 'literal':
|
|
354
|
+
return e.value;
|
|
355
|
+
case 'param':
|
|
356
|
+
return evalParam(e.name, ctx);
|
|
357
|
+
case 'column':
|
|
358
|
+
return resolveColumnValue(e.name, e.alias, ctx);
|
|
359
|
+
case 'star':
|
|
360
|
+
throw new Error('[json-storage] `*` 仅支持出现在 SELECT 投影或 COUNT(*) 中');
|
|
361
|
+
case 'valuesCol': {
|
|
362
|
+
if (!ctx.valuesRow) {
|
|
363
|
+
throw new Error(`[json-storage] VALUES(${e.column}) 仅支持在 ON DUPLICATE KEY UPDATE 中使用`);
|
|
364
|
+
}
|
|
365
|
+
return ctx.valuesRow[e.column];
|
|
366
|
+
}
|
|
367
|
+
case 'unary': {
|
|
368
|
+
if (e.op === 'IS NULL' || e.op === 'IS NOT NULL') {
|
|
369
|
+
const v = evalExpr(e.operand, ctx);
|
|
370
|
+
const isNull = v === null || v === undefined;
|
|
371
|
+
return e.op === 'IS NULL' ? isNull : !isNull;
|
|
372
|
+
}
|
|
373
|
+
if (e.op === 'NOT')
|
|
374
|
+
return !truthy(evalExpr(e.operand, ctx));
|
|
375
|
+
if (e.op === '-') {
|
|
376
|
+
const v = evalExpr(e.operand, ctx);
|
|
377
|
+
return v === null || v === undefined ? null : -Number(v);
|
|
378
|
+
}
|
|
379
|
+
throw new Error(`[json-storage] 不支持的一元运算符 ${e.op}`);
|
|
380
|
+
}
|
|
381
|
+
case 'binary':
|
|
382
|
+
return evalBinary(e.op, e.left, e.right, ctx);
|
|
383
|
+
case 'func':
|
|
384
|
+
return evalFunc(e, ctx);
|
|
385
|
+
case 'case': {
|
|
386
|
+
if (e.operand !== undefined) {
|
|
387
|
+
const base = evalExpr(e.operand, ctx);
|
|
388
|
+
for (const w of e.whens) {
|
|
389
|
+
if ((0, json_where_ts_1.looseEqual)(base, evalExpr(w.when, ctx)))
|
|
390
|
+
return evalExpr(w.then, ctx);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
for (const w of e.whens) {
|
|
395
|
+
if (truthy(evalExpr(w.when, ctx)))
|
|
396
|
+
return evalExpr(w.then, ctx);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return e.else ? evalExpr(e.else, ctx) : null;
|
|
400
|
+
}
|
|
401
|
+
case 'cast':
|
|
402
|
+
return castValue(evalExpr(e.operand, ctx), e.type);
|
|
403
|
+
case 'jsonPath': {
|
|
404
|
+
const v = evalExpr(e.source, ctx);
|
|
405
|
+
const extracted = jsonPathGet(v, e.path);
|
|
406
|
+
return e.unquote ? jsonUnquote(extracted) : extracted;
|
|
407
|
+
}
|
|
408
|
+
case 'interval':
|
|
409
|
+
throw new Error('[json-storage] INTERVAL 仅支持出现在 +/- 运算中');
|
|
410
|
+
default:
|
|
411
|
+
throw new Error(`[json-storage] 未知表达式节点 ${e.kind}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function evalBinary(op, leftE, rightE, ctx) {
|
|
415
|
+
if (op === 'AND') {
|
|
416
|
+
return truthy(evalExpr(leftE, ctx)) && truthy(evalExpr(rightE, ctx));
|
|
417
|
+
}
|
|
418
|
+
if (op === 'OR') {
|
|
419
|
+
return truthy(evalExpr(leftE, ctx)) || truthy(evalExpr(rightE, ctx));
|
|
420
|
+
}
|
|
421
|
+
if (op === 'LIKE' || op === 'NOT LIKE') {
|
|
422
|
+
const l = evalExpr(leftE, ctx);
|
|
423
|
+
const r = evalExpr(rightE, ctx);
|
|
424
|
+
const ok = (0, json_where_ts_1.likeMatch)(String(l ?? ''), String(r ?? ''));
|
|
425
|
+
return op === 'LIKE' ? ok : !ok;
|
|
426
|
+
}
|
|
427
|
+
if (['+', '-', '*', '/', '%'].includes(op)) {
|
|
428
|
+
// INTERVAL 只能作 +/- 右操作数:先判型再求值(interval 节点单独 eval 会抛错)
|
|
429
|
+
if ((op === '+' || op === '-') && rightE.kind === 'interval') {
|
|
430
|
+
const date = toDate(evalExpr(leftE, ctx));
|
|
431
|
+
if (date)
|
|
432
|
+
return dateAdd(date, op === '-' ? -1 : 1, evalExpr(rightE.value, ctx), rightE.unit);
|
|
433
|
+
}
|
|
434
|
+
const l = evalExpr(leftE, ctx);
|
|
435
|
+
const r = evalExpr(rightE, ctx);
|
|
436
|
+
if (l === null || l === undefined || r === null || r === undefined)
|
|
437
|
+
return null;
|
|
438
|
+
const ln = Number(l);
|
|
439
|
+
const rn = Number(r);
|
|
440
|
+
switch (op) {
|
|
441
|
+
case '+':
|
|
442
|
+
return ln + rn;
|
|
443
|
+
case '-':
|
|
444
|
+
return ln - rn;
|
|
445
|
+
case '*':
|
|
446
|
+
return ln * rn;
|
|
447
|
+
case '/':
|
|
448
|
+
return rn === 0 ? null : ln / rn;
|
|
449
|
+
case '%':
|
|
450
|
+
return rn === 0 ? null : ln % rn;
|
|
451
|
+
default:
|
|
452
|
+
throw new Error(`[json-storage] 不支持的运算符 ${op}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (CMP_OPS.includes(op)) {
|
|
456
|
+
const l = evalExpr(leftE, ctx);
|
|
457
|
+
const r = evalExpr(rightE, ctx);
|
|
458
|
+
if (l === null || l === undefined || r === null || r === undefined)
|
|
459
|
+
return false;
|
|
460
|
+
const cmp = (0, json_where_ts_1.compareValues)(l, r);
|
|
461
|
+
switch (op) {
|
|
462
|
+
case '=':
|
|
463
|
+
return cmp === 0;
|
|
464
|
+
case '!=':
|
|
465
|
+
return cmp !== 0;
|
|
466
|
+
case '<':
|
|
467
|
+
return cmp < 0;
|
|
468
|
+
case '>':
|
|
469
|
+
return cmp > 0;
|
|
470
|
+
case '<=':
|
|
471
|
+
return cmp <= 0;
|
|
472
|
+
case '>=':
|
|
473
|
+
return cmp >= 0;
|
|
474
|
+
default:
|
|
475
|
+
throw new Error(`[json-storage] 不支持的比较运算符 ${op}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
throw new Error(`[json-storage] 不支持的二元运算符 ${op}`);
|
|
479
|
+
}
|
|
480
|
+
function evalFunc(e, ctx) {
|
|
481
|
+
const name = e.name;
|
|
482
|
+
if (exports.AGG_FUNCS.has(name)) {
|
|
483
|
+
if (!ctx.groupRows) {
|
|
484
|
+
throw new Error(`[json-storage] 聚合函数 ${name}() 仅支持出现在 SELECT/HAVING/ORDER BY 中`);
|
|
485
|
+
}
|
|
486
|
+
const members = ctx.groupRows;
|
|
487
|
+
if (name === 'COUNT') {
|
|
488
|
+
if (e.star)
|
|
489
|
+
return members.length;
|
|
490
|
+
return aggArgValues(e, ctx).filter(v => v !== null && v !== undefined).length;
|
|
491
|
+
}
|
|
492
|
+
const raw = aggArgValues(e, ctx).filter(v => v !== null && v !== undefined);
|
|
493
|
+
if (name === 'SUM' || name === 'AVG') {
|
|
494
|
+
// 数值聚合:非数值字符串按 0 计(MySQL 隐式转换语义)。
|
|
495
|
+
// Number('abc') 为 NaN,须显式归一,否则单个脏值让整个聚合变 NaN
|
|
496
|
+
const vals = raw.map(v => {
|
|
497
|
+
const n = Number(v);
|
|
498
|
+
return Number.isNaN(n) ? 0 : n;
|
|
499
|
+
});
|
|
500
|
+
if (vals.length === 0)
|
|
501
|
+
return null;
|
|
502
|
+
const sum = vals.reduce((s, n) => s + n, 0);
|
|
503
|
+
return name === 'SUM' ? sum : sum / vals.length;
|
|
504
|
+
}
|
|
505
|
+
// MIN/MAX 保留原值比较:字符串按字典序、日期按时间序(与 MySQL 一致),
|
|
506
|
+
// 不做 Number() 强转以免非数值字符串(如标题)被折算成 NaN
|
|
507
|
+
if (name === 'MIN')
|
|
508
|
+
return raw.length === 0 ? null : raw.reduce((m, v) => ((0, json_where_ts_1.compareValues)(v, m) < 0 ? v : m));
|
|
509
|
+
return raw.length === 0 ? null : raw.reduce((m, v) => ((0, json_where_ts_1.compareValues)(v, m) > 0 ? v : m));
|
|
510
|
+
}
|
|
511
|
+
if (name === 'IN' || name === 'NOT IN') {
|
|
512
|
+
const l = evalExpr(e.args[0], ctx);
|
|
513
|
+
let values = [];
|
|
514
|
+
for (let i = 1; i < e.args.length; i += 1) {
|
|
515
|
+
const a = e.args[i];
|
|
516
|
+
const v = evalExpr(a, ctx);
|
|
517
|
+
if (a.kind === 'param' && a.name.startsWith('...')) {
|
|
518
|
+
values = values.concat(Array.isArray(v) ? v : [v]);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
values.push(v);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const ok = values.some(v => (0, json_where_ts_1.looseEqual)(l, v));
|
|
525
|
+
return name === 'IN' ? ok : !ok;
|
|
526
|
+
}
|
|
527
|
+
if (name === 'BETWEEN' || name === 'NOT BETWEEN') {
|
|
528
|
+
const v = evalExpr(e.args[0], ctx);
|
|
529
|
+
if (v === null || v === undefined)
|
|
530
|
+
return false;
|
|
531
|
+
const low = evalExpr(e.args[1], ctx);
|
|
532
|
+
const high = evalExpr(e.args[2], ctx);
|
|
533
|
+
const inRange = (0, json_where_ts_1.compareValues)(v, low) >= 0 && (0, json_where_ts_1.compareValues)(v, high) <= 0;
|
|
534
|
+
return name === 'BETWEEN' ? inRange : !inRange;
|
|
535
|
+
}
|
|
536
|
+
const args = e.args.map(a => evalExpr(a, ctx));
|
|
537
|
+
switch (name) {
|
|
538
|
+
case 'COALESCE': {
|
|
539
|
+
for (const v of args) {
|
|
540
|
+
if (v !== null && v !== undefined)
|
|
541
|
+
return v;
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
case 'NULLIF':
|
|
546
|
+
return (0, json_where_ts_1.looseEqual)(args[0], args[1]) ? null : args[0];
|
|
547
|
+
case 'IF':
|
|
548
|
+
return truthy(args[0]) ? args[1] : args[2];
|
|
549
|
+
case 'IFNULL':
|
|
550
|
+
return args[0] === null || args[0] === undefined ? args[1] : args[0];
|
|
551
|
+
case 'JSON_EXTRACT': {
|
|
552
|
+
if (e.args.length !== 2)
|
|
553
|
+
throw new Error('[json-storage] JSON_EXTRACT 需要 2 个参数');
|
|
554
|
+
// 用已求值的 args[1]:不得二次求值(会多消费一个位置参数,使后续 ? 全部左移)
|
|
555
|
+
return jsonPathGet(args[0], String(args[1]));
|
|
556
|
+
}
|
|
557
|
+
case 'JSON_UNQUOTE':
|
|
558
|
+
return jsonUnquote(args[0]);
|
|
559
|
+
case 'NOW':
|
|
560
|
+
case 'CURRENT_TIMESTAMP':
|
|
561
|
+
case 'LOCALTIME':
|
|
562
|
+
case 'LOCALTIMESTAMP':
|
|
563
|
+
return new Date();
|
|
564
|
+
case 'UNIX_TIMESTAMP':
|
|
565
|
+
return args.length === 0 ? Math.floor(Date.now() / 1000) : Math.floor(toTs(args[0]) / 1000);
|
|
566
|
+
case 'FROM_UNIXTIME':
|
|
567
|
+
return new Date(Number(args[0]) * 1000);
|
|
568
|
+
// 已知语义(见设计 Risks):DATE_FORMAT/FROM_UNIXTIME 输出按「进程本地时区」格式化,
|
|
569
|
+
// 而落盘用 UTC ISO;仅当服务 TZ 与 MySQL 会话 TZ 一致时与真实库等价,跨 TZ 导入会偏移。
|
|
570
|
+
case 'DATE_FORMAT':
|
|
571
|
+
return dateFormat(toDate(args[0]), String(args[1]));
|
|
572
|
+
case 'TIMESTAMPDIFF': {
|
|
573
|
+
const unitTok = e.args[0];
|
|
574
|
+
const unit = unitTok.kind === 'column' ? unitTok.name.toUpperCase() : String(args[0]).toUpperCase();
|
|
575
|
+
const diffMs = toTs(args[2]) - toTs(args[1]);
|
|
576
|
+
switch (unit) {
|
|
577
|
+
case 'SECOND':
|
|
578
|
+
return Math.floor(diffMs / 1000);
|
|
579
|
+
case 'MINUTE':
|
|
580
|
+
return Math.floor(diffMs / 60000);
|
|
581
|
+
case 'HOUR':
|
|
582
|
+
return Math.floor(diffMs / 3600000);
|
|
583
|
+
case 'DAY':
|
|
584
|
+
return Math.floor(diffMs / 86400000);
|
|
585
|
+
case 'WEEK':
|
|
586
|
+
return Math.floor(diffMs / 604800000);
|
|
587
|
+
default:
|
|
588
|
+
throw new Error(`[json-storage] 不支持的 TIMESTAMPDIFF 单位:${unit}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
case 'DATEDIFF': {
|
|
592
|
+
const a = toDate(args[0]);
|
|
593
|
+
const b = toDate(args[1]);
|
|
594
|
+
if (!a || !b)
|
|
595
|
+
return null;
|
|
596
|
+
// MySQL DATEDIFF(expr1, expr2) = expr1 - expr2
|
|
597
|
+
return Math.floor((startOfDay(a).getTime() - startOfDay(b).getTime()) / 86400000);
|
|
598
|
+
}
|
|
599
|
+
case 'ABS':
|
|
600
|
+
return args[0] === null || args[0] === undefined ? null : Math.abs(Number(args[0]));
|
|
601
|
+
case 'ROUND':
|
|
602
|
+
return args[0] === null || args[0] === undefined
|
|
603
|
+
? null
|
|
604
|
+
: Number(Number(args[0]).toFixed(args.length > 1 ? Number(args[1]) : 0));
|
|
605
|
+
case 'FLOOR':
|
|
606
|
+
return args[0] === null || args[0] === undefined ? null : Math.floor(Number(args[0]));
|
|
607
|
+
case 'CEIL':
|
|
608
|
+
case 'CEILING':
|
|
609
|
+
return args[0] === null || args[0] === undefined ? null : Math.ceil(Number(args[0]));
|
|
610
|
+
case 'CONCAT':
|
|
611
|
+
return args.map(v => (v === null || v === undefined ? '' : String(v))).join('');
|
|
612
|
+
case 'CONCAT_WS': {
|
|
613
|
+
const sep = String(args[0]);
|
|
614
|
+
return args
|
|
615
|
+
.slice(1)
|
|
616
|
+
.filter(v => v !== null && v !== undefined)
|
|
617
|
+
.map(v => String(v))
|
|
618
|
+
.join(sep);
|
|
619
|
+
}
|
|
620
|
+
case 'LOWER':
|
|
621
|
+
case 'LCASE':
|
|
622
|
+
return args[0] === null || args[0] === undefined ? null : String(args[0]).toLowerCase();
|
|
623
|
+
case 'UPPER':
|
|
624
|
+
case 'UCASE':
|
|
625
|
+
return args[0] === null || args[0] === undefined ? null : String(args[0]).toUpperCase();
|
|
626
|
+
case 'LENGTH':
|
|
627
|
+
case 'CHAR_LENGTH':
|
|
628
|
+
case 'CHARACTER_LENGTH':
|
|
629
|
+
return args[0] === null || args[0] === undefined ? null : String(args[0]).length;
|
|
630
|
+
case 'SUBSTRING':
|
|
631
|
+
case 'SUBSTR': {
|
|
632
|
+
const s = String(args[0] ?? '');
|
|
633
|
+
const from = Number(args[1]) || 1;
|
|
634
|
+
const startIdx = from > 0 ? from - 1 : Math.max(0, s.length + from);
|
|
635
|
+
if (e.args.length > 2)
|
|
636
|
+
return s.substr(startIdx, Number(args[2]));
|
|
637
|
+
return s.slice(startIdx);
|
|
638
|
+
}
|
|
639
|
+
case 'TRIM':
|
|
640
|
+
return args[0] === null || args[0] === undefined ? null : String(args[0]).trim();
|
|
641
|
+
case 'DATABASE':
|
|
642
|
+
return 'json';
|
|
643
|
+
default:
|
|
644
|
+
throw new Error(`[json-storage] 不支持的 SQL 函数:${name}()`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function aggArgValues(e, ctx) {
|
|
648
|
+
const arg = e.args[0];
|
|
649
|
+
const vals = ctx.groupRows.map(member =>
|
|
650
|
+
// 逐成员新建 pos:与行级 evalCtxFor 一致,使占位符对组内所有成员绑定同一值
|
|
651
|
+
// (浅拷贝会共享游标,导致首成员消费参数、后续成员读到 undefined 而结果偏小)
|
|
652
|
+
evalExpr(arg, { ...ctx, pos: { i: 0 }, row: member, groupRows: undefined, outer: undefined }));
|
|
653
|
+
if (!e.distinct)
|
|
654
|
+
return vals;
|
|
655
|
+
const seen = new Set();
|
|
656
|
+
const out = [];
|
|
657
|
+
for (const v of vals) {
|
|
658
|
+
const key = v instanceof Date ? v.toISOString() : JSON.stringify(v === undefined ? null : v);
|
|
659
|
+
if (!seen.has(key)) {
|
|
660
|
+
seen.add(key);
|
|
661
|
+
out.push(v);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
// ---------------- 值工具 ----------------
|
|
667
|
+
function toDate(v) {
|
|
668
|
+
if (v instanceof Date)
|
|
669
|
+
return Number.isNaN(v.getTime()) ? null : v;
|
|
670
|
+
if (typeof v === 'number')
|
|
671
|
+
return new Date(v);
|
|
672
|
+
if (typeof v === 'string' && v.trim() !== '') {
|
|
673
|
+
const d = new Date(v.replace(' ', 'T'));
|
|
674
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
675
|
+
}
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
function toTs(v) {
|
|
679
|
+
const d = toDate(v);
|
|
680
|
+
return d ? d.getTime() : 0;
|
|
681
|
+
}
|
|
682
|
+
function startOfDay(d) {
|
|
683
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
684
|
+
}
|
|
685
|
+
function dateAdd(date, sign, value, unit) {
|
|
686
|
+
const n = Number(value) * sign;
|
|
687
|
+
const d = new Date(date.getTime());
|
|
688
|
+
switch (unit) {
|
|
689
|
+
case 'SECOND':
|
|
690
|
+
d.setSeconds(d.getSeconds() + n);
|
|
691
|
+
return d;
|
|
692
|
+
case 'MINUTE':
|
|
693
|
+
d.setMinutes(d.getMinutes() + n);
|
|
694
|
+
return d;
|
|
695
|
+
case 'HOUR':
|
|
696
|
+
d.setHours(d.getHours() + n);
|
|
697
|
+
return d;
|
|
698
|
+
case 'DAY':
|
|
699
|
+
case 'WEEK':
|
|
700
|
+
d.setDate(d.getDate() + n * (unit === 'WEEK' ? 7 : 1));
|
|
701
|
+
return d;
|
|
702
|
+
case 'MONTH':
|
|
703
|
+
case 'QUARTER':
|
|
704
|
+
d.setMonth(d.getMonth() + n * (unit === 'QUARTER' ? 3 : 1));
|
|
705
|
+
return d;
|
|
706
|
+
case 'YEAR':
|
|
707
|
+
d.setFullYear(d.getFullYear() + n);
|
|
708
|
+
return d;
|
|
709
|
+
default:
|
|
710
|
+
throw new Error(`[json-storage] 不支持的 INTERVAL 单位:${unit}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
function castValue(v, type) {
|
|
714
|
+
if (v === null || v === undefined)
|
|
715
|
+
return null;
|
|
716
|
+
if (type.startsWith('UNSIGNED') || type.startsWith('SIGNED') || type.startsWith('DECIMAL') || type.startsWith('INT')) {
|
|
717
|
+
const n = Number(v);
|
|
718
|
+
return Number.isNaN(n) ? 0 : n;
|
|
719
|
+
}
|
|
720
|
+
if (type.startsWith('CHAR') || type.startsWith('VARCHAR') || type.startsWith('TEXT')) {
|
|
721
|
+
return v instanceof Date ? mysqlDatetime(v) : String(v);
|
|
722
|
+
}
|
|
723
|
+
if (type.startsWith('DATETIME') || type.startsWith('TIMESTAMP')) {
|
|
724
|
+
return toDate(v);
|
|
725
|
+
}
|
|
726
|
+
if (type.startsWith('DATE')) {
|
|
727
|
+
const d = toDate(v);
|
|
728
|
+
return d ? startOfDay(d) : null;
|
|
729
|
+
}
|
|
730
|
+
if (type.startsWith('JSON')) {
|
|
731
|
+
if (typeof v === 'string') {
|
|
732
|
+
try {
|
|
733
|
+
return JSON.parse(v);
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
return v;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return v;
|
|
740
|
+
}
|
|
741
|
+
throw new Error(`[json-storage] 不支持的 CAST 类型:${type}`);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* MySQL datetime 文本(本地时区格式化)。
|
|
745
|
+
* 已知语义:落盘存储为 UTC ISO,此函数按进程本地时区格式化;仅当服务 TZ 与 MySQL 会话 TZ 一致时等价。
|
|
746
|
+
*/
|
|
747
|
+
function mysqlDatetime(d) {
|
|
748
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
749
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
750
|
+
}
|
|
751
|
+
/** JSON 路径取值:`$.a.b[0].c`;`[*]` 通配符收集为数组(MySQL JSON_EXTRACT 语义);缺失返回 null(SQL NULL) */
|
|
752
|
+
function jsonPathGet(value, path) {
|
|
753
|
+
if (value === null || value === undefined)
|
|
754
|
+
return null;
|
|
755
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
756
|
+
try {
|
|
757
|
+
value = JSON.parse(value);
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return walkJsonPath(value, parseJsonPathSegments(path));
|
|
764
|
+
}
|
|
765
|
+
function parseJsonPathSegments(path) {
|
|
766
|
+
return path
|
|
767
|
+
.replace(/^\$/, '')
|
|
768
|
+
.split('.')
|
|
769
|
+
.filter(s => s !== '')
|
|
770
|
+
.map(seg => {
|
|
771
|
+
const m = seg.match(/^([^[]*)((?:\[\d+\]|\[\*\])*)$/);
|
|
772
|
+
const key = m ? m[1] : seg;
|
|
773
|
+
const indices = [];
|
|
774
|
+
const idxPart = m ? m[2] : '';
|
|
775
|
+
for (const idx of idxPart.match(/\[(\d+|\*)\]/g) ?? []) {
|
|
776
|
+
const inner = idx.slice(1, -1);
|
|
777
|
+
indices.push(inner === '*' ? '*' : Number(inner));
|
|
778
|
+
}
|
|
779
|
+
return { key, indices };
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
function walkJsonPath(cur, segs) {
|
|
783
|
+
if (segs.length === 0)
|
|
784
|
+
return cur === undefined ? null : cur;
|
|
785
|
+
if (cur === null || cur === undefined)
|
|
786
|
+
return null;
|
|
787
|
+
const [seg, ...rest] = segs;
|
|
788
|
+
let next = cur;
|
|
789
|
+
if (seg.key) {
|
|
790
|
+
next = cur[seg.key];
|
|
791
|
+
if (next === undefined)
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
for (const idx of seg.indices) {
|
|
795
|
+
if (next === null || next === undefined)
|
|
796
|
+
return null;
|
|
797
|
+
if (idx === '*') {
|
|
798
|
+
if (!Array.isArray(next))
|
|
799
|
+
return null;
|
|
800
|
+
// 通配符:对每个元素继续走剩余路径,收集非 null 结果为数组
|
|
801
|
+
return next
|
|
802
|
+
.map(el => walkJsonPath(el, rest))
|
|
803
|
+
.filter(v => v !== null && v !== undefined);
|
|
804
|
+
}
|
|
805
|
+
next = next[idx];
|
|
806
|
+
if (next === undefined)
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
return walkJsonPath(next, rest);
|
|
810
|
+
}
|
|
811
|
+
function jsonUnquote(v) {
|
|
812
|
+
if (typeof v !== 'string')
|
|
813
|
+
return v;
|
|
814
|
+
return v.length >= 2 && v.startsWith('"') && v.endsWith('"') ? v.slice(1, -1) : v;
|
|
815
|
+
}
|
|
816
|
+
/** MySQL DATE_FORMAT 常用模式 */
|
|
817
|
+
function dateFormat(d, fmt) {
|
|
818
|
+
if (!d)
|
|
819
|
+
return null;
|
|
820
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
821
|
+
return fmt.replace(/%[YymdHkhisfMp]/g, m => {
|
|
822
|
+
switch (m) {
|
|
823
|
+
case '%Y':
|
|
824
|
+
return String(d.getFullYear());
|
|
825
|
+
case '%y':
|
|
826
|
+
return String(d.getFullYear()).slice(-2);
|
|
827
|
+
case '%m':
|
|
828
|
+
return p(d.getMonth() + 1);
|
|
829
|
+
case '%c':
|
|
830
|
+
return String(d.getMonth() + 1);
|
|
831
|
+
case '%d':
|
|
832
|
+
return p(d.getDate());
|
|
833
|
+
case '%e':
|
|
834
|
+
return String(d.getDate());
|
|
835
|
+
case '%H':
|
|
836
|
+
return p(d.getHours());
|
|
837
|
+
case '%k':
|
|
838
|
+
return String(d.getHours());
|
|
839
|
+
case '%h':
|
|
840
|
+
return p(d.getHours() % 12 || 12);
|
|
841
|
+
case '%i':
|
|
842
|
+
return p(d.getMinutes());
|
|
843
|
+
case '%s':
|
|
844
|
+
case '%S':
|
|
845
|
+
return p(d.getSeconds());
|
|
846
|
+
case '%f':
|
|
847
|
+
return String(d.getMilliseconds() * 1000);
|
|
848
|
+
case '%M':
|
|
849
|
+
return ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][d.getMonth()];
|
|
850
|
+
case '%p':
|
|
851
|
+
return d.getHours() < 12 ? 'AM' : 'PM';
|
|
852
|
+
default:
|
|
853
|
+
return m;
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
//# sourceMappingURL=json-sql-expr.js.map
|