PageRenderTime 66ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/fSchema.php

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