Skip to content

Commit 6cb447f

Browse files
committed
feat: 增强函数支持与异常诊断
- 补充 JSON、时间、字符串等常用函数与 select 列解析场景 - 引入结构化 ReactorQLException 和中英文资源,统一用户可见错误建议 - 增强 ORDER BY、merge_by_key、窗口、聚合、JSON 等异常与安全限制测试
1 parent 7cdc1a3 commit 6cb447f

39 files changed

Lines changed: 1729 additions & 178 deletions

docs/plans/json-functions-support.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
## 目标
1010

1111
- 增加常用数值函数:`round``floor``ceil``abs``sqrt``pow``power`
12-
- 增加常用字符串和正则函数:`lower``upper``length``char_length``trim``ltrim``rtrim``replace``substring``regexp_replace``regexp_like``regexp_extract``regexp_substr`
12+
- 增加常用字符串和正则函数:`lower``upper``length``char_length``trim``ltrim``rtrim``replace``substring``concat_ws``contains``instr``locate``lpad``rpad``regexp_replace``regexp_like``regexp_extract``regexp_substr`
13+
- 增加常用时间序列处理函数:`date_trunc``time_bucket``to_unixtime``to_millis` / `epoch_ms`,并补充 `date_part` / `date_add` / `date_diff``quarter``week`、毫秒等常见单位。
1314
- 增加 JSONPath 提取函数:`json_get``json_path``json_extract``json_value``json_query``json_exists`
1415
- 覆盖 MySQL 常见 JSON 函数名:`json_unquote``json_type``json_valid``json_length``json_keys``json_contains``json_contains_path``json_overlaps``json_array``json_object``json_merge``json_merge_preserve``json_merge_patch`
1516
- 覆盖 PostgreSQL 常见 JSON 函数名:`json_extract_path``json_extract_path_text``jsonb_extract_path``jsonb_extract_path_text``json_array_length``jsonb_array_length``json_object_keys``jsonb_object_keys``json_typeof``to_json`
@@ -60,8 +61,9 @@
6061
- 拒绝典型嵌套量词,例如 `(a+)+``(.*){...}` 这类容易导致灾难性回溯的表达式。
6162
- 保留 Java 正则的常用能力,包括捕获组、大小写标志和 `$1` 替换。
6263
- 字符串生成函数:
63-
- `repeat``replace``regexp_replace` 的结果默认长度限制为 1MB;setting key:`function.maxGeneratedStringLength`
64+
- `repeat``replace``regexp_replace``concat_ws``lpad``rpad` 的结果默认长度限制为 1MB;setting key:`function.maxGeneratedStringLength`
6465
- `split_part` 不再通过 `String#split` 构造完整数组,按目标下标从前或从后扫描,避免大字符串被分隔成大量中间对象。
66+
- `lpad` / `rpad` 对空 padding 返回空值,避免为补齐目标长度进入无意义循环;目标长度仍受硬上限保护。
6567
- 错误语义:
6668
- 明显危险或资源超限的参数抛出 `UnsupportedOperationException`,让调用方能定位 SQL 参数问题。
6769
- 常规业务边界,例如 `regexp_extract` 捕获组不存在,返回空值,避免因下标误用中断整条查询。
@@ -92,6 +94,12 @@
9294
- `json_keys(json[, path])` / `json_object_keys(json)`:对象返回 key 数组,非对象返回空数组。
9395
- `json_array(...)`:返回参数数组。
9496
- `json_object(k1, v1, k2, v2, ...)`:返回对象;奇数参数时忽略最后一个孤立 key。
97+
- `concat_ws(separator, value...)`:按 MySQL / PostgreSQL / Spark 常见语义拼接,跳过空值;`separator` 为空时返回空值。
98+
- `contains(string, substring)` / `instr(string, substring)` / `locate(substring, string[, position])`:补充 Trino、MySQL、Spark 常用字符串检索函数,位置返回 1-based,未命中返回 0。
99+
- `lpad(string, length, pad)` / `rpad(string, length, pad)`:按常见 SQL 行为补齐或截断到目标长度;目标长度小于等于 0 时返回空字符串,`pad` 为空时返回空值。
100+
- `date_trunc(unit, timestamp)`:按 PostgreSQL / Trino / DuckDB 常见语义截断到 `year``quarter``month``week``day``hour``minute``second``millisecond``microsecond``week` 使用 ISO 周一作为起点。
101+
- `time_bucket(interval, timestamp)`:按 DuckDB / Timescale 类时间桶语义返回时间桶起点;`interval` 支持毫秒数字、`1m` / `15 minutes` / `PT1M` 等 Duration 写法,必须大于 0。
102+
- `to_unixtime(timestamp)` 返回 Unix 秒(含毫秒小数),`to_millis(timestamp)` / `epoch_ms(timestamp)` 返回 Unix 毫秒。
95103

