1<?php /* vim: se et ts=4 sw=4 sts=4 fdm=marker tw=80: */
2/**
3 * Copyright (c) 1998-2010 Manuel Lemos, Tomas V.V.Cox,
4 * Stig. S. Bakken, Lukas Smith, Igor Feghali
5 * All rights reserved.
6 *
7 * MDB2_Schema enables users to maintain RDBMS independant schema files
8 * in XML that can be used to manipulate both data and database schemas
9 * This LICENSE is in the BSD license style.
10 *
11 * Redistribution and use in source and binary forms, with or without
12 * modification, are permitted provided that the following conditions
13 * are met:
14 *
15 * Redistributions of source code must retain the above copyright
16 * notice, this list of conditions and the following disclaimer.
17 *
18 * Redistributions in binary form must reproduce the above copyright
19 * notice, this list of conditions and the following disclaimer in the
20 * documentation and/or other materials provided with the distribution.
21 *
22 * Neither the name of Manuel Lemos, Tomas V.V.Cox, Stig. S. Bakken,
23 * Lukas Smith, Igor Feghali nor the names of his contributors may be
24 * used to endorse or promote products derived from this software
25 * without specific prior written permission.
26 *
27 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
30 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
31 * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
32 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
33 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
34 *  OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
35 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
36 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
37 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
38 * POSSIBILITY OF SUCH DAMAGE.
39 *
40 * PHP version 5
41 *
42 * @category Database
43 * @package  MDB2_Schema
44 * @author   Lukas Smith <smith@pooteeweet.org>
45 * @author   Igor Feghali <ifeghali@php.net>
46 * @license  BSD http://www.opensource.org/licenses/bsd-license.php
47 * @version  SVN: $Id$
48 * @link     http://pear.php.net/packages/MDB2_Schema
49 */
50
51require_once 'MDB2.php';
52
53define('MDB2_SCHEMA_DUMP_ALL',       0);
54define('MDB2_SCHEMA_DUMP_STRUCTURE', 1);
55define('MDB2_SCHEMA_DUMP_CONTENT',   2);
56
57/**
58 * If you add an error code here, make sure you also add a textual
59 * version of it in MDB2_Schema::errorMessage().
60 */
61
62define('MDB2_SCHEMA_ERROR',             -1);
63define('MDB2_SCHEMA_ERROR_PARSE',       -2);
64define('MDB2_SCHEMA_ERROR_VALIDATE',    -3);
65define('MDB2_SCHEMA_ERROR_UNSUPPORTED', -4);    // Driver does not support this function
66define('MDB2_SCHEMA_ERROR_INVALID',     -5);    // Invalid attribute value
67define('MDB2_SCHEMA_ERROR_WRITER',      -6);
68
69/**
70 * The database manager is a class that provides a set of database
71 * management services like installing, altering and dumping the data
72 * structures of databases.
73 *
74 * @category Database
75 * @package  MDB2_Schema
76 * @author   Lukas Smith <smith@pooteeweet.org>
77 * @license  BSD http://www.opensource.org/licenses/bsd-license.php
78 * @link     http://pear.php.net/packages/MDB2_Schema
79 */
80class MDB2_Schema extends PEAR
81{
82    // {{{ properties
83
84    var $db;
85
86    var $warnings = array();
87
88    var $options = array(
89        'fail_on_invalid_names' => true,
90        'dtd_file'              => false,
91        'valid_types'           => array(),
92        'force_defaults'        => true,
93        'parser'                => 'MDB2_Schema_Parser',
94        'writer'                => 'MDB2_Schema_Writer',
95        'validate'              => 'MDB2_Schema_Validate',
96        'drop_obsolete_objects' => false
97    );
98
99    // }}}
100    // {{{ apiVersion()
101
102    /**
103     * Return the MDB2 API version
104     *
105     * @return string  the MDB2 API version number
106     * @access public
107     */
108    function apiVersion()
109    {
110        return '0.4.3';
111    }
112
113    // }}}
114    // {{{ arrayMergeClobber()
115
116    /**
117     * Clobbers two arrays together
118     *
119     * @param array $a1 array that should be clobbered
120     * @param array $a2 array that should be clobbered
121     *
122     * @return array|false  array on success and false on error
123     *
124     * @access public
125     * @author kc@hireability.com
126     */
127    function arrayMergeClobber($a1, $a2)
128    {
129        if (!is_array($a1) || !is_array($a2)) {
130            return false;
131        }
132        foreach ($a2 as $key => $val) {
133            if (is_array($val) && array_key_exists($key, $a1) && is_array($a1[$key])) {
134                $a1[$key] = MDB2_Schema::arrayMergeClobber($a1[$key], $val);
135            } else {
136                $a1[$key] = $val;
137            }
138        }
139        return $a1;
140    }
141
142    // }}}
143    // {{{ resetWarnings()
144
145    /**
146     * reset the warning array
147     *
148     * @access public
149     * @return void
150     */
151    function resetWarnings()
152    {
153        $this->warnings = array();
154    }
155
156    // }}}
157    // {{{ getWarnings()
158
159    /**
160     * Get all warnings in reverse order
161     *
162     * This means that the last warning is the first element in the array
163     *
164     * @return array with warnings
165     * @access public
166     * @see resetWarnings()
167     */
168    function getWarnings()
169    {
170        return array_reverse($this->warnings);
171    }
172
173    // }}}
174    // {{{ setOption()
175
176    /**
177     * Sets the option for the db class
178     *
179     * @param string $option option name
180     * @param mixed  $value  value for the option
181     *
182     * @return bool|MDB2_Error MDB2_OK or error object
183     * @access public
184     */
185    function setOption($option, $value)
186    {
187        if (isset($this->options[$option])) {
188            if (is_null($value)) {
189                return $this->raiseError(MDB2_SCHEMA_ERROR, null, null,
190                    'may not set an option to value null');
191            }
192            $this->options[$option] = $value;
193            return MDB2_OK;
194        }
195        return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED, null, null,
196            "unknown option $option");
197    }
198
199    // }}}
200    // {{{ getOption()
201
202    /**
203     * returns the value of an option
204     *
205     * @param string $option option name
206     *
207     * @return mixed the option value or error object
208     * @access public
209     */
210    function getOption($option)
211    {
212        if (isset($this->options[$option])) {
213            return $this->options[$option];
214        }
215        return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED,
216            null, null, "unknown option $option");
217    }
218
219    // }}}
220    // {{{ factory()
221
222    /**
223     * Create a new MDB2 object for the specified database type
224     * type
225     *
226     * @param string|array|MDB2_Driver_Common &$db     'data source name', see the
227     *                                                 MDB2::parseDSN method for a description of the dsn format.
228     *                                                 Can also be specified as an array of the
229     *                                                 format returned by @see MDB2::parseDSN.
230     *                                                 Finally you can also pass an existing db object to be used.
231     * @param array                           $options An associative array of option names and their values.
232     *
233     * @return bool|MDB2_Error MDB2_OK or error object
234     * @access public
235     * @see     MDB2::parseDSN
236     */
237    public static function factory(&$db, $options = array())
238    {
239        $obj = new MDB2_Schema();
240
241        $result = $obj->connect($db, $options);
242        if (PEAR::isError($result)) {
243            return $result;
244        }
245        return $obj;
246    }
247
248    // }}}
249    // {{{ connect()
250
251    /**
252     * Create a new MDB2 connection object and connect to the specified
253     * database
254     *
255     * @param string|array|MDB2_Driver_Common &$db     'data source name', see the
256     *              MDB2::parseDSN method for a description of the dsn format.
257     *              Can also be specified as an array of the
258     *              format returned by MDB2::parseDSN.
259     *              Finally you can also pass an existing db object to be used.
260     * @param array                           $options An associative array of option names and their values.
261     *
262     * @return bool|MDB2_Error MDB2_OK or error object
263     * @access public
264     * @see    MDB2::parseDSN
265     */
266    function connect(&$db, $options = array())
267    {
268        $db_options = array();
269        if (is_array($options)) {
270            foreach ($options as $option => $value) {
271                if (array_key_exists($option, $this->options)) {
272                    $result = $this->setOption($option, $value);
273                    if (PEAR::isError($result)) {
274                        return $result;
275                    }
276                } else {
277                    $db_options[$option] = $value;
278                }
279            }
280        }
281
282        $this->disconnect();
283        if (!MDB2::isConnection($db)) {
284            $db = MDB2::factory($db, $db_options);
285        }
286
287        if (PEAR::isError($db)) {
288            return $db;
289        }
290
291        $this->db =& $db;
292        $this->db->loadModule('Datatype');
293        $this->db->loadModule('Manager');
294        $this->db->loadModule('Reverse');
295        $this->db->loadModule('Function');
296        if (empty($this->options['valid_types'])) {
297            $this->options['valid_types'] = $this->db->datatype->getValidTypes();
298        }
299
300        return MDB2_OK;
301    }
302
303    // }}}
304    // {{{ disconnect()
305
306    /**
307     * Log out and disconnect from the database.
308     *
309     * @access public
310     * @return void
311     */
312    function disconnect()
313    {
314        if (MDB2::isConnection($this->db)) {
315            $this->db->disconnect();
316            unset($this->db);
317        }
318    }
319
320    // }}}
321    // {{{ parseDatabaseDefinition()
322
323    /**
324     * Parse a database definition from a file or an array
325     *
326     * @param string|array $schema                the database schema array or file name
327     * @param bool         $skip_unreadable       if non readable files should be skipped
328     * @param array        $variables             associative array that the defines the text string values
329     *                                            that are meant to be used to replace the variables that are
330     *                                            used in the schema description.
331     * @param bool         $fail_on_invalid_names make function fail on invalid names
332     * @param array        $structure             database structure definition
333     *
334     * @access public
335     * @return array
336     */
337    function parseDatabaseDefinition($schema, $skip_unreadable = false, $variables = array(),
338        $fail_on_invalid_names = true, $structure = false)
339    {
340        $database_definition = false;
341        if (is_string($schema)) {
342            // if $schema is not readable then we just skip it
343            // and simply copy the $current_schema file to that file name
344            if (is_readable($schema)) {
345                $database_definition = $this->parseDatabaseDefinitionFile($schema, $variables, $fail_on_invalid_names, $structure);
346            }
347        } elseif (is_array($schema)) {
348            $database_definition = $schema;
349        }
350        if (!$database_definition && !$skip_unreadable) {
351            $database_definition = $this->raiseError(MDB2_SCHEMA_ERROR, null, null,
352                'invalid data type of schema or unreadable data source');
353        }
354        return $database_definition;
355    }
356
357    // }}}
358    // {{{ parseDatabaseDefinitionFile()
359
360    /**
361     * Parse a database definition file by creating a schema format
362     * parser object and passing the file contents as parser input data stream.
363     *
364     * @param string $input_file            the database schema file.
365     * @param array  $variables             associative array that the defines the text string values
366     *                                      that are meant to be used to replace the variables that are
367     *                                      used in the schema description.
368     * @param bool   $fail_on_invalid_names make function fail on invalid names
369     * @param array  $structure             database structure definition
370     *
371     * @access public
372     * @return array
373     */
374    function parseDatabaseDefinitionFile($input_file, $variables = array(),
375        $fail_on_invalid_names = true, $structure = false)
376    {
377        $dtd_file = $this->options['dtd_file'];
378        if ($dtd_file) {
379            include_once 'XML/DTD/XmlValidator.php';
380            $dtd = new XML_DTD_XmlValidator;
381            if (!$dtd->isValid($dtd_file, $input_file)) {
382                return $this->raiseError(MDB2_SCHEMA_ERROR_PARSE, null, null, $dtd->getMessage());
383            }
384        }
385
386        $class_name = $this->options['parser'];
387
388        $result = MDB2::loadClass($class_name, $this->db->getOption('debug'));
389        if (PEAR::isError($result)) {
390            return $result;
391        }
392
393        $max_identifiers_length = null;
394        if (isset($this->db->options['max_identifiers_length'])) {
395            $max_identifiers_length = $this->db->options['max_identifiers_length'];
396        }
397
398        $parser = new $class_name($variables, $fail_on_invalid_names, $structure,
399            $this->options['valid_types'], $this->options['force_defaults'],
400            $max_identifiers_length
401        );
402
403        $result = $parser->setInputFile($input_file);
404        if (PEAR::isError($result)) {
405            return $result;
406        }
407
408        $result = $parser->parse();
409        if (PEAR::isError($result)) {
410            return $result;
411        }
412        if (PEAR::isError($parser->error)) {
413            return $parser->error;
414        }
415
416        return $parser->database_definition;
417    }
418
419    // }}}
420    // {{{ getDefinitionFromDatabase()
421
422    /**
423     * Attempt to reverse engineer a schema structure from an existing MDB2
424     * This method can be used if no xml schema file exists yet.
425     * The resulting xml schema file may need some manual adjustments.
426     *
427     * @return array|MDB2_Error array with definition or error object
428     * @access public
429     */
430    function getDefinitionFromDatabase()
431    {
432        $database = $this->db->database_name;
433        if (empty($database)) {
434            return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null,
435                'it was not specified a valid database name');
436        }
437
438        $class_name = $this->options['validate'];
439
440        $result = MDB2::loadClass($class_name, $this->db->getOption('debug'));
441        if (PEAR::isError($result)) {
442            return $result;
443        }
444
445        $max_identifiers_length = null;
446        if (isset($this->db->options['max_identifiers_length'])) {
447            $max_identifiers_length = $this->db->options['max_identifiers_length'];
448        }
449
450        $val = new $class_name(
451            $this->options['fail_on_invalid_names'],
452            $this->options['valid_types'],
453            $this->options['force_defaults'],
454            $max_identifiers_length
455        );
456
457        $database_definition = array(
458            'name' => $database,
459            'create' => true,
460            'overwrite' => false,
461            'charset' => 'utf8',
462            'description' => '',
463            'comments' => '',
464            'tables' => array(),
465            'sequences' => array(),
466        );
467
468        $tables = $this->db->manager->listTables();
469        if (PEAR::isError($tables)) {
470            return $tables;
471        }
472
473        foreach ($tables as $table_name) {
474            $fields = $this->db->manager->listTableFields($table_name);
475            if (PEAR::isError($fields)) {
476                return $fields;
477            }
478
479            $database_definition['tables'][$table_name] = array(
480                'was' => '',
481                'description' => '',
482                'comments' => '',
483                'fields' => array(),
484                'indexes' => array(),
485                'constraints' => array(),
486                'initialization' => array()
487            );
488
489            $table_definition =& $database_definition['tables'][$table_name];
490            foreach ($fields as $field_name) {
491                $definition = $this->db->reverse->getTableFieldDefinition($table_name, $field_name);
492                if (PEAR::isError($definition)) {
493                    return $definition;
494                }
495
496                if (!empty($definition[0]['autoincrement'])) {
497                    $definition[0]['default'] = '0';
498                }
499
500                $table_definition['fields'][$field_name] = $definition[0];
501
502                $field_choices = count($definition);
503                if ($field_choices > 1) {
504                    $warning = "There are $field_choices type choices in the table $table_name field $field_name (#1 is the default): ";
505
506                    $field_choice_cnt = 1;
507
508                    $table_definition['fields'][$field_name]['choices'] = array();
509                    foreach ($definition as $field_choice) {
510                        $table_definition['fields'][$field_name]['choices'][] = $field_choice;
511
512                        $warning .= 'choice #'.($field_choice_cnt).': '.serialize($field_choice);
513                        $field_choice_cnt++;
514                    }
515                    $this->warnings[] = $warning;
516                }
517
518                /**
519                 * The first parameter is used to verify if there are duplicated
520                 * fields which we can guarantee that won't happen when reverse engineering
521                 */
522                $result = $val->validateField(array(), $table_definition['fields'][$field_name], $field_name);
523                if (PEAR::isError($result)) {
524                    return $result;
525                }
526            }
527
528            $keys = array();
529
530            $indexes = $this->db->manager->listTableIndexes($table_name);
531            if (PEAR::isError($indexes)) {
532                return $indexes;
533            }
534
535            if (is_array($indexes)) {
536                foreach ($indexes as $index_name) {
537                    $this->db->expectError(MDB2_ERROR_NOT_FOUND);
538                    $definition = $this->db->reverse->getTableIndexDefinition($table_name, $index_name);
539                    $this->db->popExpect();
540                    if (PEAR::isError($definition)) {
541                        if (PEAR::isError($definition, MDB2_ERROR_NOT_FOUND)) {
542                            continue;
543                        }
544                        return $definition;
545                    }
546
547                    $keys[$index_name] = $definition;
548                }
549            }
550
551            $constraints = $this->db->manager->listTableConstraints($table_name);
552            if (PEAR::isError($constraints)) {
553                return $constraints;
554            }
555
556            if (is_array($constraints)) {
557                foreach ($constraints as $constraint_name) {
558                    $this->db->expectError(MDB2_ERROR_NOT_FOUND);
559                    $definition = $this->db->reverse->getTableConstraintDefinition($table_name, $constraint_name);
560                    $this->db->popExpect();
561                    if (PEAR::isError($definition)) {
562                        if (PEAR::isError($definition, MDB2_ERROR_NOT_FOUND)) {
563                            continue;
564                        }
565                        return $definition;
566                    }
567
568                    $keys[$constraint_name] = $definition;
569                }
570            }
571
572            foreach ($keys as $key_name => $definition) {
573                if (array_key_exists('foreign', $definition)
574                    && $definition['foreign']
575                ) {
576                    /**
577                     * The first parameter is used to verify if there are duplicated
578                     * foreign keys which we can guarantee that won't happen when reverse engineering
579                     */
580                    $result = $val->validateConstraint(array(), $definition, $key_name);
581                    if (PEAR::isError($result)) {
582                        return $result;
583                    }
584
585                    foreach ($definition['fields'] as $field_name => $field) {
586                        /**
587                         * The first parameter is used to verify if there are duplicated
588                         * referencing fields which we can guarantee that won't happen when reverse engineering
589                         */
590                        $result = $val->validateConstraintField(array(), $field_name);
591                        if (PEAR::isError($result)) {
592                            return $result;
593                        }
594
595                        $definition['fields'][$field_name] = '';
596                    }
597
598                    foreach ($definition['references']['fields'] as $field_name => $field) {
599                        /**
600                         * The first parameter is used to verify if there are duplicated
601                         * referenced fields which we can guarantee that won't happen when reverse engineering
602                         */
603                        $result = $val->validateConstraintReferencedField(array(), $field_name);
604                        if (PEAR::isError($result)) {
605                            return $result;
606                        }
607
608                        $definition['references']['fields'][$field_name] = '';
609                    }
610
611                    $table_definition['constraints'][$key_name] = $definition;
612                } else {
613                    /**
614                     * The first parameter is used to verify if there are duplicated
615                     * indices which we can guarantee that won't happen when reverse engineering
616                     */
617                    $result = $val->validateIndex(array(), $definition, $key_name);
618                    if (PEAR::isError($result)) {
619                        return $result;
620                    }
621
622                    foreach ($definition['fields'] as $field_name => $field) {
623                        /**
624                         * The first parameter is used to verify if there are duplicated
625                         * index fields which we can guarantee that won't happen when reverse engineering
626                         */
627                        $result = $val->validateIndexField(array(), $field, $field_name);
628                        if (PEAR::isError($result)) {
629                            return $result;
630                        }
631
632                        $definition['fields'][$field_name] = $field;
633                    }
634
635                    $table_definition['indexes'][$key_name] = $definition;
636                }
637            }
638
639            /**
640             * The first parameter is used to verify if there are duplicated
641             * tables which we can guarantee that won't happen when reverse engineering
642             */
643            $result = $val->validateTable(array(), $table_definition, $table_name);
644            if (PEAR::isError($result)) {
645                return $result;
646            }
647
648        }
649
650        $sequences = $this->db->manager->listSequences();
651        if (PEAR::isError($sequences)) {
652            return $sequences;
653        }
654
655        if (is_array($sequences)) {
656            foreach ($sequences as $sequence_name) {
657                $definition = $this->db->reverse->getSequenceDefinition($sequence_name);
658                if (PEAR::isError($definition)) {
659                    return $definition;
660                }
661                if (isset($database_definition['tables'][$sequence_name])
662                    && isset($database_definition['tables'][$sequence_name]['indexes'])
663                ) {
664                    foreach ($database_definition['tables'][$sequence_name]['indexes'] as $index) {
665                        if (isset($index['primary']) && $index['primary']
666                            && count($index['fields'] == 1)
667                        ) {
668                            $definition['on'] = array(
669                                'table' => $sequence_name,
670                                'field' => key($index['fields']),
671                            );
672                            break;
673                        }
674                    }
675                }
676
677                /**
678                 * The first parameter is used to verify if there are duplicated
679                 * sequences which we can guarantee that won't happen when reverse engineering
680                 */
681                $result = $val->validateSequence(array(), $definition, $sequence_name);
682                if (PEAR::isError($result)) {
683                    return $result;
684                }
685
686                $database_definition['sequences'][$sequence_name] = $definition;
687            }
688        }
689
690        $result = $val->validateDatabase($database_definition);
691        if (PEAR::isError($result)) {
692            return $result;
693        }
694
695        return $database_definition;
696    }
697
698    // }}}
699    // {{{ createTableIndexes()
700
701    /**
702     * A method to create indexes for an existing table
703     *
704     * @param string  $table_name Name of the table
705     * @param array   $indexes    An array of indexes to be created
706     * @param boolean $overwrite  If the table/index should be overwritten if it already exists
707     *
708     * @return mixed  MDB2_Error if there is an error creating an index, MDB2_OK otherwise
709     * @access public
710     */
711    function createTableIndexes($table_name, $indexes, $overwrite = false)
712    {
713        if (!$this->db->supports('indexes')) {
714            $this->db->debug('Indexes are not supported', __FUNCTION__);
715            return MDB2_OK;
716        }
717
718        $errorcodes = array(MDB2_ERROR_UNSUPPORTED, MDB2_ERROR_NOT_CAPABLE);
719        foreach ($indexes as $index_name => $index) {
720
721            // Does the index already exist, and if so, should it be overwritten?
722            $create_index = true;
723            $this->db->expectError($errorcodes);
724            if (!empty($index['primary']) || !empty($index['unique'])) {
725                $current_indexes = $this->db->manager->listTableConstraints($table_name);
726            } else {
727                $current_indexes = $this->db->manager->listTableIndexes($table_name);
728            }
729
730            $this->db->popExpect();
731            if (PEAR::isError($current_indexes)) {
732                if (!MDB2::isError($current_indexes, $errorcodes)) {
733                    return $current_indexes;
734                }
735            } elseif (is_array($current_indexes) && in_array($index_name, $current_indexes)) {
736                if (!$overwrite) {
737                    $this->db->debug('Index already exists: '.$index_name, __FUNCTION__);
738                    $create_index = false;
739                } else {
740                    $this->db->debug('Preparing to overwrite index: '.$index_name, __FUNCTION__);
741
742                    $this->db->expectError(MDB2_ERROR_NOT_FOUND);
743                    if (!empty($index['primary']) || !empty($index['unique'])) {
744                        $result = $this->db->manager->dropConstraint($table_name, $index_name);
745                    } else {
746                        $result = $this->db->manager->dropIndex($table_name, $index_name);
747                    }
748                    $this->db->popExpect();
749                    if (PEAR::isError($result) && !MDB2::isError($result, MDB2_ERROR_NOT_FOUND)) {
750                        return $result;
751                    }
752                }
753            }
754
755            // Check if primary is being used and if it's supported
756            if (!empty($index['primary']) && !$this->db->supports('primary_key')) {
757
758                // Primary not supported so we fallback to UNIQUE and making the field NOT NULL
759                $index['unique'] = true;
760
761                $changes = array();
762
763                foreach ($index['fields'] as $field => $empty) {
764                    $field_info = $this->db->reverse->getTableFieldDefinition($table_name, $field);
765                    if (PEAR::isError($field_info)) {
766                        return $field_info;
767                    }
768                    if (!$field_info[0]['notnull']) {
769                        $changes['change'][$field] = $field_info[0];
770
771                        $changes['change'][$field]['notnull'] = true;
772                    }
773                }
774                if (!empty($changes)) {
775                    $this->db->manager->alterTable($table_name, $changes, false);
776                }
777            }
778
779            // Should the index be created?
780            if ($create_index) {
781                if (!empty($index['primary']) || !empty($index['unique'])) {
782                    $result = $this->db->manager->createConstraint($table_name, $index_name, $index);
783                } else {
784                    $result = $this->db->manager->createIndex($table_name, $index_name, $index);
785                }
786                if (PEAR::isError($result)) {
787                    return $result;
788                }
789            }
790        }
791        return MDB2_OK;
792    }
793
794    // }}}
795    // {{{ createTableConstraints()
796
797    /**
798     * A method to create foreign keys for an existing table
799     *
800     * @param string  $table_name  Name of the table
801     * @param array   $constraints An array of foreign keys to be created
802     * @param boolean $overwrite   If the foreign key should be overwritten if it already exists
803     *
804     * @return mixed  MDB2_Error if there is an error creating a foreign key, MDB2_OK otherwise
805     * @access public
806     */
807    function createTableConstraints($table_name, $constraints, $overwrite = false)
808    {
809        if (!$this->db->supports('indexes')) {
810            $this->db->debug('Indexes are not supported', __FUNCTION__);
811            return MDB2_OK;
812        }
813
814        $errorcodes = array(MDB2_ERROR_UNSUPPORTED, MDB2_ERROR_NOT_CAPABLE);
815        foreach ($constraints as $constraint_name => $constraint) {
816
817            // Does the foreign key already exist, and if so, should it be overwritten?
818            $create_constraint = true;
819            $this->db->expectError($errorcodes);
820            $current_constraints = $this->db->manager->listTableConstraints($table_name);
821            $this->db->popExpect();
822            if (PEAR::isError($current_constraints)) {
823                if (!MDB2::isError($current_constraints, $errorcodes)) {
824                    return $current_constraints;
825                }
826            } elseif (is_array($current_constraints) && in_array($constraint_name, $current_constraints)) {
827                if (!$overwrite) {
828                    $this->db->debug('Foreign key already exists: '.$constraint_name, __FUNCTION__);
829                    $create_constraint = false;
830                } else {
831                    $this->db->debug('Preparing to overwrite foreign key: '.$constraint_name, __FUNCTION__);
832                    $result = $this->db->manager->dropConstraint($table_name, $constraint_name);
833                    if (PEAR::isError($result)) {
834                        return $result;
835                    }
836                }
837            }
838
839            // Should the foreign key be created?
840            if ($create_constraint) {
841                $result = $this->db->manager->createConstraint($table_name, $constraint_name, $constraint);
842                if (PEAR::isError($result)) {
843                    return $result;
844                }
845            }
846        }
847        return MDB2_OK;
848    }
849
850    // }}}
851    // {{{ createTable()
852
853    /**
854     * Create a table and inititialize the table if data is available
855     *
856     * @param string $table_name name of the table to be created
857     * @param array  $table      multi dimensional array that contains the
858     *                           structure and optional data of the table
859     * @param bool   $overwrite  if the table/index should be overwritten if it already exists
860     * @param array  $options    an array of options to be passed to the database specific driver
861     *                           version of MDB2_Driver_Manager_Common::createTable().
862     *
863     * @return bool|MDB2_Error MDB2_OK or error object
864     * @access public
865     */
866    function createTable($table_name, $table, $overwrite = false, $options = array())
867    {
868        $create = true;
869
870        $errorcodes = array(MDB2_ERROR_UNSUPPORTED, MDB2_ERROR_NOT_CAPABLE);
871
872        $this->db->expectError($errorcodes);
873
874        $tables = $this->db->manager->listTables();
875
876        $this->db->popExpect();
877        if (PEAR::isError($tables)) {
878            if (!MDB2::isError($tables, $errorcodes)) {
879                return $tables;
880            }
881        } elseif (is_array($tables) && in_array($table_name, $tables)) {
882            if (!$overwrite) {
883                $create = false;
884                $this->db->debug('Table already exists: '.$table_name, __FUNCTION__);
885            } else {
886                $result = $this->db->manager->dropTable($table_name);
887                if (PEAR::isError($result)) {
888                    return $result;
889                }
890                $this->db->debug('Overwritting table: '.$table_name, __FUNCTION__);
891            }
892        }
893
894        if ($create) {
895            $result = $this->db->manager->createTable($table_name, $table['fields'], $options);
896            if (PEAR::isError($result)) {
897                return $result;
898            }
899        }
900
901        if (!empty($table['initialization']) && is_array($table['initialization'])) {
902            $result = $this->initializeTable($table_name, $table);
903            if (PEAR::isError($result)) {
904                return $result;
905            }
906        }
907
908        if (!empty($table['indexes']) && is_array($table['indexes'])) {
909            $result = $this->createTableIndexes($table_name, $table['indexes'], $overwrite);
910            if (PEAR::isError($result)) {
911                return $result;
912            }
913        }
914
915        if (!empty($table['constraints']) && is_array($table['constraints'])) {
916            $result = $this->createTableConstraints($table_name, $table['constraints'], $overwrite);
917            if (PEAR::isError($result)) {
918                return $result;
919            }
920        }
921
922        return MDB2_OK;
923    }
924
925    // }}}
926    // {{{ initializeTable()
927
928    /**
929     * Inititialize the table with data
930     *
931     * @param string $table_name name of the table
932     * @param array  $table      multi dimensional array that contains the
933     *                           structure and optional data of the table
934     *
935     * @return bool|MDB2_Error MDB2_OK or error object
936     * @access public
937     */
938    function initializeTable($table_name, $table)
939    {
940        $query_insertselect = 'INSERT INTO %s (%s) (SELECT %s FROM %s %s)';
941
942        $query_insert = 'INSERT INTO %s (%s) VALUES (%s)';
943        $query_update = 'UPDATE %s SET %s %s';
944        $query_delete = 'DELETE FROM %s %s';
945
946        $table_name = $this->db->quoteIdentifier($table_name, true);
947
948        $result = MDB2_OK;
949
950        $support_transactions = $this->db->supports('transactions');
951
952        foreach ($table['initialization'] as $instruction) {
953            $query = '';
954            switch ($instruction['type']) {
955            case 'insert':
956                if (!isset($instruction['data']['select'])) {
957                    $data = $this->getInstructionFields($instruction['data'], $table['fields']);
958                    if (!empty($data)) {
959                        $fields = implode(', ', array_keys($data));
960                        $values = implode(', ', array_values($data));
961
962                        $query = sprintf($query_insert, $table_name, $fields, $values);
963                    }
964                } else {
965                    $data  = $this->getInstructionFields($instruction['data']['select'], $table['fields']);
966                    $where = $this->getInstructionWhere($instruction['data']['select'], $table['fields']);
967
968                    $select_table_name = $this->db->quoteIdentifier($instruction['data']['select']['table'], true);
969                    if (!empty($data)) {
970                        $fields = implode(', ', array_keys($data));
971                        $values = implode(', ', array_values($data));
972
973                        $query = sprintf($query_insertselect, $table_name, $fields, $values, $select_table_name, $where);
974                    }
975                }
976                break;
977            case 'update':
978                $data  = $this->getInstructionFields($instruction['data'], $table['fields']);
979                $where = $this->getInstructionWhere($instruction['data'], $table['fields']);
980                if (!empty($data)) {
981                    array_walk($data, array($this, 'buildFieldValue'));
982                    $fields_values = implode(', ', $data);
983
984                    $query = sprintf($query_update, $table_name, $fields_values, $where);
985                }
986                break;
987            case 'delete':
988                $where = $this->getInstructionWhere($instruction['data'], $table['fields']);
989                $query = sprintf($query_delete, $table_name, $where);
990                break;
991            }
992            if ($query) {
993                if ($support_transactions && PEAR::isError($res = $this->db->beginNestedTransaction())) {
994                    return $res;
995                }
996
997                $result = $this->db->exec($query);
998                if (PEAR::isError($result)) {
999                    return $result;
1000                }
1001
1002                if ($support_transactions && PEAR::isError($res = $this->db->completeNestedTransaction())) {
1003                    return $res;
1004                }
1005            }
1006        }
1007        return $result;
1008    }
1009
1010    // }}}
1011    // {{{ buildFieldValue()
1012
1013    /**
1014     * Appends the contents of second argument + '=' to the beginning of first
1015     * argument.
1016     *
1017     * Used with array_walk() in initializeTable() for UPDATEs.
1018     *
1019     * @param string &$element value of array's element
1020     * @param string $key      key of array's element
1021     *
1022     * @return void
1023     *
1024     * @access public
1025     * @see MDB2_Schema::initializeTable()
1026     */
1027    function buildFieldValue(&$element, $key)
1028    {
1029        $element = $key."=$element";
1030    }
1031
1032    // }}}
1033    // {{{ getExpression()
1034
1035    /**
1036     * Generates a string that represents a value that would be associated
1037     * with a column in a DML instruction.
1038     *
1039     * @param array  $element           multi dimensional array that contains the
1040     *                                  structure of the current DML instruction.
1041     * @param array  $fields_definition multi dimensional array that contains the
1042     *                                  definition for current table's fields
1043     * @param string $type              type of given field
1044     *
1045     * @return string
1046     *
1047     * @access public
1048     * @see MDB2_Schema::getInstructionFields(), MDB2_Schema::getInstructionWhere()
1049     */
1050    function getExpression($element, $fields_definition = array(), $type = null)
1051    {
1052        $str = '';
1053        switch ($element['type']) {
1054        case 'null':
1055            $str .= 'NULL';
1056            break;
1057        case 'value':
1058            $str .= $this->db->quote($element['data'], $type);
1059            break;
1060        case 'column':
1061            $str .= $this->db->quoteIdentifier($element['data'], true);
1062            break;
1063        case 'function':
1064            $arguments = array();
1065            if (!empty($element['data']['arguments'])
1066                && is_array($element['data']['arguments'])
1067            ) {
1068                foreach ($element['data']['arguments'] as $v) {
1069                    $arguments[] = $this->getExpression($v, $fields_definition);
1070                }
1071            }
1072            if (method_exists($this->db->function, $element['data']['name'])) {
1073                $user_func = array(&$this->db->function, $element['data']['name']);
1074
1075                $str .= call_user_func_array($user_func, $arguments);
1076            } else {
1077                $str .= $element['data']['name'].'(';
1078                $str .= implode(', ', $arguments);
1079                $str .= ')';
1080            }
1081            break;
1082        case 'expression':
1083            $type0 = $type1 = null;
1084            if ($element['data']['operants'][0]['type'] == 'column'
1085                && array_key_exists($element['data']['operants'][0]['data'], $fields_definition)
1086            ) {
1087                $type0 = $fields_definition[$element['data']['operants'][0]['data']]['type'];
1088            }
1089
1090            if ($element['data']['operants'][1]['type'] == 'column'
1091                && array_key_exists($element['data']['operants'][1]['data'], $fields_definition)
1092            ) {
1093                $type1 = $fields_definition[$element['data']['operants'][1]['data']]['type'];
1094            }
1095
1096            $str .= '(';
1097            $str .= $this->getExpression($element['data']['operants'][0], $fields_definition, $type1);
1098            $str .= $this->getOperator($element['data']['operator']);
1099            $str .= $this->getExpression($element['data']['operants'][1], $fields_definition, $type0);
1100            $str .= ')';
1101            break;
1102        }
1103
1104        return $str;
1105    }
1106
1107    // }}}
1108    // {{{ getOperator()
1109
1110    /**
1111     * Returns the matching SQL operator
1112     *
1113     * @param string $op parsed descriptive operator
1114     *
1115     * @return string matching SQL operator
1116     *
1117     * @access public
1118     * @static
1119     * @see MDB2_Schema::getExpression()
1120     */
1121    function getOperator($op)
1122    {
1123        switch ($op) {
1124        case 'PLUS':
1125            return ' + ';
1126        case 'MINUS':
1127            return ' - ';
1128        case 'TIMES':
1129            return ' * ';
1130        case 'DIVIDED':
1131            return ' / ';
1132        case 'EQUAL':
1133            return ' = ';
1134        case 'NOT EQUAL':
1135            return ' != ';
1136        case 'LESS THAN':
1137            return ' < ';
1138        case 'GREATER THAN':
1139            return ' > ';
1140        case 'LESS THAN OR EQUAL':
1141            return ' <= ';
1142        case 'GREATER THAN OR EQUAL':
1143            return ' >= ';
1144        default:
1145            return ' '.$op.' ';
1146        }
1147    }
1148
1149    // }}}
1150    // {{{ getInstructionFields()
1151
1152    /**
1153     * Walks the parsed DML instruction array, field by field,
1154     * storing them and their processed values inside a new array.
1155     *
1156     * @param array $instruction       multi dimensional array that contains the
1157     *                                 structure of the current DML instruction.
1158     * @param array $fields_definition multi dimensional array that contains the
1159     *                                 definition for current table's fields
1160     *
1161     * @return array  array of strings in the form 'field_name' => 'value'
1162     *
1163     * @access public
1164     * @static
1165     * @see MDB2_Schema::initializeTable()
1166     */
1167    function getInstructionFields($instruction, $fields_definition = array())
1168    {
1169        $fields = array();
1170        if (!empty($instruction['field']) && is_array($instruction['field'])) {
1171            foreach ($instruction['field'] as $field) {
1172                $field_name = $this->db->quoteIdentifier($field['name'], true);
1173
1174                $fields[$field_name] = $this->getExpression($field['group'], $fields_definition);
1175            }
1176        }
1177        return $fields;
1178    }
1179
1180    // }}}
1181    // {{{ getInstructionWhere()
1182
1183    /**
1184     * Translates the parsed WHERE expression of a DML instruction
1185     * (array structure) to a SQL WHERE clause (string).
1186     *
1187     * @param array $instruction       multi dimensional array that contains the
1188     *                                 structure of the current DML instruction.
1189     * @param array $fields_definition multi dimensional array that contains the
1190     *                                 definition for current table's fields.
1191     *
1192     * @return string SQL WHERE clause
1193     *
1194     * @access public
1195     * @static
1196     * @see MDB2_Schema::initializeTable()
1197     */
1198    function getInstructionWhere($instruction, $fields_definition = array())
1199    {
1200        $where = '';
1201        if (!empty($instruction['where'])) {
1202            $where = 'WHERE '.$this->getExpression($instruction['where'], $fields_definition);
1203        }
1204        return $where;
1205    }
1206
1207    // }}}
1208    // {{{ createSequence()
1209
1210    /**
1211     * Create a sequence
1212     *
1213     * @param string $sequence_name name of the sequence to be created
1214     * @param array  $sequence      multi dimensional array that contains the
1215     *                              structure and optional data of the table
1216     * @param bool   $overwrite     if the sequence should be overwritten if it already exists
1217     *
1218     * @return bool|MDB2_Error MDB2_OK or error object
1219     * @access public
1220     */
1221    function createSequence($sequence_name, $sequence, $overwrite = false)
1222    {
1223        if (!$this->db->supports('sequences')) {
1224            $this->db->debug('Sequences are not supported', __FUNCTION__);
1225            return MDB2_OK;
1226        }
1227
1228        $errorcodes = array(MDB2_ERROR_UNSUPPORTED, MDB2_ERROR_NOT_CAPABLE);
1229        $this->db->expectError($errorcodes);
1230        $sequences = $this->db->manager->listSequences();
1231        $this->db->popExpect();
1232        if (PEAR::isError($sequences)) {
1233            if (!MDB2::isError($sequences, $errorcodes)) {
1234                return $sequences;
1235            }
1236        } elseif (is_array($sequence) && in_array($sequence_name, $sequences)) {
1237            if (!$overwrite) {
1238                $this->db->debug('Sequence already exists: '.$sequence_name, __FUNCTION__);
1239                return MDB2_OK;
1240            }
1241
1242            $result = $this->db->manager->dropSequence($sequence_name);
1243            if (PEAR::isError($result)) {
1244                return $result;
1245            }
1246            $this->db->debug('Overwritting sequence: '.$sequence_name, __FUNCTION__);
1247        }
1248
1249        $start = 1;
1250        $field = '';
1251        if (!empty($sequence['on'])) {
1252            $table = $sequence['on']['table'];
1253            $field = $sequence['on']['field'];
1254
1255            $errorcodes = array(MDB2_ERROR_UNSUPPORTED, MDB2_ERROR_NOT_CAPABLE);
1256            $this->db->expectError($errorcodes);
1257            $tables = $this->db->manager->listTables();
1258            $this->db->popExpect();
1259            if (PEAR::isError($tables) && !MDB2::isError($tables, $errorcodes)) {
1260                 return $tables;
1261            }
1262
1263            if (!PEAR::isError($tables) && is_array($tables) && in_array($table, $tables)) {
1264                if ($this->db->supports('summary_functions')) {
1265                    $query = "SELECT MAX($field) FROM ".$this->db->quoteIdentifier($table, true);
1266                } else {
1267                    $query = "SELECT $field FROM ".$this->db->quoteIdentifier($table, true)." ORDER BY $field DESC";
1268                }
1269                $start = $this->db->queryOne($query, 'integer');
1270                if (PEAR::isError($start)) {
1271                    return $start;
1272                }
1273                ++$start;
1274            } else {
1275                $this->warnings[] = 'Could not sync sequence: '.$sequence_name;
1276            }
1277        } elseif (!empty($sequence['start']) && is_numeric($sequence['start'])) {
1278            $start = $sequence['start'];
1279            $table = '';
1280        }
1281
1282        $result = $this->db->manager->createSequence($sequence_name, $start);
1283        if (PEAR::isError($result)) {
1284            return $result;
1285        }
1286
1287        return MDB2_OK;
1288    }
1289
1290    // }}}
1291    // {{{ createDatabase()
1292
1293    /**
1294     * Create a database space within which may be created database objects
1295     * like tables, indexes and sequences. The implementation of this function
1296     * is highly DBMS specific and may require special permissions to run
1297     * successfully. Consult the documentation or the DBMS drivers that you
1298     * use to be aware of eventual configuration requirements.
1299     *
1300     * @param array $database_definition multi dimensional array that contains the current definition
1301     * @param array $options             an array of options to be passed to the
1302     *                                   database specific driver version of
1303     *                                   MDB2_Driver_Manager_Common::createTable().
1304     *
1305     * @return bool|MDB2_Error MDB2_OK or error object
1306     * @access public
1307     */
1308    function createDatabase($database_definition, $options = array())
1309    {
1310        if (!isset($database_definition['name']) || !$database_definition['name']) {
1311            return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null,
1312                'no valid database name specified');
1313        }
1314
1315        $create    = (isset($database_definition['create']) && $database_definition['create']);
1316        $overwrite = (isset($database_definition['overwrite']) && $database_definition['overwrite']);
1317
1318        /**
1319         *
1320         * We need to clean up database name before any query to prevent
1321         * database driver from using a inexistent database
1322         *
1323         */
1324        $previous_database_name = $this->db->setDatabase('');
1325
1326        // Lower / Upper case the db name if the portability deems so.
1327        if ($this->db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) {
1328            $func = $this->db->options['field_case'] == CASE_LOWER ? 'strtolower' : 'strtoupper';
1329
1330            $db_name = $func($database_definition['name']);
1331        } else {
1332            $db_name = $database_definition['name'];
1333        }
1334
1335        if ($create) {
1336
1337            $dbExists = $this->db->databaseExists($db_name);
1338            if (PEAR::isError($dbExists)) {
1339                return $dbExists;
1340            }
1341
1342            if ($dbExists && $overwrite) {
1343                $this->db->expectError(MDB2_ERROR_CANNOT_DROP);
1344                $result = $this->db->manager->dropDatabase($db_name);
1345                $this->db->popExpect();
1346                if (PEAR::isError($result) && !MDB2::isError($result, MDB2_ERROR_CANNOT_DROP)) {
1347                    return $result;
1348                }
1349                $dbExists = false;
1350                $this->db->debug('Overwritting database: ' . $db_name, __FUNCTION__);
1351            }
1352
1353            $dbOptions = array();
1354            if (array_key_exists('charset', $database_definition)
1355                && !empty($database_definition['charset'])) {
1356                $dbOptions['charset'] = $database_definition['charset'];
1357            }
1358
1359            if ($dbExists) {
1360                $this->db->debug('Database already exists: ' . $db_name, __FUNCTION__);
1361                if (!empty($dbOptions)) {
1362                    $errorcodes = array(MDB2_ERROR_UNSUPPORTED, MDB2_ERROR_NO_PERMISSION);
1363                    $this->db->expectError($errorcodes);
1364                    $result = $this->db->manager->alterDatabase($db_name, $dbOptions);
1365                    $this->db->popExpect();
1366                    if (PEAR::isError($result) && !MDB2::isError($result, $errorcodes)) {
1367                        return $result;
1368                    }
1369                }
1370                $create = false;
1371            } else {
1372                $this->db->expectError(MDB2_ERROR_UNSUPPORTED);
1373                $result = $this->db->manager->createDatabase($db_name, $dbOptions);
1374                $this->db->popExpect();
1375                if (PEAR::isError($result) && !MDB2::isError($result, MDB2_ERROR_UNSUPPORTED)) {
1376                    return $result;
1377                }
1378                $this->db->debug('Creating database: ' . $db_name, __FUNCTION__);
1379            }
1380        }
1381
1382        $this->db->setDatabase($db_name);
1383        if (($support_transactions = $this->db->supports('transactions'))
1384            && PEAR::isError($result = $this->db->beginNestedTransaction())
1385        ) {
1386            return $result;
1387        }
1388
1389        $created_objects = 0;
1390        if (isset($database_definition['tables'])
1391            && is_array($database_definition['tables'])
1392        ) {
1393            foreach ($database_definition['tables'] as $table_name => $table) {
1394                $result = $this->createTable($table_name, $table, $overwrite, $options);
1395                if (PEAR::isError($result)) {
1396                    break;
1397                }
1398                $created_objects++;
1399            }
1400        }
1401        if (!PEAR::isError($result)
1402            && isset($database_definition['sequences'])
1403            && is_array($database_definition['sequences'])
1404        ) {
1405            foreach ($database_definition['sequences'] as $sequence_name => $sequence) {
1406                $result = $this->createSequence($sequence_name, $sequence, false, $overwrite);
1407
1408                if (PEAR::isError($result)) {
1409                    break;
1410                }
1411                $created_objects++;
1412            }
1413        }
1414
1415        if ($support_transactions) {
1416            $res = $this->db->completeNestedTransaction();
1417            if (PEAR::isError($res)) {
1418                $result = $this->raiseError(MDB2_SCHEMA_ERROR, null, null,
1419                    'Could not end transaction ('.
1420                    $res->getMessage().' ('.$res->getUserinfo().'))');
1421            }
1422        } elseif (PEAR::isError($result) && $created_objects) {
1423            $result = $this->raiseError(MDB2_SCHEMA_ERROR, null, null,
1424                'the database was only partially created ('.
1425                $result->getMessage().' ('.$result->getUserinfo().'))');
1426        }
1427
1428        $this->db->setDatabase($previous_database_name);
1429
1430        if (PEAR::isError($result) && $create
1431            && PEAR::isError($result2 = $this->db->manager->dropDatabase($db_name))
1432        ) {
1433            if (!MDB2::isError($result2, MDB2_ERROR_UNSUPPORTED)) {
1434                return $this->raiseError(MDB2_SCHEMA_ERROR, null, null,
1435                       'Could not drop the created database after unsuccessful creation attempt ('.
1436                       $result2->getMessage().' ('.$result2->getUserinfo().'))');
1437            }
1438        }
1439
1440        return $result;
1441    }
1442
1443    // }}}
1444    // {{{ compareDefinitions()
1445
1446    /**
1447     * Compare a previous definition with the currently parsed definition
1448     *
1449     * @param array $current_definition  multi dimensional array that contains the current definition
1450     * @param array $previous_definition multi dimensional array that contains the previous definition
1451     *
1452     * @return array|MDB2_Error array of changes on success, or a error object
1453     * @access public
1454     */
1455    function compareDefinitions($current_definition, $previous_definition)
1456    {
1457        $changes = array();
1458
1459        if (!empty($current_definition['tables']) && is_array($current_definition['tables'])) {
1460            $changes['tables'] = $defined_tables = array();
1461            foreach ($current_definition['tables'] as $table_name => $table) {
1462                $previous_tables = array();
1463                if (!empty($previous_definition) && is_array($previous_definition)) {
1464                    $previous_tables = $previous_definition['tables'];
1465                }
1466                $change = $this->compareTableDefinitions($table_name, $table, $previous_tables, $defined_tables);
1467                if (PEAR::isError($change)) {
1468                    return $change;
1469                }
1470                if (!empty($change)) {
1471                    $changes['tables'] = MDB2_Schema::arrayMergeClobber($changes['tables'], $change);
1472                }
1473            }
1474        }
1475        if (!empty($previous_definition['tables'])
1476            && is_array($previous_definition['tables'])
1477        ) {
1478            foreach ($previous_definition['tables'] as $table_name => $table) {
1479                if (empty($defined_tables[$table_name])) {
1480                    $changes['tables']['remove'][$table_name] = true;
1481                }
1482            }
1483        }
1484
1485        if (!empty($current_definition['sequences']) && is_array($current_definition['sequences'])) {
1486            $changes['sequences'] = $defined_sequences = array();
1487            foreach ($current_definition['sequences'] as $sequence_name => $sequence) {
1488                $previous_sequences = array();
1489                if (!empty($previous_definition) && is_array($previous_definition)) {
1490                    $previous_sequences = $previous_definition['sequences'];
1491                }
1492
1493                $change = $this->compareSequenceDefinitions($sequence_name,
1494                                                            $sequence,
1495                                                            $previous_sequences,
1496                                                            $defined_sequences);
1497                if (PEAR::isError($change)) {
1498                    return $change;
1499                }
1500                if (!empty($change)) {
1501                    $changes['sequences'] = MDB2_Schema::arrayMergeClobber($changes['sequences'], $change);
1502                }
1503            }
1504        }
1505        if (!empty($previous_definition['sequences'])
1506            && is_array($previous_definition['sequences'])
1507        ) {
1508            foreach ($previous_definition['sequences'] as $sequence_name => $sequence) {
1509                if (empty($defined_sequences[$sequence_name])) {
1510                    $changes['sequences']['remove'][$sequence_name] = true;
1511                }
1512            }
1513        }
1514
1515        return $changes;
1516    }
1517
1518    // }}}
1519    // {{{ compareTableFieldsDefinitions()
1520
1521    /**
1522     * Compare a previous definition with the currently parsed definition
1523     *
1524     * @param string $table_name          name of the table
1525     * @param array  $current_definition  multi dimensional array that contains the current definition
1526     * @param array  $previous_definition multi dimensional array that contains the previous definition
1527     *
1528     * @return array|MDB2_Error array of changes on success, or a error object
1529     * @access public
1530     */
1531    function compareTableFieldsDefinitions($table_name, $current_definition,
1532        $previous_definition)
1533    {
1534        $changes = $defined_fields = array();
1535
1536        if (is_array($current_definition)) {
1537            foreach ($current_definition as $field_name => $field) {
1538                $was_field_name = $field['was'];
1539                if (!empty($previous_definition[$field_name])
1540                    && (
1541                        (isset($previous_definition[$field_name]['was'])
1542                         && $previous_definition[$field_name]['was'] == $was_field_name)
1543                        || !isset($previous_definition[$was_field_name])
1544                       )) {
1545                    $was_field_name = $field_name;
1546                }
1547
1548                if (!empty($previous_definition[$was_field_name])) {
1549                    if ($was_field_name != $field_name) {
1550                        $changes['rename'][$was_field_name] = array('name' => $field_name, 'definition' => $field);
1551                    }
1552
1553                    if (!empty($defined_fields[$was_field_name])) {
1554                        return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null,
1555                            'the field "'.$was_field_name.
1556                            '" was specified for more than one field of table');
1557                    }
1558
1559                    $defined_fields[$was_field_name] = true;
1560
1561                    $change = $this->db->compareDefinition($field, $previous_definition[$was_field_name]);
1562                    if (PEAR::isError($change)) {
1563                        return $change;
1564                    }
1565
1566                    if (!empty($change)) {
1567                        if (array_key_exists('default', $change)
1568                            && $change['default']
1569                            && !array_key_exists('default', $field)) {
1570                                $field['default'] = null;
1571                        }
1572
1573                        $change['definition'] = $field;
1574
1575                        $changes['change'][$field_name] = $change;
1576                    }
1577                } else {
1578                    if ($field_name != $was_field_name) {
1579                        return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null,
1580                            'it was specified a previous field name ("'.
1581                            $was_field_name.'") for field "'.$field_name.'" of table "'.
1582                            $table_name.'" that does not exist');
1583                    }
1584
1585                    $changes['add'][$field_name] = $field;
1586                }
1587            }
1588        }
1589
1590        if (isset($previous_definition) && is_array($previous_definition)) {
1591            foreach ($previous_definition as $field_previous_name => $field_previous) {
1592                if (empty($defined_fields[$field_previous_name])) {
1593                    $changes['remove'][$field_previous_name] = true;
1594                }
1595            }
1596        }
1597
1598        return $changes;
1599    }
1600
1601    // }}}
1602    // {{{ compareTableIndexesDefinitions()
1603
1604    /**
1605     * Compare a previous definition with the currently parsed definition
1606     *
1607     * @param string $table_name          name of the table
1608     * @param array  $current_definition  multi dimensional array that contains the current definition
1609     * @param array  $previous_definition multi dimensional array that contains the previous definition
1610     *
1611     * @return array|MDB2_Error array of changes on success, or a error object
1612     * @access public
1613     */
1614    function compareTableIndexesDefinitions($table_name, $current_definition,
1615        $previous_definition)
1616    {
1617        $changes = $defined_indexes = array();
1618
1619        if (is_array($current_definition)) {
1620            foreach ($current_definition as $index_name => $index) {
1621                $was_index_name = $index['was'];
1622                if (!empty($previous_definition[$index_name])
1623                    && isset($previous_definition[$index_name]['was'])
1624                    && $previous_definition[$index_name]['was'] == $was_index_name
1625                ) {
1626                    $was_index_name = $index_name;
1627                }
1628                if (!empty($previous_definition[$was_index_name])) {
1629                    $change = array();
1630                    if ($was_index_name != $index_name) {
1631                        $change['name'] = $was_index_name;
1632                    }
1633
1634                    if (!empty($defined_indexes[$was_index_name])) {
1635                        return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null,
1636                            'the index "'.$was_index_name.'" was specified for'.
1637                            ' more than one index of table "'.$table_name.'"');
1638                    }
1639                    $defined_indexes[$was_index_name] = true;
1640
1641                    $previous_unique = array_key_exists('unique', $previous_definition[$was_index_name])
1642                        ? $previous_definition[$was_index_name]['unique'] : false;
1643
1644                    $unique = array_key_exists('unique', $index) ? $index['unique'] : false;
1645                    if ($previous_unique != $unique) {
1646                        $change['unique'] = $unique;
1647                    }
1648
1649                    $previous_primary = array_key_exists('primary', $previous_definition[$was_index_name])
1650                        ? $previous_definition[$was_index_name]['primary'] : false;
1651
1652                    $primary = array_key_exists('primary', $index) ? $index['primary'] : false;
1653                    if ($previous_primary != $primary) {
1654                        $change['primary'] = $primary;
1655                    }
1656
1657                    $defined_fields  = array();
1658                    $previous_fields = $previous_definition[$was_index_name]['fields'];
1659                    if (!empty($index['fields']) && is_array($index['fields'])) {
1660                        foreach ($index['fields'] as $field_name => $field) {
1661                            if (!empty($previous_fields[$field_name])) {
1662                                $defined_fields[$field_name] = true;
1663
1664                                $previous_sorting = array_key_exists('sorting', $previous_fields[$field_name])
1665                                    ? $previous_fields[$field_name]['sorting'] : '';
1666
1667                                $sorting = array_key_exists('sorting', $field) ? $field['sorting'] : '';
1668                                if ($previous_sorting != $sorting) {
1669                                    $change['change'] = true;
1670                                }
1671                            } else {
1672                                $change['change'] = true;
1673                            }
1674                        }
1675                    }
1676                    if (isset($previous_fields) && is_array($previous_fields)) {
1677                        foreach ($previous_fields as $field_name => $field) {
1678                            if (empty($defined_fields[$field_name])) {
1679                                $change['change'] = true;
1680                            }
1681                        }
1682                    }
1683                    if (!empty($change)) {
1684                        $changes['change'][$index_name] = $current_definition[$index_name];
1685                    }
1686                } else {
1687                    if ($index_name != $was_index_name) {
1688                        return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null,
1689                            'it was specified a previous index name ("'.$was_index_name.
1690                            ') for index "'.$index_name.'" of table "'.$table_name.'" that does not exist');
1691                    }
1692                    $changes['add'][$index_name] = $current_definition[$index_name];
1693                }
1694            }
1695        }
1696        foreach ($previous_definition as $index_previous_name => $index_previous) {
1697            if (empty($defined_indexes[$index_previous_name])) {
1698                $changes['remove'][$index_previous_name] = $index_previous;
1699            }
1700        }
1701        return $changes;
1702    }
1703
1704    // }}}
1705    // {{{ compareTableDefinitions()
1706
1707    /**
1708     * Compare a previous definition with the currently parsed definition
1709     *
1710     * @param string $table_name          name of the table
1711     * @param array  $current_definition  multi dimensional array that contains the current definition
1712     * @param array  $previous_definition multi dimensional array that contains the previous definition
1713     * @param array  &$defined_tables     table names in the schema
1714     *
1715     * @return array|MDB2_Error array of changes on success, or a error object
1716     * @access public
1717     */
1718    function compareTableDefinitions($table_name, $current_definition,
1719        $previous_definition, &$defined_tables)
1720    {
1721        $changes = array();
1722
1723        if (is_array($current_definition)) {
1724            $was_table_name = $table_name;
1725            if (!empty($current_definition['was'])) {
1726                $was_table_name = $current_definition['was'];
1727            }
1728            if (!empty($previous_definition[$was_table_name])) {
1729                $changes['change'][$was_table_name] = array();
1730                if ($was_table_name != $table_name) {
1731                    $changes['change'][$was_table_name] = array('name' => $table_name);
1732                }
1733                if (!empty($defined_tables[$was_table_name])) {
1734                    return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null,
1735                        'the table "'.$was_table_name.
1736                        '" was specified for more than one table of the database');
1737                }
1738                $defined_tables[$was_table_name] = true;
1739                if (!empty($current_definition['fields']) && is_array($current_definition['fields'])) {
1740                    $previous_fields = array();
1741                    if (isset($previous_definition[$was_table_name]['fields'])
1742                        && is_array($previous_definition[$was_table_name]['fields'])) {
1743                        $previous_fields = $previous_definition[$was_table_name]['fields'];
1744                    }
1745
1746                    $change = $this->compareTableFieldsDefinitions($table_name,
1747                                                                   $current_definition['fields'],
1748                                                                   $previous_fields);
1749
1750                    if (PEAR::isError($change)) {
1751                        return $change;
1752                    }
1753                    if (!empty($change)) {
1754                        $changes['change'][$was_table_name] =
1755                            MDB2_Schema::arrayMergeClobber($changes['change'][$was_table_name], $change);
1756                    }
1757                }
1758                if (!empty($current_definition['indexes']) && is_array($current_definition['indexes'])) {
1759                    $previous_indexes = array();
1760                    if (isset($previous_definition[$was_table_name]['indexes'])
1761                        && is_array($previous_definition[$was_table_name]['indexes'])) {
1762                        $previous_indexes = $previous_definition[$was_table_name]['indexes'];
1763                    }
1764                    $change = $this->compareTableIndexesDefinitions($table_name,
1765                                                                    $current_definition['indexes'],
1766                                                                    $previous_indexes);
1767
1768                    if (PEAR::isError($change)) {
1769                        return $change;
1770                    }
1771                    if (!empty($change)) {
1772                        $changes['change'][$was_table_name]['indexes'] = $change;
1773                    }
1774                }
1775                if (empty($changes['change'][$was_table_name])) {
1776                    unset($changes['change'][$was_table_name]);
1777                }
1778                if (empty($changes['change'])) {
1779                    unset($changes['change']);
1780                }
1781            } else {
1782                if ($table_name != $was_table_name) {
1783                    return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null,
1784                        'it was specified a previous table name ("'.$was_table_name.
1785                        '") for table "'.$table_name.'" that does not exist');
1786                }
1787                $changes['add'][$table_name] = true;
1788            }
1789        }
1790
1791        return $changes;
1792    }
1793
1794    // }}}
1795    // {{{ compareSequenceDefinitions()
1796
1797    /**
1798     * Compare a previous definition with the currently parsed definition
1799     *
1800     * @param string $sequence_name       name of the sequence
1801     * @param array  $current_definition  multi dimensional array that contains the current definition
1802     * @param array  $previous_definition multi dimensional array that contains the previous definition
1803     * @param array  &$defined_sequences  names in the schema
1804     *
1805     * @return array|MDB2_Error array of changes on success, or a error object
1806     * @access public
1807     */
1808    function compareSequenceDefinitions($sequence_name, $current_definition,
1809        $previous_definition, &$defined_sequences)
1810    {
1811        $changes = array();
1812
1813        if (is_array($current_definition)) {
1814            $was_sequence_name = $sequence_name;
1815            if (!empty($previous_definition[$sequence_name])
1816                && isset($previous_definition[$sequence_name]['was'])
1817                && $previous_definition[$sequence_name]['was'] == $was_sequence_name
1818            ) {
1819                $was_sequence_name = $sequence_name;
1820            } elseif (!empty($current_definition['was'])) {
1821                $was_sequence_name = $current_definition['was'];
1822            }
1823            if (!empty($previous_definition[$was_sequence_name])) {
1824                if ($was_sequence_name != $sequence_name) {
1825                    $changes['change'][$was_sequence_name]['name'] = $sequence_name;
1826                }
1827
1828                if (!empty($defined_sequences[$was_sequence_name])) {
1829                    return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null,
1830                        'the sequence "'.$was_sequence_name.'" was specified as base'.
1831                        ' of more than of sequence of the database');
1832                }
1833
1834                $defined_sequences[$was_sequence_name] = true;
1835
1836                $change = array();
1837                if (!empty($current_definition['start'])
1838                    && isset($previous_definition[$was_sequence_name]['start'])
1839                    && $current_definition['start'] != $previous_definition[$was_sequence_name]['start']
1840                ) {
1841                    $change['start'] = $previous_definition[$sequence_name]['start'];
1842                }
1843                if (isset($current_definition['on']['table'])
1844                    && isset($previous_definition[$was_sequence_name]['on']['table'])
1845                    && $current_definition['on']['table'] != $previous_definition[$was_sequence_name]['on']['table']
1846                    && isset($current_definition['on']['field'])
1847                    && isset($previous_definition[$was_sequence_name]['on']['field'])
1848                    && $current_definition['on']['field'] != $previous_definition[$was_sequence_name]['on']['field']
1849                ) {
1850                    $change['on'] = $current_definition['on'];
1851                }
1852                if (!empty($change)) {
1853                    $changes['change'][$was_sequence_name][$sequence_name] = $change;
1854                }
1855            } else {
1856                if ($sequence_name != $was_sequence_name) {
1857                    return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null,
1858                        'it was specified a previous sequence name ("'.$was_sequence_name.
1859                        '") for sequence "'.$sequence_name.'" that does not exist');
1860                }
1861                $changes['add'][$sequence_name] = true;
1862            }
1863        }
1864        return $changes;
1865    }
1866    // }}}
1867    // {{{ verifyAlterDatabase()
1868
1869    /**
1870     * Verify that the changes requested are supported
1871     *
1872     * @param array $changes associative array that contains the definition of the changes
1873     *              that are meant to be applied to the database structure.
1874     *
1875     * @return bool|MDB2_Error MDB2_OK or error object
1876     * @access public
1877     */
1878    function verifyAlterDatabase($changes)
1879    {
1880        if (!empty($changes['tables']['change']) && is_array($changes['tables']['change'])) {
1881            foreach ($changes['tables']['change'] as $table_name => $table) {
1882                if (!empty($table['indexes']) && is_array($table['indexes'])) {
1883                    if (!$this->db->supports('indexes')) {
1884                        return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED, null, null,
1885                            'indexes are not supported');
1886                    }
1887                    $table_changes = count($table['indexes']);
1888                    if (!empty($table['indexes']['add'])) {
1889                        $table_changes--;
1890                    }
1891                    if (!empty($table['indexes']['remove'])) {
1892                        $table_changes--;
1893                    }
1894                    if (!empty($table['indexes']['change'])) {
1895                        $table_changes--;
1896                    }
1897                    if ($table_changes) {
1898                        return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED, null, null,
1899                            'index alteration not yet supported: '.implode(', ', array_keys($table['indexes'])));
1900                    }
1901                }
1902                unset($table['indexes']);
1903                $result = $this->db->manager->alterTable($table_name, $table, true);
1904                if (PEAR::isError($result)) {
1905                    return $result;
1906                }
1907            }
1908        }
1909        if (!empty($changes['sequences']) && is_array($changes['sequences'])) {
1910            if (!$this->db->supports('sequences')) {
1911                return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED, null, null,
1912                    'sequences are not supported');
1913            }
1914            $sequence_changes = count($changes['sequences']);
1915            if (!empty($changes['sequences']['add'])) {
1916                $sequence_changes--;
1917            }
1918            if (!empty($changes['sequences']['remove'])) {
1919                $sequence_changes--;
1920            }
1921            if (!empty($changes['sequences']['change'])) {
1922                $sequence_changes--;
1923            }
1924            if ($sequence_changes) {
1925                return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED, null, null,
1926                    'sequence alteration not yet supported: '.implode(', ', array_keys($changes['sequences'])));
1927            }
1928        }
1929        return MDB2_OK;
1930    }
1931
1932    // }}}
1933    // {{{ alterDatabaseIndexes()
1934
1935    /**
1936     * Execute the necessary actions to implement the requested changes
1937     * in the indexes inside a database structure.
1938     *
1939     * @param string $table_name name of the table
1940     * @param array  $changes    associative array that contains the definition of the changes
1941     *                       that are meant to be applied to the database structure.
1942     *
1943     * @return bool|MDB2_Error MDB2_OK or error object
1944     * @access public
1945     */
1946    function alterDatabaseIndexes($table_name, $changes)
1947    {
1948        $alterations = 0;
1949        if (empty($changes)) {
1950            return $alterations;
1951        }
1952
1953        if (!empty($changes['remove']) && is_array($changes['remove'])) {
1954            foreach ($changes['remove'] as $index_name => $index) {
1955                $this->db->expectError(MDB2_ERROR_NOT_FOUND);
1956                if (!empty($index['primary']) || !empty($index['unique'])) {
1957                    $result = $this->db->manager->dropConstraint($table_name, $index_name, !empty($index['primary']));
1958                } else {
1959                    $result = $this->db->manager->dropIndex($table_name, $index_name);
1960                }
1961                $this->db->popExpect();
1962                if (PEAR::isError($result) && !MDB2::isError($result, MDB2_ERROR_NOT_FOUND)) {
1963                    return $result;
1964                }
1965                $alterations++;
1966            }
1967        }
1968        if (!empty($changes['change']) && is_array($changes['change'])) {
1969            foreach ($changes['change'] as $index_name => $index) {
1970                /**
1971                 * Drop existing index/constraint first.
1972                 * Since $changes doesn't tell us whether it's an index or a constraint before the change,
1973                 * we have to find out and call the appropriate method.
1974                 */
1975                if (in_array($index_name, $this->db->manager->listTableIndexes($table_name))) {
1976                    $result = $this->db->manager->dropIndex($table_name, $index_name);
1977                } elseif (in_array($index_name, $this->db->manager->listTableConstraints($table_name))) {
1978                    $result = $this->db->manager->dropConstraint($table_name, $index_name);
1979                }
1980                if (!empty($result) && PEAR::isError($result)) {
1981                    return $result;
1982                }
1983
1984                if (!empty($index['primary']) || !empty($index['unique'])) {
1985                    $result = $this->db->manager->createConstraint($table_name, $index_name, $index);
1986                } else {
1987                    $result = $this->db->manager->createIndex($table_name, $index_name, $index);
1988                }
1989                if (PEAR::isError($result)) {
1990                    return $result;
1991                }
1992                $alterations++;
1993            }
1994        }
1995        if (!empty($changes['add']) && is_array($changes['add'])) {
1996            foreach ($changes['add'] as $index_name => $index) {
1997                if (!empty($index['primary']) || !empty($index['unique'])) {
1998                    $result = $this->db->manager->createConstraint($table_name, $index_name, $index);
1999                } else {
2000                    $result = $this->db->manager->createIndex($table_name, $index_name, $index);
2001                }
2002                if (PEAR::isError($result)) {
2003                    return $result;
2004                }
2005                $alterations++;
2006            }
2007        }
2008
2009        return $alterations;
2010    }
2011
2012    // }}}
2013    // {{{ alterDatabaseTables()
2014
2015    /**
2016     * Execute the necessary actions to implement the requested changes
2017     * in the tables inside a database structure.
2018     *
2019     * @param array $current_definition  multi dimensional array that contains the current definition
2020     * @param array $previous_definition multi dimensional array that contains the previous definition
2021     * @param array $changes             associative array that contains the definition of the changes
2022     *                                   that are meant to be applied to the database structure.
2023     *
2024     * @return bool|MDB2_Error MDB2_OK or error object
2025     * @access public
2026     */
2027    function alterDatabaseTables($current_definition, $previous_definition, $changes)
2028    {
2029        /* FIXME: tables marked to be added are initialized by createTable(), others don't */
2030        $alterations = 0;
2031        if (empty($changes)) {
2032            return $alterations;
2033        }
2034
2035        if (!empty($changes['add']) && is_array($changes['add'])) {
2036            foreach ($changes['add'] as $table_name => $table) {
2037                $result = $this->createTable($table_name, $current_definition[$table_name]);
2038                if (PEAR::isError($result)) {
2039                    return $result;
2040                }
2041                $alterations++;
2042            }
2043        }
2044
2045        if ($this->options['drop_obsolete_objects']
2046            && !empty($changes['remove'])
2047            && is_array($changes['remove'])
2048        ) {
2049            foreach ($changes['remove'] as $table_name => $table) {
2050                $result = $this->db->manager->dropTable($table_name);
2051                if (PEAR::isError($result)) {
2052                    return $result;
2053                }
2054                $alterations++;
2055            }
2056        }
2057
2058        if (!empty($changes['change']) && is_array($changes['change'])) {
2059            foreach ($changes['change'] as $table_name => $table) {
2060                $indexes = array();
2061                if (!empty($table['indexes'])) {
2062                    $indexes = $table['indexes'];
2063                    unset($table['indexes']);
2064                }
2065                if (!empty($indexes['remove'])) {
2066                    $result = $this->alterDatabaseIndexes($table_name, array('remove' => $indexes['remove']));
2067                    if (PEAR::isError($result)) {
2068                        return $result;
2069                    }
2070                    unset($indexes['remove']);
2071                    $alterations += $result;
2072                }
2073                $result = $this->db->manager->alterTable($table_name, $table, false);
2074                if (PEAR::isError($result)) {
2075                    return $result;
2076                }
2077                $alterations++;
2078
2079                // table may be renamed at this point
2080                if (!empty($table['name'])) {
2081                    $table_name = $table['name'];
2082                }
2083
2084                if (!empty($indexes)) {
2085                    $result = $this->alterDatabaseIndexes($table_name, $indexes);
2086                    if (PEAR::isError($result)) {
2087                        return $result;
2088                    }
2089                    $alterations += $result;
2090                }
2091            }
2092        }
2093
2094        return $alterations;
2095    }
2096
2097    // }}}
2098    // {{{ alterDatabaseSequences()
2099
2100    /**
2101     * Execute the necessary actions to implement the requested changes
2102     * in the sequences inside a database structure.
2103     *
2104     * @param array $current_definition  multi dimensional array that contains the current definition
2105     * @param array $previous_definition multi dimensional array that contains the previous definition
2106     * @param array $changes             associative array that contains the definition of the changes
2107     *                                   that are meant to be applied to the database structure.
2108     *
2109     * @return bool|MDB2_Error MDB2_OK or error object
2110     * @access public
2111     */
2112    function alterDatabaseSequences($current_definition, $previous_definition, $changes)
2113    {
2114        $alterations = 0;
2115        if (empty($changes)) {
2116            return $alterations;
2117        }
2118
2119        if (!empty($changes['add']) && is_array($changes['add'])) {
2120            foreach ($changes['add'] as $sequence_name => $sequence) {
2121                $result = $this->createSequence($sequence_name, $current_definition[$sequence_name]);
2122                if (PEAR::isError($result)) {
2123                    return $result;
2124                }
2125                $alterations++;
2126            }
2127        }
2128
2129        if ($this->options['drop_obsolete_objects']
2130            && !empty($changes['remove'])
2131            && is_array($changes['remove'])
2132        ) {
2133            foreach ($changes['remove'] as $sequence_name => $sequence) {
2134                $result = $this->db->manager->dropSequence($sequence_name);
2135                if (PEAR::isError($result)) {
2136                    return $result;
2137                }
2138                $alterations++;
2139            }
2140        }
2141
2142        if (!empty($changes['change']) && is_array($changes['change'])) {
2143            foreach ($changes['change'] as $sequence_name => $sequence) {
2144                $result = $this->db->manager->dropSequence($previous_definition[$sequence_name]['was']);
2145                if (PEAR::isError($result)) {
2146                    return $result;
2147                }
2148                $result = $this->createSequence($sequence_name, $sequence);
2149                if (PEAR::isError($result)) {
2150                    return $result;
2151                }
2152                $alterations++;
2153            }
2154        }
2155
2156        return $alterations;
2157    }
2158
2159    // }}}
2160    // {{{ alterDatabase()
2161
2162    /**
2163     * Execute the necessary actions to implement the requested changes
2164     * in a database structure.
2165     *
2166     * @param array $current_definition  multi dimensional array that contains the current definition
2167     * @param array $previous_definition multi dimensional array that contains the previous definition
2168     * @param array $changes             associative array that contains the definition of the changes
2169     *                                   that are meant to be applied to the database structure.
2170     *
2171     * @return bool|MDB2_Error MDB2_OK or error object
2172     * @access public
2173     */
2174    function alterDatabase($current_definition, $previous_definition, $changes)
2175    {
2176        $alterations = 0;
2177        if (empty($changes)) {
2178            return $alterations;
2179        }
2180
2181        $result = $this->verifyAlterDatabase($changes);
2182        if (PEAR::isError($result)) {
2183            return $result;
2184        }
2185
2186        if (!empty($current_definition['name'])) {
2187            $previous_database_name = $this->db->setDatabase($current_definition['name']);
2188        }
2189
2190        if (($support_transactions = $this->db->supports('transactions'))
2191            && PEAR::isError($result = $this->db->beginNestedTransaction())
2192        ) {
2193            return $result;
2194        }
2195
2196        if (!empty($changes['tables']) && !empty($current_definition['tables'])) {
2197            $current_tables  = isset($current_definition['tables']) ? $current_definition['tables'] : array();
2198            $previous_tables = isset($previous_definition['tables']) ? $previous_definition['tables'] : array();
2199
2200            $result = $this->alterDatabaseTables($current_tables, $previous_tables, $changes['tables']);
2201            if (is_numeric($result)) {
2202                $alterations += $result;
2203            }
2204        }
2205
2206        if (!PEAR::isError($result) && !empty($changes['sequences'])) {
2207            $current_sequences  = isset($current_definition['sequences']) ? $current_definition['sequences'] : array();
2208            $previous_sequences = isset($previous_definition['sequences']) ? $previous_definition['sequences'] : array();
2209
2210            $result = $this->alterDatabaseSequences($current_sequences, $previous_sequences, $changes['sequences']);
2211            if (is_numeric($result)) {
2212                $alterations += $result;
2213            }
2214        }
2215
2216        if ($support_transactions) {
2217            $res = $this->db->completeNestedTransaction();
2218            if (PEAR::isError($res)) {
2219                $result = $this->raiseError(MDB2_SCHEMA_ERROR, null, null,
2220                    'Could not end transaction ('.
2221                    $res->getMessage().' ('.$res->getUserinfo().'))');
2222            }
2223        } elseif (PEAR::isError($result) && $alterations) {
2224            $result = $this->raiseError(MDB2_SCHEMA_ERROR, null, null,
2225                'the requested database alterations were only partially implemented ('.
2226                $result->getMessage().' ('.$result->getUserinfo().'))');
2227        }
2228
2229        if (isset($previous_database_name)) {
2230            $this->db->setDatabase($previous_database_name);
2231        }
2232        return $result;
2233    }
2234
2235    // }}}
2236    // {{{ dumpDatabaseChanges()
2237
2238    /**
2239     * Dump the changes between two database definitions.
2240     *
2241     * @param array $changes associative array that specifies the list of database
2242     *              definitions changes as returned by the _compareDefinitions
2243     *              manager class function.
2244     *
2245     * @return bool|MDB2_Error MDB2_OK or error object
2246     * @access public
2247     */
2248    function dumpDatabaseChanges($changes)
2249    {
2250        if (!empty($changes['tables'])) {
2251            if (!empty($changes['tables']['add']) && is_array($changes['tables']['add'])) {
2252                foreach ($changes['tables']['add'] as $table_name => $table) {
2253                    $this->db->debug("$table_name:", __FUNCTION__);
2254                    $this->db->debug("\tAdded table '$table_name'", __FUNCTION__);
2255                }
2256            }
2257
2258            if (!empty($changes['tables']['remove']) && is_array($changes['tables']['remove'])) {
2259                if ($this->options['drop_obsolete_objects']) {
2260                    foreach ($changes['tables']['remove'] as $table_name => $table) {
2261                        $this->db->debug("$table_name:", __FUNCTION__);
2262                        $this->db->debug("\tRemoved table '$table_name'", __FUNCTION__);
2263                    }
2264                } else {
2265                    foreach ($changes['tables']['remove'] as $table_name => $table) {
2266                        $this->db->debug("\tObsolete table '$table_name' left as is", __FUNCTION__);
2267                    }
2268                }
2269            }
2270
2271            if (!empty($changes['tables']['change']) && is_array($changes['tables']['change'])) {
2272                foreach ($changes['tables']['change'] as $table_name => $table) {
2273                    if (array_key_exists('name', $table)) {
2274                        $this->db->debug("\tRenamed table '$table_name' to '".$table['name']."'", __FUNCTION__);
2275                    }
2276                    if (!empty($table['add']) && is_array($table['add'])) {
2277                        foreach ($table['add'] as $field_name => $field) {
2278                            $this->db->debug("\tAdded field '".$field_name."'", __FUNCTION__);
2279                        }
2280                    }
2281                    if (!empty($table['remove']) && is_array($table['remove'])) {
2282                        foreach ($table['remove'] as $field_name => $field) {
2283                            $this->db->debug("\tRemoved field '".$field_name."'", __FUNCTION__);
2284                        }
2285                    }
2286                    if (!empty($table['rename']) && is_array($table['rename'])) {
2287                        foreach ($table['rename'] as $field_name => $field) {
2288                            $this->db->debug("\tRenamed field '".$field_name."' to '".$field['name']."'", __FUNCTION__);
2289                        }
2290                    }
2291                    if (!empty($table['change']) && is_array($table['change'])) {
2292                        foreach ($table['change'] as $field_name => $field) {
2293                            $field = $field['definition'];
2294                            if (array_key_exists('type', $field)) {
2295                                $this->db->debug("\tChanged field '$field_name' type to '".$field['type']."'", __FUNCTION__);
2296                            }
2297
2298                            if (array_key_exists('unsigned', $field)) {
2299                                $this->db->debug("\tChanged field '$field_name' type to '".
2300                                                 (!empty($field['unsigned']) && $field['unsigned'] ? '' : 'not ')."unsigned'",
2301                                                 __FUNCTION__);
2302                            }
2303
2304                            if (array_key_exists('length', $field)) {
2305                                $this->db->debug("\tChanged field '$field_name' length to '".
2306                                                (!empty($field['length']) ? $field['length']: 'no length')."'", __FUNCTION__);
2307                            }
2308                            if (array_key_exists('default', $field)) {
2309                                $this->db->debug("\tChanged field '$field_name' default to ".
2310                                                 (isset($field['default']) ? "'".$field['default']."'" : 'NULL'), __FUNCTION__);
2311                            }
2312
2313                            if (array_key_exists('notnull', $field)) {
2314                                $this->db->debug("\tChanged field '$field_name' notnull to ".
2315                                                 (!empty($field['notnull']) && $field['notnull'] ? 'true' : 'false'),
2316                                                 __FUNCTION__);
2317                            }
2318                        }
2319                    }
2320                    if (!empty($table['indexes']) && is_array($table['indexes'])) {
2321                        if (!empty($table['indexes']['add']) && is_array($table['indexes']['add'])) {
2322                            foreach ($table['indexes']['add'] as $index_name => $index) {
2323                                $this->db->debug("\tAdded index '".$index_name.
2324                                    "' of table '$table_name'", __FUNCTION__);
2325                            }
2326                        }
2327                        if (!empty($table['indexes']['remove']) && is_array($table['indexes']['remove'])) {
2328                            foreach ($table['indexes']['remove'] as $index_name => $index) {
2329                                $this->db->debug("\tRemoved index '".$index_name.
2330                                    "' of table '$table_name'", __FUNCTION__);
2331                            }
2332                        }
2333                        if (!empty($table['indexes']['change']) && is_array($table['indexes']['change'])) {
2334                            foreach ($table['indexes']['change'] as $index_name => $index) {
2335                                if (array_key_exists('name', $index)) {
2336                                    $this->db->debug("\tRenamed index '".$index_name."' to '".$index['name'].
2337                                                     "' on table '$table_name'", __FUNCTION__);
2338                                }
2339                                if (array_key_exists('unique', $index)) {
2340                                    $this->db->debug("\tChanged index '".$index_name."' unique to '".
2341                                                     !empty($index['unique'])."' on table '$table_name'", __FUNCTION__);
2342                                }
2343                                if (array_key_exists('primary', $index)) {
2344                                    $this->db->debug("\tChanged index '".$index_name."' primary to '".
2345                                                     !empty($index['primary'])."' on table '$table_name'", __FUNCTION__);
2346                                }
2347                                if (array_key_exists('change', $index)) {
2348                                    $this->db->debug("\tChanged index '".$index_name.
2349                                        "' on table '$table_name'", __FUNCTION__);
2350                                }
2351                            }
2352                        }
2353                    }
2354                }
2355            }
2356        }
2357        if (!empty($changes['sequences'])) {
2358            if (!empty($changes['sequences']['add']) && is_array($changes['sequences']['add'])) {
2359                foreach ($changes['sequences']['add'] as $sequence_name => $sequence) {
2360                    $this->db->debug("$sequence_name:", __FUNCTION__);
2361                    $this->db->debug("\tAdded sequence '$sequence_name'", __FUNCTION__);
2362                }
2363            }
2364            if (!empty($changes['sequences']['remove']) && is_array($changes['sequences']['remove'])) {
2365                if ($this->options['drop_obsolete_objects']) {
2366                    foreach ($changes['sequences']['remove'] as $sequence_name => $sequence) {
2367                        $this->db->debug("$sequence_name:", __FUNCTION__);
2368                        $this->db->debug("\tRemoved sequence '$sequence_name'", __FUNCTION__);
2369                    }
2370                } else {
2371                    foreach ($changes['sequences']['remove'] as $sequence_name => $sequence) {
2372                        $this->db->debug("\tObsolete sequence '$sequence_name' left as is", __FUNCTION__);
2373                    }
2374                }
2375            }
2376            if (!empty($changes['sequences']['change']) && is_array($changes['sequences']['change'])) {
2377                foreach ($changes['sequences']['change'] as $sequence_name => $sequence) {
2378                    if (array_key_exists('name', $sequence)) {
2379                        $this->db->debug("\tRenamed sequence '$sequence_name' to '".
2380                                         $sequence['name']."'", __FUNCTION__);
2381                    }
2382                    if (!empty($sequence['change']) && is_array($sequence['change'])) {
2383                        foreach ($sequence['change'] as $sequence_name => $sequence) {
2384                            if (array_key_exists('start', $sequence)) {
2385                                $this->db->debug("\tChanged sequence '$sequence_name' start to '".
2386                                                 $sequence['start']."'", __FUNCTION__);
2387                            }
2388                        }
2389                    }
2390                }
2391            }
2392        }
2393        return MDB2_OK;
2394    }
2395
2396    // }}}
2397    // {{{ dumpDatabase()
2398
2399    /**
2400     * Dump a previously parsed database structure in the Metabase schema
2401     * XML based format suitable for the Metabase parser. This function
2402     * may optionally dump the database definition with initialization
2403     * commands that specify the data that is currently present in the tables.
2404     *
2405     * @param array $database_definition multi dimensional array that contains the current definition
2406     * @param array $arguments           associative array that takes pairs of tag
2407     * names and values that define dump options.
2408     *                 <pre>array (
2409     *                     'output_mode'    =>    String
2410     *                         'file' :   dump into a file
2411     *                         default:   dump using a function
2412     *                     'output'        =>    String
2413     *                         depending on the 'Output_Mode'
2414     *                                  name of the file
2415     *                                  name of the function
2416     *                     'end_of_line'        =>    String
2417     *                         end of line delimiter that should be used
2418     *                         default: "\n"
2419     *                 );</pre>
2420     * @param int   $dump                Int that determines what data to dump
2421     *              + MDB2_SCHEMA_DUMP_ALL       : the entire db
2422     *              + MDB2_SCHEMA_DUMP_STRUCTURE : only the structure of the db
2423     *              + MDB2_SCHEMA_DUMP_CONTENT   : only the content of the db
2424     *
2425     * @return bool|MDB2_Error MDB2_OK or error object
2426     * @access public
2427     */
2428    function dumpDatabase($database_definition, $arguments, $dump = MDB2_SCHEMA_DUMP_ALL)
2429    {
2430        $class_name = $this->options['writer'];
2431
2432        $result = MDB2::loadClass($class_name, $this->db->getOption('debug'));
2433        if (PEAR::isError($result)) {
2434            return $result;
2435        }
2436
2437        // get initialization data
2438        if (isset($database_definition['tables']) && is_array($database_definition['tables'])
2439            && $dump == MDB2_SCHEMA_DUMP_ALL || $dump == MDB2_SCHEMA_DUMP_CONTENT
2440        ) {
2441            foreach ($database_definition['tables'] as $table_name => $table) {
2442                $fields  = array();
2443                $fieldsq = array();
2444                foreach ($table['fields'] as $field_name => $field) {
2445                    $fields[$field_name] = $field['type'];
2446
2447                    $fieldsq[] = $this->db->quoteIdentifier($field_name, true);
2448                }
2449
2450                $query  = 'SELECT '.implode(', ', $fieldsq).' FROM ';
2451                $query .= $this->db->quoteIdentifier($table_name, true);
2452
2453                $data = $this->db->queryAll($query, $fields, MDB2_FETCHMODE_ASSOC);
2454
2455                if (PEAR::isError($data)) {
2456                    return $data;
2457                }
2458
2459                if (!empty($data)) {
2460                    $initialization    = array();
2461                    $lob_buffer_length = $this->db->getOption('lob_buffer_length');
2462                    foreach ($data as $row) {
2463                        $rows = array();
2464                        foreach ($row as $key => $lob) {
2465                            if (is_resource($lob)) {
2466                                $value = '';
2467                                while (!feof($lob)) {
2468                                    $value .= fread($lob, $lob_buffer_length);
2469                                }
2470                                $row[$key] = $value;
2471                            }
2472                            $rows[] = array('name' => $key, 'group' => array('type' => 'value', 'data' => $row[$key]));
2473                        }
2474                        $initialization[] = array('type' => 'insert', 'data' => array('field' => $rows));
2475                    }
2476                    $database_definition['tables'][$table_name]['initialization'] = $initialization;
2477                }
2478            }
2479        }
2480
2481        $writer = new $class_name($this->options['valid_types']);
2482        return $writer->dumpDatabase($database_definition, $arguments, $dump);
2483    }
2484
2485    // }}}
2486    // {{{ writeInitialization()
2487
2488    /**
2489     * Write initialization and sequences
2490     *
2491     * @param string|array $data      data file or data array
2492     * @param string|array $structure structure file or array
2493     * @param array        $variables associative array that is passed to the argument
2494     * of the same name to the parseDatabaseDefinitionFile function. (there third
2495     * param)
2496     *
2497     * @return bool|MDB2_Error MDB2_OK or error object
2498     * @access public
2499     */
2500    function writeInitialization($data, $structure = false, $variables = array())
2501    {
2502        if ($structure) {
2503            $structure = $this->parseDatabaseDefinition($structure, false, $variables);
2504            if (PEAR::isError($structure)) {
2505                return $structure;
2506            }
2507        }
2508
2509        $data = $this->parseDatabaseDefinition($data, false, $variables, false, $structure);
2510        if (PEAR::isError($data)) {
2511            return $data;
2512        }
2513
2514        $previous_database_name = null;
2515        if (!empty($data['name'])) {
2516            $previous_database_name = $this->db->setDatabase($data['name']);
2517        } elseif (!empty($structure['name'])) {
2518            $previous_database_name = $this->db->setDatabase($structure['name']);
2519        }
2520
2521        if (!empty($data['tables']) && is_array($data['tables'])) {
2522            foreach ($data['tables'] as $table_name => $table) {
2523                if (empty($table['initialization'])) {
2524                    continue;
2525                }
2526                $result = $this->initializeTable($table_name, $table);
2527                if (PEAR::isError($result)) {
2528                    return $result;
2529                }
2530            }
2531        }
2532
2533        if (!empty($structure['sequences']) && is_array($structure['sequences'])) {
2534            foreach ($structure['sequences'] as $sequence_name => $sequence) {
2535                if (isset($data['sequences'][$sequence_name])
2536                    || !isset($sequence['on']['table'])
2537                    || !isset($data['tables'][$sequence['on']['table']])
2538                ) {
2539                    continue;
2540                }
2541                $result = $this->createSequence($sequence_name, $sequence, true);
2542                if (PEAR::isError($result)) {
2543                    return $result;
2544                }
2545            }
2546        }
2547        if (!empty($data['sequences']) && is_array($data['sequences'])) {
2548            foreach ($data['sequences'] as $sequence_name => $sequence) {
2549                $result = $this->createSequence($sequence_name, $sequence, true);
2550                if (PEAR::isError($result)) {
2551                    return $result;
2552                }
2553            }
2554        }
2555
2556        if (isset($previous_database_name)) {
2557            $this->db->setDatabase($previous_database_name);
2558        }
2559
2560        return MDB2_OK;
2561    }
2562
2563    // }}}
2564    // {{{ updateDatabase()
2565
2566    /**
2567     * Compare the correspondent files of two versions of a database schema
2568     * definition: the previously installed and the one that defines the schema
2569     * that is meant to update the database.
2570     * If the specified previous definition file does not exist, this function
2571     * will create the database from the definition specified in the current
2572     * schema file.
2573     * If both files exist, the function assumes that the database was previously
2574     * installed based on the previous schema file and will update it by just
2575     * applying the changes.
2576     * If this function succeeds, the contents of the current schema file are
2577     * copied to replace the previous schema file contents. Any subsequent schema
2578     * changes should only be done on the file specified by the $current_schema_file
2579     * to let this function make a consistent evaluation of the exact changes that
2580     * need to be applied.
2581     *
2582     * @param string|array $current_schema            filename or array of the updated database schema definition.
2583     * @param string|array $previous_schema           filename or array of the previously installed database schema definition.
2584     * @param array        $variables                 associative array that is passed to the argument of the same
2585     *                                                name to the parseDatabaseDefinitionFile function. (there third param)
2586     * @param bool         $disable_query             determines if the disable_query option should be set to true
2587     *                                                for the alterDatabase() or createDatabase() call
2588     * @param bool         $overwrite_old_schema_file Overwrite?
2589     *
2590     * @return bool|MDB2_Error MDB2_OK or error object
2591     * @access public
2592     */
2593    function updateDatabase($current_schema, $previous_schema = false,
2594                            $variables = array(), $disable_query = false,
2595                            $overwrite_old_schema_file = false)
2596    {
2597        $current_definition = $this->parseDatabaseDefinition($current_schema, false, $variables,
2598                                                             $this->options['fail_on_invalid_names']);
2599
2600        if (PEAR::isError($current_definition)) {
2601            return $current_definition;
2602        }
2603
2604        $previous_definition = false;
2605        if ($previous_schema) {
2606            $previous_definition = $this->parseDatabaseDefinition($previous_schema, true, $variables,
2607                                                                  $this->options['fail_on_invalid_names']);
2608            if (PEAR::isError($previous_definition)) {
2609                return $previous_definition;
2610            }
2611        }
2612
2613        if ($previous_definition) {
2614            $dbExists = $this->db->databaseExists($current_definition['name']);
2615            if (PEAR::isError($dbExists)) {
2616                return $dbExists;
2617            }
2618
2619            if (!$dbExists) {
2620                 return $this->raiseError(MDB2_SCHEMA_ERROR, null, null,
2621                    'database to update does not exist: '.$current_definition['name']);
2622            }
2623
2624            $changes = $this->compareDefinitions($current_definition, $previous_definition);
2625            if (PEAR::isError($changes)) {
2626                return $changes;
2627            }
2628
2629            if (is_array($changes)) {
2630                $this->db->setOption('disable_query', $disable_query);
2631                $result = $this->alterDatabase($current_definition, $previous_definition, $changes);
2632                $this->db->setOption('disable_query', false);
2633                if (PEAR::isError($result)) {
2634                    return $result;
2635                }
2636                $copy = true;
2637                if ($this->db->options['debug']) {
2638                    $result = $this->dumpDatabaseChanges($changes);
2639                    if (PEAR::isError($result)) {
2640                        return $result;
2641                    }
2642                }
2643            }
2644        } else {
2645            $this->db->setOption('disable_query', $disable_query);
2646            $result = $this->createDatabase($current_definition);
2647            $this->db->setOption('disable_query', false);
2648            if (PEAR::isError($result)) {
2649                return $result;
2650            }
2651        }
2652
2653        if ($overwrite_old_schema_file
2654            && !$disable_query
2655            && is_string($previous_schema) && is_string($current_schema)
2656            && !copy($current_schema, $previous_schema)) {
2657
2658            return $this->raiseError(MDB2_SCHEMA_ERROR, null, null,
2659                'Could not copy the new database definition file to the current file');
2660        }
2661
2662        return MDB2_OK;
2663    }
2664    // }}}
2665    // {{{ errorMessage()
2666
2667    /**
2668     * Return a textual error message for a MDB2 error code
2669     *
2670     * @param int|array $value integer error code, <code>null</code> to get the
2671     *                          current error code-message map,
2672     *                          or an array with a new error code-message map
2673     *
2674     * @return  string  error message, or false if the error code was not recognized
2675     * @access public
2676     */
2677    public static function errorMessage($value = null)
2678    {
2679        static $errorMessages;
2680        if (is_array($value)) {
2681            $errorMessages = $value;
2682            return MDB2_OK;
2683        } elseif (!isset($errorMessages)) {
2684            $errorMessages = array(
2685                MDB2_SCHEMA_ERROR              => 'unknown error',
2686                MDB2_SCHEMA_ERROR_PARSE        => 'schema parse error',
2687                MDB2_SCHEMA_ERROR_VALIDATE     => 'schema validation error',
2688                MDB2_SCHEMA_ERROR_INVALID      => 'invalid',
2689                MDB2_SCHEMA_ERROR_UNSUPPORTED  => 'not supported',
2690                MDB2_SCHEMA_ERROR_WRITER       => 'schema writer error',
2691            );
2692        }
2693
2694        if (is_null($value)) {
2695            return $errorMessages;
2696        }
2697
2698        if (PEAR::isError($value)) {
2699            $value = $value->getCode();
2700        }
2701
2702        return !empty($errorMessages[$value]) ?
2703           $errorMessages[$value] : $errorMessages[MDB2_SCHEMA_ERROR];
2704    }
2705
2706    // }}}
2707    // {{{ raiseError()
2708
2709    /**
2710     * This method is used to communicate an error and invoke error
2711     * callbacks etc.  Basically a wrapper for PEAR::raiseError
2712     * without the message string.
2713     *
2714     * @param int|PEAR_Error $code     integer error code or and PEAR_Error instance
2715     * @param int            $mode     error mode, see PEAR_Error docs
2716     *                                 error level (E_USER_NOTICE etc).  If error mode is
2717     *                                 PEAR_ERROR_CALLBACK, this is the callback function,
2718     *                                 either as a function name, or as an array of an
2719     *                                 object and method name.  For other error modes this
2720     *                                 parameter is ignored.
2721     * @param array          $options  Options, depending on the mode, @see PEAR::setErrorHandling
2722     * @param string         $userinfo Extra debug information.  Defaults to the last
2723     *                                 query and native error code.
2724     *
2725     * @return object  a PEAR error object
2726     * @access  public
2727     * @see PEAR_Error
2728     */
2729    public static function &raiseError($code = null, $mode = null, $options = null, $userinfo = null)
2730    {
2731        $err = PEAR::raiseError(null, $code, $mode, $options,
2732                                $userinfo, 'MDB2_Schema_Error', true);
2733        return $err;
2734    }
2735
2736    // }}}
2737    // {{{ isError()
2738
2739    /**
2740     * Tell whether a value is an MDB2_Schema error.
2741     *
2742     * @param mixed $data the value to test
2743     * @param int   $code if $data is an error object, return true only if $code is
2744     *                    a string and $db->getMessage() == $code or
2745     *                    $code is an integer and $db->getCode() == $code
2746     *
2747     * @return  bool  true if parameter is an error
2748     * @access  public
2749     */
2750    public static function isError($data, $code = null)
2751    {
2752        if (is_a($data, 'MDB2_Schema_Error')) {
2753            if (is_null($code)) {
2754                return true;
2755            } elseif (is_string($code)) {
2756                return $data->getMessage() === $code;
2757            } else {
2758                $code = (array)$code;
2759                return in_array($data->getCode(), $code);
2760            }
2761        }
2762        return false;
2763    }
2764
2765    // }}}
2766}
2767
2768/**
2769 * MDB2_Schema_Error implements a class for reporting portable database error
2770 * messages.
2771 *
2772 * @category Database
2773 * @package  MDB2_Schema
2774 * @author   Stig Bakken <ssb@fast.no>
2775 * @license  BSD http://www.opensource.org/licenses/bsd-license.php
2776 * @link     http://pear.php.net/packages/MDB2_Schema
2777 */
2778class MDB2_Schema_Error extends PEAR_Error
2779{
2780    /**
2781     * MDB2_Schema_Error constructor.
2782     *
2783     * @param mixed $code      error code, or string with error message.
2784     * @param int   $mode      what 'error mode' to operate in
2785     * @param int   $level     what error level to use for $mode & PEAR_ERROR_TRIGGER
2786     * @param mixed $debuginfo additional debug info, such as the last query
2787     *
2788     * @access  public
2789     */
2790    function __construct($code = MDB2_SCHEMA_ERROR, $mode = PEAR_ERROR_RETURN,
2791              $level = E_USER_NOTICE, $debuginfo = null)
2792    {
2793        parent::__construct('MDB2_Schema Error: ' . MDB2_Schema::errorMessage($code), $code,
2794            $mode, $level, $debuginfo);
2795    }
2796}
2797