PageRenderTime 73ms CodeModel.GetById 32ms RepoModel.GetById 1ms app.codeStats 1ms

/classes/fSQLSchemaTranslation.php

https://bitbucket.org/dsqmoore/flourish
PHP | 5398 lines | 4183 code | 653 blank | 562 comment | 586 complexity | a8aad10581b20a1048ad1fa146027bbb MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * Adds cross-database `CREATE TABLE`, `ALTER TABLE` and `COMMENT ON COLUMN` statements to fSQLTranslation
  4. *
  5. * @copyright Copyright (c) 2011-2012 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/fSQLSchemaTranslation
  11. *
  12. * @version 1.0.0b3
  13. * @changes 1.0.0b3 Fixed associating a sequence with a column in PostgreSQL when setting auto-increment, fixed detection of some Oracle CHECK(IN) constraints, fixed default values for SQLite `ON DELETE` and `ON UPDATE` clauses [wb, 2012-01-12]
  14. * @changes 1.0.0b2 Fixed detection of explicitly named SQLite foreign key constraints [wb, 2011-08-23]
  15. * @changes 1.0.0b The initial implementation [wb, 2011-05-09]
  16. */
  17. class fSQLSchemaTranslation
  18. {
  19. /**
  20. * Converts a SQL identifier to lower case and removes double quotes
  21. *
  22. * @param string $identifier The SQL identifier
  23. * @return string The unescaped identifier
  24. */
  25. static private function unescapeIdentifier($identifier)
  26. {
  27. return str_replace('"', '', strtolower($identifier));
  28. }
  29. /**
  30. * Composes text using fText if loaded
  31. *
  32. * @param string $message The message to compose
  33. * @param mixed $component A string or number to insert into the message
  34. * @param mixed ...
  35. * @return string The composed and possible translated message
  36. */
  37. static protected function compose($message)
  38. {
  39. $args = array_slice(func_get_args(), 1);
  40. if (class_exists('fText', FALSE)) {
  41. return call_user_func_array(
  42. array('fText', 'compose'),
  43. array($message, $args)
  44. );
  45. } else {
  46. return vsprintf($message, $args);
  47. }
  48. }
  49. /**
  50. * Accepts a CREATE TABLE statement and parses out the column definitions
  51. *
  52. * The return value is an associative array with the keys being column
  53. * names and the values being arrays containing the following keys:
  54. * - definition: (string) the complete column definition
  55. * - pieces: (array) an associative array that can be joined back together to make the definition
  56. * - beginning
  57. * - column_name
  58. * - data_type
  59. * - not_null
  60. * - null
  61. * - default
  62. * - unique
  63. * - primary_key
  64. * - check_constraint
  65. * - foreign_key
  66. * - deferrable
  67. * - comment/end
  68. *
  69. * @param string $sql The SQL `CREATE TABLE` statement
  70. * @return array An associative array of information for each column - see method description for details
  71. */
  72. static private function parseSQLiteColumnDefinitions($sql)
  73. {
  74. preg_match_all(
  75. '#(?<=,|\(|\*/|\n)(\s*)[`"\'\[]?(\w+)[`"\'\]]?(\s+(?:[a-z]+)(?:\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\))?)(?:(\s+NOT\s+NULL)|(\s+NULL)|(\s+DEFAULT\s+([^, \'\n]*|\'(?:\'\'|[^\']+)*\'))|(\s+UNIQUE)|(\s+PRIMARY\s+KEY(?:\s+AUTOINCREMENT)?)|(\s+CHECK\s*\("?\w+"?\s+IN\s+\(\s*(?:(?:[^, \'\n]+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \'\n]+|\'(?:\'\'|[^\']+)*\')\)\)))*(\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*((?:(?!\*/).)*?)\s*\*/))?\s*(?:,[ \t]*(?:--[ \t]*([^\n]*?)[ \t]*(?=\n)|/\*\s*((?:(?!\*/).)*?)\s*\*/)?|(?:--[ \t]*([^\n]*?)[ \t]*(?=\n))?\s*(?=\))))#msi',
  76. $sql,
  77. $matches,
  78. PREG_SET_ORDER
  79. );
  80. $output = array();
  81. foreach ($matches as $match) {
  82. $comment = '';
  83. foreach (array(16, 17, 18, 19) as $key) {
  84. if (isset($match[$key])) {
  85. $comment .= $match[$key];
  86. }
  87. }
  88. $output[strtolower($match[2])] = array(
  89. 'definition' => $match[0],
  90. 'pieces' => array(
  91. 'beginning' => $match[1],
  92. 'column_name' => $match[2],
  93. 'data_type' => $match[3],
  94. 'not_null' => $match[6],
  95. 'null' => $match[7],
  96. 'default' => $match[8],
  97. 'unique' => $match[10],
  98. 'primary_key' => $match[11],
  99. 'check_constraint' => $match[12],
  100. 'foreign_key' => $match[13],
  101. 'deferrable' => $match[14],
  102. 'comment/end' => $match[15]
  103. )
  104. );
  105. }
  106. return $output;
  107. }
  108. /**
  109. * Removes a search string from a `CREATE TABLE` statement
  110. *
  111. * @param string $create_table_sql The SQL `CREATE TABLE` statement
  112. * @param string $search The string to remove
  113. * @return string The modified `CREATE TABLE` statement
  114. */
  115. static private function removeFromSQLiteCreateTable($create_table_sql, $search)
  116. {
  117. if (preg_match('#,(\s*--.*)?\s*$#D', $search)) {
  118. $regex = '#' . preg_quote($search, '#') . '#';
  119. } else {
  120. $regex = '#,(\s*/\*.*?\*/\s*|\s*--[^\n]+\n\s*)?\s*' . preg_quote($search, '#') . '\s*#';
  121. }
  122. return preg_replace($regex, "\\1\n", $create_table_sql);
  123. }
  124. /**
  125. * The fDatabase instance
  126. *
  127. * @var fDatabase
  128. */
  129. private $database;
  130. /**
  131. * Database-specific schema information needed for translation
  132. *
  133. * @var array
  134. */
  135. private $schema_info;
  136. /**
  137. * Sets up the class
  138. *
  139. * @param fDatabase $database The database being translated for
  140. * @return fSQLSchemaTranslation
  141. */
  142. public function __construct($database)
  143. {
  144. $this->database = $database;
  145. $this->schema_info = array();
  146. }
  147. /**
  148. * All requests that hit this method should be requests for callbacks
  149. *
  150. * @internal
  151. *
  152. * @param string $method The method to create a callback for
  153. * @return callback The callback for the method requested
  154. */
  155. public function __get($method)
  156. {
  157. return array($this, $method);
  158. }
  159. /**
  160. * Adds a SQLite index to the internal schema tracker
  161. *
  162. * @param string $name The index name
  163. * @param string $table The table the index applies to
  164. * @param string $sql The SQL definition of the index
  165. * @return void
  166. */
  167. private function addSQLiteIndex($name, $table, $sql)
  168. {
  169. if (!isset($this->schema_info['sqlite_indexes'])) {
  170. $this->schema_info['sqlite_indexes'] = array();
  171. }
  172. $this->schema_info['sqlite_indexes'][$name] = array(
  173. 'table' => $table,
  174. 'sql' => $sql
  175. );
  176. }
  177. /**
  178. * Stores the SQL used to create a table
  179. *
  180. * @param string $table The table to set the `CREATE TABLE` statement for
  181. * @param string $sql The SQL used to create the table
  182. * @return void
  183. */
  184. private function addSQLiteTable($table, $sql)
  185. {
  186. if (!isset($this->schema_info['sqlite_create_tables'])) {
  187. $this->getSQLiteTables();
  188. }
  189. $this->schema_info['sqlite_create_tables'][$table] = $sql;
  190. }
  191. /**
  192. * Adds a SQLite trigger to the internal schema tracker
  193. *
  194. * @param string $name The trigger name
  195. * @param string $table The table the trigger applies to
  196. * @param string $sql The SQL definition of the trigger
  197. * @return void
  198. */
  199. private function addSQLiteTrigger($name, $table, $sql)
  200. {
  201. if (!isset($this->schema_info['sqlite_triggers'])) {
  202. $this->schema_info['sqlite_triggers'] = array();
  203. }
  204. $this->schema_info['sqlite_triggers'][$name] = array(
  205. 'table' => $table,
  206. 'sql' => $sql
  207. );
  208. }
  209. /**
  210. * Creates a trigger for SQLite that handles an on delete clause
  211. *
  212. * @param array &$extra_statements An array of extra SQL statements to be added to the SQL
  213. * @param string $referencing_table The table that contains the foreign key
  214. * @param string $referencing_column The column the foreign key constraint is on
  215. * @param string $referenced_table The table the foreign key references
  216. * @param string $referenced_column The column the foreign key references
  217. * @param string $delete_clause What is to be done on a delete
  218. * @return string The trigger
  219. */
  220. private function createSQLiteForeignKeyTriggerOnDelete(&$extra_statements, $referencing_table, $referencing_column, $referenced_table, $referenced_column, $delete_clause)
  221. {
  222. switch (strtolower($delete_clause)) {
  223. case 'no action':
  224. case 'restrict':
  225. $name = 'fkd_res_' . $referencing_table . '_' . $referencing_column;
  226. $extra_statements[] = 'CREATE TRIGGER ' . $name . '
  227. BEFORE DELETE ON "' . $referenced_table . '"
  228. FOR EACH ROW BEGIN
  229. 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 . '"\')
  230. WHERE (SELECT "' . $referencing_column . '" FROM "' . $referencing_table . '" WHERE "' . $referencing_column . '" = OLD."' . $referenced_table . '") IS NOT NULL;
  231. END';
  232. $this->addSQLiteTrigger($name, $referenced_table, end($extra_statements));
  233. break;
  234. case 'set null':
  235. $name = 'fkd_nul_' . $referencing_table . '_' . $referencing_column;
  236. $extra_statements[] = 'CREATE TRIGGER ' . $name . '
  237. BEFORE DELETE ON "' . $referenced_table . '"
  238. FOR EACH ROW BEGIN
  239. UPDATE "' . $referencing_table . '" SET "' . $referencing_column . '" = NULL WHERE "' . $referencing_column . '" = OLD."' . $referenced_column . '";
  240. END';
  241. $this->addSQLiteTrigger($name, $referenced_table, end($extra_statements));
  242. break;
  243. case 'cascade':
  244. $name = 'fkd_cas_' . $referencing_table . '_' . $referencing_column;
  245. $extra_statements[] = 'CREATE TRIGGER ' . $name . '
  246. BEFORE DELETE ON "' . $referenced_table . '"
  247. FOR EACH ROW BEGIN
  248. DELETE FROM "' . $referencing_table . '" WHERE "' . $referencing_column . '" = OLD."' . $referenced_column . '";
  249. END';
  250. $this->addSQLiteTrigger($name, $referenced_table, end($extra_statements));
  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 foreign 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. $name = 'fku_res_' . $referencing_table . '_' . $referencing_column;
  271. $extra_statements[] = 'CREATE TRIGGER ' . $name . '
  272. BEFORE UPDATE ON "' . $referenced_table . '"
  273. FOR EACH ROW BEGIN
  274. 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 . '"\')
  275. WHERE (SELECT "' . $referencing_column . '" FROM "' . $referencing_table . '" WHERE "' . $referencing_column . '" = OLD."' . $referenced_column . '") IS NOT NULL;
  276. END';
  277. $this->addSQLiteTrigger($name, $referenced_table, end($extra_statements));
  278. break;
  279. case 'set null':
  280. $name = 'fku_nul_' . $referencing_table . '_' . $referencing_column;
  281. $extra_statements[] = 'CREATE TRIGGER ' . $name . '
  282. BEFORE UPDATE ON "' . $referenced_table . '"
  283. FOR EACH ROW BEGIN
  284. UPDATE "' . $referencing_table . '" SET "' . $referencing_column . '" = NULL WHERE OLD."' . $referenced_column . '" <> NEW."' . $referenced_column . '" AND "' . $referencing_column . '" = OLD."' . $referenced_column . '";
  285. END';
  286. $this->addSQLiteTrigger($name, $referenced_table, end($extra_statements));
  287. break;
  288. case 'cascade':
  289. $name = 'fku_cas_' . $referencing_table . '_' . $referencing_column;
  290. $extra_statements[] = 'CREATE TRIGGER ' . $name . '
  291. BEFORE UPDATE ON "' . $referenced_table . '"
  292. FOR EACH ROW BEGIN
  293. UPDATE "' . $referencing_table . '" SET "' . $referencing_column . '" = NEW."' . $referenced_column . '" WHERE OLD."' . $referenced_column . '" <> NEW."' . $referenced_column . '" AND "' . $referencing_column . '" = OLD."' . $referenced_column . '";
  294. END';
  295. $this->addSQLiteTrigger($name, $referenced_table, end($extra_statements));
  296. break;
  297. }
  298. }
  299. /**
  300. * Creates a trigger for SQLite that prevents inserting or updating to values the violate a `FOREIGN KEY` constraint
  301. *
  302. * @param array &$extra_statements An array of extra SQL statements to be added to the SQL
  303. * @param string $referencing_table The table that contains the foreign key
  304. * @param string $referencing_column The column the foriegn key constraint is on
  305. * @param string $referenced_table The table the foreign key references
  306. * @param string $referenced_column The column the foreign key references
  307. * @param boolean $referencing_not_null If the referencing columns is set to not null
  308. * @return string The trigger
  309. */
  310. private function createSQLiteForeignKeyTriggerValidInsertUpdate(&$extra_statements, $referencing_table, $referencing_column, $referenced_table, $referenced_column, $referencing_not_null)
  311. {
  312. // Verify key on inserts
  313. $name = 'fki_ver_' . $referencing_table . '_' . $referencing_column;
  314. $sql = 'CREATE TRIGGER ' . $name . '
  315. BEFORE INSERT ON "' . $referencing_table . '"
  316. FOR EACH ROW BEGIN
  317. SELECT RAISE(ROLLBACK, \'insert on table "' . $referencing_table . '" violates foreign key constraint on column "' . $referencing_column . '"\')
  318. WHERE ';
  319. if (!$referencing_not_null) {
  320. $sql .= 'NEW."' . $referencing_column . '" IS NOT NULL AND ';
  321. }
  322. $sql .= ' (SELECT "' . $referenced_column . '" FROM "' . $referenced_table . '" WHERE "' . $referenced_column . '" = NEW."' . $referencing_column . '") IS NULL;
  323. END';
  324. $extra_statements[] = $sql;
  325. $this->addSQLiteTrigger($name, $referencing_table, end($extra_statements));
  326. // Verify key on updates
  327. $name = 'fku_ver_' . $referencing_table . '_' . $referencing_column;
  328. $sql = 'CREATE TRIGGER ' . $name . '
  329. BEFORE UPDATE ON "' . $referencing_table . '"
  330. FOR EACH ROW BEGIN
  331. SELECT RAISE(ROLLBACK, \'update on table "' . $referencing_table . '" violates foreign key constraint on column "' . $referencing_column . '"\')
  332. WHERE ';
  333. if (!$referencing_not_null) {
  334. $sql .= 'NEW."' . $referencing_column . '" IS NOT NULL AND ';
  335. }
  336. $sql .= ' (SELECT "' . $referenced_column . '" FROM "' . $referenced_table . '" WHERE "' . $referenced_column . '" = NEW."' . $referencing_column . '") IS NULL;
  337. END';
  338. $extra_statements[] = $sql;
  339. $this->addSQLiteTrigger($name, $referencing_table, end($extra_statements));
  340. }
  341. /**
  342. * Generates a 30 character constraint name for use with `ALTER TABLE` statements
  343. *
  344. * @param string $sql The `ALTER TABLE` statement
  345. * @param string $type A 2-character string representing the type of constraint
  346. */
  347. private function generateConstraintName($sql, $type)
  348. {
  349. $constraint = '_' . $type;
  350. $constraint = '_' . substr(time(), -8) . $constraint;
  351. return substr(md5(strtolower($sql)), 0, 30 - strlen($constraint)) . $constraint;
  352. }
  353. /**
  354. * Returns the check constraint for a table and column
  355. *
  356. * @param string $schema The schema the table is in
  357. * @param string $table The table the column is in
  358. * @param string $column The column to get the check constraint for
  359. * @return array|NULL An associative array with the keys: `name` and `definition` or `NULL`
  360. */
  361. private function getDB2CheckConstraint($schema, $table, $column)
  362. {
  363. $constraint = $this->database->query(
  364. "SELECT
  365. CH.TEXT,
  366. CH.CONSTNAME
  367. FROM
  368. SYSCAT.COLUMNS AS C INNER JOIN
  369. SYSCAT.COLCHECKS AS CC ON
  370. C.TABSCHEMA = CC.TABSCHEMA AND
  371. C.TABNAME = CC.TABNAME AND
  372. C.COLNAME = CC.COLNAME AND
  373. CC.USAGE = 'R' INNER JOIN
  374. SYSCAT.CHECKS AS CH ON
  375. C.TABSCHEMA = CH.TABSCHEMA AND
  376. C.TABNAME = CH.TABNAME AND
  377. CH.TYPE = 'C' AND
  378. CH.CONSTNAME = CC.CONSTNAME
  379. WHERE
  380. LOWER(C.TABSCHEMA) = %s AND
  381. LOWER(C.TABNAME) = %s AND
  382. LOWER(C.COLNAME) = %s",
  383. $schema,
  384. $table,
  385. $column
  386. );
  387. if (!$constraint->countReturnedRows()) {
  388. return NULL;
  389. }
  390. $row = $constraint->fetchRow();
  391. return array(
  392. 'name' => $row['constname'],
  393. 'definition' => $row['text']
  394. );
  395. }
  396. /**
  397. * Returns the foreign key constraints that involve a specific table or table and column
  398. *
  399. * @param string $schema The schema the table is in
  400. * @param string $table The table the column is in
  401. * @param string $column The column to get the foreign keys for and the foreign keys that point to
  402. * @return array An associative array of the key being the constraint name and the value being an associative array containing the keys: `schema`, `table`, `column`, `foreign_schema`, `foreign_table`, `foreign_column`, `on_delete` and `on_cascade`
  403. */
  404. private function getDB2ForeignKeyConstraints($schema, $table, $column=NULL)
  405. {
  406. if ($column) {
  407. $where_conditions = "((
  408. LOWER(R.TABSCHEMA) = %s AND
  409. LOWER(R.TABNAME) = %s AND
  410. LOWER(K.COLNAME) = %s
  411. ) OR (
  412. LOWER(R.REFTABSCHEMA) = %s AND
  413. LOWER(R.REFTABNAME) = %s AND
  414. LOWER(FK.COLNAME) = %s
  415. ))";
  416. $params = array(
  417. strtolower($schema),
  418. strtolower($table),
  419. strtolower($column),
  420. strtolower($schema),
  421. strtolower($table),
  422. strtolower($column)
  423. );
  424. } else {
  425. $where_conditions = "LOWER(R.REFTABSCHEMA) = %s AND LOWER(R.REFTABNAME) = %s";
  426. $params = array(
  427. strtolower($schema),
  428. strtolower($table)
  429. );
  430. }
  431. array_unshift(
  432. $params,
  433. "SELECT
  434. R.CONSTNAME AS CONSTRAINT_NAME,
  435. TRIM(LOWER(R.TABSCHEMA)) AS \"SCHEMA\",
  436. LOWER(R.TABNAME) AS \"TABLE\",
  437. LOWER(K.COLNAME) AS \"COLUMN\",
  438. TRIM(LOWER(R.REFTABSCHEMA)) AS FOREIGN_SCHEMA,
  439. LOWER(R.REFTABNAME) AS FOREIGN_TABLE,
  440. LOWER(FK.COLNAME) AS FOREIGN_COLUMN,
  441. CASE R.DELETERULE WHEN 'C' THEN 'CASCADE' WHEN 'A' THEN 'NO ACTION' WHEN 'R' THEN 'RESTRICT' ELSE 'SET NULL' END AS ON_DELETE,
  442. CASE R.UPDATERULE WHEN 'A' THEN 'NO ACTION' WHEN 'R' THEN 'RESTRICT' END AS ON_UPDATE
  443. FROM
  444. SYSCAT.REFERENCES AS R INNER JOIN
  445. SYSCAT.KEYCOLUSE AS K ON
  446. R.CONSTNAME = K.CONSTNAME AND
  447. R.TABSCHEMA = K.TABSCHEMA AND
  448. R.TABNAME = K.TABNAME INNER JOIN
  449. SYSCAT.KEYCOLUSE AS FK ON
  450. R.REFKEYNAME = FK.CONSTNAME AND
  451. R.REFTABSCHEMA = FK.TABSCHEMA AND
  452. R.REFTABNAME = FK.TABNAME
  453. WHERE
  454. $where_conditions
  455. ORDER BY
  456. LOWER(R.CONSTNAME) ASC"
  457. );
  458. $constraints = call_user_func_array($this->database->query, $params);
  459. $keys = array();
  460. foreach ($constraints as $constraint) {
  461. $name = $constraint['constraint_name'] . $constraint['table'];
  462. $keys[$name] = $constraint;
  463. }
  464. return $keys;
  465. }
  466. /**
  467. * Returns the primary key for a table
  468. *
  469. * @param string $schema The schema the table is in
  470. * @param string $table The table to get the primary key for
  471. * @return array The columns in the primary key
  472. */
  473. private function getDB2PrimaryKeyConstraint($schema, $table)
  474. {
  475. $constraints = $this->database->query(
  476. "SELECT
  477. LOWER(C.COLNAME) AS \"COLUMN\"
  478. FROM
  479. SYSCAT.INDEXES AS I INNER JOIN
  480. SYSCAT.INDEXCOLUSE AS C ON
  481. I.INDSCHEMA = C.INDSCHEMA AND
  482. I.INDNAME = C.INDNAME
  483. WHERE
  484. I.UNIQUERULE IN ('P') AND
  485. LOWER(I.TABSCHEMA) = %s AND
  486. LOWER(I.TABNAME) = %s
  487. ORDER BY
  488. LOWER(I.INDNAME) ASC
  489. ",
  490. strtolower($schema),
  491. strtolower($table)
  492. );
  493. $key = array();
  494. foreach ($constraints as $constraint) {
  495. $key[] = $constraint['column'];
  496. }
  497. return $key;
  498. }
  499. /**
  500. * Returns the unique keys for a table and column
  501. *
  502. * @param string $schema The schema the table is in
  503. * @param string $table The table to get the unique keys for
  504. * @param string $column The column to filter the unique keys by
  505. * @return array An associative array of the key being the constraint name and the value being the columns in the unique key
  506. */
  507. private function getDB2UniqueConstraints($schema, $table, $column)
  508. {
  509. $constraints = $this->database->query(
  510. "SELECT
  511. CD.CONSTNAME AS CONSTRAINT_NAME,
  512. LOWER(C.COLNAME) AS \"COLUMN\"
  513. FROM
  514. SYSCAT.INDEXES AS I INNER JOIN
  515. SYSCAT.CONSTDEP AS CD ON
  516. I.TABSCHEMA = CD.TABSCHEMA AND
  517. I.TABNAME = CD.TABNAME AND
  518. CD.BTYPE = 'I' AND
  519. CD.BNAME = I.INDNAME INNER JOIN
  520. SYSCAT.INDEXCOLUSE AS C ON
  521. I.INDSCHEMA = C.INDSCHEMA AND
  522. I.INDNAME = C.INDNAME
  523. WHERE
  524. I.UNIQUERULE IN ('U') AND
  525. LOWER(I.TABSCHEMA) = %s AND
  526. LOWER(I.TABNAME) = %s
  527. ORDER BY
  528. LOWER(I.INDNAME) ASC
  529. ",
  530. strtolower($schema),
  531. strtolower($table)
  532. );
  533. $keys = array();
  534. foreach ($constraints as $constraint) {
  535. if (!isset($keys[$constraint['constraint_name']])) {
  536. $keys[$constraint['constraint_name']] = array();
  537. }
  538. $keys[$constraint['constraint_name']][] = $constraint['column'];
  539. }
  540. $new_keys = array();
  541. $column = strtolower($column);
  542. foreach ($keys as $name => $columns) {
  543. if (!in_array($column, $columns)) {
  544. continue;
  545. }
  546. $new_keys[$name] = $columns;
  547. }
  548. $keys = $new_keys;
  549. return $keys;
  550. }
  551. /**
  552. * Returns the check constraint for a column, if it exists
  553. *
  554. * @param string $schema The schema the column is inside of
  555. * @param string $table The table the column is part of
  556. * @param string $column The column name
  557. * @return array|NULL An associative array with the keys `name` and `definition`, or `NULL`
  558. */
  559. private function getMSSQLCheckConstraint($schema, $table, $column)
  560. {
  561. $constraint = $this->database->query(
  562. "SELECT
  563. cc.check_clause AS 'constraint',
  564. ccu.constraint_name
  565. FROM
  566. INFORMATION_SCHEMA.COLUMNS AS c INNER JOIN
  567. INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu ON
  568. c.column_name = ccu.column_name AND
  569. c.table_name = ccu.table_name AND
  570. c.table_catalog = ccu.table_catalog INNER JOIN
  571. INFORMATION_SCHEMA.CHECK_CONSTRAINTS AS cc ON
  572. ccu.constraint_name = cc.constraint_name AND
  573. ccu.constraint_catalog = cc.constraint_catalog
  574. WHERE
  575. LOWER(c.table_schema) = %s AND
  576. LOWER(c.table_name) = %s AND
  577. LOWER(c.column_name) = %s AND
  578. c.table_catalog = DB_NAME()",
  579. strtolower($schema),
  580. strtolower($table),
  581. strtolower($column)
  582. );
  583. if (!$constraint->countReturnedRows()) {
  584. return NULL;
  585. }
  586. $row = $constraint->fetchRow();
  587. return array(
  588. 'name' => $row['constraint_name'],
  589. 'definition' => $row['constraint']
  590. );
  591. }
  592. /**
  593. * Returns the foreign key constraints that a column is part of
  594. *
  595. * @param string $schema The schema the column is inside of
  596. * @param string $table The table the column is part of
  597. * @param string|array $column The column name(s)
  598. * @return array An array of constraint names that reference the column(s)
  599. */
  600. private function getMSSQLForeignKeyConstraints($schema, $table, $column)
  601. {
  602. settype($column, 'array');
  603. $constraints = $this->database->query(
  604. "SELECT
  605. LOWER(tc.table_schema + '.' + tc.table_name) AS 'table',
  606. LOWER(tc.table_schema) AS 'schema',
  607. LOWER(tc.table_name) AS 'table_without_schema',
  608. LOWER(kcu.column_name) AS 'column',
  609. kcu.constraint_name AS name
  610. FROM
  611. information_schema.table_constraints AS tc INNER JOIN
  612. information_schema.key_column_usage AS kcu ON
  613. tc.constraint_name = kcu.constraint_name AND
  614. tc.constraint_catalog = kcu.constraint_catalog AND
  615. tc.constraint_schema = kcu.constraint_schema AND
  616. tc.table_name = kcu.table_name INNER JOIN
  617. information_schema.referential_constraints AS rc ON
  618. kcu.constraint_name = rc.constraint_name AND
  619. kcu.constraint_catalog = rc.constraint_catalog AND
  620. kcu.constraint_schema = rc.constraint_schema INNER JOIN
  621. information_schema.constraint_column_usage AS ccu ON
  622. ccu.constraint_name = rc.unique_constraint_name AND
  623. ccu.constraint_catalog = rc.constraint_catalog AND
  624. ccu.constraint_schema = rc.constraint_schema
  625. WHERE
  626. tc.constraint_type = 'FOREIGN KEY' AND
  627. (
  628. LOWER(tc.table_schema) = %s AND
  629. LOWER(ccu.table_name) = %s AND
  630. LOWER(ccu.column_name) IN (%s)
  631. ) OR (
  632. LOWER(tc.table_schema) = %s AND
  633. LOWER(kcu.table_name) = %s AND
  634. LOWER(kcu.column_name) IN (%s)
  635. ) AND
  636. tc.constraint_catalog = DB_NAME()",
  637. strtolower($schema),
  638. strtolower($table),
  639. array_map('strtolower', $column),
  640. strtolower($schema),
  641. strtolower($table),
  642. array_map('strtolower', $column)
  643. );
  644. return $constraints->fetchAllRows();
  645. }
  646. /**
  647. * Returns the default constraint for a column, if it exists
  648. *
  649. * @param string $schema The schema the column is inside of
  650. * @param string $table The table the column is part of
  651. * @param string $column The column name
  652. * @return array|NULL An associative array with the keys `name` and `definition`, or `NULL`
  653. */
  654. private function getMSSQLDefaultConstraint($schema, $table, $column)
  655. {
  656. $constraint = $this->database->query(
  657. "SELECT
  658. dc.name,
  659. CAST(dc.definition AS VARCHAR(MAX)) AS definition
  660. FROM
  661. information_schema.columns AS c INNER JOIN
  662. sys.default_constraints AS dc ON
  663. OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)) = dc.parent_object_id AND
  664. COLUMNPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), c.column_name, 'ColumnId') = dc.parent_column_id
  665. WHERE
  666. LOWER(c.table_schema) = %s AND
  667. LOWER(c.table_name) = %s AND
  668. LOWER(c.column_name) = %s AND
  669. c.table_catalog = DB_NAME()",
  670. strtolower($schema),
  671. strtolower($table),
  672. strtolower($column)
  673. );
  674. if (!$constraint->countReturnedRows()) {
  675. return NULL;
  676. }
  677. $row = $constraint->fetchRow();
  678. return array(
  679. 'name' => $row['name'],
  680. 'definition' => $row['definition']
  681. );
  682. }
  683. /**
  684. * Returns the primary key constraints for a table
  685. *
  686. * @param string $schema The schema the table is inside of
  687. * @param string $table The table to get the constraint for
  688. * @return array|NULL An associative array with the keys `name`, `columns` and `autoincrement` or `NULL`
  689. */
  690. private function getMSSQLPrimaryKeyConstraint($schema, $table)
  691. {
  692. $column_info = $this->database->query(
  693. "SELECT
  694. kcu.constraint_name AS constraint_name,
  695. LOWER(kcu.column_name) AS column_name,
  696. CASE
  697. WHEN
  698. COLUMNPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), c.column_name, 'IsIdentity') = 1 AND
  699. OBJECTPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), 'IsMSShipped') = 0
  700. THEN '1'
  701. ELSE '0'
  702. END AS auto_increment
  703. FROM
  704. information_schema.table_constraints AS con INNER JOIN
  705. information_schema.key_column_usage AS kcu ON
  706. con.table_name = kcu.table_name AND
  707. con.table_schema = kcu.table_schema AND
  708. con.constraint_name = kcu.constraint_name INNER JOIN
  709. information_schema.columns AS c ON
  710. c.table_name = kcu.table_name AND
  711. c.table_schema = kcu.table_schema AND
  712. c.column_name = kcu.column_name
  713. WHERE
  714. con.constraint_type = 'PRIMARY KEY' AND
  715. LOWER(con.table_schema) = %s AND
  716. LOWER(con.table_name) = %s AND
  717. con.table_catalog = DB_NAME()",
  718. strtolower($schema),
  719. strtolower($table)
  720. );
  721. if (!$column_info->countReturnedRows()) {
  722. return NULL;
  723. }
  724. $output = array(
  725. 'columns' => array()
  726. );
  727. foreach ($column_info as $row) {
  728. $output['columns'][] = $row['column_name'];
  729. $output['name'] = $row['constraint_name'];
  730. $output['autoincrement'] = (boolean) $row['auto_increment'];
  731. }
  732. return $output;
  733. }
  734. /**
  735. * Returns the unique constraints that a column is part of
  736. *
  737. * @param string $schema The schema the column is inside of
  738. * @param string $table The table the column is part of
  739. * @param string $column The column name
  740. * @return array An associative array of constraint_name => columns
  741. */
  742. private function getMSSQLUniqueConstraints($schema, $table, $column)
  743. {
  744. $constraint_columns = $this->database->query(
  745. "SELECT
  746. c.constraint_name,
  747. LOWER(kcu.column_name) AS column_name
  748. FROM
  749. information_schema.table_constraints AS c INNER JOIN
  750. information_schema.key_column_usage AS kcu ON
  751. c.table_name = kcu.table_name AND
  752. c.constraint_name = kcu.constraint_name
  753. WHERE
  754. c.constraint_name IN (
  755. SELECT
  756. c.constraint_name
  757. FROM
  758. information_schema.table_constraints AS c INNER JOIN
  759. information_schema.key_column_usage AS kcu ON
  760. c.table_name = kcu.table_name AND
  761. c.constraint_name = kcu.constraint_name
  762. WHERE
  763. c.constraint_type = 'UNIQUE' AND
  764. LOWER(c.table_schema) = %s AND
  765. LOWER(c.table_name) = %s AND
  766. LOWER(kcu.column_name) = %s AND
  767. c.table_catalog = DB_NAME()
  768. ) AND
  769. LOWER(c.table_schema) = %s AND
  770. c.table_catalog = DB_NAME()
  771. ORDER BY
  772. c.constraint_name
  773. ",
  774. strtolower($schema),
  775. strtolower($table),
  776. strtolower($column),
  777. strtolower($schema)
  778. );
  779. $unique_constraints = array();
  780. foreach ($constraint_columns as $row) {
  781. if (!isset($unique_constraints[$row['constraint_name']])) {
  782. $unique_constraints[$row['constraint_name']] = array();
  783. }
  784. $unique_constraints[$row['constraint_name']][] = $row['column_name'];
  785. }
  786. return $unique_constraints;
  787. }
  788. /**
  789. * Returns info about all foreign keys that involve the table and one of the columns specified
  790. *
  791. * @param string $table The table
  792. * @param string|array $columns The column, or an array of valid column names
  793. * @column array An array of associative arrays containing the keys `constraint_name`, `table`, `column`, `foreign_table` and `foreign_column`
  794. */
  795. private function getMySQLForeignKeys($table, $columns)
  796. {
  797. if (is_string($columns)) {
  798. $columns = array($columns);
  799. }
  800. $columns = array_map('strtolower', $columns);
  801. $tables = $this->getMySQLTables();
  802. $keys = array();
  803. foreach ($tables as $_table) {
  804. $row = $this->database->query("SHOW CREATE TABLE %r", $_table)->fetchRow();
  805. preg_match_all(
  806. '#CONSTRAINT\s+"(\w+)"\s+FOREIGN KEY \("([^"]+)"\) REFERENCES "([^"]+)" \("([^"]+)"\)(?:\sON\sDELETE\s(SET\sNULL|SET\sDEFAULT|CASCADE|NO\sACTION|RESTRICT))?(?:\sON\sUPDATE\s(SET\sNULL|SET\sDEFAULT|CASCADE|NO\sACTION|RESTRICT))?#',
  807. $row['Create Table'],
  808. $matches,
  809. PREG_SET_ORDER
  810. );
  811. foreach ($matches as $match) {
  812. $points_to_column = strtolower($match[3]) == strtolower($table) && in_array(strtolower($match[4]), $columns);
  813. $is_column = strtolower($_table) == strtolower($table) && in_array(strtolower($match[2]), $columns);
  814. if (!$points_to_column && !$is_column) {
  815. continue;
  816. }
  817. $temp = array(
  818. 'constraint_name' => $match[1],
  819. 'table' => $_table,
  820. 'column' => $match[2],
  821. 'foreign_table' => $match[3],
  822. 'foreign_column' => $match[4],
  823. 'on_delete' => 'NO ACTION',
  824. 'on_update' => 'NO ACTION'
  825. );
  826. if (!empty($match[5])) {
  827. $temp['on_delete'] = $match[5];
  828. }
  829. if (!empty($match[6])) {
  830. $temp['on_update'] = $match[6];
  831. }
  832. $keys[] = $temp;
  833. }
  834. }
  835. return $keys;
  836. }
  837. /**
  838. * Returns a list of all tables in the database
  839. *
  840. * @return array An array of table names
  841. */
  842. private function getMySQLTables()
  843. {
  844. if (!isset($this->schema_info['version'])) {
  845. $version = $this->database->query("SELECT version()")->fetchScalar();
  846. $this->schema_info['version'] = substr($version, 0, strpos($version, '.'));
  847. }
  848. if ($this->schema_info['version'] <= 4) {
  849. $sql = 'SHOW TABLES';
  850. } else {
  851. $sql = "SHOW FULL TABLES WHERE table_type = 'BASE TABLE'";
  852. }
  853. $result = $this->database->query($sql);
  854. $tables = array();
  855. foreach ($result as $row) {
  856. $keys = array_keys($row);
  857. $tables[] = $row[$keys[0]];
  858. }
  859. return $tables;
  860. }
  861. /**
  862. * Returns an an array of the column name for a table
  863. *
  864. * @param string $table The table to retrieve the column names for
  865. * @return array The column names for the table
  866. */
  867. private function getSQLiteColumns($table)
  868. {
  869. $create_sql = $this->getSQLiteCreateTable($table);
  870. return array_keys(self::parseSQLiteColumnDefinitions($create_sql));
  871. }
  872. /**
  873. * Returns the SQL used to create a table
  874. *
  875. * @param string $table The table to retrieve the `CREATE TABLE` statement for
  876. * @return string The `CREATE TABLE` SQL statement
  877. */
  878. private function getSQLiteCreateTable($table)
  879. {
  880. if (!isset($this->schema_info['sqlite_create_tables'])) {
  881. $this->getSQLiteTables();
  882. }
  883. if (!isset($this->schema_info['sqlite_create_tables'][$table])) {
  884. return NULL;
  885. }
  886. return $this->schema_info['sqlite_create_tables'][$table];
  887. }
  888. /**
  889. * Returns a list of all foreign keys that reference the table, and optionally, column specified
  890. *
  891. * @param string $table All foreign keys returned will point to this table
  892. * @param string $column Only foreign keys pointing to this column will be returned
  893. * @return array An array of arrays containing they keys: `table`, `column`, `foreign_table`, `foreign_column`, `on_delete` and `on_update`
  894. */
  895. private function getSQLiteForeignKeys($table, $column=NULL)
  896. {
  897. $output = array();
  898. foreach ($this->getSQLiteTables() as $_table) {
  899. $create_sql = $this->getSQLiteCreateTable($_table);
  900. if (stripos($create_sql, 'references') === FALSE) {
  901. continue;
  902. }
  903. preg_match_all('#(?<=,|\(|\*/|\n)\s*[`"\[\']?(\w+)[`"\]\']?\s+(?:[a-z]+)(?:\([^)]*\))?(?:(?:\s+NOT\s+NULL)|(?:\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|SET\s+DEFAULT))|(?:\s+ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT)))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?\s*(?:,|/\*|(?:--[^\n]*\n)?\s*(?=\)))#mis', $create_sql, $matches, PREG_SET_ORDER);
  904. preg_match_all('#(?<=,|\(|\*/|\n)\s*(?:CONSTRAINT\s+["`\[]?\w+["`\]]?\s+)?FOREIGN\s+KEY\s*\(?\s*["`\[]?(\w+)["`\]]?\s*\)?\s+REFERENCES\s+["`\[]?(\w+)["`\]]?\s*\(\s*["`\[]?(\w+)["`\]]?\s*\)\s*(?:(?:\s+ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT))|(?:\s+ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT)))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?\s*(?:,|/\*|(?:--[^\n]*\n)?\s*(?=\)))#mis', $create_sql, $matches2, PREG_SET_ORDER);
  905. foreach (array_merge($matches, $matches2) as $match) {
  906. $_column = $match[1];
  907. $foreign_table = $match[2];
  908. $foreign_column = $match[3];
  909. $on_delete = empty($match[4]) ? 'RESTRICT' : $match[4];
  910. $on_update = empty($match[5]) ? 'RESTRICT' : $match[5];
  911. if ($foreign_table != $table || ($column !== NULL && $column != $foreign_column)) {
  912. continue;
  913. }
  914. if (!$on_delete) {
  915. $on_delete = 'RESTRICT';
  916. }
  917. if (!$on_update) {
  918. $on_update = 'RESTRICT';
  919. }
  920. $output[] = array(
  921. 'table' => $_table,
  922. 'column' => $_column,
  923. 'foreign_table' => $foreign_table,
  924. 'foreign_column' => $foreign_column,
  925. 'on_delete' => $on_delete,
  926. 'on_update' => $on_update
  927. );
  928. }
  929. }
  930. return $output;
  931. }
  932. /**
  933. * Returns the indexes in the current SQLite database
  934. *
  935. * @return array An associative array with the key being the index name and the value an associative arrays, each containing the keys: `table`, `sql`
  936. */
  937. private function getSQLiteIndexes($table=NULL)
  938. {
  939. if (!isset($this->schema_info['sqlite_indexes'])) {
  940. $this->schema_info['sqlite_indexes'] = array();
  941. $rows = $this->database->query(
  942. "SELECT tbl_name AS \"table\", name, sql FROM sqlite_master WHERE type = 'index' AND sql <> ''"
  943. )->fetchAllRows();
  944. foreach ($rows as $row) {
  945. $this->schema_info['sqlite_indexes'][$row['name']] = array(
  946. 'table' => $row['table'],
  947. 'sql' => $row['sql']
  948. );
  949. }
  950. }
  951. $output = $this->schema_info['sqlite_indexes'];
  952. if ($table) {
  953. $new_output = array();
  954. foreach ($output as $name => $index) {
  955. if ($index['table'] != $table) {
  956. continue;
  957. }
  958. $new_output[$name] = $index;
  959. }
  960. $output = $new_output;
  961. }
  962. return $output;
  963. }
  964. /**
  965. * Returns the tables in the current SQLite database
  966. *
  967. * @return array
  968. */
  969. private function getSQLiteTables()
  970. {
  971. if (!isset($this->schema_info['sqlite_create_tables'])) {
  972. $this->schema_info['sqlite_create_tables'] = array();
  973. $res = $this->database->query(
  974. "SELECT name, sql FROM sqlite_master WHERE type = 'table'"
  975. )->fetchAllRows();
  976. foreach ($res as $row) {
  977. $this->schema_info['sqlite_create_tables'][$row['name']] = $row['sql'];
  978. }
  979. }
  980. $tables = array_keys($this->schema_info['sqlite_create_tables']);
  981. natcasesort($tables);
  982. return $tables;
  983. }
  984. /**
  985. * Returns the triggers in the current SQLite database
  986. *
  987. * @return array An associative array with the key being the trigger name and the value an associative arrays, each containing the keys: `table`, `sql`
  988. */
  989. private function getSQLiteTriggers($exclude_table=NULL)
  990. {
  991. if (!isset($this->schema_info['sqlite_triggers'])) {
  992. $this->schema_info['sqlite_triggers'] = array();
  993. $rows = $this->database->query(
  994. "SELECT tbl_name AS \"table\", name, sql FROM sqlite_master WHERE type = 'trigger'"
  995. )->fetchAllRows();
  996. foreach ($rows as $row) {
  997. $this->schema_info['sqlite_triggers'][$row['name']] = array(
  998. 'table' => $row['table'],
  999. 'sql' => $row['sql']
  1000. );
  1001. }
  1002. }
  1003. $output = $this->schema_info['sqlite_triggers'];
  1004. if ($exclude_table) {
  1005. $new_output = array();
  1006. foreach ($output as $name => $trigger) {
  1007. if ($trigger['table'] == $exclude_table) {
  1008. continue;
  1009. }
  1010. $new_output[$name] = $trigger;
  1011. }
  1012. $output = $new_output;
  1013. }
  1014. return $output;
  1015. }
  1016. /**
  1017. * Removes the SQLite indexes from the internal schema tracker
  1018. *
  1019. * @param string $table The table to remove the indexes for
  1020. * @return void
  1021. */
  1022. private function removeSQLiteIndexes($table)
  1023. {
  1024. if (!isset($this->schema_info['sqlite_indexes'])) {
  1025. return;
  1026. }
  1027. $indexes = $this->schema_info['sqlite_indexes'];
  1028. $new_indexes = array();
  1029. foreach ($indexes as $name => $index) {
  1030. if ($index['table'] == $table) {
  1031. continue;
  1032. }
  1033. $new_indexes[$name] = $index;
  1034. }
  1035. $this->schema_info['sqlite_indexes'] = $new_indexes;
  1036. }
  1037. /**
  1038. * Removes a table from the list of SQLite table
  1039. *
  1040. * @param string $table The table to remove
  1041. * @return void
  1042. */
  1043. private function removeSQLiteTable($table)
  1044. {
  1045. if (!isset($this->schema_info['sqlite_create_tables'])) {
  1046. return;
  1047. }
  1048. unset($this->schema_info['sqlite_create_tables'][$table]);
  1049. }
  1050. /**
  1051. * Removes a SQLite trigger from the internal schema tracker
  1052. *
  1053. * @param string $name The trigger name
  1054. * @return void
  1055. */
  1056. private function removeSQLiteTrigger($name)
  1057. {
  1058. if (!isset($this->schema_info['sqlite_triggers'])) {
  1059. return;
  1060. }
  1061. unset($this->schema_info['sqlite_triggers'][$name]);
  1062. }
  1063. /**
  1064. * Removes the SQLite triggers for a table from the internal schema tracker
  1065. *
  1066. * @param string $table The table to remove the triggers for
  1067. * @return void
  1068. */
  1069. private function removeSQLiteTriggers($table)
  1070. {
  1071. if (!isset($this->schema_info['sqlite_triggers'])) {
  1072. return;
  1073. }
  1074. $triggers = $this->schema_info['sqlite_triggers'];
  1075. $new_triggers = array();
  1076. foreach ($triggers as $name => $trigger) {
  1077. if ($trigger['table'] == $table) {
  1078. continue;
  1079. }
  1080. $new_triggers[$name] = $trigger;
  1081. }
  1082. $this->schema_info['sqlite_triggers'] = $new_triggers;
  1083. }
  1084. /**
  1085. * Throws an fSQLException with the information provided
  1086. *
  1087. * @param string $error The error that occured
  1088. * @param string $sql The SQL statement that caused the error
  1089. * @return void
  1090. */
  1091. private function throwException($error, $sql)
  1092. {
  1093. $db_type_map = array(
  1094. 'db2' => 'DB2',
  1095. 'mssql' => 'MSSQL',
  1096. 'mysql' => 'MySQL',
  1097. 'oracle' => 'Oracle',
  1098. 'postgresql' => 'PostgreSQL',
  1099. 'sqlite' => 'SQLite'
  1100. );
  1101. throw new fSQLException(
  1102. '%1$s error (%2$s) in %3$s',
  1103. $db_type_map[$this->database->getType()],
  1104. $error,
  1105. $sql
  1106. );
  1107. }
  1108. /**
  1109. * Translates a Flourish SQL DDL statement into the dialect for the current database
  1110. *
  1111. * @internal
  1112. *
  1113. * @param string $sql The SQL statement to translate
  1114. * @param array &$rollback_statements SQL statements to rollback the returned SQL statements if something goes wrong - only applicable for MySQL `ALTER TABLE` statements
  1115. * @return array An array containing the translated `$sql` statement and an array of extra statements
  1116. */
  1117. public function translate($sql, &$rollback_statements=NULL)
  1118. {
  1119. $reset_sqlite_info = FALSE;
  1120. if (!isset($this->schema_info['sqlite_schema_info'])) {
  1121. $this->schema_info['sqlite_schema_info'] = TRUE;
  1122. $reset_sqlite_info = TRUE;
  1123. }
  1124. $new_sql = $sql;
  1125. $exception = NULL;
  1126. try {
  1127. $extra_statements = array();
  1128. if (!is_array($rollback_statements)) {
  1129. $rollback_statements = array();
  1130. }
  1131. $new_sql = $this->translateCreateTableStatements($new_sql, $extra_statements);
  1132. $new_sql = $this->translateAlterTableStatements($new_sql, $extra_statements, $rollback_statements);
  1133. if ($this->database->getType() == 'sqlite') {
  1134. $new_sql = $this->translateSQLiteDropTableStatements($new_sql, $extra_statements);
  1135. }
  1136. } catch (Exception $e) {
  1137. $exception = $e;
  1138. }
  1139. if ($reset_sqlite_info) {
  1140. unset($this->schema_info['sqlite_schema_info']);
  1141. unset($this->schema_info['sqlite_create_tables']);
  1142. unset($this->schema_info['sqlite_indexes']);
  1143. unset($this->schema_info['sqlite_triggers']);
  1144. }
  1145. if ($exception) {
  1146. throw $exception;
  1147. }
  1148. return array($new_sql, $extra_statements);
  1149. }
  1150. /**
  1151. * Translates the structure of `CREATE TABLE` statements to the database specific syntax
  1152. *
  1153. * @param string $sql The SQL to translate
  1154. * @param array &$extra_statements Any extra SQL statements that need to be added
  1155. * @param array &$rollback_statements SQL statements to rollback `$sql` and `$extra_statements` if something goes wrong
  1156. * @return string The translated SQL
  1157. */
  1158. private function translateAlterTableStatements($sql, &$extra_statements, &$rollback_statements=NULL)
  1159. {
  1160. if (!preg_match('#^\s*ALTER\s+TABLE\s+(\w+|"[^"]+")\s+(.*)$#siD', $sql, $table_matches) && !preg_match('#^\s*COMMENT\s+ON\s+COLUMN\s+"?((?:\w+"?\."?)?\w+)"?\.("?\w+"?\s+IS\s+(?:\'.*\'|%\d+\$s))\s*$#Dis', $sql, $table_matches)) {
  1161. return $sql;
  1162. }
  1163. $statement = $table_matches[2];
  1164. $data = array(
  1165. 'table' => $table_matches[1]
  1166. );
  1167. if (preg_match('#"?(\w+)"?\s+IS\s+(\'.*\'|:string\w+|%\d+\$s)\s*$#Dis', $statement, $statement_matches)) {
  1168. $data['type'] = 'column_comment';
  1169. $data['column_name'] = trim($statement_matches[1], '"');
  1170. $data['comment'] = $statement_matches[2];
  1171. } elseif (preg_match('#RENAME\s+TO\s+(\w+|"[^"]+")\s*$#isD', $statement, $statement_matches)) {
  1172. $data['type'] = 'rename_table';
  1173. $data['new_table_name'] = trim($statement_matches[1], '"');
  1174. } elseif (preg_match('#RENAME\s+COLUMN\s+(\w+|"[^"]+")\s+TO\s+(\w+|"[^"]+")\s*$#isD', $statement, $statement_matches)) {
  1175. $data['type'] = 'rename_column';
  1176. $data['column_name'] = trim($statement_matches[1], '"');
  1177. $data['new_column_name'] = trim($statement_matches[2], '"');
  1178. } elseif (preg_match('#ADD\s+COLUMN\s+("?(\w+)"?.*)$#isD', $statement, $statement_matches)) {
  1179. $data['type'] = 'add_column';
  1180. $data['column_definition'] = $statement_matches[1];
  1181. $data['column_name'] = $statement_matches[2];
  1182. } elseif (preg_match('#DROP\s+COLUMN\s+(\w+|"[^"]+")\s*$#isD', $statement, $statement_matches)) {
  1183. $data['type'] = 'drop_column';
  1184. $data['column_name'] = trim($statement_matches[1], '"');
  1185. } elseif (preg_match('#ALTER\s+COLUMN\s+(\w+|"[^"]+")\s+TYPE\s+(.*?)\s*$#isD', $statement, $statement_matches)) {
  1186. $data['type'] = 'alter_type';
  1187. $data['column_name'] = trim($statement_matches[1], '"');
  1188. $data['data_type'] = $statement_matches[2];
  1189. } elseif (preg_match('#ALTER\s+COLUMN\s+(\w+|"[^"]+")\s+DROP\s+DEFAULT\s*$#isD', $statement, $statement_matches)) {
  1190. $data['type'] = 'drop_default';
  1191. $data['column_name'] = trim($statement_matches[1], '"');
  1192. } elseif (preg_match('#ALTER\s+COLUMN\s+(\w+|"[^"]+")\s+SET\s+DEFAULT\s+(.*?)\s*$#isD', $statement, $statement_matches)) {
  1193. $data['type'] = 'set_default';
  1194. $data['column_name'] = trim($statement_matches[1], '"');
  1195. $data['default_value'] = trim($statement_matches[2], '"');
  1196. } elseif (preg_match('#ALTER\s+COLUMN\s+(\w+|"[^"]+")\s+DROP\s+NOT\s+NULL\s*$#isD', $statement, $statement_matches)) {
  1197. $data['type'] = 'drop_not_null';
  1198. $data['column_name'] = trim($statement_matches[1], '"');
  1199. } elseif (preg_match('#ALTER\s+COLUMN\s+(\w+|"[^"]+")\s+SET\s+NOT\s+NULL(\s+DEFAULT\s+(.*))?\s*$#isD', $statement, $statement_matches)) {
  1200. $data['type'] = 'set_not_null';
  1201. $data['column_name'] = trim($statement_matches[1], '"');
  1202. if (isset($statement_matches[2])) {
  1203. $data['default'] = $statement_matches[3];
  1204. }
  1205. } elseif (preg_match('#ALTER\s+COLUMN\s+(\w+|"[^"]+")\s+DROP\s+CHECK\s*$#isD', $statement, $statement_matches)) {
  1206. $data['type'] = 'drop_check_constraint';
  1207. $data['column_name'] = trim($statement_matches[1], '"');
  1208. } elseif (preg_match('#ALTER\s+COLUMN\s+(\w+|"[^"]+")\s+SET\s+CHECK\s+IN\s+(\(.*?\))\s*$#isD', $statement, $statement_matches)) {
  1209. $data['type'] = 'set_check_constraint';
  1210. $data['column_name'] = trim($statement_matches[1], '"');
  1211. $data['constraint'] = ' CHECK(' . $statement_matches[1] . ' IN ' . $statement_matches[2] . ')';
  1212. } elseif (preg_match('#DROP\s+PRIMARY\s+KEY\s*$#isD', $statement, $statement_matches)) {
  1213. $data['type'] = 'drop_primary_key';
  1214. } elseif (preg_match('#ADD\s+PRIMARY\s+KEY\s*\(\s*([^\)]+?)\s*\)(\s+AUTOINCREMENT)?\s*$#isD', $statement, $statement_matches)) {
  1215. $data['type'] = 'add_primary_key';
  1216. $data['column_names'] = preg_split(
  1217. '#"?\s*,\s*"?#',
  1218. trim($statement_matches[1], '"'),
  1219. -1,
  1220. PREG_SPLIT_NO_EMPTY
  1221. );
  1222. $data['autoincrement'] = count($data['column_names']) == 1 && !empty($statement_matches[2]);
  1223. if (count($data['column_names']) == 1) {
  1224. $data['column_name'] = reset($data['column_names']);
  1225. }
  1226. } elseif (preg_match('#DROP\s+FOREIGN\s+KEY\s*\(\s*(\w+|"[^"]+")\s*\)\s*$#isD', $statement, $statement_matches)) {
  1227. $data['type'] = 'drop_foreign_key';
  1228. $data['column_name'] = trim($statement_matches[1], '"');
  1229. } elseif (preg_match('#ADD\s+FOREIGN\s+KEY\s*\((\w+|"[^"]+")\)\s+REFERENCES\s+("?(\w+)"?\s*\(\s*"?(\w+)"?\s*\)\s*.*)\s*$#isD', $statement, $statement_matches)) {
  1230. $data['type'] = 'add_foreign_key';
  1231. $data['column_name'] = trim($statement_matches[1], '"');
  1232. $data['references'] = $statement_matches[2];
  1233. $data['foreign_table'] = self::unescapeIdentifier($statement_matches[3]);
  1234. $data['foreign_column'] = self::unescapeIdentifier($statement_matches[4]);
  1235. } elseif (preg_match('#DROP\s+UNIQUE\s*\(\s*([^\)]+?)\s*\)\s*$#isD', $statement, $statement_matches)) {
  1236. $data['type'] = 'drop_unique';
  1237. $data['column_names'] = preg_split(
  1238. '#"?\s*,\s*"?#',
  1239. trim($statement_matches[1], '"'),
  1240. -1,
  1241. PREG_SPLIT_NO_EMPTY
  1242. );
  1243. if (count($data['column_names']) == 1) {
  1244. $data['column_name'] = reset($data['column_names']);
  1245. }
  1246. } elseif (preg_match('#ADD\s+UNIQUE\s*\(\s*([^\)]+?)\s*\)\s*$#isD', $statement, $statement_matches)) {
  1247. $data['type'] = 'add_unique';
  1248. $data['column_names'] = preg_split(
  1249. '#"?\s*,\s*"?#',
  1250. trim($statement_matches[1], '"'),
  1251. -1,
  1252. PREG_SPLIT_NO_EMPTY
  1253. );
  1254. if (count($data['column_names']) == 1) {
  1255. $data['column_name'] = reset($data['column_names']);
  1256. }
  1257. } else {
  1258. return $sql;
  1259. }
  1260. $data['table'] = self::unescapeIdentifier($data['table']);
  1261. if (isset($data['new_table_name'])) {
  1262. $data['new_table_name'] = self::unescapeIdentifier($data['new_table_name']);
  1263. }
  1264. if (isset($data['column_name'])) {
  1265. $data['column_name'] = self::unescapeIdentifier($data['column_name']);
  1266. }
  1267. if (isset($data['column_names'])) {
  1268. $data['column_names'] = array_map(
  1269. array('fSQLSchemaTranslation', 'unescapeIdentifier'),
  1270. $data['column_names']
  1271. );
  1272. }
  1273. if (isset($data['new_column_name'])) {
  1274. $data['new_column_name'] = self::unescapeIdentifier($data['new_column_name']);
  1275. }
  1276. if ($this->database->getType() == 'db2') {
  1277. $sql = $this->translateDB2AlterTableStatements($sql, $extra_statements, $data);
  1278. }
  1279. if ($this->database->getType() == 'mssql') {
  1280. $sql = $this->translateMSSQLAlterTableStatements($sql, $extra_statements, $data);
  1281. }
  1282. if ($this->database->getType() == 'mysql') {
  1283. $sql = $this->translateMySQLAlterTableStatements($sql, $extra_statements, $rollback_statements, $data);
  1284. }
  1285. if ($this->database->getType() == 'oracle') {
  1286. $sql = $this->translateOracleAlterTableStatements($sql, $extra_statements, $data);
  1287. }
  1288. if ($this->database->getType() == 'postgresql') {
  1289. $sql = $this->translatePostgreSQLAlterTableStatements($sql, $extra_statements, $data);
  1290. }
  1291. if ($this->database->getType() == 'sqlite') {
  1292. if ($data['type'] == 'rename_table') {
  1293. $sql = $this->translateSQLiteRenameTableStatements($sql, $extra_statements, $data);
  1294. } else {
  1295. $sql = $this->translateSQLiteAlterTableStatements($sql, $extra_statements, $data);
  1296. }
  1297. }
  1298. // All databases except for MySQL and Oracle support transactions around data definition queries
  1299. // All of the Oracle statements w…

Large files files are truncated, but you can click here to view the full file