// Package sqllex recognizes PostgreSQL lexical boundaries without parsing SQL. package sqllex import "strings" func start(c byte) bool { return c >= 128 || c == '_' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' } func ident(c byte) bool { return start(c) || c >= '0' && c <= '9' || c >= 128 } // Walk visits named parameters and standalone line comments in executable SQL. Quoted // strings/identifiers, dollar bodies, casts and nested block comments are opaque. // Returning replacement text is literal: it is never scanned again. func Walk(sql string, parameter, lineComment func(string) string) string { var out strings.Builder for i := 0; i < len(sql); { begin := i switch { case strings.HasPrefix(sql[i:], "--"): i += 2 for i < len(sql) && sql[i] != '\n' && sql[i] != '\r' { i++ } text := sql[begin:i] if lineComment != nil && strings.TrimSpace(sql[strings.LastIndexAny(sql[:begin], "\r\n")+1:begin]) == "" { text = lineComment(text) } out.WriteString(text) continue case strings.HasPrefix(sql[i:], "/*"): i += 2 depth := 1 for i < len(sql) && depth > 0 { if strings.HasPrefix(sql[i:], "/*") { depth++ i += 2 } else if strings.HasPrefix(sql[i:], "*/") { depth-- i += 2 } else { i++ } } case sql[i] == '\'' || sql[i] == '"': q := sql[i] escapes := q == '\'' && i > 0 && (sql[i-1] == 'E' || sql[i-1] == 'e') && (i == 1 || !ident(sql[i-2])) i++ for i < len(sql) { if escapes && sql[i] == '\\' { i++ if i < len(sql) { i++ } continue } if sql[i] == q { i++ if i < len(sql) && sql[i] == q { i++ continue } break } i++ } case sql[i] == '$' && (i == 0 || !ident(sql[i-1]) && sql[i-1] != '$'): j := i + 1 if j < len(sql) && start(sql[j]) { for j < len(sql) && ident(sql[j]) { j++ } } if j < len(sql) && sql[j] == '$' { tag := sql[i : j+1] end := strings.Index(sql[j+1:], tag) if end < 0 { i = len(sql) } else { i = j + 1 + end + len(tag) } } else { i++ } case strings.HasPrefix(sql[i:], "::"): i += 2 case sql[i] == ':' && i+1 < len(sql) && start(sql[i+1]) && (i == 0 || !ident(sql[i-1]) && sql[i-1] != ':'): i += 2 for i < len(sql) && ident(sql[i]) { i++ } text := sql[begin:i] if parameter != nil { text = parameter(text) } out.WriteString(text) continue default: i++ } out.WriteString(sql[begin:i]) } return out.String() }