PageRenderTime 61ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/inc/flourish/fSchema.php

https://github.com/trbs/Graphite-Tattle
PHP | 3153 lines | 2247 code | 319 blank | 587 comment | 314 complexity | 45c080c223156ed8b16871645b454a99 MD5 | raw file
Possible License(s): Apache-2.0

Large files files are truncated, but you can click here to view the full 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.0b50
  13. * @changes 1.0.0b50 Fixed detection of explicitly named SQLite foreign key constraints [wb, 2011-08-23]
  14. * @changes 1.0.0b49 Added support for spatial/geometric data types in MySQL and PostgreSQL [wb, 2011-05-26]
  15. * @changes 1.0.0b48 Fixed a bug with ::getTables() not working on MySQL 4.x, fixed ::getKeys() to always return a reset array [wb, 2011-05-24]
  16. * @changes 1.0.0b47 Backwards Compatibility Break - ::getTables(), ::getColumnInfo(), ::getDatabases(), ::getKeys() and ::getRelationships() now return database, schema, table and column names in lowercase, added the `$creation_order` parameter to ::getTables(), fixed bugs with getting column and key information from MSSQL, Oracle and SQLite [wb, 2011-05-09]
  17. * @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]
  18. * @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]
  19. * @changes 1.0.0b44 Fixed the list of valid elements for ::getColumnInfo() [wb, 2010-11-28]
  20. * @changes 1.0.0b43 Added the `comment` element to the information returned by ::getColumnInfo() [wb, 2010-11-28]
  21. * @changes 1.0.0b42 Fixed a bug with MySQL detecting default `ON DELETE` clauses [wb, 2010-10-19]
  22. * @changes 1.0.0b41 Fixed handling MySQL table names that require quoting [wb, 2010-08-24]
  23. * @changes 1.0.0b40 Fixed bugs in the documentation and error message of ::getColumnInfo() about what are valid elements [wb, 2010-07-21]
  24. * @changes 1.0.0b39 Fixed a regression where key detection SQL was not compatible with PostgreSQL 8.1 [wb, 2010-04-13]
  25. * @changes 1.0.0b38 Added Oracle support to ::getDatabases() [wb, 2010-04-13]
  26. * @changes 1.0.0b37 Fixed ::getDatabases() for MSSQL [wb, 2010-04-09]
  27. * @changes 1.0.0b36 Fixed PostgreSQL to properly report explicit `NULL` default values via ::getColumnInfo() [wb, 2010-03-30]
  28. * @changes 1.0.0b35 Added `max_length` values for various text and blob data types across all databases [wb, 2010-03-29]
  29. * @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]
  30. * @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]
  31. * @changes 1.0.0b32 Fixed ::getTables() to not include views for MySQL [wb, 2010-03-14]
  32. * @changes 1.0.0b31 Fixed the creation of the default caching key for ::enableCaching() [wb, 2010-03-02]
  33. * @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]
  34. * @changes 1.0.0b29 Added on_delete and on_update elements to one-to-one relationship info retrieved by ::getRelationships() [wb, 2009-12-16]
  35. * @changes 1.0.0b28 Fixed a bug with detecting some multi-column unique constraints in SQL Server databases [wb, 2009-11-13]
  36. * @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]
  37. * @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]
  38. * @changes 1.0.0b25 One-to-one relationships utilizing the primary key as a foreign key are now properly detected [wb, 2009-09-22]
  39. * @changes 1.0.0b24 Fixed MSSQL support to work with ODBC database connections [wb, 2009-09-18]
  40. * @changes 1.0.0b23 Fixed a bug where one-to-one relationships were being listed as many-to-one [wb, 2009-07-21]
  41. * @changes 1.0.0b22 PostgreSQL UNIQUE constraints that are created as indexes and not table constraints are now properly detected [wb, 2009-07-08]
  42. * @changes 1.0.0b21 Added support for the UUID data type in PostgreSQL [wb, 2009-06-18]
  43. * @changes 1.0.0b20 Add caching of merged info, improved performance of ::getColumnInfo() [wb, 2009-06-15]
  44. * @changes 1.0.0b19 Fixed a couple of bugs with ::setKeysOverride() [wb, 2009-06-04]
  45. * @changes 1.0.0b18 Added missing support for MySQL mediumint columns [wb, 2009-05-18]
  46. * @changes 1.0.0b17 Fixed a bug with ::clearCache() not properly reseting the tables and databases list [wb, 2009-05-13]
  47. * @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]
  48. * @changes 1.0.0b15 Added support for the three different types of identifier quoting in SQLite [wb, 2009-03-28]
  49. * @changes 1.0.0b14 Added support for MySQL column definitions containing the COLLATE keyword [wb, 2009-03-28]
  50. * @changes 1.0.0b13 Fixed a bug with detecting PostgreSQL columns having both a CHECK constraint and a UNIQUE constraint [wb, 2009-02-27]
  51. * @changes 1.0.0b12 Fixed detection of multi-column primary keys in MySQL [wb, 2009-02-27]
  52. * @changes 1.0.0b11 Fixed an issue parsing MySQL tables with comments [wb, 2009-02-25]
  53. * @changes 1.0.0b10 Added the ::getDatabases() method [wb, 2009-02-24]
  54. * @changes 1.0.0b9 Now detects unsigned and zerofill MySQL data types that do not have a parenthetical part [wb, 2009-02-16]
  55. * @changes 1.0.0b8 Mapped the MySQL data type `'set'` to `'varchar'`, however valid values are not implemented yet [wb, 2009-02-01]
  56. * @changes 1.0.0b7 Fixed a bug with detecting MySQL timestamp columns [wb, 2009-01-28]
  57. * @changes 1.0.0b6 Fixed a bug with detecting MySQL columns that accept `NULL` [wb, 2009-01-19]
  58. * @changes 1.0.0b5 ::setColumnInfo(): fixed a bug with not grabbing the real database schema first, made general improvements [wb, 2009-01-19]
  59. * @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]
  60. * @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]
  61. * @changes 1.0.0b2 Fixed a bug with detecting multi-column unique keys in MySQL [wb, 2009-01-03]
  62. * @changes 1.0.0b The initial implementation [wb, 2007-09-25]
  63. */
  64. class fSchema
  65. {
  66. /**
  67. * The place to cache to
  68. *
  69. * @var fCache
  70. */
  71. private $cache = NULL;
  72. /**
  73. * The cache prefix to use for cache entries
  74. *
  75. * @var string
  76. */
  77. private $cache_prefix;
  78. /**
  79. * The cached column info
  80. *
  81. * @var array
  82. */
  83. private $column_info = array();
  84. /**
  85. * The column info to override
  86. *
  87. * @var array
  88. */
  89. private $column_info_override = array();
  90. /**
  91. * A reference to an instance of the fDatabase class
  92. *
  93. * @var fDatabase
  94. */
  95. private $database = NULL;
  96. /**
  97. * The databases on the current database server
  98. *
  99. * @var array
  100. */
  101. private $databases = NULL;
  102. /**
  103. * The cached key info
  104. *
  105. * @var array
  106. */
  107. private $keys = array();
  108. /**
  109. * The key info to override
  110. *
  111. * @var array
  112. */
  113. private $keys_override = array();
  114. /**
  115. * The merged column info
  116. *
  117. * @var array
  118. */
  119. private $merged_column_info = array();
  120. /**
  121. * The merged key info
  122. *
  123. * @var array
  124. */
  125. private $merged_keys = array();
  126. /**
  127. * The relationships in the database
  128. *
  129. * @var array
  130. */
  131. private $relationships = array();
  132. /**
  133. * The tables in the database
  134. *
  135. * @var array
  136. */
  137. private $tables = NULL;
  138. /**
  139. * Sets the database
  140. *
  141. * @param fDatabase $database The fDatabase instance
  142. * @return fSchema
  143. */
  144. public function __construct($database)
  145. {
  146. $this->database = $database;
  147. }
  148. /**
  149. * All requests that hit this method should be requests for callbacks
  150. *
  151. * @internal
  152. *
  153. * @param string $method The method to create a callback for
  154. * @return callback The callback for the method requested
  155. */
  156. public function __get($method)
  157. {
  158. return array($this, $method);
  159. }
  160. /**
  161. * Checks to see if a column is part of a single-column `UNIQUE` key
  162. *
  163. * @param string $table The table the column is located in
  164. * @param string $column The column to check
  165. * @return boolean If the column is part of a single-column unique key
  166. */
  167. private function checkForSingleColumnUniqueKey($table, $column)
  168. {
  169. foreach ($this->merged_keys[$table]['unique'] as $key) {
  170. if (array($column) == $key) {
  171. return TRUE;
  172. }
  173. }
  174. if (array($column) == $this->merged_keys[$table]['primary']) {
  175. return TRUE;
  176. }
  177. return FALSE;
  178. }
  179. /**
  180. * Clears all of the schema info out of the object and, if set, the fCache object
  181. *
  182. * @internal
  183. *
  184. * @return void
  185. */
  186. public function clearCache()
  187. {
  188. $this->column_info = array();
  189. $this->databases = NULL;
  190. $this->keys = array();
  191. $this->merged_column_info = array();
  192. $this->merged_keys = array();
  193. $this->relationships = array();
  194. $this->tables = NULL;
  195. if ($this->cache) {
  196. $prefix = $this->makeCachePrefix();
  197. $this->cache->delete($prefix . 'column_info');
  198. $this->cache->delete($prefix . 'databases');
  199. $this->cache->delete($prefix . 'keys');
  200. $this->cache->delete($prefix . 'merged_column_info');
  201. $this->cache->delete($prefix . 'merged_keys');
  202. $this->cache->delete($prefix . 'relationships');
  203. $this->cache->delete($prefix . 'tables');
  204. }
  205. }
  206. /**
  207. * Returns an ordered array of table names, in a valid table creation order
  208. *
  209. * @param string $filter_table The only return this table and tables that rely on it
  210. * @return array An array of table names
  211. */
  212. private function determineTableCreationOrder($filter_table=NULL)
  213. {
  214. $found = array();
  215. $ignored_found = array();
  216. $current_tables = $this->getTables();
  217. while ($current_tables) {
  218. $remaining_tables = array();
  219. foreach ($current_tables as $table) {
  220. $foreign_keys = $this->getKeys($table, 'foreign');
  221. if (!$foreign_keys) {
  222. if ($filter_table !== NULL) {
  223. if ($table == $filter_table) {
  224. $found[] = $table;
  225. } else {
  226. $ignored_found[] = $table;
  227. }
  228. } else {
  229. $found[] = $table;
  230. }
  231. } else {
  232. $all_dependencies_met = TRUE;
  233. $found_dependencies = 0;
  234. foreach ($foreign_keys as $foreign_key) {
  235. if (!in_array($foreign_key['foreign_table'], $found) && !in_array($foreign_key['foreign_table'], $ignored_found)) {
  236. $all_dependencies_met = FALSE;
  237. break;
  238. } elseif (in_array($foreign_key['foreign_table'], $found)) {
  239. $found_dependencies++;
  240. }
  241. }
  242. if ($all_dependencies_met) {
  243. if ($filter_table !== NULL) {
  244. if ($found_dependencies || $table == $filter_table) {
  245. $found[] = $table;
  246. } else {
  247. $ignored_found[] = $table;
  248. }
  249. } else {
  250. $found[] = $table;
  251. }
  252. } else {
  253. $remaining_tables[] = $table;
  254. }
  255. }
  256. }
  257. $current_tables = $remaining_tables;
  258. }
  259. return $found;
  260. }
  261. /**
  262. * Sets the schema to be cached to the fCache object specified
  263. *
  264. * @param fCache $cache The cache to cache to
  265. * @param string $key_token Internal use only! (this will be used in the cache key to uniquely identify the cache for this fSchema object)
  266. * @return void
  267. */
  268. public function enableCaching($cache, $key_token=NULL)
  269. {
  270. $this->cache = $cache;
  271. if ($key_token !== NULL) {
  272. $this->cache_prefix = 'fSchema::' . $this->database->getType() . '::' . $key_token . '::';
  273. }
  274. $prefix = $this->makeCachePrefix();
  275. $this->column_info = $this->cache->get($prefix . 'column_info', array());
  276. $this->databases = $this->cache->get($prefix . 'databases', NULL);
  277. $this->keys = $this->cache->get($prefix . 'keys', array());
  278. if (!$this->column_info_override && !$this->keys_override) {
  279. $this->merged_column_info = $this->cache->get($prefix . 'merged_column_info', array());
  280. $this->merged_keys = $this->cache->get($prefix . 'merged_keys', array());
  281. $this->relationships = $this->cache->get($prefix . 'relationships', array());
  282. }
  283. $this->tables = $this->cache->get($prefix . 'tables', NULL);
  284. }
  285. /**
  286. * Gets the column info from the database for later access
  287. *
  288. * @param string $table The table to fetch the column info for
  289. * @return void
  290. */
  291. private function fetchColumnInfo($table)
  292. {
  293. if (isset($this->column_info[$table])) {
  294. return;
  295. }
  296. switch ($this->database->getType()) {
  297. case 'db2':
  298. $column_info = $this->fetchDB2ColumnInfo($table);
  299. break;
  300. case 'mssql':
  301. $column_info = $this->fetchMSSQLColumnInfo($table);
  302. break;
  303. case 'mysql':
  304. $column_info = $this->fetchMySQLColumnInfo($table);
  305. break;
  306. case 'oracle':
  307. $column_info = $this->fetchOracleColumnInfo($table);
  308. break;
  309. case 'postgresql':
  310. $column_info = $this->fetchPostgreSQLColumnInfo($table);
  311. break;
  312. case 'sqlite':
  313. $column_info = $this->fetchSQLiteColumnInfo($table);
  314. break;
  315. }
  316. if (!$column_info) {
  317. return;
  318. }
  319. $this->column_info[$table] = $column_info;
  320. if ($this->cache) {
  321. $this->cache->set($this->makeCachePrefix() . 'column_info', $this->column_info);
  322. }
  323. }
  324. /**
  325. * Gets the column info from a DB2 database
  326. *
  327. * @param string $table The table to fetch the column info for
  328. * @return array The column info for the table specified - see ::getColumnInfo() for details
  329. */
  330. private function fetchDB2ColumnInfo($table)
  331. {
  332. $column_info = array();
  333. $schema = strtolower($this->database->getUsername());
  334. if (strpos($table, '.') !== FALSE) {
  335. list ($schema, $table) = explode('.', $table);
  336. }
  337. $data_type_mapping = array(
  338. 'smallint' => 'integer',
  339. 'integer' => 'integer',
  340. 'bigint' => 'integer',
  341. 'timestamp' => 'timestamp',
  342. 'date' => 'date',
  343. 'time' => 'time',
  344. 'varchar' => 'varchar',
  345. 'long varchar' => 'varchar',
  346. 'vargraphic' => 'varchar',
  347. 'long vargraphic' => 'varchar',
  348. 'character' => 'char',
  349. 'graphic' => 'char',
  350. 'real' => 'float',
  351. 'decimal' => 'float',
  352. 'numeric' => 'float',
  353. 'blob' => 'blob',
  354. 'clob' => 'text',
  355. 'dbclob' => 'text'
  356. );
  357. $max_min_values = array(
  358. 'smallint' => array('min' => new fNumber(-32768), 'max' => new fNumber(32767)),
  359. 'integer' => array('min' => new fNumber(-2147483648), 'max' => new fNumber(2147483647)),
  360. 'bigint' => array('min' => new fNumber('-9223372036854775808'), 'max' => new fNumber('9223372036854775807'))
  361. );
  362. // Get the column info
  363. $sql = "SELECT
  364. LOWER(C.COLNAME) AS \"COLUMN\",
  365. C.TYPENAME AS TYPE,
  366. C.NULLS AS NULLABLE,
  367. C.DEFAULT,
  368. C.LENGTH AS MAX_LENGTH,
  369. C.SCALE,
  370. CASE WHEN C.IDENTITY = 'Y' AND (C.GENERATED = 'D' OR C.GENERATED = 'A') THEN '1' ELSE '0' END AS AUTO_INCREMENT,
  371. CH.TEXT AS \"CONSTRAINT\",
  372. C.REMARKS AS \"COMMENT\"
  373. FROM
  374. SYSCAT.COLUMNS AS C LEFT JOIN
  375. SYSCAT.COLCHECKS AS CC ON
  376. C.TABSCHEMA = CC.TABSCHEMA AND
  377. C.TABNAME = CC.TABNAME AND
  378. C.COLNAME = CC.COLNAME AND
  379. CC.USAGE = 'R' LEFT JOIN
  380. SYSCAT.CHECKS AS CH ON
  381. C.TABSCHEMA = CH.TABSCHEMA AND
  382. C.TABNAME = CH.TABNAME AND
  383. CH.TYPE = 'C' AND
  384. CH.CONSTNAME = CC.CONSTNAME
  385. WHERE
  386. LOWER(C.TABSCHEMA) = %s AND
  387. LOWER(C.TABNAME) = %s
  388. ORDER BY
  389. C.COLNO ASC";
  390. $result = $this->database->query($sql, strtolower($schema), strtolower($table));
  391. foreach ($result as $row) {
  392. $info = array();
  393. foreach ($data_type_mapping as $data_type => $mapped_data_type) {
  394. if (stripos($row['type'], $data_type) === 0) {
  395. if (isset($max_min_values[$data_type])) {
  396. $info['min_value'] = $max_min_values[$data_type]['min'];
  397. $info['max_value'] = $max_min_values[$data_type]['max'];
  398. }
  399. $info['type'] = $mapped_data_type;
  400. break;
  401. }
  402. }
  403. // Handle decimal places and min/max for numeric/decimals
  404. if (in_array(strtolower($row['type']), array('decimal', 'numeric'))) {
  405. $info['decimal_places'] = $row['scale'];
  406. $before_digits = str_pad('', $row['max_length'] - $row['scale'], '9');
  407. $after_digits = str_pad('', $row['scale'], '9');
  408. $max_min = $before_digits . ($after_digits ? '.' : '') . $after_digits;
  409. $info['min_value'] = new fNumber('-' . $max_min);
  410. $info['max_value'] = new fNumber($max_min);
  411. }
  412. if (!isset($info['type'])) {
  413. $info['type'] = $row['type'];
  414. }
  415. // Handle the special data for varchar columns
  416. if (in_array($info['type'], array('char', 'varchar', 'text', 'blob'))) {
  417. $info['max_length'] = $row['max_length'];
  418. }
  419. // The generally accepted practice for boolean on DB2 is a CHAR(1) with a CHECK constraint
  420. if ($info['type'] == 'char' && $info['max_length'] == 1 && !empty($row['constraint'])) {
  421. if (is_resource($row['constraint'])) {
  422. $row['constraint'] = stream_get_contents($row['constraint']);
  423. }
  424. if (preg_match('/^\s*"?' . preg_quote($row['column'], '/') . '"?\s+in\s+\(\s*(\'0\',\s*\'1\'|\'1\',\s*\'0\')\s*\)\s*$/i', $row['constraint'])) {
  425. $info['type'] = 'boolean';
  426. $info['max_length'] = NULL;
  427. }
  428. }
  429. // If the column has a constraint, look for valid values
  430. if (in_array($info['type'], array('char', 'varchar')) && !empty($row['constraint'])) {
  431. if (preg_match('/^\s*"?' . preg_quote($row['column'], '/') . '"?\s+in\s+\((.*?)\)\s*$/i', $row['constraint'], $match)) {
  432. if (preg_match_all("/(?<!')'((''|[^']+)*)'/", $match[1], $matches, PREG_PATTERN_ORDER)) {
  433. $info['valid_values'] = str_replace("''", "'", $matches[1]);
  434. }
  435. }
  436. }
  437. // Handle auto increment
  438. if ($row['auto_increment']) {
  439. $info['auto_increment'] = TRUE;
  440. }
  441. // Handle default values
  442. if ($row['default'] !== NULL) {
  443. if ($row['default'] == 'NULL') {
  444. $info['default'] = NULL;
  445. } elseif (in_array($info['type'], array('timestamp', 'date', 'time')) && $row['default'][0] != "'") {
  446. $info['default'] = str_replace(' ', '_', $row['default']);
  447. } elseif (in_array($info['type'], array('char', 'varchar', 'text', 'timestamp', 'date', 'time')) ) {
  448. $info['default'] = substr($row['default'], 1, -1);
  449. } elseif ($info['type'] == 'boolean') {
  450. $info['default'] = (boolean) substr($row['default'], 1, -1);
  451. } else {
  452. $info['default'] = $row['default'];
  453. }
  454. }
  455. // Handle not null
  456. $info['not_null'] = ($row['nullable'] == 'N') ? TRUE : FALSE;
  457. $info['comment'] = $row['comment'];
  458. $column_info[$row['column']] = $info;
  459. }
  460. return $column_info;
  461. }
  462. /**
  463. * Fetches the key info for a DB2 database
  464. *
  465. * @return array The keys arrays for every table in the database - see ::getKeys() for details
  466. */
  467. private function fetchDB2Keys()
  468. {
  469. $keys = array();
  470. $default_schema = strtolower($this->database->getUsername());
  471. $tables = $this->getTables();
  472. foreach ($tables as $table) {
  473. $keys[$table] = array();
  474. $keys[$table]['primary'] = array();
  475. $keys[$table]['unique'] = array();
  476. $keys[$table]['foreign'] = array();
  477. }
  478. $params = array();
  479. $sql = "(SELECT
  480. LOWER(RTRIM(R.TABSCHEMA)) AS \"SCHEMA\",
  481. LOWER(R.TABNAME) AS \"TABLE\",
  482. R.CONSTNAME AS CONSTRAINT_NAME,
  483. 'foreign' AS \"TYPE\",
  484. LOWER(K.COLNAME) AS \"COLUMN\",
  485. LOWER(RTRIM(R.REFTABSCHEMA)) AS FOREIGN_SCHEMA,
  486. LOWER(R.REFTABNAME) AS FOREIGN_TABLE,
  487. LOWER(FK.COLNAME) AS FOREIGN_COLUMN,
  488. CASE R.DELETERULE WHEN 'C' THEN 'cascade' WHEN 'A' THEN 'no_action' WHEN 'R' THEN 'restrict' ELSE 'set_null' END AS ON_DELETE,
  489. CASE R.UPDATERULE WHEN 'A' THEN 'no_action' WHEN 'R' THEN 'restrict' END AS ON_UPDATE,
  490. K.COLSEQ
  491. FROM
  492. SYSCAT.REFERENCES AS R INNER JOIN
  493. SYSCAT.KEYCOLUSE AS K ON
  494. R.CONSTNAME = K.CONSTNAME AND
  495. R.TABSCHEMA = K.TABSCHEMA AND
  496. R.TABNAME = K.TABNAME INNER JOIN
  497. SYSCAT.KEYCOLUSE AS FK ON
  498. R.REFKEYNAME = FK.CONSTNAME AND
  499. R.REFTABSCHEMA = FK.TABSCHEMA AND
  500. R.REFTABNAME = FK.TABNAME
  501. WHERE ";
  502. $conditions = array();
  503. foreach ($tables as $table) {
  504. if (strpos($table, '.') === FALSE) {
  505. $table = $default_schema . '.' . $table;
  506. }
  507. list ($schema, $table) = explode('.', strtolower($table));
  508. $conditions[] = "LOWER(R.TABSCHEMA) = %s AND LOWER(R.TABNAME) = %s";
  509. $params[] = $schema;
  510. $params[] = $table;
  511. }
  512. $sql .= '((' . join(') OR( ', $conditions) . '))';
  513. $sql .= "
  514. ) UNION (
  515. SELECT
  516. LOWER(RTRIM(I.TABSCHEMA)) AS \"SCHEMA\",
  517. LOWER(I.TABNAME) AS \"TABLE\",
  518. LOWER(I.INDNAME) AS CONSTRAINT_NAME,
  519. CASE I.UNIQUERULE WHEN 'U' THEN 'unique' ELSE 'primary' END AS \"TYPE\",
  520. LOWER(C.COLNAME) AS \"COLUMN\",
  521. NULL AS FOREIGN_SCHEMA,
  522. NULL AS FOREIGN_TABLE,
  523. NULL AS FOREIGN_COLUMN,
  524. NULL AS ON_DELETE,
  525. NULL AS ON_UPDATE,
  526. C.COLSEQ
  527. FROM
  528. SYSCAT.INDEXES AS I INNER JOIN
  529. SYSCAT.INDEXCOLUSE AS C ON I.INDSCHEMA = C.INDSCHEMA AND I.INDNAME = C.INDNAME
  530. WHERE
  531. I.UNIQUERULE IN ('U', 'P') AND
  532. ";
  533. $conditions = array();
  534. foreach ($tables as $table) {
  535. if (strpos($table, '.') === FALSE) {
  536. $table = $default_schema . '.' . $table;
  537. }
  538. list ($schema, $table) = explode('.', strtolower($table));
  539. $conditions[] = "LOWER(I.TABSCHEMA) = %s AND LOWER(I.TABNAME) = %s";
  540. $params[] = $schema;
  541. $params[] = $table;
  542. }
  543. $sql .= '((' . join(') OR( ', $conditions) . '))';
  544. $sql .= "
  545. )
  546. ORDER BY 4, 1, 2, 3, 11";
  547. $result = $this->database->query($sql, $params);
  548. $last_name = '';
  549. $last_table = '';
  550. $last_type = '';
  551. foreach ($result as $row) {
  552. if ($row['constraint_name'] != $last_name) {
  553. if ($last_name) {
  554. if ($last_type == 'foreign' || $last_type == 'unique') {
  555. $keys[$last_table][$last_type][] = $temp;
  556. } else {
  557. $keys[$last_table][$last_type] = $temp;
  558. }
  559. }
  560. $temp = array();
  561. if ($row['type'] == 'foreign') {
  562. $temp['column'] = $row['column'];
  563. $temp['foreign_table'] = $row['foreign_table'];
  564. if ($row['foreign_schema'] != $default_schema) {
  565. $temp['foreign_table'] = $row['foreign_schema'] . '.' . $temp['foreign_table'];
  566. }
  567. $temp['foreign_column'] = $row['foreign_column'];
  568. $temp['on_delete'] = 'no_action';
  569. $temp['on_update'] = 'no_action';
  570. if (!empty($row['on_delete'])) {
  571. $temp['on_delete'] = $row['on_delete'];
  572. }
  573. if (!empty($row['on_update'])) {
  574. $temp['on_update'] = $row['on_update'];
  575. }
  576. } else {
  577. $temp[] = $row['column'];
  578. }
  579. $last_table = $row['table'];
  580. if ($row['schema'] != $default_schema) {
  581. $last_table = $row['schema'] . '.' . $last_table;
  582. }
  583. $last_name = $row['constraint_name'];
  584. $last_type = $row['type'];
  585. } else {
  586. $temp[] = $row['column'];
  587. }
  588. }
  589. if (isset($temp)) {
  590. if ($last_type == 'foreign' || $last_type == 'unique') {
  591. $keys[$last_table][$last_type][] = $temp;
  592. } else {
  593. $keys[$last_table][$last_type] = $temp;
  594. }
  595. }
  596. return $keys;
  597. }
  598. /**
  599. * Gets the `PRIMARY KEY`, `FOREIGN KEY` and `UNIQUE` key constraints from the database
  600. *
  601. * @return void
  602. */
  603. private function fetchKeys()
  604. {
  605. if ($this->keys) {
  606. return;
  607. }
  608. switch ($this->database->getType()) {
  609. case 'db2':
  610. $keys = $this->fetchDB2Keys();
  611. break;
  612. case 'mssql':
  613. $keys = $this->fetchMSSQLKeys();
  614. break;
  615. case 'mysql':
  616. $keys = $this->fetchMySQLKeys();
  617. break;
  618. case 'oracle':
  619. $keys = $this->fetchOracleKeys();
  620. break;
  621. case 'postgresql':
  622. $keys = $this->fetchPostgreSQLKeys();
  623. break;
  624. case 'sqlite':
  625. $keys = $this->fetchSQLiteKeys();
  626. break;
  627. }
  628. $this->keys = $keys;
  629. if ($this->cache) {
  630. $this->cache->set($this->makeCachePrefix() . 'keys', $this->keys);
  631. }
  632. }
  633. /**
  634. * Gets the column info from a MSSQL database
  635. *
  636. * @param string $table The table to fetch the column info for
  637. * @return array The column info for the table specified - see ::getColumnInfo() for details
  638. */
  639. private function fetchMSSQLColumnInfo($table)
  640. {
  641. $column_info = array();
  642. $schema = 'dbo';
  643. if (strpos($table, '.') !== FALSE) {
  644. list ($schema, $table) = explode('.', $table);
  645. }
  646. $data_type_mapping = array(
  647. 'bit' => 'boolean',
  648. 'tinyint' => 'integer',
  649. 'smallint' => 'integer',
  650. 'int' => 'integer',
  651. 'bigint' => 'integer',
  652. 'timestamp' => 'integer',
  653. 'datetime' => 'timestamp',
  654. 'smalldatetime' => 'timestamp',
  655. 'datetime2' => 'timestamp',
  656. 'date' => 'date',
  657. 'time' => 'time',
  658. 'varchar' => 'varchar',
  659. 'nvarchar' => 'varchar',
  660. 'uniqueidentifier' => 'varchar',
  661. 'char' => 'char',
  662. 'nchar' => 'char',
  663. 'real' => 'float',
  664. 'float' => 'float',
  665. 'money' => 'float',
  666. 'smallmoney' => 'float',
  667. 'decimal' => 'float',
  668. 'numeric' => 'float',
  669. 'binary' => 'blob',
  670. 'varbinary' => 'blob',
  671. 'image' => 'blob',
  672. 'text' => 'text',
  673. 'ntext' => 'text',
  674. 'xml' => 'text'
  675. );
  676. $max_min_values = array(
  677. 'tinyint' => array('min' => new fNumber(0), 'max' => new fNumber(255)),
  678. 'smallint' => array('min' => new fNumber(-32768), 'max' => new fNumber(32767)),
  679. 'int' => array('min' => new fNumber(-2147483648), 'max' => new fNumber(2147483647)),
  680. 'bigint' => array('min' => new fNumber('-9223372036854775808'), 'max' => new fNumber('9223372036854775807')),
  681. 'smallmoney' => array('min' => new fNumber('-214748.3648'), 'max' => new fNumber('214748.3647')),
  682. 'money' => array('min' => new fNumber('-922337203685477.5808'), 'max' => new fNumber('922337203685477.5807'))
  683. );
  684. // Get the column info
  685. $sql = "SELECT
  686. LOWER(c.column_name) AS 'column',
  687. c.data_type AS 'type',
  688. c.is_nullable AS nullable,
  689. c.column_default AS 'default',
  690. c.character_maximum_length AS max_length,
  691. c.numeric_precision AS precision,
  692. c.numeric_scale AS decimal_places,
  693. CASE
  694. WHEN
  695. COLUMNPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), c.column_name, 'IsIdentity') = 1 AND
  696. OBJECTPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), 'IsMSShipped') = 0
  697. THEN '1'
  698. ELSE '0'
  699. END AS auto_increment,
  700. cc.check_clause AS 'constraint',
  701. CAST(ex.value AS VARCHAR(7500)) AS 'comment'
  702. FROM
  703. INFORMATION_SCHEMA.COLUMNS AS c LEFT JOIN
  704. INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu ON
  705. c.column_name = ccu.column_name AND
  706. c.table_name = ccu.table_name AND
  707. c.table_catalog = ccu.table_catalog LEFT JOIN
  708. INFORMATION_SCHEMA.CHECK_CONSTRAINTS AS cc ON
  709. ccu.constraint_name = cc.constraint_name AND
  710. ccu.constraint_catalog = cc.constraint_catalog";
  711. if (version_compare($this->database->getVersion(), 9, '<')) {
  712. $sql .= " LEFT JOIN sysproperties AS ex ON ex.id = OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)) AND ex.smallid = COLUMNPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), c.column_name, 'ColumnId') AND ex.name = 'MS_Description' AND OBJECTPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), 'IsMsShipped') = 0 ";
  713. } else {
  714. $sql .= " LEFT JOIN SYS.EXTENDED_PROPERTIES AS ex ON ex.major_id = OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)) AND ex.minor_id = COLUMNPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), c.column_name, 'ColumnId') AND ex.name = 'MS_Description' AND OBJECTPROPERTY(OBJECT_ID(QUOTENAME(c.table_schema) + '.' + QUOTENAME(c.table_name)), 'IsMsShipped') = 0 ";
  715. }
  716. $sql .= "
  717. WHERE
  718. LOWER(c.table_name) = %s AND
  719. LOWER(c.table_schema) = %s AND
  720. c.table_catalog = DB_NAME()";
  721. $result = $this->database->query($sql, strtolower($table), strtolower($schema));
  722. foreach ($result as $row) {
  723. $info = array();
  724. foreach ($data_type_mapping as $data_type => $mapped_data_type) {
  725. if (stripos($row['type'], $data_type) === 0) {
  726. if (isset($max_min_values[$data_type])) {
  727. $info['min_value'] = $max_min_values[$data_type]['min'];
  728. $info['max_value'] = $max_min_values[$data_type]['max'];
  729. }
  730. $info['type'] = $mapped_data_type;
  731. break;
  732. }
  733. }
  734. // Handle decimal places and min/max for numeric/decimals
  735. if (in_array(strtolower($row['type']), array('decimal', 'numeric'))) {
  736. $info['decimal_places'] = $row['decimal_places'];
  737. $before_digits = str_pad('', $row['precision'] - $row['decimal_places'], '9');
  738. $after_digits = str_pad('', $row['decimal_places'], '9');
  739. $max_min = $before_digits . ($after_digits ? '.' : '') . $after_digits;
  740. $info['min_value'] = new fNumber('-' . $max_min);
  741. $info['max_value'] = new fNumber($max_min);
  742. }
  743. if (!isset($info['type'])) {
  744. $info['type'] = $row['type'];
  745. }
  746. // Handle decimal places for money/smallmoney
  747. if (in_array($row['type'], array('money', 'smallmoney'))) {
  748. $info['decimal_places'] = 2;
  749. }
  750. // Handle the special data for varchar columns
  751. if (in_array($info['type'], array('char', 'varchar', 'text', 'blob'))) {
  752. if ($row['type'] == 'uniqueidentifier') {
  753. $row['max_length'] = 32;
  754. } elseif ($row['max_length'] == -1) {
  755. $row['max_length'] = $row['type'] == 'nvarchar' ? 1073741823 : 2147483647;
  756. }
  757. $info['max_length'] = $row['max_length'];
  758. }
  759. // If the column has a constraint, look for valid values
  760. if (in_array($info['type'], array('char', 'varchar')) && !empty($row['constraint'])) {
  761. if (preg_match('#^\(((?:(?: OR )?\[[^\]]+\]\s*=\s*\'(?:\'\'|[^\']+)*\')+)\)$#D', $row['constraint'], $matches)) {
  762. $valid_values = explode(' OR ', $matches[1]);
  763. foreach ($valid_values as $key => $value) {
  764. $value = preg_replace('#^\s*\[' . preg_quote($row['column'], '#') . '\]\s*=\s*\'(.*)\'\s*$#', '\1', $value);
  765. $valid_values[$key] = str_replace("''", "'", $value);
  766. }
  767. // SQL Server turns CHECK constraint values into a reversed list, so we fix it here
  768. $info['valid_values'] = array_reverse($valid_values);
  769. }
  770. }
  771. // Handle auto increment
  772. if ($row['auto_increment']) {
  773. $info['auto_increment'] = TRUE;
  774. }
  775. // Handle default values
  776. if ($row['default'] !== NULL) {
  777. if ($row['default'] == '(getdate())') {
  778. $info['default'] = 'CURRENT_TIMESTAMP';
  779. } elseif (in_array($info['type'], array('char', 'varchar', 'text', 'timestamp')) ) {
  780. $info['default'] = substr($row['default'], 2, -2);
  781. } elseif ($info['type'] == 'boolean') {
  782. $info['default'] = (boolean) substr($row['default'], 2, -2);
  783. } elseif (in_array($info['type'], array('integer', 'float')) ) {
  784. $info['default'] = str_replace(array('(', ')'), '', $row['default']);
  785. } else {
  786. $info['default'] = pack('H*', substr($row['default'], 3, -1));
  787. }
  788. }
  789. // Handle not null
  790. $info['not_null'] = ($row['nullable'] == 'NO') ? TRUE : FALSE;
  791. $info['comment'] = $row['comment'];
  792. $column_info[$row['column']] = $info;
  793. }
  794. return $column_info;
  795. }
  796. /**
  797. * Fetches the key info for an MSSQL database
  798. *
  799. * @return array The key info arrays for every table in the database - see ::getKeys() for details
  800. */
  801. private function fetchMSSQLKeys()
  802. {
  803. $keys = array();
  804. $tables = $this->getTables();
  805. foreach ($tables as $table) {
  806. $keys[$table] = array();
  807. $keys[$table]['primary'] = array();
  808. $keys[$table]['unique'] = array();
  809. $keys[$table]['foreign'] = array();
  810. }
  811. $sql = "SELECT
  812. LOWER(c.table_schema) AS \"schema\",
  813. LOWER(c.table_name) AS \"table\",
  814. kcu.constraint_name AS constraint_name,
  815. CASE c.constraint_type
  816. WHEN 'PRIMARY KEY' THEN 'primary'
  817. WHEN 'FOREIGN KEY' THEN 'foreign'
  818. WHEN 'UNIQUE' THEN 'unique'
  819. END AS 'type',
  820. LOWER(kcu.column_name) AS 'column',
  821. LOWER(ccu.table_schema) AS foreign_schema,
  822. LOWER(ccu.table_name) AS foreign_table,
  823. LOWER(ccu.column_name) AS foreign_column,
  824. REPLACE(LOWER(rc.delete_rule), ' ', '_') AS on_delete,
  825. REPLACE(LOWER(rc.update_rule), ' ', '_') AS on_update
  826. FROM
  827. INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS c INNER JOIN
  828. INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu ON
  829. c.table_name = kcu.table_name AND
  830. c.constraint_name = kcu.constraint_name LEFT JOIN
  831. INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc ON
  832. c.constraint_name = rc.constraint_name LEFT JOIN
  833. INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu ON
  834. ccu.constraint_name = rc.unique_constraint_name
  835. WHERE
  836. c.constraint_catalog = DB_NAME() AND
  837. c.table_name != 'sysdiagrams'
  838. ORDER BY
  839. LOWER(c.table_schema),
  840. LOWER(c.table_name),
  841. c.constraint_type,
  842. LOWER(kcu.constraint_name),
  843. kcu.ordinal_position,
  844. LOWER(kcu.column_name)";
  845. $result = $this->database->query($sql);
  846. $last_name = '';
  847. $last_table = '';
  848. $last_type = '';
  849. foreach ($result as $row) {
  850. if ($row['constraint_name'] != $last_name) {
  851. if ($last_name) {
  852. if ($last_type == 'foreign' || $last_type == 'unique') {
  853. if (!isset($keys[$last_table][$last_type])) {
  854. $keys[$last_table][$last_type] = array();
  855. }
  856. $keys[$last_table][$last_type][] = $temp;
  857. } else {
  858. $keys[$last_table][$last_type] = $temp;
  859. }
  860. }
  861. $temp = array();
  862. if ($row['type'] == 'foreign') {
  863. $temp['column'] = $row['column'];
  864. $temp['foreign_table'] = $row['foreign_table'];
  865. if ($row['foreign_schema'] != 'dbo') {
  866. $temp['foreign_table'] = $row['foreign_schema'] . '.' . $temp['foreign_table'];
  867. }
  868. $temp['foreign_column'] = $row['foreign_column'];
  869. $temp['on_delete'] = 'no_action';
  870. $temp['on_update'] = 'no_action';
  871. if (!empty($row['on_delete'])) {
  872. $temp['on_delete'] = $row['on_delete'];
  873. }
  874. if (!empty($row['on_update'])) {
  875. $temp['on_update'] = $row['on_update'];
  876. }
  877. } else {
  878. $temp[] = $row['column'];
  879. }
  880. $last_table = $row['table'];
  881. if ($row['schema'] != 'dbo') {
  882. $last_table = $row['schema'] . '.' . $last_table;
  883. }
  884. $last_name = $row['constraint_name'];
  885. $last_type = $row['type'];
  886. } else {
  887. $temp[] = $row['column'];
  888. }
  889. }
  890. if (isset($temp)) {
  891. if ($last_type == 'foreign' || $last_type == 'unique') {
  892. if (!isset($keys[$last_table][$last_type])) {
  893. $keys[$last_table][$last_type] = array();
  894. }
  895. $keys[$last_table][$last_type][] = $temp;
  896. } else {
  897. $keys[$last_table][$last_type] = $temp;
  898. }
  899. }
  900. return $keys;
  901. }
  902. /**
  903. * Gets the column info from a MySQL database
  904. *
  905. * @param string $table The table to fetch the column info for
  906. * @return array The column info for the table specified - see ::getColumnInfo() for details
  907. */
  908. private function fetchMySQLColumnInfo($table)
  909. {
  910. $data_type_mapping = array(
  911. 'tinyint' => 'integer',
  912. 'smallint' => 'integer',
  913. 'mediumint' => 'integer',
  914. 'int' => 'integer',
  915. 'bigint' => 'integer',
  916. 'datetime' => 'timestamp',
  917. 'timestamp' => 'timestamp',
  918. 'date' => 'date',
  919. 'time' => 'time',
  920. 'enum' => 'varchar',
  921. 'set' => 'varchar',
  922. 'varchar' => 'varchar',
  923. 'char' => 'char',
  924. 'float' => 'float',
  925. 'double' => 'float',
  926. 'decimal' => 'float',
  927. 'binary' => 'blob',
  928. 'varbinary' => 'blob',
  929. 'tinyblob' => 'blob',
  930. 'blob' => 'blob',
  931. 'mediumblob' => 'blob',
  932. 'longblob' => 'blob',
  933. 'tinytext' => 'text',
  934. 'text' => 'text',
  935. 'mediumtext' => 'text',
  936. 'longtext' => 'text',
  937. 'geometry' => 'blob',
  938. 'point' => 'blob',
  939. 'linestring' => 'blob',
  940. 'polygon' => 'blob'
  941. );
  942. $max_min_values = array(
  943. 'tinyint' => array('min' => new fNumber(-128), 'max' => new fNumber(127)),
  944. 'unsigned tinyint' => array('min' => new fNumber(0), 'max' => new fNumber(255)),
  945. 'smallint' => array('min' => new fNumber(-32768), 'max' => new fNumber(32767)),
  946. 'unsigned smallint' => array('min' => new fNumber(0), 'max' => new fNumber(65535)),
  947. 'mediumint' => array('min' => new fNumber(-8388608), 'max' => new fNumber(8388607)),
  948. 'unsigned mediumint' => array('min' => new fNumber(0), 'max' => new fNumber(16777215)),
  949. 'int' => array('min' => new fNumber(-2147483648), 'max' => new fNumber(2147483647)),
  950. 'unsigned int' => array('min' => new fNumber(0), 'max' => new fNumber('4294967295')),
  951. 'bigint' => array('min' => new fNumber('-9223372036854775808'), 'max' => new fNumber('9223372036854775807')),
  952. 'unsigned bigint' => array('min' => new fNumber(0), 'max' => new fNumber('18446744073709551615'))
  953. );
  954. $column_info = array();
  955. $result = $this->database->query('SHOW CREATE TABLE %r', $table);
  956. try {
  957. $row = $result->fetchRow();
  958. $create_sql = $row['Create Table'];
  959. } catch (fNoRowsException $e) {
  960. return array();
  961. }
  962. 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);
  963. foreach ($matches as $match) {
  964. $info = array();
  965. foreach ($data_type_mapping as $data_type => $mapped_data_type) {
  966. if (stripos($match[2], $data_type) === 0) {
  967. if ($match[2] == 'tinyint' && $match[3] == 1) {
  968. $mapped_data_type = 'boolean';
  969. } elseif (preg_match('#((?:unsigned )?(?:tiny|small|medium|big)?int)#', (isset($match[4]) ? $match[4] . ' ' : '') . $data_type, $int_match)) {
  970. if (isset($max_min_values[$int_match[1]])) {
  971. $info['min_value'] = $max_min_values[$int_match[1]]['min'];
  972. $info['max_value'] = $max_min_values[$int_match[1]]['max'];
  973. }
  974. }
  975. $info['type'] = $mapped_data_type;
  976. break;
  977. }
  978. }
  979. if (!isset($info['type'])) {
  980. $info['type'] = preg_replace('#^([a-z ]+).*$#iD', '\1', $match[2]);
  981. }
  982. switch ($match[2]) {
  983. case 'tinyblob':
  984. case 'tinytext':
  985. $info['max_length'] = 255;
  986. break;
  987. case 'blob':
  988. case 'text':
  989. $info['max_length'] = 65535;
  990. break;
  991. case 'mediumblob':
  992. case 'mediumtext':
  993. $info['max_length'] = 16777215;
  994. break;
  995. case 'longblob':
  996. case 'longtext':
  997. $info['max_length'] = 4294967295;
  998. break;
  999. }
  1000. if (stripos($match[2], 'enum') === 0) {
  1001. $info['valid_values'] = preg_replace("/^'|'\$/D", '', explode(",", $match[3]));
  1002. $match[3] = 0;
  1003. foreach ($info['valid_values'] as $valid_value) {
  1004. if (strlen(utf8_decode($valid_value)) > $match[3]) {
  1005. $match[3] = strlen(utf8_decode($valid_value));
  1006. }
  1007. }
  1008. }
  1009. // The set data type is currently only supported as a varchar
  1010. // with a max length of all valid values concatenated by ,s
  1011. if (stripos($match[2], 'set') === 0) {
  1012. $values = preg_replace("/^'|'\$/D", '', explode(",", $match[3]));
  1013. $match[3] = strlen(join(',', $values));
  1014. }
  1015. // Type specific information
  1016. if (in_array($info['type'], array('char', 'varchar'))) {
  1017. $info['max_length'] = $match[3];
  1018. }
  1019. // Grab the number of decimal places
  1020. if (stripos($match[2], 'decimal') === 0) {
  1021. if (preg_match('#^\s*(\d+)\s*,\s*(\d+)\s*$#D', $match[3], $data_type_info)) {
  1022. $info['decimal_places'] = $data_type_info[2];
  1023. $before_digits = str_pad('', $data_type_info[1] - $info['decimal_places'], '9');
  1024. $after_digits = str_pad('', $info['decimal_places'], '9');
  1025. $max_min = $before_digits . ($after_digits ? '.' : '') . $after_digits;
  1026. $info['min_value'] = new fNumber('-' . $max_min);
  1027. $info['max_value'] = new fNumber($max_min);
  1028. }
  1029. }
  1030. // Not null
  1031. $info['not_null'] = (!empty($match[5])) ? TRUE : FALSE;
  1032. // Default values
  1033. if (!empty($match[6]) && $match[6] != 'NULL') {
  1034. $info['default'] = preg_replace("/^'|'\$/D", '', $match[6]);
  1035. }
  1036. if ($info['type'] == 'boolean' && isset($info['default'])) {
  1037. $info['default'] = (boolean) $info['default'];
  1038. }
  1039. // Auto increment fields
  1040. if (!empty($match[7])) {
  1041. $info['auto_increment'] = TRUE;
  1042. }
  1043. // Column comments
  1044. if (!empty($match[8])) {
  1045. $info['comment'] = str_replace("''", "'", substr($match[8], 10, -1));
  1046. }
  1047. $column_info[strtolower($match[1])] = $info;
  1048. }
  1049. return $column_info;
  1050. }
  1051. /**
  1052. * Fetches the keys for a MySQL database
  1053. *
  1054. * @return array The keys arrays for every table in the database - see ::getKeys() for details
  1055. */
  1056. private function fetchMySQLKeys()
  1057. {
  1058. $tables = $this->getTables();
  1059. $keys = array();
  1060. foreach ($tables as $table) {
  1061. $keys[$table] = array();
  1062. $keys[$table]['primary'] = array();
  1063. $keys[$table]['foreign'] = array();
  1064. $keys[$table]['unique'] = array();
  1065. $result = $this->database->query('SHOW CREATE TABLE %r', $table);
  1066. $row = $result->fetchRow();
  1067. // Primary keys
  1068. preg_match_all('/PRIMARY KEY\s+\("(.*?)"\),?\n/U', $row['Create Table'], $matches, PREG_SET_ORDER);
  1069. if (!empty($matches)) {
  1070. $keys[$table]['primary'] = explode('","', strtolower($matches[0][1]));
  1071. }
  1072. // Unique keys
  1073. preg_match_all('/UNIQUE KEY\s+"([^"]+)"\s+\("(.*?)"\),?\n/U', $row['Create Table'], $matches, PREG_SET_ORDER);
  1074. foreach ($matches as $match) {
  1075. $keys[$table]['unique'][] = explode('","', strtolower($match[2]));
  1076. }
  1077. // Foreign keys
  1078. 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);
  1079. foreach ($matches as $match) {
  1080. $temp = array(
  1081. 'column' => strtolower($match[1]),
  1082. 'foreign_table' => strtolower($match[2]),
  1083. 'foreign_column' => strtolower($match[3]),
  1084. 'on_delete' => 'no_action',
  1085. 'on_update' => 'no_action'
  1086. );
  1087. if (!empty($match[4])) {
  1088. $temp['on_delete'] = strtolower(str_replace(' ', '_', $match[4]));
  1089. }
  1090. if (!empty($match[5])) {
  1091. $temp['on_update'] = strtolower(str_replace(' ', '_', $match[5]));
  1092. }
  1093. $keys[$table]['foreign'][] = $temp;
  1094. }
  1095. }
  1096. return $keys;
  1097. }
  1098. /**
  1099. * Gets the column info from an Oracle database
  1100. *
  1101. * @param string $table The table to fetch the column info for
  1102. * @return array The column info for the table specified - see ::getColumnInfo() for details
  1103. */
  1104. private function fetchOracleColumnInfo($table)
  1105. {
  1106. $table = strtolower($table);
  1107. $schema = strtolower($this->database->getUsername());
  1108. if (strpos($table, '.') !== FALSE) {
  1109. list ($schema, $table) = explode('.', $table);
  1110. }
  1111. $column_info = array();
  1112. $data_type_mapping = array(
  1113. 'boolean' => 'boolean',
  1114. 'number' => 'integer',
  1115. 'integer' => 'integer',
  1116. 'timestamp' => 'timestamp',
  1117. 'date' => 'date',
  1118. 'varchar2' => 'varchar',
  1119. 'nvarchar2' => 'varchar',
  1120. 'char' => 'char',
  1121. 'nchar' => 'char',
  1122. 'float' => 'float',
  1123. 'binary_float' => 'float',
  1124. 'binary_double' => 'float',
  1125. 'blob' => 'blob',
  1126. 'bfile' => 'varchar',
  1127. 'clob' => 'text',
  1128. 'nclob' => 'text'
  1129. );
  1130. $sql = "SELECT
  1131. LOWER(ATC.COLUMN_NAME) COLUMN_NAME,
  1132. CASE
  1133. WHEN
  1134. ATC.DATA_TYPE = 'NUMBER' AND
  1135. ATC.DATA_PRECISION IS NULL AND
  1136. ATC.DATA_SCALE = 0
  1137. THEN
  1138. 'integer'
  1139. WHEN
  1140. ATC.DATA_TYPE = 'NUMBER' AND
  1141. ATC.DATA_PRECISION = 1 AND
  1142. ATC.DATA_SCALE = 0
  1143. THEN
  1144. 'boolean'
  1145. WHEN
  1146. ATC.DATA_TYPE = 'NUMBER' AND
  1147. ATC.DATA_PRECISION IS NOT NULL AND
  1148. ATC.DATA_SCALE != 0 AND
  1149. ATC.DATA_SCALE IS NOT NULL
  1150. THEN
  1151. 'float'
  1152. ELSE
  1153. LOWER(ATC.DATA_TYPE)
  1154. END DATA_TYPE,
  1155. CASE
  1156. WHEN
  1157. ATC.CHAR_LENGTH <> 0
  1158. THEN
  1159. ATC.CHAR_LENGTH
  1160. WHEN
  1161. ATC.DATA_TYPE = 'NUMBER' AND
  1162. ATC.DATA_PRECISION != 1 AND
  1163. ATC.DATA_SCALE != 0 AND
  1164. ATC.DATA_PRECISION IS NOT NULL
  1165. THEN
  1166. ATC.DATA_SCALE
  1167. ELSE
  1168. NULL
  1169. END LENGTH,
  1170. ATC.DATA_PRECISION PRECISION,
  1171. ATC.NULLABLE,
  1172. ATC.DATA_DEFAULT,
  1173. AC.SEARCH_CONDITION CHECK_CONSTRAINT,
  1174. ACCM.COMMENTS
  1175. FROM
  1176. ALL_TAB_COLUMNS ATC LEFT JOIN
  1177. ALL_CONS_COLUMNS ACC ON
  1178. ATC.OWNER = ACC.OWNER AND
  1179. ATC.COLUMN_NAME = ACC.COLUMN_NAME AND
  1180. ATC.TABLE_NAME = ACC.TABLE_NAME AND
  1181. ACC.POSITION IS NULL LEFT JOIN
  1182. ALL_CONSTRAINTS AC ON
  1183. AC.OWNER = ACC.OWNER AND
  1184. AC.CONSTRAINT_NAME = ACC.CONSTRAINT_NAME AND
  1185. AC.CONSTRAINT_TYPE = 'C' AND
  1186. AC.STATUS = 'ENABLED' LEFT JOIN
  1187. ALL_COL_COMMENTS ACCM ON
  1188. ATC.OWNER = ACCM.OWNER AND
  1189. ATC.COLUMN_NAME = ACCM.COLUMN_NAME AND
  1190. ATC.TABLE_NAME = ACCM.TABLE_NAME
  1191. WHERE
  1192. LOWER(ATC.TABLE_NAME) = %s AND
  1193. LOWER(ATC.OWNER) = %s
  1194. ORDER BY
  1195. ATC.TABLE_NAME ASC,
  1196. ATC.COLUMN_ID ASC";
  1197. $result = $this->database->query($sql, $table, $schema);
  1198. foreach ($result as $row) {
  1199. $column = $row['column_name'];
  1200. // Since Oracle stores check constraints in LONG columns, it is
  1201. // not possible to check or modify the constraints in SQL which
  1202. // ends up causing multiple rows with duplicate data except for
  1203. // the check constraint
  1204. $duplicate = FALSE;
  1205. if (isset($column_info[$column])) {
  1206. $info = $column_info[$column];
  1207. $duplicate = TRUE;
  1208. } else {
  1209. $info = array();
  1210. }
  1211. if (!$duplicate) {
  1212. // Get the column type
  1213. foreach ($data_type_mapping as $data_type => $mapped_data_type) {
  1214. if (stripos($row['data_type'], $data_type) === 0) {
  1215. $info['type'] = $mapped_data_type;
  1216. break;
  1217. }
  1218. }
  1219. if (!isset($info['type'])) {
  1220. $info['type'] = $row['data_type'];
  1221. }
  1222. if (in_array($info['type'], array('blob', 'text'))) {
  1223. $info['max_length'] = 4294967295;
  1224. }
  1225. if ($row['data_type'] == 'float' && $row['precision']) {
  1226. $row['length'] = (int) $row['length'];
  1227. $before_digits = str_pad('', $row['precision'] - $row['length'], '9');
  1228. $after_digits = str_pad('', $row['length'], '9');
  1229. $max_min = $before_digits . ($after_digits ? '.' : '') . $after_digits;
  1230. $info['min_value'] = new fNumber('-' . $max_min);
  1231. $info['max_value'] = new fNumber($max_min);
  1232. }
  1233. // Handle the length of decimal/numeric fields
  1234. if ($info['type'] == 'float' && $row['length']) {
  1235. $info['decimal_places'] = (int) $row['length'];
  1236. }
  1237. // Handle the special data for varchar fields
  1238. if (in_array($info['type'], array('char', 'varchar'))) {
  1239. $info['max_length'] = (int) $row['length'];
  1240. }
  1241. }
  1242. // Handle check constraints that are just simple lists
  1243. if (in_array($info['type'], array('varchar', 'char')) && $row['check_constraint']) {
  1244. if (preg_match('/^\s*"?' . preg_quote($column, '/') . '"?\s+in\s+\((.*?)\)\s*$/i', $row['check_constraint'], $match)) {
  1245. if (preg_match_all("/(?<!')'((''|[^']+)*)'/", $match[1], $matches, PREG_PATTERN_ORDER)) {
  1246. $info['valid_values'] = str_replace("''", "'", $matches[1]);
  1247. }
  1248. } elseif (preg_match('/^\s*"?' . preg_quote($column, '/') . '"?\s*=\s*\'((\'\'|[^\']+)*)\'(\s+OR\s+"?' . preg_quote($column, '/') . '"?\s*=\s*\'((\'\'|[^\']+)*)\')*\s*$/i', $row['check_constraint'], $match)) {
  1249. if (preg_match_all("/(?<!')'((''|[^']+)*)'/", $row['check_constraint'], $matches, PREG_PATTERN_ORDER)) {
  1250. $info['valid_values'] = str_replace("''", "'", $matches[1]);
  1251. }
  1252. }
  1253. }
  1254. if (!$duplicate) {
  1255. // Handle default values
  1256. if ($row['data_default'] !== NULL && trim($row['data_default']) != 'NULL') {
  1257. if (in_array($info['type'], array('date', 'time', 'timestamp')) && $row['data_default'][0] != "'") {
  1258. $info['default'] = trim(preg_replace('#^SYS#', 'CURRENT_', $row['data_default']));
  1259. } elseif (in_array($info['type'], array('char', 'varchar', 'text', 'date', 'time', 'timestamp'))) {
  1260. $info['default'] = str_replace("''", "'", substr(trim($row['data_defaul…

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