diff --git a/.changeset/moody-falcons-know.md b/.changeset/moody-falcons-know.md new file mode 100644 index 000000000..8b35c48ba --- /dev/null +++ b/.changeset/moody-falcons-know.md @@ -0,0 +1,6 @@ +--- +'@hyperdx/api': patch +'@hyperdx/app': patch +--- + +fix: Fixed parsing && and || operators in queries correctly diff --git a/packages/api/src/clickhouse/__tests__/searchQueryParser.test.ts b/packages/api/src/clickhouse/__tests__/searchQueryParser.test.ts index 2493baeea..1bc19aa16 100644 --- a/packages/api/src/clickhouse/__tests__/searchQueryParser.test.ts +++ b/packages/api/src/clickhouse/__tests__/searchQueryParser.test.ts @@ -327,6 +327,37 @@ describe('searchQueryParser', () => { }); }); + describe('operators', () => { + ( + [ + ['OR', 'OR'], + ['||', 'OR'], + ['AND', 'AND'], + ['&&', 'AND'], + [' ', 'AND'], + ['NOT', 'AND NOT'], + ['AND NOT', 'AND NOT'], + ['OR NOT', 'OR NOT'], + ] as const + ).forEach(([operator, sql]) => { + it(`parses ${operator}`, async () => { + const ast = parse(`foo ${operator} bar`); + expect( + await genWhereSQL( + ast, + propertyTypesMappingsModel, + 'TEAM_ID_UNIT_TESTS', + ), + ).toEqual( + `${hasToken(SOURCE_COL, 'foo')} ${sql} ${hasToken( + SOURCE_COL, + 'bar', + )}`, + ); + }); + }); + }); + describe('properties', () => { it('parses string 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 dc11850b7..61874205e 100644 --- a/packages/api/src/clickhouse/searchQueryParser.ts +++ b/packages/api/src/clickhouse/searchQueryParser.ts @@ -108,6 +108,7 @@ export const buildSearchColumnName = ( }; interface Serializer { + operator(op: lucene.Operator): string; eq(field: string, term: string, isNegatedField: boolean): Promise; isNotNull(field: string, isNegatedField: boolean): Promise; gte(field: string, term: string): Promise; @@ -189,6 +190,27 @@ export class SQLSerializer implements Serializer { }; } + operator(op: lucene.Operator) { + switch (op) { + case 'NOT': + case 'AND NOT': + return 'AND NOT'; + case 'OR NOT': + return 'OR NOT'; + // @ts-ignore TODO: Types need to be fixed upstream + case '&&': + case '': + case 'AND': + return 'AND'; + // @ts-ignore TODO: Types need to be fixed upstream + case '||': + case 'OR': + return 'OR'; + default: + throw new Error(`Unexpected operator. ${op}`); + } + } + async eq(field: string, term: string, isNegatedField: boolean) { const { column, found, propertyType } = await this.getColumnForField(field); if (!found) { @@ -485,8 +507,7 @@ async function serialize( // 2. LeftOnlyAST: Single term ex. "foo:bar" if ((ast as lucene.BinaryAST).right != null) { const binaryAST = ast as lucene.BinaryAST; - const operator = - binaryAST.operator === IMPLICIT_FIELD ? 'AND' : binaryAST.operator; + const operator = serializer.operator(binaryAST.operator); const parenthesized = binaryAST.parenthesized; return `${parenthesized ? '(' : ''}${await serialize( binaryAST.left, diff --git a/packages/app/src/queryv2.ts b/packages/app/src/queryv2.ts index a9e2b4312..69eb26025 100644 --- a/packages/app/src/queryv2.ts +++ b/packages/app/src/queryv2.ts @@ -25,6 +25,7 @@ export function parse(query: string): lucene.AST { const IMPLICIT_FIELD = ''; interface Serializer { + operator(op: lucene.Operator): string; eq(field: string, term: string, isNegatedField: boolean): Promise; isNotNull(field: string, isNegatedField: boolean): Promise; gte(field: string, term: string): Promise; @@ -56,6 +57,27 @@ class EnglishSerializer implements Serializer { return `'${field}'`; } + operator(op: lucene.Operator) { + switch (op) { + case 'NOT': + case 'AND NOT': + return 'AND NOT'; + case 'OR NOT': + return 'OR NOT'; + // @ts-ignore TODO: Types need to be fixed upstream + case '&&': + case '': + case 'AND': + return 'AND'; + // @ts-ignore TODO: Types need to be fixed upstream + case '||': + case 'OR': + return 'OR'; + default: + throw new Error(`Unexpected operator. ${op}`); + } + } + async eq(field: string, term: string, isNegatedField: boolean) { return `${this.translateField(field)} ${ isNegatedField ? 'is not' : 'is' @@ -284,8 +306,7 @@ async function serialize( // 2. LeftOnlyAST: Single term ex. "foo:bar" if ((ast as lucene.BinaryAST).right != null) { const binaryAST = ast as lucene.BinaryAST; - const operator = - binaryAST.operator === '' ? 'AND' : binaryAST.operator; + const operator = serializer.operator(binaryAST.operator); const parenthesized = binaryAST.parenthesized; return `${parenthesized ? '(' : ''}${await serialize( binaryAST.left,