mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
bba80b6c7a
commit
62c30dea4d
@ -118,6 +118,8 @@ The response from MySQL can be formatted as either a table or as a time series.
|
|||||||
|
|
||||||
### Dataset and Table selection
|
### Dataset and Table selection
|
||||||
|
|
||||||
|
> **Note:** If your table or database name contains a reserved word or a [not permitted character](https://dev.mysql.com/doc/refman/8.0/en/identifiers.html) the editor will put quotes around them. For example a table name like `table-name` will be quoted with backticks `` `table-name` ``.
|
||||||
|
|
||||||
In the dataset dropdown, choose the MySQL database to query. The dropdown is be populated with the databases that the user has access to.
|
In the dataset dropdown, choose the MySQL database to query. The dropdown is be populated with the databases that the user has access to.
|
||||||
When the dataset is selected, the table dropdown is populated with the tables that are available.
|
When the dataset is selected, the table dropdown is populated with the tables that are available.
|
||||||
|
|
||||||
|
21
e2e/sql-suite/datasets-response.json
Normal file
21
e2e/sql-suite/datasets-response.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"results": {
|
||||||
|
"datasets": {
|
||||||
|
"status": 200,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"schema": {
|
||||||
|
"refId": "datasets",
|
||||||
|
"meta": {
|
||||||
|
"executedQueryString": "SELECT DISTINCT TABLE_SCHEMA from information_schema.TABLES where TABLE_TYPE != 'SYSTEM VIEW' ORDER BY TABLE_SCHEMA"
|
||||||
|
},
|
||||||
|
"fields": [
|
||||||
|
{ "name": "TABLE_SCHEMA", "type": "string", "typeInfo": { "frame": "string", "nullable": true } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": { "values": [["DataMaker", "mysql", "performance_schema", "sys"]] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
e2e/sql-suite/fields-response.json
Normal file
27
e2e/sql-suite/fields-response.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"results": {
|
||||||
|
"fields": {
|
||||||
|
"status": 200,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"schema": {
|
||||||
|
"refId": "fields",
|
||||||
|
"meta": {
|
||||||
|
"executedQueryString": "SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'DataMaker' AND table_name = 'RandomIntsWithTimes' ORDER BY column_name"
|
||||||
|
},
|
||||||
|
"fields": [
|
||||||
|
{ "name": "COLUMN_NAME", "type": "string", "typeInfo": { "frame": "string", "nullable": true } },
|
||||||
|
{ "name": "DATA_TYPE", "type": "string", "typeInfo": { "frame": "string", "nullable": true } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"values": [
|
||||||
|
["createdAt", "id", "time", "updatedAt", "bigint"],
|
||||||
|
["datetime", "int", "datetime", "datetime", "int"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
e2e/sql-suite/mysql.spec.ts
Normal file
59
e2e/sql-suite/mysql.spec.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { e2e } from '@grafana/e2e';
|
||||||
|
|
||||||
|
import datasetResponse from './datasets-response.json';
|
||||||
|
import fieldsResponse from './fields-response.json';
|
||||||
|
import tablesResponse from './tables-response.json';
|
||||||
|
|
||||||
|
const tableNameWithSpecialCharacter = tablesResponse.results.tables.frames[0].data.values[0][1];
|
||||||
|
const normalTableName = tablesResponse.results.tables.frames[0].data.values[0][0];
|
||||||
|
|
||||||
|
describe('MySQL datasource', () => {
|
||||||
|
it('code editor autocomplete should handle table name escaping/quoting', () => {
|
||||||
|
e2e.flows.login('admin', 'admin');
|
||||||
|
|
||||||
|
e2e().intercept('POST', '**/api/ds/query', (req) => {
|
||||||
|
if (req.body.queries[0].refId === 'datasets') {
|
||||||
|
req.alias = 'datasets';
|
||||||
|
req.reply({
|
||||||
|
body: datasetResponse,
|
||||||
|
});
|
||||||
|
} else if (req.body.queries[0].refId === 'tables') {
|
||||||
|
req.alias = 'tables';
|
||||||
|
req.reply({
|
||||||
|
body: tablesResponse,
|
||||||
|
});
|
||||||
|
} else if (req.body.queries[0].refId === 'fields') {
|
||||||
|
req.alias = 'fields';
|
||||||
|
req.reply({
|
||||||
|
body: fieldsResponse,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e.pages.Explore.visit();
|
||||||
|
|
||||||
|
e2e.components.DataSourcePicker.container().should('be.visible').type('gdev-mysql{enter}');
|
||||||
|
|
||||||
|
e2e().get("label[for^='option-code']").should('be.visible').click();
|
||||||
|
e2e().get('textarea').type('S{downArrow}{enter}');
|
||||||
|
e2e().wait('@tables');
|
||||||
|
e2e().get('.suggest-widget').contains(tableNameWithSpecialCharacter).should('be.visible');
|
||||||
|
e2e().get('textarea').type('{enter}');
|
||||||
|
e2e().get('textarea').should('have.value', `SELECT FROM grafana.\`${tableNameWithSpecialCharacter}\``);
|
||||||
|
|
||||||
|
const deleteTimes = new Array(tableNameWithSpecialCharacter.length + 2).fill(
|
||||||
|
'{backspace}',
|
||||||
|
0,
|
||||||
|
tableNameWithSpecialCharacter.length + 2
|
||||||
|
);
|
||||||
|
e2e().get('textarea').type(deleteTimes.join(''));
|
||||||
|
|
||||||
|
e2e().get('textarea').type('{command}i');
|
||||||
|
e2e().get('.suggest-widget').contains(tableNameWithSpecialCharacter).should('be.visible');
|
||||||
|
e2e().get('textarea').type('S{downArrow}{enter}');
|
||||||
|
e2e().get('textarea').should('have.value', `SELECT FROM grafana.${normalTableName}`);
|
||||||
|
|
||||||
|
e2e().get('textarea').type('.');
|
||||||
|
e2e().get('.suggest-widget').contains('No suggestions.').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
19
e2e/sql-suite/tables-response.json
Normal file
19
e2e/sql-suite/tables-response.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"results": {
|
||||||
|
"tables": {
|
||||||
|
"status": 200,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"schema": {
|
||||||
|
"refId": "tables",
|
||||||
|
"meta": {
|
||||||
|
"executedQueryString": "SELECT table_name FROM information_schema.tables WHERE table_schema = 'DataMaker' ORDER BY table_name"
|
||||||
|
},
|
||||||
|
"fields": [{ "name": "TABLE_NAME", "type": "string", "typeInfo": { "frame": "string", "nullable": true } }]
|
||||||
|
},
|
||||||
|
"data": { "values": [["normalTable", "table-name"]] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,15 +3,13 @@ import { useCopyToClipboard } from 'react-use';
|
|||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { EditorField, EditorHeader, EditorMode, EditorRow, FlexItem, InlineSelect, Space } from '@grafana/experimental';
|
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 { QueryWithDefaults } from '../defaults';
|
||||||
import { SQLQuery, QueryFormat, QueryRowFilter, QUERY_FORMAT_OPTIONS, DB } from '../types';
|
import { SQLQuery, QueryFormat, QueryRowFilter, QUERY_FORMAT_OPTIONS, DB } from '../types';
|
||||||
import { defaultToRawSql } from '../utils/sql.utils';
|
|
||||||
|
|
||||||
import { ConfirmModal } from './ConfirmModal';
|
import { ConfirmModal } from './ConfirmModal';
|
||||||
import { DatasetSelector } from './DatasetSelector';
|
import { DatasetSelector } from './DatasetSelector';
|
||||||
import { ErrorBoundary } from './ErrorBoundary';
|
|
||||||
import { TableSelector } from './TableSelector';
|
import { TableSelector } from './TableSelector';
|
||||||
|
|
||||||
export interface QueryHeaderProps {
|
export interface QueryHeaderProps {
|
||||||
@ -43,7 +41,7 @@ export function QueryHeader({
|
|||||||
const { editorMode } = query;
|
const { editorMode } = query;
|
||||||
const [_, copyToClipboard] = useCopyToClipboard();
|
const [_, copyToClipboard] = useCopyToClipboard();
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
const toRawSql = db.toRawSql || defaultToRawSql;
|
const toRawSql = db.toRawSql;
|
||||||
|
|
||||||
const onEditorModeChange = useCallback(
|
const onEditorModeChange = useCallback(
|
||||||
(newEditorMode: EditorMode) => {
|
(newEditorMode: EditorMode) => {
|
||||||
@ -94,28 +92,14 @@ export function QueryHeader({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EditorHeader>
|
<EditorHeader>
|
||||||
{/* Backward compatibility check. Inline select uses SelectContainer that was added in 8.3 */}
|
<InlineSelect
|
||||||
<ErrorBoundary
|
label="Format"
|
||||||
fallBackComponent={
|
value={query.format}
|
||||||
<InlineField label="Format" labelWidth={15}>
|
placeholder="Select format"
|
||||||
<Select
|
menuShouldPortal
|
||||||
placeholder="Select format"
|
onChange={onFormatChange}
|
||||||
value={query.format}
|
options={QUERY_FORMAT_OPTIONS}
|
||||||
onChange={onFormatChange}
|
/>
|
||||||
options={QUERY_FORMAT_OPTIONS}
|
|
||||||
/>
|
|
||||||
</InlineField>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<InlineSelect
|
|
||||||
label="Format"
|
|
||||||
value={query.format}
|
|
||||||
placeholder="Select format"
|
|
||||||
menuShouldPortal
|
|
||||||
onChange={onFormatChange}
|
|
||||||
options={QUERY_FORMAT_OPTIONS}
|
|
||||||
/>
|
|
||||||
</ErrorBoundary>
|
|
||||||
|
|
||||||
{editorMode === EditorMode.Builder && (
|
{editorMode === EditorMode.Builder && (
|
||||||
<>
|
<>
|
||||||
|
@ -137,6 +137,7 @@ export function SelectRow({ sql, format, columns, onSqlChange, functions }: Sele
|
|||||||
<EditorField label="Alias" optional width={15}>
|
<EditorField label="Alias" optional width={15}>
|
||||||
<Select
|
<Select
|
||||||
value={item.alias ? toOption(item.alias) : null}
|
value={item.alias ? toOption(item.alias) : null}
|
||||||
|
inputId={`select-alias-${index}-${uniqueId()}`}
|
||||||
options={timeSeriesAliasOpts}
|
options={timeSeriesAliasOpts}
|
||||||
onChange={onAliasChange(item, index)}
|
onChange={onAliasChange(item, index)}
|
||||||
isClearable
|
isClearable
|
||||||
|
@ -132,7 +132,7 @@ export interface DB {
|
|||||||
dispose?: (dsID?: string) => void;
|
dispose?: (dsID?: string) => void;
|
||||||
lookup?: (path?: string) => Promise<Array<{ name: string; completion: string }>>;
|
lookup?: (path?: string) => Promise<Array<{ name: string; completion: string }>>;
|
||||||
getEditorLanguageDefinition: () => LanguageDefinition;
|
getEditorLanguageDefinition: () => LanguageDefinition;
|
||||||
toRawSql?: (query: SQLQuery) => string;
|
toRawSql: (query: SQLQuery) => string;
|
||||||
functions?: () => string[];
|
functions?: () => string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { isEmpty } from 'lodash';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
QueryEditorExpressionType,
|
QueryEditorExpressionType,
|
||||||
QueryEditorFunctionExpression,
|
QueryEditorFunctionExpression,
|
||||||
@ -7,47 +5,9 @@ import {
|
|||||||
QueryEditorPropertyExpression,
|
QueryEditorPropertyExpression,
|
||||||
QueryEditorPropertyType,
|
QueryEditorPropertyType,
|
||||||
} from '../expressions';
|
} from '../expressions';
|
||||||
import { SQLQuery, SQLExpression } from '../types';
|
import { SQLExpression } from '../types';
|
||||||
|
|
||||||
export function defaultToRawSql({ sql, dataset, table }: SQLQuery): string {
|
export function createSelectClause(sqlColumns: NonNullable<SQLExpression['columns']>): 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 {
|
|
||||||
const columns = sqlColumns.map((c) => {
|
const columns = sqlColumns.map((c) => {
|
||||||
let rawColumn = '';
|
let rawColumn = '';
|
||||||
if (c.name && c.alias) {
|
if (c.name && c.alias) {
|
||||||
|
@ -2,8 +2,6 @@ import { useCallback } from 'react';
|
|||||||
|
|
||||||
import { DB, SQLExpression, SQLQuery } from '../types';
|
import { DB, SQLExpression, SQLQuery } from '../types';
|
||||||
|
|
||||||
import { defaultToRawSql } from './sql.utils';
|
|
||||||
|
|
||||||
interface UseSqlChange {
|
interface UseSqlChange {
|
||||||
db: DB;
|
db: DB;
|
||||||
query: SQLQuery;
|
query: SQLQuery;
|
||||||
@ -13,7 +11,7 @@ interface UseSqlChange {
|
|||||||
export function useSqlChange({ query, onQueryChange, db }: UseSqlChange) {
|
export function useSqlChange({ query, onQueryChange, db }: UseSqlChange) {
|
||||||
const onSqlChange = useCallback(
|
const onSqlChange = useCallback(
|
||||||
(sql: SQLExpression) => {
|
(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 rawSql = toRawSql({ sql, dataset: query.dataset, table: query.table, refId: query.refId });
|
||||||
const newQuery: SQLQuery = { ...query, sql, rawSql };
|
const newQuery: SQLQuery = { ...query, sql, rawSql };
|
||||||
onQueryChange(newQuery);
|
onQueryChange(newQuery);
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { DataSourceInstanceSettings, ScopedVars, TimeRange } from '@grafana/data';
|
import { DataSourceInstanceSettings, TimeRange } from '@grafana/data';
|
||||||
import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/experimental';
|
import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/experimental';
|
||||||
import { TemplateSrv } from '@grafana/runtime';
|
|
||||||
import { SqlDatasource } from 'app/features/plugins/sql/datasource/SqlDatasource';
|
import { SqlDatasource } from 'app/features/plugins/sql/datasource/SqlDatasource';
|
||||||
import { DB, SQLQuery } from 'app/features/plugins/sql/types';
|
import { DB, SQLQuery } from 'app/features/plugins/sql/types';
|
||||||
import { formatSQL } from 'app/features/plugins/sql/utils/formatSQL';
|
import { formatSQL } from 'app/features/plugins/sql/utils/formatSQL';
|
||||||
|
|
||||||
import MySQLQueryModel from './MySqlQueryModel';
|
|
||||||
import { mapFieldsToTypes } from './fields';
|
import { mapFieldsToTypes } from './fields';
|
||||||
import { buildColumnQuery, buildTableQuery, showDatabases } from './mySqlMetaQuery';
|
import { buildColumnQuery, buildTableQuery, showDatabases } from './mySqlMetaQuery';
|
||||||
import { getSqlCompletionProvider } from './sqlCompletionProvider';
|
import { getSqlCompletionProvider } from './sqlCompletionProvider';
|
||||||
|
import { quoteIdentifierIfNecessary, quoteLiteral, toRawSql } from './sqlUtil';
|
||||||
import { MySQLOptions } from './types';
|
import { MySQLOptions } from './types';
|
||||||
|
|
||||||
export class MySqlDatasource extends SqlDatasource {
|
export class MySqlDatasource extends SqlDatasource {
|
||||||
@ -18,20 +17,20 @@ export class MySqlDatasource extends SqlDatasource {
|
|||||||
super(instanceSettings);
|
super(instanceSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
getQueryModel(target?: Partial<SQLQuery>, templateSrv?: TemplateSrv, scopedVars?: ScopedVars): MySQLQueryModel {
|
getQueryModel() {
|
||||||
return new MySQLQueryModel(target!, templateSrv, scopedVars);
|
return { quoteLiteral };
|
||||||
}
|
}
|
||||||
|
|
||||||
getSqlLanguageDefinition(db: DB): LanguageDefinition {
|
getSqlLanguageDefinition(): LanguageDefinition {
|
||||||
if (this.sqlLanguageDefinition !== undefined) {
|
if (this.sqlLanguageDefinition !== undefined) {
|
||||||
return this.sqlLanguageDefinition;
|
return this.sqlLanguageDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = {
|
const args = {
|
||||||
getMeta: { current: (identifier?: TableIdentifier) => this.fetchMeta(identifier) },
|
getMeta: (identifier?: TableIdentifier) => this.fetchMeta(identifier),
|
||||||
};
|
};
|
||||||
this.sqlLanguageDefinition = {
|
this.sqlLanguageDefinition = {
|
||||||
id: 'sql',
|
id: 'mysql',
|
||||||
completionProvider: getSqlCompletionProvider(args),
|
completionProvider: getSqlCompletionProvider(args),
|
||||||
formatter: formatSQL,
|
formatter: formatSQL,
|
||||||
};
|
};
|
||||||
@ -40,21 +39,27 @@ export class MySqlDatasource extends SqlDatasource {
|
|||||||
|
|
||||||
async fetchDatasets(): Promise<string[]> {
|
async fetchDatasets(): Promise<string[]> {
|
||||||
const datasets = await this.runSql<string[]>(showDatabases(), { refId: 'datasets' });
|
const datasets = await this.runSql<string[]>(showDatabases(), { refId: 'datasets' });
|
||||||
return datasets.map((t) => t[0]);
|
return datasets.map((t) => quoteIdentifierIfNecessary(t[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchTables(dataset?: string): Promise<string[]> {
|
async fetchTables(dataset?: string): Promise<string[]> {
|
||||||
const tables = await this.runSql<string[]>(buildTableQuery(dataset), { refId: 'tables' });
|
const tables = await this.runSql<string[]>(buildTableQuery(dataset), { refId: 'tables' });
|
||||||
return tables.map((t) => t[0]);
|
return tables.map((t) => quoteIdentifierIfNecessary(t[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchFields(query: Partial<SQLQuery>) {
|
async fetchFields(query: Partial<SQLQuery>) {
|
||||||
if (!query.dataset || !query.table) {
|
if (!query.dataset || !query.table) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const queryString = buildColumnQuery(this.getQueryModel(query), query.table!);
|
const queryString = buildColumnQuery(query.table, query.dataset);
|
||||||
const frame = await this.runSql<string[]>(queryString, { refId: 'fields' });
|
const frame = await this.runSql<string[]>(queryString, { refId: 'fields' });
|
||||||
const fields = frame.map((f) => ({ name: f[0], text: f[0], value: f[0], type: f[1], label: f[0] }));
|
const fields = frame.map((f) => ({
|
||||||
|
name: f[0],
|
||||||
|
text: f[0],
|
||||||
|
value: quoteIdentifierIfNecessary(f[0]),
|
||||||
|
type: f[1],
|
||||||
|
label: f[0],
|
||||||
|
}));
|
||||||
return mapFieldsToTypes(fields);
|
return mapFieldsToTypes(fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,12 +72,12 @@ export class MySqlDatasource extends SqlDatasource {
|
|||||||
const datasets = await this.fetchDatasets();
|
const datasets = await this.fetchDatasets();
|
||||||
return datasets.map((d) => ({ name: d, completion: `${d}.`, kind: CompletionItemKind.Module }));
|
return datasets.map((d) => ({ name: d, completion: `${d}.`, kind: CompletionItemKind.Module }));
|
||||||
} else {
|
} else {
|
||||||
if (!identifier?.table && !defaultDB) {
|
if (!identifier?.table && (!defaultDB || identifier?.schema)) {
|
||||||
const tables = await this.fetchTables(identifier?.schema);
|
const tables = await this.fetchTables(identifier?.schema);
|
||||||
return tables.map((t) => ({ name: t, completion: t, kind: CompletionItemKind.Class }));
|
return tables.map((t) => ({ name: t, completion: t, kind: CompletionItemKind.Class }));
|
||||||
} else if (identifier?.table && identifier.schema) {
|
} else if (identifier?.table && identifier.schema) {
|
||||||
const fields = await this.fetchFields({ dataset: identifier.schema, table: identifier.table });
|
const fields = await this.fetchFields({ dataset: identifier.schema, table: identifier.table });
|
||||||
return fields.map((t) => ({ name: t.value, completion: t.value, kind: CompletionItemKind.Field }));
|
return fields.map((t) => ({ name: t.name, completion: t.value, kind: CompletionItemKind.Field }));
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -90,8 +95,9 @@ export class MySqlDatasource extends SqlDatasource {
|
|||||||
validateQuery: (query: SQLQuery, range?: TimeRange) =>
|
validateQuery: (query: SQLQuery, range?: TimeRange) =>
|
||||||
Promise.resolve({ query, error: '', isError: false, isValid: true }),
|
Promise.resolve({ query, error: '', isError: false, isValid: true }),
|
||||||
dsID: () => this.id,
|
dsID: () => this.id,
|
||||||
|
toRawSql,
|
||||||
functions: () => ['VARIANCE', 'STDDEV'],
|
functions: () => ['VARIANCE', 'STDDEV'],
|
||||||
getEditorLanguageDefinition: () => this.getSqlLanguageDefinition(this.db),
|
getEditorLanguageDefinition: () => this.getSqlLanguageDefinition(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
import { ScopedVars } from '@grafana/data';
|
|
||||||
import { TemplateSrv } from '@grafana/runtime';
|
|
||||||
|
|
||||||
import { MySQLQuery } from './types';
|
|
||||||
|
|
||||||
export default class MySQLQueryModel {
|
|
||||||
target: Partial<MySQLQuery>;
|
|
||||||
templateSrv?: TemplateSrv;
|
|
||||||
scopedVars?: ScopedVars;
|
|
||||||
|
|
||||||
constructor(target: Partial<MySQLQuery>, templateSrv?: TemplateSrv, scopedVars?: ScopedVars) {
|
|
||||||
this.target = target;
|
|
||||||
this.templateSrv = templateSrv;
|
|
||||||
this.scopedVars = scopedVars;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove identifier quoting from identifier to use in metadata queries
|
|
||||||
unquoteIdentifier(value: string) {
|
|
||||||
if (value[0] === '"' && value[value.length - 1] === '"') {
|
|
||||||
return value.substring(1, value.length - 1).replace(/""/g, '"');
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
quoteIdentifier(value: string) {
|
|
||||||
return '"' + value.replace(/"/g, '""') + '"';
|
|
||||||
}
|
|
||||||
|
|
||||||
quoteLiteral(value: string) {
|
|
||||||
return "'" + value.replace(/'/g, "''") + "'";
|
|
||||||
}
|
|
||||||
|
|
||||||
getDatabase() {
|
|
||||||
return this.target.dataset;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,9 @@
|
|||||||
|
import { buildTableQuery } from './mySqlMetaQuery';
|
||||||
|
|
||||||
|
describe('buildTableQuery', () => {
|
||||||
|
it('should build table query with parameter `grafana`', () => {
|
||||||
|
expect(buildTableQuery('`grafana`')).toBe(
|
||||||
|
`SELECT table_name FROM information_schema.tables WHERE table_schema = 'grafana' ORDER BY table_name`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -1,7 +1,7 @@
|
|||||||
import MySQLQueryModel from './MySqlQueryModel';
|
import { quoteLiteral, unquoteIdentifier } from './sqlUtil';
|
||||||
|
|
||||||
export function buildTableQuery(dataset?: string) {
|
export function buildTableQuery(dataset?: string) {
|
||||||
const database = dataset !== undefined ? `'${dataset}'` : 'database()';
|
const database = dataset !== undefined ? quoteIdentAsLiteral(dataset) : 'database()';
|
||||||
return `SELECT table_name FROM information_schema.tables WHERE table_schema = ${database} ORDER BY table_name`;
|
return `SELECT table_name FROM information_schema.tables WHERE table_schema = ${database} ORDER BY table_name`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,52 +9,32 @@ export function showDatabases() {
|
|||||||
return `SELECT DISTINCT TABLE_SCHEMA from information_schema.TABLES where TABLE_TYPE != 'SYSTEM VIEW' ORDER BY TABLE_SCHEMA`;
|
return `SELECT DISTINCT TABLE_SCHEMA from information_schema.TABLES where TABLE_TYPE != 'SYSTEM VIEW' ORDER BY TABLE_SCHEMA`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildColumnQuery(queryModel: MySQLQueryModel, table: string, type?: string, timeColumn?: string) {
|
export function buildColumnQuery(table: string, dbName?: string) {
|
||||||
let query = 'SELECT column_name, data_type FROM information_schema.columns WHERE ';
|
let query = 'SELECT column_name, data_type FROM information_schema.columns WHERE ';
|
||||||
query += buildTableConstraint(queryModel, table);
|
query += buildTableConstraint(table, dbName);
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'time': {
|
|
||||||
query += " AND data_type IN ('timestamp','datetime','bigint','int','double','float')";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'metric': {
|
|
||||||
query += " AND data_type IN ('text','tinytext','mediumtext','longtext','varchar','char')";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'value': {
|
|
||||||
query += " AND data_type IN ('bigint','int','smallint','mediumint','tinyint','double','decimal','float')";
|
|
||||||
query += ' AND column_name <> ' + quoteIdentAsLiteral(queryModel, timeColumn!);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'group': {
|
|
||||||
query += " AND data_type IN ('text','tinytext','mediumtext','longtext','varchar','char')";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query += ' ORDER BY column_name';
|
query += ' ORDER BY column_name';
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildTableConstraint(queryModel: MySQLQueryModel, table: string) {
|
export function buildTableConstraint(table: string, dbName?: string) {
|
||||||
let query = '';
|
let query = '';
|
||||||
|
|
||||||
// check for schema qualified table
|
// check for schema qualified table
|
||||||
if (table.includes('.')) {
|
if (table.includes('.')) {
|
||||||
const parts = table.split('.');
|
const parts = table.split('.');
|
||||||
query = 'table_schema = ' + quoteIdentAsLiteral(queryModel, parts[0]);
|
query = 'table_schema = ' + quoteIdentAsLiteral(parts[0]);
|
||||||
query += ' AND table_name = ' + quoteIdentAsLiteral(queryModel, parts[1]);
|
query += ' AND table_name = ' + quoteIdentAsLiteral(parts[1]);
|
||||||
return query;
|
return query;
|
||||||
} else {
|
} else {
|
||||||
const database = queryModel.getDatabase() !== undefined ? `'${queryModel.getDatabase()}'` : 'database()';
|
const database = dbName !== undefined ? quoteIdentAsLiteral(dbName) : 'database()';
|
||||||
query = `table_schema = ${database} AND table_name = ` + quoteIdentAsLiteral(queryModel, table);
|
query = `table_schema = ${database} AND table_name = ` + quoteIdentAsLiteral(table);
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function quoteIdentAsLiteral(queryModel: MySQLQueryModel, value: string) {
|
export function quoteIdentAsLiteral(value: string) {
|
||||||
return queryModel.quoteLiteral(queryModel.unquoteIdentifier(value));
|
return quoteLiteral(unquoteIdentifier(value));
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,149 @@
|
|||||||
import {
|
import {
|
||||||
|
CompletionItemKind,
|
||||||
|
CompletionItemPriority,
|
||||||
getStandardSQLCompletionProvider,
|
getStandardSQLCompletionProvider,
|
||||||
LanguageCompletionProvider,
|
LanguageCompletionProvider,
|
||||||
|
LinkedToken,
|
||||||
|
PositionContext,
|
||||||
|
StatementPlacementProvider,
|
||||||
|
SuggestionKind,
|
||||||
|
SuggestionKindProvider,
|
||||||
TableDefinition,
|
TableDefinition,
|
||||||
TableIdentifier,
|
TableIdentifier,
|
||||||
|
TokenType,
|
||||||
} from '@grafana/experimental';
|
} from '@grafana/experimental';
|
||||||
|
|
||||||
interface CompletionProviderGetterArgs {
|
interface CompletionProviderGetterArgs {
|
||||||
getMeta: React.MutableRefObject<(t?: TableIdentifier) => Promise<TableDefinition[]>>;
|
getMeta: (t?: TableIdentifier) => Promise<TableDefinition[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSqlCompletionProvider: (args: CompletionProviderGetterArgs) => LanguageCompletionProvider =
|
export const getSqlCompletionProvider: (args: CompletionProviderGetterArgs) => LanguageCompletionProvider =
|
||||||
({ getMeta }) =>
|
({ getMeta }) =>
|
||||||
(monaco, language) => ({
|
(monaco, language) => ({
|
||||||
...(language && getStandardSQLCompletionProvider(monaco, language)),
|
...(language && getStandardSQLCompletionProvider(monaco, language)),
|
||||||
tables: {
|
customStatementPlacement: customStatementPlacementProvider,
|
||||||
resolve: getMeta.current,
|
customSuggestionKinds: customSuggestionKinds(getMeta),
|
||||||
},
|
|
||||||
columns: {
|
|
||||||
resolve: getMeta.current,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const customStatementPlacement = {
|
||||||
|
afterDatabase: 'afterDatabase',
|
||||||
|
};
|
||||||
|
|
||||||
|
const customSuggestionKind = {
|
||||||
|
tablesWithinDatabase: 'tablesWithinDatabase',
|
||||||
|
};
|
||||||
|
|
||||||
|
const FROMKEYWORD = 'FROM';
|
||||||
|
|
||||||
|
export const customStatementPlacementProvider: StatementPlacementProvider = () => [
|
||||||
|
{
|
||||||
|
id: customStatementPlacement.afterDatabase,
|
||||||
|
resolve: (currentToken, previousKeyword, previousNonWhiteSpace) => {
|
||||||
|
return Boolean(
|
||||||
|
currentToken?.is(TokenType.Delimiter, '.') &&
|
||||||
|
previousKeyword?.value === FROMKEYWORD &&
|
||||||
|
(previousNonWhiteSpace?.is(TokenType.IdentifierQuote) || previousNonWhiteSpace?.isIdentifier()) &&
|
||||||
|
// don't match after table name
|
||||||
|
currentToken
|
||||||
|
?.getPreviousUntil(TokenType.Keyword, [TokenType.IdentifierQuote], FROMKEYWORD)
|
||||||
|
?.filter((t) => t.isIdentifier()).length === 1
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const customSuggestionKinds: (getMeta: CompletionProviderGetterArgs['getMeta']) => SuggestionKindProvider =
|
||||||
|
(getMeta) => () =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: SuggestionKind.Tables,
|
||||||
|
overrideDefault: true,
|
||||||
|
suggestionsResolver: async (ctx) => {
|
||||||
|
const databaseName = getDatabaseName(ctx.currentToken);
|
||||||
|
|
||||||
|
const suggestions = await getMeta({ schema: databaseName });
|
||||||
|
|
||||||
|
return suggestions.map(mapToSuggestion(ctx));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: SuggestionKind.Columns,
|
||||||
|
overrideDefault: true,
|
||||||
|
suggestionsResolver: async (ctx) => {
|
||||||
|
const databaseToken = getDatabaseToken(ctx.currentToken);
|
||||||
|
const databaseName = getDatabaseName(databaseToken);
|
||||||
|
const tableName = getTableName(databaseToken);
|
||||||
|
|
||||||
|
if (!databaseName || !tableName) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestions = await getMeta({ schema: databaseName, table: tableName });
|
||||||
|
|
||||||
|
return suggestions.map(mapToSuggestion(ctx));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: customSuggestionKind.tablesWithinDatabase,
|
||||||
|
applyTo: [customStatementPlacement.afterDatabase],
|
||||||
|
suggestionsResolver: async (ctx) => {
|
||||||
|
const databaseName = getDatabaseName(ctx.currentToken);
|
||||||
|
|
||||||
|
const suggestions = await getMeta({ schema: databaseName });
|
||||||
|
|
||||||
|
return suggestions.map(mapToSuggestion(ctx));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function mapToSuggestion(ctx: PositionContext) {
|
||||||
|
return function (tableDefinition: TableDefinition) {
|
||||||
|
return {
|
||||||
|
label: tableDefinition.name,
|
||||||
|
insertText: tableDefinition.completion ?? tableDefinition.name,
|
||||||
|
command: { id: 'editor.action.triggerSuggest', title: '' },
|
||||||
|
kind: CompletionItemKind.Field,
|
||||||
|
sortText: CompletionItemPriority.High,
|
||||||
|
range: {
|
||||||
|
...ctx.range,
|
||||||
|
startColumn: ctx.range.endColumn,
|
||||||
|
endColumn: ctx.range.endColumn,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDatabaseName(token: LinkedToken | null | undefined) {
|
||||||
|
if (token?.isIdentifier() && token.value[token.value.length - 1] !== '.') {
|
||||||
|
return token.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token?.is(TokenType.Delimiter, '.')) {
|
||||||
|
return token.getPreviousOfType(TokenType.Identifier)?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token?.is(TokenType.IdentifierQuote)) {
|
||||||
|
return token.getPreviousOfType(TokenType.Identifier)?.value || token.getNextOfType(TokenType.Identifier)?.value;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableName(token: LinkedToken | null | undefined) {
|
||||||
|
const identifier = token?.getNextOfType(TokenType.Identifier);
|
||||||
|
return identifier?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFromKeywordToken = (currentToken: LinkedToken | null) => {
|
||||||
|
const selectToken = currentToken?.getPreviousOfType(TokenType.Keyword, 'SELECT') ?? null;
|
||||||
|
return selectToken?.getNextOfType(TokenType.Keyword, FROMKEYWORD);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDatabaseToken = (currentToken: LinkedToken | null) => {
|
||||||
|
const fromToken = getFromKeywordToken(currentToken);
|
||||||
|
const nextIdentifier = fromToken?.getNextOfType(TokenType.Identifier);
|
||||||
|
if (nextIdentifier?.isKeyword() && nextIdentifier.next?.is(TokenType.Parenthesis, '(')) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return nextIdentifier;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
18
public/app/plugins/datasource/mysql/sqlUtil.test.ts
Normal file
18
public/app/plugins/datasource/mysql/sqlUtil.test.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { isValidIdentifier } from './sqlUtil';
|
||||||
|
|
||||||
|
describe('isValidIdentifier', () => {
|
||||||
|
test.each([
|
||||||
|
{ value: 'and', expected: false }, // Reserved keyword
|
||||||
|
{ value: '1name', expected: false }, // Starts with value
|
||||||
|
{ value: 'my-sql', expected: false }, // Contains not permitted character
|
||||||
|
{ value: '$id', expected: false }, // $ sign shouldn't be the first character
|
||||||
|
{ value: 'my sql', expected: false }, // Whitespace is not permitted
|
||||||
|
{ value: 'mysql ', expected: false }, // Whitespace is not permitted at the end
|
||||||
|
{ value: ' mysql', expected: false }, // Whitespace is not permitted
|
||||||
|
{ value: 'id$', expected: true },
|
||||||
|
{ value: 'myIdentifier', expected: true },
|
||||||
|
{ value: 'table_name', expected: true },
|
||||||
|
])('should return $expected when value is $value', ({ value, expected }) => {
|
||||||
|
expect(isValidIdentifier(value)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
340
public/app/plugins/datasource/mysql/sqlUtil.ts
Normal file
340
public/app/plugins/datasource/mysql/sqlUtil.ts
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
import { SQLQuery } from 'app/features/plugins/sql/types';
|
||||||
|
import { createSelectClause, haveColumns } from 'app/features/plugins/sql/utils/sql.utils';
|
||||||
|
|
||||||
|
export function toRawSql({ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Puts backticks (`) around the identifier if it is necessary.
|
||||||
|
export function quoteIdentifierIfNecessary(value: string) {
|
||||||
|
return isValidIdentifier(value) ? value : `\`${value}\``;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the identifier from MySql and returns true if it
|
||||||
|
* doesn't need to be escaped.
|
||||||
|
*/
|
||||||
|
export function isValidIdentifier(identifier: string): boolean {
|
||||||
|
const isValidName = /^[a-zA-Z_][a-zA-Z0-9_$]*$/g.test(identifier);
|
||||||
|
const isReservedWord = RESERVED_WORDS.includes(identifier.toUpperCase());
|
||||||
|
return !isReservedWord && isValidName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove identifier quoting from identifier to use in metadata queries
|
||||||
|
export function unquoteIdentifier(value: string) {
|
||||||
|
if (value[0] === '"' && value[value.length - 1] === '"') {
|
||||||
|
return value.substring(1, value.length - 1).replace(/""/g, '"');
|
||||||
|
} else if (value[0] === '`' && value[value.length - 1] === '`') {
|
||||||
|
return value.substring(1, value.length - 1);
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function quoteLiteral(value: string) {
|
||||||
|
return "'" + value.replace(/'/g, "''") + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copied from MySQL 8.0.31 INFORMATION_SCHEMA.KEYWORDS
|
||||||
|
*/
|
||||||
|
const RESERVED_WORDS = [
|
||||||
|
'ACCESSIBLE',
|
||||||
|
'ADD',
|
||||||
|
'ALL',
|
||||||
|
'ALTER',
|
||||||
|
'ANALYZE',
|
||||||
|
'AND',
|
||||||
|
'AS',
|
||||||
|
'ASC',
|
||||||
|
'ASENSITIVE',
|
||||||
|
'BEFORE',
|
||||||
|
'BETWEEN',
|
||||||
|
'BIGINT',
|
||||||
|
'BINARY',
|
||||||
|
'BLOB',
|
||||||
|
'BOTH',
|
||||||
|
'BY',
|
||||||
|
'CALL',
|
||||||
|
'CASCADE',
|
||||||
|
'CASE',
|
||||||
|
'CHANGE',
|
||||||
|
'CHAR',
|
||||||
|
'CHARACTER',
|
||||||
|
'CHECK',
|
||||||
|
'COLLATE',
|
||||||
|
'COLUMN',
|
||||||
|
'CONDITION',
|
||||||
|
'CONSTRAINT',
|
||||||
|
'CONTINUE',
|
||||||
|
'CONVERT',
|
||||||
|
'CREATE',
|
||||||
|
'CROSS',
|
||||||
|
'CUBE',
|
||||||
|
'CUME_DIST',
|
||||||
|
'CURRENT_DATE',
|
||||||
|
'CURRENT_TIME',
|
||||||
|
'CURRENT_TIMESTAMP',
|
||||||
|
'CURRENT_USER',
|
||||||
|
'CURSOR',
|
||||||
|
'DATABASE',
|
||||||
|
'DATABASES',
|
||||||
|
'DAY_HOUR',
|
||||||
|
'DAY_MICROSECOND',
|
||||||
|
'DAY_MINUTE',
|
||||||
|
'DAY_SECOND',
|
||||||
|
'DEC',
|
||||||
|
'DECIMAL',
|
||||||
|
'DECLARE',
|
||||||
|
'DEFAULT',
|
||||||
|
'DELAYED',
|
||||||
|
'DELETE',
|
||||||
|
'DENSE_RANK',
|
||||||
|
'DESC',
|
||||||
|
'DESCRIBE',
|
||||||
|
'DETERMINISTIC',
|
||||||
|
'DISTINCT',
|
||||||
|
'DISTINCTROW',
|
||||||
|
'DIV',
|
||||||
|
'DOUBLE',
|
||||||
|
'DROP',
|
||||||
|
'DUAL',
|
||||||
|
'EACH',
|
||||||
|
'ELSE',
|
||||||
|
'ELSEIF',
|
||||||
|
'EMPTY',
|
||||||
|
'ENCLOSED',
|
||||||
|
'ESCAPED',
|
||||||
|
'EXCEPT',
|
||||||
|
'EXISTS',
|
||||||
|
'EXIT',
|
||||||
|
'EXPLAIN',
|
||||||
|
'FALSE',
|
||||||
|
'FETCH',
|
||||||
|
'FIRST_VALUE',
|
||||||
|
'FLOAT',
|
||||||
|
'FLOAT4',
|
||||||
|
'FLOAT8',
|
||||||
|
'FOR',
|
||||||
|
'FORCE',
|
||||||
|
'FOREIGN',
|
||||||
|
'FROM',
|
||||||
|
'FULLTEXT',
|
||||||
|
'FUNCTION',
|
||||||
|
'GENERATED',
|
||||||
|
'GET',
|
||||||
|
'GRANT',
|
||||||
|
'GROUP',
|
||||||
|
'GROUPING',
|
||||||
|
'GROUPS',
|
||||||
|
'HAVING',
|
||||||
|
'HIGH_PRIORITY',
|
||||||
|
'HOUR_MICROSECOND',
|
||||||
|
'HOUR_MINUTE',
|
||||||
|
'HOUR_SECOND',
|
||||||
|
'IF',
|
||||||
|
'IGNORE',
|
||||||
|
'IN',
|
||||||
|
'INDEX',
|
||||||
|
'INFILE',
|
||||||
|
'INNER',
|
||||||
|
'INOUT',
|
||||||
|
'INSENSITIVE',
|
||||||
|
'INSERT',
|
||||||
|
'INT',
|
||||||
|
'INT1',
|
||||||
|
'INT2',
|
||||||
|
'INT3',
|
||||||
|
'INT4',
|
||||||
|
'INT8',
|
||||||
|
'INTEGER',
|
||||||
|
'INTERSECT',
|
||||||
|
'INTERVAL',
|
||||||
|
'INTO',
|
||||||
|
'IO_AFTER_GTIDS',
|
||||||
|
'IO_BEFORE_GTIDS',
|
||||||
|
'IS',
|
||||||
|
'ITERATE',
|
||||||
|
'JOIN',
|
||||||
|
'JSON_TABLE',
|
||||||
|
'KEY',
|
||||||
|
'KEYS',
|
||||||
|
'KILL',
|
||||||
|
'LAG',
|
||||||
|
'LAST_VALUE',
|
||||||
|
'LATERAL',
|
||||||
|
'LEAD',
|
||||||
|
'LEADING',
|
||||||
|
'LEAVE',
|
||||||
|
'LEFT',
|
||||||
|
'LIKE',
|
||||||
|
'LIMIT',
|
||||||
|
'LINEAR',
|
||||||
|
'LINES',
|
||||||
|
'LOAD',
|
||||||
|
'LOCALTIME',
|
||||||
|
'LOCALTIMESTAMP',
|
||||||
|
'LOCK',
|
||||||
|
'LONG',
|
||||||
|
'LONGBLOB',
|
||||||
|
'LONGTEXT',
|
||||||
|
'LOOP',
|
||||||
|
'LOW_PRIORITY',
|
||||||
|
'MASTER_BIND',
|
||||||
|
'MASTER_SSL_VERIFY_SERVER_CERT',
|
||||||
|
'MATCH',
|
||||||
|
'MAXVALUE',
|
||||||
|
'MEDIUMBLOB',
|
||||||
|
'MEDIUMINT',
|
||||||
|
'MEDIUMTEXT',
|
||||||
|
'MIDDLEINT',
|
||||||
|
'MINUTE_MICROSECOND',
|
||||||
|
'MINUTE_SECOND',
|
||||||
|
'MOD',
|
||||||
|
'MODIFIES',
|
||||||
|
'NATURAL',
|
||||||
|
'NOT',
|
||||||
|
'NO_WRITE_TO_BINLOG',
|
||||||
|
'NTH_VALUE',
|
||||||
|
'NTILE',
|
||||||
|
'NULL',
|
||||||
|
'NUMERIC',
|
||||||
|
'OF',
|
||||||
|
'ON',
|
||||||
|
'OPTIMIZE',
|
||||||
|
'OPTIMIZER_COSTS',
|
||||||
|
'OPTION',
|
||||||
|
'OPTIONALLY',
|
||||||
|
'OR',
|
||||||
|
'ORDER',
|
||||||
|
'OUT',
|
||||||
|
'OUTER',
|
||||||
|
'OUTFILE',
|
||||||
|
'OVER',
|
||||||
|
'PARTITION',
|
||||||
|
'PERCENT_RANK',
|
||||||
|
'PRECISION',
|
||||||
|
'PRIMARY',
|
||||||
|
'PROCEDURE',
|
||||||
|
'PURGE',
|
||||||
|
'RANGE',
|
||||||
|
'RANK',
|
||||||
|
'READ',
|
||||||
|
'READS',
|
||||||
|
'READ_WRITE',
|
||||||
|
'REAL',
|
||||||
|
'RECURSIVE',
|
||||||
|
'REFERENCES',
|
||||||
|
'REGEXP',
|
||||||
|
'RELEASE',
|
||||||
|
'RENAME',
|
||||||
|
'REPEAT',
|
||||||
|
'REPLACE',
|
||||||
|
'REQUIRE',
|
||||||
|
'RESIGNAL',
|
||||||
|
'RESTRICT',
|
||||||
|
'RETURN',
|
||||||
|
'REVOKE',
|
||||||
|
'RIGHT',
|
||||||
|
'RLIKE',
|
||||||
|
'ROW',
|
||||||
|
'ROWS',
|
||||||
|
'ROW_NUMBER',
|
||||||
|
'SCHEMA',
|
||||||
|
'SCHEMAS',
|
||||||
|
'SECOND_MICROSECOND',
|
||||||
|
'SELECT',
|
||||||
|
'SENSITIVE',
|
||||||
|
'SEPARATOR',
|
||||||
|
'SET',
|
||||||
|
'SHOW',
|
||||||
|
'SIGNAL',
|
||||||
|
'SMALLINT',
|
||||||
|
'SPATIAL',
|
||||||
|
'SPECIFIC',
|
||||||
|
'SQL',
|
||||||
|
'SQLEXCEPTION',
|
||||||
|
'SQLSTATE',
|
||||||
|
'SQLWARNING',
|
||||||
|
'SQL_BIG_RESULT',
|
||||||
|
'SQL_CALC_FOUND_ROWS',
|
||||||
|
'SQL_SMALL_RESULT',
|
||||||
|
'SSL',
|
||||||
|
'STARTING',
|
||||||
|
'STORED',
|
||||||
|
'STRAIGHT_JOIN',
|
||||||
|
'SYSTEM',
|
||||||
|
'TABLE',
|
||||||
|
'TERMINATED',
|
||||||
|
'THEN',
|
||||||
|
'TINYBLOB',
|
||||||
|
'TINYINT',
|
||||||
|
'TINYTEXT',
|
||||||
|
'TO',
|
||||||
|
'TRAILING',
|
||||||
|
'TRIGGER',
|
||||||
|
'TRUE',
|
||||||
|
'UNDO',
|
||||||
|
'UNION',
|
||||||
|
'UNIQUE',
|
||||||
|
'UNLOCK',
|
||||||
|
'UNSIGNED',
|
||||||
|
'UPDATE',
|
||||||
|
'USAGE',
|
||||||
|
'USE',
|
||||||
|
'USING',
|
||||||
|
'UTC_DATE',
|
||||||
|
'UTC_TIME',
|
||||||
|
'UTC_TIMESTAMP',
|
||||||
|
'VALUES',
|
||||||
|
'VARBINARY',
|
||||||
|
'VARCHAR',
|
||||||
|
'VARCHARACTER',
|
||||||
|
'VARYING',
|
||||||
|
'VIRTUAL',
|
||||||
|
'WHEN',
|
||||||
|
'WHERE',
|
||||||
|
'WHILE',
|
||||||
|
'WINDOW',
|
||||||
|
'WITH',
|
||||||
|
'WRITE',
|
||||||
|
'XOR',
|
||||||
|
'YEAR_MONTH',
|
||||||
|
'ZEROFILL',
|
||||||
|
];
|
@ -1,6 +1,7 @@
|
|||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { RAQBFieldTypes, SQLExpression, SQLQuery } from 'app/features/plugins/sql/types';
|
import { RAQBFieldTypes, SQLQuery } from 'app/features/plugins/sql/types';
|
||||||
|
import { createSelectClause, haveColumns } from 'app/features/plugins/sql/utils/sql.utils';
|
||||||
|
|
||||||
export function getFieldConfig(type: string): { raqbFieldType: RAQBFieldTypes; icon: string } {
|
export function getFieldConfig(type: string): { raqbFieldType: RAQBFieldTypes; icon: string } {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -82,31 +83,3 @@ export function toRawSql({ sql, table }: SQLQuery): string {
|
|||||||
}
|
}
|
||||||
return rawQuery;
|
return rawQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSelectClause(sqlColumns: NonNullable<SQLExpression['columns']>): string {
|
|
||||||
const columns = sqlColumns.map((c) => {
|
|
||||||
let rawColumn = '';
|
|
||||||
if (c.name && c.alias) {
|
|
||||||
rawColumn += `${c.name}(${c.parameters?.map((p) => `${p.name}`)}) AS ${c.alias}`;
|
|
||||||
} else if (c.name) {
|
|
||||||
rawColumn += `${c.name}(${c.parameters?.map((p) => `${p.name}`)})`;
|
|
||||||
} else if (c.alias) {
|
|
||||||
rawColumn += `${c.parameters?.map((p) => `${p.name}`)} AS ${c.alias}`;
|
|
||||||
} else {
|
|
||||||
rawColumn += `${c.parameters?.map((p) => `${p.name}`)}`;
|
|
||||||
}
|
|
||||||
return rawColumn;
|
|
||||||
});
|
|
||||||
|
|
||||||
return `SELECT ${columns.join(', ')} `;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const haveColumns = (columns: SQLExpression['columns']): columns is NonNullable<SQLExpression['columns']> => {
|
|
||||||
if (!columns) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const haveColumn = columns.some((c) => c.parameters?.length || c.parameters?.some((p) => p.name));
|
|
||||||
const haveFunction = columns.some((c) => c.name);
|
|
||||||
return haveColumn || haveFunction;
|
|
||||||
};
|
|
||||||
|
Loading…
Reference in New Issue
Block a user