🐛 fix(elasticsearch): 修复 ES SQL 末尾分号导致空结果 Fixes #605

- 清理 parseESSQL 解析出的 WHERE 和 ORDER BY 子句尾部分号
- 避免 range 条件把数值误当成字符串传给 Elasticsearch
- 补充带分号的 ES SQL 端到端与解析回归测试
This commit is contained in:
Syngnat
2026-06-30 22:28:17 +08:00
parent 52eacdd9df
commit 923e789211
2 changed files with 79 additions and 3 deletions

View File

@@ -237,6 +237,12 @@ var reSQLOffset = regexp.MustCompile(`(?i)\bOFFSET\s+(\d+)`)
// reSQLOrderBy 匹配 ORDER BY 子句。
var reSQLOrderBy = regexp.MustCompile(`(?i)\bORDER\s+BY\s+(.+?)(?:\bLIMIT\b|\bOFFSET\b|$)`)
func trimESTrailingClauseSyntax(s string) string {
s = strings.TrimSpace(s)
s = strings.TrimRight(s, " \t\r\n;")
return strings.TrimSpace(s)
}
// parseESSQL 解析简单 SELECT SQL 为结构化组成部分。
func parseESSQL(sql string) (esParsedSQL, bool) {
upper := strings.ToUpper(strings.TrimSpace(sql))
@@ -255,7 +261,7 @@ func parseESSQL(sql string) (esParsedSQL, bool) {
// 提取 SELECT 列列表
selectEnd := strings.Index(upper, " FROM ")
if selectEnd > 6 {
parsed.Columns = strings.TrimSpace(sql[6:selectEnd])
parsed.Columns = trimESTrailingClauseSyntax(sql[6:selectEnd])
} else {
parsed.Columns = "*"
}
@@ -263,13 +269,13 @@ func parseESSQL(sql string) (esParsedSQL, bool) {
// 提取 WHERE 子句
whereMatch := regexp.MustCompile(`(?i)\bWHERE\s+(.+?)(?:\bORDER\b|\bLIMIT\b|\bOFFSET\b|$)`).FindStringSubmatch(sql)
if len(whereMatch) >= 2 {
parsed.Where = strings.TrimSpace(whereMatch[1])
parsed.Where = trimESTrailingClauseSyntax(whereMatch[1])
}
// 提取 ORDER BY
orderMatch := reSQLOrderBy.FindStringSubmatch(sql)
if len(orderMatch) >= 2 {
parsed.OrderBy = strings.TrimSpace(orderMatch[1])
parsed.OrderBy = trimESTrailingClauseSyntax(orderMatch[1])
}
// 提取 LIMIT

View File

@@ -1608,6 +1608,54 @@ func TestElasticsearchSQLSelectDoesNotRequireXPackSQL(t *testing.T) {
}
}
func TestElasticsearchSQLWhereWithTrailingSemicolonPreservesNumericRange(t *testing.T) {
var capturedBody string
server := newMockESServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/log_manage_entity_v2/_search" {
w.WriteHeader(http.StatusNotFound)
return
}
body, _ := io.ReadAll(r.Body)
capturedBody = string(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"hits": {
"total": {"value": 1},
"hits": [
{"_index": "log_manage_entity_v2", "_id": "1", "_source": {"operateTime": 1782282529001, "message": "ok"}}
]
}
}`))
})
db := newTestESDB(t, server.URL, "")
rows, _, err := db.Query(`select * from log_manage_entity_v2 where operateTime > 1782282529000;`)
if err != nil {
t.Fatalf("带分号的 ES SQL 查询应执行成功:%v", err)
}
if len(rows) != 1 || rows[0]["message"] != "ok" {
t.Fatalf("期望返回 1 条命中数据,实际 rows=%#v", rows)
}
var payload map[string]interface{}
if err := json.Unmarshal([]byte(capturedBody), &payload); err != nil {
t.Fatalf("解析发往 ES 的请求体失败:%v body=%s", err, capturedBody)
}
query, _ := payload["query"].(map[string]interface{})
rangeNode, _ := query["range"].(map[string]interface{})
fieldNode, _ := rangeNode["operateTime"].(map[string]interface{})
gtValue, exists := fieldNode["gt"]
if !exists {
t.Fatalf("期望生成 range.gt 条件,实际 payload=%v", payload)
}
if _, ok := gtValue.(float64); !ok {
t.Fatalf("operateTime.gt 应保持为数值,实际类型=%T 值=%v body=%s", gtValue, gtValue, capturedBody)
}
if gtValue.(float64) != 1782282529000 {
t.Fatalf("operateTime.gt 数值错误,实际=%v body=%s", gtValue, capturedBody)
}
}
// ---- extractESSQLFromTable 测试 ----
func TestESExtractSQLFromTable(t *testing.T) {
@@ -1684,6 +1732,28 @@ func TestESParseSQL(t *testing.T) {
}
}
func TestESParseSQLTrimsTrailingSemicolonFromClauses(t *testing.T) {
t.Run("WHERE 末尾分号不应进入条件值", func(t *testing.T) {
parsed, ok := parseESSQL(`select * from log_manage_entity_v2 where operateTime > 1782282529000;`)
if !ok {
t.Fatal("parseESSQL 应成功解析带分号的 WHERE 查询")
}
if parsed.Where != "operateTime > 1782282529000" {
t.Fatalf("WHERE 子句不应包含尾部分号,实际=%q", parsed.Where)
}
})
t.Run("ORDER BY 末尾分号不应进入排序子句", func(t *testing.T) {
parsed, ok := parseESSQL(`select * from log_manage_entity_v2 order by operateTime desc;`)
if !ok {
t.Fatal("parseESSQL 应成功解析带分号的 ORDER BY 查询")
}
if parsed.OrderBy != "operateTime desc" {
t.Fatalf("ORDER BY 子句不应包含尾部分号,实际=%q", parsed.OrderBy)
}
})
}
func TestESConvertWhere(t *testing.T) {
tests := []struct {
name string