~jan-kneschke/mysql-proxy/packet-tracking-assertions

« back to all changes in this revision

Viewing changes to tags/mysql-proxy-0.6.0/lib/xtab.lua

  • Committer: Kay Roepke
  • Author(s): Jan Kneschke
  • Date: 2008-01-23 22:00:28 UTC
  • Revision ID: kay@mysql.com-20080123220028-hq2xqb69apa75fnx
first round on mysql-shell based on the proxy code

this is mostly a verification if the proxy-code is flexible enough to handle 
all three scenarios of: client, server and forwarding (proxy)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
--[[
 
2
 
 
3
   Copyright (C) 2007 MySQL AB
 
4
 
 
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.
 
8
 
 
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.
 
13
 
 
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
 
17
 
 
18
--]]
 
19
 
 
20
---
 
21
--[[
 
22
   Uses MySQL-Proxy to create and execute a cross tabulation query
 
23
  
 
24
   Using this script, you can request commands like
 
25
  
 
26
     XTAB HELP
 
27
     XTAB VERSION
 
28
     XTAB table_name row_header col_header operation operation_fld
 
29
  
 
30
     and get the result in tabular format, from any MySQL client
 
31
  
 
32
   Written by Giuseppe Maxia, QA Developer, MySQL AB
 
33
--]]
 
34
 
 
35
assert(proxy.PROXY_VERSION >= 0x00600,
 
36
        "you need at least mysql-proxy 0.6.0 to run this module")
 
37
 
 
38
--[[
 
39
   If the environmental variable 'DEBUG' is set, then
 
40
   the proxy will print diagnostic messages
 
41
--]]
 
42
local DEBUG = os.getenv('DEBUG') or os.getenv('VERBOSE')  or 0
 
43
DEBUG = DEBUG + 0
 
44
 
 
45
local xtab_version = '0.1.3'
 
46
 
 
47
--[[
 
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
 
51
--]]
 
52
local xtab_error_status = 0
 
53
local return_xtab_query = false
 
54
 
 
55
local xtab_help_messages = {
 
56
    { 'xtab - version ' .. xtab_version .. ' - (C) MySQL AB 2007' },
 
57
    { 'Syntax: ' }, 
 
58
    { ' - ' },
 
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' },
 
65
    { ' - ' },
 
66
    { 'If the "summary" option is used, then a "WITH ROLLUP" clause ' },
 
67
    { 'is added to the query.' },
 
68
    { ' - ' },
 
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' },
 
75
}
 
76
 
 
77
local allowed_operators = {'count', 'sum', 'avg', 'min', 'max' }
 
78
 
 
79
--
 
80
-- Result with the syntax help
 
81
--
 
82
local xtab_help_resultset = { 
 
83
    fields = {
 
84
        { type = proxy.MYSQL_TYPE_STRING, name = 'XTAB help'},
 
85
    },
 
86
    rows   = xtab_help_messages       
 
87
}
 
88
 
 
89
--
 
90
-- Result with the XTAB version
 
91
--
 
92
local xtab_version_resultset = {
 
93
    fields = {  
 
94
        { type = proxy.MYSQL_TYPE_STRING, name = 'XTAB version'} 
 
95
    },
 
96
    rows = {
 
97
        { xtab_version }
 
98
    }
 
99
}
 
100
 
 
101
-- 
 
102
-- Result to comment on XTAB QUERY command
 
103
--
 
104
local xtab_query_resultset = {
 
105
    fields = {  
 
106
        { type = proxy.MYSQL_TYPE_STRING, name = 'XTAB query '} 
 
107
    },
 
108
    rows = {
 
109
        { 'Setting XTAB QUERY, the next XTAB command will return ' },
 
110
        { 'the query text instead of its result.' },
 
111
        { '' },
 
112
        { 'Setting XTAB NOQUERY (default), the XTAB command' },
 
113
        { 'executes the query and returns its result.' },
 
114
        { '' },
 
115
    }
 
116
}
 
117
 
 
118
--
 
119
-- result returned on wrong XTAB option
 
120
--
 
