3
Copyright (C) 2007 MySQL AB
5
This program is free software; you can redistribute it and/or modify
6
it under the terms of the GNU General Public License as published by
7
the Free Software Foundation; version 2 of the License.
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
GNU General Public License for more details.
14
You should have received a copy of the GNU General Public License
15
along with this program; if not, write to the Free Software
16
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22
Uses MySQL-Proxy to create and execute a cross tabulation query
24
Using this script, you can request commands like
28
XTAB table_name row_header col_header operation operation_fld
30
and get the result in tabular format, from any MySQL client
32
Written by Giuseppe Maxia, QA Developer, MySQL AB
35
assert(proxy.PROXY_VERSION >= 0x00600,
36
"you need at least mysql-proxy 0.6.0 to run this module")
39
If the environmental variable 'DEBUG' is set, then
40
the proxy will print diagnostic messages
42
local DEBUG = os.getenv('DEBUG') or os.getenv('VERBOSE') or 0
45
local xtab_version = '0.1.3'
48
error status for the xtab sequence
49
if an error happens in a query before the last one,
50
all results after it are ignored
52
local xtab_error_status = 0
53
local return_xtab_query = false
55
local xtab_help_messages = {
56
{ 'xtab - version ' .. xtab_version .. ' - (C) MySQL AB 2007' },
59
{ 'XTAB table_name row_header col_header operation operation_fld [summary]' },
60
{ '"table_name" can be a table or a view' },
61
{ '"row_field" is the field to be used as row header' },
62
{ '"col_field" is the field whose distinct values will become column headers' },
63
{ '"operation" is the required operation (COUNT|SUM|AVG|MAX|MIN)' },
64
{ '"operation_field" is the field to which the operation is applied' },
66
{ 'If the "summary" option is used, then a "WITH ROLLUP" clause ' },
67
{ 'is added to the query.' },
69
{ 'Other commands:' },
70
{ 'XTAB QUERY - the XTAB query is returned instead of its result' },
71
{ 'XTAB NOQUERY - the XTAB result is returned (default)' },
72
{ 'XTAB version - shows current version' },
73
{ 'XTAB help - shows this help' },
74
{ 'Created by Giuseppe Maxia' },
77
local allowed_operators = {'count', 'sum', 'avg', 'min', 'max' }
80
-- Result with the syntax help
82
local xtab_help_resultset = {
84
{ type = proxy.MYSQL_TYPE_STRING, name = 'XTAB help'},
86
rows = xtab_help_messages
90
-- Result with the XTAB version
92
local xtab_version_resultset = {
94
{ type = proxy.MYSQL_TYPE_STRING, name = 'XTAB version'}
102
-- Result to comment on XTAB QUERY command
104
local xtab_query_resultset = {
106
{ type = proxy.MYSQL_TYPE_STRING, name = 'XTAB query '}
109
{ 'Setting XTAB QUERY, the next XTAB command will return ' },
110
{ 'the query text instead of its result.' },
112
{ 'Setting XTAB NOQUERY (default), the XTAB command' },
113
{ 'executes the query and returns its result.' },
119
-- result returned on wrong XTAB option
121
local xtab_unknown_resultset = {
123
{ type = proxy.MYSQL_TYPE_STRING, name = 'XTAB ERROR'}
126
{ 'unknown command. Enter "XTAB HELP" for help' }
131
-- result returned on wrong operator
133
local xtab_unknown_operator = {
135
{ type = proxy.MYSQL_TYPE_STRING, name = 'XTAB ERROR'}
138
{ 'unknown operator.' },
139
{ 'Accepted operators: COUNT, SUM, AVG, MIN, MAX' },
140
{ 'Enter "XTAB HELP" for help' },
145
-- xtab parameters to be passed from read_query ro read_query_result
147
local xtab_params = {}
150
Injection codes to recognize the various queries
151
composing the xtab operation
153
local xtab_id_before = 1024
154
local xtab_id_start = 2048
155
local xtab_id_exec = 4096
160
add XTAB VERBOSE, to enable debugging from SQL CLI
161
handling errors related to missing xtab_params values
164
function read_query( packet )
165
if packet:byte() ~= proxy.COM_QUERY then
170
To be on the safe side, we clean the params that may trigger
171
behavior on read_query_result
174
xtab_error_status = 0
176
local query = packet:sub(2)
178
-- simple tokeninzing the query, looking for accepted pattern
180
local option, table_name, row_field, col_field , op, op_col , summary
181
local query_tokens = proxy.tokenize(query)
182
local START_TOKEN = 0
184
if ( query_tokens[1]['text']:lower() == 'xtab' )
187
option = query_tokens[2]['text']
188
elseif ( query_tokens[1]['text']:lower() == 'select'
190
query_tokens[2]['text']:lower() == 'xtab' )
193
option = query_tokens[3]['text']
199
First, checking for short patterns
203
print_debug('received query ' .. query)
204
if query_tokens[ START_TOKEN + 2 ] == nil then
205
if (option:lower() == 'help') then
206
proxy.response.resultset = xtab_help_resultset
207
elseif option:lower() == 'version' then
208
proxy.response.resultset = xtab_version_resultset
209
elseif option:lower() == 'query' then
210
xtab_query_resultset.rows[7] = { 'Current setting: returns a query' }
211
proxy.response.resultset = xtab_query_resultset
212
return_xtab_query = true
213
elseif option:lower() == 'noquery' then
214
xtab_query_resultset.rows[7] = { 'Current setting: returns a result set' }
215
proxy.response.resultset = xtab_query_resultset
216
return_xtab_query = false
218
proxy.response.resultset = xtab_unknown_resultset
220
proxy.response.type = proxy.MYSQLD_PACKET_OK
221
return proxy.PROXY_SEND_RESULT
225
-- parsing the query for a xtab recognized command
228
row_field = query_tokens[START_TOKEN + 2 ]['text']
229
col_field = query_tokens[START_TOKEN + 3 ]['text']
230
op = query_tokens[START_TOKEN + 4 ]['text']
231
op_col = query_tokens[START_TOKEN + 5 ]['text']
232
if (query_tokens[START_TOKEN + 6 ] ) then
233
summary = query_tokens[START_TOKEN + 6 ]['text']
238
print_debug (string.format("<xtab> <%s> (%s) (%s) [%s] [%s]",
239
table_name, row_field, col_field, op, op_col ))
245
At this point, at least in all appearance, we are dealing
246
with a full XTAB command
248
Now checking for recognized operators
250
local recognized_operator = 0
251
for i,v in pairs(allowed_operators) do
252
if string.lower(op) == v then
253
recognized_operator = 1
257
if recognized_operator == 0 then
258
print_debug('unknown operator ' .. op)
259
proxy.response.type = proxy.MYSQLD_PACKET_OK
260
proxy.response.resultset = xtab_unknown_operator
261
return proxy.PROXY_SEND_RESULT
265
records the xtab parameters for further usage
266
in the read_query_result function
268
xtab_params['table_name'] = table_name
269
xtab_params['row_header'] = row_field
270
xtab_params['col_header'] = col_field
271
xtab_params['operation'] = op
272
xtab_params['op_col'] = op_col
273
xtab_params['summary'] = summary:lower() == 'summary'
275
print_debug('summary: ' .. tostring(xtab_params['summary']))
278
Making sure that group_concat is large enough.
279
The result of this query will be ignored
281
proxy.queries:append(xtab_id_before,
282
string.char(proxy.COM_QUERY) ..
283
"set group_concat_max_len = 1024*1024" )
286
If further queries need to be executed before the
287
one that gets the distinct values for columns,
288
use an ID larger than xtab_id_before and smaller than
293
Getting all distinct values for the given column.
294
This query will be used to create the final xtab query
295
in read_query_result()
297
proxy.queries:append(xtab_id_start,
298
string.char(proxy.COM_QUERY) ..
300
select group_concat( distinct concat(
301
'%s(if( `%s`= ', quote(%s),',`%s`,null)) as `%s_',%s,'`' )
302
order by `%s` ) from `%s` order by `%s`]],
314
return proxy.PROXY_SEND_QUERY
317
function read_query_result(inj)
318
print_debug ( 'injection id ' .. inj.id ..
319
' error status: ' .. xtab_error_status)
320
if xtab_error_status > 0 then
321
print_debug('ignoring resultset ' .. inj.id ..
322
' for previous error')
323
return proxy.PROXY_IGNORE_RESULT
325
local res = assert(inj.resultset)
327
-- on error, empty the query queue and return the error message
329
if res.query_status and (res.query_status < 0 ) then
330
xtab_error_status = 1
331
print_debug('sending result' .. inj.id .. ' on error ')
332
proxy.queries:reset()
337
-- ignoring the preparatory queries
339
if (inj.id >= xtab_id_before) and (inj.id < xtab_id_start) then
340
print_debug ('ignoring preparatory query from xtab ' .. inj.id )
341
return proxy.PROXY_IGNORE_RESULT
345
-- creating the XTAB query
347
if (inj.id == xtab_id_start) then
348
print_debug ('getting columns resultset from xtab ' .. inj.id )
351
for row in inj.resultset.rows do
352
col_query = col_query .. row[1]
355
print_debug ('column values : ' .. col_query)
356
col_query = col_query:gsub(
357
',' .. xtab_params['operation'], '\n, '
358
.. xtab_params['operation'])
359
local xtab_query = string.format([[
367
xtab_params['row_header'],
369
xtab_params['operation'],
370
xtab_params['op_col'],
371
xtab_params['table_name'],
372
xtab_params['row_header']
374
if xtab_params['summary'] == true then
375
xtab_query = xtab_query .. ' WITH ROLLUP '
378
-- if the query was requested, it is returned immediately
380
if (return_xtab_query == true) then
381
proxy.queries:reset()
382
proxy.response.type = proxy.MYSQLD_PACKET_OK
383
proxy.response.resultset = {
385
{ type = proxy.MYSQL_TYPE_STRING, name = 'XTAB query'}
391
return proxy.PROXY_SEND_RESULT
394
-- The XTAB query is executed
396
proxy.queries:append(xtab_id_exec, string.char(proxy.COM_QUERY) .. xtab_query)
397
print_debug (xtab_query, 2)
398
return proxy.PROXY_IGNORE_RESULT
402
-- Getting the final xtab result
404
if (inj.id == xtab_id_exec) then
405
print_debug ('getting final xtab result ' .. inj.id )
407
-- Replacing the default NULL value provided by WITH ROLLUP
408
-- with a more human readable value
410
if xtab_params['summary'] == true then
411
local updated_rows = {}
412
local updated_fields = {}
413
for row in inj.resultset.rows do
414
if row[1] == nil then
415
row[1] = 'Grand Total'
417
table.insert(updated_rows , row)
419
local field_count = 0
420
local fields = inj.resultset.fields
422
while fields[field_count] do
423
table.insert(updated_fields, {
424
type = fields[field_count].type ,
425
name = fields[field_count].name } )
426
field_count = field_count + 1
428
proxy.response.resultset = {
429
fields = updated_fields,
432
proxy.response.type = proxy.MYSQLD_PACKET_OK
433
return proxy.PROXY_SEND_RESULT
439
function print_debug (msg, min_level)
440
if not min_level then
443
if DEBUG and (DEBUG >= min_level) then