Files
signoz/pkg/sqlschema/normalizer.go
Naman Verma 4fe311cb0e
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
feat: add functional unique index (#11307)
* feat: add functional unique index

* chore: add tag migration

* chore: add failing scratch test (to discuss)

* chore: fetch expressions as well in getindices call

* fix: remove tag unique index (will be added in a separate PR)

* fix: remove tag unique index (will be added in a separate PR)

* chore: remove temporary tests

* fix: go lint fix

* chore: better comment for unique index

* test: add test for case insensitive expression equality

* test: add equality test for columns with quotes and capital letters

* chore: add separate type for unique indices with expressions

* test: add test for postgres provider
2026-07-10 19:10:10 +00:00

168 lines
3.2 KiB
Go

package sqlschema
import (
"fmt"
"hash/fnv"
"strings"
)
type expressionNormalizer struct {
input string
foldCase bool
}
func (n *expressionNormalizer) hash() string {
hasher := fnv.New32a()
_, _ = hasher.Write([]byte(n.normalize()))
return fmt.Sprintf("%08x", hasher.Sum32())
}
func (n *expressionNormalizer) normalize() string {
expr := strings.TrimSpace(n.input)
expr = n.stripOuterParentheses(expr)
var output strings.Builder
output.Grow(len(expr))
for i := 0; i < len(expr); i++ {
switch expr[i] {
case ' ', '\t', '\n', '\r':
if output.Len() > 0 {
last := output.String()[output.Len()-1]
if last != ' ' {
output.WriteByte(' ')
}
}
case '\'':
end := n.consumeSingleQuotedLiteral(expr, i, &output)
i = end
case '"':
token, end := n.consumeDoubleQuotedToken(expr, i)
output.WriteString(token)
i = end
default:
c := expr[i]
if n.foldCase && c >= 'A' && c <= 'Z' {
c += 'a' - 'A'
}
output.WriteByte(c)
}
}
return strings.TrimSpace(output.String())
}
func (n *expressionNormalizer) stripOuterParentheses(s string) string {
for {
s = strings.TrimSpace(s)
if len(s) < 2 || s[0] != '(' || s[len(s)-1] != ')' || !n.hasWrappingParentheses(s) {
return s
}
s = s[1 : len(s)-1]
}
}
func (n *expressionNormalizer) hasWrappingParentheses(s string) bool {
depth := 0
inSingleQuotedLiteral := false
inDoubleQuotedToken := false
for i := 0; i < len(s); i++ {
switch s[i] {
case '\'':
if inDoubleQuotedToken {
continue
}
if inSingleQuotedLiteral && i+1 < len(s) && s[i+1] == '\'' {
i++
continue
}
inSingleQuotedLiteral = !inSingleQuotedLiteral
case '"':
if inSingleQuotedLiteral {
continue
}
if inDoubleQuotedToken && i+1 < len(s) && s[i+1] == '"' {
i++
continue
}
inDoubleQuotedToken = !inDoubleQuotedToken
case '(':
if inSingleQuotedLiteral || inDoubleQuotedToken {
continue
}
depth++
case ')':
if inSingleQuotedLiteral || inDoubleQuotedToken {
continue
}
depth--
if depth == 0 && i != len(s)-1 {
return false
}
}
}
return depth == 0
}
func (n *expressionNormalizer) consumeSingleQuotedLiteral(s string, start int, output *strings.Builder) int {
output.WriteByte(s[start])
for i := start + 1; i < len(s); i++ {
output.WriteByte(s[i])
if s[i] == '\'' {
if i+1 < len(s) && s[i+1] == '\'' {
i++
output.WriteByte(s[i])
continue
}
return i
}
}
return len(s) - 1
}
func (n *expressionNormalizer) consumeDoubleQuotedToken(s string, start int) (string, int) {
var ident strings.Builder
for i := start + 1; i < len(s); i++ {
if s[i] == '"' {
if i+1 < len(s) && s[i+1] == '"' {
ident.WriteByte('"')
i++
continue
}
if n.isSimpleUnquotedIdentifier(ident.String()) {
return ident.String(), i
}
return s[start : i+1], i
}
ident.WriteByte(s[i])
}
return s[start:], len(s) - 1
}
func (n *expressionNormalizer) isSimpleUnquotedIdentifier(s string) bool {
if s == "" || strings.ToLower(s) != s {
return false
}
for i := 0; i < len(s); i++ {
ch := s[i]
if (ch >= 'a' && ch <= 'z') || ch == '_' {
continue
}
if i > 0 && ch >= '0' && ch <= '9' {
continue
}
return false
}
return true
}