diff --git a/src/data/sqlGenerator.test.ts b/src/data/sqlGenerator.test.ts index c2cb9c9c..95793f1c 100644 --- a/src/data/sqlGenerator.test.ts +++ b/src/data/sqlGenerator.test.ts @@ -1,4 +1,4 @@ -import { AggregateType, ColumnHint, QueryBuilderOptions, QueryType } from 'types/queryBuilder'; +import { AggregateType, ColumnHint, FilterOperator, QueryBuilderOptions, QueryType } from 'types/queryBuilder'; import { generateSql, getColumnByHint, getColumnIndexByHint, getColumnsByHints, isAggregateQuery } from './sqlGenerator'; describe('SQL Generator', () => { @@ -13,11 +13,20 @@ describe('SQL Generator', () => { { name: 'message', type: 'String', hint: ColumnHint.LogMessage }, ], limit: 1000, - filters: [], + filters: [ + { + filterType: 'custom', + key: 'message', + type: 'String', + condition: 'AND', + operator: FilterOperator.IsNotNull + } + ], orderBy: [] }; const expectedSql = ( - 'SELECT timestamp as timestamp, message as body, level as level FROM "default"."logs" LIMIT 1000' + 'SELECT timestamp as timestamp, message as body, level as level ' + + 'FROM "default"."logs" WHERE ( message IS NOT NULL ) LIMIT 1000' ); const sql = generateSql(opts); @@ -41,7 +50,17 @@ describe('SQL Generator', () => { { name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags }, ], limit: 1000, - filters: [], + filters: [ + { + filterType: 'custom', + key: '', // hint property is used instead of column name + type: 'String', + condition: 'AND', + hint: ColumnHint.TraceId, + operator: FilterOperator.Equals, + value: '1234' + } + ], orderBy: [] }; const expectedSql = ( @@ -49,7 +68,7 @@ describe('SQL Generator', () => { '"SpanName" as operationName, "Timestamp" as startTime, "Duration" as duration, ' + 'arrayMap(key -> map(\'key\', key, \'value\',"SpanAttributes"[key]), mapKeys("SpanAttributes")) as tags, ' + 'arrayMap(key -> map(\'key\', key, \'value\',"ResourceAttributes"[key]), mapKeys("ResourceAttributes")) as serviceTags ' + - 'FROM "otel"."otel_traces" ORDER BY startTime ASC LIMIT 1000' + 'FROM "otel"."otel_traces" WHERE ( TraceId = \'1234\' ) ORDER BY startTime ASC LIMIT 1000' ); const sql = generateSql(opts); @@ -77,6 +96,44 @@ describe('SQL Generator', () => { expect(sql).toEqual(expectedSql); }); + it('generates other sql with filters', () => { + const opts: QueryBuilderOptions = { + database: 'default', + table: 'data', + queryType: QueryType.Table, + columns: [ + { name: 'timestamp', type: 'DateTime' }, + { name: 'text', type: 'String' }, + ], + limit: 1000, + filters: [ + { + operator: FilterOperator.WithInGrafanaTimeRange, + filterType: 'custom', + key: 'created_at', + type: 'datetime', + condition: 'AND' + }, + { + filterType: 'custom', + key: 'event', + type: 'String', + condition: 'AND', + operator: FilterOperator.IsNotNull + } + ], + orderBy: [] + }; + const expectedSql = ( + 'SELECT "timestamp", "text" FROM "default"."data" ' + + 'WHERE ( created_at >= $__fromTime AND created_at <= $__toTime ) AND ( event IS NOT NULL ) ' + + 'LIMIT 1000' + ); + + const sql = generateSql(opts); + expect(sql).toEqual(expectedSql); + }); + it('excludes LIMIT when limit is 0', () => { const opts: QueryBuilderOptions = { database: 'default', diff --git a/src/data/sqlGenerator.ts b/src/data/sqlGenerator.ts index bb8d0a51..3e655f89 100644 --- a/src/data/sqlGenerator.ts +++ b/src/data/sqlGenerator.ts @@ -1,7 +1,6 @@ import { getSqlFromQueryBuilderOptions, getOrderBy } from 'components/queryBuilder/utils'; import { BooleanFilter, ColumnHint, DateFilterWithValue, FilterOperator, MultiFilter, NumberFilter, QueryBuilderOptions, QueryType, SelectedColumn, StringFilter, TimeUnit } from 'types/queryBuilder'; - export const generateSql = (options: QueryBuilderOptions): string => { if (options.queryType === QueryType.Traces) { return generateTraceQuery(options); @@ -102,7 +101,7 @@ const generateTraceQuery = (options: QueryBuilderOptions): string => { queryParts.push(limit); } - return queryParts.join(' '); + return concatQueryParts(queryParts); } /** @@ -166,7 +165,7 @@ const generateLogsQuery = (options: QueryBuilderOptions): string => { queryParts.push(limit); } - return queryParts.join(' '); + return concatQueryParts(queryParts); } export const isAggregateQuery = (builder: QueryBuilderOptions): boolean => (builder.aggregates?.length || 0) > 0; @@ -233,6 +232,27 @@ const getTraceDurationSelectSql = (columnIdentifier: string, timeUnit?: TimeUnit } } +/** + * Concats query parts with no empty spaces. + */ +const concatQueryParts = (parts: readonly string[]): string => { + let query = ''; + for (let i = 0; i < parts.length; i++) { + const p = parts[i]; + if (!p) { + continue; + } + + query += p; + + if (i !== parts.length - 1) { + query += ' ' + } + } + + return query; +} + const getLimit = (limit?: number | undefined): string => { limit = Math.max(0, limit || 0); if (limit > 0) { @@ -328,11 +348,11 @@ const getFilters = (options: QueryBuilderOptions): string => { } filterParts.push(')'); - const builtFilter = filterParts.join(' '); + const builtFilter = concatQueryParts(filterParts); builtFilters.push(builtFilter); } - return builtFilters.join(' '); + return concatQueryParts(builtFilters); }; const isBooleanType = (type: string): boolean => (type?.toLowerCase().startsWith('boolean'));