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

Retrieved from "http://forge.mysql.com/wiki/ProxyCookbook"

This page has been accessed 7,066 times. This page was last modified 03:39, 20 December 2007.

Find

Browse
MySQLForge
Main Page
Current events
Recent changes
Random page
Help
Edit
Edit this page
Editing help
This page
Discuss this page
Post a comment
Printable version
Context
Page history
What links here
Related changes
My pages
Special pages
New pages
File list
Statistics
Bug reports
More...