96104
## 数据库同名函数语义对齐
97105

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# ReactorQL 结构化异常诊断开发计划
2+
3+
## 背景
4+
5+
当前 ReactorQL 大量错误直接抛出 `UnsupportedOperationException`,错误信息通常只有“Unsupported expression”或“参数数量错误”。
6+
这类自由文本不利于用户定位 SQL 问题,也不利于大模型根据错误自动修正查询。
7+
8+
## 目标
9+
10+
- 保留现有 `UnsupportedOperationException` 兼容性,不破坏外部调用方和既有测试。
11+
- 为 SQL 解析、编译、函数参数、资源限制、安全限制等错误提供结构化诊断信息。
12+
- 尽可能提供 SQL 表达式、行号、列号、原因、建议和示例。
13+
- 提供国际化扩展点:错误码、默认文案参数,以及默认中英文资源。
14+
- `suggestion``example` 只面向 SQL 使用者提供修复建议或支持用法,不暴露类名、注册表、解析树、执行器、缓存等实现细节。
15+
16+
## 异常模型
17+
18+
新增 `ReactorQLException extends UnsupportedOperationException`,核心字段:
19+
20+
- `i18nCode`:稳定错误码,例如 `error.reactorql.function_argument_count`
21+
- `i18nArgs`:用于平台或调用方国际化渲染的参数。
22+
- `line` / `column`:SQL 起始位置;能从 JSqlParser AST 或 ParseException 取得时填充。
23+
- `expression`:出错 SQL 表达式或原始 SQL。
24+
- `reason`:具体失败原因。
25+
- `suggestion`:推荐修复方向,只描述支持用法或安全边界。
26+
- `example`:可复制的正确写法。
27+
28+
`getMessage()` 输出默认英文诊断文本,`getLocalizedMessage()` 通过 `ResourceBundle` 读取 `i18n/reactorql/messages_*.properties`
29+
JetLinks 平台侧也可以直接读取 `getI18nCode()` / `getI18nArgs()`,再走平台 `LocaleUtils` 或统一错误响应体系。
30+
31+
## 首批错误码
32+
33+
- `error.reactorql.syntax`:SQL 解析失败。
34+
- `error.reactorql.unsupported_expression`:不支持的 select/value 表达式。
35+
- `error.reactorql.unsupported_condition`:不支持的 where/having 条件。
36+
- `error.reactorql.unsupported_group_expression`:不支持的 group by 表达式。
37+
- `error.reactorql.unsupported_from`:不支持的 from 表达式。
38+
- `error.reactorql.unsupported_flat_map`:不支持的列转行表达式。
39+
- `error.reactorql.function_argument_count`:函数参数数量错误。
40+
- `error.reactorql.invalid_argument`:函数参数值、setting 或安全限制错误。
41+
- `error.reactorql.resource_limit`:输入行数、JSON 文本、输出长度、窗口大小等资源限制错误。
42+
43+
## 位置信息策略
44+
45+
- 编译期表达式:利用 JSqlParser 4.6 的 `ASTNodeAccess#getASTNode()``SimpleNode#jjtGetFirstToken()` 读取行列。
46+
- 解析期错误:从 `ParseException.currentToken.next` 读取行列。
47+
- 当前 `SqlParserUtils.quoteNonAsciiAliases(...)` 可能改写 SQL。第一阶段只保证未改写或位置未受影响的 SQL 能返回准确行列;后续如需精准映射,可为 SQL 改写过程补 offset mapping。
48+
49+
## 落地阶段
50+
51+
### 阶段 1:核心模型和高频入口
52+
53+
已覆盖:
54+
55+
- `DefaultReactorQLMetadata(String sql)`:SQL 解析失败包装成 `ReactorQLException`
56+
- `ValueMapFeature#createMapperNow`:不支持的 value/select 表达式。
57+
- `FilterFeature#createPredicateNow`:不支持的 where/having 条件。
58+
- `FunctionMapFeature`:函数参数缺失和数量错误。
59+
- `DefaultReactorQL`:不支持的 select 表达式和 group by 表达式。
60+
- 常见资源/安全参数错误:正则风险、字符串长度、setting、日期单位、`time_bucket` interval。
61+
62+
### 阶段 2:继续替换重点函数
63+
64+
已覆盖:
65+
66+
- JSONPath 相关安全错误:`JsonFunctionSupport``JsonValueSupport`
67+
- `DateFormatFeature``SingleParameterFunctionMapFeature``CoalesceMapFeature`
68+
- `OrderBySupport` 的排序窗口、topN、setting 类型和值域限制。
69+
- `MergeByKeyFeature` 的参数、setting、排序、重复键和单键行数限制。
70+
- 聚合和列转行的参数错误:`MapAggFeature``CollectListAggFeature``CollectRowAggMapFeature``ArrayValueFlatMapFeature`
71+
- FROM 组合函数和子查询集合操作错误:`zip``combine`、子查询 set operation。
72+
- `if``_window` 等常用函数的参数数量和值域错误。
73+
74+
文案边界:
75+
76+
- `reason` 可以描述失败原因,但不要求用户理解内部扩展点。
77+
- `suggestion` 只给出可执行建议,例如“使用 FROM 子句”“增加 LIMIT”“使用简单 JSONPath”“使用 _window('1m')”。
78+
- `example` 给出 SQL 或 setting 写法,不展示内部类名、方法名、缓存、解析树或执行器。
79+
80+
### 阶段 3:运行时上下文增强
81+
82+
- 对运行期函数求值失败增加 row/parameter 上下文,避免只看到底层 `TypeCastException``ArithmeticException`
83+
- 对响应式链路中传播的异常保留原始 cause 和 SQL 表达式。
84+
- 评估是否为 `TypeCastException` 增加结构化诊断字段,或在 ValueMap/Filter 边界统一包装。
85+
86+
## 测试要求
87+
88+
- 断言异常仍然是 `UnsupportedOperationException` 的子类。
89+
- 断言错误码、表达式、原因、建议、示例字段存在。
90+
- 断言语法错误和表达式错误尽量带行列信息。
91+
- 断言 `getLocalizedMessage()` 可以读取英文和中文资源。
92+
- 断言新增错误码在中英文资源中存在。
93+
- 断言 JSON 非安全路径、ORDER BY 资源限制、日期格式、窗口参数、merge_by_key 参数和运行期排序校验等场景使用结构化异常。
94+
- 断言对外建议不包含实现细节关键词。
95+
- 现有错误场景测试保持通过。
96+
97+
当前验证结果:
98+
99+
- `mvn -q test`:260 tests, 0 failures, 0 errors, 0 skipped。
100+
- JaCoCo:instruction 93.57%,branch 81.43%,line 94.52%,不低于基准分支覆盖率。
101+
- `git diff --check`:通过。

