PageRenderTime 74ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/fSchema.php

https://bitbucket.org/wbond/flourish
PHP | 3157 lines | 2249 code | 319 blank | 589 comment | 318 complexity | c1a92bec7e4ab06e4c8e2ae4f356e0f0 MD5 | raw file
  1. <?php
  2. /**
  3. * Gets schema information for the selected database
  4. *
  5. * @copyright Copyright (c) 2007-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/fSchema
  11. *
  12. * @version 1.0.0b51
  13. * @changes 1.0.0b51 Fixed handling of getting tables in table creation order when a table references itself, fixed default value detection for the last column in a MySQL table [wb, 2012-01-12]
  14. * @changes 1.0.0b50 Fixed detection of explicitly named SQLite foreign key constraints [wb, 2011-08-23]
  15. * @changes 1.0.0b49 Added support for spatial/geometric data types in MySQL and PostgreSQL [wb, 2011-05-26]
  16. * @changes 1.0.0b48 Fixed a bug with ::getTables() not working on MySQL 4.x, fixed ::getKeys() to always return a reset array [wb, 2011-05-24]
  17. * @changes 1.0.0b47 Backwards Compatibility Break - ::getTables(), ::getColumnInfo(), ::getDatabases(), ::getKeys() and ::getRelationships() now return database, schema, table and column names in lowercase, added the `$creation_order` parameter to ::getTables(), fixed bugs with getting column and key information from MSSQL, Oracle and SQLite [wb, 2011-05-09]
  18. * @changes 1.0.0b46 Enhanced SQLite schema detection to cover situations where `UNIQUE` constraints are defined separately from the table and when comments are used in `CREATE TABLE` statements [wb, 2011-02-06]
  19. * @changes 1.0.0b45 Fixed Oracle auto incrementing detection to work with `INSERT OR UPDATE` triggers, fixed detection of dynamic default date/time/timestamp values for DB2 and Oracle [wb, 2010-12-04]
  20. * @changes 1.0.0b44 Fixed the list of valid elements for ::getColumnInfo() [wb, 2010-11-28]
  21. * @changes 1.0.0b43 Added the `comment` element to the information returned by ::getColumnInfo() [wb, 2010-11-28]
  22. * @changes 1.0.0b42 Fixed a bug with MySQL detecting default `ON DELETE` clauses [wb, 2010-10-19]
  23. * @changes 1.0.0b41 Fixed handling MySQL table names that require quoting [wb, 2010-08-24]
  24. * @changes 1.0.0b40 Fixed bugs in the documentation and error message of ::getColumnInfo() about what are valid elements [wb, 2010-07-21]
  25. * @changes 1.0.0b39 Fixed a regression where key detection SQL was not compatible with PostgreSQL 8.1 [wb, 2010-04-13]
  26. * @changes 1.0.0b38 Added Oracle support to ::getDatabases() [wb, 2010-04-13]
  27. * @changes 1.0.0b37 Fixed ::getDatabases() for MSSQL [wb, 2010-04-09]
  28. * @changes 1.0.0b36 Fixed PostgreSQL to properly report explicit `NULL` default values via ::getColumnInfo() [wb, 2010-03-30]
  29. * @changes 1.0.0b35 Added `max_length` values for various text and blob data types across all databases [wb, 2010-03-29]
  30. * @changes 1.0.0b34 Added `min_value` and `max_value` attributes to ::getColumnInfo() to specify the valid range for numeric columns [wb, 2010-03-16]
  31. * @changes 1.0.0b33 Changed it so that PostgreSQL unique indexes containing functions are ignored since they can't be properly detected at this point [wb, 2010-03-14]
  32. * @changes 1.0.0b32 Fixed ::getTables() to not include views for MySQL [wb, 2010-03-14]
  33. * @changes 1.0.0b31 Fixed the creation of the default caching key for ::enableCaching() [wb, 2010-03-02]
  34. * @changes 1.0.0b30 Fixed the class to work with lower privilege Oracle accounts and added detection of Oracle number columns [wb, 2010-01-25]
  35. * @changes 1.0.0b29 Added on_delete and on_update elements to one-to-one relationship info retrieved by ::getRelationships() [wb, 2009-12-16]
  36. * @changes 1.0.0b28 Fixed a bug with detecting some multi-column unique constraints in SQL Server databases [wb, 2009-11-13]
  37. * @changes 1.0.0b27 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]
  38. * @changes 1.0.0b26 Added the placeholder element to the output of ::getColumnInfo(), added support for PostgreSQL, MSSQL and Oracle "schemas", added support for parsing quoted SQLite identifiers [wb, 2009-10-22]
  39. * @changes 1.0.0b25 One-to-one relationships utilizing the primary key as a foreign key are now properly detected [wb, 2009-09-22]
  40. * @changes 1.0.0b24 Fixed MSSQL support to work with ODBC database connections [wb, 2009-09-18]
  41. * @changes 1.0.0b23 Fixed a bug where one-to-one relationships were being listed as many-to-one [wb, 2009-07-21]
  42. * @changes 1.0.0b22 PostgreSQL UNIQUE constraints that are created as indexes and not table constraints are now properly detected [wb, 2009-07-08]
  43. * @changes 1.0.0b21 Added support for the UUID data type in PostgreSQL [wb, 2009-06-18]
  44. * @changes 1.0.0b20 Add caching of merged info, improved performance of ::getColumnInfo() [wb, 2009-06-15]
  45. * @changes 1.0.0b19 Fixed a couple of bugs with ::setKeysOverride() [wb, 2009-06-04]
  46. * @changes 1.0.0b18 Added missing support for MySQL mediumint columns [wb, 2009-05-18]
  47. * @changes 1.0.0b17 Fixed a bug with ::clearCache() not properly reseting the tables and databases list [wb, 2009-05-13]
  48. * @changes 1.0.0b16 Backwards Compatibility Break - ::setCacheFile() changed to ::enableCaching() and now requires an fCache object, ::flushInfo() renamed to ::clearCache(), added Oracle support [wb, 2009-05-04]
  49. * @changes 1.0.0b15 Added support for the three different types of identifier quoting in SQLite [wb, 2009-03-28]
  50. * @changes 1.0.0b14 Added support for MySQL column definitions containing the COLLATE keyword [wb, 2009-03-28]
  51. * @changes 1.0.0b13 Fixed a bug with detecting PostgreSQL columns having both a CHECK constraint and a UNIQUE constraint [wb, 2009-02-27]
  52. * @changes 1.0.0b12 Fixed detection of multi-column primary keys in MySQL [wb, 2009-02-27]
  53. * @changes 1.0.0b11 Fixed an issue parsing MySQL tables with comments [wb, 2009-02-25]
  54. * @changes 1.0.0b10 Added the ::getDatabases() method [wb, 2009-02-24]
  55. * @changes 1.0.0b9 Now detects unsigned and zerofill MySQL data types that do not have a parenthetical part [wb, 2009-02-16]
  56. * @changes 1.0.0b8 Mapped the MySQL data type `'set'` to `'varchar'`, however valid values are not implemented yet [wb, 2009-02-01]
  57. * @changes 1.0.0b7 Fixed a bug with detecting MySQL timestamp columns [wb, 2009-01-28]
  58. * @changes 1.0.0b6 Fixed a bug with detecting MySQL columns that accept `NULL` [wb, 2009-01-19]
  59. * @changes 1.0.0b5 ::setColumnInfo(): fixed a bug with not grabbing the real database schema first, made general improvements [wb, 2009-01-19]
  60. * @changes 1.0.0b4 Added support for MySQL binary data types, numeric data type options unsigned and zerofill, and per-column character set definitions [wb, 2009-01-17]
  61. * @changes 1.0.0b3 Fixed detection of the data type of MySQL timestamp columns, added support for dynamic default date/time values [wb, 2009-01-11]
  62. * @changes 1.0.0b2 Fixed a bug with detecting multi-column unique keys in MySQL [wb, 2009-01-03]
  63. * @changes 1.0.0b The initial implementation [wb, 2007-09-25]
  64. */
  65. class fSchema
  66. {
  67. /**
  68. * The place to cache to
  69. *
  70. * @var fCache
  71. */
  72. private $cache = NULL;
  73. /**
  74. * The cache prefix to use for cache entries
  75. *
  76. * @var string
  77. */
  78. private $cache_prefix;
  79. /**
  80. * The cached column info
  81. *
  82. * @var array
  83. */
  84. private $column_info = array();
  85. /**
  86. * The column info to override
  87. *
  88. * @var array
  89. */
  90. private $column_info_override = array();
  91. /**
  92. * A reference to an instance of the fDatabase class
  93. *
  94. * @var fDatabase
  95. */
  96. private $database = NULL;
  97. /**
  98. * The databases on the current database server
  99. *
  100. * @var array
  101. */
  102. private $databases = NULL;
  103. /**
  104. * The cached key info
  105. *
  106. * @var array
  107. */
  108. private $keys = array();
  109. /**
  110. * The key info to override
  111. *
  112. * @var array
  113. */
  114. private $keys_override = array();
  115. /**
  116. * The merged column info
  117. *
  118. * @var array
  119. */
  120. private $merged_column_info = array();
  121. /**
  122. * The merged key info
  123. *
  124. * @var array
  125. */
  126. private $merged_keys = array();
  127. /**
  128. * The relationships in the database
  129. *
  130. * @var array
  131. */
  132. private $relationships = array();
  133. /**
  134. * The tables in the database
  135. *
  136. * @var array
  137. */
  138. private $tables = NULL;
  139. /**
  140. * Sets the database
  141. *
  142. * @param fDatabase $database The fDatabase instance
  143. * @return fSchema
  144. */
  145. public function __construct($database)
  146. {
  147. $this->database = $database;
  148. }
  149. /**
  150. * All requests that hit this method should be requests for callbacks
  151. *
  152. * @internal
  153. *
  154. * @param string $method The method to create a callback for
  155. * @return callback The callback for the method requested
  156. */
  157. public function __get($method)
  158. {
  159. return array($this, $method);
  160. }
  161. /**
  162. * Checks to see if a column is part of a single-column `UNIQUE` key
  163. *
  164. * @param string $table The table the column is located in
  165. * @param string $column The column to check
  166. * @return boolean If the column is part of a single-column unique key
  167. */
  168. private function checkForSingleColumnUniqueKey($table, $column)
  169. {
  170. foreach ($this->merged_keys[$table]['unique'] as $key) {
  171. if (array($column) == $key) {
  172. return TRUE;
  173. }
  174. }
  175. if (array($column) == $this->merged_keys[$table]['primary']) {
  176. return TRUE;
  177. }
  178. return FALSE;
  179. }
  180. /**
  181. * Clears all of the schema info out of the object and, if set, the fCache object
  182. *
  183. * @internal
  184. *
  185. * @return void
  186. */
  187. public function clearCache()
  188. {
  189. $this->column_info = array();
  190. $this->databases = NULL;
  191. $this->keys = array();
  192. $this->merged_column_info = array();
  193. $this->merged_keys = array();
  194. $this->relationships = array();
  195. $this->tables = NULL;
  196. if ($this->cache) {
  197. $prefix = $this->makeCachePrefix();
  198. $this->cache->delete($prefix . 'column_info');
  199. $this->cache->delete($prefix . 'databases');
  200. $this->cache->delete($prefix . 'keys');
  201. $this->cache->delete($prefix . 'merged_column_info');
  202. $this->cache->delete($prefix . 'merged_keys');
  203. $this->cache->delete($prefix . 'relationships');
  204. $this->cache->delete($prefix . 'tables');
  205. }
  206. }
  207. /**
  208. * Returns an ordered array of table names, in a valid table creation order
  209. *
  210. * @param string $filter_table The only return this table and tables that rely on it
  211. * @return array An array of table names
  212. */
  213. private function determineTableCreationOrder($filter_table=NULL)
  214. {
  215. $found = array();
  216. $ignored_found = array();
  217. $current_tables = $this->getTables();
  218. // Prevent an infinite loop
  219. $last_tables = array();
  220. while ($current_tables && $current_tables != $last_tables) {
  221. $remaining_tables = array();
  222. foreach ($current_tables as $table) {
  223. $foreign_keys = $this->getKeys($table, 'foreign');
  224. if (!$foreign_keys) {
  225. if ($filter_table !== NULL) {
  226. if ($table == $filter_table) {
  227. $found[] = $table;
  228. } else {
  229. $ignored_found[] = $table;
  230. }
  231. } else {
  232. $found[] = $table;
  233. }
  234. } else {
  235. $all_dependencies_met = TRUE;
  236. $found_dependencies = 0;
  237. foreach ($foreign_keys as $foreign_key) {
  238. if ($table != $foreign_key['foreign_table'] && !in_array($foreign_key['foreign_table'], $found) && !in_array($foreign_key['foreign_table'], $ignored_found)) {
  239. $all_dependencies_met = FALSE;
  240. break;
  241. } elseif (in_array($foreign_key['foreign_table'], $found)) {
  242. $found_dependencies++;
  243. }
  244. }
  245. if ($all_dependencies_met) {
  246. if ($filter_table !== NULL) {
  247. if ($found_dependencies || $table == $filter_table) {
  248. $found[] = $table;
  249. } else {
  250. $ignored_found[] = $table;
  251. }
  252. } else {
  253. $found[] = $table;
  254. }
  255. } else {
  256. $remaining_tables[] = $table;
  257. }
  258. }
  259. }
  260. $last_tables = $current_tables;
  261. $current_tables = $remaining_tables;
  262. }
  263. return $found;
  264. }
  265. /**
  266. * Sets the schema to be cached to the fCache object specified
  267. *
  268. * @param fCache $cache The cache to cache to
  269. * @param string $key_token Internal use only! (this will be used in the cache key to uniquely identify the cache for this fSchema object)
  270. * @return void
  271. */
  272. public function enableCaching($cache, $key_token=NULL)
  273. {
  274. $this->cache = $cache;
  275. if ($key_token !== NULL) {
  276. $this->cache_prefix = 'fSchema::' . $this->database->getType() . '::' . $key_token . '::';
  277. }
  278. $prefix = $this->makeCachePrefix();
  279. $this->column_info = $this->cache->get($prefix . 'column_info', array());
  280. $this->databases = $this->cache->get($prefix . 'databases', NULL);
  281. $this->keys = $this->cache->get($prefix . 'keys', array());
  282. if (!$this->column_info_override && !$this->keys_override) {
  283. $this->merged_column_info = $this->cache->get($prefix . 'merged_column_info', array());
  284. $this->merged_keys = $this->cache->get($prefix . 'merged_keys', array());
  285. $this->relationships = $this->cache->get($prefix . 'relationships', array());
  286. }
  287. $this->tables = $this->cache->get($prefix . 'tables', NULL);
  288. }
  289. /**
  290. * Gets the column info from the database for later access
  291. *
  292. * @param string $table The table to fetch the column info for
  293. * @return void
  294. */
  295. private function fetchColumnInfo($table)
  296. {
  297. if (isset($this->column_info[$table])) {
  298. return;
  299. }
  300. switch ($this->database->getType()) {
  301. case 'db2':
  302. $column_info = $this->fetchDB2ColumnInfo($table);
  303. break;
  304. case 'mssql':
  305. $column_info = $this->fetchMSSQLColumnInfo($table);
  306. break;
  307. case 'mysql':
  308. $column_info = $this->fetchMySQLColumnInfo($table);
  309. break;
  310. case 'oracle':
  311. $column_info = $this->fetchOracleColumnInfo($table);
  312. break;
  313. case 'postgresql':
  314. $column_info = $this->fetchPostgreSQLColumnInfo($table);
  315. break;
  316. case 'sqlite':
  317. $column_info = $this->fetchSQLiteColumnInfo($table);
  318. break;
  319. }
  320. if (!$column_info) {
  321. return;
  322. }
  323. $this->column_info[$table] = $column_info;
  324. if ($this->cache) {
  325. $this->cache->set($this->makeCachePrefix() . 'column_info', $this->column_info);
  326. }
  327. }
  328. /**
  329. * Gets the column info from a DB2 database
  330. *
  331. * @param string $table The table to fetch the column info for
  332. * @return array The column info for the table specified - see ::getColumnInfo() for details
  333. */
  334. private function fetchDB2ColumnInfo($table)
  335. {
  336. $column_info = array();
  337. $schema = strtolower($this->database->getUsername());
  338. if (strpos($table, '.') !== FALSE) {
  339. list ($schema, $table) = explode('.', $table);
  340. }
  341. $data_type_mapping = array(
  342. 'smallint' => 'integer',
  343. 'integer' => 'integer',
  344. 'bigint' => 'integer',
  345. 'timestamp' => 'timestamp',
  346. 'date' => 'date',
  347. 'time' => 'time',
  348. 'varchar' => 'varchar',
  349. 'long varchar' => 'varchar',
  350. 'vargraphic' => 'varchar',
  351. 'long vargraphic' => 'varchar',
  352. 'character' => 'char',
  353. 'graphic' => 'char',
  354. 'real' => 'float',
  355. 'decimal' => 'float',
  356. 'numeric' => 'float',
  357. 'blob' => 'blob',
  358. 'clob' => 'text',
  359. 'dbclob' => 'text'
  360. );
  361. $max_min_values = array(
  362. 'smallint' => array('min' => new fNumber(-32768), 'max' => new fNumber(32767)),
  363. 'integer' => array('min' => new fNumber(-2147483648), 'max' => new fNumber(2147483647)),
  364. 'bigint' => array('min' => new fNumber('-9223372036854775808'), 'max' => new fNumber('9223372036854775807'))
  365. );
  366. // Get the column info
  367. $sql = "SELECT
  368. LOWER(C.COLNAME) AS \"COLUMN\",
  369. C.TYPENAME AS TYPE,
  370. C.NULLS AS NULLABLE,
  371. C.DEFAULT,
  372. C.LENGTH AS MAX_LENGTH,
  373. C.SCALE,
  374. CASE WHEN C.IDENTITY = 'Y' AND (C.GENERATED = 'D' OR C.GENERATED = 'A') THEN '1' ELSE '0' END AS AUTO_INCREMENT,
  375. CH.TEXT AS \"CONSTRAINT\",
  376. C.REMARKS AS \"COMMENT\"
  377. FROM
  378. SYSCAT.COLUMNS AS C LEFT JOIN
  379. SYSCAT.COLCHECKS AS CC ON
  380. C.TABSCHEMA = CC.TABSCHEMA AND
  381. C.TABNAME = CC.TABNAME AND
  382. C.COLNAME = CC.COLNAME AND
  383. CC.USAGE = 'R' LEFT JOIN
  384. SYSCAT.CHECKS AS CH ON
  385. C.TABSCHEMA = CH.TABSCHEMA AND
  386. C.TABNAME = CH.TABNAME AND
  387. CH.TYPE = 'C' AND
  388. CH.CONSTNAME = CC.CONSTNAME
  389. WHERE
  390. LOWER(C.TABSCHEMA) = %s AND
  391. LOWER(C.TABNAME) = %s
  392. ORDER BY
  393. C.COLNO ASC";
  394. $result = $this->database->query($sql, strtolower($schema), strtolower($table));
  395. foreach ($result as $row) {
  396. $info = array();
  397. foreach ($data_type_mapping as $data_type => $mapped_data_type) {
  398. if (stripos($row['type'], $data_type) === 0) {
  399. if (isset($max_min_values[$data_type])) {
  400. $info['min_value'] = $max_min_values[$data_type]['min'];
  401. $info['max_value'] = $max_min_values[$data_type]['max'];
  402. }
  403. $info['type'] = $mapped_data_type;
  404. break;
  405. }
  406. }
  407. // Handle decimal places and min/max for numeric/decimals
  408. if (in_array(strtolower($row['type']), array('decimal', 'numeric'))) {
  409. $info['decimal_places'] = $row['scale'];
  410. $before_digits = str_pad('', $row['max_length'] - $row['scale'], '9');
  411. $after_digits = str_pad('', $row['scale'], '9');
  412. $max_min = $before_digits . ($after_digits ? '.' : '') . $after_digits;
  413. $info['min_value'] = new fNumber('-' . $max_min);
  414. $info['max_value'] = new fNumber($max_min);
  415. }
  416. if (!isset($info['type'])) {
  417. $info['type'] = $row['type'];
  418. }
  419. // Handle the special data for varchar columns
  420. if (in_array($info['type'], array('char', 'varchar', 'text', 'blob'))) {
  421. $info['max_length'] = $row['max_length'];
  422. }
  423. // The generally accepted practice for boolean on DB2 is a CHAR(1) with a CHECK constraint
  424. if ($info['type'] == 'char' && $info['max_length'] == 1 && !empty($row['constraint'])) {
  425. if (is_resource($row['constraint'])) {
  426. $row['constraint'] = stream_get_contents($row['constraint']);
  427. }
  428. if (preg_match('/^\s*"?' . preg_quote($row['column'], '/') . '"?\s+in\s+\(\s*(\'0\',\s*\'1\'|\'1\',\s*\'0\')\s*\)\s*$/i', $row['constraint'])) {
  429. $info['type'] = 'boolean';
  430. $info['max_length'] = NULL;
  431. }
  432. }
  433. // If the column has a constraint, look for valid values
  434. if (in_array($info['type'], array('char', 'varchar')) && !empty($row['constraint'])) {
  435. if (preg_match('/^\s*"?' . preg_quote($row['column'], '/') . '"?\s+in\s+\((.*?)\)\s*$/i', $row['constraint'], $match)) {
  436. if (preg_match_all("/(?<!')'((''|[^']+)*)'/", $match[1], $matches, PREG_PATTERN_ORDER)) {
  437. $info['valid_values'] = str_replace("''", "'", $matches[1]);
  438. }
  439. }
  440. }
  441. // Handle auto increment
  442. if ($row['auto_increment']) {
  443. $info['auto_increment'] = TRUE;
  444. }
  445. // Handle default values
  446. if ($row['default'] !== NULL) {
  447. if ($row['default'] == 'NULL') {
  448. $info['default'] = NULL;
  449. } elseif (in_array($info['type'], array('timestamp', 'date', 'time')) && $row['default'][0] != "'") {
  450. $info['default'] = str_replace(' ', '_', $row['default']);
  451. } elseif (in_array($info['type'], array('char', 'varchar', 'text', 'timestamp', 'date', 'time')) ) {
  452. $info['default'] = substr($row['default'], 1, -1);
  453. } elseif ($info['type'] == 'boolean') {
  454. $info['default'] = (boolean) substr($row['default'], 1, -1);
  455. } else {
  456. $info['default'] = $row['default'];
  457. }
  458. }
  459. // Handle not null
  460. $info['not_null'] = ($row['nullable'] == 'N') ? TRUE : FALSE;
  461. $info['comment'] = $row['comment'];
  462. $column_info[$row['column']] = $info;
  463. }
  464. return $column_info;
  465. }
  466. /**
  467. * Fetches the key info for a DB2 database
  468. *
  469. * @return array The keys arrays for every table in the database - see ::getKeys() for details
  470. */
  471. private function fetchDB2Keys()
  472. {
  473. $keys = array();
  474. $default_schema = strtolower($this->database->getUsername());
  475. $tables = $this->getTables();
  476. foreach ($tables as $table) {
  477. $keys[$table] = array();
  478. $keys[$table]['primary'] = array();
  479. $keys[$table]['unique'] = array();
  480. $keys[$table]['foreign'] = array();
  481. }
  482. $params = array();
  483. $sql = "(SELECT
  484. LOWER(RTRIM(R.TABSCHEMA)) AS \"SCHEMA\",
  485. LOWER(R.TABNAME) AS \"TABLE\",
  486. R.CONSTNAME AS CONSTRAINT_NAME,
  487. 'foreign' AS \"TYPE\",
  488. LOWER(K.COLNAME) AS \"COLUMN\",
  489. LOWER(RTRIM(R.REFTABSCHEMA)) AS FOREIGN_SCHEMA,
  490. LOWER(R.REFTABNAME) AS FOREIGN_TABLE,
  491. LOWER(FK.COLNAME) AS FOREIGN_COLUMN,
  492. CASE R.DELETERULE WHEN 'C' THEN 'cascade' WHEN 'A' THEN 'no_action' WHEN 'R' THEN 'restrict' ELSE 'set_null' END AS ON_DELETE,
  493. CASE R.UPDATERULE WHEN 'A' THEN 'no_action' WHEN 'R' THEN 'restrict' END AS ON_UPDATE,
  494. K.COLSEQ
  495. FROM
  496. SYSCAT.REFERENCES AS R INNER JOIN
  497. SYSCAT.KEYCOLUSE AS K ON
  498. R.CONSTNAME = K.CONSTNAME AND
  499. R.TABSCHEMA = K.TABSCHEMA AND
  500. R.TABNAME = K.TABNAME INNER JOIN
  501. SYSCAT.KEYCOLUSE AS FK ON
  502. R.REFKEYNAME = FK.CONSTNAME AND
  503. R.REFTABSCHEMA = FK.TABSCHEMA AND
  504. R.REFTABNAME = FK.TABNAME
  505. WHERE ";
  506. $conditions = array();
  507. foreach ($tables as $table) {
  508. if (strpos($table, '.') === FALSE) {
  509. $table = $default_schema . '.' . $table;
  510. }
  511. list ($schema, $table) = explode('.', strtolower($table));
  512. $conditions[] = "LOWER(R.TABSCHEMA) = %s AND LOWER(R.TABNAME) = %s";
  513. $params[] = $schema;
  514. $params[] = $table;
  515. }
  516. $sql .= '((' . join(') OR( ', $conditions) . '))';
  517. $sql .= "
  518. ) UNION (
  519. SELECT
  520. LOWER(RTRIM(I.TABSCHEMA)) AS \"SCHEMA\",
  521. LOWER(I.TABNAME) AS \"TABLE\",
  522. LOWER(I.INDNAME) AS CONSTRAINT_NAME,
  523. CASE I.UNIQUERULE WHEN 'U' THEN 'unique' ELSE 'primary' END AS \"TYPE\",
  524. LOWER(C.COLNAME) AS \"COLUMN\",
  525. NULL AS FOREIGN_SCHEMA,
  526. NULL AS FOREIGN_TABLE,
  527. NULL AS FOREIGN_COLUMN,
  528. NULL AS ON_DELETE,
  529. NULL AS ON_UPDATE,
  530. C.COLSEQ
  531. FROM
  532. SYSCAT.INDEXES AS I INNER JOIN
  533. SYSCAT.INDEXCOLUSE AS C ON I.INDSCHEMA = C.INDSCHEMA AND I.INDNAME = C.INDNAME
  534. WHERE
  535. I.UNIQUERULE IN ('U', 'P') AND
  536. ";
  537. $conditions = array();
  538. foreach ($tables as $table) {
  539. if (strpos($table, '.') === FALSE) {
  540. $table = $default_schema . '.' . $table;
  541. }
  542. list ($schema, $table) = explode('.', strtolower($table));
  543. $conditions[] = "LOWER(I.TABSCHEMA) = %s AND LOWER(I.TABNAME) = %s";
  544. $params[] = $schema;
  545. $params[] = $table;
  546. }
  547. $sql .= '((' . join(') OR( ', $conditions) . '))';
  548. $sql .= "
  549. )
  550. ORDER BY 4, 1, 2, 3, 11";
  551. $result = $this->database->query($sql, $params);
  552. $last_name = '';
  553. $last_table = '';
  554. $last_type = '';
  555. foreach ($result as $row) {
  556. if ($row['constraint_name'] != $last_name) {
  557. if ($last_name) {
  558. if ($last_type == 'foreign' || $last_type == 'unique') {
  559. $keys[$last_table][$last_type][] = $temp;
  560. } else {
  561. $keys[$last_table][$last_type] = $temp;
  562. }
  563. }
  564. $temp = array();
  565. if ($row['type'] == 'foreign') {
  566. $temp['column'] = $row['column'];
  567. $temp['foreign_table'] = $row['foreign_table'];
  568. if ($row['foreign_schema'] != $default_schema) {
  569. $temp['foreign_table'] = $row['foreign_schema'] . '.' . $temp['foreign_table'];
  570. }
  571. $temp['foreign_column'] = $row['foreign_column'];
  572. $temp['on_delete'] = 'no_action';
  573. $temp['on_update'] = 'no_action';
  574. if (!empty($row['on_delete'])) {
  575. $temp['on_delete'] = $row['on_delete'];
  576. }
  577. if (!empty($row['on_update'])) {
  578. $temp['on_update'] = $row['on_update'];
  579. }
  580. } else {
  581. $temp[] = $row['column'];
  582. }
  583. $last_table = $row['table'];
  584. if ($row['schema'] != $default_schema) {
  585. $last_table = $row['schema'] . '.' . $last_table;
  586. }
  587. $last_name = $row['constraint_name'];
  588. $last_type = $row['type'];
  589. } else {
  590. $temp[] = $row['column'];
  591. }
  592. }
  593. if (isset($temp)) {
  594. if ($last_type == 'foreign' || $last_type == 'unique') {
  595. $keys[$last_table][$last_type][] = $temp;
  596. } else {
  597. $keys[$last_table][$last_type] = $temp;
  598. }
  599. }
  600. return $keys;
  601. }
  602. /**
  603. * Gets the `PRIMARY KEY`, `FOREIGN KEY` and `UNIQUE` key constraints from the database
  604. *
  605. * @return void
  606. */
  607. private function fetchKeys()
  608. {
  609. if ($this->keys) {
  610. return;
  611. }
  612. switch ($this->database->getType()) {
  613. case 'db2':
  614. $keys = $this->fetchDB2Keys();
  615. break;
  616. case 'mssql':
  617. $keys = $this->fetchMSSQLKeys();
  618. break;
  619. case 'mysql':
  620. $keys = $this->fetchMySQLKeys();
  621. break;
  622. case 'oracle':
  623. $keys = $this->fetchOracleKeys();
  624. break;
  625. case 'postgresql':
  626. $keys = $this->fetchPostgreSQLKeys();
  627. break;
  628. case 'sqlite':
  629. $keys = $this->fetchSQLiteKeys();
  630. break;
  631. }
  632. $this->keys = $keys;
  633. if ($this->cache) {
  634. $this->cache->set($this->makeCachePrefix() . 'keys', $this->keys);
  635. }
  636. }
  637. /**
  638. * Gets the column info from a MSSQL database
  639. *
  640. * @param string $table The table to fetch the column info for
  641. * @return array The column info for the table specified - see ::getColumnInfo() for details
  642. */
  643. private function fetchMSSQLColumnInfo($table)
  644. {
  645. $column_info = array();
  646. $schema = 'dbo';
  647. if (strpos($table, '.') !== FALSE) {
  648. list ($schema, $table) = explode('.', $table);
  649. }
  650. $data_type_mapping = array(
  651. 'bit' => 'boolean',
  652. 'tinyint' => 'integer',
  653. 'smallint' => 'integer',
  654. 'int' => 'integer',
  655. 'bigint' => 'integer',
  656. 'timestamp' => 'integer',
  657. 'datetime' => 'timestamp',
  658. 'smalldatetime' => 'timestamp',
  659. 'datetime2' => 'timestamp',
  660. 'date' => 'date',
  661. 'time' => 'time',
  662. 'varchar' => 'varchar',
  663. 'nvarchar' => 'varchar',
  664. 'uniqueidentifier' => 'varchar',
  665. 'char' => 'char',
  666. 'nchar' => 'char',
  667. 'real' => 'float',
  668. 'float' => 'float',
  669. 'money' => 'float',
  670. 'smallmoney' => 'float',
  671. 'decimal' => 'float',
  672. 'numeric' => 'float',
  673. 'binary' => 'blob',
  674. 'varbinary' => 'blob',
  675. 'image' => 'blob',
  676. 'text' => 'text',
  677. 'ntext' => 'text',
  678. 'xml' => 'text'
  679. );
  680. $max_min_values = array(
  681. 'tinyint' => array('min' => new fNumber(0), 'max' => new fNumber(255)),
  682. 'smallint' => array('min' => new fNumber(-32768), 'max' => new fNumber(32767)),
  683. 'int' => array('min' => new fNumber(-2147483648), 'max' => new fNumber(2147483647)),
  684. 'bigint' => array('min' => new fNumber('-9223372036854775808'), 'max' => new fNumber('9223372036854775807')),
  685. 'smallmoney' => array('min' => new fNumber('-214748.3648'), 'max' => new fNumber('214748.3647')),
  686. 'money' => array('min' => new fNumber('-922337203685477.5808'), 'max' => new fNumber('922337203685477.5807'))
  687. );
  688. // Get the column info
  689. $sql = "SELECT
  690. LOWER(c.column_name) AS 'column',
  691. c.data_type AS 'type',
  692. c.is_nullable AS nullable,
  693. c.column_default AS 'default',
  694. c.character_maximum_length AS max_length,
  695. c.numeric_precision AS precision,
  696. c.numeric_scale AS decimal_places,
  697. CASE
  698. WHEN
  699. COLUMNPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), c.column_name, 'IsIdentity') = 1 AND
  700. OBJECTPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), 'IsMSShipped') = 0
  701. THEN '1'
  702. ELSE '0'
  703. END AS auto_increment,
  704. cc.check_clause AS 'constraint',
  705. CAST(ex.value AS VARCHAR(7500)) AS 'comment'
  706. FROM
  707. INFORMATION_SCHEMA.COLUMNS AS c LEFT JOIN
  708. INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu ON
  709. c.column_name = ccu.column_name AND
  710. c.table_name = ccu.table_name AND
  711. c.table_catalog = ccu.table_catalog LEFT JOIN
  712. INFORMATION_SCHEMA.CHECK_CONSTRAINTS AS cc ON
  713. ccu.constraint_name = cc.constraint_name AND
  714. ccu.constraint_catalog = cc.constraint_catalog";
  715. if (version_compare($this->database->getVersion(), 9, '<')) {
  716. $sql .= " LEFT JOIN sysproperties AS ex ON ex.id = OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)) AND ex.smallid = COLUMNPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), c.column_name, 'ColumnId') AND ex.name = 'MS_Description' AND OBJECTPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), 'IsMsShipped') = 0 ";
  717. } else {
  718. $sql .= " LEFT JOIN SYS.EXTENDED_PROPERTIES AS ex ON ex.major_id = OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)) AND ex.minor_id = COLUMNPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), c.column_name, 'ColumnId') AND ex.name = 'MS_Description' AND OBJECTPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), 'IsMsShipped') = 0 ";
  719. }
  720. $sql .= "
  721. WHERE
  722. LOWER(c.table_name) = %s AND
  723. LOWER(c.table_schema) = %s AND
  724. c.table_catalog = DB_NAME()";
  725. $result = $this->database->query($sql, strtolower($table), strtolower($schema));
  726. foreach ($result as $row) {
  727. $info = array();
  728. foreach ($data_type_mapping as $data_type => $mapped_data_type) {
  729. if (stripos($row['type'], $data_type) === 0) {
  730. if (isset($max_min_values[$data_type])) {
  731. $info['min_value'] = $max_min_values[$data_type]['min'];
  732. $info['max_value'] = $max_min_values[$data_type]['max'];
  733. }
  734. $info['type'] = $mapped_data_type;
  735. break;
  736. }
  737. }
  738. // Handle decimal places and min/max for numeric/decimals
  739. if (in_array(strtolower($row['type']), array('decimal', 'numeric'))) {
  740. $info['decimal_places'] = $row['decimal_places'];
  741. $before_digits = str_pad('', $row['precision'] - $row['decimal_places'], '9');
  742. $after_digits = str_pad('', $row['decimal_places'], '9');
  743. $max_min = $before_digits . ($after_digits ? '.' : '') . $after_digits;
  744. $info['min_value'] = new fNumber('-' . $max_min);
  745. $info['max_value'] = new fNumber($max_min);
  746. }
  747. if (!isset($info['type'])) {
  748. $info['type'] = $row['type'];
  749. }
  750. // Handle decimal places for money/smallmoney
  751. if (in_array($row['type'], array('money', 'smallmoney'))) {
  752. $info['decimal_places'] = 2;
  753. }
  754. // Handle the special data for varchar columns
  755. if (in_array($info['type'], array('char', 'varchar', 'text', 'blob'))) {
  756. if ($row['type'] == 'uniqueidentifier') {
  757. $row['max_length'] = 32;
  758. } elseif ($row['max_length'] == -1) {
  759. $row['max_length'] = $row['type'] == 'nvarchar' ? 1073741823 : 2147483647;
  760. }
  761. $info['max_length'] = $row['max_length'];
  762. }
  763. // If the column has a constraint, look for valid values
  764. if (in_array($info['type'], array('char', 'varchar')) && !empty($row['constraint'])) {
  765. if (preg_match('#^\(((?:(?: OR )?\[[^\]]+\]\s*=\s*\'(?:\'\'|[^\']+)*\')+)\)$#D', $row['constraint'], $matches)) {
  766. $valid_values = explode(' OR ', $matches[1]);
  767. foreach ($valid_values as $key => $value) {
  768. $value = preg_replace('#^\s*\[' . preg_quote($row['column'], '#') . '\]\s*=\s*\'(.*)\'\s*$#', '\1', $value);
  769. $valid_values[$key] = str_replace("''", "'", $value);
  770. }
  771. // SQL Server turns CHECK constraint values into a reversed list, so we fix it here
  772. $info['valid_values'] = array_reverse($valid_values);
  773. }
  774. }
  775. // Handle auto increment
  776. if ($row['auto_increment']) {
  777. $info['auto_increment'] = TRUE;
  778. }
  779. // Handle default values
  780. if ($row['default'] !== NULL) {
  781. if ($row['default'] == '(getdate())') {
  782. $info['default'] = 'CURRENT_TIMESTAMP';
  783. } elseif (in_array($info['type'], array('char', 'varchar', 'text', 'timestamp')) ) {
  784. $info['default'] = substr($row['default'], 2, -2);
  785. } elseif ($info['type'] == 'boolean') {
  786. $info['default'] = (boolean) substr($row['default'], 2, -2);
  787. } elseif (in_array($info['type'], array('integer', 'float')) ) {
  788. $info['default'] = str_replace(array('(', ')'), '', $row['default']);
  789. } else {
  790. $info['default'] = pack('H*', substr($row['default'], 3, -1));
  791. }
  792. }
  793. // Handle not null
  794. $info['not_null'] = ($row['nullable'] == 'NO') ? TRUE : FALSE;
  795. $info['comment'] = $row['comment'];
  796. $column_info[$row['column']] = $info;
  797. }
  798. return $column_info;
  799. }
  800. /**
  801. * Fetches the key info for an MSSQL database
  802. *
  803. * @return array The key info arrays for every table in the database - see ::getKeys() for details
  804. */
  805. private function fetchMSSQLKeys()
  806. {
  807. $keys = array();
  808. $tables = $this->getTables();
  809. foreach ($tables as $table) {
  810. $keys[$table] = array();
  811. $keys[$table]['primary'] = array();
  812. $keys[$table]['unique'] = array();
  813. $keys[$table]['foreign'] = array();
  814. }
  815. $sql = "SELECT
  816. LOWER(c.table_schema) AS \"schema\",
  817. LOWER(c.table_name) AS \"table\",
  818. kcu.constraint_name AS constraint_name,
  819. CASE c.constraint_type
  820. WHEN 'PRIMARY KEY' THEN 'primary'
  821. WHEN 'FOREIGN KEY' THEN 'foreign'
  822. WHEN 'UNIQUE' THEN 'unique'
  823. END AS 'type',
  824. LOWER(kcu.column_name) AS 'column',
  825. LOWER(ccu.table_schema) AS foreign_schema,
  826. LOWER(ccu.table_name) AS foreign_table,
  827. LOWER(ccu.column_name) AS foreign_column,
  828. REPLACE(LOWER(rc.delete_rule), ' ', '_') AS on_delete,
  829. REPLACE(LOWER(rc.update_rule), ' ', '_') AS on_update
  830. FROM
  831. INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS c INNER JOIN
  832. INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu ON
  833. c.table_name = kcu.table_name AND
  834. c.constraint_name = kcu.constraint_name LEFT JOIN
  835. INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc ON
  836. c.constraint_name = rc.constraint_name LEFT JOIN
  837. INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu ON
  838. ccu.constraint_name = rc.unique_constraint_name
  839. WHERE
  840. c.constraint_catalog = DB_NAME() AND
  841. c.table_name != 'sysdiagrams'
  842. ORDER BY
  843. LOWER(c.table_schema),
  844. LOWER(c.table_name),
  845. c.constraint_type,
  846. LOWER(kcu.constraint_name),
  847. kcu.ordinal_position,
  848. LOWER(kcu.column_name)";
  849. $result = $this->database->query($sql);
  850. $last_name = '';
  851. $last_table = '';
  852. $last_type = '';
  853. foreach ($result as $row) {
  854. if ($row['constraint_name'] != $last_name) {
  855. if ($last_name) {
  856. if ($last_type == 'foreign' || $last_type == 'unique') {
  857. if (!isset($keys[$last_table][$last_type])) {
  858. $keys[$last_table][$last_type] = array();
  859. }
  860. $keys[$last_table][$last_type][] = $temp;
  861. } else {
  862. $keys[$last_table][$last_type] = $temp;
  863. }
  864. }
  865. $temp = array();
  866. if ($row['type'] == 'foreign') {
  867. $temp['column'] = $row['column'];
  868. $temp['foreign_table'] = $row['foreign_table'];
  869. if ($row['foreign_schema'] != 'dbo') {
  870. $temp['foreign_table'] = $row['foreign_schema'] . '.' . $temp['foreign_table'];
  871. }
  872. $temp['foreign_column'] = $row['foreign_column'];
  873. $temp['on_delete'] = 'no_action';
  874. $temp['on_update'] = 'no_action';
  875. if (!empty($row['on_delete'])) {
  876. $temp['on_delete'] = $row['on_delete'];
  877. }
  878. if (!empty($row['on_update'])) {
  879. $temp['on_update'] = $row['on_update'];
  880. }
  881. } else {
  882. $temp[] = $row['column'];
  883. }
  884. $last_table = $row['table'];
  885. if ($row['schema'] != 'dbo') {
  886. $last_table = $row['schema'] . '.' . $last_table;
  887. }
  888. $last_name = $row['constraint_name'];
  889. $last_type = $row['type'];
  890. } else {
  891. $temp[] = $row['column'];
  892. }
  893. }
  894. if (isset($temp)) {
  895. if ($last_type == 'foreign' || $last_type == 'unique') {
  896. if (!isset($keys[$last_table][$last_type])) {
  897. $keys[$last_table][$last_type] = array();
  898. }
  899. $keys[$last_table][$last_type][] = $temp;
  900. } else {
  901. $keys[$last_table][$last_type] = $temp;
  902. }
  903. }
  904. return $keys;
  905. }
  906. /**
  907. * Gets the column info from a MySQL database
  908. *
  909. * @param string $table The table to fetch the column info for
  910. * @return array The column info for the table specified - see ::getColumnInfo() for details
  911. */
  912. private function fetchMySQLColumnInfo($table)
  913. {
  914. $data_type_mapping = array(
  915. 'tinyint' => 'integer',
  916. 'smallint' => 'integer',
  917. 'mediumint' => 'integer',
  918. 'int' => 'integer',
  919. 'bigint' => 'integer',
  920. 'datetime' => 'timestamp',
  921. 'timestamp' => 'timestamp',
  922. 'date' => 'date',
  923. 'time' => 'time',
  924. 'enum' => 'varchar',
  925. 'set' => 'varchar',
  926. 'varchar' => 'varchar',
  927. 'char' => 'char',
  928. 'float' => 'float',
  929. 'double' => 'float',
  930. 'decimal' => 'float',
  931. 'binary' => 'blob',
  932. 'varbinary' => 'blob',
  933. 'tinyblob' => 'blob',
  934. 'blob' => 'blob',
  935. 'mediumblob' => 'blob',
  936. 'longblob' => 'blob',
  937. 'tinytext' => 'text',
  938. 'text' => 'text',
  939. 'mediumtext' => 'text',
  940. 'longtext' => 'text',
  941. 'geometry' => 'blob',
  942. 'point' => 'blob',
  943. 'linestring' => 'blob',
  944. 'polygon' => 'blob'
  945. );
  946. $max_min_values = array(
  947. 'tinyint' => array('min' => new fNumber(-128), 'max' => new fNumber(127)),
  948. 'unsigned tinyint' => array('min' => new fNumber(0), 'max' => new fNumber(255)),
  949. 'smallint' => array('min' => new fNumber(-32768), 'max' => new fNumber(32767)),
  950. 'unsigned smallint' => array('min' => new fNumber(0), 'max' => new fNumber(65535)),
  951. 'mediumint' => array('min' => new fNumber(-8388608), 'max' => new fNumber(8388607)),
  952. 'unsigned mediumint' => array('min' => new fNumber(0), 'max' => new fNumber(16777215)),
  953. 'int' => array('min' => new fNumber(-2147483648), 'max' => new fNumber(2147483647)),
  954. 'unsigned int' => array('min' => new fNumber(0), 'max' => new fNumber('4294967295')),
  955. 'bigint' => array('min' => new fNumber('-9223372036854775808'), 'max' => new fNumber('9223372036854775807')),
  956. 'unsigned bigint' => array('min' => new fNumber(0), 'max' => new fNumber('18446744073709551615'))
  957. );
  958. $column_info = array();
  959. $result = $this->database->query('SHOW CREATE TABLE %r', $table);
  960. try {
  961. $row = $result->fetchRow();
  962. $create_sql = $row['Create Table'];
  963. } catch (fNoRowsException $e) {
  964. return array();
  965. }
  966. preg_match_all('#(?<=,|\()\s+(?:"|\`)(\w+)(?:"|\`)\s+(?:([a-z]+)(?:\(([^)]+)\))?( unsigned)?(?: zerofill)?)(?: character set [^ ]+)?(?: collate [^ ]+)?(?: NULL)?( NOT NULL)?(?: DEFAULT ((?:[^, \'\n]*|\'(?:\'\'|[^\']+)*\')))?( auto_increment)?( COMMENT \'(?:\'\'|[^\']+)*\')?( ON UPDATE CURRENT_TIMESTAMP)?\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER);
  967. foreach ($matches as $match) {
  968. $info = array();
  969. foreach ($data_type_mapping as $data_type => $mapped_data_type) {
  970. if (stripos($match[2], $data_type) === 0) {
  971. if ($match[2] == 'tinyint' && $match[3] == 1) {
  972. $mapped_data_type = 'boolean';
  973. } elseif (preg_match('#((?:unsigned )?(?:tiny|small|medium|big)?int)#', (isset($match[4]) ? $match[4] . ' ' : '') . $data_type, $int_match)) {
  974. if (isset($max_min_values[$int_match[1]])) {
  975. $info['min_value'] = $max_min_values[$int_match[1]]['min'];
  976. $info['max_value'] = $max_min_values[$int_match[1]]['max'];
  977. }
  978. }
  979. $info['type'] = $mapped_data_type;
  980. break;
  981. }
  982. }
  983. if (!isset($info['type'])) {
  984. $info['type'] = preg_replace('#^([a-z ]+).*$#iD', '\1', $match[2]);
  985. }
  986. switch ($match[2]) {
  987. case 'tinyblob':
  988. case 'tinytext':
  989. $info['max_length'] = 255;
  990. break;
  991. case 'blob':
  992. case 'text':
  993. $info['max_length'] = 65535;
  994. break;
  995. case 'mediumblob':
  996. case 'mediumtext':
  997. $info['max_length'] = 16777215;
  998. break;
  999. case 'longblob':
  1000. case 'longtext':
  1001. $info['max_length'] = 4294967295;
  1002. break;
  1003. }
  1004. if (stripos($match[2], 'enum') === 0) {
  1005. $info['valid_values'] = preg_replace("/^'|'\$/D", '', explode(",", $match[3]));
  1006. $match[3] = 0;
  1007. foreach ($info['valid_values'] as $valid_value) {
  1008. if (strlen(utf8_decode($valid_value)) > $match[3]) {
  1009. $match[3] = strlen(utf8_decode($valid_value));
  1010. }
  1011. }
  1012. }
  1013. // The set data type is currently only supported as a varchar
  1014. // with a max length of all valid values concatenated by ,s
  1015. if (stripos($match[2], 'set') === 0) {
  1016. $values = preg_replace("/^'|'\$/D", '', explode(",", $match[3]));
  1017. $match[3] = strlen(join(',', $values));
  1018. }
  1019. // Type specific information
  1020. if (in_array($info['type'], array('char', 'varchar'))) {
  1021. $info['max_length'] = $match[3];
  1022. }
  1023. // Grab the number of decimal places
  1024. if (stripos($match[2], 'decimal') === 0) {
  1025. if (preg_match('#^\s*(\d+)\s*,\s*(\d+)\s*$#D', $match[3], $data_type_info)) {
  1026. $info['decimal_places'] = $data_type_info[2];
  1027. $before_digits = str_pad('', $data_type_info[1] - $info['decimal_places'], '9');
  1028. $after_digits = str_pad('', $info['decimal_places'], '9');
  1029. $max_min = $before_digits . ($after_digits ? '.' : '') . $after_digits;
  1030. $info['min_value'] = new fNumber('-' . $max_min);
  1031. $info['max_value'] = new fNumber($max_min);
  1032. }
  1033. }
  1034. // Not null
  1035. $info['not_null'] = (!empty($match[5])) ? TRUE : FALSE;
  1036. // Default values
  1037. if (!empty($match[6]) && $match[6] != 'NULL') {
  1038. $info['default'] = preg_replace("/^'|'\$/D", '', $match[6]);
  1039. }
  1040. if ($info['type'] == 'boolean' && isset($info['default'])) {
  1041. $info['default'] = (boolean) $info['default'];
  1042. }
  1043. // Auto increment fields
  1044. if (!empty($match[7])) {
  1045. $info['auto_increment'] = TRUE;
  1046. }
  1047. // Column comments
  1048. if (!empty($match[8])) {
  1049. $info['comment'] = str_replace("''", "'", substr($match[8], 10, -1));
  1050. }
  1051. $column_info[strtolower($match[1])] = $info;
  1052. }
  1053. return $column_info;
  1054. }
  1055. /**
  1056. * Fetches the keys for a MySQL database
  1057. *
  1058. * @return array The keys arrays for every table in the database - see ::getKeys() for details
  1059. */
  1060. private function fetchMySQLKeys()
  1061. {
  1062. $tables = $this->getTables();
  1063. $keys = array();
  1064. foreach ($tables as $table) {
  1065. $keys[$table] = array();
  1066. $keys[$table]['primary'] = array();
  1067. $keys[$table]['foreign'] = array();
  1068. $keys[$table]['unique'] = array();
  1069. $result = $this->database->query('SHOW CREATE TABLE %r', $table);
  1070. $row = $result->fetchRow();
  1071. // Primary keys
  1072. preg_match_all('/PRIMARY KEY\s+\("(.*?)"\),?\n/U', $row['Create Table'], $matches, PREG_SET_ORDER);
  1073. if (!empty($matches)) {
  1074. $keys[$table]['primary'] = explode('","', strtolower($matches[0][1]));
  1075. }
  1076. // Unique keys
  1077. preg_match_all('/UNIQUE KEY\s+"([^"]+)"\s+\("(.*?)"\),?\n/U', $row['Create Table'], $matches, PREG_SET_ORDER);
  1078. foreach ($matches as $match) {
  1079. $keys[$table]['unique'][] = explode('","', strtolower($match[2]));
  1080. }
  1081. // Foreign keys
  1082. preg_match_all('#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))?#', $row['Create Table'], $matches, PREG_SET_ORDER);
  1083. foreach ($matches as $match) {
  1084. $temp = array(
  1085. 'column' => strtolower($match[1]),
  1086. 'foreign_table' => strtolower($match[2]),
  1087. 'foreign_column' => strtolower($match[3]),
  1088. 'on_delete' => 'no_action',
  1089. 'on_update' => 'no_action'
  1090. );
  1091. if (!empty($match[4])) {
  1092. $temp['on_delete'] = strtolower(str_replace(' ', '_', $match[4]));
  1093. }
  1094. if (!empty($match[5])) {
  1095. $temp['on_update'] = strtolower(str_replace(' ', '_', $match[5]));
  1096. }
  1097. $keys[$table]['foreign'][] = $temp;
  1098. }
  1099. }
  1100. return $keys;
  1101. }
  1102. /**
  1103. * Gets the column info from an Oracle database
  1104. *
  1105. * @param string $table The table to fetch the column info for
  1106. * @return array The column info for the table specified - see ::getColumnInfo() for details
  1107. */
  1108. private function fetchOracleColumnInfo($table)
  1109. {
  1110. $table = strtolower($table);
  1111. $schema = strtolower($this->database->getUsername());
  1112. if (strpos($table, '.') !== FALSE) {
  1113. list ($schema, $table) = explode('.', $table);
  1114. }
  1115. $column_info = array();
  1116. $data_type_mapping = array(
  1117. 'boolean' => 'boolean',
  1118. 'number' => 'integer',
  1119. 'integer' => 'integer',
  1120. 'timestamp' => 'timestamp',
  1121. 'date' => 'date',
  1122. 'varchar2' => 'varchar',
  1123. 'nvarchar2' => 'varchar',
  1124. 'char' => 'char',
  1125. 'nchar' => 'char',
  1126. 'float' => 'float',
  1127. 'binary_float' => 'float',
  1128. 'binary_double' => 'float',
  1129. 'blob' => 'blob',
  1130. 'bfile' => 'varchar',
  1131. 'clob' => 'text',
  1132. 'nclob' => 'text'
  1133. );
  1134. $sql = "SELECT
  1135. LOWER(ATC.COLUMN_NAME) COLUMN_NAME,
  1136. CASE
  1137. WHEN
  1138. ATC.DATA_TYPE = 'NUMBER' AND
  1139. ATC.DATA_PRECISION IS NULL AND
  1140. ATC.DATA_SCALE = 0
  1141. THEN
  1142. 'integer'
  1143. WHEN
  1144. ATC.DATA_TYPE = 'NUMBER' AND
  1145. ATC.DATA_PRECISION = 1 AND
  1146. ATC.DATA_SCALE = 0
  1147. THEN
  1148. 'boolean'
  1149. WHEN
  1150. ATC.DATA_TYPE = 'NUMBER' AND
  1151. ATC.DATA_PRECISION IS NOT NULL AND
  1152. ATC.DATA_SCALE != 0 AND
  1153. ATC.DATA_SCALE IS NOT NULL
  1154. THEN
  1155. 'float'
  1156. ELSE
  1157. LOWER(ATC.DATA_TYPE)
  1158. END DATA_TYPE,
  1159. CASE
  1160. WHEN
  1161. ATC.CHAR_LENGTH <> 0
  1162. THEN
  1163. ATC.CHAR_LENGTH
  1164. WHEN
  1165. ATC.DATA_TYPE = 'NUMBER' AND
  1166. ATC.DATA_PRECISION != 1 AND
  1167. ATC.DATA_SCALE != 0 AND
  1168. ATC.DATA_PRECISION IS NOT NULL
  1169. THEN
  1170. ATC.DATA_SCALE
  1171. ELSE
  1172. NULL
  1173. END LENGTH,
  1174. ATC.DATA_PRECISION PRECISION,
  1175. ATC.NULLABLE,
  1176. ATC.DATA_DEFAULT,
  1177. AC.SEARCH_CONDITION CHECK_CONSTRAINT,
  1178. ACCM.COMMENTS
  1179. FROM
  1180. ALL_TAB_COLUMNS ATC LEFT JOIN
  1181. ALL_CONS_COLUMNS ACC ON
  1182. ATC.OWNER = ACC.OWNER AND
  1183. ATC.COLUMN_NAME = ACC.COLUMN_NAME AND
  1184. ATC.TABLE_NAME = ACC.TABLE_NAME AND
  1185. ACC.POSITION IS NULL LEFT JOIN
  1186. ALL_CONSTRAINTS AC ON
  1187. AC.OWNER = ACC.OWNER AND
  1188. AC.CONSTRAINT_NAME = ACC.CONSTRAINT_NAME AND
  1189. AC.CONSTRAINT_TYPE = 'C' AND
  1190. AC.STATUS = 'ENABLED' LEFT JOIN
  1191. ALL_COL_COMMENTS ACCM ON
  1192. ATC.OWNER = ACCM.OWNER AND
  1193. ATC.COLUMN_NAME = ACCM.COLUMN_NAME AND
  1194. ATC.TABLE_NAME = ACCM.TABLE_NAME
  1195. WHERE
  1196. LOWER(ATC.TABLE_NAME) = %s AND
  1197. LOWER(ATC.OWNER) = %s
  1198. ORDER BY
  1199. ATC.TABLE_NAME ASC,
  1200. ATC.COLUMN_ID ASC";
  1201. $result = $this->database->query($sql, $table, $schema);
  1202. foreach ($result as $row) {
  1203. $column = $row['column_name'];
  1204. // Since Oracle stores check constraints in LONG columns, it is
  1205. // not possible to check or modify the constraints in SQL which
  1206. // ends up causing multiple rows with duplicate data except for
  1207. // the check constraint
  1208. $duplicate = FALSE;
  1209. if (isset($column_info[$column])) {
  1210. $info = $column_info[$column];
  1211. $duplicate = TRUE;
  1212. } else {
  1213. $info = array();
  1214. }
  1215. if (!$duplicate) {
  1216. // Get the column type
  1217. foreach ($data_type_mapping as $data_type => $mapped_data_type) {
  1218. if (stripos($row['data_type'], $data_type) === 0) {
  1219. $info['type'] = $mapped_data_type;
  1220. break;
  1221. }
  1222. }
  1223. if (!isset($info['type'])) {
  1224. $info['type'] = $row['data_type'];
  1225. }
  1226. if (in_array($info['type'], array('blob', 'text'))) {
  1227. $info['max_length'] = 4294967295;
  1228. }
  1229. if ($row['data_type'] == 'float' && $row['precision']) {
  1230. $row['length'] = (int) $row['length'];
  1231. $before_digits = str_pad('', $row['precision'] - $row['length'], '9');
  1232. $after_digits = str_pad('', $row['length'], '9');
  1233. $max_min = $before_digits . ($after_digits ? '.' : '') . $after_digits;
  1234. $info['min_value'] = new fNumber('-' . $max_min);
  1235. $info['max_value'] = new fNumber($max_min);
  1236. }
  1237. // Handle the length of decimal/numeric fields
  1238. if ($info['type'] == 'float' && $row['length']) {
  1239. $info['decimal_places'] = (int) $row['length'];
  1240. }
  1241. // Handle the special data for varchar fields
  1242. if (in_array($info['type'], array('char', 'varchar'))) {
  1243. $info['max_length'] = (int) $row['length'];
  1244. }
  1245. }
  1246. // Handle check constraints that are just simple lists
  1247. if (in_array($info['type'], array('varchar', 'char')) && $row['check_constraint']) {
  1248. if (preg_match('/^\s*"?' . preg_quote($column, '/') . '"?\s+in\s+\((.*?)\)\s*$/i', $row['check_constraint'], $match)) {
  1249. if (preg_match_all("/(?<!')'((''|[^']+)*)'/", $match[1], $matches, PREG_PATTERN_ORDER)) {
  1250. $info['valid_values'] = str_replace("''", "'", $matches[1]);
  1251. }
  1252. } elseif (preg_match('/^\s*"?' . preg_quote($column, '/') . '"?\s*=\s*\'((\'\'|[^\']+)*)\'(\s+OR\s+"?' . preg_quote($column, '/') . '"?\s*=\s*\'((\'\'|[^\']+)*)\')*\s*$/i', $row['check_constraint'], $match)) {
  1253. if (preg_match_all("/(?<!')'((''|[^']+)*)'/", $row['check_constraint'], $matches, PREG_PATTERN_ORDER)) {
  1254. $info['valid_values'] = str_replace("''", "'", $matches[1]);
  1255. }
  1256. }
  1257. }
  1258. if (!$duplicate) {
  1259. // Handle default values
  1260. if ($row['data_default'] !== NULL && trim($row['data_default']) != 'NULL') {
  1261. if (in_array($info['type'], array('date', 'time', 'timestamp')) && $row['data_default'][0] != "'") {
  1262. $info['default'] = trim(preg_replace('#^SYS#', 'CURRENT_', $row['data_default']));
  1263. } elseif (in_array($info['type'], array('char', 'varchar', 'text', 'date', 'time', 'timestamp'))) {
  1264. $info['default'] = str_replace("''", "'", substr(trim($row['data_default']), 1, -1));
  1265. } elseif ($info['type'] == 'boolean') {
  1266. $info['default'] = (boolean) trim($row['data_default']);
  1267. } elseif (in_array($info['type'], array('integer', 'float'))) {
  1268. $info['default'] = trim($row['data_default']);
  1269. } else {
  1270. $info['default'] = $row['data_default'];
  1271. }
  1272. }
  1273. // Not null values
  1274. $info['not_null'] = ($row['nullable'] == 'N') ? TRUE : FALSE;
  1275. $info['comment'] = $row['comments'];
  1276. }
  1277. $column_info[$column] = $info;
  1278. }
  1279. $sql = "SELECT
  1280. TRIGGER_BODY
  1281. FROM
  1282. ALL_TRIGGERS
  1283. WHERE
  1284. TRIGGERING_EVENT LIKE 'INSERT%' AND
  1285. STATUS = 'ENABLED' AND
  1286. TRIGGER_NAME NOT LIKE 'BIN\$%' AND
  1287. LOWER(TABLE_NAME) = %s AND
  1288. LOWER(OWNER) = %s";
  1289. foreach ($this->database->query($sql, $table, $schema) as $row) {
  1290. if (preg_match('#SELECT\s+(["\w.]+).nextval\s+INTO\s+:new\.(\w+)\s+FROM\s+dual#i', $row['trigger_body'], $matches)) {
  1291. $column = strtolower($matches[2]);
  1292. $column_info[$column]['auto_increment'] = TRUE;
  1293. }
  1294. }
  1295. return $column_info;
  1296. }
  1297. /**
  1298. * Fetches the key info for an Oracle database
  1299. *
  1300. * @return array The keys arrays for every table in the database - see ::getKeys() for details
  1301. */
  1302. private function fetchOracleKeys()
  1303. {
  1304. $keys = array();
  1305. $default_schema = strtolower($this->database->getUsername());
  1306. $tables = $this->getTables();
  1307. foreach ($tables as $table) {
  1308. $keys[$table] = array();
  1309. $keys[$table]['primary'] = array();
  1310. $keys[$table]['unique'] = array();
  1311. $keys[$table]['foreign'] = array();
  1312. }
  1313. $params = array();
  1314. $sql = "SELECT
  1315. LOWER(AC.OWNER) \"SCHEMA\",
  1316. LOWER(AC.TABLE_NAME) \"TABLE\",
  1317. AC.CONSTRAINT_NAME CONSTRAINT_NAME,
  1318. CASE AC.CONSTRAINT_TYPE
  1319. WHEN 'P' THEN 'primary'
  1320. WHEN 'R' THEN 'foreign'
  1321. WHEN 'U' THEN 'unique'
  1322. END TYPE,
  1323. LOWER(ACC.COLUMN_NAME) \"COLUMN\",
  1324. LOWER(FKC.OWNER) FOREIGN_SCHEMA,
  1325. LOWER(FKC.TABLE_NAME) FOREIGN_TABLE,
  1326. LOWER(FKC.COLUMN_NAME) FOREIGN_COLUMN,
  1327. CASE WHEN FKC.TABLE_NAME IS NOT NULL THEN REPLACE(LOWER(AC.DELETE_RULE), ' ', '_') ELSE NULL END ON_DELETE
  1328. FROM
  1329. ALL_CONSTRAINTS AC INNER JOIN
  1330. ALL_CONS_COLUMNS ACC ON AC.CONSTRAINT_NAME = ACC.CONSTRAINT_NAME AND AC.OWNER = ACC.OWNER LEFT JOIN
  1331. ALL_CONSTRAINTS FK ON AC.R_CONSTRAINT_NAME = FK.CONSTRAINT_NAME AND AC.OWNER = FK.OWNER LEFT JOIN
  1332. ALL_CONS_COLUMNS FKC ON FK.CONSTRAINT_NAME = FKC.CONSTRAINT_NAME AND FK.OWNER = FKC.OWNER
  1333. WHERE
  1334. AC.CONSTRAINT_TYPE IN ('U', 'P', 'R') AND ";
  1335. $conditions = array();
  1336. foreach ($tables as $table) {
  1337. if (strpos($table, '.') === FALSE) {
  1338. $table = $default_schema . '.' . $table;
  1339. }
  1340. list ($schema, $table) = explode('.', strtolower($table));
  1341. $conditions[] = "LOWER(AC.OWNER) = %s AND LOWER(AC.TABLE_NAME) = %s";
  1342. $params[] = $schema;
  1343. $params[] = $table;
  1344. }
  1345. $sql .= '((' . join(') OR( ', $conditions) . '))';
  1346. $sql .= " ORDER BY
  1347. AC.OWNER ASC,
  1348. AC.TABLE_NAME ASC,
  1349. AC.CONSTRAINT_TYPE ASC,
  1350. AC.CONSTRAINT_NAME ASC,
  1351. ACC.POSITION ASC";
  1352. $result = $this->database->query($sql, $params);
  1353. $last_name = '';
  1354. $last_table = '';
  1355. $last_type = '';
  1356. foreach ($result as $row) {
  1357. if ($row['constraint_name'] != $last_name) {
  1358. if ($last_name) {
  1359. if ($last_type == 'foreign' || $last_type == 'unique') {
  1360. $keys[$last_table][$last_type][] = $temp;
  1361. } else {
  1362. $keys[$last_table][$last_type] = $temp;
  1363. }
  1364. }
  1365. $temp = array();
  1366. if ($row['type'] == 'foreign') {
  1367. $temp['column'] = $row['column'];
  1368. $temp['foreign_table'] = $row['foreign_table'];
  1369. if ($row['foreign_schema'] != $default_schema) {
  1370. $temp['foreign_table'] = $row['foreign_schema'] . '.' . $temp['foreign_table'];
  1371. }
  1372. $temp['foreign_column'] = $row['foreign_column'];
  1373. $temp['on_delete'] = 'no_action';
  1374. $temp['on_update'] = 'no_action';
  1375. if (!empty($row['on_delete'])) {
  1376. $temp['on_delete'] = $row['on_delete'];
  1377. }
  1378. } else {
  1379. $temp[] = $row['column'];
  1380. }
  1381. $last_table = $row['table'];
  1382. if ($row['schema'] != $default_schema) {
  1383. $last_table = $row['schema'] . '.' . $last_table;
  1384. }
  1385. $last_name = $row['constraint_name'];
  1386. $last_type = $row['type'];
  1387. } else {
  1388. $temp[] = $row['column'];
  1389. }
  1390. }
  1391. if (isset($temp)) {
  1392. if ($last_type == 'foreign' || $last_type == 'unique') {
  1393. $keys[$last_table][$last_type][] = $temp;
  1394. } else {
  1395. $keys[$last_table][$last_type] = $temp;
  1396. }
  1397. }
  1398. return $keys;
  1399. }
  1400. /**
  1401. * Gets the column info from a PostgreSQL database
  1402. *
  1403. * @param string $table The table to fetch the column info for
  1404. * @return array The column info for the table specified - see ::getColumnInfo() for details
  1405. */
  1406. private function fetchPostgreSQLColumnInfo($table)
  1407. {
  1408. $column_info = array();
  1409. $schema = 'public';
  1410. if (strpos($table, '.') !== FALSE) {
  1411. list ($schema, $table) = explode('.', $table);
  1412. }
  1413. $data_type_mapping = array(
  1414. 'boolean' => 'boolean',
  1415. 'smallint' => 'integer',
  1416. 'int' => 'integer',
  1417. 'bigint' => 'integer',
  1418. 'serial' => 'integer',
  1419. 'bigserial' => 'integer',
  1420. 'timestamp' => 'timestamp',
  1421. 'date' => 'date',
  1422. 'time' => 'time',
  1423. 'uuid' => 'varchar',
  1424. 'character varying' => 'varchar',
  1425. 'character' => 'char',
  1426. 'real' => 'float',
  1427. 'double' => 'float',
  1428. 'numeric' => 'float',
  1429. 'bytea' => 'blob',
  1430. 'text' => 'text',
  1431. 'mediumtext' => 'text',
  1432. 'longtext' => 'text',
  1433. 'point' => 'varchar',
  1434. 'line' => 'varchar',
  1435. 'lseg' => 'varchar',
  1436. 'box' => 'varchar',
  1437. 'path' => 'varchar',
  1438. 'polygon' => 'varchar',
  1439. 'circle' => 'varchar'
  1440. );
  1441. $max_min_values = array(
  1442. 'smallint' => array('min' => new fNumber(-32768), 'max' => new fNumber(32767)),
  1443. 'int' => array('min' => new fNumber(-2147483648), 'max' => new fNumber(2147483647)),
  1444. 'bigint' => array('min' => new fNumber('-9223372036854775808'), 'max' => new fNumber('9223372036854775807')),
  1445. 'serial' => array('min' => new fNumber(-2147483648), 'max' => new fNumber(2147483647)),
  1446. 'bigserial' => array('min' => new fNumber('-9223372036854775808'), 'max' => new fNumber('9223372036854775807'))
  1447. );
  1448. // PgSQL required this complicated SQL to get the column info
  1449. $sql = "SELECT
  1450. LOWER(pg_attribute.attname) AS column,
  1451. format_type(pg_attribute.atttypid, pg_attribute.atttypmod) AS data_type,
  1452. pg_attribute.attnotnull AS not_null,
  1453. pg_attrdef.adsrc AS default,
  1454. pg_get_constraintdef(pg_constraint.oid) AS constraint,
  1455. col_description(pg_class.oid, pg_attribute.attnum) AS comment
  1456. FROM
  1457. pg_attribute LEFT JOIN
  1458. pg_class ON pg_attribute.attrelid = pg_class.oid LEFT JOIN
  1459. pg_namespace ON pg_class.relnamespace = pg_namespace.oid LEFT JOIN
  1460. pg_type ON pg_type.oid = pg_attribute.atttypid LEFT JOIN
  1461. pg_constraint ON pg_constraint.conrelid = pg_class.oid AND
  1462. pg_attribute.attnum = ANY (pg_constraint.conkey) AND
  1463. pg_constraint.contype = 'c' LEFT JOIN
  1464. pg_attrdef ON pg_class.oid = pg_attrdef.adrelid AND
  1465. pg_attribute.attnum = pg_attrdef.adnum
  1466. WHERE
  1467. NOT pg_attribute.attisdropped AND
  1468. LOWER(pg_class.relname) = %s AND
  1469. LOWER(pg_namespace.nspname) = %s AND
  1470. pg_type.typname NOT IN ('oid', 'cid', 'xid', 'cid', 'xid', 'tid')
  1471. ORDER BY
  1472. pg_attribute.attnum,
  1473. pg_constraint.contype";
  1474. $result = $this->database->query($sql, strtolower($table), strtolower($schema));
  1475. foreach ($result as $row) {
  1476. $info = array();
  1477. // Get the column type
  1478. preg_match('#([\w ]+)\s*(?:\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\))?#', $row['data_type'], $column_data_type);
  1479. foreach ($data_type_mapping as $data_type => $mapped_data_type) {
  1480. if (stripos($column_data_type[1], $data_type) === 0) {
  1481. $info['type'] = $mapped_data_type;
  1482. if (isset($max_min_values[$data_type])) {
  1483. $info['min_value'] = $max_min_values[$data_type]['min'];
  1484. $info['max_value'] = $max_min_values[$data_type]['max'];
  1485. }
  1486. break;
  1487. }
  1488. }
  1489. if (!isset($info['type'])) {
  1490. $info['type'] = $column_data_type[1];
  1491. }
  1492. if ($info['type'] == 'blob' || $info['type'] == 'text') {
  1493. $info['max_length'] = 1073741824;
  1494. }
  1495. // Handle the length of decimal/numeric fields
  1496. if ($info['type'] == 'float' && isset($column_data_type[3]) && strlen($column_data_type[3]) > 0) {
  1497. $info['decimal_places'] = (int) $column_data_type[3];
  1498. $before_digits = str_pad('', $column_data_type[2] - $info['decimal_places'], '9');
  1499. $after_digits = str_pad('', $info['decimal_places'], '9');
  1500. $max_min = $before_digits . ($after_digits ? '.' : '') . $after_digits;
  1501. $info['min_value'] = new fNumber('-' . $max_min);
  1502. $info['max_value'] = new fNumber($max_min);
  1503. }
  1504. // Handle the special data for varchar fields
  1505. if (in_array($info['type'], array('char', 'varchar'))) {
  1506. if (!empty($column_data_type[2])) {
  1507. $info['max_length'] = $column_data_type[2];
  1508. } else {
  1509. $info['max_length'] = 1073741824;
  1510. }
  1511. }
  1512. // In PostgreSQL, a UUID can be the 32 digits, 32 digits plus 4 hyphens or 32 digits plus 4 hyphens and 2 curly braces
  1513. if ($row['data_type'] == 'uuid') {
  1514. $info['max_length'] = 38;
  1515. }
  1516. // Handle check constraints that are just simple lists
  1517. if (in_array($info['type'], array('varchar', 'char')) && !empty($row['constraint'])) {
  1518. if (preg_match('/CHECK[\( "]+' . $row['column'] . '[a-z\) ":]+\s+=\s+/i', $row['constraint'])) {
  1519. if (preg_match_all("/(?!').'((''|[^']+)*)'/", $row['constraint'], $matches, PREG_PATTERN_ORDER)) {
  1520. $info['valid_values'] = str_replace("''", "'", $matches[1]);
  1521. }
  1522. }
  1523. }
  1524. // Handle default values and serial data types
  1525. if ($info['type'] == 'integer' && stripos($row['default'], 'nextval(') !== FALSE) {
  1526. $info['auto_increment'] = TRUE;
  1527. } elseif ($row['default'] !== NULL) {
  1528. if (preg_match('#^NULL::[\w\s]+$#', $row['default'])) {
  1529. $info['default'] = NULL;
  1530. } elseif ($row['default'] == 'now()') {
  1531. $info['default'] = 'CURRENT_TIMESTAMP';
  1532. } elseif ($row['default'] == "('now'::text)::date") {
  1533. $info['default'] = 'CURRENT_DATE';
  1534. } elseif ($row['default'] == "('now'::text)::time with time zone") {
  1535. $info['default'] = 'CURRENT_TIME';
  1536. } else {
  1537. $info['default'] = str_replace("''", "'", preg_replace("/^'(.*)'::[a-z ]+\$/iD", '\1', $row['default']));
  1538. if ($info['type'] == 'boolean') {
  1539. $info['default'] = ($info['default'] == 'false' || !$info['default']) ? FALSE : TRUE;
  1540. }
  1541. }
  1542. }
  1543. // Not null values
  1544. $info['not_null'] = ($row['not_null'] == 't') ? TRUE : FALSE;
  1545. $info['comment'] = $row['comment'];
  1546. $column_info[$row['column']] = $info;
  1547. }
  1548. return $column_info;
  1549. }
  1550. /**
  1551. * Fetches the key info for a PostgreSQL database
  1552. *
  1553. * @return array The keys arrays for every table in the database - see ::getKeys() for details
  1554. */
  1555. private function fetchPostgreSQLKeys()
  1556. {
  1557. $keys = array();
  1558. $tables = $this->getTables();
  1559. foreach ($tables as $table) {
  1560. $keys[$table] = array();
  1561. $keys[$table]['primary'] = array();
  1562. $keys[$table]['unique'] = array();
  1563. $keys[$table]['foreign'] = array();
  1564. }
  1565. $sql = "(
  1566. SELECT
  1567. LOWER(s.nspname) AS \"schema\",
  1568. LOWER(t.relname) AS \"table\",
  1569. con.conname AS constraint_name,
  1570. CASE con.contype
  1571. WHEN 'f' THEN 'foreign'
  1572. WHEN 'p' THEN 'primary'
  1573. WHEN 'u' THEN 'unique'
  1574. END AS type,
  1575. LOWER(col.attname) AS column,
  1576. LOWER(fs.nspname) AS foreign_schema,
  1577. LOWER(ft.relname) AS foreign_table,
  1578. LOWER(fc.attname) AS foreign_column,
  1579. CASE con.confdeltype
  1580. WHEN 'c' THEN 'cascade'
  1581. WHEN 'a' THEN 'no_action'
  1582. WHEN 'r' THEN 'restrict'
  1583. WHEN 'n' THEN 'set_null'
  1584. WHEN 'd' THEN 'set_default'
  1585. END AS on_delete,
  1586. CASE con.confupdtype
  1587. WHEN 'c' THEN 'cascade'
  1588. WHEN 'a' THEN 'no_action'
  1589. WHEN 'r' THEN 'restrict'
  1590. WHEN 'n' THEN 'set_null'
  1591. WHEN 'd' THEN 'set_default'
  1592. END AS on_update,
  1593. CASE WHEN con.conkey IS NOT NULL THEN position('-'||col.attnum||'-' in '-'||array_to_string(con.conkey, '-')||'-') ELSE 0 END AS column_order
  1594. FROM
  1595. pg_attribute AS col INNER JOIN
  1596. pg_class AS t ON
  1597. col.attrelid = t.oid INNER JOIN
  1598. pg_namespace AS s ON
  1599. t.relnamespace = s.oid INNER JOIN
  1600. pg_constraint AS con ON
  1601. col.attnum = ANY (con.conkey) AND
  1602. con.conrelid = t.oid LEFT JOIN
  1603. pg_class AS ft ON
  1604. con.confrelid = ft.oid LEFT JOIN
  1605. pg_namespace AS fs ON
  1606. ft.relnamespace = fs.oid LEFT JOIN
  1607. pg_attribute AS fc ON
  1608. fc.attnum = ANY (con.confkey) AND
  1609. ft.oid = fc.attrelid
  1610. WHERE
  1611. NOT col.attisdropped AND
  1612. (con.contype = 'p' OR
  1613. con.contype = 'f' OR
  1614. con.contype = 'u')
  1615. ) UNION (
  1616. SELECT
  1617. LOWER(n.nspname) AS \"schema\",
  1618. LOWER(t.relname) AS \"table\",
  1619. ic.relname AS constraint_name,
  1620. 'unique' AS type,
  1621. LOWER(col.attname) AS column,
  1622. NULL AS foreign_schema,
  1623. NULL AS foreign_table,
  1624. NULL AS foreign_column,
  1625. NULL AS on_delete,
  1626. NULL AS on_update,
  1627. CASE WHEN ind.indkey IS NOT NULL THEN position('-'||col.attnum||'-' in '-'||array_to_string(ind.indkey, '-')||'-') ELSE 0 END AS column_order
  1628. FROM
  1629. pg_class AS t INNER JOIN
  1630. pg_index AS ind ON
  1631. ind.indrelid = t.oid INNER JOIN
  1632. pg_namespace AS n ON
  1633. t.relnamespace = n.oid INNER JOIN
  1634. pg_class AS ic ON
  1635. ind.indexrelid = ic.oid LEFT JOIN
  1636. pg_constraint AS con ON
  1637. con.conrelid = t.oid AND
  1638. con.contype = 'u' AND
  1639. con.conname = ic.relname INNER JOIN
  1640. pg_attribute AS col ON
  1641. col.attrelid = t.oid AND
  1642. col.attnum = ANY (ind.indkey)
  1643. WHERE
  1644. n.nspname NOT IN ('pg_catalog', 'pg_toast') AND
  1645. indisunique = TRUE AND
  1646. indisprimary = FALSE AND
  1647. con.oid IS NULL AND
  1648. 0 != ALL ((ind.indkey)::int[])
  1649. ) ORDER BY 1, 2, 4, 3, 11";
  1650. $result = $this->database->query($sql);
  1651. $last_name = '';
  1652. $last_table = '';
  1653. $last_type = '';
  1654. foreach ($result as $row) {
  1655. if ($row['constraint_name'] != $last_name) {
  1656. if ($last_name) {
  1657. if ($last_type == 'foreign' || $last_type == 'unique') {
  1658. $keys[$last_table][$last_type][] = $temp;
  1659. } else {
  1660. $keys[$last_table][$last_type] = $temp;
  1661. }
  1662. }
  1663. $temp = array();
  1664. if ($row['type'] == 'foreign') {
  1665. $temp['column'] = $row['column'];
  1666. $temp['foreign_table'] = $row['foreign_table'];
  1667. if ($row['foreign_schema'] != 'public') {
  1668. $temp['foreign_table'] = $row['foreign_schema'] . '.' . $temp['foreign_table'];
  1669. }
  1670. $temp['foreign_column'] = $row['foreign_column'];
  1671. $temp['on_delete'] = 'no_action';
  1672. $temp['on_update'] = 'no_action';
  1673. if (!empty($row['on_delete'])) {
  1674. $temp['on_delete'] = $row['on_delete'];
  1675. }
  1676. if (!empty($row['on_update'])) {
  1677. $temp['on_update'] = $row['on_update'];
  1678. }
  1679. } else {
  1680. $temp[] = $row['column'];
  1681. }
  1682. $last_table = $row['table'];
  1683. if ($row['schema'] != 'public') {
  1684. $last_table = $row['schema'] . '.' . $last_table;
  1685. }
  1686. $last_name = $row['constraint_name'];
  1687. $last_type = $row['type'];
  1688. } else {
  1689. $temp[] = $row['column'];
  1690. }
  1691. }
  1692. if (isset($temp)) {
  1693. if ($last_type == 'foreign' || $last_type == 'unique') {
  1694. $keys[$last_table][$last_type][] = $temp;
  1695. } else {
  1696. $keys[$last_table][$last_type] = $temp;
  1697. }
  1698. }
  1699. return $keys;
  1700. }
  1701. /**
  1702. * Gets the column info from a SQLite database
  1703. *
  1704. * @param string $table The table to fetch the column info for
  1705. * @return array The column info for the table specified - see ::getColumnInfo() for details
  1706. */
  1707. private function fetchSQLiteColumnInfo($table)
  1708. {
  1709. $column_info = array();
  1710. $data_type_mapping = array(
  1711. 'boolean' => 'boolean',
  1712. 'serial' => 'integer',
  1713. 'smallint' => 'integer',
  1714. 'int' => 'integer',
  1715. 'integer' => 'integer',
  1716. 'bigint' => 'integer',
  1717. 'timestamp' => 'timestamp',
  1718. 'date' => 'date',
  1719. 'time' => 'time',
  1720. 'varchar' => 'varchar',
  1721. 'char' => 'char',
  1722. 'real' => 'float',
  1723. 'numeric' => 'float',
  1724. 'float' => 'float',
  1725. 'double' => 'float',
  1726. 'decimal' => 'float',
  1727. 'blob' => 'blob',
  1728. 'text' => 'text'
  1729. );
  1730. $result = $this->database->query("SELECT sql FROM sqlite_master WHERE type = 'table' AND LOWER(name) = %s", strtolower($table));
  1731. try {
  1732. $row = $result->fetchRow();
  1733. $create_sql = $row['sql'];
  1734. } catch (fNoRowsException $e) {
  1735. return array();
  1736. }
  1737. preg_match_all('#(?<=,|\(|\*/|\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))?)?([ \t]*(?:/\*(?:(?!\*/).)*\*/))?\s*(?:,([ \t]*--[^\n]*\n)?|(--[^\n]*\n)?\s*(?:/\*(?:(?!\*/).)*\*/)?\s*(?=\)))#msi', $create_sql, $matches, PREG_SET_ORDER);
  1738. foreach ($matches as $match) {
  1739. $info = array();
  1740. foreach ($data_type_mapping as $data_type => $mapped_data_type) {
  1741. if (stripos($match[2], $data_type) === 0) {
  1742. $info['type'] = $mapped_data_type;
  1743. break;
  1744. }
  1745. }
  1746. // Type specific information
  1747. if (in_array($info['type'], array('char', 'varchar'))) {
  1748. if (!empty($match[3])) {
  1749. $info['max_length'] = $match[3];
  1750. } else {
  1751. $info['max_length'] = 1000000000;
  1752. }
  1753. }
  1754. if ($info['type'] == 'text' || $info['type'] == 'blob') {
  1755. $info['max_length'] = 1000000000;
  1756. }
  1757. // Figure out how many decimal places for a decimal
  1758. if (in_array(strtolower($match[2]), array('decimal', 'numeric')) && !empty($match[4])) {
  1759. $info['decimal_places'] = $match[4];
  1760. $before_digits = str_pad('', $match[3] - $match[4], '9');
  1761. $after_digits = str_pad('', $match[4], '9');
  1762. $max_min = $before_digits . ($after_digits ? '.' : '') . $after_digits;
  1763. $info['min_value'] = new fNumber('-' . $max_min);
  1764. $info['max_value'] = new fNumber($max_min);
  1765. }
  1766. // Not null
  1767. $info['not_null'] = (!empty($match[5]) || !empty($match[8])) ? TRUE : FALSE;
  1768. // Default values
  1769. if (isset($match[6]) && $match[6] != '' && $match[6] != 'NULL') {
  1770. $info['default'] = preg_replace("/^'|'\$/D", '', $match[6]);
  1771. }
  1772. if ($info['type'] == 'boolean' && isset($info['default'])) {
  1773. $info['default'] = ($info['default'] == 'f' || $info['default'] == 0 || $info['default'] == 'false') ? FALSE : TRUE;
  1774. }
  1775. // Check constraints
  1776. if (isset($match[9]) && preg_match('/CHECK\s*\(\s*"?' . $match[1] . '"?\s+IN\s+\(\s*((?:(?:[^, \']*|\'(?:\'\'|[^\']+)*\')\s*,\s*)*(?:[^, \']*|\'(?:\'\'|[^\']+)*\'))\s*\)/i', $match[9], $check_match)) {
  1777. $info['valid_values'] = str_replace("''", "'", preg_replace("/^'|'\$/D", '', preg_split("#\s*,\s*#", $check_match[1])));
  1778. }
  1779. // Auto increment fields
  1780. if (!empty($match[8]) && (stripos($match[8], 'autoincrement') !== FALSE || $info['type'] == 'integer')) {
  1781. $info['auto_increment'] = TRUE;
  1782. }
  1783. // Column comments
  1784. if (!empty($match[11]) || !empty($match[12]) || !empty($match[13])) {
  1785. if (!empty($match[11])) {
  1786. $comment = $match[11];
  1787. } elseif (!empty($match[12])) {
  1788. $comment = $match[12];
  1789. } else {
  1790. $comment = $match[13];
  1791. }
  1792. $comment = trim($comment);
  1793. $comment = substr($comment, 0, 2) == '--' ? substr($comment, 2) : substr($comment, 2, -2);
  1794. $info['comment'] = trim($comment);
  1795. }
  1796. $column_info[strtolower($match[1])] = $info;
  1797. }
  1798. return $column_info;
  1799. }
  1800. /**
  1801. * Fetches the key info for an SQLite database
  1802. *
  1803. * @return array The keys arrays for every table in the database - see ::getKeys() for details
  1804. */
  1805. private function fetchSQLiteKeys()
  1806. {
  1807. $tables = $this->getTables();
  1808. $keys = array();
  1809. foreach ($tables as $table) {
  1810. $keys[$table] = array();
  1811. $keys[$table]['primary'] = array();
  1812. $keys[$table]['foreign'] = array();
  1813. $keys[$table]['unique'] = array();
  1814. $result = $this->database->query("SELECT sql FROM sqlite_master WHERE type = 'table' AND LOWER(name) = %s", strtolower($table));
  1815. $row = $result->fetchRow();
  1816. $create_sql = $row['sql'];
  1817. // Collapse strings into empty string to make the matching simpler
  1818. $create_sql = preg_replace('#\'(?:\'\'|[^\']+)*\'#', "''", $create_sql);
  1819. // Remove single-line comments
  1820. $create_sql = preg_replace('#--[^\n]*\n#', "\n", $create_sql);
  1821. // Remove multi-line comments
  1822. $create_sql = preg_replace('#/\*((?!\*/).)*\*/#', '', $create_sql);
  1823. // Get column level key definitions
  1824. 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|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*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER);
  1825. foreach ($matches as $match) {
  1826. if (!empty($match[2])) {
  1827. $keys[$table]['unique'][] = array(strtolower($match[1]));
  1828. }
  1829. if (!empty($match[3])) {
  1830. $keys[$table]['primary'] = array(strtolower($match[1]));
  1831. }
  1832. if (!empty($match[4])) {
  1833. $temp = array(
  1834. 'column' => strtolower($match[1]),
  1835. 'foreign_table' => strtolower($match[5]),
  1836. 'foreign_column' => strtolower($match[6]),
  1837. 'on_delete' => 'no_action',
  1838. 'on_update' => 'no_action'
  1839. );
  1840. if (isset($match[7])) {
  1841. $temp['on_delete'] = strtolower(str_replace(' ', '_', $match[7]));
  1842. }
  1843. if (isset($match[8])) {
  1844. $temp['on_update'] = strtolower(str_replace(' ', '_', $match[8]));
  1845. }
  1846. $keys[$table]['foreign'][] = $temp;
  1847. }
  1848. }
  1849. // Get table level primary key definitions
  1850. preg_match_all('#(?<=,|\()\s*(?:CONSTRAINT\s+["`\[]?\w+["`\]]?\s+)?PRIMARY\s+KEY\s*\(\s*((?:\s*["`\[]?\w+["`\]]?\s*,\s*)*["`\[]?\w+["`\]]?)\s*\)\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER);
  1851. foreach ($matches as $match) {
  1852. $columns = preg_split('#\s*,\s*#', strtolower($match[1]));
  1853. foreach ($columns as $column) {
  1854. $keys[$table]['primary'][] = str_replace(array('[', '"', '`', ']'), '', $column);
  1855. }
  1856. }
  1857. // Get table level foreign key definitions
  1858. preg_match_all('#(?<=,|\()\s*(?:CONSTRAINT\s+["`\[]?\w+["`\]]?\s+)?FOREIGN\s+KEY\s*(?:["`\[]?(\w+)["`\]]?|\(\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*(?:,|\s*(?=\)))#mis', $create_sql, $matches, PREG_SET_ORDER);
  1859. foreach ($matches as $match) {
  1860. if (empty($match[1])) { $match[1] = $match[2]; }
  1861. $temp = array(
  1862. 'column' => strtolower($match[1]),
  1863. 'foreign_table' => strtolower($match[3]),
  1864. 'foreign_column' => strtolower($match[4]),
  1865. 'on_delete' => 'no_action',
  1866. 'on_update' => 'no_action'
  1867. );
  1868. if (isset($match[5])) {
  1869. $temp['on_delete'] = strtolower(str_replace(' ', '_', $match[5]));
  1870. }
  1871. if (isset($match[6])) {
  1872. $temp['on_update'] = strtolower(str_replace(' ', '_', $match[6]));
  1873. }
  1874. $keys[$table]['foreign'][] = $temp;
  1875. }
  1876. // Get table level unique key definitions
  1877. preg_match_all('#(?<=,|\()\s*(?:CONSTRAINT\s+["`\[]?\w+["`\]]?\s+)?UNIQUE\s*\(\s*((?:\s*["`\[]?\w+["`\]]?\s*,\s*)*["`\[]?\w+["`\]]?)\s*\)\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER);
  1878. foreach ($matches as $match) {
  1879. $columns = preg_split('#\s*,\s*#', strtolower($match[1]));
  1880. $key = array();
  1881. foreach ($columns as $column) {
  1882. $key[] = str_replace(array('[', '"', '`', ']'), '', $column);
  1883. }
  1884. $keys[$table]['unique'][] = $key;
  1885. }
  1886. // Get all CREATE UNIQUE INDEX statements
  1887. $result = $this->database->query("SELECT sql FROM sqlite_master WHERE type = 'index' AND sql <> '' AND LOWER(tbl_name) = %s", strtolower($table));
  1888. foreach ($result as $row) {
  1889. $create_sql = $row['sql'];
  1890. if (!preg_match('#^\s*CREATE\s+UNIQUE\s+INDEX\s+(?:["`\[]?\w+["`\]]?\.)?["`\[]?\w+["`\]]?\s+ON\s+[\'"`\[]?\w+[\'"`\]]?\s*\(\s*((?:\s*["`\[]?\w+["`\]]?\s*,\s*)*["`\[]?\w+["`\]]?)\s*\)\s*$#Di', $create_sql, $match)) {
  1891. continue;
  1892. }
  1893. $columns = preg_split('#\s*,\s*#', strtolower($match[1]));
  1894. $key = array();
  1895. foreach ($columns as $column) {
  1896. $key[] = str_replace(array('[', '"', '`', ']'), '', $column);
  1897. }
  1898. $keys[$table]['unique'][] = $key;
  1899. }
  1900. }
  1901. return $keys;
  1902. }
  1903. /**
  1904. * Finds many-to-many relationship for the table specified
  1905. *
  1906. * @param string $table The table to find the relationships on
  1907. * @return void
  1908. */
  1909. private function findManyToManyRelationships($table)
  1910. {
  1911. if (!$this->isJoiningTable($table)) {
  1912. return;
  1913. }
  1914. list ($key1, $key2) = $this->merged_keys[$table]['foreign'];
  1915. $temp = array();
  1916. $temp['table'] = $key1['foreign_table'];
  1917. $temp['column'] = $key1['foreign_column'];
  1918. $temp['related_table'] = $key2['foreign_table'];
  1919. $temp['related_column'] = $key2['foreign_column'];
  1920. $temp['join_table'] = $table;
  1921. $temp['join_column'] = $key1['column'];
  1922. $temp['join_related_column'] = $key2['column'];
  1923. $temp['on_update'] = $key1['on_update'];
  1924. $temp['on_delete'] = $key1['on_delete'];
  1925. $this->relationships[$key1['foreign_table']]['many-to-many'][] = $temp;
  1926. $temp = array();
  1927. $temp['table'] = $key2['foreign_table'];
  1928. $temp['column'] = $key2['foreign_column'];
  1929. $temp['related_table'] = $key1['foreign_table'];
  1930. $temp['related_column'] = $key1['foreign_column'];
  1931. $temp['join_table'] = $table;
  1932. $temp['join_column'] = $key2['column'];
  1933. $temp['join_related_column'] = $key1['column'];
  1934. $temp['on_update'] = $key2['on_update'];
  1935. $temp['on_delete'] = $key2['on_delete'];
  1936. $this->relationships[$key2['foreign_table']]['many-to-many'][] = $temp;
  1937. }
  1938. /**
  1939. * Finds one-to-many relationship for the table specified
  1940. *
  1941. * @param string $table The table to find the relationships on
  1942. * @return void
  1943. */
  1944. private function findOneToManyRelationships($table)
  1945. {
  1946. foreach ($this->merged_keys[$table]['foreign'] as $key) {
  1947. $type = ($this->checkForSingleColumnUniqueKey($table, $key['column'])) ? 'one-to-one' : 'one-to-many';
  1948. $temp = array();
  1949. $temp['table'] = $key['foreign_table'];
  1950. $temp['column'] = $key['foreign_column'];
  1951. $temp['related_table'] = $table;
  1952. $temp['related_column'] = $key['column'];
  1953. $temp['on_delete'] = $key['on_delete'];
  1954. $temp['on_update'] = $key['on_update'];
  1955. $this->relationships[$key['foreign_table']][$type][] = $temp;
  1956. }
  1957. }
  1958. /**
  1959. * Finds one-to-one and many-to-one relationship for the table specified
  1960. *
  1961. * @param string $table The table to find the relationships on
  1962. * @return void
  1963. */
  1964. private function findStarToOneRelationships($table)
  1965. {
  1966. foreach ($this->merged_keys[$table]['foreign'] as $key) {
  1967. $temp = array();
  1968. $temp['table'] = $table;
  1969. $temp['column'] = $key['column'];
  1970. $temp['related_table'] = $key['foreign_table'];
  1971. $temp['related_column'] = $key['foreign_column'];
  1972. $type = ($this->checkForSingleColumnUniqueKey($table, $key['column'])) ? 'one-to-one' : 'many-to-one';
  1973. if ($type == 'one-to-one') {
  1974. $temp['on_delete'] = $key['on_delete'];
  1975. $temp['on_update'] = $key['on_update'];
  1976. }
  1977. $this->relationships[$table][$type][] = $temp;
  1978. }
  1979. }
  1980. /**
  1981. * Finds the one-to-one, many-to-one, one-to-many and many-to-many relationships in the database
  1982. *
  1983. * @return void
  1984. */
  1985. private function findRelationships()
  1986. {
  1987. $this->relationships = array();
  1988. $tables = $this->getTables();
  1989. foreach ($tables as $table) {
  1990. $this->relationships[$table]['one-to-one'] = array();
  1991. $this->relationships[$table]['many-to-one'] = array();
  1992. $this->relationships[$table]['one-to-many'] = array();
  1993. $this->relationships[$table]['many-to-many'] = array();
  1994. }
  1995. // Calculate the relationships
  1996. foreach ($this->merged_keys as $table => $keys) {
  1997. $this->findManyToManyRelationships($table);
  1998. if ($this->isJoiningTable($table)) {
  1999. continue;
  2000. }
  2001. $this->findStarToOneRelationships($table);
  2002. $this->findOneToManyRelationships($table);
  2003. }
  2004. if ($this->cache) {
  2005. $this->cache->set($this->makeCachePrefix() . 'relationships', $this->relationships);
  2006. }
  2007. }
  2008. /**
  2009. * Returns column information for the table specified
  2010. *
  2011. * If only a table is specified, column info is in the following format:
  2012. *
  2013. * {{{
  2014. * array(
  2015. * (string) {column name} => array(
  2016. * 'type' => (string) {data type},
  2017. * 'placeholder' => (string) {fDatabase::escape() placeholder for this data type},
  2018. * 'not_null' => (boolean) {if value can't be null},
  2019. * 'default' => (mixed) {the default value},
  2020. * 'valid_values' => (array) {the valid values for a varchar field},
  2021. * 'max_length' => (integer) {the maximum length in a varchar field},
  2022. * 'min_value' => (numeric) {the minimum value for an integer/float field},
  2023. * 'max_value' => (numeric) {the maximum value for an integer/float field},
  2024. * 'decimal_places' => (integer) {the number of decimal places for a decimal/numeric/money/smallmoney field},
  2025. * 'auto_increment' => (boolean) {if the integer primary key column is a serial/autoincrement/auto_increment/indentity column},
  2026. * 'comment' => (string) {the SQL comment/description for the column}
  2027. * ), ...
  2028. * )
  2029. * }}}
  2030. *
  2031. * If a table and column are specified, column info is in the following format:
  2032. *
  2033. * {{{
  2034. * array(
  2035. * 'type' => (string) {data type},
  2036. * 'placeholder' => (string) {fDatabase::escape() placeholder for this data type},
  2037. * 'not_null' => (boolean) {if value can't be null},
  2038. * 'default' => (mixed) {the default value-may contain special strings CURRENT_TIMESTAMP, CURRENT_TIME or CURRENT_DATE},
  2039. * 'valid_values' => (array) {the valid values for a varchar field},
  2040. * 'max_length' => (integer) {the maximum length in a char/varchar field},
  2041. * 'min_value' => (fNumber) {the minimum value for an integer/float field},
  2042. * 'max_value' => (fNumber) {the maximum value for an integer/float field},
  2043. * 'decimal_places' => (integer) {the number of decimal places for a decimal/numeric/money/smallmoney field},
  2044. * 'auto_increment' => (boolean) {if the integer primary key column is a serial/autoincrement/auto_increment/indentity column},
  2045. * 'comment' => (string) {the SQL comment/description for the column}
  2046. * )
  2047. * }}}
  2048. *
  2049. * If a table, column and element are specified, returned value is the single element specified.
  2050. *
  2051. * The `'type'` element is homogenized to a value from the following list:
  2052. *
  2053. * - `'varchar'`
  2054. * - `'char'`
  2055. * - `'text'`
  2056. * - `'integer'`
  2057. * - `'float'`
  2058. * - `'timestamp'`
  2059. * - `'date'`
  2060. * - `'time'`
  2061. * - `'boolean'`
  2062. * - `'blob'`
  2063. *
  2064. * Please note that MySQL reports boolean data types as `tinyint(1)`, so
  2065. * all `tinyint(1)` columns will be listed as `boolean`. This can be fixed
  2066. * by calling:
  2067. *
  2068. * {{{
  2069. * #!php
  2070. * $schema->setColumnInfoOverride(
  2071. * array(
  2072. * 'type' => 'integer',
  2073. * 'placeholder' => '%i',
  2074. * 'default' => {default integer},
  2075. * 'min_value' => new fNumber(-128),
  2076. * 'max_value' => new fNumber(127)
  2077. * ),
  2078. * '{table name}',
  2079. * '{column name}'
  2080. * );
  2081. * }}}
  2082. *
  2083. * The `'comment'` element pulls from the database's column comment facility
  2084. * with the exception of MSSQL and SQLite.
  2085. *
  2086. * For MSSQL, the comment is pulled from the `MS_Description` extended
  2087. * property, which can be added via the `Description` field in SQL Server
  2088. * Management Studio, or via the `sp_addextendedproperty` stored procedure.
  2089. *
  2090. * For SQLite, the comment is extracted from any SQL comment that is placed
  2091. * at the end of the line on which the column is defined:
  2092. *
  2093. * {{{
  2094. * #!sql
  2095. * CREATE TABLE users (
  2096. * user_id INTEGER PRIMARY KEY AUTOINCREMENT,
  2097. * name VARCHAR(200) NOT NULL -- This is the full name
  2098. * );
  2099. * }}}
  2100. *
  2101. * For the SQLite `users` table defined above, the `name` column will have
  2102. * the comment `This is the full name`.
  2103. *
  2104. * @param string $table The table to get the column info for
  2105. * @param string $column The column to get the info for
  2106. * @param string $element The element to return: `'type'`, `'placeholder'`, `'not_null'`, `'default'`, `'valid_values'`, `'max_length'`, `'min_value'`, `'max_value'`, `'decimal_places'`, `'auto_increment'`, `'comment'`
  2107. * @return mixed The column info for the table/column/element specified - see method description for format
  2108. */
  2109. public function getColumnInfo($table, $column=NULL, $element=NULL)
  2110. {
  2111. $table = strtolower($table);
  2112. if ($column !== NULL) {
  2113. $column = strtolower($column);
  2114. }
  2115. // Return the saved column info if possible
  2116. if (!$column && isset($this->merged_column_info[$table])) {
  2117. return $this->merged_column_info[$table];
  2118. }
  2119. if ($column && isset($this->merged_column_info[$table][$column])) {
  2120. if ($element !== NULL) {
  2121. if (!isset($this->merged_column_info[$table][$column][$element]) && !array_key_exists($element, $this->merged_column_info[$table][$column])) {
  2122. throw new fProgrammerException(
  2123. 'The element specified, %1$s, is invalid. Must be one of: %2$s.',
  2124. $element,
  2125. join(', ', array('type', 'placeholder', 'not_null', 'default', 'valid_values', 'max_length', 'min_value', 'max_value', 'decimal_places', 'auto_increment'))
  2126. );
  2127. }
  2128. return $this->merged_column_info[$table][$column][$element];
  2129. }
  2130. return $this->merged_column_info[$table][$column];
  2131. }
  2132. if (!in_array($table, $this->getTables())) {
  2133. throw new fProgrammerException(
  2134. 'The table specified, %s, does not exist in the database',
  2135. $table
  2136. );
  2137. }
  2138. $this->fetchColumnInfo($table);
  2139. $this->mergeColumnInfo();
  2140. if ($column && !isset($this->merged_column_info[$table][$column])) {
  2141. throw new fProgrammerException(
  2142. 'The column specified, %1$s, does not exist in the table %2$s',
  2143. $column,
  2144. $table
  2145. );
  2146. }
  2147. if ($column) {
  2148. if ($element) {
  2149. return $this->merged_column_info[$table][$column][$element];
  2150. }
  2151. return $this->merged_column_info[$table][$column];
  2152. }
  2153. return $this->merged_column_info[$table];
  2154. }
  2155. /**
  2156. * Returns the databases on the current server
  2157. *
  2158. * @return array The databases on the current server
  2159. */
  2160. public function getDatabases()
  2161. {
  2162. if ($this->databases !== NULL) {
  2163. return $this->databases;
  2164. }
  2165. $this->databases = array();
  2166. switch ($this->database->getType()) {
  2167. case 'mssql':
  2168. $sql = 'EXECUTE sp_databases';
  2169. break;
  2170. case 'mysql':
  2171. $sql = 'SHOW DATABASES';
  2172. break;
  2173. case 'oracle':
  2174. $sql = 'SELECT ora_database_name FROM dual';
  2175. case 'postgresql':
  2176. $sql = "SELECT
  2177. datname
  2178. FROM
  2179. pg_database
  2180. ORDER BY
  2181. LOWER(datname)";
  2182. break;
  2183. case 'db2':
  2184. $this->databases[] = strtolower($this->database->getDatabase());
  2185. return $this->databases;
  2186. case 'sqlite':
  2187. $this->databases[] = $this->database->getDatabase();
  2188. return $this->databases;
  2189. }
  2190. $result = $this->database->query($sql);
  2191. foreach ($result as $row) {
  2192. $keys = array_keys($row);
  2193. $this->databases[] = strtolower($row[$keys[0]]);
  2194. }
  2195. if ($this->cache) {
  2196. $this->cache->set($this->makeCachePrefix() . 'databases', $this->databases);
  2197. }
  2198. return $this->databases;
  2199. }
  2200. /**
  2201. * Returns a list of primary key, foreign key and unique key constraints for the table specified
  2202. *
  2203. * The structure of the returned array is:
  2204. *
  2205. * {{{
  2206. * array(
  2207. * 'primary' => array(
  2208. * {column name}, ...
  2209. * ),
  2210. * 'unique' => array(
  2211. * array(
  2212. * {column name}, ...
  2213. * ), ...
  2214. * ),
  2215. * 'foreign' => array(
  2216. * array(
  2217. * 'column' => {column name},
  2218. * 'foreign_table' => {foreign table name},
  2219. * 'foreign_column' => {foreign column name},
  2220. * 'on_delete' => {the ON DELETE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'},
  2221. * 'on_update' => {the ON UPDATE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'}
  2222. * ), ...
  2223. * )
  2224. * )
  2225. * }}}
  2226. *
  2227. * @param string $table The table to return the keys for
  2228. * @param string $key_type The type of key to return: `'primary'`, `'foreign'`, `'unique'`
  2229. * @return array An array of all keys, or just the type specified - see method description for format
  2230. */
  2231. public function getKeys($table, $key_type=NULL)
  2232. {
  2233. $table = strtolower($table);
  2234. $valid_key_types = array('primary', 'foreign', 'unique');
  2235. if ($key_type !== NULL && !in_array($key_type, $valid_key_types)) {
  2236. throw new fProgrammerException(
  2237. 'The key type specified, %1$s, is invalid. Must be one of: %2$s.',
  2238. $key_type,
  2239. join(', ', $valid_key_types)
  2240. );
  2241. }
  2242. // Return the saved column info if possible
  2243. if (!$key_type && isset($this->merged_keys[$table])) {
  2244. reset($this->merged_keys[$table]);
  2245. return $this->merged_keys[$table];
  2246. }
  2247. if ($key_type && isset($this->merged_keys[$table][$key_type])) {
  2248. reset($this->merged_keys[$table][$key_type]);
  2249. return $this->merged_keys[$table][$key_type];
  2250. }
  2251. if (!in_array($table, $this->getTables())) {
  2252. throw new fProgrammerException(
  2253. 'The table specified, %s, does not exist in the database',
  2254. $table
  2255. );
  2256. }
  2257. $this->fetchKeys();
  2258. $this->mergeKeys();
  2259. if ($key_type) {
  2260. reset($this->merged_keys[$table][$key_type]);
  2261. return $this->merged_keys[$table][$key_type];
  2262. }
  2263. reset($this->merged_keys[$table]);
  2264. return $this->merged_keys[$table];
  2265. }
  2266. /**
  2267. * Returns a list of one-to-one, many-to-one, one-to-many and many-to-many relationships for the table specified
  2268. *
  2269. * The structure of the returned array is:
  2270. *
  2271. * {{{
  2272. * array(
  2273. * 'one-to-one' => array(
  2274. * array(
  2275. * 'table' => (string) {the name of the table this relationship is for},
  2276. * 'column' => (string) {the column in the specified table},
  2277. * 'related_table' => (string) {the related table},
  2278. * 'related_column' => (string) {the related column},
  2279. * 'on_delete' => (string) {the ON DELETE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'},
  2280. * 'on_update' => (string) {the ON UPDATE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'}
  2281. * ), ...
  2282. * ),
  2283. * 'many-to-one' => array(
  2284. * array(
  2285. * 'table' => (string) {the name of the table this relationship is for},
  2286. * 'column' => (string) {the column in the specified table},
  2287. * 'related_table' => (string) {the related table},
  2288. * 'related_column' => (string) {the related column}
  2289. * ), ...
  2290. * ),
  2291. * 'one-to-many' => array(
  2292. * array(
  2293. * 'table' => (string) {the name of the table this relationship is for},
  2294. * 'column' => (string) {the column in the specified table},
  2295. * 'related_table' => (string) {the related table},
  2296. * 'related_column' => (string) {the related column},
  2297. * 'on_delete' => (string) {the ON DELETE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'},
  2298. * 'on_update' => (string) {the ON UPDATE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'}
  2299. * ), ...
  2300. * ),
  2301. * 'many-to-many' => array(
  2302. * array(
  2303. * 'table' => (string) {the name of the table this relationship is for},
  2304. * 'column' => (string) {the column in the specified table},
  2305. * 'related_table' => (string) {the related table},
  2306. * 'related_column' => (string) {the related column},
  2307. * 'join_table' => (string) {the table that joins the specified table to the related table},
  2308. * 'join_column' => (string) {the column in the join table that references 'column'},
  2309. * 'join_related_column' => (string) {the column in the join table that references 'related_column'},
  2310. * 'on_delete' => (string) {the ON DELETE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'},
  2311. * 'on_update' => (string) {the ON UPDATE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'}
  2312. * ), ...
  2313. * )
  2314. * )
  2315. * }}}
  2316. *
  2317. * @param string $table The table to return the relationships for
  2318. * @param string $relationship_type The type of relationship to return: `'one-to-one'`, `'many-to-one'`, `'one-to-many'`, `'many-to-many'`
  2319. * @return array An array of all relationships, or just the type specified - see method description for format
  2320. */
  2321. public function getRelationships($table, $relationship_type=NULL)
  2322. {
  2323. $table = strtolower($table);
  2324. $valid_relationship_types = array('one-to-one', 'many-to-one', 'one-to-many', 'many-to-many');
  2325. if ($relationship_type !== NULL && !in_array($relationship_type, $valid_relationship_types)) {
  2326. throw new fProgrammerException(
  2327. 'The relationship type specified, %1$s, is invalid. Must be one of: %2$s.',
  2328. $relationship_type,
  2329. join(', ', $valid_relationship_types)
  2330. );
  2331. }
  2332. // Return the saved column info if possible
  2333. if (!$relationship_type && isset($this->relationships[$table])) {
  2334. return $this->relationships[$table];
  2335. }
  2336. if ($relationship_type && isset($this->relationships[$table][$relationship_type])) {
  2337. return $this->relationships[$table][$relationship_type];
  2338. }
  2339. if (!in_array($table, $this->getTables())) {
  2340. throw new fProgrammerException(
  2341. 'The table specified, %s, does not exist in the database',
  2342. $table
  2343. );
  2344. }
  2345. $this->fetchKeys();
  2346. $this->mergeKeys();
  2347. if ($relationship_type) {
  2348. return $this->relationships[$table][$relationship_type];
  2349. }
  2350. return $this->relationships[$table];
  2351. }
  2352. /**
  2353. * Returns the tables in the current database
  2354. *
  2355. * @param boolean|string $creation_order `TRUE` to return in a valid table creation order, or a table name to return that table and any tables that depend on it, in table creation order
  2356. * @return array The tables in the current database, all converted to lowercase
  2357. */
  2358. public function getTables($creation_order=NULL)
  2359. {
  2360. if ($creation_order) {
  2361. return $this->determineTableCreationOrder(is_bool($creation_order) ? NULL : $creation_order);
  2362. }
  2363. if ($this->tables !== NULL) {
  2364. return $this->tables;
  2365. }
  2366. switch ($this->database->getType()) {
  2367. case 'db2':
  2368. $sql = "SELECT
  2369. LOWER(RTRIM(TABSCHEMA)) AS \"schema\",
  2370. LOWER(TABNAME) AS \"table\"
  2371. FROM
  2372. SYSCAT.TABLES
  2373. WHERE
  2374. TYPE = 'T' AND
  2375. TABSCHEMA != 'SYSIBM' AND
  2376. DEFINER != 'SYSIBM' AND
  2377. TABSCHEMA != 'SYSTOOLS' AND
  2378. DEFINER != 'SYSTOOLS'
  2379. ORDER BY
  2380. LOWER(TABNAME)";
  2381. break;
  2382. case 'mssql':
  2383. $sql = "SELECT
  2384. TABLE_SCHEMA AS \"schema\",
  2385. TABLE_NAME AS \"table\"
  2386. FROM
  2387. INFORMATION_SCHEMA.TABLES
  2388. WHERE
  2389. TABLE_NAME != 'sysdiagrams'
  2390. ORDER BY
  2391. LOWER(TABLE_NAME)";
  2392. break;
  2393. case 'mysql':
  2394. if (version_compare($this->database->getVersion(), 5, '<')) {
  2395. $sql = 'SHOW TABLES';
  2396. } else {
  2397. $sql = "SHOW FULL TABLES WHERE table_type = 'BASE TABLE'";
  2398. }
  2399. break;
  2400. case 'oracle':
  2401. $sql = "SELECT
  2402. LOWER(OWNER) AS \"SCHEMA\",
  2403. LOWER(TABLE_NAME) AS \"TABLE\"
  2404. FROM
  2405. ALL_TABLES
  2406. WHERE
  2407. OWNER NOT IN (
  2408. 'SYS',
  2409. 'SYSTEM',
  2410. 'OUTLN',
  2411. 'ANONYMOUS',
  2412. 'AURORA\$ORB\$UNAUTHENTICATED',
  2413. 'AWR_STAGE',
  2414. 'CSMIG',
  2415. 'CTXSYS',
  2416. 'DBSNMP',
  2417. 'DIP',
  2418. 'DMSYS',
  2419. 'DSSYS',
  2420. 'EXFSYS',
  2421. 'FLOWS_020100',
  2422. 'FLOWS_FILES',
  2423. 'LBACSYS',
  2424. 'MDSYS',
  2425. 'ORACLE_OCM',
  2426. 'ORDPLUGINS',
  2427. 'ORDSYS',
  2428. 'PERFSTAT',
  2429. 'TRACESVR',
  2430. 'TSMSYS',
  2431. 'XDB'
  2432. ) AND
  2433. DROPPED = 'NO'
  2434. ORDER BY
  2435. TABLE_NAME ASC";
  2436. break;
  2437. case 'postgresql':
  2438. $sql = "SELECT
  2439. schemaname AS \"schema\",
  2440. tablename as \"table\"
  2441. FROM
  2442. pg_tables
  2443. WHERE
  2444. tablename !~ '^(pg|sql)_'
  2445. ORDER BY
  2446. LOWER(tablename)";
  2447. break;
  2448. case 'sqlite':
  2449. $sql = "SELECT
  2450. name
  2451. FROM
  2452. sqlite_master
  2453. WHERE
  2454. type = 'table' AND
  2455. name NOT LIKE 'sqlite_%'
  2456. ORDER BY
  2457. name ASC";
  2458. break;
  2459. }
  2460. $result = $this->database->query($sql);
  2461. $this->tables = array();
  2462. // For databases with schemas we only include the schema
  2463. // name if there are conflicting table names
  2464. if (!in_array($this->database->getType(), array('mysql', 'sqlite'))) {
  2465. $default_schema_map = array(
  2466. 'db2' => strtolower($this->database->getUsername()),
  2467. 'mssql' => 'dbo',
  2468. 'oracle' => strtolower($this->database->getUsername()),
  2469. 'postgresql' => 'public'
  2470. );
  2471. $default_schema = $default_schema_map[$this->database->getType()];
  2472. foreach ($result as $row) {
  2473. if ($row['schema'] == $default_schema) {
  2474. $this->tables[] = strtolower($row['table']);
  2475. } else {
  2476. $this->tables[] = strtolower($row['schema'] . '.' . $row['table']);
  2477. }
  2478. }
  2479. // SQLite and MySQL don't support schemas
  2480. } else {
  2481. foreach ($result as $row) {
  2482. $keys = array_keys($row);
  2483. $this->tables[] = strtolower($row[$keys[0]]);
  2484. }
  2485. }
  2486. sort($this->tables);
  2487. if ($this->cache) {
  2488. $this->cache->set($this->makeCachePrefix() . 'tables', $this->tables);
  2489. }
  2490. return $this->tables;
  2491. }
  2492. /**
  2493. * Determines if a table is a joining table
  2494. *
  2495. * @param string $table The table to check
  2496. * @return boolean If the table is a joining table
  2497. */
  2498. private function isJoiningTable($table)
  2499. {
  2500. $primary_key_columns = $this->merged_keys[$table]['primary'];
  2501. if (sizeof($primary_key_columns) != 2) {
  2502. return FALSE;
  2503. }
  2504. if (empty($this->merged_column_info[$table])) {
  2505. $this->getColumnInfo($table);
  2506. }
  2507. if (sizeof($this->merged_column_info[$table]) != 2) {
  2508. return FALSE;
  2509. }
  2510. $foreign_key_columns = array();
  2511. foreach ($this->merged_keys[$table]['foreign'] as $key) {
  2512. $foreign_key_columns[] = $key['column'];
  2513. }
  2514. return sizeof($foreign_key_columns) == 2 && !array_diff($foreign_key_columns, $primary_key_columns);
  2515. }
  2516. /**
  2517. * Creates a unique cache prefix to help prevent cache conflicts
  2518. *
  2519. * @return string The cache prefix to use
  2520. */
  2521. private function makeCachePrefix()
  2522. {
  2523. if (!$this->cache_prefix) {
  2524. $prefix = 'fSchema::' . $this->database->getType() . '::';
  2525. if ($this->database->getHost()) {
  2526. $prefix .= $this->database->getHost() . '::';
  2527. }
  2528. if ($this->database->getPort()) {
  2529. $prefix .= $this->database->getPort() . '::';
  2530. }
  2531. $prefix .= $this->database->getDatabase() . '::';
  2532. if ($this->database->getUsername()) {
  2533. $prefix .= $this->database->getUsername() . '::';
  2534. }
  2535. $this->cache_prefix = $prefix;
  2536. }
  2537. return $this->cache_prefix;
  2538. }
  2539. /**
  2540. * Merges the column info with the column info override
  2541. *
  2542. * @return void
  2543. */
  2544. private function mergeColumnInfo()
  2545. {
  2546. $this->merged_column_info = $this->column_info;
  2547. foreach ($this->column_info_override as $table => $columns) {
  2548. // Remove a table if the columns are set to NULL
  2549. if ($columns === NULL) {
  2550. unset($this->merged_column_info[$table]);
  2551. continue;
  2552. }
  2553. if (!isset($this->merged_column_info[$table])) {
  2554. $this->merged_column_info[$table] = array();
  2555. }
  2556. foreach ($columns as $column => $info) {
  2557. // Remove a column if it is set to NULL
  2558. if ($info === NULL) {
  2559. unset($this->merged_column_info[$table][$column]);
  2560. continue;
  2561. }
  2562. if (!isset($this->merged_column_info[$table][$column])) {
  2563. $this->merged_column_info[$table][$column] = array();
  2564. }
  2565. $this->merged_column_info[$table][$column] = array_merge($this->merged_column_info[$table][$column], $info);
  2566. }
  2567. }
  2568. $optional_elements = array(
  2569. 'not_null',
  2570. 'default',
  2571. 'valid_values',
  2572. 'max_length',
  2573. 'max_value',
  2574. 'min_value',
  2575. 'decimal_places',
  2576. 'auto_increment',
  2577. 'comment'
  2578. );
  2579. foreach ($this->merged_column_info as $table => $column_array) {
  2580. foreach ($column_array as $column => $info) {
  2581. if (empty($info['type'])) {
  2582. throw new fProgrammerException('The data type for the column %1$s is empty', $column);
  2583. }
  2584. if (empty($this->merged_column_info[$table][$column]['placeholder'])) {
  2585. $this->merged_column_info[$table][$column]['placeholder'] = strtr(
  2586. $info['type'],
  2587. array(
  2588. 'blob' => '%l',
  2589. 'boolean' => '%b',
  2590. 'date' => '%d',
  2591. 'float' => '%f',
  2592. 'integer' => '%i',
  2593. 'char' => '%s',
  2594. 'text' => '%s',
  2595. 'varchar' => '%s',
  2596. 'time' => '%t',
  2597. 'timestamp' => '%p'
  2598. )
  2599. );
  2600. }
  2601. foreach ($optional_elements as $element) {
  2602. if (!isset($this->merged_column_info[$table][$column][$element])) {
  2603. $this->merged_column_info[$table][$column][$element] = ($element == 'auto_increment') ? FALSE : NULL;
  2604. }
  2605. }
  2606. }
  2607. }
  2608. if ($this->cache) {
  2609. $this->cache->set($this->makeCachePrefix() . 'merged_column_info', $this->merged_column_info);
  2610. }
  2611. }
  2612. /**
  2613. * Merges the keys with the keys override
  2614. *
  2615. * @return void
  2616. */
  2617. private function mergeKeys()
  2618. {
  2619. // Handle the database and override key info
  2620. $this->merged_keys = $this->keys;
  2621. foreach ($this->keys_override as $table => $info) {
  2622. if (!isset($this->merged_keys[$table])) {
  2623. $this->merged_keys[$table] = array();
  2624. }
  2625. $this->merged_keys[$table] = array_merge($this->merged_keys[$table], $info);
  2626. }
  2627. if ($this->cache) {
  2628. $this->cache->set($this->makeCachePrefix() . 'merged_keys', $this->merged_keys);
  2629. }
  2630. $this->findRelationships();
  2631. }
  2632. /**
  2633. * Allows overriding of column info
  2634. *
  2635. * Performs an array merge with the column info detected from the database.
  2636. *
  2637. * To erase a whole table, set the `$column_info` to `NULL`. To erase a
  2638. * column, set the `$column_info` for that column to `NULL`.
  2639. *
  2640. * If the `$column_info` parameter is not `NULL`, it should be an
  2641. * associative array containing one or more of the following keys. Please
  2642. * see ::getColumnInfo() for a description of each.
  2643. * - `'type'`
  2644. * - `'placeholder'`
  2645. * - `'not_null'`
  2646. * - `'default'`
  2647. * - `'valid_values'`
  2648. * - `'max_length'`
  2649. * - `'min_value'`
  2650. * - `'max_value'`
  2651. * - `'decimal_places'`
  2652. * - `'auto_increment'`
  2653. * - `'comment'`
  2654. *
  2655. * The following keys may be set to `NULL`:
  2656. * - `'not_null'`
  2657. * - `'default'`
  2658. * - `'valid_values'`
  2659. * - `'max_length'`
  2660. * - `'min_value'`
  2661. * - `'max_value'`
  2662. * - `'decimal_places'`
  2663. * - `'comment'`
  2664. *
  2665. * The key `'auto_increment'` should be a boolean.
  2666. *
  2667. * The `'type'` key should be one of:
  2668. * - `'blob'`
  2669. * - `'boolean'`
  2670. * - `'char'`
  2671. * - `'date'`
  2672. * - `'float'`
  2673. * - `'integer'`
  2674. * - `'text'`
  2675. * - `'time'`
  2676. * - `'timestamp'`
  2677. * - `'varchar'`
  2678. *
  2679. * @param array $column_info The modified column info - see method description for format
  2680. * @param string $table The table to override
  2681. * @param string $column The column to override
  2682. * @return void
  2683. */
  2684. public function setColumnInfoOverride($column_info, $table, $column=NULL)
  2685. {
  2686. $table = strotlower($table);
  2687. if ($column !== NULL) {
  2688. $column = strtolower($column);
  2689. }
  2690. if (!isset($this->column_info_override[$table])) {
  2691. $this->column_info_override[$table] = array();
  2692. }
  2693. if (!empty($column)) {
  2694. $this->column_info_override[$table][$column] = $column_info;
  2695. } else {
  2696. $this->column_info_override[$table] = $column_info;
  2697. }
  2698. $this->fetchColumnInfo($table);
  2699. $this->mergeColumnInfo();
  2700. }
  2701. /**
  2702. * Allows overriding of key info. Replaces existing info, so be sure to provide full key info for type selected or all types.
  2703. *
  2704. * @param array $keys The modified keys - see ::getKeys() for format
  2705. * @param string $table The table to override
  2706. * @param string $key_type The key type to override: `'primary'`, `'foreign'`, `'unique'`
  2707. * @return void
  2708. */
  2709. public function setKeysOverride($keys, $table, $key_type=NULL)
  2710. {
  2711. $table = strtolower($table);
  2712. $valid_key_types = array('primary', 'foreign', 'unique');
  2713. if (!in_array($key_type, $valid_key_types)) {
  2714. throw new fProgrammerException(
  2715. 'The key type specified, %1$s, is invalid. Must be one of: %2$s.',
  2716. $key_type,
  2717. join(', ', $valid_key_types)
  2718. );
  2719. }
  2720. if (!isset($this->keys_override[$table])) {
  2721. $this->keys_override[$table] = array();
  2722. }
  2723. if (!empty($key_type)) {
  2724. $this->keys_override[$table][$key_type] = $keys;
  2725. } else {
  2726. $this->keys_override[$table] = $keys;
  2727. }
  2728. $this->fetchKeys();
  2729. $this->mergeKeys();
  2730. }
  2731. }
  2732. /**
  2733. * Copyright (c) 2007-2012 Will Bond <will@flourishlib.com>
  2734. *
  2735. * Permission is hereby granted, free of charge, to any person obtaining a copy
  2736. * of this software and associated documentation files (the "Software"), to deal
  2737. * in the Software without restriction, including without limitation the rights
  2738. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  2739. * copies of the Software, and to permit persons to whom the Software is
  2740. * furnished to do so, subject to the following conditions:
  2741. *
  2742. * The above copyright notice and this permission notice shall be included in
  2743. * all copies or substantial portions of the Software.
  2744. *
  2745. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  2746. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  2747. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  2748. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  2749. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  2750. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  2751. * THE SOFTWARE.
  2752. */