Coverage for Doctrine_Export

Back to coverage report

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