PageRenderTime 48ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/libs/dibi/libs/DibiTranslator.php

https://github.com/premiumcombination/nts
PHP | 592 lines | 392 code | 101 blank | 99 comment | 79 complexity | 87890d1f8aaa70ee2e145da47565b83f MD5 | raw file
  1. <?php
  2. /**
  3. * This file is part of the "dibi" - smart database abstraction layer.
  4. *
  5. * Copyright (c) 2005, 2010 David Grudl (http://davidgrudl.com)
  6. *
  7. * For the full copyright and license information, please view
  8. * the file license.txt that was distributed with this source code.
  9. *
  10. * @package dibi
  11. */
  12. /**
  13. * dibi SQL translator.
  14. *
  15. * @author David Grudl
  16. */
  17. final class DibiTranslator extends DibiObject
  18. {
  19. /** @var DibiConnection */
  20. private $connection;
  21. /** @var IDibiDriver */
  22. private $driver;
  23. /** @var int */
  24. private $cursor;
  25. /** @var array */
  26. private $args;
  27. /** @var bool */
  28. private $hasError;
  29. /** @var bool */
  30. private $comment;
  31. /** @var int */
  32. private $ifLevel;
  33. /** @var int */
  34. private $ifLevelStart;
  35. /** @var int */
  36. private $limit;
  37. /** @var int */
  38. private $offset;
  39. /** @var DibiHashMap */
  40. private $identifiers;
  41. public function __construct(DibiConnection $connection)
  42. {
  43. $this->connection = $connection;
  44. }
  45. /**
  46. * Generates SQL.
  47. * @param array
  48. * @return string
  49. * @throws DibiException
  50. */
  51. public function translate(array $args)
  52. {
  53. $this->identifiers = new DibiHashMap(array($this, 'delimite'));
  54. $this->driver = $this->connection->getDriver();
  55. $args = array_values($args);
  56. while (count($args) === 1 && is_array($args[0])) { // implicit array expansion
  57. $args = array_values($args[0]);
  58. }
  59. $this->args = $args;
  60. $this->limit = -1;
  61. $this->offset = 0;
  62. $this->hasError = FALSE;
  63. $commandIns = NULL;
  64. $lastArr = NULL;
  65. // shortcuts
  66. $cursor = & $this->cursor;
  67. $cursor = 0;
  68. // conditional sql
  69. $this->ifLevel = $this->ifLevelStart = 0;
  70. $comment = & $this->comment;
  71. $comment = FALSE;
  72. // iterate
  73. $sql = array();
  74. while ($cursor < count($this->args))
  75. {
  76. $arg = $this->args[$cursor];
  77. $cursor++;
  78. // simple string means SQL
  79. if (is_string($arg)) {
  80. // speed-up - is regexp required?
  81. $toSkip = strcspn($arg, '`[\'":%');
  82. if (strlen($arg) === $toSkip) { // needn't be translated
  83. $sql[] = $arg;
  84. } else {
  85. $sql[] = substr($arg, 0, $toSkip)
  86. /*
  87. . preg_replace_callback('/
  88. (?=[`[\'":%?]) ## speed-up
  89. (?:
  90. `(.+?)`| ## 1) `identifier`
  91. \[(.+?)\]| ## 2) [identifier]
  92. (\')((?:\'\'|[^\'])*)\'| ## 3,4) 'string'
  93. (")((?:""|[^"])*)"| ## 5,6) "string"
  94. (\'|")| ## 7) lone quote
  95. :(\S*?:)([a-zA-Z0-9._]?)| ## 8,9) :substitution:
  96. %([a-zA-Z~][a-zA-Z0-9~]{0,5})|## 10) modifier
  97. (\?) ## 11) placeholder
  98. )/xs',
  99. */ // note: this can change $this->args & $this->cursor & ...
  100. . preg_replace_callback('/(?=[`[\'":%?])(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|:(\S*?:)([a-zA-Z0-9._]?)|%([a-zA-Z~][a-zA-Z0-9~]{0,5})|(\?))/s',
  101. array($this, 'cb'),
  102. substr($arg, $toSkip)
  103. );
  104. if (preg_last_error()) throw new DibiPcreException;
  105. }
  106. continue;
  107. }
  108. if ($comment) {
  109. $sql[] = '...';
  110. continue;
  111. }
  112. if ($arg instanceof Traversable) {
  113. $arg = iterator_to_array($arg);
  114. }
  115. if (is_array($arg)) {
  116. if (is_string(key($arg))) {
  117. // associative array -> autoselect between SET or VALUES & LIST
  118. if ($commandIns === NULL) {
  119. $commandIns = strtoupper(substr(ltrim($this->args[0]), 0, 6));
  120. $commandIns = $commandIns === 'INSERT' || $commandIns === 'REPLAC';
  121. $sql[] = $this->formatValue($arg, $commandIns ? 'v' : 'a');
  122. } else {
  123. if ($lastArr === $cursor - 1) $sql[] = ',';
  124. $sql[] = $this->formatValue($arg, $commandIns ? 'l' : 'a');
  125. }
  126. $lastArr = $cursor;
  127. continue;
  128. }
  129. }
  130. // default processing
  131. $sql[] = $this->formatValue($arg, FALSE);
  132. } // while
  133. if ($comment) $sql[] = "*/";
  134. $sql = implode(' ', $sql);
  135. if ($this->hasError) {
  136. throw new DibiException('SQL translate error', 0, $sql);
  137. }
  138. // apply limit
  139. if ($this->limit > -1 || $this->offset > 0) {
  140. $this->driver->applyLimit($sql, $this->limit, $this->offset);
  141. }
  142. return $sql;
  143. }
  144. /**
  145. * Apply modifier to single value.
  146. * @param mixed
  147. * @param string
  148. * @return string
  149. */
  150. public function formatValue($value, $modifier)
  151. {
  152. if ($this->comment) {
  153. return "...";
  154. }
  155. // array processing (with or without modifier)
  156. if ($value instanceof Traversable) {
  157. $value = iterator_to_array($value);
  158. }
  159. if (is_array($value)) {
  160. $vx = $kx = array();
  161. switch ($modifier) {
  162. case 'and':
  163. case 'or': // key=val AND key IS NULL AND ...
  164. if (empty($value)) {
  165. return '1=1';
  166. }
  167. foreach ($value as $k => $v) {
  168. if (is_string($k)) {
  169. $pair = explode('%', $k, 2); // split into identifier & modifier
  170. $k = $this->identifiers->{$pair[0]} . ' ';
  171. if (!isset($pair[1])) {
  172. $v = $this->formatValue($v, FALSE);
  173. $vx[] = $k . ($v === 'NULL' ? 'IS ' : '= ') . $v;
  174. } elseif ($pair[1] === 'ex') { // TODO: this will be removed
  175. $vx[] = $k . $this->formatValue($v, 'ex');
  176. } else {
  177. $v = $this->formatValue($v, $pair[1]);
  178. $vx[] = $k . ($pair[1] === 'l' || $pair[1] === 'in' ? 'IN ' : ($v === 'NULL' ? 'IS ' : '= ')) . $v;
  179. }
  180. } else {
  181. $vx[] = $this->formatValue($v, 'ex');
  182. }
  183. }
  184. return '(' . implode(') ' . strtoupper($modifier) . ' (', $vx) . ')';
  185. case 'n': // key, key, ... identifier names
  186. foreach ($value as $k => $v) {
  187. if (is_string($k)) {
  188. $vx[] = $this->identifiers->$k . (empty($v) ? '' : ' AS ' . $v);
  189. } else {
  190. $pair = explode('%', $v, 2); // split into identifier & modifier
  191. $vx[] = $this->identifiers->{$pair[0]};
  192. }
  193. }
  194. return implode(', ', $vx);
  195. case 'a': // key=val, key=val, ...
  196. foreach ($value as $k => $v) {
  197. $pair = explode('%', $k, 2); // split into identifier & modifier
  198. $vx[] = $this->identifiers->{$pair[0]} . '='
  199. . $this->formatValue($v, isset($pair[1]) ? $pair[1] : (is_array($v) ? 'ex' : FALSE));
  200. }
  201. return implode(', ', $vx);
  202. case 'in':// replaces scalar %in modifier!
  203. case 'l': // (val, val, ...)
  204. foreach ($value as $k => $v) {
  205. $pair = explode('%', $k, 2); // split into identifier & modifier
  206. $vx[] = $this->formatValue($v, isset($pair[1]) ? $pair[1] : (is_array($v) ? 'ex' : FALSE));
  207. }
  208. return '(' . (($vx || $modifier === 'l') ? implode(', ', $vx) : 'NULL') . ')';
  209. case 'v': // (key, key, ...) VALUES (val, val, ...)
  210. foreach ($value as $k => $v) {
  211. $pair = explode('%', $k, 2); // split into identifier & modifier
  212. $kx[] = $this->identifiers->{$pair[0]};
  213. $vx[] = $this->formatValue($v, isset($pair[1]) ? $pair[1] : (is_array($v) ? 'ex' : FALSE));
  214. }
  215. return '(' . implode(', ', $kx) . ') VALUES (' . implode(', ', $vx) . ')';
  216. case 'm': // (key, key, ...) VALUES (val, val, ...), (val, val, ...), ...
  217. foreach ($value as $k => $v) {
  218. if (is_array($v)) {
  219. if (isset($proto)) {
  220. if ($proto !== array_keys($v)) {
  221. $this->hasError = TRUE;
  222. return '**Multi-insert array "' . $k . '" is different.**';
  223. }
  224. } else {
  225. $proto = array_keys($v);
  226. }
  227. } else {
  228. $this->hasError = TRUE;
  229. return '**Unexpected type ' . gettype($v) . '**';
  230. }
  231. $pair = explode('%', $k, 2); // split into identifier & modifier
  232. $kx[] = $this->identifiers->{$pair[0]};
  233. foreach ($v as $k2 => $v2) {
  234. $vx[$k2][] = $this->formatValue($v2, isset($pair[1]) ? $pair[1] : (is_array($v2) ? 'ex' : FALSE));
  235. }
  236. }
  237. foreach ($vx as $k => $v) {
  238. $vx[$k] = '(' . implode(', ', $v) . ')';
  239. }
  240. return '(' . implode(', ', $kx) . ') VALUES ' . implode(', ', $vx);
  241. case 'by': // key ASC, key DESC
  242. foreach ($value as $k => $v) {
  243. if (is_array($v)) {
  244. $vx[] = $this->formatValue($v, 'ex');
  245. } elseif (is_string($k)) {
  246. $v = (is_string($v) && strncasecmp($v, 'd', 1)) || $v > 0 ? 'ASC' : 'DESC';
  247. $vx[] = $this->identifiers->$k . ' ' . $v;
  248. } else {
  249. $vx[] = $this->identifiers->$v;
  250. }
  251. }
  252. return implode(', ', $vx);
  253. case 'ex':
  254. case 'sql':
  255. $translator = new self($this->connection);
  256. return $translator->translate($value);
  257. default: // value, value, value - all with the same modifier
  258. foreach ($value as $v) {
  259. $vx[] = $this->formatValue($v, $modifier);
  260. }
  261. return implode(', ', $vx);
  262. }
  263. }
  264. // with modifier procession
  265. if ($modifier) {
  266. if ($value !== NULL && !is_scalar($value) && !($value instanceof DateTime)) { // array is already processed
  267. $this->hasError = TRUE;
  268. return '**Unexpected type ' . gettype($value) . '**';
  269. }
  270. switch ($modifier) {
  271. case 's': // string
  272. case 'bin':// binary
  273. case 'b': // boolean
  274. return $value === NULL ? 'NULL' : $this->driver->escape($value, $modifier);
  275. case 'sN': // string or NULL
  276. case 'sn':
  277. return $value == '' ? 'NULL' : $this->driver->escape($value, dibi::TEXT); // notice two equal signs
  278. case 'iN': // signed int or NULL
  279. case 'in': // deprecated
  280. if ($value == '') $value = NULL;
  281. // intentionally break omitted
  282. case 'i': // signed int
  283. case 'u': // unsigned int, ignored
  284. // support for long numbers - keep them unchanged
  285. if (is_string($value) && preg_match('#[+-]?\d++(e\d+)?$#A', $value)) {
  286. return $value;
  287. } else {
  288. return $value === NULL ? 'NULL' : (string) (int) ($value + 0);
  289. }
  290. case 'f': // float
  291. // support for extreme numbers - keep them unchanged
  292. if (is_string($value) && is_numeric($value) && strpos($value, 'x') === FALSE) {
  293. return $value; // something like -9E-005 is accepted by SQL, HEX values are not
  294. } else {
  295. return $value === NULL ? 'NULL' : rtrim(rtrim(number_format($value + 0, 5, '.', ''), '0'), '.');
  296. }
  297. case 'd': // date
  298. case 't': // datetime
  299. if ($value === NULL) {
  300. return 'NULL';
  301. } else {
  302. if (is_numeric($value)) {
  303. $value = (int) $value; // timestamp
  304. } elseif (is_string($value)) {
  305. $value = new DateTime($value);
  306. }
  307. return $this->driver->escape($value, $modifier);
  308. }
  309. case 'by':
  310. case 'n': // identifier name
  311. return $this->identifiers->$value;
  312. case 'ex':
  313. case 'sql': // preserve as dibi-SQL (TODO: leave only %ex)
  314. $value = (string) $value;
  315. // speed-up - is regexp required?
  316. $toSkip = strcspn($value, '`[\'":');
  317. if (strlen($value) !== $toSkip) {
  318. $value = substr($value, 0, $toSkip)
  319. . preg_replace_callback(
  320. '/(?=[`[\'":])(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|:(\S*?:)([a-zA-Z0-9._]?))/s',
  321. array($this, 'cb'),
  322. substr($value, $toSkip)
  323. );
  324. if (preg_last_error()) throw new DibiPcreException;
  325. }
  326. return $value;
  327. case 'SQL': // preserve as real SQL (TODO: rename to %sql)
  328. return (string) $value;
  329. case 'like~': // LIKE string%
  330. return $this->driver->escapeLike($value, 1);
  331. case '~like': // LIKE %string
  332. return $this->driver->escapeLike($value, -1);
  333. case '~like~': // LIKE %string%
  334. return $this->driver->escapeLike($value, 0);
  335. case 'and':
  336. case 'or':
  337. case 'a':
  338. case 'l':
  339. case 'v':
  340. $this->hasError = TRUE;
  341. return '**Unexpected type ' . gettype($value) . '**';
  342. default:
  343. $this->hasError = TRUE;
  344. return "**Unknown or invalid modifier %$modifier**";
  345. }
  346. }
  347. // without modifier procession
  348. if (is_string($value)) {
  349. return $this->driver->escape($value, dibi::TEXT);
  350. } elseif (is_int($value)) {
  351. return (string) $value;
  352. } elseif (is_float($value)) {
  353. return rtrim(rtrim(number_format($value, 5, '.', ''), '0'), '.');
  354. } elseif (is_bool($value)) {
  355. return $this->driver->escape($value, dibi::BOOL);
  356. } elseif ($value === NULL) {
  357. return 'NULL';
  358. } elseif ($value instanceof DateTime) {
  359. return $this->driver->escape($value, dibi::DATETIME);
  360. } elseif ($value instanceof IDibiVariable) {
  361. return (string) $value->toSql();
  362. } else {
  363. $this->hasError = TRUE;
  364. return '**Unexpected ' . gettype($value) . '**';
  365. }
  366. }
  367. /**
  368. * PREG callback from translate() or formatValue().
  369. * @param array
  370. * @return string
  371. */
  372. private function cb($matches)
  373. {
  374. // [1] => `ident`
  375. // [2] => [ident]
  376. // [3] => '
  377. // [4] => string
  378. // [5] => "
  379. // [6] => string
  380. // [7] => lone-quote
  381. // [8] => substitution
  382. // [9] => substitution flag
  383. // [10] => modifier (when called from self::translate())
  384. // [11] => placeholder (when called from self::translate())
  385. if (!empty($matches[11])) { // placeholder
  386. $cursor = & $this->cursor;
  387. if ($cursor >= count($this->args)) {
  388. $this->hasError = TRUE;
  389. return "**Extra placeholder**";
  390. }
  391. $cursor++;
  392. return $this->formatValue($this->args[$cursor - 1], FALSE);
  393. }
  394. if (!empty($matches[10])) { // modifier
  395. $mod = $matches[10];
  396. $cursor = & $this->cursor;
  397. if ($cursor >= count($this->args) && $mod !== 'else' && $mod !== 'end') {
  398. $this->hasError = TRUE;
  399. return "**Extra modifier %$mod**";
  400. }
  401. if ($mod === 'if') {
  402. $this->ifLevel++;
  403. $cursor++;
  404. if (!$this->comment && !$this->args[$cursor - 1]) {
  405. // open comment
  406. $this->ifLevelStart = $this->ifLevel;
  407. $this->comment = TRUE;
  408. return "/*";
  409. }
  410. return '';
  411. } elseif ($mod === 'else') {
  412. if ($this->ifLevelStart === $this->ifLevel) {
  413. $this->ifLevelStart = 0;
  414. $this->comment = FALSE;
  415. return "*/";
  416. } elseif (!$this->comment) {
  417. $this->ifLevelStart = $this->ifLevel;
  418. $this->comment = TRUE;
  419. return "/*";
  420. }
  421. } elseif ($mod === 'end') {
  422. $this->ifLevel--;
  423. if ($this->ifLevelStart === $this->ifLevel + 1) {
  424. // close comment
  425. $this->ifLevelStart = 0;
  426. $this->comment = FALSE;
  427. return "*/";
  428. }
  429. return '';
  430. } elseif ($mod === 'ex') { // array expansion
  431. array_splice($this->args, $cursor, 1, $this->args[$cursor]);
  432. return '';
  433. } elseif ($mod === 'lmt') { // apply limit
  434. if ($this->args[$cursor] !== NULL) $this->limit = (int) $this->args[$cursor];
  435. $cursor++;
  436. return '';
  437. } elseif ($mod === 'ofs') { // apply offset
  438. if ($this->args[$cursor] !== NULL) $this->offset = (int) $this->args[$cursor];
  439. $cursor++;
  440. return '';
  441. } else { // default processing
  442. $cursor++;
  443. return $this->formatValue($this->args[$cursor - 1], $mod);
  444. }
  445. }
  446. if ($this->comment) return '...';
  447. if ($matches[1]) // SQL identifiers: `ident`
  448. return $this->identifiers->{$matches[1]};
  449. if ($matches[2]) // SQL identifiers: [ident]
  450. return $this->identifiers->{$matches[2]};
  451. if ($matches[3]) // SQL strings: '...'
  452. return $this->driver->escape( str_replace("''", "'", $matches[4]), dibi::TEXT);
  453. if ($matches[5]) // SQL strings: "..."
  454. return $this->driver->escape( str_replace('""', '"', $matches[6]), dibi::TEXT);
  455. if ($matches[7]) { // string quote
  456. $this->hasError = TRUE;
  457. return '**Alone quote**';
  458. }
  459. if ($matches[8]) { // SQL identifier substitution
  460. $m = substr($matches[8], 0, -1);
  461. $m = $this->connection->getSubstitutes()->$m;
  462. return $matches[9] == '' ? $this->formatValue($m, FALSE) : $m . $matches[9]; // value or identifier
  463. }
  464. die('this should be never executed');
  465. }
  466. /**
  467. * Apply substitutions to indentifier and delimites it.
  468. * @param string indentifier
  469. * @return string
  470. * @internal
  471. */
  472. public function delimite($value)
  473. {
  474. $value = $this->connection->substitute($value);
  475. $parts = explode('.', $value);
  476. foreach ($parts as & $v) {
  477. if ($v !== '*') $v = $this->driver->escape($v, dibi::IDENTIFIER);
  478. }
  479. return implode('.', $parts);
  480. }
  481. }