diff --git a/weed/command/sql.go b/weed/command/sql.go index 48e15ee58..0d14a5984 100644 --- a/weed/command/sql.go +++ b/weed/command/sql.go @@ -15,138 +15,9 @@ import ( "github.com/peterh/liner" "github.com/seaweedfs/seaweedfs/weed/query/engine" "github.com/seaweedfs/seaweedfs/weed/util/grace" + "github.com/seaweedfs/seaweedfs/weed/util/sqlutil" ) -// splitSQLStatements splits a query string into individual SQL statements -// This robust implementation handles SQL comments, quoted strings, and escaped characters -func splitSQLStatements(query string) []string { - var statements []string - var current strings.Builder - - query = strings.TrimSpace(query) - if query == "" { - return []string{} - } - - runes := []rune(query) - i := 0 - - for i < len(runes) { - char := runes[i] - - // Handle single-line comments (-- comment) - if char == '-' && i+1 < len(runes) && runes[i+1] == '-' { - // Skip the entire comment without including it in any statement - for i < len(runes) && runes[i] != '\n' && runes[i] != '\r' { - i++ - } - // Skip the newline if present - if i < len(runes) { - i++ - } - continue - } - - // Handle multi-line comments (/* comment */) - if char == '/' && i+1 < len(runes) && runes[i+1] == '*' { - // Skip the /* opening - i++ - i++ - - // Skip to end of comment or end of input without including content - for i < len(runes) { - if runes[i] == '*' && i+1 < len(runes) && runes[i+1] == '/' { - i++ // Skip the * - i++ // Skip the / - break - } - i++ - } - continue - } - - // Handle single-quoted strings - if char == '\'' { - current.WriteRune(char) - i++ - - for i < len(runes) { - char = runes[i] - current.WriteRune(char) - - if char == '\'' { - // Check if it's an escaped quote - if i+1 < len(runes) && runes[i+1] == '\'' { - i++ // Skip the next quote (it's escaped) - if i < len(runes) { - current.WriteRune(runes[i]) - } - } else { - break // End of string - } - } - i++ - } - i++ - continue - } - - // Handle double-quoted identifiers - if char == '"' { - current.WriteRune(char) - i++ - - for i < len(runes) { - char = runes[i] - current.WriteRune(char) - - if char == '"' { - // Check if it's an escaped quote - if i+1 < len(runes) && runes[i+1] == '"' { - i++ // Skip the next quote (it's escaped) - if i < len(runes) { - current.WriteRune(runes[i]) - } - } else { - break // End of identifier - } - } - i++ - } - i++ - continue - } - - // Handle semicolon (statement separator) - if char == ';' { - stmt := strings.TrimSpace(current.String()) - if stmt != "" { - statements = append(statements, stmt) - } - current.Reset() - } else { - current.WriteRune(char) - } - - i++ - } - - // Add any remaining statement - if current.Len() > 0 { - stmt := strings.TrimSpace(current.String()) - if stmt != "" { - statements = append(statements, stmt) - } - } - - // If no statements found, return the original query as a single statement - if len(statements) == 0 { - return []string{strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(query), ";"))} - } - - return statements -} - func init() { cmdSql.Run = runSql } @@ -286,7 +157,7 @@ func executeFileQueries(ctx *SQLContext, filename string) bool { } // Split file content into individual queries (robust approach) - queries := splitSQLStatements(string(content)) + queries := sqlutil.SplitStatements(string(content)) for i, query := range queries { query = strings.TrimSpace(query) diff --git a/weed/server/postgres/protocol.go b/weed/server/postgres/protocol.go index 2822c92e4..433c0a872 100644 --- a/weed/server/postgres/protocol.go +++ b/weed/server/postgres/protocol.go @@ -12,138 +12,10 @@ import ( "github.com/seaweedfs/seaweedfs/weed/pb/schema_pb" "github.com/seaweedfs/seaweedfs/weed/query/engine" "github.com/seaweedfs/seaweedfs/weed/query/sqltypes" + "github.com/seaweedfs/seaweedfs/weed/util/sqlutil" "github.com/seaweedfs/seaweedfs/weed/util/version" ) -// splitSQLStatements splits a query string into individual SQL statements -// This robust implementation handles SQL comments, quoted strings, and escaped characters -func splitSQLStatements(query string) []string { - var statements []string - var current strings.Builder - - query = strings.TrimSpace(query) - if query == "" { - return []string{} - } - - runes := []rune(query) - i := 0 - - for i < len(runes) { - char := runes[i] - - // Handle single-line comments (-- comment) - if char == '-' && i+1 < len(runes) && runes[i+1] == '-' { - // Skip the entire comment without including it in any statement - for i < len(runes) && runes[i] != '\n' && runes[i] != '\r' { - i++ - } - // Skip the newline if present - if i < len(runes) { - i++ - } - continue - } - - // Handle multi-line comments (/* comment */) - if char == '/' && i+1 < len(runes) && runes[i+1] == '*' { - // Skip the /* opening - i++ - i++ - - // Skip to end of comment or end of input without including content - for i < len(runes) { - if runes[i] == '*' && i+1 < len(runes) && runes[i+1] == '/' { - i++ // Skip the * - i++ // Skip the / - break - } - i++ - } - continue - } - - // Handle single-quoted strings - if char == '\'' { - current.WriteRune(char) - i++ - - for i < len(runes) { - char = runes[i] - current.WriteRune(char) - - if char == '\'' { - // Check if it's an escaped quote - if i+1 < len(runes) && runes[i+1] == '\'' { - i++ // Skip the next quote (it's escaped) - if i < len(runes) { - current.WriteRune(runes[i]) - } - } else { - break // End of string - } - } - i++ - } - i++ - continue - } - - // Handle double-quoted identifiers - if char == '"' { - current.WriteRune(char) - i++ - - for i < len(runes) { - char = runes[i] - current.WriteRune(char) - - if char == '"' { - // Check if it's an escaped quote - if i+1 < len(runes) && runes[i+1] == '"' { - i++ // Skip the next quote (it's escaped) - if i < len(runes) { - current.WriteRune(runes[i]) - } - } else { - break // End of identifier - } - } - i++ - } - i++ - continue - } - - // Handle semicolon (statement separator) - if char == ';' { - stmt := strings.TrimSpace(current.String()) - if stmt != "" { - statements = append(statements, stmt) - } - current.Reset() - } else { - current.WriteRune(char) - } - i++ - } - - // Add any remaining statement - if current.Len() > 0 { - stmt := strings.TrimSpace(current.String()) - if stmt != "" { - statements = append(statements, stmt) - } - } - - // If no statements found, return the original query as a single statement - if len(statements) == 0 { - return []string{strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(query), ";"))} - } - - return statements -} - // mapErrorToPostgreSQLCode maps SeaweedFS SQL engine errors to appropriate PostgreSQL error codes func mapErrorToPostgreSQLCode(err error) string { if err == nil { @@ -301,7 +173,7 @@ func (s *PostgreSQLServer) handleSimpleQuery(session *PostgreSQLSession, query s } // Split query string into individual statements to handle multi-statement queries - queries := splitSQLStatements(query) + queries := sqlutil.SplitStatements(query) // Execute each statement sequentially for _, singleQuery := range queries { diff --git a/weed/util/sqlutil/splitter.go b/weed/util/sqlutil/splitter.go new file mode 100644 index 000000000..098a7ecb3 --- /dev/null +++ b/weed/util/sqlutil/splitter.go @@ -0,0 +1,142 @@ +package sqlutil + +import ( + "strings" +) + +// SplitStatements splits a query string into individual SQL statements. +// This robust implementation handles SQL comments, quoted strings, and escaped characters. +// +// Features: +// - Handles single-line comments (-- comment) +// - Handles multi-line comments (/* comment */) +// - Properly escapes single quotes in strings ('don”t') +// - Properly escapes double quotes in identifiers ("column""name") +// - Ignores semicolons within quoted strings and comments +// - Returns clean, trimmed statements with empty statements filtered out +func SplitStatements(query string) []string { + var statements []string + var current strings.Builder + + query = strings.TrimSpace(query) + if query == "" { + return []string{} + } + + runes := []rune(query) + i := 0 + + for i < len(runes) { + char := runes[i] + + // Handle single-line comments (-- comment) + if char == '-' && i+1 < len(runes) && runes[i+1] == '-' { + // Skip the entire comment without including it in any statement + for i < len(runes) && runes[i] != '\n' && runes[i] != '\r' { + i++ + } + // Skip the newline if present + if i < len(runes) { + i++ + } + continue + } + + // Handle multi-line comments (/* comment */) + if char == '/' && i+1 < len(runes) && runes[i+1] == '*' { + // Skip the /* opening + i++ + i++ + + // Skip to end of comment or end of input without including content + for i < len(runes) { + if runes[i] == '*' && i+1 < len(runes) && runes[i+1] == '/' { + i++ // Skip the * + i++ // Skip the / + break + } + i++ + } + continue + } + + // Handle single-quoted strings + if char == '\'' { + current.WriteRune(char) + i++ + + for i < len(runes) { + char = runes[i] + current.WriteRune(char) + + if char == '\'' { + // Check if it's an escaped quote + if i+1 < len(runes) && runes[i+1] == '\'' { + i++ // Skip the next quote (it's escaped) + if i < len(runes) { + current.WriteRune(runes[i]) + } + } else { + break // End of string + } + } + i++ + } + i++ + continue + } + + // Handle double-quoted identifiers + if char == '"' { + current.WriteRune(char) + i++ + + for i < len(runes) { + char = runes[i] + current.WriteRune(char) + + if char == '"' { + // Check if it's an escaped quote + if i+1 < len(runes) && runes[i+1] == '"' { + i++ // Skip the next quote (it's escaped) + if i < len(runes) { + current.WriteRune(runes[i]) + } + } else { + break // End of identifier + } + } + i++ + } + i++ + continue + } + + // Handle semicolon (statement separator) + if char == ';' { + stmt := strings.TrimSpace(current.String()) + if stmt != "" { + statements = append(statements, stmt) + } + current.Reset() + } else { + current.WriteRune(char) + } + i++ + } + + // Add any remaining statement + if current.Len() > 0 { + stmt := strings.TrimSpace(current.String()) + if stmt != "" { + statements = append(statements, stmt) + } + } + + // If no statements found, return the original query as a single statement + if len(statements) == 0 { + return []string{strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(query), ";"))} + } + + return statements +} diff --git a/weed/command/sql_test.go b/weed/util/sqlutil/splitter_test.go similarity index 92% rename from weed/command/sql_test.go rename to weed/util/sqlutil/splitter_test.go index c1ef6f9ec..91fac6196 100644 --- a/weed/command/sql_test.go +++ b/weed/util/sqlutil/splitter_test.go @@ -1,11 +1,11 @@ -package command +package sqlutil import ( "reflect" "testing" ) -func TestSplitSQLStatements(t *testing.T) { +func TestSplitStatements(t *testing.T) { tests := []struct { name string input string @@ -100,15 +100,15 @@ func TestSplitSQLStatements(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := splitSQLStatements(tt.input) + result := SplitStatements(tt.input) if !reflect.DeepEqual(result, tt.expected) { - t.Errorf("splitSQLStatements() = %v, expected %v", result, tt.expected) + t.Errorf("SplitStatements() = %v, expected %v", result, tt.expected) } }) } } -func TestSplitSQLStatements_EdgeCases(t *testing.T) { +func TestSplitStatements_EdgeCases(t *testing.T) { tests := []struct { name string input string @@ -138,9 +138,9 @@ func TestSplitSQLStatements_EdgeCases(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := splitSQLStatements(tt.input) + result := SplitStatements(tt.input) if !reflect.DeepEqual(result, tt.expected) { - t.Errorf("splitSQLStatements() = %v, expected %v", result, tt.expected) + t.Errorf("SplitStatements() = %v, expected %v", result, tt.expected) } }) }