From d9d158e71f9840a5a0da82bb56d8021cb1820ef5 Mon Sep 17 00:00:00 2001 From: Eugene Klimov Date: Sun, 27 Oct 2024 19:25:38 +0400 Subject: [PATCH] fix https://github.com/Altinity/clickhouse-grafana/issues/648, golang part --- pkg/eval_query.go | 65 +++++++++++++++++---- pkg/eval_query_test.go | 125 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 12 deletions(-) diff --git a/pkg/eval_query.go b/pkg/eval_query.go index 9c174db37..8b276a0aa 100644 --- a/pkg/eval_query.go +++ b/pkg/eval_query.go @@ -1105,7 +1105,7 @@ func newEvalAST(isObj bool) *EvalAST { var obj map[string]interface{} var arr []interface{} if isObj { - obj = make(map[string]interface{}, 0) + obj = make(map[string]interface{}) } else { arr = make([]interface{}, 0) } @@ -1826,21 +1826,62 @@ func toAST(s string) (*EvalAST, error) { return scanner.toAST() } -func isClosured(argument string) bool { - var bracketsQueue []rune - for _, v := range argument { - switch v { - case '(': - bracketsQueue = append(bracketsQueue, v) - case ')': - if 0 < len(bracketsQueue) && bracketsQueue[len(bracketsQueue)-1] == '(' { - bracketsQueue = bracketsQueue[:len(bracketsQueue)-1] - } else { +// isClosured checks if a string has properly balanced brackets while ignoring brackets within quotes +// https://github.com/Altinity/clickhouse-grafana/issues/648 +func isClosured(str string) bool { + stack := make([]rune, 0) + isInQuote := false + var quoteType rune + + openBrackets := map[rune]rune{ + '(': ')', + '[': ']', + '{': '}', + } + + closeBrackets := map[rune]rune{ + ')': '(', + ']': '[', + '}': '{', + } + + runes := []rune(str) + for i := 0; i < len(runes); i++ { + char := runes[i] + + // Handle quotes + if (char == '\'' || char == '"' || char == '`') && (i == 0 || runes[i-1] != '\\') { + if !isInQuote { + isInQuote = true + quoteType = char + } else if char == quoteType { + isInQuote = false + quoteType = 0 + } + continue + } + + // Skip characters inside quotes + if isInQuote { + continue + } + + // Handle brackets + if _, ok := openBrackets[char]; ok { + stack = append(stack, char) + } else if closingPair, ok := closeBrackets[char]; ok { + if len(stack) == 0 { + return false + } + lastOpen := stack[len(stack)-1] + stack = stack[:len(stack)-1] // pop + if lastOpen != closingPair { return false } } } - return len(bracketsQueue) == 0 + + return len(stack) == 0 } func betweenBraces(query string) string { diff --git a/pkg/eval_query_test.go b/pkg/eval_query_test.go index 20b0e4327..5c466a493 100644 --- a/pkg/eval_query_test.go +++ b/pkg/eval_query_test.go @@ -1850,3 +1850,128 @@ func TestTableMacroProperlyEscaping(t *testing.T) { r.Equal(expQuery, actualQuery, description+" unexpected result") } + +// https://github.com/Altinity/clickhouse-grafana/issues/648 +func TestIsClosured(t *testing.T) { + // Simple brackets test cases + t.Run("handles simple brackets", func(t *testing.T) { + testCases := []struct { + input string + expected bool + }{ + {"(test)", true}, + {"[test]", true}, + {"{test}", true}, + } + + for _, tc := range testCases { + result := isClosured(tc.input) + if result != tc.expected { + t.Errorf("isClosured(%q) = %v; want %v", tc.input, result, tc.expected) + } + } + }) + + // Nested brackets test cases + t.Run("handles nested brackets", func(t *testing.T) { + testCases := []struct { + input string + expected bool + }{ + {"({[test]})", true}, + {"({[test}])", false}, + } + + for _, tc := range testCases { + result := isClosured(tc.input) + if result != tc.expected { + t.Errorf("isClosured(%q) = %v; want %v", tc.input, result, tc.expected) + } + } + }) + + // Quotes test cases + t.Run("handles quotes correctly", func(t *testing.T) { + testCases := []struct { + input string + expected bool + }{ + {"'(not a bracket)'", true}, + {"\"[also not a bracket]\"", true}, + {"`{template literal}`", true}, + } + + for _, tc := range testCases { + result := isClosured(tc.input) + if result != tc.expected { + t.Errorf("isClosured(%q) = %v; want %v", tc.input, result, tc.expected) + } + } + }) + + // Escaped quotes test cases + t.Run("handles escaped quotes", func(t *testing.T) { + testCases := []struct { + input string + expected bool + }{ + {"''(this is a real bracket)'", true}, + {"\\'(this is a bracket after escaped quotes)", true}, + } + + for _, tc := range testCases { + result := isClosured(tc.input) + if result != tc.expected { + t.Errorf("isClosured(%q) = %v; want %v", tc.input, result, tc.expected) + } + } + }) + + // Provided test cases + t.Run("handles provided test cases", func(t *testing.T) { + testCases := []struct { + input string + expected bool + }{ + {"('('+test)", true}, + {"[\"(\"+test+\"]]\"] ", true}, + {"('('+test+']]')", true}, + {"'('+test ]", false}, + {"]['('+test]", false}, + } + + for _, tc := range testCases { + result := isClosured(tc.input) + if result != tc.expected { + t.Errorf("isClosured(%q) = %v; want %v", tc.input, result, tc.expected) + } + } + }) + + // Empty input test case + t.Run("handles empty input", func(t *testing.T) { + result := isClosured("") + if !result { + t.Error("isClosured(\"\") = false; want true") + } + }) + + // Unmatched brackets test cases + t.Run("handles unmatched brackets", func(t *testing.T) { + testCases := []struct { + input string + expected bool + }{ + {"(((", false}, + {")))", false}, + {"((())", false}, + } + + for _, tc := range testCases { + result := isClosured(tc.input) + if result != tc.expected { + t.Errorf("isClosured(%q) = %v; want %v", tc.input, result, tc.expected) + } + } + }) +}