--[[ $%BEGINLICENSE%$ Copyright (c) 2007, 2012, Oracle and/or its affiliates. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA $%ENDLICENSE%$ --]] --- --[[ Uses MySQL-Proxy to create and execute a cross tabulation query Using this script, you can request commands like XTAB HELP XTAB VERSION XTAB table_name row_header col_header operation operation_fld and get the result in tabular format, from any MySQL client Written by Giuseppe Maxia, QA Developer, MySQL AB --]] assert(proxy.PROXY_VERSION >= 0x00600, "you need at least mysql-proxy 0.6.0 to run this module") --[[ If the environmental variable 'DEBUG' is set, then the proxy will print diagnostic messages --]] local DEBUG = os.getenv('DEBUG') or os.getenv('VERBOSE') or 0 DEBUG = DEBUG + 0 local xtab_version = '0.1.3' local tokenizer = require("proxy.tokenizer") --[[ error status for the xtab sequence if an error happens in a query before the last one, all results after it are ignored --]] local xtab_error_status = 0 local return_xtab_query = false local xtab_help_messages = { { 'xtab - version ' .. xtab_version .. ' - (C) MySQL AB 2007' }, { 'Syntax: ' }, { ' - ' }, { 'XTAB table_name row_header col_header operation operation_fld [summary]' }, { '"table_name" can be a table or a view' }, { '"row_field" is the field to be used as row header' }, { '"col_field" is the field whose distinct values will become column headers' }, { '"operation" is the required operation (COUNT|SUM|AVG|MAX|MIN)' }, { '"operation_field" is the field to which the operation is applied' }, { ' - ' }, { 'If the "summary" option is used, then a "WITH ROLLUP" clause ' }, { 'is added to the query.' }, { ' - ' }, { 'Other commands:' }, { 'XTAB QUERY - the XTAB query is returned instead of its result' }, { 'XTAB NOQUERY - the XTAB result is returned (default)' }, { 'XTAB version - shows current version' }, { 'XTAB help - shows this help' }, { 'Created by Giuseppe Maxia' }, } local allowed_operators = {'count', 'sum', 'avg', 'min', 'max' } -- -- Result with the syntax help -- local xtab_help_resultset = { fields = { { type = proxy.MYSQL_TYPE_STRING, name = 'XTAB help'}, }, rows = xtab_help_messages } -- -- Result with the XTAB version -- local xtab_version_resultset = { fields = { { type = proxy.MYSQL_TYPE_STRING, name = 'XTAB version'} }, rows = { { xtab_version } } } -- -- Result to comment on XTAB QUERY command -- local xtab_query_resultset = { fields = { { type = proxy.MYSQL_TYPE_STRING, name = 'XTAB query '} }, rows = { { 'Setting XTAB QUERY, the next XTAB command will return ' }, { 'the query text instead of its result.' }, { '' }, { 'Setting XTAB NOQUERY (default), the XTAB command' }, { 'executes the query and returns its result.' }, { '' }, } } -- -- result returned on wrong XTAB option -- local xtab_unknown_resultset = { fields = { { type = proxy.MYSQL_TYPE_STRING, name = 'XTAB ERROR'} }, rows = { { 'unknown command. Enter "XTAB HELP" for help' } } } -- -- result returned on wrong operator -- local xtab_unknown_operator = { fields = { { type = proxy.MYSQL_TYPE_STRING, name = 'XTAB ERROR'} }, rows = { { 'unknown operator.' }, { 'Accepted operators: COUNT, SUM, AVG, MIN, MAX' }, { 'Enter "XTAB HELP" for help' }, } } -- -- xtab parameters to be passed from read_query ro read_query_result -- local xtab_params = {} --[[ Injection codes to recognize the various queries composing the xtab operation --]] local xtab_id_before = 1024 local xtab_id_start = 2048 local xtab_id_exec = 4096 --[[ TODO add XTAB VERBOSE, to enable debugging from SQL CLI handling errors related to missing xtab_params values --]] function read_query( packet ) if packet:byte() ~= proxy.COM_QUERY then return end --[[ To be on the safe side, we clean the params that may trigger behavior on read_query_result --]] xtab_params = {} xtab_error_status = 0 local query = packet:sub(2) -- -- simple tokeninzing the query, looking for accepted pattern -- local option, table_name, row_field, col_field , op, op_col , summary local query_tokens = tokenizer.tokenize(query) local START_TOKEN = 0 if ( query_tokens[1]['text']:lower() == 'xtab' ) then START_TOKEN = 1 option = query_tokens[2]['text'] elseif ( query_tokens[1]['text']:lower() == 'select' and query_tokens[2]['text']:lower() == 'xtab' ) then START_TOKEN = 2 option = query_tokens[3]['text'] else return end --[[ First, checking for short patterns XTAB HELP XTAB VERSION --]] print_debug('received query ' .. query) if query_tokens[ START_TOKEN + 2 ] == nil then if (option:lower() == 'help') then proxy.response.resultset = xtab_help_resultset elseif option:lower() == 'version' then proxy.response.resultset = xtab_version_resultset elseif option:lower() == 'query' then xtab_query_resultset.rows[7] = { 'Current setting: returns a query' } proxy.response.resultset = xtab_query_resultset return_xtab_query = true elseif option:lower() == 'noquery' then xtab_query_resultset.rows[7] = { 'Current setting: returns a result set' } proxy.response.resultset = xtab_query_resultset return_xtab_query = false else proxy.response.resultset = xtab_unknown_resultset end proxy.response.type = proxy.MYSQLD_PACKET_OK return proxy.PROXY_SEND_RESULT end -- -- parsing the query for a xtab recognized command -- table_name = option row_field = query_tokens[START_TOKEN + 2 ]['text'] col_field = query_tokens[START_TOKEN + 3 ]['text'] op = query_tokens[START_TOKEN + 4 ]['text'] op_col = query_tokens[START_TOKEN + 5 ]['text'] if (query_tokens[START_TOKEN + 6 ] ) then summary = query_tokens[START_TOKEN + 6 ]['text'] else summary = '' end if op_col then print_debug (string.format(" <%s> (%s) (%s) [%s] [%s]", table_name, row_field, col_field, op, op_col )) else return nil end --[[ At this point, at least in all appearance, we are dealing with a full XTAB command Now checking for recognized operators ]] local recognized_operator = 0 for i,v in pairs(allowed_operators) do if string.lower(op) == v then recognized_operator = 1 end end if recognized_operator == 0 then print_debug('unknown operator ' .. op) proxy.response.type = proxy.MYSQLD_PACKET_OK proxy.response.resultset = xtab_unknown_operator return proxy.PROXY_SEND_RESULT end --[[ records the xtab parameters for further usage in the read_query_result function --]] xtab_params['table_name'] = table_name xtab_params['row_header'] = row_field xtab_params['col_header'] = col_field xtab_params['operation'] = op xtab_params['op_col'] = op_col xtab_params['summary'] = summary:lower() == 'summary' print_debug('summary: ' .. tostring(xtab_params['summary'])) --[[ Making sure that group_concat is large enough. The result of this query will be ignored --]] proxy.queries:append(xtab_id_before, string.char(proxy.COM_QUERY) .. "set group_concat_max_len = 1024*1024", { resultset_is_needed = true }) --[[ If further queries need to be executed before the one that gets the distinct values for columns, use an ID larger than xtab_id_before and smaller than xtab_id_start --]] --[[ Getting all distinct values for the given column. This query will be used to create the final xtab query in read_query_result() --]] proxy.queries:append(xtab_id_start, string.char(proxy.COM_QUERY) .. string.format([[ select group_concat( distinct concat( '%s(if( `%s`= ', quote(%s),',`%s`,null)) as `%s_',%s,'`' ) order by `%s` ) from `%s` order by `%s`]], op, col_field, col_field, op_col, col_field, col_field, col_field, table_name, col_field ), { resultset_is_needed = true } ) return proxy.PROXY_SEND_QUERY end function read_query_result(inj) print_debug ( 'injection id ' .. inj.id .. ' error status: ' .. xtab_error_status) if xtab_error_status > 0 then print_debug('ignoring resultset ' .. inj.id .. ' for previous error') return proxy.PROXY_IGNORE_RESULT end local res = assert(inj.resultset) -- -- on error, empty the query queue and return the error message -- if res.query_status and (res.query_status < 0 ) then xtab_error_status = 1 print_debug('sending result' .. inj.id .. ' on error ') proxy.queries:reset() return end -- -- ignoring the preparatory queries -- if (inj.id >= xtab_id_before) and (inj.id < xtab_id_start) then print_debug ('ignoring preparatory query from xtab ' .. inj.id ) return proxy.PROXY_IGNORE_RESULT end -- -- creating the XTAB query -- if (inj.id == xtab_id_start) then print_debug ('getting columns resultset from xtab ' .. inj.id ) local col_query = '' for row in inj.resultset.rows do col_query = col_query .. row[1] end print_debug ('column values : ' .. col_query) col_query = col_query:gsub( ',' .. xtab_params['operation'], '\n, ' .. xtab_params['operation']) local xtab_query = string.format([[ SELECT %s , %s , %s(`%s`) AS total FROM %s GROUP BY %s ]], xtab_params['row_header'], col_query, xtab_params['operation'], xtab_params['op_col'], xtab_params['table_name'], xtab_params['row_header'] ) if xtab_params['summary'] == true then xtab_query = xtab_query .. ' WITH ROLLUP ' end -- -- if the query was requested, it is returned immediately -- if (return_xtab_query == true) then proxy.queries:reset() proxy.response.type = proxy.MYSQLD_PACKET_OK proxy.response.resultset = { fields = { { type = proxy.MYSQL_TYPE_STRING, name = 'XTAB query'} }, rows = { { xtab_query } } } return proxy.PROXY_SEND_RESULT end -- -- The XTAB query is executed -- proxy.queries:append(xtab_id_exec, string.char(proxy.COM_QUERY) .. xtab_query, { resultset_is_needed = true }) print_debug (xtab_query, 2) return proxy.PROXY_IGNORE_RESULT end -- -- Getting the final xtab result -- if (inj.id == xtab_id_exec) then print_debug ('getting final xtab result ' .. inj.id ) -- -- Replacing the default NULL value provided by WITH ROLLUP -- with a more human readable value -- if xtab_params['summary'] == true then local updated_rows = {} local updated_fields = {} for row in inj.resultset.rows do if row[1] == nil then row[1] = 'Grand Total' end table.insert(updated_rows , row) end local field_count = 1 local fields = inj.resultset.fields while fields[field_count] do table.insert(updated_fields, { type = fields[field_count].type , name = fields[field_count].name } ) field_count = field_count + 1 end proxy.response.resultset = { fields = updated_fields, rows = updated_rows } proxy.response.type = proxy.MYSQLD_PACKET_OK return proxy.PROXY_SEND_RESULT end return end end function print_debug (msg, min_level) if not min_level then min_level = 1 end if DEBUG and (DEBUG >= min_level) then print(msg) end end