PageRenderTime 56ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/base/lib/flourishlib/fSQLTranslation.php

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