diff --git a/src/components/queryBuilder/utils.ts b/src/components/queryBuilder/utils.ts index 24f6daa..1d05587 100644 --- a/src/components/queryBuilder/utils.ts +++ b/src/components/queryBuilder/utils.ts @@ -1,14 +1,18 @@ import { astVisitor, Expr, - ExprBinary, ExprBool, - ExprCall, ExprCast, + ExprBinary, + ExprBool, + ExprCall, + ExprCast, ExprInteger, - ExprList, ExprNumeric, + ExprList, + ExprNumeric, ExprRef, ExprString, ExprUnary, - FromTable, IAstVisitor, + FromTable, + IAstVisitor, SelectedColumn, } from '@questdb/sql-ast-parser'; import { @@ -26,19 +30,20 @@ import { OrderBy, SampleByAlignToMode, SqlBuilderOptions, - SqlBuilderOptionsAggregate, SqlBuilderOptionsList, + SqlBuilderOptionsAggregate, + SqlBuilderOptionsList, SqlBuilderOptionsTrend, } from 'types'; -import {sqlToStatement} from 'data/ast'; -import {Datasource} from "../../data/QuestDbDatasource"; -import {isString} from "lodash"; +import { sqlToStatement } from 'data/ast'; +import { Datasource } from '../../data/QuestDbDatasource'; +import { isString } from 'lodash'; export const isBooleanType = (type: string): boolean => { return ['boolean'].includes(type?.toLowerCase()); }; export const isGeoHashType = (type: string): boolean => { - return type?.toLowerCase().startsWith("geohash") + return type?.toLowerCase().startsWith('geohash'); }; export const isNumberType = (type: string): boolean => { @@ -84,7 +89,7 @@ export const isDateFilter = (filter: Filter): filter is DateFilter => { }; export const isMultiFilter = (filter: Filter): filter is MultiFilter => { - return FilterOperator.In === filter.operator || FilterOperator.NotIn === filter.operator; + return FilterOperator.In === filter.operator || FilterOperator.NotIn === filter.operator; }; export const isSetFilter = (filter: Filter): filter is MultiFilter => { @@ -96,13 +101,13 @@ const getListQuery = (table = '', fields: string[] = []): string => { return `SELECT ${fields.join(', ')} FROM ${escaped(table)}`; }; -const getLatestOn = (timeField = '', partitionBy: string[] = []): string => { - if ( timeField.length === 0 || partitionBy.length === 0 ){ +const getLatestOn = (timeField = '', partitionBy: string[] = []): string => { + if (timeField.length === 0 || partitionBy.length === 0) { return ''; } return ` LATEST ON ${timeField} PARTITION BY ${partitionBy.join(', ')}`; -} +}; const getAggregationQuery = ( table = '', @@ -154,7 +159,7 @@ const getSampleByQuery = ( return `SELECT ${metricsQuery} FROM ${escaped(table)}`; }; -const getFilters = (filters: Filter[]): {filters: string; hasTimeFilter: boolean} => { +const getFilters = (filters: Filter[]): { filters: string; hasTimeFilter: boolean } => { let hasTsFilter = false; let combinedFilters = filters.reduce((previousValue, currentFilter, currentIndex) => { @@ -170,26 +175,26 @@ const getFilters = (filters: Filter[]): {filters: string; hasTimeFilter: boolean } else if (currentFilter.operator === FilterOperator.OutsideGrafanaTimeRange) { operator = ''; notOperator = true; - field = ` \$__timeFilter(${currentFilter.key})` + field = ` \$__timeFilter(${currentFilter.key})`; hasTsFilter = true; } else if (FilterOperator.WithInGrafanaTimeRange === currentFilter.operator) { operator = ''; - field = ` \$__timeFilter(${currentFilter.key})` + field = ` \$__timeFilter(${currentFilter.key})`; hasTsFilter = true; } else { operator = currentFilter.operator; } - if ( operator.length > 0 ){ + if (operator.length > 0) { filter = ` ${field} ${operator}`; } else { - filter = ` ${field}` + filter = ` ${field}`; } if (isNullFilter(currentFilter)) { // don't add anything } else if (isMultiFilter(currentFilter)) { let values = currentFilter.value; - if (isNumberType(currentFilter.type)){ + if (isNumberType(currentFilter.type)) { filter += ` (${values?.map((v) => v.trim()).join(', ')} )`; } else { filter += ` (${values?.map((v) => formatStringValue(v).trim()).join(', ')} )`; @@ -202,56 +207,58 @@ const getFilters = (filters: Filter[]): {filters: string; hasTimeFilter: boolean if (!isDateFilterWithOutValue(currentFilter)) { switch (currentFilter.value) { case 'GRAFANA_START_TIME': - filter += ` \$__fromTime`; + filter += ` \$__fromTime`; break; case 'GRAFANA_END_TIME': - filter += ` \$__toTime`; + filter += ` \$__toTime`; break; default: filter += ` ${currentFilter.value || 'TODAY'}`; } } } else { - filter += formatStringValue(currentFilter.value || ''); + filter += formatStringValue(currentFilter.value || ''); } if (notOperator) { filter = ` NOT (${filter} )`; } - if ( !filter ){ + if (!filter) { return previousValue; } - if ( previousValue.length > 0 ){ - return `${previousValue} ${prefixCondition}${filter}` + if (previousValue.length > 0) { + return `${previousValue} ${prefixCondition}${filter}`; } else { return filter; } }, ''); - return { filters: combinedFilters, hasTimeFilter: hasTsFilter } + return { filters: combinedFilters, hasTimeFilter: hasTsFilter }; }; const getSampleBy = (sampleByMode: SampleByAlignToMode, sampleByValue?: string, sampleByFill?: string[]): string => { - let fills = ''; - if (sampleByFill !== undefined && sampleByFill.length > 0){ + if (sampleByFill !== undefined && sampleByFill.length > 0) { // remove suffixes - fills = ` FILL ( ${sampleByFill.map((s)=>s.replace(/_[0-9]+$/, '')).join(', ')} )`; + fills = ` FILL ( ${sampleByFill.map((s) => s.replace(/_[0-9]+$/, '')).join(', ')} )`; } let mode = ''; - if (sampleByMode !== undefined ){ + if (sampleByMode !== undefined) { mode = ` ALIGN TO ${sampleByMode}`; } let offsetOrTz = ''; - if ( (sampleByMode === SampleByAlignToMode.CalendarOffset || sampleByMode === SampleByAlignToMode.CalendarTimeZone) && - sampleByValue !== undefined && sampleByValue.length > 0 ){ + if ( + (sampleByMode === SampleByAlignToMode.CalendarOffset || sampleByMode === SampleByAlignToMode.CalendarTimeZone) && + sampleByValue !== undefined && + sampleByValue.length > 0 + ) { offsetOrTz = ` '${sampleByValue}'`; } return ` SAMPLE BY \$__sampleByInterval${fills}${mode}${offsetOrTz}`; -} +}; const getGroupBy = (groupBy: string[] = [], timeField?: string): string => { const clause = groupBy.length > 0 ? ` GROUP BY ${groupBy.join(', ')}` : ''; @@ -281,12 +288,19 @@ const getLimit = (limit?: string): string => { return ` LIMIT ` + (limit || '100'); }; +const escapeFields = (fields: string[]): string[] => { + return fields.map((f) => { + return f.match(/^\d/) ? `"${f}"` : f; + }); +}; + export const getSQLFromQueryOptions = (options: SqlBuilderOptions): string => { const limit = options.limit ? getLimit(options.limit) : ''; + const fields = escapeFields(options.fields || []); let query = ``; switch (options.mode) { case BuilderMode.Aggregate: - query += getAggregationQuery(options.table, options.fields, options.metrics, options.groupBy); + query += getAggregationQuery(options.table, fields, options.metrics, options.groupBy); const aggregateFilters = getFilters(options.filters || []); if (aggregateFilters.filters) { query += ` WHERE${aggregateFilters.filters}`; @@ -294,20 +308,14 @@ export const getSQLFromQueryOptions = (options: SqlBuilderOptions): string => { query += getGroupBy(options.groupBy); break; case BuilderMode.Trend: - query += getSampleByQuery( - options.table, - options.fields, - options.metrics, - options.groupBy, - options.timeField - ); + query += getSampleByQuery(options.table, fields, options.metrics, options.groupBy, options.timeField); const sampleByFilters = getFilters(options.filters || []); - if ( options.timeField || sampleByFilters.filters.length > 0 ){ + if (options.timeField || sampleByFilters.filters.length > 0) { query += ' WHERE'; - if ( options.timeField && !sampleByFilters.hasTimeFilter ){ + if (options.timeField && !sampleByFilters.hasTimeFilter) { query += ` $__timeFilter(${options.timeField})`; - if ( sampleByFilters.filters.length > 0 ){ + if (sampleByFilters.filters.length > 0) { query += ' AND'; } } @@ -319,7 +327,7 @@ export const getSQLFromQueryOptions = (options: SqlBuilderOptions): string => { break; case BuilderMode.List: default: - query += getListQuery(options.table, options.fields); + query += getListQuery(options.table, fields); const filters = getFilters(options.filters || []); if (filters.filters) { query += ` WHERE${filters.filters}`; @@ -333,10 +341,13 @@ export const getSQLFromQueryOptions = (options: SqlBuilderOptions): string => { return query; }; -export async function getQueryOptionsFromSql(sql: string, datasource?: Datasource): Promise { +export async function getQueryOptionsFromSql( + sql: string, + datasource?: Datasource +): Promise { const ast = sqlToStatement(sql); if (!ast || ast.type !== 'select') { - return 'The query can\'t be parsed.'; + return "The query can't be parsed."; } if (!ast.from || ast.from.length !== 1) { return `The query has too many 'FROM' clauses.`; @@ -349,14 +360,16 @@ export async function getQueryOptionsFromSql(sql: string, datasource?: Datasourc let timeField; let fieldsToTypes = new Map(); - if ( fromTable?.name?.name.length > 0 && datasource ){ - const dbFields = await datasource.fetchFields(fromTable?.name?.name); - dbFields.forEach((f)=>{ fieldsToTypes.set(f.name, f.type) }); - timeField = dbFields.find( (f) => f.designated)?.name; + if (fromTable?.name?.name.length > 0 && datasource) { + const dbFields = await datasource.fetchFields(fromTable?.name?.name); + dbFields.forEach((f) => { + fieldsToTypes.set(f.name, f.type); + }); + timeField = dbFields.find((f) => f.designated)?.name; } - if ( timeField === undefined ){ - timeField = ""; + if (timeField === undefined) { + timeField = ''; } const fieldsAndMetrics = getMetricsFromAst(ast.columns ? ast.columns : null); @@ -364,7 +377,7 @@ export async function getQueryOptionsFromSql(sql: string, datasource?: Datasourc let builder = { mode: BuilderMode.List, table: fromTable.name.name, - timeField: timeField + timeField: timeField, } as SqlBuilderOptions; if (fieldsAndMetrics.fields) { @@ -394,9 +407,9 @@ export async function getQueryOptionsFromSql(sql: string, datasource?: Datasourc } builder.limit = undefined; - if (ast.limit){ - if (ast.limit.upperBound && ast.limit.upperBound.type === 'integer'){ - if (ast.limit.lowerBound && ast.limit.lowerBound.type === 'integer'){ + if (ast.limit) { + if (ast.limit.upperBound && ast.limit.upperBound.type === 'integer') { + if (ast.limit.lowerBound && ast.limit.lowerBound.type === 'integer') { builder.limit = `${ast.limit.lowerBound.value}, ${ast.limit.upperBound.value}`; } else { builder.limit = `${ast.limit.upperBound.value}`; @@ -404,32 +417,32 @@ export async function getQueryOptionsFromSql(sql: string, datasource?: Datasourc } } - if (ast.sampleBy){ + if (ast.sampleBy) { builder.mode = BuilderMode.Trend; - if (ast.sampleByAlignTo){ + if (ast.sampleByAlignTo) { (builder as SqlBuilderOptionsTrend).sampleByAlignTo = ast.sampleByAlignTo.alignTo as SampleByAlignToMode; } - if (ast.sampleByFill){ - (builder as SqlBuilderOptionsTrend).sampleByFill = ast.sampleByFill.map( f => { - if ( f.type === 'sampleByKeyword' ){ + if (ast.sampleByFill) { + (builder as SqlBuilderOptionsTrend).sampleByFill = ast.sampleByFill.map((f) => { + if (f.type === 'sampleByKeyword') { return f.keyword; - } else if ( f.type === 'null'){ + } else if (f.type === 'null') { return 'null'; } else { return f.value.toString(); } }); } - if (ast.sampleByAlignTo?.alignValue){ + if (ast.sampleByAlignTo?.alignValue) { (builder as SqlBuilderOptionsTrend).sampleByAlignToValue = ast.sampleByAlignTo?.alignValue; } } - if (ast.latestOn){ + if (ast.latestOn) { builder.mode = BuilderMode.List; - if (ast.partitionBy){ + if (ast.partitionBy) { (builder as SqlBuilderOptionsList).partitionBy = ast.partitionBy.map((p) => { - if (p.table){ + if (p.table) { return p.table.name + '.' + p.name; } else { return p.name; @@ -458,7 +471,7 @@ type MapperState = { filters: Filter[]; notFlag: boolean; condition: 'AND' | 'OR' | null; -} +}; function getFiltersFromAst(expr: Expr, fieldsToTypes: Map): Filter[] { let state: MapperState = { currentFilter: null, filters: [], notFlag: false, condition: null } as MapperState; @@ -503,9 +516,10 @@ function getFiltersFromAst(expr: Expr, fieldsToTypes: Map): Filt }, })); - try {// don't break conversion + try { + // don't break conversion visitor.expr(expr); - } catch ( error ){ + } catch (error) { console.error(error); } @@ -514,32 +528,32 @@ function getFiltersFromAst(expr: Expr, fieldsToTypes: Map): Filt function getRefFilter(e: ExprRef, state: MapperState, fieldsToTypes: Map) { let doAdd = false; - if ( state.currentFilter === null){ + if (state.currentFilter === null) { state.currentFilter = {} as Filter; doAdd = true; } - if ( e.name?.toLowerCase() === '$__fromtime'){ - state.currentFilter = { ...state.currentFilter, value: 'GRAFANA_START_TIME', type: 'timestamp' } as Filter; + if (e.name?.toLowerCase() === '$__fromtime') { + state.currentFilter = { ...state.currentFilter, value: 'GRAFANA_START_TIME', type: 'timestamp' } as Filter; return; } - if ( e.name?.toLowerCase() === '$__totime'){ + if (e.name?.toLowerCase() === '$__totime') { state.currentFilter = { ...state.currentFilter, value: 'GRAFANA_END_TIME', type: 'timestamp' } as Filter; return; } let type = fieldsToTypes.get(e.name); - if ( !state.currentFilter.key ) { - state.currentFilter = { ...state.currentFilter, key: e.name} ; - if (type){ + if (!state.currentFilter.key) { + state.currentFilter = { ...state.currentFilter, key: e.name }; + if (type) { state.currentFilter.type = type; } } else { state.currentFilter = { ...state.currentFilter, value: [e.name], type: type || 'string' } as Filter; } - if ( doAdd ){ + if (doAdd) { state.filters.push(state.currentFilter); state.currentFilter = null; } @@ -556,25 +570,27 @@ function getListFilter(e: ExprList, state: MapperState) { } as Filter; } -function getCallString(e: ExprCall){ - let args: string = e.args.map((x) =>{ - switch (x.type){ - case 'string': - return `'${x.value}'`; - case 'boolean': - case 'numeric': - case 'integer': - return x.value; - case 'ref': - return x.name; - case 'null': - return 'null'; - case 'call': - return getCallString(x); - default: - return '' - } - }).join(', '); +function getCallString(e: ExprCall) { + let args: string = e.args + .map((x) => { + switch (x.type) { + case 'string': + return `'${x.value}'`; + case 'boolean': + case 'numeric': + case 'integer': + return x.value; + case 'ref': + return x.name; + case 'null': + return 'null'; + case 'call': + return getCallString(x); + default: + return ''; + } + }) + .join(', '); return `${e.function.name}(${args})`; } @@ -594,21 +610,23 @@ function toString(x: Expr) { case 'call': return getCallString(x); default: - return '' + return ''; } } function getCallFilter(e: ExprCall, state: MapperState) { let doAdd = false; - if ( !state.currentFilter ){ + if (!state.currentFilter) { // map f(x) to true = f(x) so it can be displayed in builder - state.currentFilter = {key: 'true', type: 'boolean'} as Filter; + state.currentFilter = { key: 'true', type: 'boolean' } as Filter; doAdd = true; } - let args = e.args.map((x) =>{ - return toString(x); - }).join(', '); + let args = e.args + .map((x) => { + return toString(x); + }) + .join(', '); const val = `${e.function.name}(${args})`; if (val.startsWith('$__timefilter(')) { @@ -622,8 +640,8 @@ function getCallFilter(e: ExprCall, state: MapperState) { state.currentFilter = { ...state.currentFilter, value: val } as Filter; } - if ( doAdd ){ - if (state.condition){ + if (doAdd) { + if (state.condition) { state.currentFilter.condition = state.condition; state.condition = null; } @@ -642,7 +660,7 @@ function getUnaryFilter(mapper: IAstVisitor, e: ExprUnary, state: MapperState) { } state.currentFilter = { operator: e.op as FilterOperator } as Filter; - if ( state.condition ){ + if (state.condition) { state.currentFilter.condition = state.condition; state.condition = null; } @@ -656,7 +674,11 @@ function getStringFilter(e: ExprString, state: MapperState) { state.currentFilter = { ...state.currentFilter, key: e.value } as Filter; return; } - state.currentFilter = { ...state.currentFilter, value: e.value, type: state.currentFilter?.type || 'string' } as Filter; + state.currentFilter = { + ...state.currentFilter, + value: e.value, + type: state.currentFilter?.type || 'string', + } as Filter; } function getNumericFilter(e: ExprNumeric, state: MapperState) { @@ -679,14 +701,13 @@ function getCastFilter(e: ExprCast, state: MapperState) { let val = `cast( ${toString(e.operand)} as ${e.to.kind === undefined ? e.to.name : ''} )`; if (state.currentFilter != null && !state.currentFilter.key) { - state.currentFilter = {...state.currentFilter, key: val} as Filter; + state.currentFilter = { ...state.currentFilter, key: val } as Filter; return; } else { - state.currentFilter = {...state.currentFilter, value: val, type: state.currentFilter?.type || 'int'} as Filter; + state.currentFilter = { ...state.currentFilter, value: val, type: state.currentFilter?.type || 'int' } as Filter; } } - function getBooleanFilter(e: ExprBool, state: MapperState) { state.currentFilter = { ...state.currentFilter, value: e.value, type: 'boolean' } as Filter; } @@ -700,7 +721,7 @@ function getBinaryFilter(mapper: IAstVisitor, e: ExprBinary, state: MapperState) } else if (Object.values(FilterOperator).find((x) => e.op === x)) { state.currentFilter = {} as Filter; state.currentFilter.operator = e.op as FilterOperator; - if ( state.condition ){ + if (state.condition) { state.currentFilter.condition = state.condition; state.condition = null; } @@ -725,7 +746,11 @@ function selectCallFunc(s: SelectedColumn): BuilderMetricField | string { } return x.name; }); - if ( Object.values(BuilderMetricFieldAggregation).includes( s.expr.function.name.toLowerCase() as BuilderMetricFieldAggregation ) ) { + if ( + Object.values(BuilderMetricFieldAggregation).includes( + s.expr.function.name.toLowerCase() as BuilderMetricFieldAggregation + ) + ) { return { aggregation: s.expr.function.name as BuilderMetricFieldAggregation, field: fields[0], @@ -770,7 +795,7 @@ function getMetricsFromAst(selectClauses: SelectedColumn[] | null): { fields.push(`${s.expr.value}`); break; case 'cast': - fields.push(`cast(${toString(s.expr.operand)} as ${s.expr.to.kind === undefined ? s.expr.to?.name : '' })`) + fields.push(`cast(${toString(s.expr.operand)} as ${s.expr.to.kind === undefined ? s.expr.to?.name : ''})`); break; default: break; @@ -780,7 +805,7 @@ function getMetricsFromAst(selectClauses: SelectedColumn[] | null): { } function formatStringValue(currentFilter: string): string { - if ( Array.isArray(currentFilter) ){ + if (Array.isArray(currentFilter)) { currentFilter = currentFilter[0]; } if (currentFilter.startsWith('$')) {