mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-05 18:11:32 +08:00
🐛 fix(elasticsearch): 修复 ES SQL 末尾分号导致空结果 Fixes #605
- 清理 parseESSQL 解析出的 WHERE 和 ORDER BY 子句尾部分号 - 避免 range 条件把数值误当成字符串传给 Elasticsearch - 补充带分号的 ES SQL 端到端与解析回归测试
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user