Coverage for Doctrine_Export

Back to coverage report

1 <?php
2 /*
3  *  $Id: Export.php 3149 2007-11-13 22:28:37Z zYne $
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_Connection_Module');
22 /**
23  * Doctrine_Export
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: 3149 $
33  */
34 class Doctrine_Export extends Doctrine_Connection_Module
35 {
36     protected $valid_default_values = array(
37         'text'      => '',
38         'boolean'   => true,
39         'integer'   => 0,
40         'decimal'   => 0.0,
41         'float'     => 0.0,
42         'timestamp' => '1970-01-01 00:00:00',
43         'time'      => '00:00:00',
44         'date'      => '1970-01-01',
45         'clob'      => '',
46         'blob'      => '',
47         'string'    => ''
48     );
49
50     /**
51      * drop an existing database
52      * (this method is implemented by the drivers)
53      *
54      * @param string $name name of the database that should be dropped
55      * @return void
56      */
57     public function dropDatabase($database)
58     {
59         $this->conn->execute($this->dropDatabaseSql($database));
60     }
61
62     /**
63      * drop an existing database
64      * (this method is implemented by the drivers)
65      *
66      * @param string $name name of the database that should be dropped
67      * @return void
68      */
69     public function dropDatabaseSql($database)
70     {
71         throw new Doctrine_Export_Exception('Drop database not supported by this driver.');
72     }
73
74     /**
75      * dropTableSql
76      * drop an existing table
77      *
78      * @param string $table           name of table that should be dropped from the database
79      * @return string
80      */
81     public function dropTableSql($table)
82     {
83         return 'DROP TABLE ' . $this->conn->quoteIdentifier($table);
84     }
85
86     /**
87      * dropTable
88      * drop an existing table
89      *
90      * @param string $table           name of table that should be dropped from the database
91      * @return void
92      */
93     public function dropTable($table)
94     {
95         $this->conn->execute($this->dropTableSql($table));
96     }
97
98     /**
99      * drop existing index
100      *
101      * @param string    $table        name of table that should be used in method
102      * @param string    $name         name of the index to be dropped
103      * @return void
104      */
105     public function dropIndex($table, $name)
106     {
107         return $this->conn->exec($this->dropIndexSql($table, $name));
108     }
109
110     /**
111      * dropIndexSql
112      *
113      * @param string    $table        name of table that should be used in method
114      * @param string    $name         name of the index to be dropped
115      * @return string                 SQL that is used for dropping an index
116      */
117     public function dropIndexSql($table, $name)
118     {
119         $name = $this->conn->quoteIdentifier($this->conn->formatter->getIndexName($name));
120         
121         return 'DROP INDEX ' . $name;
122     }
123
124     /**
125      * drop existing constraint
126      *
127      * @param string    $table        name of table that should be used in method
128      * @param string    $name         name of the constraint to be dropped
129      * @param string    $primary      hint if the constraint is primary
130      * @return void
131      */
132     public function dropConstraint($table, $name, $primary = false)
133     {
134         $table = $this->conn->quoteIdentifier($table);
135         $name  = $this->conn->quoteIdentifier($name);
136         
137         return $this->conn->exec('ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $name);
138     }
139
140     /**
141      * drop existing foreign key
142      *
143      * @param string    $table        name of table that should be used in method
144      * @param string    $name         name of the foreign key to be dropped
145      * @return void
146      */
147     public function dropForeignKey($table, $name)
148     {
149         return $this->dropConstraint($table, $name);
150     }
151
152     /**
153      * dropSequenceSql
154      * drop existing sequence
155      * (this method is implemented by the drivers)
156      *
157      * @throws Doctrine_Connection_Exception     if something fails at database level
158      * @param string $sequenceName      name of the sequence to be dropped
159      * @return void
160      */
161     public function dropSequence($sequenceName)
162     {
163         $this->conn->exec($this->dropSequenceSql($sequenceName));
164     }
165
166     /**
167      * dropSequenceSql
168      * drop existing sequence
169      *
170      * @throws Doctrine_Connection_Exception     if something fails at database level
171      * @param string $sequenceName name of the sequence to be dropped
172      * @return void
173      */
174     public function dropSequenceSql($sequenceName)
175     {
176         throw new Doctrine_Export_Exception('Drop sequence not supported by this driver.');
177     }
178
179     /**
180      * create a new database
181      * (this method is implemented by the drivers)
182      *
183      * @param string $name name of the database that should be created
184      * @return void
185      */
186     public function createDatabase($database)
187     {
188         $this->conn->execute($this->createDatabaseSql($database));
189     }
190
191     /**
192      * create a new database
193      * (this method is implemented by the drivers)
194      *
195      * @param string $name name of the database that should be created
196      * @return string
197      */
198     public function createDatabaseSql($database)
199     {
200         throw new Doctrine_Export_Exception('Create database not supported by this driver.');
201     }
202
203     /**
204      * create a new table
205      *
206      * @param string $name   Name of the database that should be created
207      * @param array $fields  Associative array that contains the definition of each field of the new table
208      *                       The indexes of the array entries are the names of the fields of the table an
209      *                       the array entry values are associative arrays like those that are meant to be
210      *                       passed with the field definitions to get[Type]Declaration() functions.
211      *                          array(
212      *                              'id' => array(
213      *                                  'type' => 'integer',
214      *                                  'unsigned' => 1
215      *                                  'notnull' => 1
216      *                                  'default' => 0
217      *                              ),
218      *                              'name' => array(
219      *                                  'type' => 'text',
220      *                                  'length' => 12
221      *                              ),
222      *                              'password' => array(
223      *                                  'type' => 'text',
224      *                                  'length' => 12
225      *                              )
226      *                          );
227      * @param array $options  An associative array of table options:
228      *
229      * @return string
230      */
231     public function createTableSql($name, array $fields, array $options = array())
232     {
233         if ( ! $name) {
234             throw new Doctrine_Export_Exception('no valid table name specified');
235         }
236
237         if (empty($fields)) {
238             throw new Doctrine_Export_Exception('no fields specified for table ' . $name);
239         }
240
241         $queryFields = $this->getFieldDeclarationList($fields);
242
243
244         if (isset($options['primary']) && ! empty($options['primary'])) {
245             $queryFields .= ', PRIMARY KEY(' . implode(', ', array_values($options['primary'])) . ')';
246         }
247
248         if (isset($options['indexes']) && ! empty($options['indexes'])) {
249             foreach($options['indexes'] as $index => $definition) {
250                 $queryFields .= ', ' . $this->getIndexDeclaration($index, $definition);
251             }
252         }
253
254         $query = 'CREATE TABLE ' . $this->conn->quoteIdentifier($name, true) . ' (' . $queryFields;
255
256         $check = $this->getCheckDeclaration($fields);
257
258         if ( ! empty($check)) {
259             $query .= ', ' . $check;
260         }
261
262         $query .= ')';
263
264
265
266         $sql[] = $query;
267
268         if (isset($options['foreignKeys'])) {
269
270             foreach ((array) $options['foreignKeys'] as $k => $definition) {
271                 if (is_array($definition)) {
272                     $sql[] = $this->createForeignKeySql($name, $definition);
273                 }
274             }
275         }
276         return $sql;
277     }
278
279     /**
280      * create a new table
281      *
282      * @param string $name   Name of the database that should be created
283      * @param array $fields  Associative array that contains the definition of each field of the new table
284      * @param array $options  An associative array of table options:
285      * @see Doctrine_Export::createTableSql()
286      *
287      * @return void
288      */
289     public function createTable($name, array $fields, array $options = array())
290     {
291         $sql = (array) $this->createTableSql($name, $fields, $options);
292
293         foreach ($sql as $query) {
294             $this->conn->execute($query);
295         }
296     }
297
298     /**
299      * create sequence
300      *
301      * @throws Doctrine_Connection_Exception     if something fails at database level
302      * @param string    $seqName        name of the sequence to be created
303      * @param string    $start          start value of the sequence; default is 1
304      * @param array     $options  An associative array of table options:
305      *                          array(
306      *                              'comment' => 'Foo',
307      *                              'charset' => 'utf8',
308      *                              'collate' => 'utf8_unicode_ci',
309      *                          );
310      * @return void
311      */
312     public function createSequence($seqName, $start = 1, array $options = array())
313     {
314         return $this->conn->execute($this->createSequenceSql($seqName, $start = 1, $options));
315     }
316
317     /**
318      * return RDBMS specific create sequence statement
319      * (this method is implemented by the drivers)
320      *
321      * @throws Doctrine_Connection_Exception     if something fails at database level
322      * @param string    $seqName        name of the sequence to be created
323      * @param string    $start          start value of the sequence; default is 1
324      * @param array     $options  An associative array of table options:
325      *                          array(
326      *                              'comment' => 'Foo',
327      *                              'charset' => 'utf8',
328      *                              'collate' => 'utf8_unicode_ci',
329      *                          );
330      * @return string
331      */
332     public function createSequenceSql($seqName, $start = 1, array $options = array())
333     {
334         throw new Doctrine_Export_Exception('Create sequence not supported by this driver.');
335     }
336
337     /**
338      * create a constraint on a table
339      *
340      * @param string    $table         name of the table on which the constraint is to be created
341      * @param string    $name          name of the constraint to be created
342      * @param array     $definition    associative array that defines properties of the constraint to be created.
343      *                                 Currently, only one property named FIELDS is supported. This property
344      *                                 is also an associative with the names of the constraint fields as array
345      *                                 constraints. Each entry of this array is set to another type of associative
346      *                                 array that specifies properties of the constraint that are specific to
347      *                                 each field.
348      *
349      *                                 Example
350      *                                    array(
351      *                                        'fields' => array(
352      *                                            'user_name' => array(),
353      *                                            'last_login' => array()
354      *                                        )
355      *                                    )
356      * @return void
357      */
358     public function createConstraint($table, $name, $definition)
359     {
360         $sql = $this->createConstraintSql($table, $name, $definition);
361         
362         return $this->conn->exec($sql);
363     }
364
365     /**
366      * create a constraint on a table
367      *
368      * @param string    $table         name of the table on which the constraint is to be created
369      * @param string    $name          name of the constraint to be created
370      * @param array     $definition    associative array that defines properties of the constraint to be created.
371      *                                 Currently, only one property named FIELDS is supported. This property
372      *                                 is also an associative with the names of the constraint fields as array
373      *                                 constraints. Each entry of this array is set to another type of associative
374      *                                 array that specifies properties of the constraint that are specific to
375      *                                 each field.
376      *
377      *                                 Example
378      *                                    array(
379      *                                        'fields' => array(
380      *                                            'user_name' => array(),
381      *                                            'last_login' => array()
382      *                                        )
383      *                                    )
384      * @return void
385      */
386     public function createConstraintSql($table, $name, $definition)
387     {
388         $table = $this->conn->quoteIdentifier($table);
389         $name  = $this->conn->quoteIdentifier($this->conn->formatter->getIndexName($name));
390         $query = 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $name;
391
392         if (isset($definition['primary']) && $definition['primary']) {
393             $query .= ' PRIMARY KEY';
394         } elseif (isset($definition['unique']) && $definition['unique']) {
395             $query .= ' UNIQUE';
396         }
397
398         $fields = array();
399         foreach (array_keys($definition['fields']) as $field) {
400             $fields[] = $this->conn->quoteIdentifier($field, true);
401         }
402         $query .= ' ('. implode(', ', $fields) . ')';
403
404         return $query;
405     }
406
407     /**
408      * Get the stucture of a field into an array
409      *
410      * @param string    $table         name of the table on which the index is to be created
411      * @param string    $name          name of the index to be created
412      * @param array     $definition    associative array that defines properties of the index to be created.
413      *                                 Currently, only one property named FIELDS is supported. This property
414      *                                 is also an associative with the names of the index fields as array
415      *                                 indexes. Each entry of this array is set to another type of associative
416      *                                 array that specifies properties of the index that are specific to
417      *                                 each field.
418      *
419      *                                 Currently, only the sorting property is supported. It should be used
420      *                                 to define the sorting direction of the index. It may be set to either
421      *                                 ascending or descending.
422      *
423      *                                 Not all DBMS support index sorting direction configuration. The DBMS
424      *                                 drivers of those that do not support it ignore this property. Use the
425      *                                 function supports() to determine whether the DBMS driver can manage indexes.
426      *
427      *                                 Example
428      *                                    array(
429      *                                        'fields' => array(
430      *                                            'user_name' => array(
431      *                                                'sorting' => 'ascending'
432      *                                            ),
433      *                                            'last_login' => array()
434      *                                        )
435      *                                    )
436      * @return void
437      */
438     public function createIndex($table, $name, array $definition)
439     {
440         return $this->conn->execute($this->createIndexSql($table, $name, $definition));
441     }
442
443     /**
444      * Get the stucture of a field into an array
445      *
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      * @see Doctrine_Export::createIndex()
450      * @return string
451      */
452     public function createIndexSql($table, $name, array $definition)
453     {
454         $table  = $this->conn->quoteIdentifier($table);
455         $name   = $this->conn->quoteIdentifier($name);
456         $type   = '';
457
458         if (isset($definition['type'])) {
459             switch (strtolower($definition['type'])) {
460                 case 'unique':
461                     $type = strtoupper($definition['type']) . ' ';
462                 break;
463                 default:
464                     throw new Doctrine_Export_Exception('Unknown index type ' . $definition['type']);
465             }
466         }
467
468         $query = 'CREATE ' . $type . 'INDEX ' . $name . ' ON ' . $table;
469
470         $fields = array();
471         foreach ($definition['fields'] as $field) {
472             $fields[] = $this->conn->quoteIdentifier($field);
473         }
474         $query .= ' (' . implode(', ', $fields) . ')';
475
476         return $query;
477     }    
478     /**
479      * createForeignKeySql
480      *
481      * @param string    $table         name of the table on which the foreign key is to be created
482      * @param array     $definition    associative array that defines properties of the foreign key to be created.
483      * @return string
484      */
485     public function createForeignKeySql($table, array $definition)
486     {
487         $table = $this->conn->quoteIdentifier($table);
488
489         $query = 'ALTER TABLE ' . $table . ' ADD ' . $this->getForeignKeyDeclaration($definition);
490
491         return $query;
492     }
493
494     /**
495      * createForeignKey
496      *
497      * @param string    $table         name of the table on which the foreign key is to be created
498      * @param array     $definition    associative array that defines properties of the foreign key to be created.
499      * @return string
500      */
501     public function createForeignKey($table, array $definition)
502     {
503         $sql = $this->createForeignKeySql($table, $definition);
504         
505         return $this->conn->execute($sql);
506     }
507
508     /**
509      * alter an existing table
510      * (this method is implemented by the drivers)
511      *
512      * @param string $name         name of the table that is intended to be changed.
513      * @param array $changes     associative array that contains the details of each type
514      *                             of change that is intended to be performed. The types of
515      *                             changes that are currently supported are defined as follows:
516      *
517      *                             name
518      *
519      *                                New name for the table.
520      *
521      *                            add
522      *
523      *                                Associative array with the names of fields to be added as
524      *                                 indexes of the array. The value of each entry of the array
525      *                                 should be set to another associative array with the properties
526      *                                 of the fields to be added. The properties of the fields should
527      *                                 be the same as defined by the MDB2 parser.
528      *
529      *
530      *                            remove
531      *
532      *                                Associative array with the names of fields to be removed as indexes
533      *                                 of the array. Currently the values assigned to each entry are ignored.
534      *                                 An empty array should be used for future compatibility.
535      *
536      *                            rename
537      *
538      *                                Associative array with the names of fields to be renamed as indexes
539      *                                 of the array. The value of each entry of the array should be set to
540      *                                 another associative array with the entry named name with the new
541      *                                 field name and the entry named Declaration that is expected to contain
542      *                                 the portion of the field declaration already in DBMS specific SQL code
543      *                                 as it is used in the CREATE TABLE statement.
544      *
545      *                            change
546      *
547      *                                Associative array with the names of the fields to be changed as indexes
548      *                                 of the array. Keep in mind that if it is intended to change either the
549      *                                 name of a field and any other properties, the change array entries
550      *                                 should have the new names of the fields as array indexes.
551      *
552      *                                The value of each entry of the array should be set to another associative
553      *                                 array with the properties of the fields to that are meant to be changed as
554      *                                 array entries. These entries should be assigned to the new values of the
555      *                                 respective properties. The properties of the fields should be the same
556      *                                 as defined by the MDB2 parser.
557      *
558      *                            Example
559      *                                array(
560      *                                    'name' => 'userlist',
561      *                                    'add' => array(
562      *                                        'quota' => array(
563      *                                            'type' => 'integer',
564      *                                            'unsigned' => 1
565      *                                        )
566      *                                    ),
567      *                                    'remove' => array(
568      *                                        'file_limit' => array(),
569      *                                        'time_limit' => array()
570      *                                    ),
571      *                                    'change' => array(
572      *                                        'name' => array(
573      *                                            'length' => '20',
574      *                                            'definition' => array(
575      *                                                'type' => 'text',
576      *                                                'length' => 20,
577      *                                            ),
578      *                                        )
579      *                                    ),
580      *                                    'rename' => array(
581      *                                        'sex' => array(
582      *                                            'name' => 'gender',
583      *                                            'definition' => array(
584      *                                                'type' => 'text',
585      *                                                'length' => 1,
586      *                                                'default' => 'M',
587      *                                            ),
588      *                                        )
589      *                                    )
590      *                                )
591      *
592      * @param boolean $check     indicates whether the function should just check if the DBMS driver
593      *                             can perform the requested table alterations if the value is true or
594      *                             actually perform them otherwise.
595      * @return void
596      */
597     public function alterTable($name, array $changes, $check = false)
598     {
599         $sql = $this->alterTableSql($name, $changes, $check);
600         
601         if (is_string($sql) && $sql) {
602             $this->conn->execute($sql);
603         }
604     }
605
606     /**
607      * generates the sql for altering an existing table
608      * (this method is implemented by the drivers)
609      *
610      * @param string $name          name of the table that is intended to be changed.
611      * @param array $changes        associative array that contains the details of each type      *
612      * @param boolean $check        indicates whether the function should just check if the DBMS driver
613      *                              can perform the requested table alterations if the value is true or
614      *                              actually perform them otherwise.
615      * @see Doctrine_Export::alterTable()
616      * @return string
617      */
618     public function alterTableSql($name, array $changes, $check = false)
619     {
620         throw new Doctrine_Export_Exception('Alter table not supported by this driver.');
621     }
622
623     /**
624      * Get declaration of a number of field in bulk
625      *
626      * @param array $fields  a multidimensional associative array.
627      *      The first dimension determines the field name, while the second
628      *      dimension is keyed with the name of the properties
629      *      of the field being declared as array indexes. Currently, the types
630      *      of supported field properties are as follows:
631      *
632      *      length
633      *          Integer value that determines the maximum length of the text
634      *          field. If this argument is missing the field should be
635      *          declared to have the longest length allowed by the DBMS.
636      *
637      *      default
638      *          Text value to be used as default for this field.
639      *
640      *      notnull
641      *          Boolean flag that indicates whether this field is constrained
642      *          to not be set to null.
643      *      charset
644      *          Text value with the default CHARACTER SET for this field.
645      *      collation
646      *          Text value with the default COLLATION for this field.
647      *      unique
648      *          unique constraint
649      *
650      * @return string
651      */
652     public function getFieldDeclarationList(array $fields)
653     {
654         foreach ($fields as $fieldName => $field) {
655             $query = $this->getDeclaration($fieldName, $field);
656
657             $queryFields[] = $query;
658         }
659         return implode(', ', $queryFields);
660     }
661
662     /**
663      * Obtain DBMS specific SQL code portion needed to declare a generic type
664      * field to be used in statements like CREATE TABLE.
665      *
666      * @param string $name   name the field to be declared.
667      * @param array  $field  associative array with the name of the properties
668      *      of the field being declared as array indexes. Currently, the types
669      *      of supported field properties are as follows:
670      *
671      *      length
672      *          Integer value that determines the maximum length of the text
673      *          field. If this argument is missing the field should be
674      *          declared to have the longest length allowed by the DBMS.
675      *
676      *      default
677      *          Text value to be used as default for this field.
678      *
679      *      notnull
680      *          Boolean flag that indicates whether this field is constrained
681      *          to not be set to null.
682      *      charset
683      *          Text value with the default CHARACTER SET for this field.
684      *      collation
685      *          Text value with the default COLLATION for this field.
686      *      unique
687      *          unique constraint
688      *      check
689      *          column check constraint
690      *
691      * @return string  DBMS specific SQL code portion that should be used to
692      *      declare the specified field.
693      */
694     public function getDeclaration($name, array $field)
695     {
696
697         $default   = $this->getDefaultFieldDeclaration($field);
698
699         $charset   = (isset($field['charset']) && $field['charset']) ?
700                     ' ' . $this->getCharsetFieldDeclaration($field['charset']) : '';
701
702         $collation = (isset($field['collation']) && $field['collation']) ?
703                     ' ' . $this->getCollationFieldDeclaration($field['collation']) : '';
704
705         $notnull   = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : '';
706
707         $unique    = (isset($field['unique']) && $field['unique']) ?
708                     ' ' . $this->getUniqueFieldDeclaration() : '';
709
710         $check     = (isset($field['check']) && $field['check']) ?
711                     ' ' . $field['check'] : '';
712
713         $method = 'get' . $field['type'] . 'Declaration';
714
715         if (method_exists($this->conn->dataDict, $method)) {
716             return $this->conn->dataDict->$method($name, $field);
717         } else {
718             $dec = $this->conn->dataDict->getNativeDeclaration($field);
719         }
720         return $this->conn->quoteIdentifier($name, true) . ' ' . $dec . $charset . $default . $notnull . $unique . $check . $collation;
721     }
722
723     /**
724      * getDefaultDeclaration
725      * Obtain DBMS specific SQL code portion needed to set a default value
726      * declaration to be used in statements like CREATE TABLE.
727      *
728      * @param array $field      field definition array
729      * @return string           DBMS specific SQL code portion needed to set a default value
730      */
731     public function getDefaultFieldDeclaration($field)
732     {
733         $default = '';
734         if (isset($field['default'])) {
735             if ($field['default'] === '') {
736                 $field['default'] = empty($field['notnull'])
737                     ? null : $this->valid_default_values[$field['type']];
738
739                 if ($field['default'] === '' &&
740                    ($this->conn->getAttribute(Doctrine::ATTR_PORTABILITY) & Doctrine::PORTABILITY_EMPTY_TO_NULL)) {
741                     $field['default'] = null;
742                 }
743             }
744
745             if ($field['type'] === 'boolean') {
746                 $fields['default'] = $this->conn->convertBooleans($field['default']);
747             }
748             $default = ' DEFAULT ' . $this->conn->quote($field['default'], $field['type']);
749         }
750         return $default;
751     }
752
753     /**
754      * Obtain DBMS specific SQL code portion needed to set a CHECK constraint
755      * declaration to be used in statements like CREATE TABLE.
756      *
757      * @param array $definition     check definition
758      * @return string               DBMS specific SQL code portion needed to set a CHECK constraint
759      */
760     public function getCheckDeclaration(array $definition)
761     {
762         $constraints = array();
763         foreach ($definition as $field => $def) {
764             if (is_string($def)) {
765                 $constraints[] = 'CHECK (' . $def . ')';
766             } else {
767                 if (isset($def['min'])) {
768                     $constraints[] = 'CHECK (' . $field . ' >= ' . $def['min'] . ')';
769                 }
770
771                 if (isset($def['max'])) {
772                     $constraints[] = 'CHECK (' . $field . ' <= ' . $def['max'] . ')';
773                 }
774             }
775         }
776
777         return implode(', ', $constraints);
778     }
779
780     /**
781      * Obtain DBMS specific SQL code portion needed to set an index
782      * declaration to be used in statements like CREATE TABLE.
783      *
784      * @param string $name          name of the index
785      * @param array $definition     index definition
786      * @return string               DBMS specific SQL code portion needed to set an index
787      */
788     public function getIndexDeclaration($name, array $definition)
789     {
790         $name   = $this->conn->quoteIdentifier($name);
791         $type   = '';
792
793         if (isset($definition['type'])) {
794             if (strtolower($definition['type']) == 'unique') {
795                 $type = strtoupper($definition['type']) . ' ';
796             } else {
797                 throw new Doctrine_Export_Exception('Unknown index type ' . $definition['type']);
798             }
799         }
800
801         if ( ! isset($definition['fields']) || ! is_array($definition['fields'])) {
802             throw new Doctrine_Export_Exception('No index columns given.');
803         }
804
805         $query = $type . 'INDEX ' . $name;
806
807         $query .= ' (' . $this->getIndexFieldDeclarationList($definition['fields']) . ')';
808
809         return $query;
810     }
811
812     /**
813      * getIndexFieldDeclarationList
814      * Obtain DBMS specific SQL code portion needed to set an index
815      * declaration to be used in statements like CREATE TABLE.
816      *
817      * @return string
818      */
819     public function getIndexFieldDeclarationList(array $fields)
820     {
821         $ret = array();
822         foreach ($fields as $field => $definition) {
823             if (is_array($definition)) {
824                 $ret[] = $this->conn->quoteIdentifier($field);
825             } else {
826                 $ret[] = $this->conn->quoteIdentifier($definition);
827             }
828         }
829         return implode(', ', $ret);
830     }
831
832     /**
833      * A method to return the required SQL string that fits between CREATE ... TABLE
834      * to create the table as a temporary table.
835      *
836      * Should be overridden in driver classes to return the correct string for the
837      * specific database type.
838      *
839      * The default is to return the string "TEMPORARY" - this will result in a
840      * SQL error for any database that does not support temporary tables, or that
841      * requires a different SQL command from "CREATE TEMPORARY TABLE".
842      *
843      * @return string The string required to be placed between "CREATE" and "TABLE"
844      *                to generate a temporary table, if possible.
845      */
846     public function getTemporaryTableQuery()
847     {
848         return 'TEMPORARY';
849     }
850
851     /**
852      * getForeignKeyDeclaration
853      * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint
854      * of a field declaration to be used in statements like CREATE TABLE.
855      *
856      * @param array $definition         an associative array with the following structure:
857      *          name                    optional constraint name
858      *
859      *          local                   the local field(s)
860      *
861      *          foreign                 the foreign reference field(s)
862      *
863      *          foreignTable            the name of the foreign table
864      *
865      *          onDelete                referential delete action
866      *
867      *          onUpdate                referential update action
868      *
869      *          deferred                deferred constraint checking
870      *
871      * The onDelete and onUpdate keys accept the following values:
872      *
873      * CASCADE: Delete or update the row from the parent table and automatically delete or
874      *          update the matching rows in the child table. Both ON DELETE CASCADE and ON UPDATE CASCADE are supported.
875      *          Between two tables, you should not define several ON UPDATE CASCADE clauses that act on the same column
876      *          in the parent table or in the child table.
877      *
878      * SET NULL: Delete or update the row from the parent table and set the foreign key column or columns in the
879      *          child table to NULL. This is valid only if the foreign key columns do not have the NOT NULL qualifier
880      *          specified. Both ON DELETE SET NULL and ON UPDATE SET NULL clauses are supported.
881      *
882      * NO ACTION: In standard SQL, NO ACTION means no action in the sense that an attempt to delete or update a primary
883      *           key value is not allowed to proceed if there is a related foreign key value in the referenced table.
884      *
885      * RESTRICT: Rejects the delete or update operation for the parent table. NO ACTION and RESTRICT are the same as
886      *           omitting the ON DELETE or ON UPDATE clause.
887      *
888      * SET DEFAULT
889      *
890      * @return string  DBMS specific SQL code portion needed to set the FOREIGN KEY constraint
891      *                 of a field declaration.
892      */
893     public function getForeignKeyDeclaration(array $definition)
894     {
895         $sql  = $this->getForeignKeyBaseDeclaration($definition);
896         $sql .= $this->getAdvancedForeignKeyOptions($definition);
897
898         return $sql;
899     }
900
901     /**
902      * getAdvancedForeignKeyOptions
903      * Return the FOREIGN KEY query section dealing with non-standard options
904      * as MATCH, INITIALLY DEFERRED, ON UPDATE, ...
905      *
906      * @param array $definition     foreign key definition
907      * @return string
908      */
909     public function getAdvancedForeignKeyOptions(array $definition)
910     {
911         $query = '';
912         if ( ! empty($definition['onUpdate'])) {
913             $query .= ' ON UPDATE ' . $this->getForeignKeyRefentialAction($definition['onUpdate']);
914         }
915         if ( ! empty($definition['onDelete'])) {
916             $query .= ' ON DELETE ' . $this->getForeignKeyRefentialAction($definition['onDelete']);
917         }
918         return $query;
919     }
920
921     /**
922      * getForeignKeyReferentialAction
923      *
924      * returns given referential action in uppercase if valid, otherwise throws
925      * an exception
926      *
927      * @throws Doctrine_Exception_Exception     if unknown referential action given
928      * @param string $action    foreign key referential action
929      * @param string            foreign key referential action in uppercase
930      */
931     public function getForeignKeyReferentialAction($action)
932     {
933         $upper = strtoupper($action);
934         switch ($upper) {
935             case 'CASCADE':
936             case 'SET NULL':
937             case 'NO ACTION':
938             case 'RESTRICT':
939             case 'SET DEFAULT':
940                 return $upper;
941             break;
942             default:
943                 throw new Doctrine_Export_Exception('Unknown foreign key referential action \'' . $upper . '\' given.');
944         }
945     }
946
947     /**
948      * getForeignKeyBaseDeclaration
949      * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint
950      * of a field declaration to be used in statements like CREATE TABLE.
951      *
952      * @param array $definition
953      * @return string
954      */
955     public function getForeignKeyBaseDeclaration(array $definition)
956     {
957         $sql = '';
958         if (isset($definition['name'])) {
959             $sql .= ' CONSTRAINT ' . $this->conn->quoteIdentifier($definition['name']) . ' ';
960         }
961         $sql .= 'FOREIGN KEY (';
962
963         if ( ! isset($definition['local'])) {
964             throw new Doctrine_Export_Exception('Local reference field missing from definition.');
965         }
966         if ( ! isset($definition['foreign'])) {
967             throw new Doctrine_Export_Exception('Foreign reference field missing from definition.');
968         }
969         if ( ! isset($definition['foreignTable'])) {
970             throw new Doctrine_Export_Exception('Foreign reference table missing from definition.');
971         }
972
973         if ( ! is_array($definition['local'])) {
974             $definition['local'] = array($definition['local']);
975         }
976         if ( ! is_array($definition['foreign'])) {
977             $definition['foreign'] = array($definition['foreign']);
978         }
979
980         $sql .= implode(', ', array_map(array($this->conn, 'quoteIdentifier'), $definition['local']))
981               . ') REFERENCES '
982               . $this->conn->quoteIdentifier($definition['foreignTable']) . '('
983               . implode(', ', array_map(array($this->conn, 'quoteIdentifier'), $definition['foreign'])) . ')';
984
985         return $sql;
986     }
987
988     /**
989      * Obtain DBMS specific SQL code portion needed to set the UNIQUE constraint
990      * of a field declaration to be used in statements like CREATE TABLE.
991      *
992      * @return string  DBMS specific SQL code portion needed to set the UNIQUE constraint
993      *                 of a field declaration.
994      */
995     public function getUniqueFieldDeclaration()
996     {
997         return 'UNIQUE';
998     }
999
1000     /**
1001      * Obtain DBMS specific SQL code portion needed to set the CHARACTER SET
1002      * of a field declaration to be used in statements like CREATE TABLE.
1003      *
1004      * @param string $charset   name of the charset
1005      * @return string  DBMS specific SQL code portion needed to set the CHARACTER SET
1006      *                 of a field declaration.
1007      */
1008     public function getCharsetFieldDeclaration($charset)
1009     {
1010         return '';
1011     }
1012
1013     /**
1014      * Obtain DBMS specific SQL code portion needed to set the COLLATION
1015      * of a field declaration to be used in statements like CREATE TABLE.
1016      *
1017      * @param string $collation   name of the collation
1018      * @return string  DBMS specific SQL code portion needed to set the COLLATION
1019      *                 of a field declaration.
1020      */
1021     public function getCollationFieldDeclaration($collation)
1022     {
1023         return '';
1024     }
1025
1026     /**
1027      * exportSchema
1028      * method for exporting Doctrine_Record classes to a schema
1029      *
1030      * if the directory parameter is given this method first iterates
1031      * recursively trhough the given directory in order to find any model classes
1032      *
1033      * Then it iterates through all declared classes and creates tables for the ones
1034      * that extend Doctrine_Record and are not abstract classes
1035      *
1036      * @throws Doctrine_Connection_Exception    if some error other than Doctrine::ERR_ALREADY_EXISTS
1037      *                                          occurred during the create table operation
1038      * @param string $directory     optional directory parameter
1039      * @return void
1040      */
1041     public function exportSchema($directory = null)
1042     {
1043         if ($directory !== null) {
1044             $models = Doctrine::loadModels($directory);
1045         } else {
1046             $models = Doctrine::getLoadedModels();
1047         }
1048         
1049         $this->exportClasses($models);
1050     }
1051
1052     /**
1053      * exportClasses
1054      * method for exporting Doctrine_Record classes to a schema
1055      *
1056      * @throws Doctrine_Connection_Exception    if some error other than Doctrine::ERR_ALREADY_EXISTS
1057      *                                          occurred during the create table operation
1058      * @param array $classes
1059      * @return void
1060      */
1061     public function exportClasses(array $classes)
1062     {
1063         $connections = array();
1064         foreach ($classes as $class) {
1065             $record = new $class();
1066             $connection = $record->getTable()->getConnection();
1067             $connectionName = Doctrine_Manager::getInstance()->getConnectionName($connection);
1068             
1069             if ( ! isset($connections[$connectionName])) {
1070                 $connections[$connectionName] = array();
1071                 $connections[$connectionName]['creates'] = array();
1072                 $connections[$connectionName]['alters'] = array();
1073             }
1074             
1075             $sql = $this->exportClassesSql(array($class));
1076             // Build array of all the creates
1077             // We need these to happen first
1078             foreach ($sql as $key => $query) {
1079                 if (strstr($query, 'CREATE')) {
1080                     $connections[$connectionName]['creates'][] = $query;
1081                     // Unset the create from sql so we can have an array of everything else but creates
1082                     unset($sql[$key]);
1083                 }
1084             }
1085             
1086             $connections[$connectionName]['alters'] = array_merge($connections[$connectionName]['alters'], $sql);
1087         }
1088
1089         // Loop over all the sql again to merge the creates and alters in to the same array, but so that the alters are at the bottom
1090         $build = array();
1091         foreach ($connections as $connectionName => $sql) {
1092             $build[$connectionName] = array_merge($sql['creates'], $sql['alters']);
1093         }
1094
1095         foreach ($build as $connectionName => $sql) {
1096             $connection = Doctrine_Manager::getInstance()->getConnection($connectionName);
1097             
1098             $connection->beginTransaction();
1099             
1100             foreach ($sql as $query) {
1101                 try {
1102                     $connection->exec($query);
1103                 } catch (Doctrine_Connection_Exception $e) {
1104                     // we only want to silence table already exists errors
1105                     if ($e->getPortableCode() !== Doctrine::ERR_ALREADY_EXISTS) {
1106                         $connection->rollback();
1107                         throw new Doctrine_Export_Exception($e->getMessage() . '. Failing Query: ' . $query);
1108                     }
1109                 }
1110             }
1111             
1112             $connection->commit();
1113         }
1114     }
1115
1116     /**
1117      * exportClassesSql
1118      * method for exporting Doctrine_Record classes to a schema
1119      *
1120      * @throws Doctrine_Connection_Exception    if some error other than Doctrine::ERR_ALREADY_EXISTS
1121      *                                          occurred during the create table operation
1122      * @param array $classes
1123      * @return void
1124      */
1125     public function exportClassesSql(array $classes)
1126     {
1127         $models = Doctrine::getLoadedModels($classes);
1128         
1129         $sql = array();
1130         
1131         foreach ($models as $name) {
1132             $record = new $name();
1133             $table  = $record->getTable();
1134
1135             $parents = $table->getOption('joinedParents');
1136
1137             foreach ($parents as $parent) {
1138                 $data  = $table->getConnection()->getTable($parent)->getExportableFormat();
1139
1140                 $query = $this->conn->export->createTableSql($data['tableName'], $data['columns'], $data['options']);
1141                 
1142                 $sql = array_merge($sql, (array) $query);
1143             }
1144
1145             $data = $table->getExportableFormat();
1146
1147             $query = $this->conn->export->createTableSql($data['tableName'], $data['columns'], $data['options']);
1148
1149             if (is_array($query)) {
1150                 $sql = array_merge($sql, $query);
1151             } else {
1152                 $sql[] = $query;
1153             }
1154             
1155             if ($table->getAttribute(Doctrine::ATTR_EXPORT) & Doctrine::EXPORT_PLUGINS) {
1156                 $sql = array_merge($sql, $this->exportPluginsSql($table));
1157             }
1158         }
1159         
1160         $sql = array_unique($sql);
1161         
1162         rsort($sql);
1163
1164         return $sql;
1165     }
1166
1167     /**
1168      * fetches all plugins recursively for given table
1169      *
1170      * @param Doctrine_Table $table     table object to retrieve the plugins from
1171      * @return array                    an array of Doctrine_Plugin objects
1172      */
1173     public function getAllPlugins(Doctrine_Table $table)
1174     {
1175         $plugins = array();
1176
1177         foreach ($table->getTemplates() as $name => $template) {
1178             $plugin = $template->getPlugin();
1179
1180             if ($plugin === null) {
1181                 continue;                     
1182             }
1183
1184             $plugins[] = $plugin;
1185
1186             $pluginTable = $plugin->getOption('pluginTable');
1187             
1188             if ($pluginTable instanceof Doctrine_Table) {
1189                 $plugins = array_merge($plugins, $this->getAllPlugins($pluginTable));
1190             }
1191         }
1192
1193         return $plugins;
1194     }
1195
1196     /**
1197      * exportPluginsSql
1198      * exports plugin tables for given table
1199      *
1200      * @param Doctrine_Table $table     the table in which the plugins belong to
1201      * @return array    an array of sql strings
1202      */
1203     public function exportPluginsSql(Doctrine_Table $table)
1204     {
1205      $sql = array();
1206
1207         foreach ($this->getAllPlugins($table) as $name => $plugin) {
1208             $table = $plugin->getOption('pluginTable');
1209             
1210             // Make sure plugin has a valid table
1211             if ($table instanceof Doctrine_Table) {
1212                 $data = $table->getExportableFormat();
1213
1214                 $query = $this->conn->export->createTableSql($data['tableName'], $data['columns'], $data['options']);
1215
1216                 $sql = array_merge($sql, (array) $query);
1217             }
1218         }
1219
1220         return $sql;
1221     }
1222
1223     /**
1224      * exportSql
1225      * returns the sql for exporting Doctrine_Record classes to a schema
1226      *
1227      * if the directory parameter is given this method first iterates
1228      * recursively trhough the given directory in order to find any model classes
1229      *
1230      * Then it iterates through all declared classes and creates tables for the ones
1231      * that extend Doctrine_Record and are not abstract classes
1232      *
1233      * @throws Doctrine_Connection_Exception    if some error other than Doctrine::ERR_ALREADY_EXISTS
1234      *                                          occurred during the create table operation
1235      * @param string $directory     optional directory parameter
1236      * @return void
1237      */
1238     public function exportSql($directory = null)
1239     {
1240         if ($directory !== null) {
1241             $models = Doctrine::loadModels($directory);
1242         } else {
1243             $models = Doctrine::getLoadedModels();
1244         }
1245         
1246         return $this->exportClassesSql($models);
1247     }
1248
1249     /**
1250      * exportTable
1251      * exports given table into database based on column and option definitions
1252      *
1253      * @throws Doctrine_Connection_Exception    if some error other than Doctrine::ERR_ALREADY_EXISTS
1254      *                                          occurred during the create table operation
1255      * @return boolean                          whether or not the export operation was successful
1256      *                                          false if table already existed in the database
1257      */
1258     public function exportTable(Doctrine_Table $table)
1259     {
1260         /**
1261         TODO: maybe there should be portability option for the following check
1262         if ( ! Doctrine::isValidClassname($table->getOption('declaringClass')->getName())) {
1263             throw new Doctrine_Export_Exception('Class name not valid.');
1264         }
1265         */
1266
1267         try {
1268             $data = $table->getExportableFormat();
1269
1270             $this->conn->export->createTable($data['tableName'], $data['columns'], $data['options']);
1271         } catch(Doctrine_Connection_Exception $e) {
1272             // we only want to silence table already exists errors
1273             if ($e->getPortableCode() !== Doctrine::ERR_ALREADY_EXISTS) {
1274                 throw $e;
1275             }
1276         }
1277     }
1278 }