121
local xtab_unknown_resultset = {
 
122
    fields = {  
 
123
        { type = proxy.MYSQL_TYPE_STRING, name = 'XTAB ERROR'} 
 
124
    },
 
125
    rows = {
 
126
        { 'unknown command. Enter "XTAB HELP" for help' }
 
127
    }
 
128
}
 
129
 
 
130
-- 
 
131
-- result returned on wrong operator
 
132
--
 
133
local xtab_unknown_operator = {
 
134
    fields = {  
 
135
        { type = proxy.MYSQL_TYPE_STRING, name = 'XTAB ERROR'} 
 
136
    },
 
137
    rows = {
 
138
        { 'unknown operator.' },
 
139
        { 'Accepted operators: COUNT, SUM, AVG, MIN, MAX' },
 
140
        { 'Enter "XTAB HELP" for help' },
 
141
    }
 
142
}
 
143
 
 
144
--
 
145
-- xtab parameters to be passed from read_query ro read_query_result 
 
146
--
 
147
local xtab_params = {}
 
148
 
 
149
--[[
 
150
  Injection codes to recognize the various queries
 
151
  composing the xtab operation
 
152
--]]
 
153
local xtab_id_before = 1024
 
154
local xtab_id_start  = 2048
 
155
local xtab_id_exec   = 4096
 
156
 
 
157
 
 
158
--[[ 
 
159
   TODO
 
160
   add XTAB VERBOSE, to enable debugging from SQL CLI
 
161
   handling errors related to missing xtab_params values
 
162
--]]
 
163
 
 
164
function read_query( packet )
 
165
    if packet:byte() ~= proxy.COM_QUERY then
 
166
        return
 
167
    end
 
168
 
 
169
    --[[
 
170
       To be on the safe side, we clean the params that may trigger 
 
171
       behavior on read_query_result
 
172
    --]]
 
173
    xtab_params = {}
 
174
    xtab_error_status = 0
 
175
 
 
176
    local query = packet:sub(2)
 
177
    --
 
178
    -- simple tokeninzing the query, looking for accepted pattern
 
179
    --
 
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
 
183
 
 
184
    if  ( query_tokens[1]['text']:lower() == 'xtab' )
 
185
    then
 
186
        START_TOKEN = 1
 
187
        option = query_tokens[2]['text']
 
188
    elseif ( query_tokens[1]['text']:lower() == 'select' 
 
189
          and  
 
190
          query_tokens[2]['text']:lower() == 'xtab' ) 
 
191
    then 
 
192
        START_TOKEN = 2
 
193
        option = query_tokens[3]['text']
 
194
    else
 
195
        return 
 
196
    end
 
197
 
 
198
    --[[ 
 
199
       First, checking for short patterns
 
200
       XTAB HELP
 
201
       XTAB VERSION
 
202
    --]]
 
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
 
217
        else
 
218
            proxy.response.resultset = xtab_unknown_resultset 
 
219
        end
 
220
        proxy.response.type = proxy.MYSQLD_PACKET_OK
 
221
        return proxy.PROXY_SEND_RESULT
 
222
    end
 
223
 
 
224
    -- 
 
225
    -- parsing the query for a xtab recognized command
 
226
    --
 
227
    table_name = option
 
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']
 
234
    else
 
235
        summary = ''
 
236
    end
 
237
    if op_col then
 
238
        print_debug (string.format("<xtab> <%s> (%s) (%s) [%s] [%s]",
 
239
            table_name, row_field, col_field, op, op_col ))
 
240
    else
 
241
        return nil
 
242
    end
 
243
    
 
244
    --[[
 
245
       At this point, at least in all appearance, we are dealing
 
246
       with a full XTAB command
 
247
       
 
248
       Now checking for recognized operators
 
249
      ]]
 
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
 
254
        end
 
255
    end
 
256
    
 
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
 
262
    end
 
263
 
 
264
    --[[
 
265
       records the xtab parameters for further usage
 
266
       in the read_query_result function
 
267
    --]]
 
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'
 
274
 
 
275
    print_debug('summary: ' .. tostring(xtab_params['summary']))
 
276
 
 
277
    --[[
 
278
       Making sure that group_concat is large enough.
 
279
       The result of this query will be ignored
 
280
    --]]
 
