-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduce util.SplitStatements
- Loading branch information
Showing
3 changed files
with
227 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package util | ||
|
||
import ( | ||
"strings" | ||
|
||
"github.com/samber/lo" | ||
) | ||
|
||
// SplitStatements splits a string containing multiple sql statements separated by semicolons. | ||
// It strips out comments from the statements, both simple (--) and bracketed (/* */) ones. | ||
// It also handles sql strings properly which can contain semi colons, escaped quotes and comment character sequences without affecting the splitting behaviour. | ||
func SplitStatements(statements string) []string { | ||
var inString bool // flag signalling that we are inside a SQL string | ||
var inEscapedQuote bool // flag signalling that we are inside an escaped quote character inside a SQL string | ||
var inSimpleComment bool // flag signalling that we are inside a simple comment (--) | ||
var inBracketedComment bool // flag signalling that we are inside a bracketed comment (/* */) | ||
|
||
var stmts []string // | ||
var stmt string | ||
var previous rune | ||
|
||
next := func(input string, i int) (rune, bool) { | ||
runes := []rune(input) | ||
if len(input) > i+1 { | ||
return runes[i+1], true | ||
} | ||
return 0, false | ||
} | ||
|
||
for i, c := range statements { | ||
if inString { | ||
if c == '\'' { | ||
if inEscapedQuote { | ||
inEscapedQuote = false | ||
} else { | ||
if next, ok := next(statements, i); ok { | ||
if next == '\'' { | ||
inEscapedQuote = true | ||
} else { | ||
inString = false | ||
} | ||
} | ||
} | ||
} | ||
stmt += string(c) | ||
} else if inSimpleComment { | ||
if c == '\n' { | ||
inSimpleComment = false | ||
} | ||
} else if inBracketedComment { | ||
if c == '/' && previous == '*' { | ||
inBracketedComment = false | ||
} | ||
} else { | ||
if c == '\'' { | ||
inString = true | ||
stmt += string(c) | ||
} else if c == '-' && previous == '-' { | ||
inSimpleComment = true | ||
stmtRunes := []rune(stmt) | ||
stmt = string(stmtRunes[:len(stmtRunes)-1]) // remove the previous dash | ||
} else if c == '*' && previous == '/' { | ||
inBracketedComment = true | ||
stmtRunes := []rune(stmt) | ||
stmt = string(stmtRunes[:len(stmtRunes)-1]) // remove the previous slash | ||
} else if c == ';' { | ||
stmts = append(stmts, stmt) | ||
stmt = "" | ||
continue | ||
} else { | ||
stmt += string(c) | ||
} | ||
} | ||
previous = c | ||
} | ||
if stmt != "" { | ||
stmts = append(stmts, stmt) | ||
} | ||
|
||
return lo.FilterMap(stmts, func(stmt string, _ int) (string, bool) { | ||
// remove leading and trailing whitespaces tabs and newlines | ||
stmt = strings.TrimRight(strings.TrimLeft(stmt, "\n\t "), "\n\t ") | ||
return stmt, stmt != "" | ||
}) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package util_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/rudderlabs/sqlconnect-go/sqlconnect/util" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestSplitStatements(t *testing.T) { | ||
|
||
t.Run("single statement", func(t *testing.T) { | ||
query := "SELECT * FROM table" | ||
expected := []string{"SELECT * FROM table"} | ||
actual := util.SplitStatements(query) | ||
require.ElementsMatch(t, expected, actual) | ||
}) | ||
|
||
t.Run("single statement with semicolon", func(t *testing.T) { | ||
query := "SELECT * FROM table;" | ||
expected := []string{"SELECT * FROM table"} | ||
actual := util.SplitStatements(query) | ||
require.ElementsMatch(t, expected, actual) | ||
}) | ||
|
||
t.Run("multiple statements", func(t *testing.T) { | ||
query := ` | ||
SELECT * FROM table1; | ||
SELECT * FROM table2; | ||
` | ||
expected := []string{"SELECT * FROM table1", "SELECT * FROM table2"} | ||
actual := util.SplitStatements(query) | ||
require.ElementsMatch(t, expected, actual) | ||
}) | ||
|
||
t.Run("multiple statements with simple comments", func(t *testing.T) { | ||
query := ` | ||
SELECT * FROM table1; -- this is an inline comment | ||
-- this is another comment on its own line | ||
SELECT * FROM table2 | ||
` | ||
expected := []string{"SELECT * FROM table1", "SELECT * FROM table2"} | ||
actual := util.SplitStatements(query) | ||
require.ElementsMatch(t, expected, actual) | ||
}) | ||
|
||
t.Run("multiple statements with bracketed comments", func(t *testing.T) { | ||
query := ` | ||
SELECT * FROM table1; /* this is a bracketed comment | ||
that spans multiple lines */ | ||
/* this is another bracketed comment */ | ||
SELECT * FROM table2; | ||
` | ||
expected := []string{"SELECT * FROM table1", "SELECT * FROM table2"} | ||
actual := util.SplitStatements(query) | ||
require.ElementsMatch(t, expected, actual) | ||
}) | ||
|
||
t.Run("multiple statements with both types of comments", func(t *testing.T) { | ||
query := ` | ||
SELECT * FROM table1; -- this is an inline comment | ||
/* this is a bracketed comment | ||
that spans multiple lines */ | ||
-- this is another inline comment | ||
SELECT * FROM table2; | ||
/* this is another bracketed | ||
comment */ | ||
` | ||
expected := []string{"SELECT * FROM table1", "SELECT * FROM table2"} | ||
actual := util.SplitStatements(query) | ||
require.ElementsMatch(t, expected, actual) | ||
}) | ||
|
||
t.Run("multiple statements with semicolon inside comments", func(t *testing.T) { | ||
query := ` | ||
SELECT * FROM table1; -- this is an inline comment; | ||
/* this is a bracketed comment; | ||
that spans multiple lines */ | ||
-- this is another inline; comment; | ||
SELECT * FROM table2; | ||
/* this is another bracketed | ||
comment; */ | ||
` | ||
expected := []string{"SELECT * FROM table1", "SELECT * FROM table2"} | ||
actual := util.SplitStatements(query) | ||
require.ElementsMatch(t, expected, actual) | ||
}) | ||
|
||
t.Run("multiple mulitline statements with semicolon in sql string", func(t *testing.T) { | ||
query := ` | ||
SELECT * | ||
FROM table1 | ||
WHERE value='some;value'; | ||
SELECT | ||
* | ||
FROM table2 | ||
WHERE value='another '' ; value;'; | ||
SELECT * FROM table3 WHERE value='' AND value1='some' ; | ||
` | ||
expected := []string{ | ||
"SELECT * \n\t\t\tFROM table1 \n\t\tWHERE value='some;value'", | ||
"SELECT \n\t\t\t* \n\t\tFROM table2 \n\t\t WHERE value='another '' ; value;'", | ||
"SELECT * FROM table3 WHERE value='' AND value1='some'", | ||
} | ||
actual := util.SplitStatements(query) | ||
require.ElementsMatch(t, expected, actual) | ||
}) | ||
|
||
t.Run("single statement with simple comment char sequence in sql string", func(t *testing.T) { | ||
query := `SELECT * FROM table1 WHERE value='some --value'` | ||
|
||
expected := []string{ | ||
"SELECT * FROM table1 WHERE value='some --value'", | ||
} | ||
actual := util.SplitStatements(query) | ||
require.ElementsMatch(t, expected, actual) | ||
}) | ||
|
||
t.Run("single statement with bracketed comment char sequence in sql string", func(t *testing.T) { | ||
query := `SELECT * FROM table1 WHERE value='some /* value */'` | ||
|
||
expected := []string{ | ||
"SELECT * FROM table1 WHERE value='some /* value */'", | ||
} | ||
actual := util.SplitStatements(query) | ||
require.ElementsMatch(t, expected, actual) | ||
}) | ||
|
||
t.Run("single statement with all kinds of impediments in sql string", func(t *testing.T) { | ||
query := `SELECT * FROM table1 WHERE value='''some /* value */ -- comment'''` | ||
|
||
expected := []string{ | ||
"SELECT * FROM table1 WHERE value='''some /* value */ -- comment'''", | ||
} | ||
actual := util.SplitStatements(query) | ||
require.ElementsMatch(t, expected, actual) | ||
}) | ||
} |