Files
Erik Sundell bab78a9e64 CloudWatch: Add support for AWS Metric Insights (#42487)
* add support for code editor and builder

* refactor cloudwatch migration

* Add tooltip to editor field (#56)

* add tooltip

* add old tooltips

* Bug bash feedback fixes (#58)

* make ASC the default option

* update sql preview whenever sql changes

* don't allow queries without aggregation

* set default value for aggregation

* use new input field

* cleanup

* pr feedback

* prevent unnecessary rerenders

* use frame error instead of main error

* remove not used snapshot

* Use dimension filter in schema picker  (#63)

* use dimension key filter in group by and schema labels

* add dimension filter also to code editor

* add tests

* fix build error

* fix strict error

* remove debug code

* fix annotation editor (#64)

* fix annotation editor

* fix broken test

* revert annotation backend change

* PR feedback (#67)

* pr feedback

* removed dimension filter from group by

* add spacing between common fields and rest

* do not generate deep link for metric queries (#70)

* update docs (#69)

Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>

* fix lint problem caused by merge conflict

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
2021-11-30 10:53:31 +01:00

158 lines
4.8 KiB
TypeScript

import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
import { SQLExpression } from '../types';
import {
QueryEditorArrayExpression,
QueryEditorExpression,
QueryEditorExpressionType,
QueryEditorFunctionExpression,
QueryEditorOperatorExpression,
QueryEditorPropertyExpression,
} from '../expressions';
export default class SQLGenerator {
constructor(private templateSrv: TemplateSrv = getTemplateSrv()) {}
expressionToSqlQuery({
select,
from,
where,
groupBy,
orderBy,
orderByDirection,
limit,
}: SQLExpression): string | undefined {
if (!from || !select?.name || !select?.parameters?.length) {
return undefined;
}
let parts: string[] = [];
this.appendSelect(select, parts);
this.appendFrom(from, parts);
this.appendWhere(where, parts, true, where?.expressions?.length ?? 0);
this.appendGroupBy(groupBy, parts);
this.appendOrderBy(orderBy, orderByDirection, parts);
this.appendLimit(limit, parts);
return parts.join(' ');
}
private appendSelect(select: QueryEditorFunctionExpression | undefined, parts: string[]) {
parts.push('SELECT');
this.appendFunction(select, parts);
}
private appendFrom(from: QueryEditorPropertyExpression | QueryEditorFunctionExpression | undefined, parts: string[]) {
parts.push('FROM');
from?.type === QueryEditorExpressionType.Function
? this.appendFunction(from, parts)
: parts.push(this.formatValue(from?.property?.name ?? ''));
}
private appendWhere(
filter: QueryEditorExpression | undefined,
parts: string[],
isTopLevelExpression: boolean,
topLevelExpressionsCount: number
) {
if (!filter) {
return;
}
const hasChildExpressions = 'expressions' in filter && filter.expressions.length > 0;
if (isTopLevelExpression && hasChildExpressions) {
parts.push('WHERE');
}
if (filter.type === QueryEditorExpressionType.And) {
const andParts: string[] = [];
filter.expressions.map((exp) => this.appendWhere(exp, andParts, false, topLevelExpressionsCount));
if (andParts.length === 0) {
return;
}
const andCombined = andParts.join(' AND ');
const wrapInParentheses = !isTopLevelExpression && topLevelExpressionsCount > 1 && andParts.length > 1;
return parts.push(wrapInParentheses ? `(${andCombined})` : andCombined);
}
if (filter.type === QueryEditorExpressionType.Or) {
const orParts: string[] = [];
filter.expressions.map((exp) => this.appendWhere(exp, orParts, false, topLevelExpressionsCount));
if (orParts.length === 0) {
return;
}
const orCombined = orParts.join(' OR ');
const wrapInParentheses = !isTopLevelExpression && topLevelExpressionsCount > 1 && orParts.length > 1;
parts.push(wrapInParentheses ? `(${orCombined})` : orCombined);
return;
}
if (filter.type === QueryEditorExpressionType.Operator) {
return this.appendOperator(filter, parts);
}
}
private appendGroupBy(groupBy: QueryEditorArrayExpression | undefined, parts: string[]) {
const groupByParts: string[] = [];
for (const expression of groupBy?.expressions ?? []) {
if (expression?.type !== QueryEditorExpressionType.GroupBy || !expression.property.name) {
continue;
}
groupByParts.push(this.formatValue(expression.property.name));
}
if (groupByParts.length > 0) {
parts.push(`GROUP BY ${groupByParts.join(', ')}`);
}
}
private appendOrderBy(
orderBy: QueryEditorFunctionExpression | undefined,
orderByDirection: string | undefined,
parts: string[]
) {
if (orderBy) {
parts.push('ORDER BY');
this.appendFunction(orderBy, parts);
parts.push(orderByDirection ?? 'ASC');
}
}
private appendLimit(limit: number | undefined, parts: string[]) {
limit && parts.push(`LIMIT ${limit}`);
}
private appendOperator(expression: QueryEditorOperatorExpression, parts: string[], prefix?: string) {
const { property, operator } = expression;
if (!property.name || !operator.name || !operator.value) {
return;
}
parts.push(`${this.formatValue(property.name)} ${operator.name} '${operator.value}'`);
}
private appendFunction(select: QueryEditorFunctionExpression | undefined, parts: string[]) {
if (!select?.name) {
return;
}
const params = (select.parameters ?? [])
.map((p) => p.name && this.formatValue(p.name))
.filter(Boolean)
.join(', ');
parts.push(`${select.name}(${params})`);
}
private formatValue(label: string): string {
const specialCharacters = /[/\s\.-]/; // slash, space, dot or dash
const interpolated = this.templateSrv.replace(label, {}, 'raw');
if (specialCharacters.test(interpolated)) {
return `"${label}"`;
}
return label;
}
}