281
    proxy.queries:append(xtab_id_before, 
 
282
        string.char(proxy.COM_QUERY) .. 
 
283
        "set group_concat_max_len = 1024*1024" )
 
284
 
 
285
    --[[
 
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
 
289
       xtab_id_start
 
290
    --]]
 
291
 
 
292
    --[[
 
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()
 
296
    --]]
 
297
    proxy.queries:append(xtab_id_start, 
 
298
        string.char(proxy.COM_QUERY) .. 
 
299
        string.format([[
 
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`]],
 
303
            op, 
 
304
            col_field, 
 
305
            col_field, 
 
306
            op_col, 
 
307
            col_field, 
 
308
            col_field,
 
309
            col_field, 
 
310
            table_name, 
 
311
            col_field
 
312
        )
 
313
    )
 
314
    return proxy.PROXY_SEND_QUERY
 
315
end
 
316
 
 
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
 
324
    end
 
325
    local res = assert(inj.resultset)
 
326
    -- 
 
327
    -- on error, empty the query queue and return the error message
 
328
    --
 
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()
 
333
        return 
 
334
    end 
 
335
 
 
336
    --
 
337
    -- ignoring the preparatory queries
 
338
    --
 
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
 
342
    end 
 
343
 
 
344
    --
 
345
    -- creating the XTAB query
 
346
    --
 
347
    if (inj.id == xtab_id_start) then
 
348
        print_debug ('getting columns resultset from xtab ' .. inj.id )
 
349
        local col_query = ''
 
350
        
 
351
        for row in inj.resultset.rows do
 
352
            col_query = col_query .. row[1]
 
353
        end
 
354
        
 
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([[
 
360
          SELECT 
 
361
            %s ,
 
362
            %s 
 
363
            , %s(`%s`) AS total  
 
364
          FROM %s 
 
365
          GROUP BY %s
 
366
        ]],
 
367
            xtab_params['row_header'], 
 
368
            col_query,
 
369
            xtab_params['operation'], 
 
370
            xtab_params['op_col'], 
 
371
            xtab_params['table_name'], 
 
372
            xtab_params['row_header']
 
373
        )
 
374
        if xtab_params['summary'] == true then
 
375
            xtab_query = xtab_query .. ' WITH ROLLUP '
 
376
        end
 
377
        --
 
378
        -- if the query was requested, it is returned immediately
 
379
        --
 
380
        if (return_xtab_query == true) then
 
381
            proxy.queries:reset()
 
382
            proxy.response.type = proxy.MYSQLD_PACKET_OK
 
383
            proxy.response.resultset = {
 
384
                fields = {
 
385
                    { type = proxy.MYSQL_TYPE_STRING, name = 'XTAB query'}
 
386
                },
 
387
                rows = {  
 
388
                    { xtab_query }
 
389
                }
 
390
            }
 
391
            return proxy.PROXY_SEND_RESULT
 
392
        end
 
393
        --
 
394
        -- The XTAB query is executed
 
395
        --
 
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
 
399
    end 
 
400
 
 
401
    -- 
 
402
    -- Getting the final xtab result
 
403
    --
 
404
    if (inj.id == xtab_id_exec) then
 
405
        print_debug ('getting final xtab result ' .. inj.id )
 
406
        -- 
 
407
        -- Replacing the default NULL value provided by WITH ROLLUP
 
408
        -- with a more human readable value
 
409
        --
 
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'
 
416
                end
 
417
                table.insert(updated_rows , row)
 
418
            end
 
419
            local field_count = 0
 
420
            local fields = inj.resultset.fields
 
421
 
 
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
 
427
            end
 
428
            proxy.response.resultset = {
 
429
                fields = updated_fields,    
 
430
                rows = updated_rows
 
431
            }
 
432
            proxy.response.type = proxy.MYSQLD_PACKET_OK
 
433
            return proxy.PROXY_SEND_RESULT
 
434
        end
 
435
        return
 
436
    end 
 
437
end
 
438
 
 
439
function print_debug (msg, min_level)
 
440
    if not min_level then
 
441
        min_level = 1
 
442
    end
 
443
    if DEBUG and (DEBUG >= min_level) then
 
444
        print(msg)
 
445
    end
 
446
end