src/main/java/org/jetlinks/reactor/ql/DefaultReactorQL.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import net.sf.jsqlparser.schema.Table;
2525
import net.sf.jsqlparser.statement.select.*;
2626
import org.apache.commons.collections.CollectionUtils;
27+
import org.jetlinks.reactor.ql.exception.ReactorQLException;
2728
import org.jetlinks.reactor.ql.feature.*;
2829
import org.jetlinks.reactor.ql.supports.DefaultReactorQLMetadata;
2930
import org.jetlinks.reactor.ql.utils.CastUtils;
@@ -218,7 +219,7 @@ else if ((from instanceof Table)) {
218219
.addRecords(left.getRecords(false)));
219220
}
220221
if (rightStreamGetter == null) {
221-
throw new UnsupportedOperationException("不支持的表关联: " + from);
222+
throw ReactorQLException.unsupportedFrom(from);
222223
}
223224
Function<ReactorQLRecord, Flux<ReactorQLRecord>> fiRightStreamGetter = rightStreamGetter;
224225
if (joinInfo.isLeft()) {
@@ -328,7 +329,7 @@ else if (groupByExpression instanceof BinaryExpression) {
328329
metadata.getFeatureNow(FeatureId.GroupBy.of(((BinaryExpression) groupByExpression).getStringExpression()),
329330
groupByExpression::toString));
330331
} else {
331-
throw new UnsupportedOperationException("Unsupported group expression:" + groupByExpression);
332+
throw ReactorQLException.unsupportedGroupExpression(groupByExpression);
332333
}
333334
}
334335

