Coverage for Doctrine_Export_Mysql

Back to coverage report

1 <?php
2 /*
3  *  $Id: Mysql.php 2702 2007-10-03 21:43:22Z Jonathan.Wage $
4  *
5  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16  *
17  * This software consists of voluntary contributions made by many individuals
18  * and is licensed under the LGPL. For more information, see
19  * <http://www.phpdoctrine.com>.
20  */
21 Doctrine::autoload('Doctrine_Export');
22 /**
23  * Doctrine_Export_Mysql
24  *
25  * @package     Doctrine
26  * @subpackage  Export
27  * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
28  * @author      Lukas Smith <smith@pooteeweet.org> (PEAR MDB2 library)
29  * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
30  * @link        www.phpdoctrine.com
31  * @since       1.0
32  * @version     $Revision: 2702 $
33  */
34 class Doctrine_Export_Mysql extends Doctrine_Export
35 {
36    /**
37      * create a new database
38      *
39      * @param string $name name of the database that should be created
40      * @return string
41      */
42     public function createDatabaseSql($name)
43     {
44         return 'CREATE DATABASE ' . $this->conn->quoteIdentifier($name, true);
45     }
46     /**
47      * drop an existing database
48      *
49      * @param string $name name of the database that should be dropped
50      * @return string
51      */
52     public function dropDatabaseSql($name)
53     {
54         return 'DROP DATABASE ' . $this->conn->quoteIdentifier($name);
55     }
56     /**
57      * create a new table
58      *
59      * @param string $name   Name of the database that should be created
60      * @param array $fields  Associative array that contains the definition of each field of the new table
61      *                       The indexes of the array entries are the names of the fields of the table an
62      *                       the array entry values are associative arrays like those that are meant to be
63      *                       passed with the field definitions to get[Type]Declaration() functions.
64      *                          array(
65      *                              'id' => array(
66      *                                  'type' => 'integer',
67      *                                  'unsigned' => 1
68      *                                  'notnull' => 1
69      *                                  'default' => 0
70      *                              ),
71      *                              'name' => array(
72      *                                  'type' => 'text',
73      *                                  'length' => 12
74      *                              ),
75      *                              'password' => array(
76      *                                  'type' => 'text',
77      *                                  'length' => 12
78      *                              )
79      *                          );
80      * @param array $options  An associative array of table options:
81      *                          array(
82      *                              'comment' => 'Foo',
83      *                              'charset' => 'utf8',
84      *                              'collate' => 'utf8_unicode_ci',
85      *                              'type'    => 'innodb',
86      *                          );
87      *
88      * @return void
89      */
90     public function createTableSql($name, array $fields, array $options = array()) 
91     {
92         if ( ! $name)
93             throw new Doctrine_Export_Exception('no valid table name specified');
94
95         if (empty($fields)) {
96             throw new Doctrine_Export_Exception('no fields specified for table "'.$name.'"');
97         }
98         $queryFields = $this->getFieldDeclarationList($fields);
99         
100         // build indexes for all foreign key fields (needed in MySQL!!)
101         if (isset($options['foreignKeys'])) {
102             foreach ($options['foreignKeys'] as $fk) {
103                 $local = $fk['local'];
104                 $found = false;
105                 if (isset($options['indexes'])) {
106                     foreach ($options['indexes'] as $definition) {
107                         if (is_string($definition['fields'])) {
108                             // Check if index already exists on the column                            
109                             $found = ($local == $definition['fields']);                        
110                         } else if (in_array($local, $definition['fields']) && count($definition['fields']) === 1) {
111                             // Index already exists on the column
112                             $found = true;
113                         }
114                     }
115                 }
116                 if (isset($options['primary']) && !empty($options['primary']) &&
117                         in_array($local, $options['primary'])) {
118                     // field is part of the PK and therefore already indexed
119                     $found = true;
120                 }
121                 
122                 if ( ! $found) {
123                     $options['indexes'][$local] = array('fields' => array($local => array()));
124                 }
125             }
126         }
127
128         // add all indexes
129         if (isset($options['indexes']) && ! empty($options['indexes'])) {
130             foreach($options['indexes'] as $index => $definition) {
131                 $queryFields .= ', ' . $this->getIndexDeclaration($index, $definition);
132             }
133         }
134
135         // attach all primary keys
136         if (isset($options['primary']) && ! empty($options['primary'])) {
137             $keyColumns = array_values($options['primary']);
138             $keyColumns = array_map(array($this->conn, 'quoteIdentifier'), $keyColumns);
139             $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
140         }
141
142         $query = 'CREATE TABLE ' . $this->conn->quoteIdentifier($name, true) . ' (' . $queryFields . ')';
143
144         $optionStrings = array();
145
146         if (isset($options['comment'])) {
147             $optionStrings['comment'] = 'COMMENT = ' . $this->dbh->quote($options['comment'], 'text');
148         }
149         if (isset($options['charset'])) {
150             $optionStrings['charset'] = 'DEFAULT CHARACTER SET ' . $options['charset'];
151             if (isset($options['collate'])) {
152                 $optionStrings['charset'] .= ' COLLATE ' . $options['collate'];
153             }
154         }
155
156         $type = false;
157
158         // get the type of the table
159         if (isset($options['type'])) {
160             $type = $options['type'];
161         } else {
162             $type = $this->conn->getAttribute(Doctrine::ATTR_DEFAULT_TABLE_TYPE);
163         }
164
165         if ($type) {
166             $optionStrings[] = 'ENGINE = ' . $type;
167         }
168
169         if ( ! empty($optionStrings)) {
170             $query.= ' '.implode(' ', $optionStrings);
171         }
172         $sql[] = $query;
173
174         if (isset($options['foreignKeys'])) {
175
176             foreach ((array) $options['foreignKeys'] as $k => $definition) {
177                 if (is_array($definition)) {
178                     $sql[] = $this->createForeignKeySql($name, $definition);
179                 }
180             }
181         }   
182         return $sql;
183     }
184     /**
185      * alter an existing table
186      *
187      * @param string $name         name of the table that is intended to be changed.
188      * @param array $changes     associative array that contains the details of each type
189      *                             of change that is intended to be performed. The types of
190      *                             changes that are currently supported are defined as follows:
191      *
192      *                             name
193      *
194      *                                New name for the table.
195      *
196      *                            add
197      *
198      *                                Associative array with the names of fields to be added as
199      *                                 indexes of the array. The value of each entry of the array
200      *                                 should be set to another associative array with the properties
201      *                                 of the fields to be added. The properties of the fields should
202      *                                 be the same as defined by the Metabase parser.
203      *
204      *
205      *                            remove
206      *
207      *                                Associative array with the names of fields to be removed as indexes
208      *                                 of the array. Currently the values assigned to each entry are ignored.
209      *                                 An empty array should be used for future compatibility.
210      *
211      *                            rename
212      *
213      *                                Associative array with the names of fields to be renamed as indexes
214      *                                 of the array. The value of each entry of the array should be set to
215      *                                 another associative array with the entry named name with the new
216      *                                 field name and the entry named Declaration that is expected to contain
217      *                                 the portion of the field declaration already in DBMS specific SQL code
218      *                                 as it is used in the CREATE TABLE statement.
219      *
220      *                            change
221      *
222      *                                Associative array with the names of the fields to be changed as indexes
223      *                                 of the array. Keep in mind that if it is intended to change either the
224      *                                 name of a field and any other properties, the change array entries
225      *                                 should have the new names of the fields as array indexes.
226      *
227      *                                The value of each entry of the array should be set to another associative
228      *                                 array with the properties of the fields to that are meant to be changed as
229      *                                 array entries. These entries should be assigned to the new values of the
230      *                                 respective properties. The properties of the fields should be the same
231      *                                 as defined by the Metabase parser.
232      *
233      *                            Example
234      *                                array(
235      *                                    'name' => 'userlist',
236      *                                    'add' => array(
237      *                                        'quota' => array(
238      *                                            'type' => 'integer',
239      *                                            'unsigned' => 1
240      *                                        )
241      *                                    ),
242      *                                    'remove' => array(
243      *                                        'file_limit' => array(),
244      *                                        'time_limit' => array()
245      *                                    ),
246      *                                    'change' => array(
247      *                                        'name' => array(
248      *                                            'length' => '20',
249      *                                            'definition' => array(
250      *                                                'type' => 'text',
251      *                                                'length' => 20,
252      *                                            ),
253      *                                        )
254      *                                    ),
255      *                                    'rename' => array(
256      *                                        'sex' => array(
257      *                                            'name' => 'gender',
258      *                                            'definition' => array(
259      *                                                'type' => 'text',
260      *                                                'length' => 1,
261      *                                                'default' => 'M',
262      *                                            ),
263      *                                        )
264      *                                    )
265      *                                )
266      *
267      * @param boolean $check     indicates whether the function should just check if the DBMS driver
268      *                           can perform the requested table alterations if the value is true or
269      *                           actually perform them otherwise.
270      * @return boolean
271      */
272     public function alterTableSql($name, array $changes, $check = false)
273     {
274         if ( ! $name) {
275             throw new Doctrine_Export_Exception('no valid table name specified');
276         }
277         foreach ($changes as $changeName => $change) {
278             switch ($changeName) {
279                 case 'add':
280                 case 'remove':
281                 case 'change':
282                 case 'rename':
283                 case 'name':
284                     break;
285                 default:
286                     throw new Doctrine_Export_Exception('change type "' . $changeName . '" not yet supported');
287             }
288         }
289
290         if ($check) {
291             return true;
292         }
293
294         $query = '';
295         if ( ! empty($changes['name'])) {
296             $change_name = $this->conn->quoteIdentifier($changes['name']);
297             $query .= 'RENAME TO ' . $change_name;
298         }
299
300         if ( ! empty($changes['add']) && is_array($changes['add'])) {
301             foreach ($changes['add'] as $fieldName => $field) {
302                 if ($query) {
303                     $query.= ', ';
304                 }
305                 $query.= 'ADD ' . $this->getDeclaration($field['type'], $fieldName, $field);
306             }
307         }
308
309         if ( ! empty($changes['remove']) && is_array($changes['remove'])) {
310             foreach ($changes['remove'] as $fieldName => $field) {
311                 if ($query) {
312                     $query .= ', ';
313                 }
314                 $fieldName = $this->conn->quoteIdentifier($fieldName);
315                 $query .= 'DROP ' . $fieldName;
316             }
317         }
318
319         $rename = array();
320         if ( ! empty($changes['rename']) && is_array($changes['rename'])) {
321             foreach ($changes['rename'] as $fieldName => $field) {
322                 $rename[$field['name']] = $fieldName;
323             }
324         }
325
326         if ( ! empty($changes['change']) && is_array($changes['change'])) {
327             foreach ($changes['change'] as $fieldName => $field) {
328                 if ($query) {
329                     $query.= ', ';
330                 }
331                 if (isset($rename[$fieldName])) {
332                     $oldFieldName = $rename[$fieldName];
333                     unset($rename[$fieldName]);
334                 } else {
335                     $oldFieldName = $fieldName;
336                 }
337                 $oldFieldName = $this->conn->quoteIdentifier($oldFieldName, true);
338                 $query .= 'CHANGE ' . $oldFieldName . ' ' 
339                         . $this->getDeclaration($field['definition']['type'], $fieldName, $field['definition']);
340             }
341         }
342
343         if ( ! empty($rename) && is_array($rename)) {
344             foreach ($rename as $renameName => $renamedField) {
345                 if ($query) {
346                     $query.= ', ';
347                 }
348                 $field = $changes['rename'][$renamedField];
349                 $renamedField = $this->conn->quoteIdentifier($renamedField, true);
350                 $query .= 'CHANGE ' . $renamedField . ' '
351                         . $this->getDeclaration($field['definition']['type'], $field['name'], $field['definition']);
352             }
353         }
354
355         if ( ! $query) {
356             return false;
357         }
358
359         $name = $this->conn->quoteIdentifier($name, true);
360         
361         return 'ALTER TABLE ' . $name . ' ' . $query;
362     }
363     /**
364      * create sequence
365      *
366      * @param string    $sequenceName name of the sequence to be created
367      * @param string    $start        start value of the sequence; default is 1
368      * @param array     $options  An associative array of table options:
369      *                          array(
370      *                              'comment' => 'Foo',
371      *                              'charset' => 'utf8',
372      *                              'collate' => 'utf8_unicode_ci',
373      *                              'type'    => 'innodb',
374      *                          );
375      * @return boolean
376      */
377     public function createSequence($sequenceName, $start = 1, array $options = array())
378     {
379         $sequenceName   = $this->conn->quoteIdentifier($this->conn->getSequenceName($sequenceName), true);
380         $seqcolName     = $this->conn->quoteIdentifier($this->conn->getAttribute(Doctrine::ATTR_SEQCOL_NAME), true);
381
382         $optionsStrings = array();
383
384         if (isset($options['comment']) && ! empty($options['comment'])) {
385             $optionsStrings['comment'] = 'COMMENT = ' . $this->conn->quote($options['comment'], 'string');
386         }
387
388         if (isset($options['charset']) && ! empty($options['charset'])) {
389             $optionsStrings['charset'] = 'DEFAULT CHARACTER SET ' . $options['charset'];
390
391             if (isset($options['collate'])) {
392                 $optionsStrings['collate'] .= ' COLLATE ' . $options['collate'];
393             }
394         }
395
396         $type = false;
397
398         if (isset($options['type'])) {
399             $type = $options['type'];
400         } else {
401             $type = $this->conn->default_table_type;
402         }
403         if ($type) {
404             $optionsStrings[] = 'ENGINE = ' . $type;
405         }
406
407
408         try {
409             $query  = 'CREATE TABLE ' . $sequenceName
410                     . ' (' . $seqcolName . ' INT NOT NULL AUTO_INCREMENT, PRIMARY KEY ('
411                     . $seqcolName . '))'
412                     . strlen($this->conn->default_table_type) ? ' TYPE = '
413                     . $this->conn->default_table_type : '';
414
415             $res    = $this->conn->exec($query);
416         } catch(Doctrine_Connection_Exception $e) {
417             throw new Doctrine_Export_Exception('could not create sequence table');
418         }
419
420         if ($start == 1)
421             return true;
422
423         $query  = 'INSERT INTO ' . $sequenceName
424                 . ' (' . $seqcolName . ') VALUES (' . ($start - 1) . ')';
425
426         $res    = $this->conn->exec($query);
427
428         // Handle error
429         try {
430             $result = $this->conn->exec('DROP TABLE ' . $sequenceName);
431         } catch(Doctrine_Connection_Exception $e) {
432             throw new Doctrine_Export_Exception('could not drop inconsistent sequence table');
433         }
434
435
436     }
437     /**
438      * Get the stucture of a field into an array
439      *
440      * @author Leoncx
441      * @param string    $table         name of the table on which the index is to be created
442      * @param string    $name          name of the index to be created
443      * @param array     $definition    associative array that defines properties of the index to be created.
444      *                                 Currently, only one property named FIELDS is supported. This property
445      *                                 is also an associative with the names of the index fields as array
446      *                                 indexes. Each entry of this array is set to another type of associative
447      *                                 array that specifies properties of the index that are specific to
448      *                                 each field.
449      *
450      *                                 Currently, only the sorting property is supported. It should be used
451      *                                 to define the sorting direction of the index. It may be set to either
452      *                                 ascending or descending.
453      *
454      *                                 Not all DBMS support index sorting direction configuration. The DBMS
455      *                                 drivers of those that do not support it ignore this property. Use the
456      *                                 function supports() to determine whether the DBMS driver can manage indexes.
457      *
458      *                                 Example
459      *                                    array(
460      *                                        'fields' => array(
461      *                                            'user_name' => array(
462      *                                                'sorting' => 'ASC'
463      *                                                'length' => 10
464      *                                            ),
465      *                                            'last_login' => array()
466      *                                        )
467      *                                    )
468      * @throws PDOException
469      * @return void
470      */
471     public function createIndexSql($table, $name, array $definition)
472     {
473         $table  = $table;
474         $name   = $this->conn->getIndexName($name);
475         $name   = $this->conn->quoteIdentifier($name);
476         $type   = '';
477         if (isset($definition['type'])) {
478             switch (strtolower($definition['type'])) {
479                 case 'fulltext':
480                 case 'unique':
481                     $type = strtoupper($definition['type']) . ' ';
482                 break;
483                 default:
484                     throw new Doctrine_Export_Exception('Unknown index type ' . $definition['type']);
485             }
486         }
487         $query  = 'CREATE ' . $type . 'INDEX ' . $name . ' ON ' . $table;
488         $query .= ' (' . $this->getIndexFieldDeclarationList() . ')';
489
490         return $query;
491     }
492     /** 
493      * getDefaultDeclaration
494      * Obtain DBMS specific SQL code portion needed to set a default value
495      * declaration to be used in statements like CREATE TABLE.
496      *
497      * @param array $field      field definition array
498      * @return string           DBMS specific SQL code portion needed to set a default value
499      */
500     public function getDefaultFieldDeclaration($field)
501     {
502         $default = '';
503         if (isset($field['default']) && $field['length'] <= 255) {
504             if ($field['default'] === '') {
505                 $field['default'] = empty($field['notnull'])
506                     ? null : $this->valid_default_values[$field['type']];
507
508                 if ($field['default'] === ''
509                     && ($this->conn->getAttribute(Doctrine::ATTR_PORTABILITY) & Doctrine::PORTABILITY_EMPTY_TO_NULL)
510                 ) {
511                     $field['default'] = ' ';
512                 }
513             }
514     
515             $default = ' DEFAULT ' . $this->conn->quote($field['default'], $field['type']);
516         }
517         return $default;
518     }
519     /**
520      * Obtain DBMS specific SQL code portion needed to set an index 
521      * declaration to be used in statements like CREATE TABLE.
522      *
523      * @param string $charset       name of the index
524      * @param array $definition     index definition
525      * @return string  DBMS specific SQL code portion needed to set an index
526      */
527     public function getIndexDeclaration($name, array $definition)
528     {
529         $name   = $this->conn->formatter->getIndexName($name);
530         $type   = '';
531         if (isset($definition['type'])) {
532             switch (strtolower($definition['type'])) {
533                 case 'fulltext':
534                 case 'unique':
535                     $type = strtoupper($definition['type']) . ' ';
536                 break;
537                 default:
538                     throw new Doctrine_Export_Exception('Unknown index type ' . $definition['type']);
539             }
540         }
541         
542         if ( ! isset($definition['fields'])) {
543             throw new Doctrine_Export_Exception('No index columns given.');
544         }
545         if ( ! is_array($definition['fields'])) {
546             $definition['fields'] = array($definition['fields']);
547         }
548
549         $query = $type . 'INDEX ' . $this->conn->quoteIdentifier($name);
550
551         $query .= ' (' . $this->getIndexFieldDeclarationList($definition['fields']) . ')';
552         
553         return $query;
554     }
555     /**
556      * getIndexFieldDeclarationList
557      * Obtain DBMS specific SQL code portion needed to set an index
558      * declaration to be used in statements like CREATE TABLE.
559      *
560      * @return string
561      */
562     public function getIndexFieldDeclarationList(array $fields)
563     {
564         $declFields = array();
565
566         foreach ($fields as $fieldName => $field) {
567             $fieldString = $this->conn->quoteIdentifier($fieldName);
568
569             if (is_array($field)) {
570                 if (isset($field['length'])) {
571                     $fieldString .= '(' . $field['length'] . ')';
572                 }
573
574                 if (isset($field['sorting'])) {
575                     $sort = strtoupper($field['sorting']);
576                     switch ($sort) {
577                         case 'ASC':
578                         case 'DESC':
579                             $fieldString .= ' ' . $sort;
580                             break;
581                         default:
582                             throw new Doctrine_Export_Exception('Unknown index sorting option given.');
583                     }
584                 }
585             } else {
586                 $fieldString = $this->conn->quoteIdentifier($field);
587             }
588             $declFields[] = $fieldString;
589         }
590         return implode(', ', $declFields);
591     }
592     /**
593      * getAdvancedForeignKeyOptions
594      * Return the FOREIGN KEY query section dealing with non-standard options
595      * as MATCH, INITIALLY DEFERRED, ON UPDATE, ...
596      *
597      * @param array $definition
598      * @return string
599      */
600     public function getAdvancedForeignKeyOptions(array $definition)
601     {
602         $query = '';
603         if ( ! empty($definition['match'])) {
604             $query .= ' MATCH ' . $definition['match'];
605         }
606         if ( ! empty($definition['onUpdate'])) {
607             $query .= ' ON UPDATE ' . $this->getForeignKeyReferentialAction($definition['onUpdate']);
608         }
609         if ( ! empty($definition['onDelete'])) {
610             $query .= ' ON DELETE ' . $this->getForeignKeyReferentialAction($definition['onDelete']);
611         }
612         return $query;
613     }
614     /**
615      * drop existing index
616      *
617      * @param string    $table          name of table that should be used in method
618      * @param string    $name           name of the index to be dropped
619      * @return void
620      */
621     public function dropIndexSql($table, $name)
622     {
623         $table  = $this->conn->quoteIdentifier($table, true);
624         $name   = $this->conn->quoteIdentifier($this->conn->formatter->getIndexName($name), true);
625         return 'DROP INDEX ' . $name . ' ON ' . $table;
626     }
627     /**
628      * dropTable
629      *
630      * @param string    $table          name of table that should be dropped from the database
631      * @throws PDOException
632      * @return void
633      */
634     public function dropTableSql($table)
635     {
636         $table  = $this->conn->quoteIdentifier($table, true);
637         return 'DROP TABLE ' . $table;
638     }
639 }