PageRenderTime 64ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/base/lib/flourishlib/fSchema.php

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