feat:完善表达式中负数常量

This commit is contained in:
rulego-team
2025-06-16 20:32:22 +08:00
parent 89d0878913
commit d0b35dbda0
5 changed files with 862 additions and 1019 deletions
+225
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-13
View File
@@ -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"])
// 验证四舍五入的平均值