MySQL: Quote identifiers that include special characters (#61135)

* SQL: toRawSQL required and escape table

* Fix autocomplete for MySQL

* Change the way we escape for builder

* Rework escape ident to be smart instead

* Fix A11y for alias

* Add first e2e test

* Add test for code editor

* Add doc

* Review comments

* Move functions to sqlUtil
This commit is contained in:
Zoltán Bedi
2023-01-31 18:16:28 +01:00
committed by GitHub
parent bba80b6c7a
commit 62c30dea4d
18 changed files with 678 additions and 191 deletions

View File

@@ -3,15 +3,13 @@ import { useCopyToClipboard } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { EditorField, EditorHeader, EditorMode, EditorRow, FlexItem, InlineSelect, Space } from '@grafana/experimental';
import { Button, InlineField, InlineSwitch, RadioButtonGroup, Select, Tooltip } from '@grafana/ui';
import { Button, InlineSwitch, RadioButtonGroup, Tooltip } from '@grafana/ui';
import { QueryWithDefaults } from '../defaults';
import { SQLQuery, QueryFormat, QueryRowFilter, QUERY_FORMAT_OPTIONS, DB } from '../types';
import { defaultToRawSql } from '../utils/sql.utils';
import { ConfirmModal } from './ConfirmModal';
import { DatasetSelector } from './DatasetSelector';
import { ErrorBoundary } from './ErrorBoundary';
import { TableSelector } from './TableSelector';
export interface QueryHeaderProps {
@@ -43,7 +41,7 @@ export function QueryHeader({
const { editorMode } = query;
const [_, copyToClipboard] = useCopyToClipboard();
const [showConfirm, setShowConfirm] = useState(false);
const toRawSql = db.toRawSql || defaultToRawSql;
const toRawSql = db.toRawSql;
const onEditorModeChange = useCallback(
(newEditorMode: EditorMode) => {
@@ -94,28 +92,14 @@ export function QueryHeader({
return (
<>
<EditorHeader>
{/* Backward compatibility check. Inline select uses SelectContainer that was added in 8.3 */}
<ErrorBoundary
fallBackComponent={
<InlineField label="Format" labelWidth={15}>
<Select
placeholder="Select format"
value={query.format}
onChange={onFormatChange}
options={QUERY_FORMAT_OPTIONS}
/>
</InlineField>
}
>
<InlineSelect
label="Format"
value={query.format}
placeholder="Select format"
menuShouldPortal
onChange={onFormatChange}
options={QUERY_FORMAT_OPTIONS}
/>
</ErrorBoundary>
<InlineSelect
label="Format"
value={query.format}
placeholder="Select format"
menuShouldPortal
onChange={onFormatChange}
options={QUERY_FORMAT_OPTIONS}
/>
{editorMode === EditorMode.Builder && (
<>

View File

@@ -137,6 +137,7 @@ export function SelectRow({ sql, format, columns, onSqlChange, functions }: Sele
<EditorField label="Alias" optional width={15}>
<Select
value={item.alias ? toOption(item.alias) : null}
inputId={`select-alias-${index}-${uniqueId()}`}
options={timeSeriesAliasOpts}
onChange={onAliasChange(item, index)}
isClearable

View File

@@ -132,7 +132,7 @@ export interface DB {
dispose?: (dsID?: string) => void;
lookup?: (path?: string) => Promise<Array<{ name: string; completion: string }>>;
getEditorLanguageDefinition: () => LanguageDefinition;
toRawSql?: (query: SQLQuery) => string;
toRawSql: (query: SQLQuery) => string;
functions?: () => string[];
}

View File

@@ -1,5 +1,3 @@
import { isEmpty } from 'lodash';
import {
QueryEditorExpressionType,
QueryEditorFunctionExpression,
@@ -7,47 +5,9 @@ import {
QueryEditorPropertyExpression,
QueryEditorPropertyType,
} from '../expressions';
import { SQLQuery, SQLExpression } from '../types';
import { SQLExpression } from '../types';
export function defaultToRawSql({ sql, dataset, table }: SQLQuery): string {
let rawQuery = '';
// Return early with empty string if there is no sql column
if (!sql || !haveColumns(sql.columns)) {
return rawQuery;
}
rawQuery += createSelectClause(sql.columns);
if (dataset && table) {
rawQuery += `FROM ${dataset}.${table} `;
}
if (sql.whereString) {
rawQuery += `WHERE ${sql.whereString} `;
}
if (sql.groupBy?.[0]?.property.name) {
const groupBy = sql.groupBy.map((g) => g.property.name).filter((g) => !isEmpty(g));
rawQuery += `GROUP BY ${groupBy.join(', ')} `;
}
if (sql.orderBy?.property.name) {
rawQuery += `ORDER BY ${sql.orderBy.property.name} `;
}
if (sql.orderBy?.property.name && sql.orderByDirection) {
rawQuery += `${sql.orderByDirection} `;
}
// Altough LIMIT 0 doesn't make sense, it is still possible to have LIMIT 0
if (sql.limit !== undefined && sql.limit >= 0) {
rawQuery += `LIMIT ${sql.limit} `;
}
return rawQuery;
}
function createSelectClause(sqlColumns: NonNullable<SQLExpression['columns']>): string {
export function createSelectClause(sqlColumns: NonNullable<SQLExpression['columns']>): string {
const columns = sqlColumns.map((c) => {
let rawColumn = '';
if (c.name && c.alias) {

View File

@@ -2,8 +2,6 @@ import { useCallback } from 'react';
import { DB, SQLExpression, SQLQuery } from '../types';
import { defaultToRawSql } from './sql.utils';
interface UseSqlChange {
db: DB;
query: SQLQuery;
@@ -13,7 +11,7 @@ interface UseSqlChange {
export function useSqlChange({ query, onQueryChange, db }: UseSqlChange) {
const onSqlChange = useCallback(
(sql: SQLExpression) => {
const toRawSql = db.toRawSql || defaultToRawSql;
const toRawSql = db.toRawSql;
const rawSql = toRawSql({ sql, dataset: query.dataset, table: query.table, refId: query.refId });
const newQuery: SQLQuery = { ...query, sql, rawSql };
onQueryChange(newQuery);