Coverage for Doctrine_Export_Mysql

Back to coverage report

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