1
0
mirror of https://github.com/dg/dibi.git synced 2025-02-24 19:02:36 +01:00
php-dibi/dibi/libs/DibiTranslator.php

606 lines
16 KiB
PHP
Raw Normal View History

2008-07-17 03:51:29 +00:00
<?php
/**
* dibi - tiny'n'smart database abstraction layer
* ----------------------------------------------
*
2010-01-03 15:15:37 +01:00
* @copyright Copyright (c) 2005, 2010 David Grudl
2008-07-17 03:51:29 +00:00
* @license http://dibiphp.com/license dibi license
* @link http://dibiphp.com
* @package dibi
*/
/**
* dibi SQL translator.
*
2010-01-03 15:15:37 +01:00
* @copyright Copyright (c) 2005, 2010 David Grudl
2008-07-17 03:51:29 +00:00
* @package dibi
*/
final class DibiTranslator extends DibiObject
2008-07-17 03:51:29 +00:00
{
/** @var IDibiDriver */
private $driver;
/** @var int */
private $cursor;
/** @var array */
private $args;
/** @var bool */
private $hasError;
/** @var bool */
private $comment;
/** @var int */
private $ifLevel;
/** @var int */
private $ifLevelStart;
/** @var int */
private $limit;
/** @var int */
private $offset;
/** @var DibiLazyStorage */
private $identifiers;
2010-08-03 00:57:35 +02:00
2008-07-17 03:51:29 +00:00
public function __construct(IDibiDriver $driver)
{
$this->driver = $driver;
}
/**
* Generates SQL.
* @param array
* @return string
* @throws DibiException
2008-07-17 03:51:29 +00:00
*/
public function translate(array $args)
{
$this->identifiers = new DibiLazyStorage(array($this, 'delimite'));
2010-08-03 00:57:35 +02:00
$args = array_values($args);
while (count($args) === 1 && is_array($args[0])) { // implicit array expansion
$args = array_values($args[0]);
}
$this->args = $args;
2008-07-17 03:51:29 +00:00
$this->limit = -1;
$this->offset = 0;
$this->hasError = FALSE;
$commandIns = NULL;
$lastArr = NULL;
// shortcuts
$cursor = & $this->cursor;
$cursor = 0;
// conditional sql
$this->ifLevel = $this->ifLevelStart = 0;
$comment = & $this->comment;
$comment = FALSE;
// iterate
$sql = array();
2010-08-03 00:57:35 +02:00
while ($cursor < count($this->args))
2008-07-17 03:51:29 +00:00
{
2010-08-03 00:57:35 +02:00
$arg = $this->args[$cursor];
2008-07-17 03:51:29 +00:00
$cursor++;
// simple string means SQL
if (is_string($arg)) {
// speed-up - is regexp required?
$toSkip = strcspn($arg, '`[\'":%');
2008-07-17 03:51:29 +00:00
if (strlen($arg) === $toSkip) { // needn't be translated
$sql[] = $arg;
} else {
$sql[] = substr($arg, 0, $toSkip)
/*
2010-05-16 22:40:39 +02:00
. preg_replace_callback('/
2009-05-11 18:43:30 +00:00
(?=[`[\'":%?]) ## speed-up
2008-07-17 03:51:29 +00:00
(?:
`(.+?)`| ## 1) `identifier`
\[(.+?)\]| ## 2) [identifier]
(\')((?:\'\'|[^\'])*)\'| ## 3,4) 'string'
2008-07-17 03:51:29 +00:00
(")((?:""|[^"])*)"| ## 5,6) "string"
(\'|")| ## 7) lone quote
2010-05-16 22:40:39 +02:00
:(\S*?:)([a-zA-Z0-9._]?)| ## 8,9) :substitution:
%([a-zA-Z~][a-zA-Z0-9~]{0,5})|## 10) modifier
2009-05-11 18:43:30 +00:00
(\?) ## 11) placeholder
2008-07-17 03:51:29 +00:00
)/xs',
*/ // note: this can change $this->args & $this->cursor & ...
. preg_replace_callback('/(?=[`[\'":%?])(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|:(\S*?:)([a-zA-Z0-9._]?)|%([a-zA-Z~][a-zA-Z0-9~]{0,5})|(\?))/s',
2008-07-17 03:51:29 +00:00
array($this, 'cb'),
substr($arg, $toSkip)
);
if (preg_last_error()) throw new PcreException;
2008-07-17 03:51:29 +00:00
}
continue;
}
if ($comment) {
$sql[] = '...';
continue;
}
if ($arg instanceof Traversable) {
$arg = iterator_to_array($arg);
}
2008-07-17 03:51:29 +00:00
if (is_array($arg)) {
if (is_string(key($arg))) {
// associative array -> autoselect between SET or VALUES & LIST
if ($commandIns === NULL) {
2010-08-03 00:57:35 +02:00
$commandIns = strtoupper(substr(ltrim($this->args[0]), 0, 6));
2008-07-17 03:51:29 +00:00
$commandIns = $commandIns === 'INSERT' || $commandIns === 'REPLAC';
$sql[] = $this->formatValue($arg, $commandIns ? 'v' : 'a');
} else {
if ($lastArr === $cursor - 1) $sql[] = ',';
$sql[] = $this->formatValue($arg, $commandIns ? 'l' : 'a');
}
$lastArr = $cursor;
continue;
}
}
// default processing
$sql[] = $this->formatValue($arg, FALSE);
} // while
if ($comment) $sql[] = "*/";
$sql = implode(' ', $sql);
if ($this->hasError) {
throw new DibiException('SQL translate error', 0, $sql);
}
2008-07-17 03:51:29 +00:00
// apply limit
if ($this->limit > -1 || $this->offset > 0) {
$this->driver->applyLimit($sql, $this->limit, $this->offset);
}
return $sql;
2008-07-17 03:51:29 +00:00
}
/**
* Apply modifier to single value.
* @param mixed
* @param string
* @return string
*/
public function formatValue($value, $modifier)
{
// array processing (with or without modifier)
if ($value instanceof Traversable) {
$value = iterator_to_array($value);
}
2008-07-17 03:51:29 +00:00
if (is_array($value)) {
2008-07-17 03:51:29 +00:00
$vx = $kx = array();
switch ($modifier) {
case 'and':
case 'or': // key=val AND key IS NULL AND ...
if (empty($value)) {
return '1=1';
}
foreach ($value as $k => $v) {
if (is_string($k)) {
$pair = explode('%', $k, 2); // split into identifier & modifier
$k = $this->identifiers->{$pair[0]} . ' ';
if (!isset($pair[1])) {
$v = $this->formatValue($v, FALSE);
$vx[] = $k . ($v === 'NULL' ? 'IS ' : '= ') . $v;
} elseif ($pair[1] === 'ex') { // TODO: this will be removed
$vx[] = $k . $this->formatValue($v, 'ex');
} else {
$v = $this->formatValue($v, $pair[1]);
$vx[] = $k . ($pair[1] === 'l' || $pair[1] === 'in' ? 'IN ' : ($v === 'NULL' ? 'IS ' : '= ')) . $v;
}
} else {
$vx[] = $this->formatValue($v, 'ex');
}
2008-07-17 03:51:29 +00:00
}
return '(' . implode(') ' . strtoupper($modifier) . ' (', $vx) . ')';
case 'n': // key, key, ... identifier names
foreach ($value as $k => $v) {
if (is_string($k)) {
$vx[] = $this->identifiers->$k . (empty($v) ? '' : ' AS ' . $v);
} else {
2009-08-13 12:33:28 +00:00
$pair = explode('%', $v, 2); // split into identifier & modifier
$vx[] = $this->identifiers->{$pair[0]};
}
}
return implode(', ', $vx);
case 'a': // key=val, key=val, ...
2008-07-17 03:51:29 +00:00
foreach ($value as $k => $v) {
$pair = explode('%', $k, 2); // split into identifier & modifier
$vx[] = $this->identifiers->{$pair[0]} . '='
. $this->formatValue($v, isset($pair[1]) ? $pair[1] : (is_array($v) ? 'ex' : FALSE));
2008-07-17 03:51:29 +00:00
}
return implode(', ', $vx);
2008-07-17 03:51:29 +00:00
case 'in':// replaces scalar %in modifier!
case 'l': // (val, val, ...)
2008-07-17 03:51:29 +00:00
foreach ($value as $k => $v) {
$pair = explode('%', $k, 2); // split into identifier & modifier
$vx[] = $this->formatValue($v, isset($pair[1]) ? $pair[1] : (is_array($v) ? 'ex' : FALSE));
2008-07-17 03:51:29 +00:00
}
return '(' . (($vx || $modifier === 'l') ? implode(', ', $vx) : 'NULL') . ')';
2008-07-17 03:51:29 +00:00
case 'v': // (key, key, ...) VALUES (val, val, ...)
foreach ($value as $k => $v) {
$pair = explode('%', $k, 2); // split into identifier & modifier
$kx[] = $this->identifiers->{$pair[0]};
$vx[] = $this->formatValue($v, isset($pair[1]) ? $pair[1] : (is_array($v) ? 'ex' : FALSE));
2008-07-17 03:51:29 +00:00
}
return '(' . implode(', ', $kx) . ') VALUES (' . implode(', ', $vx) . ')';
case 'm': // (key, key, ...) VALUES (val, val, ...), (val, val, ...), ...
foreach ($value as $k => $v) {
if (is_array($v)) {
if (isset($proto)) {
if ($proto !== array_keys($v)) {
$this->hasError = TRUE;
return '**Multi-insert array "' . $k . '" is different.**';
}
} else {
$proto = array_keys($v);
}
} else {
$this->hasError = TRUE;
return '**Unexpected type ' . gettype($v) . '**';
}
$pair = explode('%', $k, 2); // split into identifier & modifier
$kx[] = $this->identifiers->{$pair[0]};
foreach ($v as $k2 => $v2) {
$vx[$k2][] = $this->formatValue($v2, isset($pair[1]) ? $pair[1] : (is_array($v2) ? 'ex' : FALSE));
}
}
foreach ($vx as $k => $v) {
$vx[$k] = '(' . implode(', ', $v) . ')';
}
return '(' . implode(', ', $kx) . ') VALUES ' . implode(', ', $vx);
case 'by': // key ASC, key DESC
foreach ($value as $k => $v) {
if (is_array($v)) {
$vx[] = $this->formatValue($v, 'ex');
} elseif (is_string($k)) {
2008-10-28 01:03:50 +00:00
$v = (is_string($v) && strncasecmp($v, 'd', 1)) || $v > 0 ? 'ASC' : 'DESC';
$vx[] = $this->identifiers->$k . ' ' . $v;
2008-10-01 16:23:44 +00:00
} else {
$vx[] = $this->identifiers->$v;
2008-10-01 16:23:44 +00:00
}
}
return implode(', ', $vx);
case 'ex':
case 'sql':
$translator = new self($this->driver);
return $translator->translate($value);
default: // value, value, value - all with the same modifier
2008-07-17 03:51:29 +00:00
foreach ($value as $v) {
$vx[] = $this->formatValue($v, $modifier);
}
return implode(', ', $vx);
2008-07-17 03:51:29 +00:00
}
}
// with modifier procession
if ($modifier) {
if ($value !== NULL && !is_scalar($value) && !($value instanceof DateTime)) { // array is already processed
2008-07-17 03:51:29 +00:00
$this->hasError = TRUE;
return '**Unexpected type ' . gettype($value) . '**';
}
switch ($modifier) {
case 's': // string
case 'bin':// binary
case 'b': // boolean
return $value === NULL ? 'NULL' : $this->driver->escape($value, $modifier);
2008-07-17 03:51:29 +00:00
case 'sN': // string or NULL
case 'sn':
return $value == '' ? 'NULL' : $this->driver->escape($value, dibi::TEXT); // notice two equal signs
2008-07-17 03:51:29 +00:00
case 'iN': // signed int or NULL
case 'in': // deprecated
if ($value == '') $value = NULL;
// intentionally break omitted
2008-07-17 03:51:29 +00:00
case 'i': // signed int
case 'u': // unsigned int, ignored
// support for long numbers - keep them unchanged
2010-05-16 22:40:39 +02:00
if (is_string($value) && preg_match('#[+-]?\d++(e\d+)?$#A', $value)) {
2008-07-17 03:51:29 +00:00
return $value;
} else {
return $value === NULL ? 'NULL' : (string) (int) ($value + 0);
2008-07-17 03:51:29 +00:00
}
case 'f': // float
// support for extreme numbers - keep them unchanged
if (is_string($value) && is_numeric($value) && strpos($value, 'x') === FALSE) {
return $value; // something like -9E-005 is accepted by SQL, HEX values are not
} else {
return $value === NULL ? 'NULL' : rtrim(rtrim(number_format($value + 0, 5, '.', ''), '0'), '.');
2008-07-17 03:51:29 +00:00
}
case 'd': // date
case 't': // datetime
if ($value === NULL) {
return 'NULL';
} else {
if (is_numeric($value)) {
$value = (int) $value; // timestamp
} elseif (is_string($value)) {
$value = new DateTime($value);
}
return $this->driver->escape($value, $modifier);
}
2008-07-17 03:51:29 +00:00
2008-10-01 16:23:44 +00:00
case 'by':
2008-07-17 03:51:29 +00:00
case 'n': // identifier name
return $this->identifiers->$value;
2008-07-17 03:51:29 +00:00
case 'ex':
case 'sql': // preserve as dibi-SQL (TODO: leave only %ex)
2008-07-17 03:51:29 +00:00
$value = (string) $value;
// speed-up - is regexp required?
$toSkip = strcspn($value, '`[\'":');
if (strlen($value) !== $toSkip) {
$value = substr($value, 0, $toSkip)
. preg_replace_callback(
'/(?=[`[\'":])(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|:(\S*?:)([a-zA-Z0-9._]?))/s',
array($this, 'cb'),
substr($value, $toSkip)
2008-07-17 03:51:29 +00:00
);
if (preg_last_error()) throw new PcreException;
2008-07-17 03:51:29 +00:00
}
return $value;
2008-07-17 03:51:29 +00:00
case 'SQL': // preserve as real SQL (TODO: rename to %sql)
return (string) $value;
case 'like~': // LIKE string%
return $this->driver->escapeLike($value, 1);
case '~like': // LIKE %string
return $this->driver->escapeLike($value, -1);
case '~like~': // LIKE %string%
return $this->driver->escapeLike($value, 0);
2008-07-17 03:51:29 +00:00
case 'and':
case 'or':
case 'a':
case 'l':
case 'v':
$this->hasError = TRUE;
return '**Unexpected type ' . gettype($value) . '**';
default:
$this->hasError = TRUE;
return "**Unknown or invalid modifier %$modifier**";
}
}
// without modifier procession
2009-11-16 01:51:49 +01:00
if (is_string($value)) {
return $this->driver->escape($value, dibi::TEXT);
2008-07-17 03:51:29 +00:00
2010-08-03 00:57:35 +02:00
} elseif (is_int($value)) {
return (string) $value;
} elseif (is_float($value)) {
return rtrim(rtrim(number_format($value, 5, '.', ''), '0'), '.');
2008-07-17 03:51:29 +00:00
2009-11-16 01:51:49 +01:00
} elseif (is_bool($value)) {
return $this->driver->escape($value, dibi::BOOL);
2008-07-17 03:51:29 +00:00
2009-11-16 01:51:49 +01:00
} elseif ($value === NULL) {
2008-07-17 03:51:29 +00:00
return 'NULL';
2009-11-16 01:51:49 +01:00
} elseif ($value instanceof DateTime) {
return $this->driver->escape($value, dibi::DATETIME);
2009-05-11 18:43:30 +00:00
} else {
2009-11-16 01:51:49 +01:00
$this->hasError = TRUE;
return '**Unexpected ' . gettype($value) . '**';
}
2008-07-17 03:51:29 +00:00
}
/**
* PREG callback from translate() or formatValue().
* @param array
* @return string
*/
private function cb($matches)
{
// [1] => `ident`
// [2] => [ident]
// [3] => '
// [4] => string
// [5] => "
// [6] => string
// [7] => lone-quote
// [8] => substitution
// [9] => substitution flag
// [10] => modifier (when called from self::translate())
2009-05-11 18:43:30 +00:00
// [11] => placeholder (when called from self::translate())
if (!empty($matches[11])) { // placeholder
$cursor = & $this->cursor;
if ($cursor >= count($this->args)) {
$this->hasError = TRUE;
return "**Extra placeholder**";
}
$cursor++;
return $this->formatValue($this->args[$cursor - 1], FALSE);
}
2008-07-17 03:51:29 +00:00
if (!empty($matches[10])) { // modifier
$mod = $matches[10];
2008-07-17 03:51:29 +00:00
$cursor = & $this->cursor;
if ($cursor >= count($this->args) && $mod !== 'else' && $mod !== 'end') {
$this->hasError = TRUE;
return "**Extra modifier %$mod**";
}
if ($mod === 'if') {
$this->ifLevel++;
$cursor++;
if (!$this->comment && !$this->args[$cursor - 1]) {
// open comment
$this->ifLevelStart = $this->ifLevel;
$this->comment = TRUE;
return "/*";
}
return '';
} elseif ($mod === 'else') {
if ($this->ifLevelStart === $this->ifLevel) {
$this->ifLevelStart = 0;
$this->comment = FALSE;
return "*/";
} elseif (!$this->comment) {
$this->ifLevelStart = $this->ifLevel;
$this->comment = TRUE;
return "/*";
}
} elseif ($mod === 'end') {
$this->ifLevel--;
if ($this->ifLevelStart === $this->ifLevel + 1) {
// close comment
$this->ifLevelStart = 0;
$this->comment = FALSE;
return "*/";
}
return '';
} elseif ($mod === 'ex') { // array expansion
array_splice($this->args, $cursor, 1, $this->args[$cursor]);
return '';
} elseif ($mod === 'lmt') { // apply limit
if ($this->args[$cursor] !== NULL) $this->limit = (int) $this->args[$cursor];
$cursor++;
return '';
} elseif ($mod === 'ofs') { // apply offset
if ($this->args[$cursor] !== NULL) $this->offset = (int) $this->args[$cursor];
$cursor++;
return '';
} else { // default processing
$cursor++;
return $this->formatValue($this->args[$cursor - 1], $mod);
}
}
if ($this->comment) return '...';
if ($matches[1]) // SQL identifiers: `ident`
return $this->identifiers->{$matches[1]};
2008-07-17 03:51:29 +00:00
if ($matches[2]) // SQL identifiers: [ident]
return $this->identifiers->{$matches[2]};
2008-07-17 03:51:29 +00:00
if ($matches[3]) // SQL strings: '...'
return $this->driver->escape( str_replace("''", "'", $matches[4]), dibi::TEXT);
2008-07-17 03:51:29 +00:00
if ($matches[5]) // SQL strings: "..."
return $this->driver->escape( str_replace('""', '"', $matches[6]), dibi::TEXT);
2008-07-17 03:51:29 +00:00
if ($matches[7]) { // string quote
$this->hasError = TRUE;
return '**Alone quote**';
}
if ($matches[8]) { // SQL identifier substitution
$m = substr($matches[8], 0, -1);
return $matches[9] == '' ? $this->formatValue(dibi::$substs->$m, FALSE) : dibi::$substs->$m . $matches[9]; // value or identifier
}
2008-07-17 03:51:29 +00:00
die('this should be never executed');
}
/**
* Apply substitutions to indentifier and delimites it.
* @param string indentifier
* @return string
2010-08-03 22:48:44 +02:00
* @internal
2008-07-17 03:51:29 +00:00
*/
public function delimite($value)
2008-07-17 03:51:29 +00:00
{
2010-08-03 00:57:35 +02:00
$value = self::substitute($value);
$parts = explode('.', $value);
foreach ($parts as & $v) {
if ($v !== '*') $v = $this->driver->escape($v, dibi::IDENTIFIER);
}
return implode('.', $parts);
2008-07-17 03:51:29 +00:00
}
/**
* Provides substitution.
* @return string
*/
public static function substitute($value)
{
if (strpos($value, ':') !== FALSE) { // provide substitution
return preg_replace_callback('#:([^:\s]*):#', array(__CLASS__, 'subCb'), $value);
}
return $value;
}
/**
* Substitution callback.
* @param array
* @return string
*/
private static function subCb($m)
{
return dibi::$substs->{$m[1]};
}
}