InfluxDB SQL: Use double quotes instead of backticks (#75443)

Use double quotes instead of backticks
This commit is contained in:
ismail simsek
2023-10-04 15:47:54 +02:00
committed by GitHub
parent d00ea685e6
commit c64e73540d
7 changed files with 590 additions and 11 deletions

View File

@@ -8,10 +8,9 @@ import { SQLQuery } from '../../../../../../../features/plugins/sql';
import { SqlQueryEditor } from '../../../../../../../features/plugins/sql/components/QueryEditor';
import { applyQueryDefaults } from '../../../../../../../features/plugins/sql/defaults';
import InfluxDatasource from '../../../../datasource';
import { FlightSQLDatasource } from '../../../../fsql/datasource.flightsql';
import { InfluxQuery } from '../../../../types';
import { FlightSQLDatasource } from './FlightSQLDatasource';
interface Props extends Themeable2 {
onChange: (query: InfluxQuery) => void;
onRunQuery: () => void;

View File

@@ -1,20 +1,19 @@
import { DataSourceInstanceSettings, TimeRange } from '@grafana/data';
import { DataSourceInstanceSettings, TimeRange } from '@grafana/data/src';
import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/experimental';
import { SqlDatasource } from 'app/features/plugins/sql/datasource/SqlDatasource';
import { DB, SQLQuery } from 'app/features/plugins/sql/types';
import { formatSQL } from 'app/features/plugins/sql/utils/formatSQL';
// @todo These are being imported for PoC, but should probably be reimplemented within the influx datasource?
import { mapFieldsToTypes } from '../../../../../mysql/fields';
import { buildColumnQuery, buildTableQuery, showDatabases } from '../../../../../mysql/mySqlMetaQuery';
import { getSqlCompletionProvider } from '../../../../../mysql/sqlCompletionProvider';
import { quoteIdentifierIfNecessary, quoteLiteral, toRawSql } from '../../../../../mysql/sqlUtil';
import { MySQLOptions } from '../../../../../mysql/types';
import { mapFieldsToTypes } from './fields';
import { buildColumnQuery, buildTableQuery, showDatabases } from './flightsqlMetaQuery';
import { getSqlCompletionProvider } from './sqlCompletionProvider';
import { quoteLiteral, quoteIdentifierIfNecessary, toRawSql } from './sqlUtil';
import { FlightSQLOptions } from './types';
export class FlightSQLDatasource extends SqlDatasource {
sqlLanguageDefinition: LanguageDefinition | undefined;
constructor(private instanceSettings: DataSourceInstanceSettings<MySQLOptions>) {
constructor(private instanceSettings: DataSourceInstanceSettings<FlightSQLOptions>) {
super(instanceSettings);
}
@@ -31,7 +30,7 @@ export class FlightSQLDatasource extends SqlDatasource {
getMeta: (identifier?: TableIdentifier) => this.fetchMeta(identifier),
};
this.sqlLanguageDefinition = {
id: 'mysql',
id: 'flightsql',
completionProvider: getSqlCompletionProvider(args),
formatter: formatSQL,
};

View File

@@ -0,0 +1,91 @@
import { RAQBFieldTypes, SQLSelectableValue } from 'app/features/plugins/sql/types';
export function mapFieldsToTypes(columns: SQLSelectableValue[]) {
const fields: SQLSelectableValue[] = [];
for (const col of columns) {
let type: RAQBFieldTypes = 'text';
switch (col.type?.toUpperCase()) {
case 'BOOLEAN':
case 'BOOL': {
type = 'boolean';
break;
}
case 'BYTES':
case 'VARCHAR': {
type = 'text';
break;
}
case 'FLOAT':
case 'FLOAT64':
case 'INT':
case 'INTEGER':
case 'INT64':
case 'NUMERIC':
case 'BIGNUMERIC': {
type = 'number';
break;
}
case 'DATE': {
type = 'date';
break;
}
case 'DATETIME': {
type = 'datetime';
break;
}
case 'TIME': {
type = 'time';
break;
}
case 'TIMESTAMP': {
type = 'datetime';
break;
}
case 'GEOGRAPHY': {
type = 'text';
break;
}
default:
break;
}
fields.push({ ...col, raqbFieldType: type, icon: mapColumnTypeToIcon(col.type!.toUpperCase()) });
}
return fields;
}
export function mapColumnTypeToIcon(type: string) {
switch (type) {
case 'TIME':
case 'DATETIME':
case 'TIMESTAMP':
return 'clock-nine';
case 'BOOLEAN':
return 'toggle-off';
case 'INTEGER':
case 'FLOAT':
case 'FLOAT64':
case 'INT':
case 'SMALLINT':
case 'BIGINT':
case 'TINYINT':
case 'BYTEINT':
case 'INT64':
case 'NUMERIC':
case 'DECIMAL':
return 'calculator-alt';
case 'CHAR':
case 'VARCHAR':
case 'STRING':
case 'BYTES':
case 'TEXT':
case 'TINYTEXT':
case 'MEDIUMTEXT':
case 'LONGTEXT':
return 'text';
case 'GEOGRAPHY':
return 'map';
default:
return undefined;
}
}

View File

@@ -0,0 +1,40 @@
import { quoteLiteral, unquoteIdentifier } from './sqlUtil';
export function buildTableQuery(dataset?: string) {
const database = dataset !== undefined ? quoteIdentAsLiteral(dataset) : 'database()';
return `SELECT table_name FROM information_schema.tables WHERE table_schema = ${database} ORDER BY table_name`;
}
export function showDatabases() {
return `SELECT DISTINCT TABLE_SCHEMA from information_schema.TABLES where TABLE_TYPE != 'SYSTEM VIEW' ORDER BY TABLE_SCHEMA`;
}
export function buildColumnQuery(table: string, dbName?: string) {
let query = 'SELECT column_name, data_type FROM information_schema.columns WHERE ';
query += buildTableConstraint(table, dbName);
query += ' ORDER BY column_name';
return query;
}
export function buildTableConstraint(table: string, dbName?: string) {
let query = '';
// check for schema qualified table
if (table.includes('.')) {
const parts = table.split('.');
query = 'table_schema = ' + quoteIdentAsLiteral(parts[0]);
query += ' AND table_name = ' + quoteIdentAsLiteral(parts[1]);
return query;
} else {
const database = dbName !== undefined ? quoteIdentAsLiteral(dbName) : 'database()';
query = `table_schema = ${database} AND table_name = ` + quoteIdentAsLiteral(table);
return query;
}
}
export function quoteIdentAsLiteral(value: string) {
return quoteLiteral(unquoteIdentifier(value));
}

View File

@@ -0,0 +1,148 @@
import {
CompletionItemKind,
CompletionItemPriority,
getStandardSQLCompletionProvider,
LanguageCompletionProvider,
LinkedToken,
PositionContext,
StatementPlacementProvider,
SuggestionKind,
SuggestionKindProvider,
TableDefinition,
TableIdentifier,
TokenType,
} from '@grafana/experimental';
interface CompletionProviderGetterArgs {
getMeta: (t?: TableIdentifier) => Promise<TableDefinition[]>;
}
export const getSqlCompletionProvider: (args: CompletionProviderGetterArgs) => LanguageCompletionProvider =
({ getMeta }) =>
(monaco, language) => ({
...(language && getStandardSQLCompletionProvider(monaco, language)),
customStatementPlacement: customStatementPlacementProvider,
customSuggestionKinds: customSuggestionKinds(getMeta),
});
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;
}
};

View File

@@ -3,6 +3,21 @@ import { isEmpty } from 'lodash';
import { SQLQuery } from 'app/features/plugins/sql/types';
import { createSelectClause, haveColumns } from 'app/features/plugins/sql/utils/sql.utils';
// 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, "''") + "'";
}
export function toRawSql({ sql, dataset, table }: SQLQuery): string {
let rawQuery = '';
@@ -43,3 +58,283 @@ export function toRawSql({ sql, dataset, table }: SQLQuery): string {
}
const isLimit = (limit: number | undefined): boolean => limit !== undefined && limit >= 0;
// Puts double quotes (") 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;
}
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',
];

View File

@@ -0,0 +1,7 @@
import { SQLOptions, SQLQuery } from 'app/features/plugins/sql/types';
export interface FlightSQLOptions extends SQLOptions {
allowCleartextPasswords?: boolean;
}
export interface FlightSQLQuery extends SQLQuery {}