1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SQLParser; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* OracleSQLTranslator. |
7
|
|
|
* |
8
|
|
|
* A translator from MySQL dialect into Oracle dialect for Limesurvey |
9
|
|
|
* (http://www.limesurvey.org/) |
10
|
|
|
* |
11
|
|
|
* Copyright (c) 2012, André Rothe <[email protected], [email protected]> |
12
|
|
|
* |
13
|
|
|
* All rights reserved. |
14
|
|
|
* |
15
|
|
|
* Redistribution and use in source and binary forms, with or without modification, |
16
|
|
|
* are permitted provided that the following conditions are met: |
17
|
|
|
* |
18
|
|
|
* * Redistributions of source code must retain the above copyright notice, |
19
|
|
|
* this list of conditions and the following disclaimer. |
20
|
|
|
* * Redistributions in binary form must reproduce the above copyright notice, |
21
|
|
|
* this list of conditions and the following disclaimer in the documentation |
22
|
|
|
* and/or other materials provided with the distribution. |
23
|
|
|
* |
24
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY |
25
|
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
26
|
|
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT |
27
|
|
|
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
28
|
|
|
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED |
29
|
|
|
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR |
30
|
|
|
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
31
|
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
32
|
|
|
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH |
33
|
|
|
* DAMAGE. |
34
|
|
|
*/ |
35
|
|
|
|
36
|
|
|
//include_once(ROOT_PATH . 'classes/adodb/adodb.inc.php'); |
|
|
|
|
37
|
|
|
|
38
|
|
|
//$_ENV['DEBUG'] = 1; |
|
|
|
|
39
|
|
|
|
40
|
|
|
class OracleSQLTranslator extends PHPSQLCreator |
41
|
|
|
{ |
42
|
|
|
private $con; # this is the database connection from LimeSurvey |
43
|
|
|
private $preventColumnRefs = array(); |
44
|
|
|
private $allTables = array(); |
45
|
|
|
const ASTERISK_ALIAS = '[#RePl#]'; |
46
|
|
|
|
47
|
|
|
public function __construct($con) |
48
|
|
|
{ |
49
|
|
|
parent::__construct(); |
50
|
|
|
$this->con = $con; |
51
|
|
|
$this->initGlobalVariables(); |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
private function initGlobalVariables() |
55
|
|
|
{ |
56
|
|
|
$this->preventColumnRefs = false; |
|
|
|
|
57
|
|
|
$this->allTables = array(); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
public static function dbgprint($txt) |
|
|
|
|
61
|
|
|
{ |
62
|
|
|
if (isset($_ENV['DEBUG'])) { |
63
|
|
|
print $txt; |
64
|
|
|
} |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
public static function preprint($s, $return = false) |
68
|
|
|
{ |
69
|
|
|
$x = '<pre>'; |
70
|
|
|
$x .= print_r($s, 1); |
71
|
|
|
$x .= '</pre>'; |
72
|
|
|
if ($return) { |
73
|
|
|
return $x; |
74
|
|
|
} |
75
|
|
|
self::dbgprint($x."<br/>\n"); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
protected function processAlias($parsed) |
79
|
|
|
{ |
80
|
|
|
if ($parsed === false) { |
81
|
|
|
return ''; |
82
|
|
|
} |
83
|
|
|
# we don't need an AS between expression and alias |
84
|
|
|
$sql = ' '.$parsed['name']; |
85
|
|
|
|
86
|
|
|
return $sql; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
protected function processDELETE($parsed) |
90
|
|
|
{ |
91
|
|
|
if (count($parsed['TABLES']) > 1) { |
92
|
|
|
die('cannot translate delete statement into Oracle dialect, multiple tables are not allowed.'); |
|
|
|
|
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
return 'DELETE'; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
public static function getColumnNameFor($column) |
99
|
|
|
{ |
100
|
|
|
if (strtolower($column) === 'uid') { |
101
|
|
|
$column = 'uid_'; |
102
|
|
|
} |
103
|
|
|
// TODO: add more here, if necessary |
104
|
|
|
return $column; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
public static function getShortTableNameFor($table) |
108
|
|
|
{ |
109
|
|
|
if (strtolower($table) === 'surveys_languagesettings') { |
110
|
|
|
$table = 'surveys_lngsettings'; |
111
|
|
|
} |
112
|
|
|
// TODO: add more here, if necessary |
113
|
|
|
return $table; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
protected function processTable($parsed, $index) |
117
|
|
|
{ |
118
|
|
|
if ($parsed['expr_type'] !== 'table') { |
119
|
|
|
return ''; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
$sql = $this->getShortTableNameFor($parsed['table']); |
123
|
|
|
$alias = $this->processAlias($parsed['alias']); |
124
|
|
|
$sql .= $alias; |
125
|
|
|
|
126
|
|
|
if ($index !== 0) { |
127
|
|
|
$sql = $this->processJoin($parsed['join_type']).' '.$sql; |
128
|
|
|
$sql .= $this->processRefType($parsed['ref_type']); |
129
|
|
|
$sql .= $this->processRefClause($parsed['ref_clause']); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
# store the table and its alias for later use |
133
|
|
|
$last = array_pop($this->allTables); |
134
|
|
|
$last['tables'][] = array('table' => $this->getShortTableNameFor($parsed['table']), 'alias' => trim($alias)); |
135
|
|
|
$this->allTables[] = $last; |
136
|
|
|
|
137
|
|
|
return $sql; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
protected function processFROM($parsed) |
141
|
|
|
{ |
142
|
|
|
$this->allTables[] = array('tables' => array(), 'alias' => ''); |
143
|
|
|
|
144
|
|
|
return parent::processFROM($parsed); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
protected function processTableExpression($parsed, $index) |
148
|
|
|
{ |
149
|
|
|
if ($parsed['expr_type'] !== 'table_expression') { |
150
|
|
|
return ''; |
151
|
|
|
} |
152
|
|
|
$sql = substr($this->processFROM($parsed['sub_tree']), 5); // remove FROM keyword |
153
|
|
|
$sql = '('.$sql.')'; |
154
|
|
|
|
155
|
|
|
$alias .= $this->processAlias($parsed['alias']); |
|
|
|
|
156
|
|
|
$sql .= $alias; |
157
|
|
|
|
158
|
|
|
# store the tables-expression-alias for later use |
159
|
|
|
$last = array_pop($this->allTables); |
160
|
|
|
$last['alias'] = trim($alias); |
161
|
|
|
$this->allTables[] = $last; |
162
|
|
|
|
163
|
|
|
if ($index !== 0) { |
164
|
|
|
$sql = $this->processJoin($parsed['join_type']).' '.$sql; |
165
|
|
|
$sql .= $this->processRefType($parsed['ref_type']); |
166
|
|
|
$sql .= $this->processRefClause($parsed['ref_clause']); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
return $sql; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
View Code Duplication |
private function getTableNameFromExpression($expr) |
|
|
|
|
173
|
|
|
{ |
174
|
|
|
$pos = strpos($expr, '.'); |
175
|
|
|
if ($pos === false) { |
176
|
|
|
$pos = -1; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
return trim(substr($expr, 0, $pos + 1), '.'); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
View Code Duplication |
private function getColumnNameFromExpression($expr) |
|
|
|
|
183
|
|
|
{ |
184
|
|
|
$pos = strpos($expr, '.'); |
185
|
|
|
if ($pos === false) { |
186
|
|
|
$pos = -1; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
return substr($expr, $pos + 1); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
private function isCLOBColumnInDB($table, $column) |
193
|
|
|
{ |
194
|
|
|
$res = $this->con->GetOne( |
195
|
|
|
"SELECT count(*) FROM user_lobs WHERE table_name='".strtoupper($table)."' AND column_name='" |
196
|
|
|
.strtoupper($column)."'"); |
197
|
|
|
|
198
|
|
|
return ($res >= 1); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
protected function isCLOBColumn($table, $column) |
202
|
|
|
{ |
203
|
|
|
$tables = end($this->allTables); |
204
|
|
|
|
205
|
|
|
if ($table === '') { |
206
|
|
|
foreach ($tables['tables'] as $k => $v) { |
207
|
|
|
if ($this->isCLOBColumn($v['table'], $column)) { |
208
|
|
|
return true; |
209
|
|
|
} |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
return false; |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
# check the aliases, $table cannot be empty |
216
|
|
|
foreach ($tables['tables'] as $k => $v) { |
217
|
|
|
if ((strtolower($v['alias']) === strtolower($table)) |
218
|
|
|
|| (strtolower($tables['alias']) === strtolower($table))) { |
219
|
|
|
if ($this->isCLOBColumnInDB($v['table'], $column)) { |
220
|
|
|
return true; |
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
# it must be a valid table name |
226
|
|
|
return $this->isCLOBColumnInDB($table, $column); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
protected function processOrderByExpression($parsed) |
230
|
|
|
{ |
231
|
|
|
if ($parsed['type'] !== 'expression') { |
232
|
|
|
return ''; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
$table = $this->getTableNameFromExpression($parsed['base_expr']); |
236
|
|
|
$col = $this->getColumnNameFromExpression($parsed['base_expr']); |
237
|
|
|
|
238
|
|
|
$sql = ($table !== '' ? $table.'.' : '').$col; |
239
|
|
|
|
240
|
|
|
# check, if the column is a CLOB |
241
|
|
|
if ($this->isCLOBColumn($table, $col)) { |
242
|
|
|
$sql = 'cast(substr('.$sql.',1,200) as varchar2(200))'; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
return $sql.' '.$parsed['direction']; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
protected function processColRef($parsed) |
249
|
|
|
{ |
250
|
|
|
if ($parsed['expr_type'] !== 'colref') { |
251
|
|
|
return ''; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
$table = $this->getTableNameFromExpression($parsed['base_expr']); |
255
|
|
|
$col = $this->getColumnNameFromexpression($parsed['base_expr']); |
256
|
|
|
|
257
|
|
|
# we have to change the column name, if the column is uid |
258
|
|
|
# we have to change the tablereference, if the tablename is too long |
259
|
|
|
$col = $this->getColumnNameFor($col); |
260
|
|
|
$table = $this->getShortTableNameFor($table); |
261
|
|
|
|
262
|
|
|
# if we have * as colref, we cannot use other columns |
263
|
|
|
# we have to add alias.* if we know all table aliases |
264
|
|
|
if (($table === '') && ($col === '*')) { |
265
|
|
|
array_pop($this->preventColumnRefs); |
266
|
|
|
$this->preventColumnRefs[] = true; |
267
|
|
|
|
268
|
|
|
return ASTERISK_ALIAS; # this is the position, we have to replace later |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
$alias = ''; |
272
|
|
|
if (isset($parsed['alias'])) { |
273
|
|
|
$alias = $this->processAlias($parsed['alias']); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
return (($table !== '') ? ($table.'.'.$col) : $col).$alias; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
protected function processFunctionOnSelect($parsed) |
280
|
|
|
{ |
281
|
|
|
$old = end($this->preventColumnRefs); |
282
|
|
|
$sql = $this->processFunction($parsed); |
283
|
|
|
|
284
|
|
|
if ($old !== end($this->preventColumnRefs)) { |
285
|
|
|
# prevents wrong handling of count(*) |
286
|
|
|
array_pop($this->preventColumnRefs); |
287
|
|
|
$this->preventColumnRefs[] = $old; |
288
|
|
|
$sql = str_replace(ASTERISK_ALIAS, '*', $sql); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
return $sql; |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
protected function processSELECT($parsed) |
295
|
|
|
{ |
296
|
|
|
$this->preventColumnRefs[] = false; |
297
|
|
|
|
298
|
|
|
$sql = ''; |
299
|
|
|
foreach ($parsed as $k => $v) { |
300
|
|
|
$len = strlen($sql); |
301
|
|
|
$sql .= $this->processColRef($v); |
302
|
|
|
$sql .= $this->processSelectExpression($v); |
303
|
|
|
$sql .= $this->processFunctionOnSelect($v); |
304
|
|
|
$sql .= $this->processConstant($v); |
305
|
|
|
|
306
|
|
|
if ($len == strlen($sql)) { |
307
|
|
|
$this->stop('SELECT', $k, $v, 'expr_type'); |
|
|
|
|
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
$sql .= ','; |
311
|
|
|
} |
312
|
|
|
$sql = substr($sql, 0, -1); |
313
|
|
|
|
314
|
|
|
return 'SELECT '.$sql; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
private function correctColRefStatement($sql) |
318
|
|
|
{ |
319
|
|
|
$alias = ''; |
320
|
|
|
$tables = end($this->allTables); |
321
|
|
|
|
322
|
|
|
# should we correct the selection list? |
323
|
|
|
if (array_pop($this->preventColumnRefs)) { |
324
|
|
|
|
325
|
|
|
# do we have a table-expression alias? |
326
|
|
|
if ($tables['alias'] !== '') { |
327
|
|
|
$alias = $tables['alias'].'.*'; |
328
|
|
|
} else { |
329
|
|
|
foreach ($tables['tables'] as $k => $v) { |
330
|
|
|
$alias .= ($v['alias'] === '' ? $v['table'] : $v['alias']).'.*,'; |
331
|
|
|
} |
332
|
|
|
$alias = substr($alias, 0, -1); |
333
|
|
|
} |
334
|
|
|
$sql = str_replace(ASTERISK_ALIAS, $alias, $sql); |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
return $sql; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
protected function processSelectStatement($parsed) |
341
|
|
|
{ |
342
|
|
|
$sql = $this->processSELECT($parsed['SELECT']); |
343
|
|
|
$from = $this->processFROM($parsed['FROM']); |
344
|
|
|
|
345
|
|
|
# correct * references with tablealias.* |
346
|
|
|
# this must be called after processFROM(), because we need the table information |
347
|
|
|
$sql = $this->correctColRefStatement($sql).' '.$from; |
348
|
|
|
|
349
|
|
|
if (isset($parsed['WHERE'])) { |
350
|
|
|
$sql .= ' '.$this->processWHERE($parsed['WHERE']); |
351
|
|
|
} |
352
|
|
|
if (isset($parsed['GROUP'])) { |
353
|
|
|
$sql .= ' '.$this->processGROUP($parsed['GROUP']); |
354
|
|
|
} |
355
|
|
|
if (isset($parsed['ORDER'])) { |
356
|
|
|
$sql .= ' '.$this->processORDER($parsed['ORDER']); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
# select finished, we remove its tables |
360
|
|
|
# FIXME: we should add it to the previous tablelist with the |
361
|
|
|
# global alias, if such one exists |
362
|
|
|
array_pop($this->allTables); |
363
|
|
|
|
364
|
|
|
return $sql; |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
public function create($parsed) |
368
|
|
|
{ |
369
|
|
|
$k = key($parsed); |
370
|
|
|
switch ($k) { |
371
|
|
|
case 'USE': |
372
|
|
|
# this statement is not an Oracle statement |
373
|
|
|
$this->created = ''; |
|
|
|
|
374
|
|
|
break; |
375
|
|
|
|
376
|
|
|
default: |
377
|
|
|
$this->created = parent::create($parsed); |
378
|
|
|
break; |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
return $this->created; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
public function process($sql) |
385
|
|
|
{ |
386
|
|
|
self::dbgprint($sql.'<br/>'); |
387
|
|
|
|
388
|
|
|
$this->initGlobalVariables(); |
389
|
|
|
$parser = new SQLParser($sql); |
390
|
|
|
self::preprint($parser->parsed); |
|
|
|
|
391
|
|
|
|
392
|
|
|
$sql = $this->create($parser->parsed); |
393
|
|
|
self::dbgprint($sql.'<br/>'); |
394
|
|
|
|
395
|
|
|
return $sql; |
396
|
|
|
} |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
//$translator = new OracleSQLTranslator(false); |
|
|
|
|
400
|
|
|
//$translator->process("SELECT q.qid, question, gid FROM questions as q WHERE (select count(*) from answers as a where a.qid=q.qid and scale_id=0)=0 and sid=11929 AND type IN ('F', 'H', 'W', 'Z', '1') and q.parent_qid=0"); |
401
|
|
|
//$translator->process("SELECT *, (SELECT a from xyz WHERE b>1) haha, (SELECT *, b from zks,abc WHERE d=1) hoho FROM blubb d, blibb c WHERE d.col = c.col"); |
402
|
|
|
|
403
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.