From 3326f054162e99ed113384fcdd2701bbd7ee9c6d Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Thu, 18 Jun 2026 07:31:01 +0000 Subject: [PATCH 1/3] fix(mysql/splitter): distinguish IF control flow from IF() when condition uses parentheses The splitter treated any IF followed by '(' as the IF() function, so stored procedures using IF (cond) AND ... THEN were split incorrectly and online execution failed with MySQL 1064. Use THEN at parenthesis depth zero to detect control-flow IF blocks instead. --- sqle/driver/mysql/splitter/block.go | 4 + sqle/driver/mysql/splitter/splitter.go | 38 +++++-- sqle/driver/mysql/splitter/splitter_test.go | 119 +++++++++++++++++--- 3 files changed, 131 insertions(+), 30 deletions(-) diff --git a/sqle/driver/mysql/splitter/block.go b/sqle/driver/mysql/splitter/block.go index 488a93a4ac..462f87a42e 100644 --- a/sqle/driver/mysql/splitter/block.go +++ b/sqle/driver/mysql/splitter/block.go @@ -11,6 +11,10 @@ type Block interface { MatchEnd(token *parser.Token) bool } +func isThenToken(token *parser.Token) bool { + return strings.ToUpper(token.Ident()) == "THEN" +} + var allBlocks []Block = []Block{ BeginEndBlock{}, IfEndIfBlock{}, diff --git a/sqle/driver/mysql/splitter/splitter.go b/sqle/driver/mysql/splitter/splitter.go index bf9d77fc09..bcba8d2edc 100644 --- a/sqle/driver/mysql/splitter/splitter.go +++ b/sqle/driver/mysql/splitter/splitter.go @@ -16,10 +16,8 @@ type splitter struct { } func NewSplitter() *splitter { - p := parser.New() - p.EnableWindowFunc(true) return &splitter{ - parser: p, + parser: parser.New(), delimiter: NewDelimiter(), scanner: parser.NewScanner(""), } @@ -187,19 +185,13 @@ func (s *splitter) skipBeginEndBlock(token *parser.Token) *parser.Token { } } if matchedBlock != nil { - // 如果匹配到的是 IF 块,需要向前看一个 token 来确定它是一个控制流语句还是一个函数。 + // 如果匹配到的是 IF 块,需要向前看以区分控制流 IF 与 IF() 函数。 if _, ok := matchedBlock.(IfEndIfBlock); ok { - // 向前看一个 token - nextToken := s.scanner.NextToken() - // **非常重要**:将扫描器位置恢复,以便后续逻辑可以重新处理这个 token s.scanner.SetCursor(currentOffset) - - if nextToken.Ident() == "(" { - // 如果 IF 后面是 '(',说明是 IF() 函数,而不是块语句,继续处理下一个 token。 - } else { - // 否则,它是一个真正的 IF 块语句,将其压入堆栈。 + if s.ifIsControlFlowStatement() { blockStack = append(blockStack, matchedBlock) } + s.scanner.SetCursor(currentOffset) } else { // 对于其他类型的块,直接压入堆栈 blockStack = append(blockStack, matchedBlock) @@ -235,6 +227,28 @@ func (s *splitter) skipBeginEndBlock(token *parser.Token) *parser.Token { return token } +// ifIsControlFlowStatement 从 IF 之后向前看,判断是控制流 IF 块还是 IF() 函数。 +// 规则:跟踪括号深度,若在深度 0 处遇到 THEN,则为控制流 IF。 +func (s *splitter) ifIsControlFlowStatement() bool { + depth := 0 + for s.scanner.Offset() < len(s.scanner.Text()) { + token := s.scanner.NextToken() + switch token.Ident() { + case "(": + depth++ + case ")": + if depth > 0 { + depth-- + } + default: + if isThenToken(token) && depth == 0 { + return true + } + } + } + return false +} + // isBeginQuit 判断BEGIN语句跟的是否就是结束符 func (s *splitter) isBeginQuit(token *parser.Token, tokenIndex int) bool { if token.Ident() == s.delimiter.DelimiterStr && tokenIndex == 1 { diff --git a/sqle/driver/mysql/splitter/splitter_test.go b/sqle/driver/mysql/splitter/splitter_test.go index ec94ad10d2..ec14d4f968 100644 --- a/sqle/driver/mysql/splitter/splitter_test.go +++ b/sqle/driver/mysql/splitter/splitter_test.go @@ -249,24 +249,6 @@ Alter table point_trans_shard_00_part_202401 ADD CONSTRAINT chk_point_trans_shar } } -func TestParseWindowFunctionSelect(t *testing.T) { - parser := NewSplitter() - sql := "SELECT id, ROW_NUMBER() OVER (PARTITION BY nominate_app_id ORDER BY create_date ASC) AS rn FROM sourcing.tt_nomi_record WHERE is_delete = 0" - stmt, err := parser.ParseSqlText(sql) - if err != nil { - t.Fatal(err) - } - if len(stmt) != 1 { - t.Fatalf("expect 1 stmt, got %d", len(stmt)) - } - if _, ok := stmt[0].(*ast.UnparsedStmt); ok { - t.Fatal("expect SelectStmt, got UnparsedStmt") - } - if _, ok := stmt[0].(*ast.SelectStmt); !ok { - t.Fatalf("expect SelectStmt, got %T", stmt[0]) - } -} - func TestPerfectParse(t *testing.T) { parser := NewSplitter() @@ -1426,3 +1408,104 @@ SELECT * FROM users;`, }) } } + +// TestIfControlFlowStatementInProcedure 测试 IF 控制流与 IF() 函数在存储过程中的拆分。 +func TestIfControlFlowStatementInProcedure(t *testing.T) { + parser := NewSplitter() + + testCases := []struct { + name string + sql string + expected int + contains string + }{ + { + name: "IF with parenthesized condition in procedure", + sql: `CREATE PROCEDURE test_proc(IN Arg0 INT) +BEGIN + DECLARE v_lng DECIMAL(10,6); + DECLARE v_lat DECIMAL(10,6); + IF ( v_lng IS NULL OR v_lat IS NULL ) AND Arg0 IS NOT NULL THEN + SET v_lng = 0; + END IF; +END;`, + expected: 1, + contains: "END;", + }, + { + name: "IF without parenthesized condition in procedure", + sql: `CREATE PROCEDURE test_proc() +BEGIN + DECLARE v_variable INT DEFAULT 0; + IF v_variable = 0 THEN + SET v_variable = 1; + END IF; +END;`, + expected: 1, + contains: "END;", + }, + { + name: "IF with ELSEIF in procedure", + sql: `CREATE PROCEDURE test_proc() +BEGIN + DECLARE v_variable INT DEFAULT 0; + IF v_variable = 0 THEN + SET v_variable = 1; + ELSEIF v_variable = 1 THEN + SET v_variable = 2; + ELSE + SET v_variable = 0; + END IF; +END;`, + expected: 1, + contains: "END;", + }, + { + name: "IF function in trigger body", + sql: `CREATE TRIGGER trg BEFORE INSERT ON account +FOR EACH ROW +BEGIN + SET @deposits = @deposits + IF(NEW.amount>0,NEW.amount,0); +END;`, + expected: 1, + contains: "IF(NEW.amount>0,NEW.amount,0)", + }, + { + name: "IF function with nested CASE THEN in arguments", + sql: `CREATE TRIGGER trg BEFORE INSERT ON account +FOR EACH ROW +BEGIN + SET @x = IF(CASE WHEN NEW.amount > 0 THEN 1 ELSE 0 END, NEW.amount, 0); +END;`, + expected: 1, + contains: "IF(CASE WHEN NEW.amount > 0 THEN 1 ELSE 0 END, NEW.amount, 0)", + }, + { + name: "procedure followed by select", + sql: `CREATE PROCEDURE test_proc() +BEGIN + IF ( 1 = 1 ) AND 2 > 0 THEN + SELECT 1; + END IF; +END; +SELECT 1;`, + expected: 2, + contains: "END;", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + stmts, err := parser.ParseSqlText(tc.sql) + if err != nil { + t.Fatalf("解析失败: %v", err) + } + if len(stmts) != tc.expected { + t.Fatalf("期望分割出 %d 个语句,实际得到 %d 个", tc.expected, len(stmts)) + } + if tc.contains != "" && !strings.Contains(stmts[0].Text(), tc.contains) { + t.Fatalf("第一条语句应包含 %q,实际为: %s", tc.contains, stmts[0].Text()) + } + }) + } +} From 39edb9b4af25f2a593dcbd45d97524a46316a9cc Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Thu, 18 Jun 2026 07:35:21 +0000 Subject: [PATCH 2/3] chore(mysql/splitter): restore EnableWindowFunc accidentally dropped in IF fix Keep the CE window-function parser setup when applying the IF control-flow splitter fix; the removal came from copying sqle-ee files that lack this CE change. --- sqle/driver/mysql/splitter/splitter.go | 4 +++- sqle/driver/mysql/splitter/splitter_test.go | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/sqle/driver/mysql/splitter/splitter.go b/sqle/driver/mysql/splitter/splitter.go index bcba8d2edc..fc6b39b6ea 100644 --- a/sqle/driver/mysql/splitter/splitter.go +++ b/sqle/driver/mysql/splitter/splitter.go @@ -16,8 +16,10 @@ type splitter struct { } func NewSplitter() *splitter { + p := parser.New() + p.EnableWindowFunc(true) return &splitter{ - parser: parser.New(), + parser: p, delimiter: NewDelimiter(), scanner: parser.NewScanner(""), } diff --git a/sqle/driver/mysql/splitter/splitter_test.go b/sqle/driver/mysql/splitter/splitter_test.go index ec14d4f968..463b788ae4 100644 --- a/sqle/driver/mysql/splitter/splitter_test.go +++ b/sqle/driver/mysql/splitter/splitter_test.go @@ -249,6 +249,24 @@ Alter table point_trans_shard_00_part_202401 ADD CONSTRAINT chk_point_trans_shar } } +func TestParseWindowFunctionSelect(t *testing.T) { + parser := NewSplitter() + sql := "SELECT id, ROW_NUMBER() OVER (PARTITION BY nominate_app_id ORDER BY create_date ASC) AS rn FROM sourcing.tt_nomi_record WHERE is_delete = 0" + stmt, err := parser.ParseSqlText(sql) + if err != nil { + t.Fatal(err) + } + if len(stmt) != 1 { + t.Fatalf("expect 1 stmt, got %d", len(stmt)) + } + if _, ok := stmt[0].(*ast.UnparsedStmt); ok { + t.Fatal("expect SelectStmt, got UnparsedStmt") + } + if _, ok := stmt[0].(*ast.SelectStmt); !ok { + t.Fatalf("expect SelectStmt, got %T", stmt[0]) + } +} + func TestPerfectParse(t *testing.T) { parser := NewSplitter() From 5c7e24721555546db75a257ac600f606ad90c676 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Thu, 18 Jun 2026 07:39:35 +0000 Subject: [PATCH 3/3] refactor(mysql/splitter): drop redundant SetCursor before IF lookahead The scanner already sits at currentOffset (right after IF) when entering the branch, so the first SetCursor was a no-op. Keep only the reset after the lookahead scan. --- sqle/driver/mysql/splitter/splitter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqle/driver/mysql/splitter/splitter.go b/sqle/driver/mysql/splitter/splitter.go index fc6b39b6ea..6ada2c85ae 100644 --- a/sqle/driver/mysql/splitter/splitter.go +++ b/sqle/driver/mysql/splitter/splitter.go @@ -188,8 +188,8 @@ func (s *splitter) skipBeginEndBlock(token *parser.Token) *parser.Token { } if matchedBlock != nil { // 如果匹配到的是 IF 块,需要向前看以区分控制流 IF 与 IF() 函数。 + // 此处 scanner 正位于 IF 之后(currentOffset),前向扫描后需复位以便后续 END 检测。 if _, ok := matchedBlock.(IfEndIfBlock); ok { - s.scanner.SetCursor(currentOffset) if s.ifIsControlFlowStatement() { blockStack = append(blockStack, matchedBlock) }