PageRenderTime 52ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/classphp/flourish/fSQLTranslation.php

https://github.com/jsuarez/Lexer
PHP | 1258 lines | 732 code | 201 blank | 325 comment | 141 complexity | e91542351d8467afc58e54131ba1321a MD5 | raw file
  1. <?php
  2. /**
  3. * Takes a subset of SQL from MySQL, PostgreSQL, Oracle, SQLite and MSSQL and translates into the various dialects allowing for cross-database code
  4. *
  5. * @copyright Copyright (c) 2007-2009 Will Bond
  6. * @author Will Bond [wb] <will@flourishlib.com>
  7. * @license http://flourishlib.com/license
  8. *
  9. * @package Flourish
  10. * @link http://flourishlib.com/fSQLTranslation
  11. *
  12. * @version 1.0.0b11
  13. * @changes 1.0.0b11 Fixed a bug with translating MSSQL national columns over an ODBC connection [wb, 2009-09-18]
  14. * @changes 1.0.0b10 Changed last bug fix to support PHP 5.1.6 [wb, 2009-09-18]
  15. * @changes 1.0.0b9 Fixed another bug with parsing table aliases for MSSQL national columns [wb, 2009-09-18]
  16. * @changes 1.0.0b8 Fixed a bug with parsing table aliases that occurs when handling MSSQL national columns [wb, 2009-09-09]
  17. * @changes 1.0.0b7 Fixed a bug with translating `NOT LIKE` operators in PostgreSQL [wb, 2009-07-15]
  18. * @changes 1.0.0b6 Changed replacement values in preg_replace() calls to be properly escaped [wb, 2009-06-11]
  19. * @changes 1.0.0b5 Update code to only translate data types inside of `CREATE TABLE` queries [wb, 2009-05-22]
  20. * @changes 1.0.0b4 Added the missing ::__get() method for callback support [wb, 2009-05-06]
  21. * @changes 1.0.0b3 Added Oracle and caching support, various bug fixes [wb, 2009-05-04]
  22. * @changes 1.0.0b2 Fixed a notice with SQLite foreign key constraints having no `ON` clauses [wb, 2009-02-21]
  23. * @changes 1.0.0b The initial implementation [wb, 2007-09-25]
  24. */
  25. class fSQLTranslation
  26. {
  27. // The following constants allow for nice looking callbacks to static methods
  28. const sqliteCotangent = 'fSQLTranslation::sqliteCotangent';
  29. const sqliteLogBaseFirst = 'fSQLTranslation::sqliteLogBaseFirst';
  30. const sqliteSign = 'fSQLTranslation::sqliteSign';
  31. /**
  32. * Composes text using fText if loaded
  33. *
  34. * @param string $message The message to compose
  35. * @param mixed $component A string or number to insert into the message
  36. * @param mixed ...
  37. * @return string The composed and possible translated message
  38. */
  39. static protected function compose($message)
  40. {
  41. $args = array_slice(func_get_args(), 1);
  42. if (class_exists('fText', FALSE)) {
  43. return call_user_func_array(
  44. array('fText', 'compose'),
  45. array($message, $args)
  46. );
  47. } else {
  48. return vsprintf($message, $args);
  49. }
  50. }
  51. /**
  52. * Takes the `FROM` clause from ::parseSelectSQL() and returns all of the tables and each one's alias
  53. *
  54. * @param string $clause The SQL `FROM` clause to parse
  55. * @return array The tables in the `FROM` clause, in the format `{table_alias} => {table_name}`
  56. */
  57. static private function parseTableAliases($sql)
  58. {
  59. $aliases = array();
  60. // Turn comma joins into cross joins
  61. if (preg_match('#^(?:\w+(?:\s+(?:as\s+)?(?:\w+))?)(?:\s*,\s*(?:\w+(?:\s+(?:as\s+)?(?:\w+))?))*$#isD', $sql)) {
  62. $sql = str_replace(',', ' CROSS JOIN ', $sql);
  63. }
  64. $tables = preg_split('#\s+((?:(?:CROSS|INNER|OUTER|LEFT|RIGHT)?\s+)*?JOIN)\s+#i', $sql);
  65. foreach ($tables as $table) {
  66. // This grabs the table name and alias (if there is one)
  67. preg_match('#^\s*([\w.]+|\(((?:[^()]+|\((?2)\))*)\))(?:\s+(?:as\s+)?((?!ON|USING)[\w.]+))?\s*(?:(?:ON|USING)\s+(.*))?\s*$#im', $table, $parts);
  68. $table_name = $parts[1];
  69. $table_alias = (!empty($parts[3])) ? $parts[3] : $parts[1];
  70. $aliases[$table_alias] = $table_name;
  71. }
  72. return $aliases;
  73. }
  74. /**
  75. * Callback for custom SQLite function; calculates the cotangent of a number
  76. *
  77. * @internal
  78. *
  79. * @param numeric $x The number to calculate the cotangent of
  80. * @return numeric The contangent of `$x`
  81. */
  82. static public function sqliteCotangent($x)
  83. {
  84. return 1/tan($x);
  85. }
  86. /**
  87. * Callback for custom SQLite function; returns the current date
  88. *
  89. * @internal
  90. *
  91. * @return string The current date
  92. */
  93. static public function sqliteDate()
  94. {
  95. return date('Y-m-d');
  96. }
  97. /**
  98. * Callback for custom SQLite function; calculates the log to a specific base of a number
  99. *
  100. * @internal
  101. *
  102. * @param integer $base The base for the log calculation
  103. * @param numeric $num The number to calculate the logarithm of
  104. * @return numeric The logarithm of `$num` to `$base`
  105. */
  106. static public function sqliteLogBaseFirst($base, $num)
  107. {
  108. return log($num, $base);
  109. }
  110. /**
  111. * Callback for custom SQLite function; returns the sign of the number
  112. *
  113. * @internal
  114. *
  115. * @param numeric $x The number to change the sign of
  116. * @return numeric `-1` if a negative sign, `0` if zero, `1` if positive sign
  117. */
  118. static public function sqliteSign($x)
  119. {
  120. if ($x == 0) {
  121. return 0;
  122. }
  123. if ($x > 0) {
  124. return 1;
  125. }
  126. return -1;
  127. }
  128. /**
  129. * Callback for custom SQLite function; returns the current time
  130. *
  131. * @internal
  132. *
  133. * @return string The current time
  134. */
  135. static public function sqliteTime()
  136. {
  137. return date('H:i:s');
  138. }
  139. /**
  140. * Callback for custom SQLite function; returns the current timestamp
  141. *
  142. * @internal
  143. *
  144. * @return string The current date
  145. */
  146. static public function sqliteTimestamp()
  147. {
  148. return date('Y-m-d H:i:s');
  149. }
  150. /**
  151. * The fCache object to cache schema info and, optionally, translated queries to
  152. *
  153. * @var fCache
  154. */
  155. private $cache;
  156. /**
  157. * The fDatabase instance
  158. *
  159. * @var fDatabase
  160. */
  161. private $database;
  162. /**
  163. * If debugging is enabled
  164. *
  165. * @var boolean
  166. */
  167. private $debug;
  168. /**
  169. * Database-specific schema information needed for translation
  170. *
  171. * @var array
  172. */
  173. private $schema_info;
  174. /**
  175. * Sets up the class and creates functions for SQLite databases
  176. *
  177. * @param fDatabase $database The database being translated for
  178. * @param mixed $connection The connection resource or PDO object
  179. * @return fSQLTranslation
  180. */
  181. public function __construct($database)
  182. {
  183. $this->database = $database;
  184. $this->database->inject($this);
  185. if ($database->getType() == 'sqlite') {
  186. $this->createSQLiteFunctions();
  187. }
  188. $this->schema_info = array();
  189. }
  190. /**
  191. * All requests that hit this method should be requests for callbacks
  192. *
  193. * @internal
  194. *
  195. * @param string $method The method to create a callback for
  196. * @return callback The callback for the method requested
  197. */
  198. public function __get($method)
  199. {
  200. return array($this, $method);
  201. }
  202. /**
  203. * Clears all of the schema info out of the object and, if set, the fCache object
  204. *
  205. * @return void
  206. */
  207. public function clearCache()
  208. {
  209. $this->schema_info = array();
  210. if ($this->cache) {
  211. $prefix = $this->makeCachePrefix();
  212. $this->cache->delete($prefix . 'schema_info');
  213. }
  214. }
  215. /**
  216. * Creates a trigger for SQLite that handles an on delete clause
  217. *
  218. * @param array &$extra_statements An array of extra SQL statements to be added to the SQL
  219. * @param string $referencing_table The table that contains the foreign key
  220. * @param string $referencing_column The column the foriegn key constraint is on
  221. * @param string $referenced_table The table the foreign key references
  222. * @param string $referenced_column The column the foreign key references
  223. * @param string $delete_clause What is to be done on a delete
  224. * @return string The trigger
  225. */
  226. private function createSQLiteForeignKeyTriggerOnDelete(&$extra_statements, $referencing_table, $referencing_column, $referenced_table, $referenced_column, $delete_clause)
  227. {
  228. switch (strtolower($delete_clause)) {
  229. case 'no action':
  230. case 'restrict':
  231. $extra_statements[] = "CREATE TRIGGER fkd_res_" . $referencing_table . "_" . $referencing_column . "
  232. BEFORE DELETE ON " . $referenced_table . "
  233. FOR EACH ROW BEGIN
  234. SELECT RAISE(ROLLBACK, 'delete on table \"" . $referenced_table . "\" can not be executed because it would violate the foreign key constraint on column \"" . $referencing_column . "\" of table \"" . $referencing_table . "\"')
  235. WHERE (SELECT " . $referencing_column . " FROM " . $referencing_table . " WHERE " . $referencing_column . " = OLD." . $referenced_table . ") IS NOT NULL;
  236. END";
  237. break;
  238. case 'set null':
  239. $extra_statements[] = "CREATE TRIGGER fkd_nul_" . $referencing_table . "_" . $referencing_column . "
  240. BEFORE DELETE ON " . $referenced_table . "
  241. FOR EACH ROW BEGIN
  242. UPDATE " . $referencing_table . " SET " . $referencing_column . " = NULL WHERE " . $referencing_column . " = OLD." . $referenced_column . ";
  243. END";
  244. break;
  245. case 'cascade':
  246. $extra_statements[] = "CREATE TRIGGER fkd_cas_" . $referencing_table . "_" . $referencing_column . "
  247. BEFORE DELETE ON " . $referenced_table . "
  248. FOR EACH ROW BEGIN
  249. DELETE FROM " . $referencing_table . " WHERE " . $referencing_column . " = OLD." . $referenced_column . ";
  250. END";
  251. break;
  252. }
  253. }
  254. /**
  255. * Creates a trigger for SQLite that handles an on update clause
  256. *
  257. * @param array &$extra_statements An array of extra SQL statements to be added to the SQL
  258. * @param string $referencing_table The table that contains the foreign key
  259. * @param string $referencing_column The column the foriegn key constraint is on
  260. * @param string $referenced_table The table the foreign key references
  261. * @param string $referenced_column The column the foreign key references
  262. * @param string $update_clause What is to be done on an update
  263. * @return string The trigger
  264. */
  265. private function createSQLiteForeignKeyTriggerOnUpdate(&$extra_statements, $referencing_table, $referencing_column, $referenced_table, $referenced_column, $update_clause)
  266. {
  267. switch (strtolower($update_clause)) {
  268. case 'no action':
  269. case 'restrict':
  270. $extra_statements[] = "\nCREATE TRIGGER fku_res_" . $referencing_table . "_" . $referencing_column . "
  271. BEFORE UPDATE ON " . $referenced_table . "
  272. FOR EACH ROW BEGIN
  273. SELECT RAISE(ROLLBACK, 'update on table \"" . $referenced_table . "\" can not be executed because it would violate the foreign key constraint on column \"" . $referencing_column . "\" of table \"" . $referencing_table . "\"')
  274. WHERE (SELECT " . $referencing_column . " FROM " . $referencing_table . " WHERE " . $referencing_column . " = OLD." . $referenced_column . ") IS NOT NULL;
  275. END";
  276. break;
  277. case 'set null':
  278. $extra_statements[] = "\nCREATE TRIGGER fku_nul_" . $referencing_table . "_" . $referencing_column . "
  279. BEFORE UPDATE ON " . $referenced_table . "
  280. FOR EACH ROW BEGIN
  281. UPDATE " . $referencing_table . " SET " . $referencing_column . " = NULL WHERE OLD." . $referenced_column . " <> NEW." . $referenced_column . " AND " . $referencing_column . " = OLD." . $referenced_column . ";
  282. END";
  283. break;
  284. case 'cascade':
  285. $extra_statements[] = "\nCREATE TRIGGER fku_cas_" . $referencing_table . "_" . $referencing_column . "
  286. BEFORE UPDATE ON " . $referenced_table . "
  287. FOR EACH ROW BEGIN
  288. UPDATE " . $referencing_table . " SET " . $referencing_column . " = NEW." . $referenced_column . " WHERE OLD." . $referenced_column . " <> NEW." . $referenced_column . " AND " . $referencing_column . " = OLD." . $referenced_column . ";
  289. END";
  290. break;
  291. }
  292. }
  293. /**
  294. * Creates a trigger for SQLite that prevents inserting or updating to values the violate a `FOREIGN KEY` constraint
  295. *
  296. * @param array &$extra_statements An array of extra SQL statements to be added to the SQL
  297. * @param string $referencing_table The table that contains the foreign key
  298. * @param string $referencing_column The column the foriegn key constraint is on
  299. * @param string $referenced_table The table the foreign key references
  300. * @param string $referenced_column The column the foreign key references
  301. * @param boolean $referencing_not_null If the referencing columns is set to not null
  302. * @return string The trigger
  303. */
  304. private function createSQLiteForeignKeyTriggerValidInsertUpdate(&$extra_statements, $referencing_table, $referencing_column, $referenced_table, $referenced_column, $referencing_not_null)
  305. {
  306. // Verify key on inserts
  307. $sql = "\nCREATE TRIGGER fki_ver_" . $referencing_table . "_" . $referencing_column . "
  308. BEFORE INSERT ON " . $referencing_table . "
  309. FOR EACH ROW BEGIN
  310. SELECT RAISE(ROLLBACK, 'insert on table \"" . $referencing_table . "\" violates foreign key constraint on column \"" . $referencing_column . "\"')
  311. WHERE ";
  312. if (!$referencing_not_null) {
  313. $sql .= "NEW." . $referencing_column . " IS NOT NULL AND ";
  314. }
  315. $sql .= " (SELECT " . $referenced_column . " FROM " . $referenced_table . " WHERE " . $referenced_column . " = NEW." . $referencing_column . ") IS NULL;
  316. END";
  317. $extra_statements[] = $sql;
  318. // Verify key on updates
  319. $sql = "\nCREATE TRIGGER fku_ver_" . $referencing_table . "_" . $referencing_column . "
  320. BEFORE UPDATE ON " . $referencing_table . "
  321. FOR EACH ROW BEGIN
  322. SELECT RAISE(ROLLBACK, 'update on table \"" . $referencing_table . "\" violates foreign key constraint on column \"" . $referencing_column . "\"')
  323. WHERE ";
  324. if (!$referencing_not_null) {
  325. $sql .= "NEW." . $referencing_column . " IS NOT NULL AND ";
  326. }
  327. $sql .= " (SELECT " . $referenced_column . " FROM " . $referenced_table . " WHERE " . $referenced_column . " = NEW." . $referencing_column . ") IS NULL;
  328. END";
  329. $extra_statements[] = $sql;
  330. }
  331. /**
  332. * Adds a number of math functions to SQLite that MSSQL, MySQL and PostgreSQL have by default
  333. *
  334. * @return void
  335. */
  336. private function createSQLiteFunctions()
  337. {
  338. $function = array();
  339. $functions[] = array('acos', 'acos', 1);
  340. $functions[] = array('asin', 'asin', 1);
  341. $functions[] = array('atan', 'atan', 1);
  342. $functions[] = array('atan2', 'atan2', 2);
  343. $functions[] = array('ceil', 'ceil', 1);
  344. $functions[] = array('ceiling', 'ceil', 1);
  345. $functions[] = array('cos', 'cos', 1);
  346. $functions[] = array('cot', array('fSQLTranslation', 'sqliteCotangent'), 1);
  347. $functions[] = array('degrees', 'rad2deg', 1);
  348. $functions[] = array('exp', 'exp', 1);
  349. $functions[] = array('floor', 'floor', 1);
  350. $functions[] = array('ln', 'log', 1);
  351. $functions[] = array('log', array('fSQLTranslation', 'sqliteLogBaseFirst'), 2);
  352. $functions[] = array('ltrim', 'ltrim', 1);
  353. $functions[] = array('pi', 'pi', 0);
  354. $functions[] = array('power', 'pow', 2);
  355. $functions[] = array('radians', 'deg2rad', 1);
  356. $functions[] = array('rtrim', 'rtrim', 1);
  357. $functions[] = array('sign', array('fSQLTranslation', 'sqliteSign'), 1);
  358. $functions[] = array('sqrt', 'sqrt', 1);
  359. $functions[] = array('sin', 'sin', 1);
  360. $functions[] = array('tan', 'tan', 1);
  361. $functions[] = array('trim', 'trim', 1);
  362. if ($this->database->getExtension() == 'sqlite') {
  363. $functions[] = array('current_date', array('fSQLTranslation', 'sqliteDate'), 0);
  364. $functions[] = array('current_time', array('fSQLTranslation', 'sqliteTime'), 0);
  365. $functions[] = array('current_timestamp', array('fSQLTranslation', 'sqliteTimestamp'), 0);
  366. }
  367. foreach ($functions as $function) {
  368. if ($this->database->getExtension() == 'pdo') {
  369. $this->database->getConnection()->sqliteCreateFunction($function[0], $function[1], $function[2]);
  370. } else {
  371. sqlite_create_function($this->database->getConnection(), $function[0], $function[1], $function[2]);
  372. }
  373. }
  374. }
  375. /**
  376. * Sets the schema info to be cached to the fCache object specified
  377. *
  378. * @param fCache $cache The cache to cache to
  379. * @return void
  380. */
  381. public function enableCaching($cache)
  382. {
  383. $this->cache = $cache;
  384. $this->schema_info = $this->cache->get($this->makeCachePrefix() . 'schema_info', array());
  385. }
  386. /**
  387. * Sets if debug messages should be shown
  388. *
  389. * @param boolean $flag If debugging messages should be shown
  390. * @return void
  391. */
  392. public function enableDebugging($flag)
  393. {
  394. $this->debug = (boolean) $flag;
  395. }
  396. /**
  397. * Fixes pulling unicode data out of national data type MSSQL columns
  398. *
  399. * @param string $sql The SQL to fix
  400. * @return string The fixed SQL
  401. */
  402. private function fixMSSQLNationalColumns($sql)
  403. {
  404. if (!preg_match_all('#select((?:(?:(?!\sfrom\s)[^()])+|\(((?:[^()]+|\((?2)\))*)\))*\s)from((?:(?:(?!\sunion\s|\swhere\s|\sgroup by\s|\slimit\s|\sorder by\s)[^()])+|\(((?:[^()]+|\((?4)\))*)\))*)(?=\swhere\s|\sgroup by\s|\slimit\s|\sorder by\s|\sunion\s|\)|$)#i', $sql, $matches, PREG_SET_ORDER)) {
  405. return $sql;
  406. }
  407. if (!isset($this->schema_info['national_columns'])) {
  408. $result = $this->database->query(
  409. "SELECT
  410. c.table_name AS 'table',
  411. c.column_name AS 'column',
  412. c.data_type AS 'type'
  413. FROM
  414. INFORMATION_SCHEMA.COLUMNS AS c
  415. WHERE
  416. (c.data_type = 'nvarchar' OR
  417. c.data_type = 'ntext' OR
  418. c.data_type = 'nchar') AND
  419. c.table_catalog = DB_NAME()
  420. ORDER BY
  421. lower(c.table_name) ASC,
  422. lower(c.column_name) ASC"
  423. );
  424. $national_columns = array();
  425. $national_types = array();
  426. foreach ($result as $row) {
  427. if (!isset($national_columns[$row['table']])) {
  428. $national_columns[$row['table']] = array();
  429. $national_types[$row['table']] = array();
  430. }
  431. $national_columns[$row['table']][] = $row['column'];
  432. $national_types[$row['table']][$row['column']] = $row['type'];
  433. }
  434. $this->schema_info['national_columns'] = $national_columns;
  435. $this->schema_info['national_types'] = $national_types;
  436. if ($this->cache) {
  437. $this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);
  438. }
  439. } else {
  440. $national_columns = $this->schema_info['national_columns'];
  441. $national_types = $this->schema_info['national_types'];
  442. }
  443. $additions = array();
  444. foreach ($matches as $select) {
  445. $select_clause = trim($select[1]);
  446. $from_clause = trim($select[3]);
  447. // This recursively fixes sub-selects
  448. if (preg_match('#\bselect\b#', $from_clause)) {
  449. $from_clause = $this->fixMSSQLNationalColumns($from_clause);
  450. }
  451. $table_aliases = self::parseTableAliases($from_clause);
  452. preg_match_all('#([^,()]+|\((?:(?1)|,)*\))+#i', $select_clause, $selections);
  453. $selections = array_map('trim', $selections[0]);
  454. $to_fix = array();
  455. foreach ($selections as $selection) {
  456. // We just skip CASE statements since we can't really do those reliably
  457. if (preg_match('#^case#i', $selection)) {
  458. continue;
  459. }
  460. if (preg_match('#(\w+)\.\*#i', $selection, $match)) {
  461. $table = $table_aliases[$match[1]];
  462. if (empty($national_columns[$table])) {
  463. continue;
  464. }
  465. if (!isset($to_fix[$table])) {
  466. $to_fix[$table] = array();
  467. }
  468. $to_fix[$table] = array_merge($to_fix[$table], $national_columns[$table]);
  469. } elseif (preg_match('#\*#', $selection, $match)) {
  470. foreach ($table_aliases as $alias => $table) {
  471. if (empty($national_columns[$table])) {
  472. continue;
  473. }
  474. if (!isset($to_fix[$table])) {
  475. $to_fix[$table] = array();
  476. }
  477. $to_fix[$table] = array_merge($to_fix[$table], $national_columns[$table]);
  478. }
  479. } elseif (preg_match('#^(?:(\w+)\.(\w+)|((?:min|max|trim|rtrim|ltrim|substring|replace)\((\w+)\.(\w+).*?\)))(?:\s+as\s+(\w+))?$#iD', $selection, $match)) {
  480. $table = $match[1] . ((isset($match[4])) ? $match[4] : '');
  481. $table = $table_aliases[$table];
  482. $column = $match[2] . ((isset($match[5])) ? $match[5] : '');;
  483. if (empty($national_columns[$table]) || !in_array($column, $national_columns[$table])) {
  484. continue;
  485. }
  486. if (!isset($to_fix[$table])) {
  487. $to_fix[$table] = array();
  488. }
  489. // Handle column aliasing
  490. if (!empty($match[6])) {
  491. $column = array('column' => $column, 'alias' => $match[6]);
  492. }
  493. if (!empty($match[3])) {
  494. if (!is_array($column)) {
  495. $column = array('column' => $column);
  496. }
  497. $column['expression'] = $match[3];
  498. }
  499. $to_fix[$table] = array_merge($to_fix[$table], array($column));
  500. // Match unqualified column names
  501. } elseif (preg_match('#^(?:(\w+)|((?:min|max|trim|rtrim|ltrim|substring|replace)\((\w+).*?\)))(?:\s+as\s+(\w+))?$#iD', $selection, $match)) {
  502. $column = $match[1] . ((isset($match[3])) ? $match[3] : '');
  503. foreach ($table_aliases as $alias => $table) {
  504. if (empty($national_columns[$table])) {
  505. continue;
  506. }
  507. if (!in_array($column, $national_columns[$table])) {
  508. continue;
  509. }
  510. if (!isset($to_fix[$table])) {
  511. $to_fix[$table] = array();
  512. }
  513. // Handle column aliasing
  514. if (!empty($match[4])) {
  515. $column = array('column' => $column, 'alias' => $match[4]);
  516. }
  517. if (!empty($match[2])) {
  518. if (!is_array($column)) {
  519. $column = array('column' => $column);
  520. }
  521. $column['expression'] = $match[2];
  522. }
  523. $to_fix[$table] = array_merge($to_fix[$table], array($column));
  524. }
  525. }
  526. }
  527. $reverse_table_aliases = array_flip($table_aliases);
  528. foreach ($to_fix as $table => $columns) {
  529. $columns = array_unique($columns);
  530. $alias = $reverse_table_aliases[$table];
  531. foreach ($columns as $column) {
  532. if (is_array($column)) {
  533. if (isset($column['alias'])) {
  534. $as = ' AS fmssqln__' . $column['alias'];
  535. } else {
  536. $as = ' AS fmssqln__' . $column['column'];
  537. }
  538. if (isset($column['expression'])) {
  539. $expression = $column['expression'];
  540. } else {
  541. $expression = $alias . '.' . $column['column'];
  542. }
  543. $column = $column['column'];
  544. } else {
  545. $as = ' AS fmssqln__' . $column;
  546. $expression = $alias . '.' . $column;
  547. }
  548. if ($national_types[$table][$column] == 'ntext') {
  549. $cast = 'CAST(' . $expression . ' AS IMAGE)';
  550. } else {
  551. $cast = 'CAST(' . $expression . ' AS VARBINARY(MAX))';
  552. }
  553. $additions[] = $cast . $as;
  554. }
  555. }
  556. $replace = preg_replace('#\bselect\s+' . preg_quote($select_clause, '#') . '#i', 'SELECT ' . strtr(join(', ', array_merge($selections, $additions)), array('\\' => '\\\\', '$' => '\\$')), $select);
  557. $sql = str_replace($select, $replace, $sql);
  558. }
  559. return $sql;
  560. }
  561. /**
  562. * Fixes pulling unicode data out of national data type MSSQL columns
  563. *
  564. * @param string $sql The SQL to fix
  565. * @param array &$strings The strings from the SQL
  566. * @return string The fixed SQL
  567. */
  568. private function fixOracleEmptyStrings($sql, &$strings)
  569. {
  570. if (preg_match('#^(UPDATE\s+(?:\w+\.)?\w+\s+)(SET((?:(?:(?!\bwhere\b|\breturning\b)[^()])+|\(((?:[^()]+|\((?3)\))*)\))*))(.*)$#i', $sql, $set_match)) {
  571. $sql = $set_match[1] . ':set_clause ' . $set_match[5];
  572. $set_clause = $set_match[2];
  573. } else {
  574. $set_clause = FALSE;
  575. }
  576. foreach ($strings as $number => &$string) {
  577. if ($string == "''") {
  578. $sql = preg_replace('#(\s)=(?=\s+:string_' . $number . '\b)#', '\1IS', $sql, 1, $count_equal);
  579. $sql = preg_replace('#(\s)(!=|<>)(?=\s+:string_' . $number . '\b)#', '\1IS NOT', $sql, 1, $count_not_equal);
  580. if ($count_equal || $count_not_equal) {
  581. $string = 'NULL';
  582. }
  583. }
  584. }
  585. if ($set_clause) {
  586. $sql = preg_replace('#:set_clause\b#', strtr($set_clause, array('\\' => '\\\\', '$' => '\\$')), $sql, 1);
  587. }
  588. return $sql;
  589. }
  590. /**
  591. * Creates a unique cache prefix to help prevent cache conflicts
  592. *
  593. * @return void
  594. */
  595. private function makeCachePrefix()
  596. {
  597. $prefix = 'fSQLTranslation::' . $this->database->getType() . '::';
  598. if ($this->database->getHost()) {
  599. $prefix .= $this->database->getHost() . '::';
  600. }
  601. if ($this->database->getPort()) {
  602. $prefix .= $this->database->getPort() . '::';
  603. }
  604. $prefix .= $this->database->getDatabase() . '::';
  605. if ($this->database->getUsername()) {
  606. $prefix .= $this->database->getUsername() . '::';
  607. }
  608. return $prefix;
  609. }
  610. /**
  611. * Translates Flourish SQL into the dialect for the current database
  612. *
  613. * @internal
  614. *
  615. * @param array $statements The SQL statements to translate
  616. * @param array $strings The strings to interpolate back into the SQL statements
  617. * @return array The translated SQL statements all ready for execution. Statements that have been translated will have string key of the original SQL, all other will have a numeric key.
  618. */
  619. public function translate($statements, $strings)
  620. {
  621. $output = array();
  622. foreach ($statements as $number => $sql) {
  623. // These fixes don't need to know about strings
  624. $new_sql = $this->translateBasicSyntax($sql);
  625. if ($this->database->getType() == 'mssql' || $this->database->getType() == 'oracle') {
  626. $new_sql = $this->translateLimitOffsetToRowNumber($new_sql);
  627. }
  628. // SQL Server does not like to give unicode results back to PHP without some coersion
  629. if ($this->database->getType() == 'mssql') {
  630. $new_sql = $this->fixMSSQLNationalColumns($new_sql);
  631. }
  632. // Oracle has this nasty habit of silently translating empty strings to null
  633. if ($this->database->getType() == 'oracle') {
  634. $new_sql = $this->fixOracleEmptyStrings($new_sql, $strings[$number]);
  635. }
  636. // Unescape literal semicolons in the queries
  637. $sql = preg_replace('#(?<!\\\\)\\\\;#', ';', $sql);
  638. $new_sql = preg_replace('#(?<!\\\\)\\\\;#', ';', $new_sql);
  639. $extra_statements = array();
  640. $new_sql = $this->translateCreateTableStatements($new_sql, $extra_statements);
  641. // Put the strings back into the SQL
  642. foreach ($strings[$number] as $index => $string) {
  643. $string = strtr($string, array('\\' => '\\\\', '$' => '\\$'));
  644. $sql = preg_replace('#:string_' . $index . '\b#', $string, $sql, 1);
  645. $new_sql = preg_replace('#:string_' . $index . '\b#', $string, $new_sql, 1);
  646. }
  647. $new_sql = $this->translateDateFunctions($new_sql);
  648. if ($sql != $new_sql || $extra_statements) {
  649. fCore::debug(
  650. self::compose(
  651. "Original SQL:%s",
  652. "\n" . $sql
  653. ),
  654. $this->debug
  655. );
  656. $translated_sql = $new_sql;
  657. if ($extra_statements) {
  658. $translated_sql .= '; ' . join('; ', $extra_statements);
  659. }
  660. fCore::debug(
  661. self::compose(
  662. "Translated SQL:%s",
  663. "\n" . $translated_sql
  664. ),
  665. $this->debug
  666. );
  667. }
  668. $output = array_merge($output, array($sql => $new_sql), $extra_statements);
  669. }
  670. return $output;
  671. }
  672. /**
  673. * Translates basic syntax differences of the current database
  674. *
  675. * @param string $sql The SQL to translate
  676. * @return string The translated SQL
  677. */
  678. private function translateBasicSyntax($sql)
  679. {
  680. if ($this->database->getType() == 'mssql') {
  681. $regex = array(
  682. '#\bbegin\s*(?!tran)#i' => 'BEGIN TRANSACTION ',
  683. '#\brandom\(#i' => 'RAND(',
  684. '#\batan2\(#i' => 'ATN2(',
  685. '#\bceil\(#i' => 'CEILING(',
  686. '#\bln\(#i' => 'LOG(',
  687. '#\blength\(#i' => 'LEN(',
  688. '#\bsubstr\(#i' => 'SUBSTRING(',
  689. '#\btrue\b#i' => "'1'",
  690. '#\bfalse\b#i' => "'0'",
  691. '#\|\|#i' => '+',
  692. '#\btrim\(\s*((?>[^(),]+|\((?1)\)|\(\))+)\s*\)#i' => 'RTRIM(LTRIM(\1))',
  693. '#\bround\(\s*((?>[^(),]+|\((?1)\)|\(\))+)\s*\)#i' => 'round(\1, 0)',
  694. '#\blog\(\s*((?>[^(),]+|\((?1)(?:,(?1))?\)|\(\))+)\s*,\s*((?>[^(),]+|\((?2)(?:,(?2))?\)|\(\))+)\s*\)#i' => '(LOG(\2)/LOG(\1))'
  695. );
  696. } elseif ($this->database->getType() == 'mysql') {
  697. $regex = array(
  698. '#\brandom\(#i' => 'rand(',
  699. '#\bpi\(\)#i' => '(pi()+0.0000000000000)'
  700. );
  701. } elseif ($this->database->getType() == 'oracle') {
  702. $regex = array(
  703. '#\btrue\b#i' => '1',
  704. '#\bfalse\b#i' => '0',
  705. '#\bceiling\(#i' => 'CEIL(',
  706. '#\brandom\(\)#i' => '(ABS(DBMS_RANDOM.RANDOM)/2147483647)',
  707. '#\bpi\(\)#i' => '3.14159265358979',
  708. '#\bcot\(\s*((?>[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*\)#i' => '(1/TAN(\1))',
  709. '#\bdegrees\(\s*((?>[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*\)#i' => '(\1 * 57.295779513083)',
  710. '#\bradians\(\s*((?>[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*\)#i' => '(\1 * 0.017453292519943)',
  711. '#(?:\b|^)((?>[^()%\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*%\s*((?>[^()\s]+|\(((?:[^()]+|\((?4)\))*)\))+)(?:\b|$)#i' => 'MOD(\1, \3)',
  712. '#(?:\b|^)((?>[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s+LIKE\s+((?>[^()\s]+|\(((?:[^()]+|\((?4)\))*)\))+)(?:\b|$)#i' => 'LOWER(\1) LIKE LOWER(\3)'
  713. );
  714. } elseif ($this->database->getType() == 'postgresql') {
  715. $regex = array(
  716. '#\b([\w.]+)\s+(not\s+)?like\b#i' => 'CAST(\1 AS VARCHAR) \2ILIKE',
  717. '#\blike\b#i' => 'ILIKE'
  718. );
  719. } elseif ($this->database->getType() == 'sqlite') {
  720. if ($this->database->getExtension() == 'pdo') {
  721. $regex = array(
  722. '#\bcurrent_timestamp\b#i' => "datetime(CURRENT_TIMESTAMP, 'localtime')",
  723. '#\btrue\b#i' => "'1'",
  724. '#\bfalse\b#i' => "'0'",
  725. '#\brandom\(\)#i' => '(ABS(RANDOM())/9223372036854775807)'
  726. );
  727. } else {
  728. $regex = array(
  729. '#\bcurrent_timestamp\b#i' => "CURRENT_TIMESTAMP()",
  730. '#\bcurrent_time\b#i' => "CURRENT_TIME()",
  731. '#\bcurrent_date\b#i' => "CURRENT_DATE()",
  732. '#\btrue\b#i' => "'1'",
  733. '#\bfalse\b#i' => "'0'",
  734. '#\brandom\(\)#i' => '(ABS(RANDOM())/9223372036854775807)',
  735. // SQLite 2 doesn't support CAST, but is also type-less, so we remove it
  736. '#\bcast\(\s*((?:[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s+as\s+(?:[^()\s]+|\(((?:[^()]+|\((?3)\))*)\))+\s*\)#i' => '\1'
  737. );
  738. }
  739. }
  740. return preg_replace(array_keys($regex), array_values($regex), $sql);
  741. }
  742. /**
  743. * Translates the structure of `CREATE TABLE` statements to the database specific syntax
  744. *
  745. * @param string $sql The SQL to translate
  746. * @param array &$extra_statements Any extra SQL statements that need to be added
  747. * @return string The translated SQL
  748. */
  749. private function translateCreateTableStatements($sql, &$extra_statements)
  750. {
  751. if (!preg_match('#^\s*CREATE\s+TABLE\s+(\w+)#i', $sql, $table_matches) ) {
  752. return $sql;
  753. }
  754. $table = $table_matches[1];
  755. if ($this->database->getType() == 'mssql') {
  756. // Data type translation
  757. $regex = array(
  758. '#\bblob\b#i' => 'IMAGE',
  759. '#\btimestamp\b#i' => 'DATETIME',
  760. '#\btime\b#i' => 'DATETIME',
  761. '#\bdate\b#i' => 'DATETIME',
  762. '#\binteger(?:\(\d+\))?\s+autoincrement\b#i' => 'INTEGER IDENTITY(1,1)',
  763. '#\bboolean\b#i' => 'BIT',
  764. '#\bvarchar\b#i' => 'NVARCHAR',
  765. '#\bchar\b#i' => 'NCHAR',
  766. '#\btext\b#i' => 'NTEXT'
  767. );
  768. $sql = preg_replace(array_keys($regex), array_values($regex), $sql);
  769. } elseif ($this->database->getType() == 'mysql') {
  770. // Data type translation
  771. $regex = array(
  772. '#\btext\b#i' => 'MEDIUMTEXT',
  773. '#\bblob\b#i' => 'LONGBLOB',
  774. '#\btimestamp\b#i' => 'DATETIME',
  775. '#\binteger(?:\(\d+\))?\s+autoincrement\b#i' => 'INTEGER AUTO_INCREMENT'
  776. );
  777. $sql = preg_replace(array_keys($regex), array_values($regex), $sql);
  778. // Make sure MySQL uses InnoDB tables, translate check constraints to enums and fix column-level foreign key definitions
  779. preg_match_all('#(?<=,|\()\s*(\w+)\s+(?:[a-z]+)(?:\(\d+\))?(?:\s+unsigned|\s+zerofill|\s+character\s+set\s+[^ ]+|\s+collate\s+[^ ]+|\s+NULL|\s+NOT\s+NULL|(\s+DEFAULT\s+(?:[^, \']*|\'(?:\'\'|[^\']+)*\'))|\s+UNIQUE|\s+PRIMARY\s+KEY|(\s+CHECK\s*\(\w+\s+IN\s+(\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\))\)))*(\s+REFERENCES\s+\w+\s*\(\s*\w+\s*\)\s*(?:\s+(?:ON\s+DELETE|ON\s+UPDATE)\s+(?:CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?)?\s*(,|\s*(?=\)))#mi', $sql, $matches, PREG_SET_ORDER);
  780. foreach ($matches as $match) {
  781. // MySQL has the enum data type, so we switch check constraints to that
  782. if (!empty($match[3])) {
  783. $replacement = "\n " . $match[1] . ' enum' . $match[4] . $match[2] . $match[5] . $match[6];
  784. $sql = str_replace($match[0], $replacement, $sql);
  785. // This allows us to do a str_replace below for converting foreign key syntax
  786. $match[0] = $replacement;
  787. }
  788. // Even InnoDB table types don't allow specify foreign key constraints in the column
  789. // definition, so we move it to its own definition on the next line
  790. if (!empty($match[5])) {
  791. $updated_match_0 = str_replace($match[5], ",\nFOREIGN KEY (" . $match[1] . ') ' . $match[5], $match[0]);
  792. $sql = str_replace($match[0], $updated_match_0, $sql);
  793. }
  794. }
  795. $sql = preg_replace('#\)\s*;?\s*$#D', ')ENGINE=InnoDB', $sql);
  796. } elseif ($this->database->getType() == 'oracle') {
  797. // Data type translation
  798. $regex = array(
  799. '#\bbigint\b#i' => 'INTEGER',
  800. '#\bboolean\b#i' => 'NUMBER(1)',
  801. '#\btext\b#i' => 'CLOB',
  802. '#\bvarchar\b#i' => 'VARCHAR2',
  803. '#\btime\b#i' => 'TIMESTAMP'
  804. );
  805. $sql = preg_replace(array_keys($regex), array_values($regex), $sql);
  806. // Create sequences and triggers for Oracle
  807. if (stripos($sql, 'autoincrement') !== FALSE && preg_match('#(?<=,|\()\s*(\w+)\s+(?:[a-z]+)(?:\((?:\d+)\))?.*?\bAUTOINCREMENT\b[^,\)]*(?:,|\s*(?=\)))#mi', $sql, $matches)) {
  808. $column = $matches[1];
  809. $table_column = substr($table . '_' . $column, 0, 26);
  810. $sequence_name = $table_column . '_seq';
  811. $trigger_name = $table_column . '_trg';
  812. $sequence = 'CREATE SEQUENCE ' . $sequence_name;
  813. $trigger = 'CREATE OR REPLACE TRIGGER '. $trigger_name . "\n";
  814. $trigger .= "BEFORE INSERT ON " . $table . "\n";
  815. $trigger .= "FOR EACH ROW\n";
  816. $trigger .= "BEGIN\n";
  817. $trigger .= " IF :new." . $column . " IS NULL THEN\n";
  818. $trigger .= " SELECT " . $sequence_name . ".nextval INTO :new." . $column . " FROM dual;\n";
  819. $trigger .= " END IF;\n";
  820. $trigger .= "END;";
  821. $extra_statements[] = $sequence;
  822. $extra_statements[] = $trigger;
  823. $sql = preg_replace('#\s+autoincrement\b#i', '', $sql);
  824. }
  825. } elseif ($this->database->getType() == 'postgresql') {
  826. // Data type translation
  827. $regex = array(
  828. '#\bblob\b#i' => 'BYTEA',
  829. '#\binteger(?:\(\d+\))?\s+autoincrement\b#i' => 'SERIAL'
  830. );
  831. $sql = preg_replace(array_keys($regex), array_values($regex), $sql);
  832. } elseif ($this->database->getType() == 'sqlite') {
  833. // Data type translation
  834. if ($this->database->getExtension() == 'pdo') {
  835. $sql = preg_replace('#\binteger(?:\(\d+\))?\s+autoincrement\s+primary\s+key\b#i', 'INTEGER PRIMARY KEY AUTOINCREMENT', $sql);
  836. } else {
  837. $sql = preg_replace('#\binteger(?:\(\d+\))?\s+autoincrement\s+primary\s+key\b#i', 'INTEGER PRIMARY KEY', $sql);
  838. }
  839. // Create foreign key triggers for SQLite
  840. if (stripos($sql, 'REFERENCES') !== FALSE) {
  841. preg_match_all('#(?:(?<=,|\()\s*(\w+)\s+(?:[a-z]+)(?:\((?:\d+)\))?(?:(\s+NOT\s+NULL)|(?:\s+DEFAULT\s+(?:[^, \']*|\'(?:\'\'|[^\']+)*\'))|(?:\s+UNIQUE)|(?:\s+PRIMARY\s+KEY(?:\s+AUTOINCREMENT)?)|(?:\s+CHECK\s*\(\w+\s+IN\s+\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\)\)))*(\s+REFERENCES\s+(\w+)\s*\(\s*(\w+)\s*\)\s*(?:\s+(?:ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?(?:\s+(?:ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?)?\s*(?:,|\s*(?=\)))|(?<=,|\()\s*FOREIGN\s+KEY\s*(?:(\w+)|\((\w+)\))\s+REFERENCES\s+(\w+)\s*\(\s*(\w+)\s*\)\s*(?:\s+(?:ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?(?:\s+(?:ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?\s*(?:,|\s*(?=\))))#mi', $sql, $matches, PREG_SET_ORDER);
  842. $not_null_columns = array();
  843. foreach ($matches as $match) {
  844. // Find all of the not null columns
  845. if (!empty($match[2])) {
  846. $not_null_columns[] = $match[1];
  847. }
  848. // If neither of these fields is matched, we don't have a foreign key
  849. if (empty($match[3]) && empty($match[10])) {
  850. continue;
  851. }
  852. // 8 and 9 will be an either/or set, so homogenize
  853. if (empty($match[9]) && !empty($match[8])) { $match[9] = $match[8]; }
  854. // Handle column level foreign key inserts/updates
  855. if ($match[1]) {
  856. $this->createSQLiteForeignKeyTriggerValidInsertUpdate($extra_statements, $table, $match[1], $match[4], $match[5], in_array($match[1], $not_null_columns));
  857. // Handle table level foreign key inserts/update
  858. } elseif ($match[9]) {
  859. $this->createSQLiteForeignKeyTriggerValidInsertUpdate($extra_statements, $table, $match[9], $match[10], $match[11], in_array($match[9], $not_null_columns));
  860. }
  861. // If none of these fields is matched, we don't have on delete or on update clauses
  862. if (empty($match[6]) && empty($match[7]) && empty($match[12]) && empty($match[13])) {
  863. continue;
  864. }
  865. // Handle column level foreign key delete/update clauses
  866. if (!empty($match[3])) {
  867. if ($match[6]) {
  868. $this->createSQLiteForeignKeyTriggerOnDelete($extra_statements, $table, $match[1], $match[4], $match[5], $match[6]);
  869. }
  870. if (!empty($match[7])) {
  871. $this->createSQLiteForeignKeyTriggerOnUpdate($extra_statements, $table, $match[1], $match[4], $match[5], $match[7]);
  872. }
  873. continue;
  874. }
  875. // Handle table level foreign key delete/update clauses
  876. if ($match[12]) {
  877. $this->createSQLiteForeignKeyTriggerOnDelete($extra_statements, $table, $match[9], $match[10], $match[11], $match[12]);
  878. }
  879. if ($match[13]) {
  880. $this->createSQLiteForeignKeyTriggerOnUpdate($extra_statements, $table, $match[9], $match[10], $match[11], $match[13]);
  881. }
  882. }
  883. }
  884. }
  885. return $sql;
  886. }
  887. /**
  888. * Translates custom date/time functions to the current database
  889. *
  890. * @param string $sql The SQL to translate
  891. * @return string The translated SQL
  892. */
  893. private function translateDateFunctions($sql)
  894. {
  895. // Handle diff_seconds()
  896. // diff_seconds() accepts two parameters, the two dates to get the difference between
  897. // Regex matches:
  898. // 1 - The first date
  899. // 2 - The second date
  900. preg_match_all("#diff_seconds\(((?>(?:[^()',]+|'[^']+')|\((?1)(?:,(?1))?\)|\(\))+)\s*,\s*((?>(?:[^()',]+|'[^']+')|\((?2)(?:,(?2))?\)|\(\))+)\)#ims", $sql, $diff_matches, PREG_SET_ORDER);
  901. foreach ($diff_matches as $match) {
  902. // SQLite
  903. if ($this->database->getType() == 'sqlite') {
  904. $sql = str_replace($match[0], "round((julianday(" . $match[2] . ") - julianday('1970-01-01 00:00:00')) * 86400) - round((julianday(" . $match[1] . ") - julianday('1970-01-01 00:00:00')) * 86400)", $sql);
  905. // PostgreSQL
  906. } elseif ($this->database->getType() == 'postgresql') {
  907. $sql = str_replace($match[0], "extract(EPOCH FROM age(" . $match[2] . ", " . $match[1] . "))", $sql);
  908. // Oracle
  909. } elseif ($this->database->getType() == 'oracle') {
  910. if (substr($match[1], 0, 1) == "'") {
  911. $match[1] = 'CAST(' . $match[1] . " AS TIMESTAMP)";
  912. }
  913. if (substr($match[2], 0, 1) == "'") {
  914. $match[2] = 'CAST(' . $match[2] . " AS TIMESTAMP)";
  915. }
  916. $sql = str_replace($match[0], "((TO_NUMBER(TO_CHAR(" . $match[2] . ", 'J')) - TO_NUMBER(TO_CHAR(" . $match[1] . ", 'J'))) * 86400) + (TO_NUMBER(TO_CHAR(" . $match[2] . ", 'SSSSS')) - TO_NUMBER(TO_CHAR(" . $match[1] . ", 'SSSSS')))", $sql);
  917. // MySQL
  918. } elseif ($this->database->getType() == 'mysql') {
  919. $sql = str_replace($match[0], "(UNIX_TIMESTAMP(" . $match[2] . ") - UNIX_TIMESTAMP(" . $match[1] . "))", $sql);
  920. // MSSQL
  921. } elseif ($this->database->getType() == 'mssql') {
  922. $sql = str_replace($match[0], "DATEDIFF(second, " . $match[1] . ", " . $match[2] . ")", $sql);
  923. }
  924. }
  925. // Handle add_interval()
  926. // add_interval() accepts two parameters, the date to modify and the interval to add
  927. // Regex matches:
  928. // 1 - The first parameter
  929. // 2 - The second parameter
  930. preg_match_all("#add_interval\(((?>(?:[^()',]+|'[^']+')|\((?1)(?:,(?1))?\)|\(\))+)\s*,\s*'([^']+)'\s*\)#i", $sql, $add_matches, PREG_SET_ORDER);
  931. foreach ($add_matches as $match) {
  932. // SQLite
  933. if ($this->database->getType() == 'sqlite') {
  934. // Regex matches:
  935. // 0 - The adjustment in the form: +/- number units
  936. preg_match_all("#(?:\\+|\\-)\\d+\\s+(?:year|month|day|hour|minute|second)(?:s)?#i", $match[2], $individual_matches);
  937. $strings = "'" . join("', '", $individual_matches[0]) . "'";
  938. $sql = str_replace($match[0], "datetime(" . $match[1] . ", " . $strings . ")", $sql);
  939. // PostgreSQL
  940. } elseif ($this->database->getType() == 'postgresql') {
  941. if (substr($match[1], 0, 1) == "'") {
  942. if (preg_match('#^\'\d{4}-\d{2}-\d{2}\'$#', $match[1])) {
  943. $match[1] = 'DATE ' . $match[1];
  944. } elseif (preg_match('#^\'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\'$#', $match[1])) {
  945. $match[1] = 'TIMESTAMP ' . $match[1];
  946. } elseif (preg_match('#^\'\d{2}:\d{2}:\d{2}\'$#', $match[1])) {
  947. $match[1] = 'TIME ' . $match[1];
  948. }
  949. }
  950. $sql = str_replace($match[0], "(" . $match[1] . " + INTERVAL '" . $match[2] . "')", $sql);
  951. // MySQL and Oracle
  952. } elseif ($this->database->getType() == 'mysql' || $this->database->getType() == 'oracle') {
  953. // Regex matches:
  954. // 1 - The sign, +/-
  955. // 2 - The number
  956. // 3 - The units, hour, minute, etc
  957. preg_match_all("#(\\+|\\-)(\\d+)\\s+(year|month|day|hour|minute|second)(?:s)?#i", $match[2], $individual_matches, PREG_SET_ORDER);
  958. $intervals_string = '';
  959. foreach ($individual_matches as $individual_match) {
  960. $intervals_string .= ' ' . $individual_match[1] . " INTERVAL '" . $individual_match[2] . "' " . strtoupper($individual_match[3]);
  961. }
  962. if ($this->database->getType() == 'oracle' && substr($match[1], 0, 1) == "'") {
  963. if (preg_match('#^\'\d{4}-\d{2}-\d{2}\'$#', $match[1])) {
  964. $match[1] = 'CAST(' . $match[1] . ' AS DATE)';
  965. } elseif (preg_match('#^\'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\'$#', $match[1])) {
  966. $match[1] = 'CAST(' . $match[1] . ' AS TIMESTAMP)';
  967. }
  968. }
  969. $sql = str_replace($match[0], "(" . $match[1] . $intervals_string . ")", $sql);
  970. // MSSQL
  971. } elseif ($this->database->getType() == 'mssql') {
  972. // Regex matches:
  973. // 1 - The sign, +/-
  974. // 2 - The number
  975. // 3 - The units, hour, minute, etc
  976. preg_match_all("#(\\+|\\-)(\\d+)\\s+(year|month|day|hour|minute|second)(?:s)?#i", $match[2], $individual_matches, PREG_SET_ORDER);
  977. $date_add_string = '';
  978. $stack = 0;
  979. foreach ($individual_matches as $individual_match) {
  980. $stack++;
  981. $date_add_string .= 'DATEADD(' . $individual_match[3] . ', ' . $individual_match[1] . $individual_match[2] . ', ';
  982. }
  983. $sql = str_replace($match[0], $date_add_string . $match[1] . str_pad('', $stack, ')'), $sql);
  984. }
  985. }
  986. return $sql;
  987. }
  988. /**
  989. * Translates `LIMIT x OFFSET x` to `ROW_NUMBER() OVER (ORDER BY)` syntax
  990. *
  991. * @param string $sql The SQL to translate
  992. * @return string The translated SQL
  993. */
  994. private function translateLimitOffsetToRowNumber($sql)
  995. {
  996. // Regex details:
  997. // 1 - The SELECT clause
  998. // 2 - () recursion handler
  999. // 3 - FROM clause
  1000. // 4 - () recursion handler
  1001. // 5 - ORDER BY clause
  1002. // 6 - () recursion handler
  1003. // 7 - LIMIT number
  1004. // 8 - OFFSET number
  1005. preg_match_all('#select((?:(?:(?!\sfrom\s)[^()])+|\(((?:[^()]+|\((?2)\))*)\))*\s)(from(?:(?:(?!\slimit\s|\sorder by\s)[^()])+|\(((?:[^()]+|\((?4)\))*)\))*\s)(order by(?:(?:(?!\slimit\s)[^()])+|\(((?:[^()]+|\((?6)\))*)\))*\s)?limit\s+(\d+)(?:\s+offset\s+(\d+))?#i', $sql, $matches, PREG_SET_ORDER);
  1006. foreach ($matches as $match) {
  1007. if ($this->database->getType() == 'mssql') {
  1008. // This means we have an offset clause
  1009. if (!empty($match[8])) {
  1010. if ($match[5] === '') {
  1011. $match[5] = "ORDER BY rand(1) ASC";
  1012. }
  1013. $select = $match[1] . ', ROW_NUMBER() OVER (';
  1014. $select .= $match[5];
  1015. $select .= ') AS flourish__row__num ';
  1016. $select .= $match[3];
  1017. $replacement = 'SELECT * FROM (SELECT ' . trim($match[1]) . ', ROW_NUMBER() OVER (' . $match[5] . ') AS flourish__row__num ' . $match[3] . ') AS original_query WHERE flourish__row__num > ' . $match[8] . ' AND flourish__row__num <= ' . ($match[7] + $match[8]) . ' ORDER BY flourish__row__num';
  1018. // Otherwise we just have a limit
  1019. } else {
  1020. $replacement = 'SELECT TOP ' . $match[7] . ' ' . trim($match[1] . $match[3] . $match[5]);
  1021. }
  1022. // While Oracle has the row_number() construct, the rownum pseudo-column is better
  1023. } elseif ($this->database->getType() == 'oracle') {
  1024. // This means we have an offset clause
  1025. if (!empty($match[8])) {
  1026. $replacement = 'SELECT * FROM (SELECT flourish__sq.*, rownum flourish__row__num FROM (SELECT' . $match[1] . $match[3] . $match[5] . ') flourish__sq WHERE rownum <= ' . ($match[7] + $match[8]) . ') WHERE flourish__row__num > ' . $match[8];
  1027. // Otherwise we just have a limit
  1028. } else {
  1029. $replacement = 'SELECT * FROM (SELECT' . $match[1] . $match[3] . $match[5] . ') WHERE rownum <= ' . $match[7];
  1030. }
  1031. }
  1032. $sql = str_replace($match[0], $replacement, $sql);
  1033. }
  1034. return $sql;
  1035. }
  1036. }
  1037. /**
  1038. * Copyright (c) 2007-2009 Will Bond <will@flourishlib.com>
  1039. *
  1040. * Permission is hereby granted, free of charge, to any person obtaining a copy
  1041. * of this software and associated documentation files (the "Software"), to deal
  1042. * in the Software without restriction, including without limitation the rights
  1043. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  1044. * copies of the Software, and to permit persons to whom the Software is
  1045. * furnished to do so, subject to the following conditions:
  1046. *
  1047. * The above copyright notice and this permission notice shall be included in
  1048. * all copies or substantial portions of the Software.
  1049. *
  1050. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  1051. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  1052. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  1053. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  1054. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  1055. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  1056. * THE SOFTWARE.
  1057. */