ProxyCookbook
Contents |
[edit] MySQL Proxy Cookbook
[edit] Basics
[edit] Determine What is Being Sent
Problem:
For many of the recipes in this collection, you will want to test for certain statement types before proceeding. For example, when filtering as in "Filtering Out Unwanted Query Commands With User Feedback", you likely want to filter out a certain type of query – denying "ALTER TABLE" to anyone. In that recipe, the first two words are converted to lower case and checked to see if they match "alter" and "table". However, unexpected results occur when the statement sent does not contain two words:
[mysql client]
mysql> quit
Bye
[mysql-proxy standard output]
(read_query) running.lua:3: attempt to index local 'command' (a nil value)
[edit] Figure: Unexpected results when trying to retrieve a word that does not exist
Solution:
The first byte of the packet is the packet type. Values and their meanings can be looked up in the manual for Command Constants - but here are a few common packet types:
0 COM_SLEEP Sleep
1 COM_QUIT Quit
2 COM_INIT_DB Initialize database
3 COM_QUERY Query
[edit] Figure: Common Packet Types
function read_query( packet )
local packettype = packet:byte()if (packettype==proxy.COM_QUERY) then
print ("We have a query: "..packet:sub(2))
elseif (packettype==proxy.COM_QUIT) then
print ("So sad to see you go")
end
end
[edit] Figure: Finding the packet type
> mysql
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 246
Server version: 5.0.45 MySQL Community Server (GPL)
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
mysql> use test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
Empty set (0.00 sec)
mysql> exit
Bye
[mysql-proxy output]
We have a query: select @@version_comment limit 1
We have a query: SELECT DATABASE()
We have a query: show databases
We have a query: show tables
We have a query: show tables
So sad to see you go
[edit] Figure: Packet type test
The last line of the mysql-proxy output is the result of our exit command being a packet of type COM_QUIT. The penultimate line is the result of our show tables command – it is a packet of type COM_QUERY. The first four lines are a puzzle, as we never typed those commands into the client.
The first query we see is select @@version_comment limit 1. This is called implicitly on connect to populate the version part of the banner message:
Server version: 5.0.45 MySQL Community Server (GPL)
The use command to change the database turns into SELECT DATABASE(), which the server uses to actually change the database. The show databases and show tables queries that are issued are an artifact of the table and column name completion:
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
These queries are issued whenever a use command is sent, unless the client is started with -A:
mysql -u root -h 127.0.0.1 -P3405 -A
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 248
Server version: 5.0.45 MySQL Community Server (GPL)
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
mysql> use test;
Database changed
mysql> show tables;
Empty set (0.00 sec)
mysql> exit
Bye
[mysql-proxy output]
We have a query: select @@version_comment limit 1
We have a query: SELECT DATABASE()
We have a query: show tables
So sad to see you go
[edit] Figure: Starting the mysql client with -A
[edit] Tokenizing a Query
Problem:
You want to parse the parts of a query without using regular expressions.
In lua, object:find(pattern [, init [, plain]]) finds a pattern, returning the start and end index of the pattern and a value that matches. The pattern is a regular expression pattern to find the first word, and the initialization position is 2, so the find will start after the packet type byte (see Determine What is Being Sent).
packet:find("^%s*(%w+)", 2)
[edit] Figure: Finding the first word
Finding words uses regular expressions, and is hard to read. As well, there is no information about what the word is – whether it is a string, number, SQL reserved word, etc.
Solution:
mysql-proxy 0.6.0 and higher provides a tokenizer for this problem. The tokenizer splits a string into words, populating a lua table with them. A token object has an index of "text" that contains the text of the word, and an index of "token_name" that is an internal representation of the object. "token_name" may be an SQL reserved word, data type, or special character:
TK_SQL_SELECT
TK_SQL_FROM
TK_SQL_WHERE
TK_SQL_ORDER
TK_SQL_BY
TK_FUNCTION [denotes functions such as COUNT, SUM]
TK_STAR
TK_OBRACE [denotes an open parenthesis]
TK_CBRACE [denotes a closed parenthesis]
TK_COMMA
TK_STAR
TK_PLUS
TK_MINUS
TK_DIV
TK_DOT
TK_STRING
TK_INTEGER
TK_FLOAT
TK_LITERAL
TK_UNKNOWN
The following will print out the text and token name for tokens in a query:
local tokens = proxy.tokenize(packet:sub(2))
for i, token in ipairs(tokens) do
-- print the token and what we know about it
local txt = token["text"]
-- if it's a string, put quotes around it
if token["token_name"] == 'TK_STRING' then
txt = string.format("%q", txt)
end
print(i ..": ".." { "..token["token_name"]..", "..txt.." }" )
end
mysql> select user,host from mysql.`user` where user like 'root';
1: { TK_SQL_SELECT, select }
2: { TK_LITERAL, user }
3: { TK_COMMA, , }
4: { TK_LITERAL, host }
5: { TK_SQL_FROM, from }
6: { TK_LITERAL, mysql }
7: { TK_DOT, . }
8: { TK_LITERAL, user }
9: { TK_SQL_WHERE, where }
10: { TK_LITERAL, user }
11: { TK_SQL_LIKE, like }
12: { TK_STRING, "root" }
+------+--------------------------------+
| user | host |
+------+--------------------------------+
| root | 127.0.0.1 |
| root | localhost |
+------+--------------------------------+
2 rows in set (0.00 sec)
mysql>
Figure 7: Sample tokenization
ipairs(elements) is a lua iterator. It traverses the elements passed to it via the lua primitive next. Each iteration increments the index and contains a state. The above example assigned the index to the variable i and the state to the variable token.
We have now changed the hard-to-read:
local word1_begin, word1_end, word1 = packet:find("^%s*(%w+)", 2)
local word2_begin, word2_end, word2 = packet:find("^%s*(%w+)", word1_end+1)
into:
local tokens = proxy.tokenize(packet:sub(2))
word1 = tokens[1]["text"]
word2 = tokens[2]["text"]
[edit] Client, Server and Connection Metadata
Problem:
You want to find out metadata about the client, MySQL server or the established connection.
Solution:
Override the read_handshake() function, read_auth() function and read_auth_result() functions and utilize the lua table information.
From read_handshake() we can obtain:
mysqld_version — the version of the MySQL server.
thread_id — the thread ID.
scramble — the password scramble buffer.
server_addr — the IP address of the server.
client_addr — the IP address of the client.
[edit] Figure 8: read_handshake() properties
From read_auth() we can obtain:
username – the username attempting authorization
password – the password attempting authorization (hashed/salted)
default_db – the database to attempt to connect to upon startup
[edit] Figure 9: read_auth() properties
From read_auth_result() we can obtain whether or not the attempt was successful by checking the packet type of the result:
MYSQLD_PACKET_OK – authorization succeeded
MYSQLD_PACKET_ERR – authorization failed
[edit] Figure: read_auth_result() properties
From this, we can put together an example that prints authentication attempts and results to the mysql-proxy standard output. It will also filter out the username "evil" and all attempts when the host is not localhost:
function read_handshake( auth )
print("<-- let's send some information about us")
print(" mysqld-version: " .. auth.mysqld_version)
print(" thread-id : " .. auth.thread_id)
print(" scramble-buf : " .. string.format("%q", auth.scramble))
print(" server-addr : " .. auth.server_addr)
print(" client-addr : " .. auth.client_addr)
-- deny clients from !127.0.0.1
if not auth.client_addr:match("^127.0.0.1:") then
proxy.response.type = proxy.MYSQLD_PACKET_ERR
proxy.response.errmsg = "only local connects are allowed"
print("we don't like this client");
return proxy.PROXY_SEND_RESULT
end
end
[edit] Figure: Sample metadata proxy script
We can use this in conjunction with Logging to log all connection attempts to MySQL.
[edit] Filtering
[edit] Filter Out Unwanted Queries With User Feedback
Problem:
You have a need for filtering out certain queries. For example, a slave server replicates a master, and you do not want anyone to be able to perform schema changes via ALTER TABLE directly on your server.
Solution:
Starting a slave server with the --read-only command helps, but it only goes so far. We can use tokens to see if the value of the first word is ALTER and the value of the second word is TABLE, using a case insensitive match. If there is a positive match for the first 2 words being ALTER TABLE then change the query sent.
function read_query( packet )
local comm_begin, comm_end, command, comm2 = packet:find("^%s*(%w+)", 2)
if command:lower() == "alter" then
--find the second word - start at init position of comm_end+1
local opt_begin, opt_end, option = packet:find("^%s*(%w+)", comm_end+1)
if option:lower() == "table" then
proxy.queries:append(1, string.char(proxy.COM_QUERY) .. "SELECT 'PERMISSION DENIED'")
return proxy.PROXY_SEND_QUERY
end
-- end if option == "table"
else proxy.queries:append(1, packet)
end
-- end if command == "alter"
return proxy.PROXY_SEND_QUERY
end
[edit] Figure 12: Full filter example
Advanced:
mysql-proxy 0.6.0 and higher contains a tokenizer that eliminates the need for the regular expression to find a work. See Tokenizing a Query.
local comm_begin, comm_end, command, comm2 = packet:find("^%s*(%w+)", 2)
local opt_begin, opt_end, option = packet:find("^%s*(%w+)", comm_end+1)
Changes into the more readable:
local tokens = proxy.tokenize(packet:sub(2))
command=tokens[1]["text"]
option=tokens[2]["text"]
[edit] Figure 13: Cleaner word parsing
Another way to filter out the query is to bypass sending any query to the MySQL server using the proxy.PROXY_SEND_RESULT</code< return string in the <code>read_query() function along with the custom result set (see Custom Result Sets):
if option:lower() == "table" then
proxy.response.type = proxy.MYSQLD_PACKET_OK
proxy.response.resultset = {
fields = {
{ type = proxy.MYSQL_TYPE_STRING, name = "PERMISSION DENIED", },
},
rows = {
{ "PERMISSION DENIED" }
}
}
return proxy.PROXY_SEND_RESULT
end
[edit] Figure 14: Sample bypass, no error signaled
To return an error, use the proxy.MYSQLD_PACKET_ERR return code and an error message string (see Custom Error Handling):
function read_query( packet )
local tokens = proxy.tokenize(packet:sub(2))
command=tokens[1]["text"]
option=tokens[2]["text"]
if command:lower() == "alter" and option:lower() == "table" then
proxy.response.type = proxy.MYSQLD_PACKET_ERR
proxy.response.errmsg = "I'm afraid I can't let you do that, Dave"
return proxy.PROXY_SEND_RESULT
end
end
[edit] Figure 15: Sample bypass, error signaled
[edit] (TODO) Filter Out Unwanted Queries Without User Feedback
Problem:
In the recipes, Filter Out Unwanted Queries Providing User Feedback, Custom Result Sets and Custom Error Messages, you saw how to send feedback when a user attempted to run a restricted command. What if you want to silently filter out an unwanted query, bypassing the server entirely?
Solution:
The filtering out of the query happens in the read_query() function. In order to eliminate user feedback, trap the results using read_query_result() function.
[edit] Logging
[edit] Simulating the General Log
Problem:
You want the functionality of the general log without restarting the server.
Solution:
Gather information about the query and write it to a log file before sending it to the server. Lua provides io functions, and the rest is formatting.
local logfile="/var/tmp/proxyLogging.sql"
io.output(io.open(logfile,"a+"))
io.output:write(os.date("\n-- Thread " .. proxy.connection.thread_id.. " %Y-%m-%d %X \n" .. packet:sub(2)))
io.close()
[edit] Figure: Lua io functions
Discussion:
MySQL's general log contains the date, time, packet type and query. mysql_proxy makes this information readily available.
io.open (filename [, mode]) opens a file using the same mode as in the standard C function fopen. For logging, use a+, which preserves the file's current contents and appends writes to the end of the file.
io.output([file]) opens the file and sets it to the default. This enables you to use functions like io.close() and io.write() without specifying a filehandle. io.write() is syntactic sugar for io.output.write().
The full example below changes what is written based on the type of packet seen.
function read_query( packet )
local logfile="/var/tmp/proxyLogging.sql"
local packettype=packet:byte()
--
-- if the packet is any of the ones we want, open the logfile and write the header
--
if packettype == proxy.COM_QUERY or packettype == proxy.COM_INIT_DB or packettype == proxy.COM_QUIT then
io.output(io.open(logfile,"a+"))
io.write(os.date("-- Thread " .. proxy.connection.server.thread_id.. " %Y-%m-%d %X \n"))
--
-- Now write the appropriate data given what type the packet is
--
if packettype == proxy.COM_INIT_DB then
io.write("-- Changed database to " .. packet:sub(2))
elseif packettype == proxy.COM_QUIT then
io.write("-- Client disconnect")
elseif packettype == proxy.COM_QUERY then
io.write(packet:sub(2) .. ";" )
end
--
-- Write the footer and close the file
--
io.write("\n");
io.close()
end
end
[edit] Figure: Full io example
[edit] (TODO) Simulating the Binary Log
Problem:
You want the functionality of the binary log without restarting the server.
Solution:
This solution is much like Simulating the General Log, except that we only need to write the DML and DDL. Using mysql-proxy's tokenizer greatly helps (see Tokenizing a Query). Appendix B contains all of the proxy token constants.
We would start with the code from "Simulating the General Log" and use that as a skeleton. The first type of token to filter out is TK_SQL_SELECT – that is, we do not want to log any SELECT statements.
[edit] Monitoring
[edit] (TODO) Basic Monitoring
[edit] (TODO) Monitoring Proxy Traffic
[edit] Replication
[edit] (TODO) Load Balancing Read Queries
[edit] (TODO) Simulating Replication
[edit] Command Extension
[edit] (TODO) Shell Interface
[edit] (TODO) Crosstab Queries and Pivot Tables
[edit] (TODO) FOR loops
[edit] (TODO) Data Summaries with Crosstab Queries and Pivot Tables
Problem: You want to summarize data.
Solution:
Tasks such as building a complex query are much easier with a procedural language like lua than a declarative language like SQL. In this way, mysql-proxy can help formulate summary queries such as crosstab queries.
A crosstab is a distribution matrix for more than one variable.
If we have a table of employees and want to find out the gender distribution, we would run:
mysql> SELECT COUNT(person_id) AS total,gender FROM all_personnel GROUP BY gender;
+-------+--------+
| total | gender |
+-------+--------+
| 3 | f |
| 6 | m |
+-------+--------+
2 rows in set (0.00 sec)
This shows the distribution for one variable. If we want to find out the gender distribution per country, then we would want a crosstab as our result:
+--------+-----------------+---------------+------------+-------+
| gender | country_Germany | country_Italy | country_UK | total |
+--------+-----------------+---------------+------------+-------+
| f | 2 | 1 | 0 | 3 |
| m | 3 | 1 | 2 | 6 |
+--------+-----------------+---------------+------------+-------+
The accompanying query for that matrix is:
SELECT gender,
count(if( country='Germany',person_id,null)) as country_Germany,
count(if( country='Italy',person_id,null)) as country_Italy,
count(if( country='UK',person_id,null)) as country_UK,
count(person_id) as total FROM all_personnel GROUP BY gender;
This is a tedious query to write by hand.
In addition, if another gender (say, “unknown”) were to be added, the GROUP BY clause would handle it. However, if another country is added to our table we would have to update this query. It would be great if there was a GROUP BY clause for the columns as well as the rows in our result.
Fortunately, there is. GROUP_CONCAT() does what we want:
mysql> SELECT GROUP_CONCAT(DISTINCT country) FROM all_personnel;
+--------------------------------+
| GROUP_CONCAT(DISTINCT country) |
+--------------------------------+
| UK,Italy,Germany |
+--------------------------------+
1 row in set (0.00 sec)
Now add a few strings to the GROUP_CONCAT() to form one of the crucial building blocks of the crosstab query. Note that the carriage return on the first line is deliberate. This will ensure pretty printing of longer lines, and is only necessary for the example, not the final code.
mysql> SET @first:="
count(if (country=";
Query OK, 0 rows affected (0.00 sec)
mysql> SET @second:=",person_id,null)) AS country_";
Query OK, 0 rows affected (0.00 sec)
mysql> SET @hardpart:=(SELECT GROUP_CONCAT(DISTINCT @first, QUOTE(country), @second, country) FROM all_personnel);
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @hardpart\G
*************************** 1. row ***************************
@hardpart:
count(if (country='UK',person_id,null)) AS country_UK,
count(if (country='Italy',person_id,null)) AS country_Italy,
count(if (country='Germany',person_id,null)) AS country_Germany
1 row in set (0.00 sec)
This will ensure that no matter how many countries we add, our query will always be correct and up-to-date.
To transform the GROUP_CONCAT() query into the full crosstab query, we have to add the FROM and GROUP BY clauses as well as the first part of the SELECT query. We can use user-defined variables, as we have been doing, to create the full crosstab query:
mysql> set @begin:="SELECT gender,";
Query OK, 0 rows affected (0.00 sec)
mysql> set @end:=" FROM all_personnel GROUP BY gender;";
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT CONCAT(@begin,@hardpart,@end)\G
*************************** 1. row ***************************
CONCAT(@begin,@hardpart,@end): SELECT gender,
count(if (country='UK',person_id,null)) AS country_UK,
count(if (country='Italy',person_id,null)) AS country_Italy,
count(if (country='Germany',person_id,null)) AS country_Germany FROM all_personnel GROUP BY gender;
1 row in set (0.01 sec)
Now that we have the building blocks, mysql-proxy can tie all the building blocks together. By generalizing the information used in the query, we can create a formula that will make a 2-dimensional crosstab query:
+---------------+-------------------+
| example | general |
+---------------+-------------------+
| gender | column |
| country | row |
| all_personnel | table |
| count | operation |
| person_id | field operated on |
+---------------+-------------------+
Our user-defined variables now become:
+----------+---------------------------------------------+
| variable | value |
+----------+---------------------------------------------+
| begin | SELECT <column>, |
| | |
| end | FROM <table> GROUP BY <column> |
| | |
| first | <operation> (if (<row>= |
| | |
| second | ,<field operated on>,null)) AS <column>_ |
| | |
| hardpart | (SELECT GROUP_CONCAT(DISTINCT @first, |
| | QUOTE(<row>), @second, <row>) FROM <table>) |
| | |
| query | CONCAT(@begin,@hardpart,@end) |
+----------+---------------------------------------------+
Once we set up the formula, we can issue a command, have our query created and run for us:
XTAB <table> <column> <row> <operation> <field operated on>
Prove to yourself that this formula is correct by substituting in the values in the example:
XTAB all_personnel gender country count person_id
Should parse out to become:
SELECT gender,
count(if( country='Germany',person_id,null)) as country_Germany,
count(if( country='Italy',person_id,null)) as country_Italy,
count(if( country='UK',person_id,null)) as country_UK,
count(person_id) as total FROM all_personnel GROUP BY gender;
Why use a lua script to do this instead of a stored procedure? A stored procedure is a programmatic element, and must be defined on a system. Sometimes it is desirable to have such features abstracted from the database instance. Most importantly, though, it is cumbersome to use SQL in a stored procedure to create a long string and then execute that string as a command.
Why use a lua script instead of a user-defined function? UDFs require the server to be recompiled, whereas a mysql-proxy lua script can be changed on the fly.
To implement this in lua,
It's still doing everything under the hood, so with large tables the subqueries might make the operation quite slow, however the syntax is much easier. As for pivot tables, it's trivial to pivot this. It does make more sense to have the gender be the columns and the country be the rows, so all we do is change the call to:
[edit] Proxy Tricks
[edit] Using Proxy For Testing
[edit] Creating a Server On-the-Fly
[edit] Starting and Stopping mysql-proxy
[edit] Wrappers
[edit] Integrate mysql-proxy Without Restarting the MySQL Server
[edit] Appendices
[edit] Appendix A: Glossary
DML: Data Manipulation Language accounts for writes to data. Examples of DML are INSERT, UPDATE and DELETE.
DDL: Data Definition Language is how schemas are defined. Examples of DDL are CREATE TABLE, DROP TABLE and ALTER TABLE.
[edit] Appendix B: Tokenizer Constants
TK_UNKNOWN TK_LE TK_GE TK_LT TK_GT TK_EQ TK_NE TK_STRING TK_COMMENT TK_LITERAL TK_FUNCTION TK_INTEGER TK_FLOAT TK_DOT TK_COMMA TK_ASSIGN TK_OBRACE TK_CBRACE TK_SEMICOLON TK_STAR TK_PLUS TK_MINUS TK_DIV TK_BITWISE_AND TK_BITWISE_OR TK_BITWISE_XOR TK_LOGICAL_AND TK_LOGICAL_OR TK_SQL_ACCESSIBLE TK_SQL_ACTION TK_SQL_ADD TK_SQL_ALL TK_SQL_ALTER TK_SQL_ANALYZE TK_SQL_AND TK_SQL_AS TK_SQL_ASC TK_SQL_ASENSITIVE TK_SQL_BEFORE TK_SQL_BETWEEN TK_SQL_BIGINT TK_SQL_BINARY TK_SQL_BIT TK_SQL_BLOB TK_SQL_BOTH TK_SQL_BY TK_SQL_CALL TK_SQL_CASCADE TK_SQL_CASE TK_SQL_CHANGE TK_SQL_CHAR TK_SQL_CHARACTER TK_SQL_CHECK TK_SQL_COLLATE TK_SQL_COLUMN TK_SQL_CONDITION TK_SQL_CONSTRAINT TK_SQL_CONTINUE TK_SQL_CONVERT TK_SQL_CREATE TK_SQL_CROSS TK_SQL_CURRENT_DATE TK_SQL_CURRENT_TIME TK_SQL_CURRENT_TIMESTAMP TK_SQL_CURRENT_USER TK_SQL_CURSOR TK_SQL_DATABASE TK_SQL_DATABASES TK_SQL_DATE TK_SQL_DAY_HOUR TK_SQL_DAY_MICROSECOND TK_SQL_DAY_MINUTE TK_SQL_DAY_SECOND TK_SQL_DEC TK_SQL_DECIMAL TK_SQL_DECLARE TK_SQL_DEFAULT TK_SQL_DELAYED TK_SQL_DELETE TK_SQL_DESC TK_SQL_DESCRIBE TK_SQL_DETERMINISTIC TK_SQL_DISTINCT TK_SQL_DISTINCTROW TK_SQL_DIV TK_SQL_DOUBLE TK_SQL_DROP TK_SQL_DUAL TK_SQL_EACH TK_SQL_ELSE TK_SQL_ELSEIF TK_SQL_ENCLOSED TK_SQL_ENUM TK_SQL_ESCAPED TK_SQL_EXISTS TK_SQL_EXIT TK_SQL_EXPLAIN TK_SQL_FALSE TK_SQL_FETCH TK_SQL_FLOAT TK_SQL_FLOAT4 TK_SQL_FLOAT8 TK_SQL_FOR TK_SQL_FORCE TK_SQL_FOREIGN TK_SQL_FROM TK_SQL_FULLTEXT TK_SQL_GRANT TK_SQL_GROUP TK_SQL_HAVING TK_SQL_HIGH_PRIORITY TK_SQL_HOUR_MICROSECOND TK_SQL_HOUR_MINUTE TK_SQL_HOUR_SECOND TK_SQL_IF TK_SQL_IGNORE TK_SQL_IN TK_SQL_INDEX TK_SQL_INFILE TK_SQL_INNER TK_SQL_INOUT TK_SQL_INSENSITIVE TK_SQL_INSERT TK_SQL_INT TK_SQL_INT1 TK_SQL_INT2 TK_SQL_INT3 TK_SQL_INT4 TK_SQL_INT8 TK_SQL_INTEGER TK_SQL_INTERVAL TK_SQL_INTO TK_SQL_IS TK_SQL_ITERATE TK_SQL_JOIN TK_SQL_KEY TK_SQL_KEYS TK_SQL_KILL TK_SQL_LEADING TK_SQL_LEAVE TK_SQL_LEFT TK_SQL_LIKE TK_SQL_LIMIT TK_SQL_LINEAR TK_SQL_LINES TK_SQL_LOAD TK_SQL_LOCALTIME TK_SQL_LOCALTIMESTAMP TK_SQL_LOCK TK_SQL_LONG TK_SQL_LONGBLOB TK_SQL_LONGTEXT TK_SQL_LOOP TK_SQL_LOW_PRIORITY TK_SQL_MASTER_SSL_VERIFY_SERVER_CERT TK_SQL_MATCH TK_SQL_MEDIUMBLOB TK_SQL_MEDIUMINT TK_SQL_MEDIUMTEXT TK_SQL_MIDDLEINT TK_SQL_MINUTE_MICROSECOND TK_SQL_MINUTE_SECOND TK_SQL_MOD TK_SQL_MODIFIES TK_SQL_NATURAL TK_SQL_NO TK_SQL_NOT TK_SQL_NO_WRITE_TO_BINLOG TK_SQL_NULL TK_SQL_NUMERIC TK_SQL_ON TK_SQL_OPTIMIZE TK_SQL_OPTION TK_SQL_OPTIONALLY TK_SQL_OR TK_SQL_ORDER TK_SQL_OUT TK_SQL_OUTER TK_SQL_OUTFILE TK_SQL_PRECISION TK_SQL_PRIMARY TK_SQL_PROCEDURE TK_SQL_PURGE TK_SQL_RANGE TK_SQL_READ TK_SQL_READ_ONLY TK_SQL_READS TK_SQL_READ_WRITE TK_SQL_REAL TK_SQL_REFERENCES TK_SQL_REGEXP TK_SQL_RELEASE TK_SQL_RENAME TK_SQL_REPEAT TK_SQL_REPLACE TK_SQL_REQUIRE TK_SQL_RESTRICT TK_SQL_RETURN TK_SQL_REVOKE TK_SQL_RIGHT TK_SQL_RLIKE TK_SQL_SCHEMA TK_SQL_SCHEMAS TK_SQL_SECOND_MICROSECOND TK_SQL_SELECT TK_SQL_SENSITIVE TK_SQL_SEPARATOR TK_SQL_SET TK_SQL_SHOW TK_SQL_SMALLINT TK_SQL_SPATIAL TK_SQL_SPECIFIC TK_SQL_SQL TK_SQL_SQL_BIG_RESULT TK_SQL_SQL_CALC_FOUND_ROWS TK_SQL_SQLEXCEPTION TK_SQL_SQL_SMALL_RESULT TK_SQL_SQLSTATE TK_SQL_SQLWARNING TK_SQL_SSL TK_SQL_STARTING TK_SQL_STRAIGHT_JOIN TK_SQL_TABLE TK_SQL_TERMINATED TK_SQL_TEXT TK_SQL_THEN TK_SQL_TIME TK_SQL_TIMESTAMP TK_SQL_TINYBLOB TK_SQL_TINYINT TK_SQL_TINYTEXT TK_SQL_TO TK_SQL_TRAILING TK_SQL_TRIGGER TK_SQL_TRUE TK_SQL_UNDO TK_SQL_UNION TK_SQL_UNIQUE TK_SQL_UNLOCK TK_SQL_UNSIGNED TK_SQL_UPDATE TK_SQL_USAGE TK_SQL_USE TK_SQL_USING TK_SQL_UTC_DATE TK_SQL_UTC_TIME TK_SQL_UTC_TIMESTAMP TK_SQL_VALUES TK_SQL_VARBINARY TK_SQL_VARCHAR TK_SQL_VARCHARACTER TK_SQL_VARYING TK_SQL_WHEN TK_SQL_WHERE TK_SQL_WHILE TK_SQL_WITH TK_SQL_WRITE TK_SQL_X509 TK_SQL_XOR TK_SQL_YEAR_MONTH TK_SQL_ZEROFILL TK_LAST_TOKEN