Source for file DibiTranslator.php
Documentation is available at DibiTranslator.php
4: * dibi - tiny'n'smart database abstraction layer
5: * ----------------------------------------------
7: * @copyright Copyright (c) 2005, 2010 David Grudl
8: * @license http://dibiphp.com/license dibi license
9: * @link http://dibiphp.com
16: * dibi SQL translator.
18: * @copyright Copyright (c) 2005, 2010 David Grudl
23: /** @var IDibiDriver */
42: private $ifLevelStart;
50: /** @var DibiLazyStorage */
51: private $identifiers;
57: $this->driver =
$driver;
66: * @throws DibiException
70: $this->identifiers =
new DibiLazyStorage(array($this, 'delimite'));
71: $args =
array_values($args);
72: while (count($args) ===
1 &&
is_array($args[0])) { // implicit array expansion
73: $args =
array_values($args[0]);
79: $this->hasError =
FALSE;
83: $cursor =
& $this->cursor;
87: $this->ifLevel =
$this->ifLevelStart =
0;
88: $comment =
& $this->comment;
93: while ($cursor <
count($this->args))
95: $arg =
$this->args[$cursor];
98: // simple string means SQL
100: // speed-up - is regexp required?
103: if (strlen($arg) ===
$toSkip) { // needn't be translated
108: . preg_replace_callback('/
109: (?=[`[\'":%?]) ## speed-up
111: `(.+?)`| ## 1) `identifier`
112: \[(.+?)\]| ## 2) [identifier]
113: (\')((?:\'\'|[^\'])*)\'| ## 3,4) 'string'
114: (")((?:""|[^"])*)"| ## 5,6) "string"
115: (\'|")| ## 7) lone quote
116: :(\S*?:)([a-zA-Z0-9._]?)| ## 8,9) :substitution:
117: %([a-zA-Z~][a-zA-Z0-9~]{0,5})|## 10) modifier
118: (\?) ## 11) placeholder
120: */ // note: this can change $this->args & $this->cursor & ...
121: .
preg_replace_callback('/(?=[`[\'":%?])(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|:(\S*?:)([a-zA-Z0-9._]?)|%([a-zA-Z~][a-zA-Z0-9~]{0,5})|(\?))/s',
135: if ($arg instanceof
Traversable) {
141: // associative array -> autoselect between SET or VALUES & LIST
142: if ($commandIns ===
NULL) {
144: $commandIns =
$commandIns ===
'INSERT' ||
$commandIns ===
'REPLAC';
147: if ($lastArr ===
$cursor -
1) $sql[] =
',';
155: // default processing
160: if ($comment) $sql[] =
"*/";
164: if ($this->hasError) {
169: if ($this->limit > -
1 ||
$this->offset >
0) {
170: $this->driver->applyLimit($sql, $this->limit, $this->offset);
179: * Apply modifier to single value.
186: // array processing (with or without modifier)
187: if ($value instanceof
Traversable) {
192: $vx =
$kx =
array();
193: switch ($modifier) {
195: case 'or':
// key=val AND key IS NULL AND ...
196: if (empty($value)) {
200: foreach ($value as $k =>
$v) {
202: $pair =
explode('%', $k, 2); // split into identifier & modifier
203: $k =
$this->identifiers->{$pair[0]} .
' ';
204: if (!isset($pair[1])) {
206: $vx[] =
$k .
($v ===
'NULL' ?
'IS ' :
'= ') .
$v;
208: } elseif ($pair[1] ===
'ex') { // TODO: this will be removed
213: $vx[] =
$k .
($pair[1] ===
'l' ||
$pair[1] ===
'in' ?
'IN ' :
($v ===
'NULL' ?
'IS ' :
'= ')) .
$v;
222: case 'n':
// key, key, ... identifier names
223: foreach ($value as $k =>
$v) {
225: $vx[] =
$this->identifiers->$k .
(empty($v) ?
'' :
' AS ' .
$v);
227: $pair =
explode('%', $v, 2); // split into identifier & modifier
228: $vx[] =
$this->identifiers->{$pair[0]};
234: case 'a':
// key=val, key=val, ...
235: foreach ($value as $k =>
$v) {
236: $pair =
explode('%', $k, 2); // split into identifier & modifier
237: $vx[] =
$this->identifiers->{$pair[0]} .
'='
243: case 'in':
// replaces scalar %in modifier!
244: case 'l':
// (val, val, ...)
245: foreach ($value as $k =>
$v) {
246: $pair =
explode('%', $k, 2); // split into identifier & modifier
249: return '(' .
(($vx ||
$modifier ===
'l') ?
implode(', ', $vx) :
'NULL') .
')';
252: case 'v':
// (key, key, ...) VALUES (val, val, ...)
253: foreach ($value as $k =>
$v) {
254: $pair =
explode('%', $k, 2); // split into identifier & modifier
255: $kx[] =
$this->identifiers->{$pair[0]};
260: case 'm':
// (key, key, ...) VALUES (val, val, ...), (val, val, ...), ...
261: foreach ($value as $k =>
$v) {
263: if (isset($proto)) {
265: $this->hasError =
TRUE;
266: return '**Multi-insert array "' .
$k .
'" is different.**';
272: $this->hasError =
TRUE;
276: $pair =
explode('%', $k, 2); // split into identifier & modifier
277: $kx[] =
$this->identifiers->{$pair[0]};
278: foreach ($v as $k2 =>
$v2) {
282: foreach ($vx as $k =>
$v) {
287: case 'by':
// key ASC, key DESC
288: foreach ($value as $k =>
$v) {
293: $vx[] =
$this->identifiers->$k .
' ' .
$v;
295: $vx[] =
$this->identifiers->$v;
302: $translator =
new self($this->driver);
305: default:
// value, value, value - all with the same modifier
306: foreach ($value as $v) {
314: // with modifier procession
316: if ($value !==
NULL &&
!is_scalar($value) &&
!($value instanceof
DateTime)) { // array is already processed
317: $this->hasError =
TRUE;
318: return '**Unexpected type ' .
gettype($value) .
'**';
321: switch ($modifier) {
323: case 'bin':
// binary
324: case 'b':
// boolean
325: return $value ===
NULL ?
'NULL' :
$this->driver->escape($value, $modifier);
327: case 'sN':
// string or NULL
329: return $value ==
'' ?
'NULL' :
$this->driver->escape($value, dibi::TEXT); // notice two equal signs
331: case 'iN':
// signed int or NULL
332: case 'in':
// deprecated
333: if ($value ==
'') $value =
NULL;
334: // intentionally break omitted
336: case 'i':
// signed int
337: case 'u':
// unsigned int, ignored
338: // support for long numbers - keep them unchanged
342: return $value ===
NULL ?
'NULL' : (string) (int)
($value +
0);
346: // support for extreme numbers - keep them unchanged
348: return $value; // something like -9E-005 is accepted by SQL, HEX values are not
354: case 't':
// datetime
355: if ($value ===
NULL) {
359: $value = (int)
$value; // timestamp
362: $value =
new DateTime($value);
364: return $this->driver->escape($value, $modifier);
368: case 'n':
// identifier name
369: return $this->identifiers->$value;
372: case 'sql':
// preserve as dibi-SQL (TODO: leave only %ex)
373: $value = (string)
$value;
374: // speed-up - is regexp required?
379: '/(?=[`[\'":])(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|:(\S*?:)([a-zA-Z0-9._]?))/s',
387: case 'SQL':
// preserve as real SQL (TODO: rename to %sql)
388: return (string)
$value;
390: case 'like~':
// LIKE string%
391: return $this->driver->escapeLike($value, 1);
393: case '~like':
// LIKE %string
394: return $this->driver->escapeLike($value, -
1);
396: case '~like~':
// LIKE %string%
397: return $this->driver->escapeLike($value, 0);
404: $this->hasError =
TRUE;
405: return '**Unexpected type ' .
gettype($value) .
'**';
408: $this->hasError =
TRUE;
409: return "**Unknown or invalid modifier %$modifier**";
414: // without modifier procession
416: return $this->driver->escape($value, dibi::TEXT);
419: return (string)
$value;
425: return $this->driver->escape($value, dibi::BOOL);
427: } elseif ($value ===
NULL) {
430: } elseif ($value instanceof
DateTime) {
431: return $this->driver->escape($value, dibi::DATETIME);
434: $this->hasError =
TRUE;
442: * PREG callback from translate() or formatValue().
446: private function cb($matches)
454: // [7] => lone-quote
455: // [8] => substitution
456: // [9] => substitution flag
457: // [10] => modifier (when called from self::translate())
458: // [11] => placeholder (when called from self::translate())
461: if (!empty($matches[11])) { // placeholder
462: $cursor =
& $this->cursor;
465: $this->hasError =
TRUE;
466: return "**Extra placeholder**";
473: if (!empty($matches[10])) { // modifier
474: $mod =
$matches[10];
475: $cursor =
& $this->cursor;
477: if ($cursor >=
count($this->args) &&
$mod !==
'else' &&
$mod !==
'end') {
478: $this->hasError =
TRUE;
479: return "**Extra modifier %$mod**";
482: if ($mod ===
'if') {
485: if (!$this->comment &&
!$this->args[$cursor -
1]) {
487: $this->ifLevelStart =
$this->ifLevel;
488: $this->comment =
TRUE;
493: } elseif ($mod ===
'else') {
494: if ($this->ifLevelStart ===
$this->ifLevel) {
495: $this->ifLevelStart =
0;
496: $this->comment =
FALSE;
498: } elseif (!$this->comment) {
499: $this->ifLevelStart =
$this->ifLevel;
500: $this->comment =
TRUE;
504: } elseif ($mod ===
'end') {
506: if ($this->ifLevelStart ===
$this->ifLevel +
1) {
508: $this->ifLevelStart =
0;
509: $this->comment =
FALSE;
514: } elseif ($mod ===
'ex') { // array expansion
518: } elseif ($mod ===
'lmt') { // apply limit
519: if ($this->args[$cursor] !==
NULL) $this->limit = (int)
$this->args[$cursor];
523: } elseif ($mod ===
'ofs') { // apply offset
524: if ($this->args[$cursor] !==
NULL) $this->offset = (int)
$this->args[$cursor];
528: } else { // default processing
534: if ($this->comment) return '...';
536: if ($matches[1]) // SQL identifiers: `ident`
537: return $this->identifiers->{$matches[1]};
539: if ($matches[2]) // SQL identifiers: [ident]
540: return $this->identifiers->{$matches[2]};
542: if ($matches[3]) // SQL strings: '...'
545: if ($matches[5]) // SQL strings: "..."
548: if ($matches[7]) { // string quote
549: $this->hasError =
TRUE;
550: return '**Alone quote**';
553: if ($matches[8]) { // SQL identifier substitution
555: return $matches[9] ==
'' ?
$this->formatValue(dibi::$substs->$m, FALSE) :
dibi::$substs->$m .
$matches[9]; // value or identifier
558: die('this should be never executed');
564: * Apply substitutions to indentifier and delimites it.
565: * @param string indentifier
569: public function delimite($value)
571: $value =
self::substitute($value);
573: foreach ($parts as & $v) {
574: if ($v !==
'*') $v =
$this->driver->escape($v, dibi::IDENTIFIER);
582: * Provides substitution.
587: if (strpos($value, ':') !==
FALSE) { // provide substitution
596: * Substitution callback.
600: private static function subCb($m)