@@ -426,7 +427,11 @@ public void visit(SelectExpressionItem item) {
426427
.ifPresent(mapper -> flatMappers.put(fAlias, mapper));
427428

428429
if (!mappers.containsKey(fAlias) && !aggMapper.containsKey(fAlias) && !flatMappers.containsKey(fAlias)) {
429-
throw new UnsupportedOperationException("Unsupported expression:" + expression);
430+
throw ReactorQLException.unsupportedExpression(
431+
expression,
432+
"select 列必须是普通表达式、聚合函数或当前支持的列转行函数;表函数应放到 FROM 子句中使用。",
433+
"select count(1) total, date_trunc('minute', timestamp) ts from test group by date_trunc('minute', timestamp)"
434+
);
430435
}
431436
}
432437

src/main/java/org/jetlinks/reactor/ql/OrderBySupport.java

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import net.sf.jsqlparser.statement.select.Limit;
2020
import net.sf.jsqlparser.statement.select.OrderByElement;
2121
import org.apache.commons.collections.CollectionUtils;
22+
import org.jetlinks.reactor.ql.exception.ReactorQLException;
2223
import org.jetlinks.reactor.ql.feature.ValueMapFeature;
2324
import org.jetlinks.reactor.ql.utils.CastUtils;
2425
import org.jetlinks.reactor.ql.utils.CompareUtils;
@@ -229,8 +230,11 @@ private static Flux<ReactorQLRecord> limitSortRows(Flux<ReactorQLRecord> flux, l
229230
.index()
230231
.handle((Tuple2<Long, ReactorQLRecord> tuple, SynchronousSink<ReactorQLRecord> sink) -> {
231232
if (tuple.getT1() >= maxRows) {
232-
sink.error(new UnsupportedOperationException("order by rows exceeded setting["
233-
+ DefaultReactorQL.SETTING_ORDER_BY_MAX_ROWS + "]:" + maxRows));
233+
sink.error(ReactorQLException.resourceLimit(
234+
"ORDER BY 输入行数超过 setting[" + DefaultReactorQL.SETTING_ORDER_BY_MAX_ROWS + "]: " + maxRows,
235+
"为 ORDER BY 增加 LIMIT 以启用 Top-N 排序,或在可信场景下调大 orderBy.maxRows;无限流建议使用窗口排序。",
236+
"select * from test order by timestamp desc limit 100"
237+
));
234238
} else {
235239
sink.next(tuple.getT2());
236240
}
@@ -242,11 +246,19 @@ private static Long topNSize(Long rowCount, Long offset) {
242246
return null;
243247
}
244248
if (rowCount < 0) {
245-
throw new UnsupportedOperationException("invalid limit:" + rowCount);
249+
throw ReactorQLException.invalidArgument(
250+
"LIMIT 不能为负数: " + rowCount,
251+
"使用非负整数 LIMIT;如果不需要返回结果,可使用 limit 0。",
252+
"select * from test order by timestamp desc limit 100"
253+
);
246254
}
247255
long skip = offset == null ? 0 : offset;
248256
if (skip < 0) {
249-
throw new UnsupportedOperationException("invalid offset:" + skip);
257+
throw ReactorQLException.invalidArgument(
258+
"LIMIT offset 不能为负数: " + skip,
259+
"使用非负整数 offset,或省略 offset。",
260+
"select * from test order by timestamp desc limit 10, 100"
261+
);
250262
}
251263
if (Long.MAX_VALUE - skip < rowCount) {
252264
return Long.MAX_VALUE;
@@ -270,8 +282,11 @@ private static Long limitValue(Limit limit, ReactorQLContext context, boolean ro
270282

271283
private static void assertSortRows(long rows, long maxRows) {
272284
if (rows > maxRows || rows > Integer.MAX_VALUE) {
273-
throw new UnsupportedOperationException("order by buffer size exceeded setting["
274-
+ DefaultReactorQL.SETTING_ORDER_BY_MAX_ROWS + "]:" + maxRows + ", actual:" + rows);
285+
throw ReactorQLException.resourceLimit(
286+
"ORDER BY 缓冲行数超过 setting[" + DefaultReactorQL.SETTING_ORDER_BY_MAX_ROWS + "]: " + maxRows + ", actual=" + rows,
287+
"为 ORDER BY 增加 LIMIT,或在可信场景下调大 orderBy.maxRows;如果只需要局部有序,可设置 orderBy.windowSize。",
288+
"select * from test order by timestamp desc limit 100"
289+
);
275290
}
276291
}
277292

@@ -280,13 +295,27 @@ private static long longSetting(ReactorQLMetadata metadata,
280295
long defaultValue,
281296
long hardMax,
282297
boolean allowZero) {
283-
long value = metadata
284-
.getSetting(key)
285-
.map(CastUtils::castNumber)
286-
.map(Number::longValue)
287-
.orElse(defaultValue);
298+
long value;
299+
try {
300+
value = metadata
301+
.getSetting(key)
302+
.map(CastUtils::castNumber)
303+
.map(Number::longValue)
304+
.orElse(defaultValue);
305+
} catch (RuntimeException e) {
306+
throw ReactorQLException.builder(ReactorQLException.INVALID_ARGUMENT)
307+
.reason("ORDER BY setting[" + key + "] 必须是数字")
308+
.suggestion("确认 setting 在允许范围内;orderBy.maxRows 必须为正数,orderBy.windowSize 可为 0 或正数。")
309+
.example("orderBy.maxRows=10000 或 orderBy.windowSize=1000")
310+
.cause(e)
311+
.build();
312+
}
288313
if ((allowZero ? value < 0 : value <= 0) || value > hardMax) {
289-
throw new UnsupportedOperationException("invalid setting[" + key + "]:" + value);
314+
throw ReactorQLException.invalidArgument(
315+
"非法 ORDER BY setting[" + key + "]: " + value + ", hardMax=" + hardMax,
316+
"确认 setting 在允许范围内;orderBy.maxRows 必须为正数,orderBy.windowSize 可为 0 或正数。",
317+
"orderBy.maxRows=10000 或 orderBy.windowSize=1000"
318+
);
290319
}
291320
return value;
292321
}

src/main/java/org/jetlinks/reactor/ql/ReactorQLMetadata.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.jetlinks.reactor.ql;
1717

1818
import net.sf.jsqlparser.statement.select.PlainSelect;
19+
import org.jetlinks.reactor.ql.exception.ReactorQLException;
1920
import org.jetlinks.reactor.ql.feature.Feature;
2021
import org.jetlinks.reactor.ql.feature.FeatureId;
2122
import org.jetlinks.reactor.ql.utils.CastUtils;
@@ -141,7 +142,11 @@ default <T extends Feature> T getFeatureNow(FeatureId<T> featureId) {
141142
*/
142143
default <T extends Feature> T getFeatureNow(FeatureId<T> featureId, Supplier<String> errorMessage) {
143144
return getFeature(featureId)
144-
.orElseThrow(() -> new UnsupportedOperationException("unsupported feature: " + errorMessage.get()));
145+
.orElseThrow(() -> ReactorQLException.builder(ReactorQLException.UNSUPPORTED_EXPRESSION)
146+
.reason("当前函数、操作符或查询能力不可用")
147+
.suggestion("确认 SQL 使用的是当前运行环境支持的函数、操作符和 FROM 写法;内置函数名大小写不敏感。")
148+
.example(errorMessage.get())
149+
.build());
145150
}
146151

147152
Collection<Feature> getFeatures();

0 commit comments

Comments
 (0)