From e982467584b2080d4a047f1e392398bc4fad8453 Mon Sep 17 00:00:00 2001 From: Eugene Klimov Date: Sun, 27 Oct 2024 18:49:04 +0400 Subject: [PATCH 1/5] fix https://github.com/Altinity/clickhouse-grafana/issues/648 --- src/datasource/scanner/scanner.ts | 73 +++++++++++++---- src/spec/scanner_specs.jest.ts | 129 +++++++++++++++++++++--------- 2 files changed, 151 insertions(+), 51 deletions(-) diff --git a/src/datasource/scanner/scanner.ts b/src/datasource/scanner/scanner.ts index dd0cf7a8b..78523a481 100644 --- a/src/datasource/scanner/scanner.ts +++ b/src/datasource/scanner/scanner.ts @@ -133,7 +133,7 @@ const tokenRe = [ const tabSize = ' '; // 4 spaces const newLine = '\n'; -export default class Scanner { +class Scanner { tree: any; rootToken: any; token: any; @@ -173,7 +173,6 @@ export default class Scanner { } _next() { - if (this._s.length === 0) { return false; } @@ -219,7 +218,7 @@ export default class Scanner { isExpectedNext(): boolean { let v = this.expectedNext; this.expectedNext = false; - return v as boolean + return v as boolean; } appendToken(argument): string { @@ -242,7 +241,7 @@ export default class Scanner { argument += this.appendToken(argument); continue; } - if (!isClosured(argument)) { + if (!Scanner.isClosured(argument)) { argument += this.appendToken(argument); continue; } @@ -254,7 +253,7 @@ export default class Scanner { continue; } - if (this.token === ',' && isClosured(argument)) { + if (this.token === ',' && Scanner.isClosured(argument)) { this.push(argument); argument = ''; if (this.rootToken === 'where') { @@ -299,7 +298,7 @@ export default class Scanner { } if (isIn(this.token)) { - argument += ' ' +this.token; + argument += ' ' + this.token; if (!this.next()) { throw 'wrong in signature for `' + argument + '` at [' + this._s + ']'; } @@ -329,7 +328,7 @@ export default class Scanner { } if (isCond(this.token) && (this.rootToken === 'where' || this.rootToken === 'prewhere')) { - if (isClosured(argument)) { + if (Scanner.isClosured(argument)) { this.push(argument); argument = this.token; } else { @@ -377,7 +376,6 @@ export default class Scanner { } argument += this.appendToken(argument); - } if (argument !== '') { @@ -488,9 +486,59 @@ export default class Scanner { } static AddMetadata(query) { - return "/* grafana dashboard=$__dashboard, user=$__user */\n" + query - } + return '/* grafana dashboard=$__dashboard, user=$__user */\n' + query; + } + + static isClosured(str) { + const stack: string[] = []; + let isInQuote = false; + let quoteType = null; + + const openBrackets = { + '(': ')', + '[': ']', + '{': '}', + }; + + const closeBrackets = { + ')': '(', + ']': '[', + '}': '{', + }; + + for (let i = 0; i < str.length; i++) { + const char = str[i]; + + // Handle quotes + if ((char === "'" || char === '"' || char === '`') && (i === 0 || str[i - 1] !== '\\')) { + if (!isInQuote) { + isInQuote = true; + quoteType = char; + } else if (char === quoteType) { + isInQuote = false; + quoteType = null; + } + continue; + } + + // Skip characters inside quotes + if (isInQuote) { + continue; + } + + // Handle brackets + if (char in openBrackets) { + stack.push(char); + } else if (char in closeBrackets) { + const lastOpen = stack.pop(); + if (lastOpen !== closeBrackets[char]) { + return false; + } + } + } + return stack.length === 0; + } } const isSkipSpace = (token: string) => skipSpaceOnlyRe.test(token); const isCond = (token: string) => condOnlyRe.test(token); @@ -537,9 +585,6 @@ function isSet(obj, prop) { return obj.hasOwnProperty(prop) && !isEmpty(obj[prop]); } -function isClosured(argument) { - return (argument.match(/\(/g) || []).length === (argument.match(/\)/g) || []).length; -} function betweenBraces(query) { let openBraces = 1, @@ -713,4 +758,4 @@ function print(AST, tab = '') { } - +export default Scanner; diff --git a/src/spec/scanner_specs.jest.ts b/src/spec/scanner_specs.jest.ts index a117febbc..2adc76fa7 100644 --- a/src/spec/scanner_specs.jest.ts +++ b/src/spec/scanner_specs.jest.ts @@ -1,6 +1,6 @@ import Scanner from '../datasource/scanner/scanner'; -describe('scanner:', () => { +describe('Scanner:', () => { describe('AST case 1', () => { let query = 'SELECT EventDate, col1, col2, toUInt32(col1 > 0 ? col2/col1*10000 : 0)/100 AS percent ' + @@ -648,14 +648,12 @@ describe('scanner:', () => { const scanner = new Scanner(query); let expectedAST = { - root: [], - '$columns': [ 'service_name', 'count() c' ], - select: [], - from: [ '$table' ], - where: [ "service_name IN ['mysql', 'postgresql'] AND $timeFilter" ] - } - ; - + root: [], + $columns: ['service_name', 'count() c'], + select: [], + from: ['$table'], + where: ["service_name IN ['mysql', 'postgresql'] AND $timeFilter"], + }; it('expects equality', () => { expect(scanner.toAST()).toEqual(expectedAST); }); @@ -663,15 +661,20 @@ describe('scanner:', () => { /* https://github.com/Altinity/clickhouse-grafana/issues/386 */ describe('AST case 24 $rateColumnsAggregated', () => { let query = - '/* comment */ $rateColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) '+ - " FROM traffic WHERE datacenter = 'dc1' HAVING rx_bytes > $interval", + '/* comment */ $rateColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) ' + + " FROM traffic WHERE datacenter = 'dc1' HAVING rx_bytes > $interval", scanner = new Scanner(query); let expectedAST = { - root: [ - "/* comment */\n" + root: ['/* comment */\n'], + $rateColumnsAggregated: [ + 'datacenter', + 'concat(datacenter, interface) AS dc_interface', + 'sum', + 'tx_bytes * 1024 AS tx_kbytes', + 'sum', + 'max(rx_bytes) AS rx_bytes', ], - $rateColumnsAggregated: ["datacenter", "concat(datacenter, interface) AS dc_interface", "sum", "tx_bytes * 1024 AS tx_kbytes", "sum", "max(rx_bytes) AS rx_bytes",], select: [], from: ['traffic'], where: ["datacenter = 'dc1'"], @@ -686,15 +689,20 @@ describe('scanner:', () => { /* https://github.com/Altinity/clickhouse-grafana/issues/386 */ describe('AST case 25 $perSecondColumnsAggregated', () => { let query = - '/* comment */ $perSecondColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) '+ + '/* comment */ $perSecondColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) ' + " FROM traffic WHERE datacenter = 'dc1' HAVING rx_bytes > $interval", scanner = new Scanner(query); let expectedAST = { - root: [ - "/* comment */\n" + root: ['/* comment */\n'], + $perSecondColumnsAggregated: [ + 'datacenter', + 'concat(datacenter, interface) AS dc_interface', + 'sum', + 'tx_bytes * 1024 AS tx_kbytes', + 'sum', + 'max(rx_bytes) AS rx_bytes', ], - $perSecondColumnsAggregated: ["datacenter", "concat(datacenter, interface) AS dc_interface", "sum", "tx_bytes * 1024 AS tx_kbytes", "sum", "max(rx_bytes) AS rx_bytes",], select: [], from: ['traffic'], where: ["datacenter = 'dc1'"], @@ -705,19 +713,24 @@ describe('scanner:', () => { expect(scanner.toAST()).toEqual(expectedAST); }); }); - + /* https://github.com/Altinity/clickhouse-grafana/issues/386 */ describe('AST case 26 $increaseColumnsAggregated', () => { let query = - '/* comment */ $increaseColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) '+ + '/* comment */ $increaseColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) ' + " FROM traffic WHERE datacenter = 'dc1' HAVING rx_bytes > $interval", scanner = new Scanner(query); let expectedAST = { - root: [ - "/* comment */\n" + root: ['/* comment */\n'], + $increaseColumnsAggregated: [ + 'datacenter', + 'concat(datacenter, interface) AS dc_interface', + 'sum', + 'tx_bytes * 1024 AS tx_kbytes', + 'sum', + 'max(rx_bytes) AS rx_bytes', ], - $increaseColumnsAggregated: ["datacenter", "concat(datacenter, interface) AS dc_interface", "sum", "tx_bytes * 1024 AS tx_kbytes", "sum", "max(rx_bytes) AS rx_bytes",], select: [], from: ['traffic'], where: ["datacenter = 'dc1'"], @@ -732,15 +745,20 @@ describe('scanner:', () => { /* https://github.com/Altinity/clickhouse-grafana/issues/386 */ describe('AST case 27 $deltaColumnsAggregated', () => { let query = - '/* comment */ $deltaColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) '+ + '/* comment */ $deltaColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) ' + " FROM traffic WHERE datacenter = 'dc1' HAVING rx_bytes > $interval", scanner = new Scanner(query); let expectedAST = { - root: [ - "/* comment */\n" + root: ['/* comment */\n'], + $deltaColumnsAggregated: [ + 'datacenter', + 'concat(datacenter, interface) AS dc_interface', + 'sum', + 'tx_bytes * 1024 AS tx_kbytes', + 'sum', + 'max(rx_bytes) AS rx_bytes', ], - $deltaColumnsAggregated: ["datacenter", "concat(datacenter, interface) AS dc_interface", "sum", "tx_bytes * 1024 AS tx_kbytes", "sum", "max(rx_bytes) AS rx_bytes",], select: [], from: ['traffic'], where: ["datacenter = 'dc1'"], @@ -754,20 +772,21 @@ describe('scanner:', () => { /* https://github.com/Altinity/clickhouse-grafana/issues/409 */ describe('AST case 28 $columns + ORDER BY WITH FILL', () => { - let query = "$columns(\n"+ - " service_name, \n"+ - " sum(agg_value) as value\n"+ - ")\n"+ - "FROM $table\n" + - "WHERE service_name='mysql'\n" + - "GROUP BY t, service_name\n" + - "HAVING value>100\n"+ - "ORDER BY t, service_name WITH FILL 60000", + let query = + '$columns(\n' + + ' service_name, \n' + + ' sum(agg_value) as value\n' + + ')\n' + + 'FROM $table\n' + + "WHERE service_name='mysql'\n" + + 'GROUP BY t, service_name\n' + + 'HAVING value>100\n' + + 'ORDER BY t, service_name WITH FILL 60000', scanner = new Scanner(query); let expectedAST = { root: [], - $columns: ["service_name", "sum(agg_value) as value", ], + $columns: ['service_name', 'sum(agg_value) as value'], select: [], from: ['$table'], where: ["service_name = 'mysql'"], @@ -780,5 +799,41 @@ describe('scanner:', () => { expect(scanner.toAST()).toEqual(expectedAST); }); }); +}); + +// https://github.com/Altinity/clickhouse-grafana/issues/648 +describe('Scanner.isClosured: ', () => { + test('handles simple brackets', () => { + expect(Scanner.isClosured('(test)')).toBe(true); + expect(Scanner.isClosured('[test]')).toBe(true); + expect(Scanner.isClosured('{test}')).toBe(true); + }); + test('handles nested brackets', () => { + expect(Scanner.isClosure'\'(not a bracket)\''Be(true); + expect(Scanner.isClosured('({[test}])')).toBe(false); + }); + + test('handles quotes correctly', () => { + expect(Scanner.isClosured("'(not a bracket)'")); // Ignores brackets in quotes + expect(Scanner.isClosured('"[also not a bra'\'\'(this is a real bracket)\''ect(Scanner.isClosured('`{template literal}`''\\\'(this is a bracket after escaped quotes)' quotes', () => { + expect(Scanner.isClosured("''(this is a real bracket)'")).toBe(true); + ex'(\'(\'+test)'.isClosured("\\'(this is a bracket after escaped quotes)")).toBe(true); + }); + + test('handles provided test'(\'(\'+test+\']]\')' expect(Scanner.isClosured("('('+test)")).'\'(\'+test ]' expect(Scanner.isClosured('["("+test+"]]"]'][\'(\'+test]'e); + expect(Scanner.isClosured("('('+test+']]')")).toBe(true); + expect(Scanner.isClosured("'('+test ]")).toBe(false); + expect(Scanner.isClosured("]['('+test]")).toBe(false); + }); + + test('handles empty input', () => { + expect(Scanner.isClosured('')).toBe(true); + }); + + test('handles unmatched brackets', () => { + expect(Scanner.isClosured('(((')).toBe(false); + expect(Scanner.isClosured(')))')).toBe(false); + expect(Scanner.isClosured('((())')).toBe(false); + }); }); From 6bf1bd0afb3af521791c4b228decb8ce3813cc96 Mon Sep 17 00:00:00 2001 From: Eugene Klimov Date: Sun, 27 Oct 2024 18:49:45 +0400 Subject: [PATCH 2/5] fix https://github.com/Altinity/clickhouse-grafana/issues/648 --- src/spec/scanner_specs.jest.ts | 67 +++++++++++++--------------------- 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/src/spec/scanner_specs.jest.ts b/src/spec/scanner_specs.jest.ts index 2adc76fa7..dbe7838f2 100644 --- a/src/spec/scanner_specs.jest.ts +++ b/src/spec/scanner_specs.jest.ts @@ -662,19 +662,13 @@ describe('Scanner:', () => { describe('AST case 24 $rateColumnsAggregated', () => { let query = '/* comment */ $rateColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) ' + - " FROM traffic WHERE datacenter = 'dc1' HAVING rx_bytes > $interval", - scanner = new Scanner(query); + " FROM traffic WHERE dat' FROM traffic WHERE datacenter = \'dc1\' HAVING rx_bytes > $interval'anner(query); let expectedAST = { - root: ['/* comment */\n'], - $rateColumnsAggregated: [ - 'datacenter', - 'concat(datacenter, interface) AS dc_interface', - 'sum', - 'tx_bytes * 1024 AS tx_kbytes', - 'sum', - 'max(rx_bytes) AS rx_bytes', + root: [ + '/* comment */\n', ], + $rateColumnsAggregated: ['datacenter', 'concat(datacenter, interface) AS dc_interface', 'sum', 'tx_bytes * 1024 AS tx_kbytes', 'sum', 'max(rx_bytes) AS rx_bytes'], select: [], from: ['traffic'], where: ["datacenter = 'dc1'"], @@ -694,15 +688,10 @@ describe('Scanner:', () => { scanner = new Scanner(query); let expectedAST = { - root: ['/* comment */\n'], - $perSecondColumnsAggregated: [ - 'datacenter', - 'concat(datacenter, interface) AS dc_interface', - 'sum', - 'tx_bytes * 1024 AS tx_kbytes', - 'sum', - 'max(rx_bytes) AS rx_bytes', + root: [ + '/* comment */\n', ], + $perSecondColumnsAggregated: ['datacenter', 'concat(datacenter, interface) AS dc_interface', 'sum', 'tx_bytes * 1024 AS tx_kbytes', 'sum', 'max(rx_bytes) AS rx_bytes'], select: [], from: ['traffic'], where: ["datacenter = 'dc1'"], @@ -722,15 +711,10 @@ describe('Scanner:', () => { scanner = new Scanner(query); let expectedAST = { - root: ['/* comment */\n'], - $increaseColumnsAggregated: [ - 'datacenter', - 'concat(datacenter, interface) AS dc_interface', - 'sum', - 'tx_bytes * 1024 AS tx_kbytes', - 'sum', - 'max(rx_bytes) AS rx_bytes', + root: [ + '/* comment */\n', ], + $increaseColumnsAggregated: ['datacenter', 'concat(datacenter, interface) AS dc_interface', 'sum', 'tx_bytes * 1024 AS tx_kbytes', 'sum', 'max(rx_bytes) AS rx_bytes'], select: [], from: ['traffic'], where: ["datacenter = 'dc1'"], @@ -750,15 +734,10 @@ describe('Scanner:', () => { scanner = new Scanner(query); let expectedAST = { - root: ['/* comment */\n'], - $deltaColumnsAggregated: [ - 'datacenter', - 'concat(datacenter, interface) AS dc_interface', - 'sum', - 'tx_bytes * 1024 AS tx_kbytes', - 'sum', - 'max(rx_bytes) AS rx_bytes', + root: [ + '/* comment */\n', ], + $deltaColumnsAggregated: ['datacenter', 'concat(datacenter, interface) AS dc_interface', 'sum', 'tx_bytes * 1024 AS tx_kbytes', 'sum', 'max(rx_bytes) AS rx_bytes'], select: [], from: ['traffic'], where: ["datacenter = 'dc1'"], @@ -772,13 +751,12 @@ describe('Scanner:', () => { /* https://github.com/Altinity/clickhouse-grafana/issues/409 */ describe('AST case 28 $columns + ORDER BY WITH FILL', () => { - let query = - '$columns(\n' + + let query = '$columns(\n' + ' service_name, \n' + ' sum(agg_value) as value\n' + ')\n' + 'FROM $table\n' + - "WHERE service_name='mysql'\n" + + 'WHERE service_name=\'mysql\'\n' + 'GROUP BY t, service_name\n' + 'HAVING value>100\n' + 'ORDER BY t, service_name WITH FILL 60000', @@ -799,6 +777,7 @@ describe('Scanner:', () => { expect(scanner.toAST()).toEqual(expectedAST); }); }); + }); // https://github.com/Altinity/clickhouse-grafana/issues/648 @@ -810,18 +789,24 @@ describe('Scanner.isClosured: ', () => { }); test('handles nested brackets', () => { - expect(Scanner.isClosure'\'(not a bracket)\''Be(true); + expect(Scanner.isClosured('({[test]})')).toBe(true); expect(Scanner.isClosured('({[test}])')).toBe(false); }); test('handles quotes correctly', () => { expect(Scanner.isClosured("'(not a bracket)'")); // Ignores brackets in quotes - expect(Scanner.isClosured('"[also not a bra'\'\'(this is a real bracket)\''ect(Scanner.isClosured('`{template literal}`''\\\'(this is a bracket after escaped quotes)' quotes', () => { + expect(Scanner.isClosured('"[also not a bracket]"')).toBe(true); + expect(Scanner.isClosured('`{template literal}`')).toBe(true); + }); + + test('handles escaped quotes', () => { expect(Scanner.isClosured("''(this is a real bracket)'")).toBe(true); - ex'(\'(\'+test)'.isClosured("\\'(this is a bracket after escaped quotes)")).toBe(true); + expect(Scanner.isClosured('\\\'(this is a bracket after escaped quotes)')).toBe(true); }); - test('handles provided test'(\'(\'+test+\']]\')' expect(Scanner.isClosured("('('+test)")).'\'(\'+test ]' expect(Scanner.isClosured('["("+test+"]]"]'][\'(\'+test]'e); + test('handles provided test cases', () => { + expect(Scanner.isClosured('(\'(\'+test)')).toBe(true); + expect(Scanner.isClosured('["("+test+"]]"] ')).toBe(true); expect(Scanner.isClosured("('('+test+']]')")).toBe(true); expect(Scanner.isClosured("'('+test ]")).toBe(false); expect(Scanner.isClosured("]['('+test]")).toBe(false); From a97f6d3e9f3f2314b0b1cab021365808875477a2 Mon Sep 17 00:00:00 2001 From: Eugene Klimov Date: Sun, 27 Oct 2024 19:03:15 +0400 Subject: [PATCH 3/5] fix https://github.com/Altinity/clickhouse-grafana/issues/648, wtf IDEA reformat code? Signed-off-by: Eugene Klimov --- src/spec/scanner_specs.jest.ts | 61 ++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/src/spec/scanner_specs.jest.ts b/src/spec/scanner_specs.jest.ts index dbe7838f2..f20f9acc9 100644 --- a/src/spec/scanner_specs.jest.ts +++ b/src/spec/scanner_specs.jest.ts @@ -648,12 +648,14 @@ describe('Scanner:', () => { const scanner = new Scanner(query); let expectedAST = { - root: [], - $columns: ['service_name', 'count() c'], - select: [], - from: ['$table'], - where: ["service_name IN ['mysql', 'postgresql'] AND $timeFilter"], - }; + root: [], + '$columns': [ 'service_name', 'count() c' ], + select: [], + from: [ '$table' ], + where: [ "service_name IN ['mysql', 'postgresql'] AND $timeFilter" ] + } + ; + it('expects equality', () => { expect(scanner.toAST()).toEqual(expectedAST); }); @@ -661,14 +663,15 @@ describe('Scanner:', () => { /* https://github.com/Altinity/clickhouse-grafana/issues/386 */ describe('AST case 24 $rateColumnsAggregated', () => { let query = - '/* comment */ $rateColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) ' + - " FROM traffic WHERE dat' FROM traffic WHERE datacenter = \'dc1\' HAVING rx_bytes > $interval'anner(query); + '/* comment */ $rateColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) '+ + " FROM traffic WHERE datacenter = 'dc1' HAVING rx_bytes > $interval", + scanner = new Scanner(query); let expectedAST = { root: [ - '/* comment */\n', + "/* comment */\n" ], - $rateColumnsAggregated: ['datacenter', 'concat(datacenter, interface) AS dc_interface', 'sum', 'tx_bytes * 1024 AS tx_kbytes', 'sum', 'max(rx_bytes) AS rx_bytes'], + $rateColumnsAggregated: ["datacenter", "concat(datacenter, interface) AS dc_interface", "sum", "tx_bytes * 1024 AS tx_kbytes", "sum", "max(rx_bytes) AS rx_bytes",], select: [], from: ['traffic'], where: ["datacenter = 'dc1'"], @@ -683,15 +686,15 @@ describe('Scanner:', () => { /* https://github.com/Altinity/clickhouse-grafana/issues/386 */ describe('AST case 25 $perSecondColumnsAggregated', () => { let query = - '/* comment */ $perSecondColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) ' + + '/* comment */ $perSecondColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) '+ " FROM traffic WHERE datacenter = 'dc1' HAVING rx_bytes > $interval", scanner = new Scanner(query); let expectedAST = { root: [ - '/* comment */\n', + "/* comment */\n" ], - $perSecondColumnsAggregated: ['datacenter', 'concat(datacenter, interface) AS dc_interface', 'sum', 'tx_bytes * 1024 AS tx_kbytes', 'sum', 'max(rx_bytes) AS rx_bytes'], + $perSecondColumnsAggregated: ["datacenter", "concat(datacenter, interface) AS dc_interface", "sum", "tx_bytes * 1024 AS tx_kbytes", "sum", "max(rx_bytes) AS rx_bytes",], select: [], from: ['traffic'], where: ["datacenter = 'dc1'"], @@ -706,15 +709,15 @@ describe('Scanner:', () => { /* https://github.com/Altinity/clickhouse-grafana/issues/386 */ describe('AST case 26 $increaseColumnsAggregated', () => { let query = - '/* comment */ $increaseColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) ' + + '/* comment */ $increaseColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) '+ " FROM traffic WHERE datacenter = 'dc1' HAVING rx_bytes > $interval", scanner = new Scanner(query); let expectedAST = { root: [ - '/* comment */\n', + "/* comment */\n" ], - $increaseColumnsAggregated: ['datacenter', 'concat(datacenter, interface) AS dc_interface', 'sum', 'tx_bytes * 1024 AS tx_kbytes', 'sum', 'max(rx_bytes) AS rx_bytes'], + $increaseColumnsAggregated: ["datacenter", "concat(datacenter, interface) AS dc_interface", "sum", "tx_bytes * 1024 AS tx_kbytes", "sum", "max(rx_bytes) AS rx_bytes",], select: [], from: ['traffic'], where: ["datacenter = 'dc1'"], @@ -729,15 +732,15 @@ describe('Scanner:', () => { /* https://github.com/Altinity/clickhouse-grafana/issues/386 */ describe('AST case 27 $deltaColumnsAggregated', () => { let query = - '/* comment */ $deltaColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) ' + + '/* comment */ $deltaColumnsAggregated(datacenter, concat(datacenter,interface) AS dc_interface, sum, tx_bytes * 1024 AS tx_kbytes, sum, max(rx_bytes) AS rx_bytes) '+ " FROM traffic WHERE datacenter = 'dc1' HAVING rx_bytes > $interval", scanner = new Scanner(query); let expectedAST = { root: [ - '/* comment */\n', + "/* comment */\n" ], - $deltaColumnsAggregated: ['datacenter', 'concat(datacenter, interface) AS dc_interface', 'sum', 'tx_bytes * 1024 AS tx_kbytes', 'sum', 'max(rx_bytes) AS rx_bytes'], + $deltaColumnsAggregated: ["datacenter", "concat(datacenter, interface) AS dc_interface", "sum", "tx_bytes * 1024 AS tx_kbytes", "sum", "max(rx_bytes) AS rx_bytes",], select: [], from: ['traffic'], where: ["datacenter = 'dc1'"], @@ -751,20 +754,20 @@ describe('Scanner:', () => { /* https://github.com/Altinity/clickhouse-grafana/issues/409 */ describe('AST case 28 $columns + ORDER BY WITH FILL', () => { - let query = '$columns(\n' + - ' service_name, \n' + - ' sum(agg_value) as value\n' + - ')\n' + - 'FROM $table\n' + - 'WHERE service_name=\'mysql\'\n' + - 'GROUP BY t, service_name\n' + - 'HAVING value>100\n' + - 'ORDER BY t, service_name WITH FILL 60000', + let query = "$columns(\n"+ + " service_name, \n"+ + " sum(agg_value) as value\n"+ + ")\n"+ + "FROM $table\n" + + "WHERE service_name='mysql'\n" + + "GROUP BY t, service_name\n" + + "HAVING value>100\n"+ + "ORDER BY t, service_name WITH FILL 60000", scanner = new Scanner(query); let expectedAST = { root: [], - $columns: ['service_name', 'sum(agg_value) as value'], + $columns: ["service_name", "sum(agg_value) as value", ], select: [], from: ['$table'], where: ["service_name = 'mysql'"], From d9d158e71f9840a5a0da82bb56d8021cb1820ef5 Mon Sep 17 00:00:00 2001 From: Eugene Klimov Date: Sun, 27 Oct 2024 19:25:38 +0400 Subject: [PATCH 4/5] 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) + } + } + }) +} From 202249d11a67005b38a95a9ec5e419aaa7c3cc16 Mon Sep 17 00:00:00 2001 From: Eugene Klimov Date: Sun, 27 Oct 2024 19:39:33 +0400 Subject: [PATCH 5/5] update go.mod --- go.mod | 14 +++++++------- go.sum | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 15c557e19..35f0aec4d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23 require ( github.com/andybalholm/brotli v1.1.1 github.com/dlclark/regexp2 v1.11.4 - github.com/grafana/grafana-plugin-sdk-go v0.255.0 + github.com/grafana/grafana-plugin-sdk-go v0.258.0 github.com/klauspost/compress v1.17.11 github.com/stretchr/testify v1.9.0 golang.org/x/sync v0.8.0 @@ -18,11 +18,11 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheekybits/genny v1.0.0 // indirect - github.com/chromedp/cdproto v0.0.0-20241014181340-cb3a7a1d51d7 // indirect + github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/elazarl/goproxy v0.0.0-20240909085733-6741dbfc16a1 // indirect - github.com/fatih/color v1.17.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/getkin/kin-openapi v0.128.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -41,7 +41,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect - github.com/hashicorp/go-plugin v1.6.1 // indirect + github.com/hashicorp/go-plugin v1.6.2 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -65,7 +65,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.60.0 // indirect + github.com/prometheus/common v0.60.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -92,8 +92,8 @@ require ( golang.org/x/text v0.19.0 // indirect golang.org/x/tools v0.26.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect diff --git a/go.sum b/go.sum index d52530c8a..cf3841fc4 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/chromedp/cdproto v0.0.0-20240426225625-909263490071 h1:RdCf9hH3xq5vJi github.com/chromedp/cdproto v0.0.0-20240426225625-909263490071/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/cdproto v0.0.0-20241014181340-cb3a7a1d51d7 h1:VDBgUGgdCBw9lTKwp0KPExhnqmGfGVJQTER2MehoICk= github.com/chromedp/cdproto v0.0.0-20241014181340-cb3a7a1d51d7/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb h1:noKVm2SsG4v0Yd0lHNtFYc9EUxIVvrr4kJ6hM8wvIYU= +github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -47,6 +49,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= @@ -90,6 +94,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.250.0 h1:9EBucp9jLqMx2b8NTlOXH+4OuQW github.com/grafana/grafana-plugin-sdk-go v0.250.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= github.com/grafana/grafana-plugin-sdk-go v0.255.0 h1:e9bmDTdpR6Ho9ru+UhyVoWguVSSGFgInRjOsl4AftBQ= github.com/grafana/grafana-plugin-sdk-go v0.255.0/go.mod h1:sE25SkFQSj8DwX4qkF7w++6KL6mzOXo1ycDlk9GgFGw= +github.com/grafana/grafana-plugin-sdk-go v0.258.0 h1:rWsaD+5wuGUSNr9fFnSwS6t/jcRtAoEJ51pIR9bbPNs= +github.com/grafana/grafana-plugin-sdk-go v0.258.0/go.mod h1:jN19FbzhAcPTLPIy31X5nvx38rR5eoD/1rASiip0GBY= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= @@ -106,6 +112,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= +github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= +github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= @@ -189,6 +197,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= +github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -359,10 +369,14 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1: google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 h1:2oV8dfuIkM1Ti7DwXc0BJfnwr9csz4TDXI9EmiI+Rbw= +google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38/go.mod h1:vuAjtvlwkDKF6L1GQ0SokiRLCGFfeBUXWr/aFFkHACc= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=