Coverage for Doctrine_Export_Mysql

Back to coverage report

1 <?php
2 /*
3  *  $Id: Mysql.php 3967 2008-03-11 02:51:33Z jwage $
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: 3967 $
33  */
34 class Doctrine_Export_Mysql extends Doctrine_Export
35 {
36     /**
37      * createDatabaseSql
38      *
39      * @param string $name 
40      * @return void
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($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['charset'] .= ' 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->getAttribute(Doctrine::ATTR_DEFAULT_TABLE_TYPE);
406         }
407         if ($type) {
408             $optionsStrings[] = 'ENGINE = ' . $type;
409         }
410
411
412         try {
413             $query  = 'CREATE TABLE ' . $sequenceName
414                     . ' (' . $seqcolName . ' BIGINT NOT NULL AUTO_INCREMENT, PRIMARY KEY ('
415                     . $seqcolName . ')) ' . implode($optionsStrings, ' ');
416
417             $res    = $this->conn->exec($query);
418         } catch(Doctrine_Connection_Exception $e) {
419             throw new Doctrine_Export_Exception('could not create sequence table');
420         }
421
422         if ($start == 1 && $res == 1)
423             return true;
424
425         $query  = 'INSERT INTO ' . $sequenceName
426                 . ' (' . $seqcolName . ') VALUES (' . ($start - 1) . ')';
427
428         $res    = $this->conn->exec($query);
429
430         if ($res == 1)
431             return true;
432
433         // Handle error
434         try {
435             $result = $this->conn->exec('DROP TABLE ' . $sequenceName);
436         } catch(Doctrine_Connection_Exception $e) {
437             throw new Doctrine_Export_Exception('could not drop inconsistent sequence table');
438         }
439
440
441     }
442
443     /**
444      * Get the stucture of a field into an array
445      *
446      * @author Leoncx
447      * @param string    $table         name of the table on which the index is to be created
448      * @param string    $name          name of the index to be created
449      * @param array     $definition    associative array that defines properties of the index to be created.
450      *                                 Currently, only one property named FIELDS is supported. This property
451      *                                 is also an associative with the names of the index fields as array
452      *                                 indexes. Each entry of this array is set to another type of associative
453      *                                 array that specifies properties of the index that are specific to
454      *                                 each field.
455      *
456      *                                 Currently, only the sorting property is supported. It should be used
457      *                                 to define the sorting direction of the index. It may be set to either
458      *                                 ascending or descending.
459      *
460      *                                 Not all DBMS support index sorting direction configuration. The DBMS
461      *                                 drivers of those that do not support it ignore this property. Use the
462      *                                 function supports() to determine whether the DBMS driver can manage indexes.
463      *
464      *                                 Example
465      *                                    array(
466      *                                        'fields' => array(
467      *                                            'user_name' => array(
468      *                                                'sorting' => 'ASC'
469      *                                                'length' => 10
470      *                                            ),
471      *                                            'last_login' => array()
472      *                                        )
473      *                                    )
474      * @throws PDOException
475      * @return void
476      */
477     public function createIndexSql($table, $name, array $definition)
478     {
479         $table  = $table;
480         $name   = $this->conn->formatter->getIndexName($name);
481         $name   = $this->conn->quoteIdentifier($name);
482         $type   = '';
483         if (isset($definition['type'])) {
484             switch (strtolower($definition['type'])) {
485                 case 'fulltext':
486                 case 'unique':
487                     $type = strtoupper($definition['type']) . ' ';
488                 break;
489                 default:
490                     throw new Doctrine_Export_Exception('Unknown index type ' . $definition['type']);
491             }
492         }
493         $query  = 'CREATE ' . $type . 'INDEX ' . $name . ' ON ' . $table;
494         $query .= ' (' . $this->getIndexFieldDeclarationList($definition['fields']) . ')';
495
496         return $query;
497     }
498
499     /** 
500      * getDefaultDeclaration
501      * Obtain DBMS specific SQL code portion needed to set a default value
502      * declaration to be used in statements like CREATE TABLE.
503      *
504      * @param array $field      field definition array
505      * @return string           DBMS specific SQL code portion needed to set a default value
506      */
507     public function getDefaultFieldDeclaration($field)
508     {
509         $default = '';
510         if (isset($field['default']) && ( ! isset($field['length']) || $field['length'] <= 255)) {
511             if ($field['default'] === '') {
512                 $field['default'] = empty($field['notnull'])
513                     ? null : $this->valid_default_values[$field['type']];
514
515                 if ($field['default'] === ''
516                     && ($this->conn->getAttribute(Doctrine::ATTR_PORTABILITY) & Doctrine::PORTABILITY_EMPTY_TO_NULL)
517                 ) {
518                     $field['default'] = ' ';
519                 }
520             }
521     
522             // Proposed patch:
523             if ($field['type'] == 'enum' && $this->conn->getAttribute(Doctrine::ATTR_USE_NATIVE_ENUM)) {
524                 $fieldType = 'varchar';
525             } else {
526                 $fieldType = $field['type'];
527             }
528             
529             $default = ' DEFAULT ' . $this->conn->quote($field['default'], $fieldType);
530             //$default = ' DEFAULT ' . $this->conn->quote($field['default'], $field['type']);
531         }
532         return $default;
533     }
534
535     /**
536      * Obtain DBMS specific SQL code portion needed to set an index 
537      * declaration to be used in statements like CREATE TABLE.
538      *
539      * @param string $charset       name of the index
540      * @param array $definition     index definition
541      * @return string  DBMS specific SQL code portion needed to set an index
542      */
543     public function getIndexDeclaration($name, array $definition)
544     {
545         $name   = $this->conn->formatter->getIndexName($name);
546         $type   = '';
547         if (isset($definition['type'])) {
548             switch (strtolower($definition['type'])) {
549                 case 'fulltext':
550                 case 'unique':
551                     $type = strtoupper($definition['type']) . ' ';
552                 break;
553                 default:
554                     throw new Doctrine_Export_Exception('Unknown index type ' . $definition['type']);
555             }
556         }
557         
558         if ( ! isset($definition['fields'])) {
559             throw new Doctrine_Export_Exception('No index columns given.');
560         }
561         if ( ! is_array($definition['fields'])) {
562             $definition['fields'] = array($definition['fields']);
563         }
564
565         $query = $type . 'INDEX ' . $this->conn->quoteIdentifier($name);
566
567         $query .= ' (' . $this->getIndexFieldDeclarationList($definition['fields']) . ')';
568         
569         return $query;
570     }
571
572     /**
573      * getIndexFieldDeclarationList
574      * Obtain DBMS specific SQL code portion needed to set an index
575      * declaration to be used in statements like CREATE TABLE.
576      *
577      * @return string
578      */
579     public function getIndexFieldDeclarationList(array $fields)
580     {
581         $declFields = array();
582
583         foreach ($fields as $fieldName => $field) {
584             $fieldString = $this->conn->quoteIdentifier($fieldName);
585
586             if (is_array($field)) {
587                 if (isset($field['length'])) {
588                     $fieldString .= '(' . $field['length'] . ')';
589                 }
590
591                 if (isset($field['sorting'])) {
592                     $sort = strtoupper($field['sorting']);
593                     switch ($sort) {
594                         case 'ASC':
595                         case 'DESC':
596                             $fieldString .= ' ' . $sort;
597                             break;
598                         default:
599                             throw new Doctrine_Export_Exception('Unknown index sorting option given.');
600                     }
601                 }
602             } else {
603                 $fieldString = $this->conn->quoteIdentifier($field);
604             }
605             $declFields[] = $fieldString;
606         }
607         return implode(', ', $declFields);
608     }
609
610     /**
611      * getAdvancedForeignKeyOptions
612      * Return the FOREIGN KEY query section dealing with non-standard options
613      * as MATCH, INITIALLY DEFERRED, ON UPDATE, ...
614      *
615      * @param array $definition
616      * @return string
617      */
618     public function getAdvancedForeignKeyOptions(array $definition)
619     {
620         $query = '';
621         if ( ! empty($definition['match'])) {
622             $query .= ' MATCH ' . $definition['match'];
623         }
624         if ( ! empty($definition['onUpdate'])) {
625             $query .= ' ON UPDATE ' . $this->getForeignKeyReferentialAction($definition['onUpdate']);
626         }
627         if ( ! empty($definition['onDelete'])) {
628             $query .= ' ON DELETE ' . $this->getForeignKeyReferentialAction($definition['onDelete']);
629         }
630         return $query;
631     }
632
633     /**
634      * drop existing index
635      *
636      * @param string    $table          name of table that should be used in method
637      * @param string    $name           name of the index to be dropped
638      * @return void
639      */
640     public function dropIndexSql($table, $name)
641     {
642         $table  = $this->conn->quoteIdentifier($table, true);
643         $name   = $this->conn->quoteIdentifier($this->conn->formatter->getIndexName($name), true);
644         return 'DROP INDEX ' . $name . ' ON ' . $table;
645     }
646
647     /**
648      * dropTable
649      *
650      * @param string    $table          name of table that should be dropped from the database
651      * @throws PDOException
652      * @return void
653      */
654     public function dropTableSql($table)
655     {
656         $table  = $this->conn->quoteIdentifier($table, true);
657         return 'DROP TABLE ' . $table;
658     }
659     
660     public function dropForeignKey($table, $name)
661     {
662         $table = $this->conn->quoteIdentifier($table);
663         $name  = $this->conn->quoteIdentifier($name);
664         return $this->conn->exec('ALTER TABLE ' . $table . ' DROP FOREIGN KEY ' . $name);
665     }
666 }