mirror of
https://gitee.com/rulego/streamsql.git
synced 2026-05-05 18:35:44 +00:00
feat:完善表达式中负数常量
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
# StreamSQL 负数支持文档
|
||||
|
||||
## 概述
|
||||
|
||||
StreamSQL 现在全面支持负数在 CASE 表达式中的使用。本文档总结了负数支持的完善情况、支持范围和使用建议。
|
||||
|
||||
## ✅ 已支持的负数用法
|
||||
|
||||
### 1. 基本负数常量
|
||||
|
||||
```sql
|
||||
-- CASE 表达式中的负数常量
|
||||
CASE WHEN temperature > 0 THEN 1 ELSE -1 END
|
||||
|
||||
-- 负数小数
|
||||
CASE WHEN temperature > 0 THEN 1.5 ELSE -2.5 END
|
||||
|
||||
-- 负零
|
||||
CASE WHEN temperature = -0 THEN 1 ELSE 0 END
|
||||
```
|
||||
|
||||
### 2. 比较运算符后的负数
|
||||
|
||||
```sql
|
||||
-- 比较运算符后直接跟负数
|
||||
CASE WHEN temperature < -10 THEN 'FREEZING' ELSE 'NORMAL' END
|
||||
CASE WHEN temperature >= -5.5 THEN 'ABOVE' ELSE 'BELOW' END
|
||||
CASE WHEN temperature > -20 THEN 'WARM' ELSE 'COLD' END
|
||||
```
|
||||
|
||||
### 3. 简单 CASE 表达式中的负数
|
||||
|
||||
```sql
|
||||
-- 简单 CASE 中使用负数作为匹配值
|
||||
CASE temperature
|
||||
WHEN -10 THEN 'FROZEN'
|
||||
WHEN -5 THEN 'COLD'
|
||||
WHEN 0 THEN 'ZERO'
|
||||
ELSE 'OTHER'
|
||||
END
|
||||
```
|
||||
|
||||
### 4. 算术表达式中的负数
|
||||
|
||||
```sql
|
||||
-- 括号内的负数运算
|
||||
CASE WHEN temperature + (-10) > 0 THEN 1 ELSE 0 END
|
||||
CASE WHEN (temperature * -1) > 10 THEN 1 ELSE 0 END
|
||||
```
|
||||
|
||||
## ⚠️ 部分支持或限制
|
||||
|
||||
### 1. 函数参数中的负数表达式
|
||||
|
||||
```sql
|
||||
-- 当前不完全支持:函数参数中的负数变量
|
||||
CASE WHEN ABS(-temperature) > 10 THEN 1 ELSE 0 END -- ❌
|
||||
|
||||
-- 推荐替代方案:使用括号或先计算
|
||||
CASE WHEN ABS(temperature * -1) > 10 THEN 1 ELSE 0 END -- ✅
|
||||
```
|
||||
|
||||
### 2. BETWEEN 语句中的负数范围
|
||||
|
||||
```sql
|
||||
-- 当前不支持:BETWEEN 与负数组合
|
||||
CASE WHEN temperature BETWEEN -20 AND -10 THEN 1 ELSE 0 END -- ❌
|
||||
|
||||
-- 推荐替代方案:使用比较运算符
|
||||
CASE WHEN temperature >= -20 AND temperature <= -10 THEN 1 ELSE 0 END -- ✅
|
||||
```
|
||||
|
||||
### 3. SQL 中的空格分隔负数
|
||||
|
||||
```sql
|
||||
-- 避免在 SQL 中使用空格分隔的负数
|
||||
SELECT CASE WHEN temperature < - 10 THEN 'COLD' END -- ❌ 解析问题
|
||||
|
||||
-- 推荐写法:紧密连接或使用括号
|
||||
SELECT CASE WHEN temperature < -10 THEN 'COLD' END -- ✅
|
||||
SELECT CASE WHEN temperature < (-10) THEN 'COLD' END -- ✅
|
||||
```
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 词法分析器增强
|
||||
|
||||
1. **智能负数识别**:
|
||||
- 识别比较运算符后的负数(`<`, `>`, `<=`, `>=`, `==`, `!=`)
|
||||
- 支持逻辑运算符后的负数(`AND`, `OR`)
|
||||
- 支持 CASE 关键字后的负数(`WHEN`, `THEN`, `ELSE`)
|
||||
|
||||
2. **连续运算符检查优化**:
|
||||
- 允许比较运算符后跟负数的合法组合
|
||||
- 智能区分负数与减号运算符
|
||||
|
||||
3. **空格处理**:
|
||||
- 正确处理空格分隔的负数标记
|
||||
- 改进 token 化过程以支持各种负数格式
|
||||
|
||||
### 表达式求值增强
|
||||
|
||||
1. **负数常量解析**:完全支持负整数和负小数
|
||||
2. **类型转换**:正确处理负数的数值转换
|
||||
3. **NULL 值处理**:负数与 NULL 值的正确交互
|
||||
|
||||
## 📊 测试覆盖
|
||||
|
||||
### 表达式级别测试
|
||||
|
||||
- ✅ 负数常量在 THEN/ELSE 中
|
||||
- ✅ 负数常量在 WHEN 条件中
|
||||
- ✅ 负数小数支持
|
||||
- ✅ 负数在算术表达式中
|
||||
- ✅ 负数在简单 CASE 中
|
||||
- ✅ 负零处理
|
||||
|
||||
### SQL 集成测试
|
||||
|
||||
- ✅ 完整 SQL 语句中的负数支持
|
||||
- ✅ 非聚合查询中的负数表达式
|
||||
- ✅ 聚合查询中的负数处理
|
||||
|
||||
## 🎯 使用建议
|
||||
|
||||
### 1. 推荐的负数写法
|
||||
|
||||
```sql
|
||||
-- ✅ 推荐:紧密连接的负数
|
||||
CASE WHEN temperature < -10 THEN 'FREEZING' END
|
||||
|
||||
-- ✅ 推荐:括号包围的负数(最安全)
|
||||
CASE WHEN temperature < (-10) THEN 'FREEZING' END
|
||||
|
||||
-- ✅ 推荐:负数小数
|
||||
CASE WHEN temperature < -10.5 THEN 'FREEZING' END
|
||||
```
|
||||
|
||||
### 2. 避免的写法
|
||||
|
||||
```sql
|
||||
-- ❌ 避免:空格分隔的负数
|
||||
CASE WHEN temperature < - 10 THEN 'FREEZING' END
|
||||
|
||||
-- ❌ 避免:复杂的负数表达式在函数中
|
||||
CASE WHEN ABS(-temperature) > 10 THEN 1 END
|
||||
```
|
||||
|
||||
### 3. 最佳实践
|
||||
|
||||
1. **使用括号**:当不确定负数解析时,总是使用括号包围负数
|
||||
2. **避免空格**:在负号和数字之间不要添加空格
|
||||
3. **测试验证**:对包含负数的复杂表达式进行充分测试
|
||||
4. **版本兼容**:确保使用的 StreamSQL 版本支持所需的负数功能
|
||||
|
||||
## 🚀 未来改进计划
|
||||
|
||||
1. **完全支持函数参数中的负数表达式**
|
||||
2. **支持 BETWEEN 语句中的负数范围**
|
||||
3. **改进 SQL 解析器对空格分隔负数的处理**
|
||||
4. **扩展负数支持到更多数学和字符串函数**
|
||||
|
||||
## 示例代码
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rulego/streamsql"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建 StreamSQL 实例
|
||||
sql := streamsql.New()
|
||||
defer sql.Stop()
|
||||
|
||||
// 包含负数的 SQL 查询
|
||||
query := `
|
||||
SELECT deviceId,
|
||||
temperature,
|
||||
CASE
|
||||
WHEN temperature < -10 THEN 'FREEZING'
|
||||
WHEN temperature < 0 THEN 'COLD'
|
||||
WHEN temperature = 0 THEN 'ZERO'
|
||||
ELSE 'POSITIVE'
|
||||
END as temp_category,
|
||||
CASE
|
||||
WHEN temperature > 0 THEN temperature
|
||||
ELSE (-1.0)
|
||||
END as adjusted_temp
|
||||
FROM stream
|
||||
`
|
||||
|
||||
// 执行查询
|
||||
err := sql.Execute(query)
|
||||
if err != nil {
|
||||
fmt.Printf("执行失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 添加数据处理器
|
||||
sql.AddSink(func(result interface{}) {
|
||||
fmt.Printf("结果: %+v\n", result)
|
||||
})
|
||||
|
||||
// 添加测试数据
|
||||
testData := []map[string]interface{}{
|
||||
{"deviceId": "sensor1", "temperature": -15.0},
|
||||
{"deviceId": "sensor2", "temperature": -5.0},
|
||||
{"deviceId": "sensor3", "temperature": 0.0},
|
||||
{"deviceId": "sensor4", "temperature": 10.0},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
sql.AddData(data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**更新日期**: 2025-06-17
|
||||
**版本**: StreamSQL v0.x
|
||||
**作者**: StreamSQL 开发团队
|
||||
+45
-2
@@ -136,7 +136,9 @@ func validateBasicSyntax(exprStr string) error {
|
||||
// checkConsecutiveOperators 检查连续运算符
|
||||
func checkConsecutiveOperators(expr string) error {
|
||||
// 简化的连续运算符检查:查找明显的双运算符模式
|
||||
// 但要允许比较运算符后跟负数的情况
|
||||
operators := []string{"+", "-", "*", "/", "%", "^", "==", "!=", ">=", "<=", ">", "<"}
|
||||
comparisonOps := []string{"==", "!=", ">=", "<=", ">", "<"}
|
||||
|
||||
for i := 0; i < len(expr)-1; i++ {
|
||||
// 跳过空白字符
|
||||
@@ -147,10 +149,12 @@ func checkConsecutiveOperators(expr string) error {
|
||||
// 检查当前位置是否是运算符
|
||||
isCurrentOp := false
|
||||
currentOpLen := 0
|
||||
currentOp := ""
|
||||
for _, op := range operators {
|
||||
if i+len(op) <= len(expr) && expr[i:i+len(op)] == op {
|
||||
isCurrentOp = true
|
||||
currentOpLen = len(op)
|
||||
currentOp = op
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -164,10 +168,35 @@ func checkConsecutiveOperators(expr string) error {
|
||||
|
||||
// 检查下一个字符是否也是运算符
|
||||
if nextPos < len(expr) {
|
||||
// 特殊处理:如果当前是比较运算符,下一个是负号,且负号后跟数字,则允许
|
||||
isCurrentComparison := false
|
||||
for _, compOp := range comparisonOps {
|
||||
if currentOp == compOp {
|
||||
isCurrentComparison = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否是负数的情况
|
||||
if isCurrentComparison && nextPos < len(expr) && expr[nextPos] == '-' {
|
||||
// 检查负号后是否跟数字
|
||||
digitPos := nextPos + 1
|
||||
for digitPos < len(expr) && (expr[digitPos] == ' ' || expr[digitPos] == '\t') {
|
||||
digitPos++
|
||||
}
|
||||
if digitPos < len(expr) && expr[digitPos] >= '0' && expr[digitPos] <= '9' {
|
||||
// 这是比较运算符后跟负数,允许通过
|
||||
i = nextPos // 跳过到负号位置
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 检查其他连续运算符
|
||||
for _, op := range operators {
|
||||
if nextPos+len(op) <= len(expr) && expr[nextPos:nextPos+len(op)] == op {
|
||||
// 如果不是允许的负数情况,则报错
|
||||
return fmt.Errorf("consecutive operators found: '%s' followed by '%s'",
|
||||
expr[i:i+currentOpLen], op)
|
||||
currentOp, op)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -985,8 +1014,12 @@ func tokenize(expr string) ([]string, error) {
|
||||
prevToken == "(" || // 左括号后
|
||||
prevToken == "," || // 逗号后(函数参数)
|
||||
isOperator(prevToken) || // 运算符后
|
||||
isComparisonOperator(prevToken) || // 比较运算符后
|
||||
strings.ToUpper(prevToken) == "THEN" || // THEN后
|
||||
strings.ToUpper(prevToken) == "ELSE" // ELSE后
|
||||
strings.ToUpper(prevToken) == "ELSE" || // ELSE后
|
||||
strings.ToUpper(prevToken) == "WHEN" || // WHEN后
|
||||
strings.ToUpper(prevToken) == "AND" || // AND后
|
||||
strings.ToUpper(prevToken) == "OR" // OR后
|
||||
}
|
||||
|
||||
if canBeNegativeNumber && i+1 < len(expr) && isDigit(expr[i+1]) {
|
||||
@@ -1621,6 +1654,16 @@ func isOperator(s string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// isComparisonOperator 检查是否是比较运算符
|
||||
func isComparisonOperator(s string) bool {
|
||||
switch s {
|
||||
case ">", "<", ">=", "<=", "==", "=", "!=", "<>":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isStringLiteral(expr string) bool {
|
||||
return len(expr) > 1 && (expr[0] == '\'' || expr[0] == '"') && expr[len(expr)-1] == expr[0]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+242
-1004
File diff suppressed because it is too large
Load Diff
@@ -449,12 +449,6 @@ func TestFunctionIntegrationMixed(t *testing.T) {
|
||||
|
||||
item := resultSlice[0]
|
||||
|
||||
// 打印调试信息
|
||||
t.Logf("Result item: %+v", item)
|
||||
for key, value := range item {
|
||||
t.Logf(" %s: %v (type: %T)", key, value, value)
|
||||
}
|
||||
|
||||
assert.Equal(t, "sensor1", item["device"])
|
||||
assert.Equal(t, "SENSOR1", item["device_upper"])
|
||||
|
||||
@@ -472,7 +466,6 @@ func TestFunctionIntegrationMixed(t *testing.T) {
|
||||
} else if val, ok := roundedAvg.(float64); ok {
|
||||
// 验证结果在合理范围内
|
||||
assert.True(t, val >= 25.0 && val <= 25.5, "rounded_avg should be between 25.0 and 25.5, got %v", val)
|
||||
t.Logf("rounded_avg test passed: %v", val)
|
||||
} else {
|
||||
t.Errorf("rounded_avg is not a float64: %v (type: %T)", roundedAvg, roundedAvg)
|
||||
}
|
||||
@@ -584,7 +577,6 @@ func TestNestedFunctionSupport(t *testing.T) {
|
||||
|
||||
// 执行包含 round(avg(temperature), 2) 的查询
|
||||
query := "SELECT device, round(avg(temperature), 2) as rounded_avg FROM stream GROUP BY device, TumblingWindow('1s')"
|
||||
t.Logf("Executing query: %s", query)
|
||||
err := streamsql.Execute(query)
|
||||
assert.Nil(t, err)
|
||||
|
||||
@@ -620,11 +612,6 @@ func TestNestedFunctionSupport(t *testing.T) {
|
||||
assert.Len(t, resultSlice, 1)
|
||||
|
||||
item := resultSlice[0]
|
||||
t.Logf("Result item: %+v", item)
|
||||
for key, value := range item {
|
||||
t.Logf(" %s: %v (type: %T)", key, value, value)
|
||||
}
|
||||
|
||||
assert.Equal(t, "sensor1", item["device"])
|
||||
|
||||
// 验证四舍五入的平均值
|
||||
|
||||
Reference in New Issue
Block a user