mirror of https://github.com/grafana/grafana
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 sqlUtilpull/62619/head
parent
bba80b6c7a
commit
62c30dea4d
@ -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"]] } |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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"] |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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'); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -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"]] } |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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,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; |
||||||
|
} |
||||||
|
}; |
||||||
|
|||||||
@ -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); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -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', |
||||||
|
]; |
||||||
Loading…
Reference in new issue