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 { |
||||
CompletionItemKind, |
||||
CompletionItemPriority, |
||||
getStandardSQLCompletionProvider, |
||||
LanguageCompletionProvider, |
||||
LinkedToken, |
||||
PositionContext, |
||||
StatementPlacementProvider, |
||||
SuggestionKind, |
||||
SuggestionKindProvider, |
||||
TableDefinition, |
||||
TableIdentifier, |
||||
TokenType, |
||||
} from '@grafana/experimental'; |
||||
|
||||
interface CompletionProviderGetterArgs { |
||||
getMeta: React.MutableRefObject<(t?: TableIdentifier) => Promise<TableDefinition[]>>; |
||||
getMeta: (t?: TableIdentifier) => Promise<TableDefinition[]>; |
||||
} |
||||
|
||||
export const getSqlCompletionProvider: (args: CompletionProviderGetterArgs) => LanguageCompletionProvider = |
||||
({ getMeta }) => |
||||
(monaco, language) => ({ |
||||
...(language && getStandardSQLCompletionProvider(monaco, language)), |
||||
tables: { |
||||
resolve: getMeta.current, |
||||
}, |
||||
columns: { |
||||
resolve: getMeta.current, |
||||
}, |
||||
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; |
||||
} |
||||
}; |
||||
|
||||
@ -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