root/fSchema.php

Revision 659, 75.9 kB (checked in by wbond, 1 year ago)

Fixed tickets #225 and #270 - Loading fActiveRecord objects by multi-column primary key now works even if the columns are in a different order, one-to-one relationships now are properly detected the ORM API was improved to handle them

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