Files
upstream-mirror 0f4df23290
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

242 lines
5.4 KiB
Go

package sqlfile
import (
"bufio"
"context"
"io"
"strings"
)
// Split reads SQL queries from an io.Reader and returns them as a slice of strings.
// Each SQL query is delimited by a semicolon (;).
// The function handles:
// - Single-line comments (-- comment)
// - Multi-line comments (/* comment */)
// - Single-quoted strings ('string')
// - Double-quoted identifiers ("identifier")
// - Dollar-quoted strings ($$string$$ or $tag$string$tag$)
func Split(ctx context.Context, r io.Reader) ([]string, error) {
scanner := bufio.NewScanner(r)
var queries []string
var currentQuery strings.Builder
var inSingleQuote bool
var inDoubleQuote bool
var inDollarQuote bool
var dollarTag string
var inMultiLineComment bool
for scanner.Scan() {
// Check context cancellation
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
line := scanner.Text()
i := 0
lineLen := len(line)
for i < lineLen {
ch := line[i]
// Handle multi-line comments
if inMultiLineComment {
if i+1 < lineLen && ch == '*' && line[i+1] == '/' {
inMultiLineComment = false
currentQuery.WriteString("*/")
i += 2
continue
}
currentQuery.WriteByte(ch)
i++
continue
}
// Handle dollar-quoted strings (PostgreSQL)
if inDollarQuote {
if ch == '$' {
// Try to match the closing tag
endTag := extractDollarTag(line[i:])
if endTag == dollarTag {
inDollarQuote = false
currentQuery.WriteString(endTag)
i += len(endTag)
continue
}
}
currentQuery.WriteByte(ch)
i++
continue
}
// Handle single-quoted strings
if inSingleQuote {
currentQuery.WriteByte(ch)
if ch == '\'' {
// Check for escaped quote ''
if i+1 < lineLen && line[i+1] == '\'' {
currentQuery.WriteByte('\'')
i += 2
continue
}
inSingleQuote = false
}
i++
continue
}
// Handle double-quoted identifiers
if inDoubleQuote {
currentQuery.WriteByte(ch)
if ch == '"' {
// Check for escaped quote ""
if i+1 < lineLen && line[i+1] == '"' {
currentQuery.WriteByte('"')
i += 2
continue
}
inDoubleQuote = false
}
i++
continue
}
// Check for single-line comment
if i+1 < lineLen && ch == '-' && line[i+1] == '-' {
// Rest of line is a comment
currentQuery.WriteString(line[i:])
break
}
// Check for multi-line comment start
if i+1 < lineLen && ch == '/' && line[i+1] == '*' {
inMultiLineComment = true
currentQuery.WriteString("/*")
i += 2
continue
}
// Check for dollar quote start
if ch == '$' {
tag := extractDollarTag(line[i:])
if tag != "" {
inDollarQuote = true
dollarTag = tag
currentQuery.WriteString(tag)
i += len(tag)
continue
}
}
// Check for single quote
if ch == '\'' {
inSingleQuote = true
currentQuery.WriteByte(ch)
i++
continue
}
// Check for double quote
if ch == '"' {
inDoubleQuote = true
currentQuery.WriteByte(ch)
i++
continue
}
// Check for semicolon (statement terminator)
if ch == ';' {
currentQuery.WriteByte(ch)
// Check if there's a comment after the semicolon on the same line
i++
if i < lineLen {
// Skip whitespace
for i < lineLen && (line[i] == ' ' || line[i] == '\t') {
currentQuery.WriteByte(line[i])
i++
}
// If there's a comment, include it
if i+1 < lineLen && line[i] == '-' && line[i+1] == '-' {
currentQuery.WriteString(line[i:])
}
}
query := strings.TrimSpace(currentQuery.String())
if query != "" && query != ";" {
queries = append(queries, query)
}
currentQuery.Reset()
break // Move to next line
}
// Regular character
currentQuery.WriteByte(ch)
i++
}
// Add newline if we're building a query
if currentQuery.Len() > 0 {
currentQuery.WriteByte('\n')
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
// Handle any remaining query
query := strings.TrimSpace(currentQuery.String())
if query != "" && query != ";" {
queries = append(queries, query)
}
return queries, nil
}
// extractDollarTag extracts a dollar-quoted string tag from the beginning of s.
// Returns empty string if no valid dollar tag is found.
// Valid tags: $$ or $identifier$ where identifier contains only alphanumeric and underscore.
func extractDollarTag(s string) string {
if len(s) == 0 || s[0] != '$' {
return ""
}
// Find the closing $
for i := 1; i < len(s); i++ {
if s[i] == '$' {
tag := s[:i+1]
// Validate tag content (only alphanumeric and underscore allowed between $)
tagContent := tag[1 : len(tag)-1]
if isValidDollarTagContent(tagContent) {
return tag
}
return ""
}
// If we hit a character that's not allowed in a tag, it's not a dollar quote
if !isValidDollarTagChar(s[i]) {
return ""
}
}
return ""
}
// isValidDollarTagContent returns true if s contains only valid characters for a dollar tag.
func isValidDollarTagContent(s string) bool {
if s == "" {
return true // $$ is valid
}
for _, ch := range s {
if !isValidDollarTagChar(byte(ch)) {
return false
}
}
return true
}
// isValidDollarTagChar returns true if ch is a valid character in a dollar tag.
// Valid characters are alphanumeric and underscore.
func isValidDollarTagChar(ch byte) bool {
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_'
}