diff --git a/.changeset/angry-seahorses-sort.md b/.changeset/angry-seahorses-sort.md new file mode 100644 index 000000000..9a8fd68a2 --- /dev/null +++ b/.changeset/angry-seahorses-sort.md @@ -0,0 +1,9 @@ +--- +'@hyperdx/api': minor +'@hyperdx/app': minor +--- + +Breaking Search Syntax Change: Backslashes will be treated as an escape +character for a double quotes (ex. message:"\"" will search for the double quote +character). Two backslashes will be treated as a backslash literal (ex. +message:\\ will search for the backslash literal) diff --git a/packages/api/src/clickhouse/__tests__/searchQueryParser.test.ts b/packages/api/src/clickhouse/__tests__/searchQueryParser.test.ts index 8e90b0d88..2493baeea 100644 --- a/packages/api/src/clickhouse/__tests__/searchQueryParser.test.ts +++ b/packages/api/src/clickhouse/__tests__/searchQueryParser.test.ts @@ -460,6 +460,36 @@ describe('searchQueryParser', () => { ).toEqual('(1 = 0)'); }); + it('parses escaped quotes in quoted searches', async () => { + const ast = parse('foo:"b\\"ar"'); + jest.spyOn(propertyTypesMappingsModel, 'get').mockReturnValue('string'); + expect( + await genWhereSQL( + ast, + propertyTypesMappingsModel, + 'TEAM_ID_UNIT_TESTS', + ), + ).toEqual(eq("_string_attributes['foo']", 'b\\"ar')); + }); + + it('parses backslash literals', async () => { + const ast = parse('foo:"b\\\\ar"'); + jest.spyOn(propertyTypesMappingsModel, 'get').mockReturnValue('string'); + expect( + await genWhereSQL( + ast, + propertyTypesMappingsModel, + 'TEAM_ID_UNIT_TESTS', + ), + ).toEqual(eq("_string_attributes['foo']", 'b\\\\ar')); + }); + + it('does not escape quotes with backslash literals', async () => { + expect(() => parse('foo:"b\\\\"ar"')).toThrowErrorMatchingInlineSnapshot( + `"Expected \\"\\\\\\"\\", \\"\\\\\\\\\\", or any character but end of input found."`, + ); + }); + describe('negation', () => { it('negates property values', async () => { const ast = parse('-foo:bar'); diff --git a/packages/api/src/clickhouse/searchQueryParser.ts b/packages/api/src/clickhouse/searchQueryParser.ts index 0e989163e..dc11850b7 100644 --- a/packages/api/src/clickhouse/searchQueryParser.ts +++ b/packages/api/src/clickhouse/searchQueryParser.ts @@ -8,6 +8,7 @@ import { PropertyTypeMappingsModel } from './propertyTypeMappingsModel'; function encodeSpecialTokens(query: string): string { return query + .replace(/\\\\/g, 'HDX_BACKSLASH_LITERAL') .replace('http://', 'http_COLON_//') .replace('https://', 'https_COLON_//') .replace(/localhost:(\d{1,5})/, 'localhost_COLON_$1') @@ -15,6 +16,8 @@ function encodeSpecialTokens(query: string): string { } function decodeSpecialTokens(query: string): string { return query + .replace(/\\"/g, '"') + .replace(/HDX_BACKSLASH_LITERAL/g, '\\') .replace('http_COLON_//', 'http://') .replace('https_COLON_//', 'https://') .replace(/localhost_COLON_(\d{1,5})/, 'localhost:$1') diff --git a/packages/app/src/DBQuerySidePanel.tsx b/packages/app/src/DBQuerySidePanel.tsx index 2b752ddc2..2d301b600 100644 --- a/packages/app/src/DBQuerySidePanel.tsx +++ b/packages/app/src/DBQuerySidePanel.tsx @@ -44,7 +44,7 @@ export default function DBQuerySidePanel() { const scopeWhereQuery = React.useCallback( (where: string) => { const spanNameQuery = dbQuery - ? `${DB_STATEMENT_PROPERTY}:"${dbQuery}" ` + ? `${DB_STATEMENT_PROPERTY}:"${dbQuery.replace(/"/g, '\\"')}" ` : ''; const whereQuery = where ? `(${where})` : ''; const serviceQuery = service ? `service:"${service}" ` : ''; diff --git a/packages/app/src/queryv2.ts b/packages/app/src/queryv2.ts index 25d710476..a9e2b4312 100644 --- a/packages/app/src/queryv2.ts +++ b/packages/app/src/queryv2.ts @@ -2,6 +2,7 @@ import lucene from '@hyperdx/lucene'; function encodeSpecialTokens(query: string): string { return query + .replace(/\\\\/g, 'HDX_BACKSLASH_LITERAL') .replace('http://', 'http_COLON_//') .replace('https://', 'https_COLON_//') .replace(/localhost:(\d{1,5})/, 'localhost_COLON_$1') @@ -9,6 +10,8 @@ function encodeSpecialTokens(query: string): string { } function decodeSpecialTokens(query: string): string { return query + .replace(/\\"/g, '"') + .replace(/HDX_BACKSLASH_LITERAL/g, '\\') .replace('http_COLON_//', 'http://') .replace('https_COLON_//', 'https://') .replace(/localhost_COLON_(\d{1,5})/, 'localhost:$1')