diff options
Diffstat (limited to '3dparty/MDB2/Schema.php')
-rw-r--r-- | 3dparty/MDB2/Schema.php | 2763 |
1 files changed, 2763 insertions, 0 deletions
diff --git a/3dparty/MDB2/Schema.php b/3dparty/MDB2/Schema.php new file mode 100644 index 00000000000..44518b32658 --- /dev/null +++ b/3dparty/MDB2/Schema.php @@ -0,0 +1,2763 @@ +<?php +/** + * PHP version 4, 5 + * + * Copyright (c) 1998-2008 Manuel Lemos, Tomas V.V.Cox, + * Stig. S. Bakken, Lukas Smith, Igor Feghali + * All rights reserved. + * + * MDB2_Schema enables users to maintain RDBMS independant schema files + * in XML that can be used to manipulate both data and database schemas + * This LICENSE is in the BSD license style. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of Manuel Lemos, Tomas V.V.Cox, Stig. S. Bakken, + * Lukas Smith, Igor Feghali nor the names of his contributors may be + * used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * Author: Lukas Smith <smith@pooteeweet.org> + * Author: Igor Feghali <ifeghali@php.net> + * + * @category Database + * @package MDB2_Schema + * @author Lukas Smith <smith@pooteeweet.org> + * @author Igor Feghali <ifeghali@php.net> + * @license BSD http://www.opensource.org/licenses/bsd-license.php + * @version CVS: $Id: Schema.php,v 1.132 2009/02/22 21:43:22 ifeghali Exp $ + * @link http://pear.php.net/packages/MDB2_Schema + */ + +// require_once('MDB2.php'); + +define('MDB2_SCHEMA_DUMP_ALL', 0); +define('MDB2_SCHEMA_DUMP_STRUCTURE', 1); +define('MDB2_SCHEMA_DUMP_CONTENT', 2); + +/** + * If you add an error code here, make sure you also add a textual + * version of it in MDB2_Schema::errorMessage(). + */ + +define('MDB2_SCHEMA_ERROR', -1); +define('MDB2_SCHEMA_ERROR_PARSE', -2); +define('MDB2_SCHEMA_ERROR_VALIDATE', -3); +define('MDB2_SCHEMA_ERROR_UNSUPPORTED', -4); // Driver does not support this function +define('MDB2_SCHEMA_ERROR_INVALID', -5); // Invalid attribute value +define('MDB2_SCHEMA_ERROR_WRITER', -6); + +/** + * The database manager is a class that provides a set of database + * management services like installing, altering and dumping the data + * structures of databases. + * + * @category Database + * @package MDB2_Schema + * @author Lukas Smith <smith@pooteeweet.org> + * @license BSD http://www.opensource.org/licenses/bsd-license.php + * @link http://pear.php.net/packages/MDB2_Schema + */ +class MDB2_Schema extends PEAR +{ + // {{{ properties + + var $db; + + var $warnings = array(); + + var $options = array( + 'fail_on_invalid_names' => true, + 'dtd_file' => false, + 'valid_types' => array(), + 'force_defaults' => true, + 'parser' => 'MDB2_Schema_Parser', + 'writer' => 'MDB2_Schema_Writer', + 'validate' => 'MDB2_Schema_Validate', + 'drop_missing_tables' => false + ); + + // }}} + // {{{ apiVersion() + + /** + * Return the MDB2 API version + * + * @return string the MDB2 API version number + * @access public + */ + function apiVersion() + { + return '0.4.3'; + } + + // }}} + // {{{ arrayMergeClobber() + + /** + * Clobbers two arrays together + * + * @param array $a1 array that should be clobbered + * @param array $a2 array that should be clobbered + * + * @return array|false array on success and false on error + * + * @access public + * @author kc@hireability.com + */ + function arrayMergeClobber($a1, $a2) + { + if (!is_array($a1) || !is_array($a2)) { + return false; + } + foreach ($a2 as $key => $val) { + if (is_array($val) && array_key_exists($key, $a1) && is_array($a1[$key])) { + $a1[$key] = MDB2_Schema::arrayMergeClobber($a1[$key], $val); + } else { + $a1[$key] = $val; + } + } + return $a1; + } + + // }}} + // {{{ resetWarnings() + + /** + * reset the warning array + * + * @access public + * @return void + */ + function resetWarnings() + { + $this->warnings = array(); + } + + // }}} + // {{{ getWarnings() + + /** + * Get all warnings in reverse order + * + * This means that the last warning is the first element in the array + * + * @return array with warnings + * @access public + * @see resetWarnings() + */ + function getWarnings() + { + return array_reverse($this->warnings); + } + + // }}} + // {{{ setOption() + + /** + * Sets the option for the db class + * + * @param string $option option name + * @param mixed $value value for the option + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function setOption($option, $value) + { + if (isset($this->options[$option])) { + if (is_null($value)) { + return $this->raiseError(MDB2_SCHEMA_ERROR, null, null, + 'may not set an option to value null'); + } + $this->options[$option] = $value; + return MDB2_OK; + } + return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED, null, null, + "unknown option $option"); + } + + // }}} + // {{{ getOption() + + /** + * returns the value of an option + * + * @param string $option option name + * + * @return mixed the option value or error object + * @access public + */ + function getOption($option) + { + if (isset($this->options[$option])) { + return $this->options[$option]; + } + return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED, + null, null, "unknown option $option"); + } + + // }}} + // {{{ factory() + + /** + * Create a new MDB2 object for the specified database type + * type + * + * @param string|array|MDB2_Driver_Common &$db 'data source name', see the + * MDB2::parseDSN method for a description of the dsn format. + * Can also be specified as an array of the + * format returned by @see MDB2::parseDSN. + * Finally you can also pass an existing db object to be used. + * @param array $options An associative array of option names and their values. + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + * @see MDB2::parseDSN + */ + function &factory(&$db, $options = array()) + { + $obj =new MDB2_Schema(); + $result = $obj->connect($db, $options); + if (PEAR::isError($result)) { + return $result; + } + return $obj; + } + + // }}} + // {{{ connect() + + /** + * Create a new MDB2 connection object and connect to the specified + * database + * + * @param string|array|MDB2_Driver_Common &$db 'data source name', see the + * MDB2::parseDSN method for a description of the dsn format. + * Can also be specified as an array of the + * format returned by MDB2::parseDSN. + * Finally you can also pass an existing db object to be used. + * @param array $options An associative array of option names and their values. + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + * @see MDB2::parseDSN + */ + function connect(&$db, $options = array()) + { + $db_options = array(); + if (is_array($options)) { + foreach ($options as $option => $value) { + if (array_key_exists($option, $this->options)) { + $result = $this->setOption($option, $value); + if (PEAR::isError($result)) { + return $result; + } + } else { + $db_options[$option] = $value; + } + } + } + $this->disconnect(); + if (!MDB2::isConnection($db)) { + $db =& MDB2::factory($db, $db_options); + } + + if (PEAR::isError($db)) { + return $db; + } + $this->db =& $db; + $this->db->loadModule('Datatype'); + $this->db->loadModule('Manager'); + $this->db->loadModule('Reverse'); + $this->db->loadModule('Function'); + if (empty($this->options['valid_types'])) { + $this->options['valid_types'] = $this->db->datatype->getValidTypes(); + } + + return MDB2_OK; + } + + // }}} + // {{{ disconnect() + + /** + * Log out and disconnect from the database. + * + * @access public + * @return void + */ + function disconnect() + { + if (MDB2::isConnection($this->db)) { + $this->db->disconnect(); + unset($this->db); + } + } + + // }}} + // {{{ parseDatabaseDefinition() + + /** + * Parse a database definition from a file or an array + * + * @param string|array $schema the database schema array or file name + * @param bool $skip_unreadable if non readable files should be skipped + * @param array $variables associative array that the defines the text string values + * that are meant to be used to replace the variables that are + * used in the schema description. + * @param bool $fail_on_invalid_names make function fail on invalid names + * @param array $structure database structure definition + * + * @access public + * @return array + */ + function parseDatabaseDefinition($schema, $skip_unreadable = false, $variables = array(), + $fail_on_invalid_names = true, $structure = false) + { + $database_definition = false; + if (is_string($schema)) { + // if $schema is not readable then we just skip it + // and simply copy the $current_schema file to that file name + if (is_readable($schema)) { + $database_definition = $this->parseDatabaseDefinitionFile($schema, $variables, $fail_on_invalid_names, $structure); + } + } elseif (is_array($schema)) { + $database_definition = $schema; + } + if (!$database_definition && !$skip_unreadable) { + $database_definition = $this->raiseError(MDB2_SCHEMA_ERROR, null, null, + 'invalid data type of schema or unreadable data source'); + } + return $database_definition; + } + + // }}} + // {{{ parseDatabaseDefinitionFile() + + /** + * Parse a database definition file by creating a schema format + * parser object and passing the file contents as parser input data stream. + * + * @param string $input_file the database schema file. + * @param array $variables associative array that the defines the text string values + * that are meant to be used to replace the variables that are + * used in the schema description. + * @param bool $fail_on_invalid_names make function fail on invalid names + * @param array $structure database structure definition + * + * @access public + * @return array + */ + function parseDatabaseDefinitionFile($input_file, $variables = array(), + $fail_on_invalid_names = true, $structure = false) + { + $dtd_file = $this->options['dtd_file']; + if ($dtd_file) { + include_once 'XML/DTD/XmlValidator.php'; + $dtd =new XML_DTD_XmlValidator; + if (!$dtd->isValid($dtd_file, $input_file)) { + return $this->raiseError(MDB2_SCHEMA_ERROR_PARSE, null, null, $dtd->getMessage()); + } + } + + $class_name = $this->options['parser']; + + $result = MDB2::loadClass($class_name, $this->db->getOption('debug')); + if (PEAR::isError($result)) { + return $result; + } + + $parser =new $class_name($variables, $fail_on_invalid_names, $structure, $this->options['valid_types'], $this->options['force_defaults']); + $result = $parser->setInputFile($input_file); + if (PEAR::isError($result)) { + return $result; + } + + $result = $parser->parse(); + if (PEAR::isError($result)) { + return $result; + } + if (PEAR::isError($parser->error)) { + return $parser->error; + } + + return $parser->database_definition; + } + + // }}} + // {{{ getDefinitionFromDatabase() + + /** + * Attempt to reverse engineer a schema structure from an existing MDB2 + * This method can be used if no xml schema file exists yet. + * The resulting xml schema file may need some manual adjustments. + * + * @return array|MDB2_Error array with definition or error object + * @access public + */ + function getDefinitionFromDatabase() + { + $database = $this->db->database_name; + if (empty($database)) { + return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null, + 'it was not specified a valid database name'); + } + $class_name = $this->options['validate']; + + $result = MDB2::loadClass($class_name, $this->db->getOption('debug')); + if (PEAR::isError($result)) { + return $result; + } + + $val =new $class_name($this->options['fail_on_invalid_names'], $this->options['valid_types'], $this->options['force_defaults']); + + $database_definition = array( + 'name' => $database, + 'create' => true, + 'overwrite' => false, + 'charset' => 'utf8', + 'description' => '', + 'comments' => '', + 'tables' => array(), + 'sequences' => array(), + ); + + $tables = $this->db->manager->listTables(); + if (PEAR::isError($tables)) { + return $tables; + } + + foreach ($tables as $table_name) { + $fields = $this->db->manager->listTableFields($table_name); + if (PEAR::isError($fields)) { + return $fields; + } + + $database_definition['tables'][$table_name] = array( + 'was' => '', + 'description' => '', + 'comments' => '', + 'fields' => array(), + 'indexes' => array(), + 'constraints' => array(), + 'initialization' => array() + ); + + $table_definition =& $database_definition['tables'][$table_name]; + foreach ($fields as $field_name) { + $definition = $this->db->reverse->getTableFieldDefinition($table_name, $field_name); + if (PEAR::isError($definition)) { + return $definition; + } + + if (!empty($definition[0]['autoincrement'])) { + $definition[0]['default'] = '0'; + } + + $table_definition['fields'][$field_name] = $definition[0]; + + $field_choices = count($definition); + if ($field_choices > 1) { + $warning = "There are $field_choices type choices in the table $table_name field $field_name (#1 is the default): "; + + $field_choice_cnt = 1; + + $table_definition['fields'][$field_name]['choices'] = array(); + foreach ($definition as $field_choice) { + $table_definition['fields'][$field_name]['choices'][] = $field_choice; + + $warning .= 'choice #'.($field_choice_cnt).': '.serialize($field_choice); + $field_choice_cnt++; + } + $this->warnings[] = $warning; + } + + /** + * The first parameter is used to verify if there are duplicated + * fields which we can guarantee that won't happen when reverse engineering + */ + $result = $val->validateField(array(), $table_definition['fields'][$field_name], $field_name); + if (PEAR::isError($result)) { + return $result; + } + } + + $keys = array(); + + $indexes = $this->db->manager->listTableIndexes($table_name); + if (PEAR::isError($indexes)) { + return $indexes; + } + + if (is_array($indexes)) { + foreach ($indexes as $index_name) { + $this->db->expectError(MDB2_ERROR_NOT_FOUND); + $definition = $this->db->reverse->getTableIndexDefinition($table_name, $index_name); + $this->db->popExpect(); + if (PEAR::isError($definition)) { + if (PEAR::isError($definition, MDB2_ERROR_NOT_FOUND)) { + continue; + } + return $definition; + } + + $keys[$index_name] = $definition; + } + } + + $constraints = $this->db->manager->listTableConstraints($table_name); + if (PEAR::isError($constraints)) { + return $constraints; + } + + if (is_array($constraints)) { + foreach ($constraints as $constraint_name) { + $this->db->expectError(MDB2_ERROR_NOT_FOUND); + $definition = $this->db->reverse->getTableConstraintDefinition($table_name, $constraint_name); + $this->db->popExpect(); + if (PEAR::isError($definition)) { + if (PEAR::isError($definition, MDB2_ERROR_NOT_FOUND)) { + continue; + } + return $definition; + } + + $keys[$constraint_name] = $definition; + } + } + + foreach ($keys as $key_name => $definition) { + if (array_key_exists('foreign', $definition) + && $definition['foreign'] + ) { + /** + * The first parameter is used to verify if there are duplicated + * foreign keys which we can guarantee that won't happen when reverse engineering + */ + $result = $val->validateConstraint(array(), $definition, $key_name); + if (PEAR::isError($result)) { + return $result; + } + + foreach ($definition['fields'] as $field_name => $field) { + /** + * The first parameter is used to verify if there are duplicated + * referencing fields which we can guarantee that won't happen when reverse engineering + */ + $result = $val->validateConstraintField(array(), $field_name); + if (PEAR::isError($result)) { + return $result; + } + + $definition['fields'][$field_name] = ''; + } + + foreach ($definition['references']['fields'] as $field_name => $field) { + /** + * The first parameter is used to verify if there are duplicated + * referenced fields which we can guarantee that won't happen when reverse engineering + */ + $result = $val->validateConstraintReferencedField(array(), $field_name); + if (PEAR::isError($result)) { + return $result; + } + + $definition['references']['fields'][$field_name] = ''; + } + + $table_definition['constraints'][$key_name] = $definition; + } else { + /** + * The first parameter is used to verify if there are duplicated + * indices which we can guarantee that won't happen when reverse engineering + */ + $result = $val->validateIndex(array(), $definition, $key_name); + if (PEAR::isError($result)) { + return $result; + } + + foreach ($definition['fields'] as $field_name => $field) { + /** + * The first parameter is used to verify if there are duplicated + * index fields which we can guarantee that won't happen when reverse engineering + */ + $result = $val->validateIndexField(array(), $field, $field_name); + if (PEAR::isError($result)) { + return $result; + } + + $definition['fields'][$field_name] = $field; + } + + $table_definition['indexes'][$key_name] = $definition; + } + } + + /** + * The first parameter is used to verify if there are duplicated + * tables which we can guarantee that won't happen when reverse engineering + */ + $result = $val->validateTable(array(), $table_definition, $table_name); + if (PEAR::isError($result)) { + return $result; + } + + } + + $sequences = $this->db->manager->listSequences(); + if (PEAR::isError($sequences)) { + return $sequences; + } + + if (is_array($sequences)) { + foreach ($sequences as $sequence_name) { + $definition = $this->db->reverse->getSequenceDefinition($sequence_name); + if (PEAR::isError($definition)) { + return $definition; + } + if (isset($database_definition['tables'][$sequence_name]) + && isset($database_definition['tables'][$sequence_name]['indexes']) + ) { + foreach ($database_definition['tables'][$sequence_name]['indexes'] as $index) { + if (isset($index['primary']) && $index['primary'] + && count($index['fields'] == 1) + ) { + $definition['on'] = array( + 'table' => $sequence_name, + 'field' => key($index['fields']), + ); + break; + } + } + } + + /** + * The first parameter is used to verify if there are duplicated + * sequences which we can guarantee that won't happen when reverse engineering + */ + $result = $val->validateSequence(array(), $definition, $sequence_name); + if (PEAR::isError($result)) { + return $result; + } + + $database_definition['sequences'][$sequence_name] = $definition; + } + } + + $result = $val->validateDatabase($database_definition); + if (PEAR::isError($result)) { + return $result; + } + + return $database_definition; + } + + // }}} + // {{{ createTableIndexes() + + /** + * A method to create indexes for an existing table + * + * @param string $table_name Name of the table + * @param array $indexes An array of indexes to be created + * @param boolean $overwrite If the table/index should be overwritten if it already exists + * + * @return mixed MDB2_Error if there is an error creating an index, MDB2_OK otherwise + * @access public + */ + function createTableIndexes($table_name, $indexes, $overwrite = false) + { + if (!$this->db->supports('indexes')) { + $this->db->debug('Indexes are not supported', __FUNCTION__); + return MDB2_OK; + } + + $errorcodes = array(MDB2_ERROR_UNSUPPORTED, MDB2_ERROR_NOT_CAPABLE); + foreach ($indexes as $index_name => $index) { + + // Does the index already exist, and if so, should it be overwritten? + $create_index = true; + $this->db->expectError($errorcodes); + if (!empty($index['primary']) || !empty($index['unique'])) { + $current_indexes = $this->db->manager->listTableConstraints($table_name); + } else { + $current_indexes = $this->db->manager->listTableIndexes($table_name); + } + + $this->db->popExpect(); + if (PEAR::isError($current_indexes)) { + if (!MDB2::isError($current_indexes, $errorcodes)) { + return $current_indexes; + } + } elseif (is_array($current_indexes) && in_array($index_name, $current_indexes)) { + if (!$overwrite) { + $this->db->debug('Index already exists: '.$index_name, __FUNCTION__); + $create_index = false; + } else { + $this->db->debug('Preparing to overwrite index: '.$index_name, __FUNCTION__); + + $this->db->expectError(MDB2_ERROR_NOT_FOUND); + if (!empty($index['primary']) || !empty($index['unique'])) { + $result = $this->db->manager->dropConstraint($table_name, $index_name); + } else { + $result = $this->db->manager->dropIndex($table_name, $index_name); + } + $this->db->popExpect(); + if (PEAR::isError($result) && !MDB2::isError($result, MDB2_ERROR_NOT_FOUND)) { + return $result; + } + } + } + + // Check if primary is being used and if it's supported + if (!empty($index['primary']) && !$this->db->supports('primary_key')) { + + // Primary not supported so we fallback to UNIQUE and making the field NOT NULL + $index['unique'] = true; + + $changes = array(); + + foreach ($index['fields'] as $field => $empty) { + $field_info = $this->db->reverse->getTableFieldDefinition($table_name, $field); + if (PEAR::isError($field_info)) { + return $field_info; + } + if (!$field_info[0]['notnull']) { + $changes['change'][$field] = $field_info[0]; + + $changes['change'][$field]['notnull'] = true; + } + } + if (!empty($changes)) { + $this->db->manager->alterTable($table_name, $changes, false); + } + } + + // Should the index be created? + if ($create_index) { + if (!empty($index['primary']) || !empty($index['unique'])) { + $result = $this->db->manager->createConstraint($table_name, $index_name, $index); + } else { + $result = $this->db->manager->createIndex($table_name, $index_name, $index); + } + if (PEAR::isError($result)) { + return $result; + } + } + } + return MDB2_OK; + } + + // }}} + // {{{ createTableConstraints() + + /** + * A method to create foreign keys for an existing table + * + * @param string $table_name Name of the table + * @param array $constraints An array of foreign keys to be created + * @param boolean $overwrite If the foreign key should be overwritten if it already exists + * + * @return mixed MDB2_Error if there is an error creating a foreign key, MDB2_OK otherwise + * @access public + */ + function createTableConstraints($table_name, $constraints, $overwrite = false) + { + if (!$this->db->supports('indexes')) { + $this->db->debug('Indexes are not supported', __FUNCTION__); + return MDB2_OK; + } + + $errorcodes = array(MDB2_ERROR_UNSUPPORTED, MDB2_ERROR_NOT_CAPABLE); + foreach ($constraints as $constraint_name => $constraint) { + + // Does the foreign key already exist, and if so, should it be overwritten? + $create_constraint = true; + $this->db->expectError($errorcodes); + $current_constraints = $this->db->manager->listTableConstraints($table_name); + $this->db->popExpect(); + if (PEAR::isError($current_constraints)) { + if (!MDB2::isError($current_constraints, $errorcodes)) { + return $current_constraints; + } + } elseif (is_array($current_constraints) && in_array($constraint_name, $current_constraints)) { + if (!$overwrite) { + $this->db->debug('Foreign key already exists: '.$constraint_name, __FUNCTION__); + $create_constraint = false; + } else { + $this->db->debug('Preparing to overwrite foreign key: '.$constraint_name, __FUNCTION__); + $result = $this->db->manager->dropConstraint($table_name, $constraint_name); + if (PEAR::isError($result)) { + return $result; + } + } + } + + // Should the foreign key be created? + if ($create_constraint) { + $result = $this->db->manager->createConstraint($table_name, $constraint_name, $constraint); + if (PEAR::isError($result)) { + return $result; + } + } + } + return MDB2_OK; + } + + // }}} + // {{{ createTable() + + /** + * Create a table and inititialize the table if data is available + * + * @param string $table_name name of the table to be created + * @param array $table multi dimensional array that contains the + * structure and optional data of the table + * @param bool $overwrite if the table/index should be overwritten if it already exists + * @param array $options an array of options to be passed to the database specific driver + * version of MDB2_Driver_Manager_Common::createTable(). + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function createTable($table_name, $table, $overwrite = false, $options = array()) + { + $create = true; + + $errorcodes = array(MDB2_ERROR_UNSUPPORTED, MDB2_ERROR_NOT_CAPABLE); + + $this->db->expectError($errorcodes); + + $tables = $this->db->manager->listTables(); + + $this->db->popExpect(); + if (PEAR::isError($tables)) { + if (!MDB2::isError($tables, $errorcodes)) { + return $tables; + } + } elseif (is_array($tables) && in_array($table_name, $tables)) { + if (!$overwrite) { + $create = false; + $this->db->debug('Table already exists: '.$table_name, __FUNCTION__); + } else { + $result = $this->db->manager->dropTable($table_name); + if (PEAR::isError($result)) { + return $result; + } + $this->db->debug('Overwritting table: '.$table_name, __FUNCTION__); + } + } + + if ($create) { + $result = $this->db->manager->createTable($table_name, $table['fields'], $options); + if (PEAR::isError($result)) { + return $result; + } + } + + if (!empty($table['initialization']) && is_array($table['initialization'])) { + $result = $this->initializeTable($table_name, $table); + if (PEAR::isError($result)) { + return $result; + } + } + + if (!empty($table['indexes']) && is_array($table['indexes'])) { + $result = $this->createTableIndexes($table_name, $table['indexes'], $overwrite); + if (PEAR::isError($result)) { + return $result; + } + } + + if (!empty($table['constraints']) && is_array($table['constraints'])) { + $result = $this->createTableConstraints($table_name, $table['constraints'], $overwrite); + if (PEAR::isError($result)) { + return $result; + } + } + + return MDB2_OK; + } + + // }}} + // {{{ initializeTable() + + /** + * Inititialize the table with data + * + * @param string $table_name name of the table + * @param array $table multi dimensional array that contains the + * structure and optional data of the table + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function initializeTable($table_name, $table) + { + $query_insertselect = 'INSERT INTO %s (%s) (SELECT %s FROM %s %s)'; + + $query_insert = 'INSERT INTO %s (%s) VALUES (%s)'; + $query_update = 'UPDATE %s SET %s %s'; + $query_delete = 'DELETE FROM %s %s'; + + $table_name = $this->db->quoteIdentifier($table_name, true); + + $result = MDB2_OK; + + $support_transactions = $this->db->supports('transactions'); + + foreach ($table['initialization'] as $instruction) { + $query = ''; + switch ($instruction['type']) { + case 'insert': + if (!isset($instruction['data']['select'])) { + $data = $this->getInstructionFields($instruction['data'], $table['fields']); + if (!empty($data)) { + $fields = implode(', ', array_keys($data)); + $values = implode(', ', array_values($data)); + + $query = sprintf($query_insert, $table_name, $fields, $values); + } + } else { + $data = $this->getInstructionFields($instruction['data']['select'], $table['fields']); + $where = $this->getInstructionWhere($instruction['data']['select'], $table['fields']); + + $select_table_name = $this->db->quoteIdentifier($instruction['data']['select']['table'], true); + if (!empty($data)) { + $fields = implode(', ', array_keys($data)); + $values = implode(', ', array_values($data)); + + $query = sprintf($query_insertselect, $table_name, $fields, $values, $select_table_name, $where); + } + } + break; + case 'update': + $data = $this->getInstructionFields($instruction['data'], $table['fields']); + $where = $this->getInstructionWhere($instruction['data'], $table['fields']); + if (!empty($data)) { + array_walk($data, array($this, 'buildFieldValue')); + $fields_values = implode(', ', $data); + + $query = sprintf($query_update, $table_name, $fields_values, $where); + } + break; + case 'delete': + $where = $this->getInstructionWhere($instruction['data'], $table['fields']); + $query = sprintf($query_delete, $table_name, $where); + break; + } + if ($query) { + if ($support_transactions && PEAR::isError($res = $this->db->beginNestedTransaction())) { + return $res; + } + + $result = $this->db->exec($query); + if (PEAR::isError($result)) { + return $result; + } + + if ($support_transactions && PEAR::isError($res = $this->db->completeNestedTransaction())) { + return $res; + } + } + } + return $result; + } + + // }}} + // {{{ buildFieldValue() + + /** + * Appends the contents of second argument + '=' to the beginning of first + * argument. + * + * Used with array_walk() in initializeTable() for UPDATEs. + * + * @param string &$element value of array's element + * @param string $key key of array's element + * + * @return void + * + * @access public + * @see MDB2_Schema::initializeTable() + */ + function buildFieldValue(&$element, $key) + { + $element = $key."=$element"; + } + + // }}} + // {{{ getExpression() + + /** + * Generates a string that represents a value that would be associated + * with a column in a DML instruction. + * + * @param array $element multi dimensional array that contains the + * structure of the current DML instruction. + * @param array $fields_definition multi dimensional array that contains the + * definition for current table's fields + * @param string $type type of given field + * + * @return string + * + * @access public + * @see MDB2_Schema::getInstructionFields(), MDB2_Schema::getInstructionWhere() + */ + function getExpression($element, $fields_definition = array(), $type = null) + { + $str = ''; + switch ($element['type']) { + case 'null': + $str .= 'NULL'; + break; + case 'value': + $str .= $this->db->quote($element['data'], $type); + break; + case 'column': + $str .= $this->db->quoteIdentifier($element['data'], true); + break; + case 'function': + $arguments = array(); + if (!empty($element['data']['arguments']) + && is_array($element['data']['arguments']) + ) { + foreach ($element['data']['arguments'] as $v) { + $arguments[] = $this->getExpression($v, $fields_definition); + } + } + if (method_exists($this->db->function, $element['data']['name'])) { + $user_func = array(&$this->db->function, $element['data']['name']); + + $str .= call_user_func_array($user_func, $arguments); + } else { + $str .= $element['data']['name'].'('; + $str .= implode(', ', $arguments); + $str .= ')'; + } + break; + case 'expression': + $type0 = $type1 = null; + if ($element['data']['operants'][0]['type'] == 'column' + && array_key_exists($element['data']['operants'][0]['data'], $fields_definition) + ) { + $type0 = $fields_definition[$element['data']['operants'][0]['data']]['type']; + } + + if ($element['data']['operants'][1]['type'] == 'column' + && array_key_exists($element['data']['operants'][1]['data'], $fields_definition) + ) { + $type1 = $fields_definition[$element['data']['operants'][1]['data']]['type']; + } + + $str .= '('; + $str .= $this->getExpression($element['data']['operants'][0], $fields_definition, $type1); + $str .= $this->getOperator($element['data']['operator']); + $str .= $this->getExpression($element['data']['operants'][1], $fields_definition, $type0); + $str .= ')'; + break; + } + + return $str; + } + + // }}} + // {{{ getOperator() + + /** + * Returns the matching SQL operator + * + * @param string $op parsed descriptive operator + * + * @return string matching SQL operator + * + * @access public + * @static + * @see MDB2_Schema::getExpression() + */ + function getOperator($op) + { + switch ($op) { + case 'PLUS': + return ' + '; + case 'MINUS': + return ' - '; + case 'TIMES': + return ' * '; + case 'DIVIDED': + return ' / '; + case 'EQUAL': + return ' = '; + case 'NOT EQUAL': + return ' != '; + case 'LESS THAN': + return ' < '; + case 'GREATER THAN': + return ' > '; + case 'LESS THAN OR EQUAL': + return ' <= '; + case 'GREATER THAN OR EQUAL': + return ' >= '; + default: + return ' '.$op.' '; + } + } + + // }}} + // {{{ getInstructionFields() + + /** + * Walks the parsed DML instruction array, field by field, + * storing them and their processed values inside a new array. + * + * @param array $instruction multi dimensional array that contains the + * structure of the current DML instruction. + * @param array $fields_definition multi dimensional array that contains the + * definition for current table's fields + * + * @return array array of strings in the form 'field_name' => 'value' + * + * @access public + * @static + * @see MDB2_Schema::initializeTable() + */ + function getInstructionFields($instruction, $fields_definition = array()) + { + $fields = array(); + if (!empty($instruction['field']) && is_array($instruction['field'])) { + foreach ($instruction['field'] as $field) { + $field_name = $this->db->quoteIdentifier($field['name'], true); + + $fields[$field_name] = $this->getExpression($field['group'], $fields_definition); + } + } + return $fields; + } + + // }}} + // {{{ getInstructionWhere() + + /** + * Translates the parsed WHERE expression of a DML instruction + * (array structure) to a SQL WHERE clause (string). + * + * @param array $instruction multi dimensional array that contains the + * structure of the current DML instruction. + * @param array $fields_definition multi dimensional array that contains the + * definition for current table's fields. + * + * @return string SQL WHERE clause + * + * @access public + * @static + * @see MDB2_Schema::initializeTable() + */ + function getInstructionWhere($instruction, $fields_definition = array()) + { + $where = ''; + if (!empty($instruction['where'])) { + $where = 'WHERE '.$this->getExpression($instruction['where'], $fields_definition); + } + return $where; + } + + // }}} + // {{{ createSequence() + + /** + * Create a sequence + * + * @param string $sequence_name name of the sequence to be created + * @param array $sequence multi dimensional array that contains the + * structure and optional data of the table + * @param bool $overwrite if the sequence should be overwritten if it already exists + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function createSequence($sequence_name, $sequence, $overwrite = false) + { + if (!$this->db->supports('sequences')) { + $this->db->debug('Sequences are not supported', __FUNCTION__); + return MDB2_OK; + } + + $errorcodes = array(MDB2_ERROR_UNSUPPORTED, MDB2_ERROR_NOT_CAPABLE); + $this->db->expectError($errorcodes); + $sequences = $this->db->manager->listSequences(); + $this->db->popExpect(); + if (PEAR::isError($sequences)) { + if (!MDB2::isError($sequences, $errorcodes)) { + return $sequences; + } + } elseif (is_array($sequence) && in_array($sequence_name, $sequences)) { + if (!$overwrite) { + $this->db->debug('Sequence already exists: '.$sequence_name, __FUNCTION__); + return MDB2_OK; + } + + $result = $this->db->manager->dropSequence($sequence_name); + if (PEAR::isError($result)) { + return $result; + } + $this->db->debug('Overwritting sequence: '.$sequence_name, __FUNCTION__); + } + + $start = 1; + $field = ''; + if (!empty($sequence['on'])) { + $table = $sequence['on']['table']; + $field = $sequence['on']['field']; + + $errorcodes = array(MDB2_ERROR_UNSUPPORTED, MDB2_ERROR_NOT_CAPABLE); + $this->db->expectError($errorcodes); + $tables = $this->db->manager->listTables(); + $this->db->popExpect(); + if (PEAR::isError($tables) && !MDB2::isError($tables, $errorcodes)) { + return $tables; + } + + if (!PEAR::isError($tables) && is_array($tables) && in_array($table, $tables)) { + if ($this->db->supports('summary_functions')) { + $query = "SELECT MAX($field) FROM ".$this->db->quoteIdentifier($table, true); + } else { + $query = "SELECT $field FROM ".$this->db->quoteIdentifier($table, true)." ORDER BY $field DESC"; + } + $start = $this->db->queryOne($query, 'integer'); + if (PEAR::isError($start)) { + return $start; + } + ++$start; + } else { + $this->warnings[] = 'Could not sync sequence: '.$sequence_name; + } + } elseif (!empty($sequence['start']) && is_numeric($sequence['start'])) { + $start = $sequence['start']; + $table = ''; + } + + $result = $this->db->manager->createSequence($sequence_name, $start); + if (PEAR::isError($result)) { + return $result; + } + + return MDB2_OK; + } + + // }}} + // {{{ createDatabase() + + /** + * Create a database space within which may be created database objects + * like tables, indexes and sequences. The implementation of this function + * is highly DBMS specific and may require special permissions to run + * successfully. Consult the documentation or the DBMS drivers that you + * use to be aware of eventual configuration requirements. + * + * @param array $database_definition multi dimensional array that contains the current definition + * @param array $options an array of options to be passed to the + * database specific driver version of + * MDB2_Driver_Manager_Common::createTable(). + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function createDatabase($database_definition, $options = array()) + { + if (!isset($database_definition['name']) || !$database_definition['name']) { + return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null, + 'no valid database name specified'); + } + + $create = (isset($database_definition['create']) && $database_definition['create']); + $overwrite = (isset($database_definition['overwrite']) && $database_definition['overwrite']); + + /** + * + * We need to clean up database name before any query to prevent + * database driver from using a inexistent database + * + */ + $previous_database_name = $this->db->setDatabase(''); + + // Lower / Upper case the db name if the portability deems so. + if ($this->db->options['portability'] & MDB2_PORTABILITY_FIX_CASE) { + $func = $this->db->options['field_case'] == CASE_LOWER ? 'strtolower' : 'strtoupper'; + + $db_name = $func($database_definition['name']); + } else { + $db_name = $database_definition['name']; + } + + if ($create) { + + $dbExists = $this->db->databaseExists($db_name); + if (PEAR::isError($dbExists)) { + return $dbExists; + } + + if ($dbExists && $overwrite) { + $this->db->expectError(MDB2_ERROR_CANNOT_DROP); + $result = $this->db->manager->dropDatabase($db_name); + $this->db->popExpect(); + if (PEAR::isError($result) && !MDB2::isError($result, MDB2_ERROR_CANNOT_DROP)) { + return $result; + } + $dbExists = false; + $this->db->debug('Overwritting database: ' . $db_name, __FUNCTION__); + } + + $dbOptions = array(); + if (array_key_exists('charset', $database_definition) + && !empty($database_definition['charset'])) { + $dbOptions['charset'] = $database_definition['charset']; + } + + if ($dbExists) { + $this->db->debug('Database already exists: ' . $db_name, __FUNCTION__); + if (!empty($dbOptions)) { + $errorcodes = array(MDB2_ERROR_UNSUPPORTED, MDB2_ERROR_NO_PERMISSION); + $this->db->expectError($errorcodes); + $result = $this->db->manager->alterDatabase($db_name, $dbOptions); + $this->db->popExpect(); + if (PEAR::isError($result) && !MDB2::isError($result, $errorcodes)) { + return $result; + } + } + $create = false; + } else { + $this->db->expectError(MDB2_ERROR_UNSUPPORTED); + $result = $this->db->manager->createDatabase($db_name, $dbOptions); + $this->db->popExpect(); + if (PEAR::isError($result) && !MDB2::isError($result, MDB2_ERROR_UNSUPPORTED)) { + return $result; + } + $this->db->debug('Creating database: ' . $db_name, __FUNCTION__); + } + } + + $this->db->setDatabase($db_name); + if (($support_transactions = $this->db->supports('transactions')) + && PEAR::isError($result = $this->db->beginNestedTransaction()) + ) { + return $result; + } + + $created_objects = 0; + if (isset($database_definition['tables']) + && is_array($database_definition['tables']) + ) { + foreach ($database_definition['tables'] as $table_name => $table) { + $result = $this->createTable($table_name, $table, $overwrite, $options); + if (PEAR::isError($result)) { + break; + } + $created_objects++; + } + } + if (!PEAR::isError($result) + && isset($database_definition['sequences']) + && is_array($database_definition['sequences']) + ) { + foreach ($database_definition['sequences'] as $sequence_name => $sequence) { + $result = $this->createSequence($sequence_name, $sequence, false, $overwrite); + + if (PEAR::isError($result)) { + break; + } + $created_objects++; + } + } + + if ($support_transactions) { + $res = $this->db->completeNestedTransaction(); + if (PEAR::isError($res)) { + $result = $this->raiseError(MDB2_SCHEMA_ERROR, null, null, + 'Could not end transaction ('. + $res->getMessage().' ('.$res->getUserinfo().'))'); + } + } elseif (PEAR::isError($result) && $created_objects) { + $result = $this->raiseError(MDB2_SCHEMA_ERROR, null, null, + 'the database was only partially created ('. + $result->getMessage().' ('.$result->getUserinfo().'))'); + } + + $this->db->setDatabase($previous_database_name); + + if (PEAR::isError($result) && $create + && PEAR::isError($result2 = $this->db->manager->dropDatabase($db_name)) + ) { + if (!MDB2::isError($result2, MDB2_ERROR_UNSUPPORTED)) { + return $this->raiseError(MDB2_SCHEMA_ERROR, null, null, + 'Could not drop the created database after unsuccessful creation attempt ('. + $result2->getMessage().' ('.$result2->getUserinfo().'))'); + } + } + + return $result; + } + + // }}} + // {{{ compareDefinitions() + + /** + * Compare a previous definition with the currently parsed definition + * + * @param array $current_definition multi dimensional array that contains the current definition + * @param array $previous_definition multi dimensional array that contains the previous definition + * + * @return array|MDB2_Error array of changes on success, or a error object + * @access public + */ + function compareDefinitions($current_definition, $previous_definition) + { + $changes = array(); + + if (!empty($current_definition['tables']) && is_array($current_definition['tables'])) { + $changes['tables'] = $defined_tables = array(); + foreach ($current_definition['tables'] as $table_name => $table) { + $previous_tables = array(); + if (!empty($previous_definition) && is_array($previous_definition)) { + $previous_tables = $previous_definition['tables']; + } + $change = $this->compareTableDefinitions($table_name, $table, $previous_tables, $defined_tables); + if (PEAR::isError($change)) { + return $change; + } + if (!empty($change)) { + $changes['tables'] = MDB2_Schema::arrayMergeClobber($changes['tables'], $change); + } + } + + if (!empty($previous_definition['tables']) + && is_array($previous_definition['tables'])) { + foreach ($previous_definition['tables'] as $table_name => $table) { + if (empty($defined_tables[$table_name])) { + $changes['tables']['remove'][$table_name] = true; + } + } + } + } + if (!empty($current_definition['sequences']) && is_array($current_definition['sequences'])) { + $changes['sequences'] = $defined_sequences = array(); + foreach ($current_definition['sequences'] as $sequence_name => $sequence) { + $previous_sequences = array(); + if (!empty($previous_definition) && is_array($previous_definition)) { + $previous_sequences = $previous_definition['sequences']; + } + + $change = $this->compareSequenceDefinitions($sequence_name, + $sequence, + $previous_sequences, + $defined_sequences); + if (PEAR::isError($change)) { + return $change; + } + if (!empty($change)) { + $changes['sequences'] = MDB2_Schema::arrayMergeClobber($changes['sequences'], $change); + } + } + if (!empty($previous_definition['sequences']) && is_array($previous_definition['sequences'])) { + foreach ($previous_definition['sequences'] as $sequence_name => $sequence) { + if (empty($defined_sequences[$sequence_name])) { + $changes['sequences']['remove'][$sequence_name] = true; + } + } + } + } + return $changes; + } + + // }}} + // {{{ compareTableFieldsDefinitions() + + /** + * Compare a previous definition with the currently parsed definition + * + * @param string $table_name name of the table + * @param array $current_definition multi dimensional array that contains the current definition + * @param array $previous_definition multi dimensional array that contains the previous definition + * + * @return array|MDB2_Error array of changes on success, or a error object + * @access public + */ + function compareTableFieldsDefinitions($table_name, $current_definition, + $previous_definition) + { + $changes = $defined_fields = array(); + + if (is_array($current_definition)) { + foreach ($current_definition as $field_name => $field) { + $was_field_name = $field['was']; + if (!empty($previous_definition[$field_name]) + && ( + (isset($previous_definition[$field_name]['was']) + && $previous_definition[$field_name]['was'] == $was_field_name) + || !isset($previous_definition[$was_field_name]) + )) { + $was_field_name = $field_name; + } + + if (!empty($previous_definition[$was_field_name])) { + if ($was_field_name != $field_name) { + $changes['rename'][$was_field_name] = array('name' => $field_name, 'definition' => $field); + } + + if (!empty($defined_fields[$was_field_name])) { + return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null, + 'the field "'.$was_field_name. + '" was specified for more than one field of table'); + } + + $defined_fields[$was_field_name] = true; + + $change = $this->db->compareDefinition($field, $previous_definition[$was_field_name]); + if (PEAR::isError($change)) { + return $change; + } + + if (!empty($change)) { + if (array_key_exists('default', $change) + && $change['default'] + && !array_key_exists('default', $field)) { + $field['default'] = null; + } + + $change['definition'] = $field; + + $changes['change'][$field_name] = $change; + } + } else { + if ($field_name != $was_field_name) { + return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null, + 'it was specified a previous field name ("'. + $was_field_name.'") for field "'.$field_name.'" of table "'. + $table_name.'" that does not exist'); + } + + $changes['add'][$field_name] = $field; + } + } + } + + if (isset($previous_definition) && is_array($previous_definition)) { + foreach ($previous_definition as $field_previous_name => $field_previous) { + if (empty($defined_fields[$field_previous_name])) { + $changes['remove'][$field_previous_name] = true; + } + } + } + + return $changes; + } + + // }}} + // {{{ compareTableIndexesDefinitions() + + /** + * Compare a previous definition with the currently parsed definition + * + * @param string $table_name name of the table + * @param array $current_definition multi dimensional array that contains the current definition + * @param array $previous_definition multi dimensional array that contains the previous definition + * + * @return array|MDB2_Error array of changes on success, or a error object + * @access public + */ + function compareTableIndexesDefinitions($table_name, $current_definition, + $previous_definition) + { + $changes = $defined_indexes = array(); + + if (is_array($current_definition)) { + foreach ($current_definition as $index_name => $index) { + $was_index_name = $index['was']; + if (!empty($previous_definition[$index_name]) + && isset($previous_definition[$index_name]['was']) + && $previous_definition[$index_name]['was'] == $was_index_name + ) { + $was_index_name = $index_name; + } + if (!empty($previous_definition[$was_index_name])) { + $change = array(); + if ($was_index_name != $index_name) { + $change['name'] = $was_index_name; + } + + if (!empty($defined_indexes[$was_index_name])) { + return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null, + 'the index "'.$was_index_name.'" was specified for'. + ' more than one index of table "'.$table_name.'"'); + } + $defined_indexes[$was_index_name] = true; + + $previous_unique = array_key_exists('unique', $previous_definition[$was_index_name]) + ? $previous_definition[$was_index_name]['unique'] : false; + + $unique = array_key_exists('unique', $index) ? $index['unique'] : false; + if ($previous_unique != $unique) { + $change['unique'] = $unique; + } + + $previous_primary = array_key_exists('primary', $previous_definition[$was_index_name]) + ? $previous_definition[$was_index_name]['primary'] : false; + + $primary = array_key_exists('primary', $index) ? $index['primary'] : false; + if ($previous_primary != $primary) { + $change['primary'] = $primary; + } + + $defined_fields = array(); + $previous_fields = $previous_definition[$was_index_name]['fields']; + if (!empty($index['fields']) && is_array($index['fields'])) { + foreach ($index['fields'] as $field_name => $field) { + if (!empty($previous_fields[$field_name])) { + $defined_fields[$field_name] = true; + + $previous_sorting = array_key_exists('sorting', $previous_fields[$field_name]) + ? $previous_fields[$field_name]['sorting'] : ''; + + $sorting = array_key_exists('sorting', $field) ? $field['sorting'] : ''; + if ($previous_sorting != $sorting) { + $change['change'] = true; + } + } else { + $change['change'] = true; + } + } + } + if (isset($previous_fields) && is_array($previous_fields)) { + foreach ($previous_fields as $field_name => $field) { + if (empty($defined_fields[$field_name])) { + $change['change'] = true; + } + } + } + if (!empty($change)) { + $changes['change'][$index_name] = $current_definition[$index_name]; + } + } else { + if ($index_name != $was_index_name) { + return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null, + 'it was specified a previous index name ("'.$was_index_name. + ') for index "'.$index_name.'" of table "'.$table_name.'" that does not exist'); + } + $changes['add'][$index_name] = $current_definition[$index_name]; + } + } + } + foreach ($previous_definition as $index_previous_name => $index_previous) { + if (empty($defined_indexes[$index_previous_name])) { + $changes['remove'][$index_previous_name] = $index_previous; + } + } + return $changes; + } + + // }}} + // {{{ compareTableDefinitions() + + /** + * Compare a previous definition with the currently parsed definition + * + * @param string $table_name name of the table + * @param array $current_definition multi dimensional array that contains the current definition + * @param array $previous_definition multi dimensional array that contains the previous definition + * @param array &$defined_tables table names in the schema + * + * @return array|MDB2_Error array of changes on success, or a error object + * @access public + */ + function compareTableDefinitions($table_name, $current_definition, + $previous_definition, &$defined_tables) + { + $changes = array(); + + if (is_array($current_definition)) { + $was_table_name = $table_name; + if (!empty($current_definition['was'])) { + $was_table_name = $current_definition['was']; + } + if (!empty($previous_definition[$was_table_name])) { + $changes['change'][$was_table_name] = array(); + if ($was_table_name != $table_name) { + $changes['change'][$was_table_name] = array('name' => $table_name); + } + if (!empty($defined_tables[$was_table_name])) { + return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null, + 'the table "'.$was_table_name. + '" was specified for more than one table of the database'); + } + $defined_tables[$was_table_name] = true; + if (!empty($current_definition['fields']) && is_array($current_definition['fields'])) { + $previous_fields = array(); + if (isset($previous_definition[$was_table_name]['fields']) + && is_array($previous_definition[$was_table_name]['fields'])) { + $previous_fields = $previous_definition[$was_table_name]['fields']; + } + + $change = $this->compareTableFieldsDefinitions($table_name, + $current_definition['fields'], + $previous_fields); + + if (PEAR::isError($change)) { + return $change; + } + if (!empty($change)) { + $changes['change'][$was_table_name] = + MDB2_Schema::arrayMergeClobber($changes['change'][$was_table_name], $change); + } + } + if (!empty($current_definition['indexes']) && is_array($current_definition['indexes'])) { + $previous_indexes = array(); + if (isset($previous_definition[$was_table_name]['indexes']) + && is_array($previous_definition[$was_table_name]['indexes'])) { + $previous_indexes = $previous_definition[$was_table_name]['indexes']; + } + $change = $this->compareTableIndexesDefinitions($table_name, + $current_definition['indexes'], + $previous_indexes); + + if (PEAR::isError($change)) { + return $change; + } + if (!empty($change)) { + $changes['change'][$was_table_name]['indexes'] = $change; + } + } + if (empty($changes['change'][$was_table_name])) { + unset($changes['change'][$was_table_name]); + } + if (empty($changes['change'])) { + unset($changes['change']); + } + } else { + if ($table_name != $was_table_name) { + return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null, + 'it was specified a previous table name ("'.$was_table_name. + '") for table "'.$table_name.'" that does not exist'); + } + $changes['add'][$table_name] = true; + } + } + + return $changes; + } + + // }}} + // {{{ compareSequenceDefinitions() + + /** + * Compare a previous definition with the currently parsed definition + * + * @param string $sequence_name name of the sequence + * @param array $current_definition multi dimensional array that contains the current definition + * @param array $previous_definition multi dimensional array that contains the previous definition + * @param array &$defined_sequences names in the schema + * + * @return array|MDB2_Error array of changes on success, or a error object + * @access public + */ + function compareSequenceDefinitions($sequence_name, $current_definition, + $previous_definition, &$defined_sequences) + { + $changes = array(); + + if (is_array($current_definition)) { + $was_sequence_name = $sequence_name; + if (!empty($previous_definition[$sequence_name]) + && isset($previous_definition[$sequence_name]['was']) + && $previous_definition[$sequence_name]['was'] == $was_sequence_name + ) { + $was_sequence_name = $sequence_name; + } elseif (!empty($current_definition['was'])) { + $was_sequence_name = $current_definition['was']; + } + if (!empty($previous_definition[$was_sequence_name])) { + if ($was_sequence_name != $sequence_name) { + $changes['change'][$was_sequence_name]['name'] = $sequence_name; + } + + if (!empty($defined_sequences[$was_sequence_name])) { + return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null, + 'the sequence "'.$was_sequence_name.'" was specified as base'. + ' of more than of sequence of the database'); + } + + $defined_sequences[$was_sequence_name] = true; + + $change = array(); + if (!empty($current_definition['start']) + && isset($previous_definition[$was_sequence_name]['start']) + && $current_definition['start'] != $previous_definition[$was_sequence_name]['start'] + ) { + $change['start'] = $previous_definition[$sequence_name]['start']; + } + if (isset($current_definition['on']['table']) + && isset($previous_definition[$was_sequence_name]['on']['table']) + && $current_definition['on']['table'] != $previous_definition[$was_sequence_name]['on']['table'] + && isset($current_definition['on']['field']) + && isset($previous_definition[$was_sequence_name]['on']['field']) + && $current_definition['on']['field'] != $previous_definition[$was_sequence_name]['on']['field'] + ) { + $change['on'] = $current_definition['on']; + } + if (!empty($change)) { + $changes['change'][$was_sequence_name][$sequence_name] = $change; + } + } else { + if ($sequence_name != $was_sequence_name) { + return $this->raiseError(MDB2_SCHEMA_ERROR_INVALID, null, null, + 'it was specified a previous sequence name ("'.$was_sequence_name. + '") for sequence "'.$sequence_name.'" that does not exist'); + } + $changes['add'][$sequence_name] = true; + } + } + return $changes; + } + // }}} + // {{{ verifyAlterDatabase() + + /** + * Verify that the changes requested are supported + * + * @param array $changes associative array that contains the definition of the changes + * that are meant to be applied to the database structure. + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function verifyAlterDatabase($changes) + { + if (!empty($changes['tables']['change']) && is_array($changes['tables']['change'])) { + foreach ($changes['tables']['change'] as $table_name => $table) { + if (!empty($table['indexes']) && is_array($table['indexes'])) { + if (!$this->db->supports('indexes')) { + return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED, null, null, + 'indexes are not supported'); + } + $table_changes = count($table['indexes']); + if (!empty($table['indexes']['add'])) { + $table_changes--; + } + if (!empty($table['indexes']['remove'])) { + $table_changes--; + } + if (!empty($table['indexes']['change'])) { + $table_changes--; + } + if ($table_changes) { + return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED, null, null, + 'index alteration not yet supported: '.implode(', ', array_keys($table['indexes']))); + } + } + unset($table['indexes']); + $result = $this->db->manager->alterTable($table_name, $table, true); + if (PEAR::isError($result)) { + return $result; + } + } + } + if (!empty($changes['sequences']) && is_array($changes['sequences'])) { + if (!$this->db->supports('sequences')) { + return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED, null, null, + 'sequences are not supported'); + } + $sequence_changes = count($changes['sequences']); + if (!empty($changes['sequences']['add'])) { + $sequence_changes--; + } + if (!empty($changes['sequences']['remove'])) { + $sequence_changes--; + } + if (!empty($changes['sequences']['change'])) { + $sequence_changes--; + } + if ($sequence_changes) { + return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED, null, null, + 'sequence alteration not yet supported: '.implode(', ', array_keys($changes['sequences']))); + } + } + return MDB2_OK; + } + + // }}} + // {{{ alterDatabaseIndexes() + + /** + * Execute the necessary actions to implement the requested changes + * in the indexes inside a database structure. + * + * @param string $table_name name of the table + * @param array $changes associative array that contains the definition of the changes + * that are meant to be applied to the database structure. + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function alterDatabaseIndexes($table_name, $changes) + { + $alterations = 0; + if (empty($changes)) { + return $alterations; + } + + if (!empty($changes['remove']) && is_array($changes['remove'])) { + foreach ($changes['remove'] as $index_name => $index) { + $this->db->expectError(MDB2_ERROR_NOT_FOUND); + if (!empty($index['primary']) || !empty($index['unique'])) { + $result = $this->db->manager->dropConstraint($table_name, $index_name, !empty($index['primary'])); + } else { + $result = $this->db->manager->dropIndex($table_name, $index_name); + } + $this->db->popExpect(); + if (PEAR::isError($result) && !MDB2::isError($result, MDB2_ERROR_NOT_FOUND)) { + return $result; + } + $alterations++; + } + } + if (!empty($changes['change']) && is_array($changes['change'])) { + foreach ($changes['change'] as $index_name => $index) { + /** + * Drop existing index/constraint first. + * Since $changes doesn't tell us whether it's an index or a constraint before the change, + * we have to find out and call the appropriate method. + */ + if (in_array($index_name, $this->db->manager->listTableIndexes($table_name))) { + $result = $this->db->manager->dropIndex($table_name, $index_name); + } elseif (in_array($index_name, $this->db->manager->listTableConstraints($table_name))) { + $result = $this->db->manager->dropConstraint($table_name, $index_name); + } + if (!empty($result) && PEAR::isError($result)) { + return $result; + } + + if (!empty($index['primary']) || !empty($index['unique'])) { + $result = $this->db->manager->createConstraint($table_name, $index_name, $index); + } else { + $result = $this->db->manager->createIndex($table_name, $index_name, $index); + } + if (PEAR::isError($result)) { + return $result; + } + $alterations++; + } + } + if (!empty($changes['add']) && is_array($changes['add'])) { + foreach ($changes['add'] as $index_name => $index) { + if (!empty($index['primary']) || !empty($index['unique'])) { + $result = $this->db->manager->createConstraint($table_name, $index_name, $index); + } else { + $result = $this->db->manager->createIndex($table_name, $index_name, $index); + } + if (PEAR::isError($result)) { + return $result; + } + $alterations++; + } + } + + return $alterations; + } + + // }}} + // {{{ alterDatabaseTables() + + /** + * Execute the necessary actions to implement the requested changes + * in the tables inside a database structure. + * + * @param array $current_definition multi dimensional array that contains the current definition + * @param array $previous_definition multi dimensional array that contains the previous definition + * @param array $changes associative array that contains the definition of the changes + * that are meant to be applied to the database structure. + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function alterDatabaseTables($current_definition, $previous_definition, $changes) + { + /* FIXME: tables marked to be added are initialized by createTable(), others don't */ + $alterations = 0; + if (empty($changes)) { + return $alterations; + } + + if (!empty($changes['add']) && is_array($changes['add'])) { + foreach ($changes['add'] as $table_name => $table) { + $result = $this->createTable($table_name, $current_definition[$table_name]); + if (PEAR::isError($result)) { + return $result; + } + $alterations++; + } + } + + if ($this->options['drop_missing_tables'] + && !empty($changes['remove']) + && is_array($changes['remove'])) { + foreach ($changes['remove'] as $table_name => $table) { + $result = $this->db->manager->dropTable($table_name); + if (PEAR::isError($result)) { + return $result; + } + $alterations++; + } + } + + if (!empty($changes['change']) && is_array($changes['change'])) { + foreach ($changes['change'] as $table_name => $table) { + $indexes = array(); + if (!empty($table['indexes'])) { + $indexes = $table['indexes']; + unset($table['indexes']); + } + if (!empty($indexes['remove'])) { + $result = $this->alterDatabaseIndexes($table_name, array('remove' => $indexes['remove'])); + if (PEAR::isError($result)) { + return $result; + } + unset($indexes['remove']); + $alterations += $result; + } + $result = $this->db->manager->alterTable($table_name, $table, false); + if (PEAR::isError($result)) { + return $result; + } + $alterations++; + + // table may be renamed at this point + if (!empty($table['name'])) { + $table_name = $table['name']; + } + + if (!empty($indexes)) { + $result = $this->alterDatabaseIndexes($table_name, $indexes); + if (PEAR::isError($result)) { + return $result; + } + $alterations += $result; + } + } + } + + return $alterations; + } + + // }}} + // {{{ alterDatabaseSequences() + + /** + * Execute the necessary actions to implement the requested changes + * in the sequences inside a database structure. + * + * @param array $current_definition multi dimensional array that contains the current definition + * @param array $previous_definition multi dimensional array that contains the previous definition + * @param array $changes associative array that contains the definition of the changes + * that are meant to be applied to the database structure. + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function alterDatabaseSequences($current_definition, $previous_definition, $changes) + { + $alterations = 0; + if (empty($changes)) { + return $alterations; + } + + if (!empty($changes['add']) && is_array($changes['add'])) { + foreach ($changes['add'] as $sequence_name => $sequence) { + $result = $this->createSequence($sequence_name, $current_definition[$sequence_name]); + if (PEAR::isError($result)) { + return $result; + } + $alterations++; + } + } + + if (!empty($changes['remove']) && is_array($changes['remove'])) { + foreach ($changes['remove'] as $sequence_name => $sequence) { + $result = $this->db->manager->dropSequence($sequence_name); + if (PEAR::isError($result)) { + return $result; + } + $alterations++; + } + } + + if (!empty($changes['change']) && is_array($changes['change'])) { + foreach ($changes['change'] as $sequence_name => $sequence) { + $result = $this->db->manager->dropSequence($previous_definition[$sequence_name]['was']); + if (PEAR::isError($result)) { + return $result; + } + $result = $this->createSequence($sequence_name, $sequence); + if (PEAR::isError($result)) { + return $result; + } + $alterations++; + } + } + + return $alterations; + } + + // }}} + // {{{ alterDatabase() + + /** + * Execute the necessary actions to implement the requested changes + * in a database structure. + * + * @param array $current_definition multi dimensional array that contains the current definition + * @param array $previous_definition multi dimensional array that contains the previous definition + * @param array $changes associative array that contains the definition of the changes + * that are meant to be applied to the database structure. + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function alterDatabase($current_definition, $previous_definition, $changes) + { + $alterations = 0; + if (empty($changes)) { + return $alterations; + } + + $result = $this->verifyAlterDatabase($changes); + if (PEAR::isError($result)) { + return $result; + } + + if (!empty($current_definition['name'])) { + $previous_database_name = $this->db->setDatabase($current_definition['name']); + } + + if (($support_transactions = $this->db->supports('transactions')) + && PEAR::isError($result = $this->db->beginNestedTransaction()) + ) { + return $result; + } + + if (!empty($changes['tables']) && !empty($current_definition['tables'])) { + $current_tables = isset($current_definition['tables']) ? $current_definition['tables'] : array(); + $previous_tables = isset($previous_definition['tables']) ? $previous_definition['tables'] : array(); + + $result = $this->alterDatabaseTables($current_tables, $previous_tables, $changes['tables']); + if (is_numeric($result)) { + $alterations += $result; + } + } + + if (!PEAR::isError($result) && !empty($changes['sequences'])) { + $current_sequences = isset($current_definition['sequences']) ? $current_definition['sequences'] : array(); + $previous_sequences = isset($previous_definition['sequences']) ? $previous_definition['sequences'] : array(); + + $result = $this->alterDatabaseSequences($current_sequences, $previous_sequences, $changes['sequences']); + if (is_numeric($result)) { + $alterations += $result; + } + } + + if ($support_transactions) { + $res = $this->db->completeNestedTransaction(); + if (PEAR::isError($res)) { + $result = $this->raiseError(MDB2_SCHEMA_ERROR, null, null, + 'Could not end transaction ('. + $res->getMessage().' ('.$res->getUserinfo().'))'); + } + } elseif (PEAR::isError($result) && $alterations) { + $result = $this->raiseError(MDB2_SCHEMA_ERROR, null, null, + 'the requested database alterations were only partially implemented ('. + $result->getMessage().' ('.$result->getUserinfo().'))'); + } + + if (isset($previous_database_name)) { + $this->db->setDatabase($previous_database_name); + } + return $result; + } + + // }}} + // {{{ dumpDatabaseChanges() + + /** + * Dump the changes between two database definitions. + * + * @param array $changes associative array that specifies the list of database + * definitions changes as returned by the _compareDefinitions + * manager class function. + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function dumpDatabaseChanges($changes) + { + if (!empty($changes['tables'])) { + if (!empty($changes['tables']['add']) && is_array($changes['tables']['add'])) { + foreach ($changes['tables']['add'] as $table_name => $table) { + $this->db->debug("$table_name:", __FUNCTION__); + $this->db->debug("\tAdded table '$table_name'", __FUNCTION__); + } + } + + if (!empty($changes['tables']['remove']) && is_array($changes['tables']['remove'])) { + if ($this->options['drop_missing_tables']) { + foreach ($changes['tables']['remove'] as $table_name => $table) { + $this->db->debug("$table_name:", __FUNCTION__); + $this->db->debug("\tRemoved table '$table_name'", __FUNCTION__); + } + } else { + foreach ($changes['tables']['remove'] as $table_name => $table) { + $this->db->debug("\tObsolete table '$table_name' left as is", __FUNCTION__); + } + } + } + + if (!empty($changes['tables']['change']) && is_array($changes['tables']['change'])) { + foreach ($changes['tables']['change'] as $table_name => $table) { + if (array_key_exists('name', $table)) { + $this->db->debug("\tRenamed table '$table_name' to '".$table['name']."'", __FUNCTION__); + } + if (!empty($table['add']) && is_array($table['add'])) { + foreach ($table['add'] as $field_name => $field) { + $this->db->debug("\tAdded field '".$field_name."'", __FUNCTION__); + } + } + if (!empty($table['remove']) && is_array($table['remove'])) { + foreach ($table['remove'] as $field_name => $field) { + $this->db->debug("\tRemoved field '".$field_name."'", __FUNCTION__); + } + } + if (!empty($table['rename']) && is_array($table['rename'])) { + foreach ($table['rename'] as $field_name => $field) { + $this->db->debug("\tRenamed field '".$field_name."' to '".$field['name']."'", __FUNCTION__); + } + } + if (!empty($table['change']) && is_array($table['change'])) { + foreach ($table['change'] as $field_name => $field) { + $field = $field['definition']; + if (array_key_exists('type', $field)) { + $this->db->debug("\tChanged field '$field_name' type to '".$field['type']."'", __FUNCTION__); + } + + if (array_key_exists('unsigned', $field)) { + $this->db->debug("\tChanged field '$field_name' type to '". + (!empty($field['unsigned']) && $field['unsigned'] ? '' : 'not ')."unsigned'", + __FUNCTION__); + } + + if (array_key_exists('length', $field)) { + $this->db->debug("\tChanged field '$field_name' length to '". + (!empty($field['length']) ? $field['length']: 'no length')."'", __FUNCTION__); + } + if (array_key_exists('default', $field)) { + $this->db->debug("\tChanged field '$field_name' default to ". + (isset($field['default']) ? "'".$field['default']."'" : 'NULL'), __FUNCTION__); + } + + if (array_key_exists('notnull', $field)) { + $this->db->debug("\tChanged field '$field_name' notnull to ". + (!empty($field['notnull']) && $field['notnull'] ? 'true' : 'false'), + __FUNCTION__); + } + } + } + if (!empty($table['indexes']) && is_array($table['indexes'])) { + if (!empty($table['indexes']['add']) && is_array($table['indexes']['add'])) { + foreach ($table['indexes']['add'] as $index_name => $index) { + $this->db->debug("\tAdded index '".$index_name. + "' of table '$table_name'", __FUNCTION__); + } + } + if (!empty($table['indexes']['remove']) && is_array($table['indexes']['remove'])) { + foreach ($table['indexes']['remove'] as $index_name => $index) { + $this->db->debug("\tRemoved index '".$index_name. + "' of table '$table_name'", __FUNCTION__); + } + } + if (!empty($table['indexes']['change']) && is_array($table['indexes']['change'])) { + foreach ($table['indexes']['change'] as $index_name => $index) { + if (array_key_exists('name', $index)) { + $this->db->debug("\tRenamed index '".$index_name."' to '".$index['name']. + "' on table '$table_name'", __FUNCTION__); + } + if (array_key_exists('unique', $index)) { + $this->db->debug("\tChanged index '".$index_name."' unique to '". + !empty($index['unique'])."' on table '$table_name'", __FUNCTION__); + } + if (array_key_exists('primary', $index)) { + $this->db->debug("\tChanged index '".$index_name."' primary to '". + !empty($index['primary'])."' on table '$table_name'", __FUNCTION__); + } + if (array_key_exists('change', $index)) { + $this->db->debug("\tChanged index '".$index_name. + "' on table '$table_name'", __FUNCTION__); + } + } + } + } + } + } + } + if (!empty($changes['sequences'])) { + if (!empty($changes['sequences']['add']) && is_array($changes['sequences']['add'])) { + foreach ($changes['sequences']['add'] as $sequence_name => $sequence) { + $this->db->debug("$sequence_name:", __FUNCTION__); + $this->db->debug("\tAdded sequence '$sequence_name'", __FUNCTION__); + } + } + if (!empty($changes['sequences']['remove']) && is_array($changes['sequences']['remove'])) { + foreach ($changes['sequences']['remove'] as $sequence_name => $sequence) { + $this->db->debug("$sequence_name:", __FUNCTION__); + $this->db->debug("\tAdded sequence '$sequence_name'", __FUNCTION__); + } + } + if (!empty($changes['sequences']['change']) && is_array($changes['sequences']['change'])) { + foreach ($changes['sequences']['change'] as $sequence_name => $sequence) { + if (array_key_exists('name', $sequence)) { + $this->db->debug("\tRenamed sequence '$sequence_name' to '". + $sequence['name']."'", __FUNCTION__); + } + if (!empty($sequence['change']) && is_array($sequence['change'])) { + foreach ($sequence['change'] as $sequence_name => $sequence) { + if (array_key_exists('start', $sequence)) { + $this->db->debug("\tChanged sequence '$sequence_name' start to '". + $sequence['start']."'", __FUNCTION__); + } + } + } + } + } + } + return MDB2_OK; + } + + // }}} + // {{{ dumpDatabase() + + /** + * Dump a previously parsed database structure in the Metabase schema + * XML based format suitable for the Metabase parser. This function + * may optionally dump the database definition with initialization + * commands that specify the data that is currently present in the tables. + * + * @param array $database_definition multi dimensional array that contains the current definition + * @param array $arguments associative array that takes pairs of tag + * names and values that define dump options. + * <pre>array ( + * 'output_mode' => String + * 'file' : dump into a file + * default: dump using a function + * 'output' => String + * depending on the 'Output_Mode' + * name of the file + * name of the function + * 'end_of_line' => String + * end of line delimiter that should be used + * default: "\n" + * );</pre> + * @param int $dump Int that determines what data to dump + * + MDB2_SCHEMA_DUMP_ALL : the entire db + * + MDB2_SCHEMA_DUMP_STRUCTURE : only the structure of the db + * + MDB2_SCHEMA_DUMP_CONTENT : only the content of the db + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function dumpDatabase($database_definition, $arguments, $dump = MDB2_SCHEMA_DUMP_ALL) + { + $class_name = $this->options['writer']; + + $result = MDB2::loadClass($class_name, $this->db->getOption('debug')); + if (PEAR::isError($result)) { + return $result; + } + + // get initialization data + if (isset($database_definition['tables']) && is_array($database_definition['tables']) + && $dump == MDB2_SCHEMA_DUMP_ALL || $dump == MDB2_SCHEMA_DUMP_CONTENT + ) { + foreach ($database_definition['tables'] as $table_name => $table) { + $fields = array(); + $fieldsq = array(); + foreach ($table['fields'] as $field_name => $field) { + $fields[$field_name] = $field['type']; + + $fieldsq[] = $this->db->quoteIdentifier($field_name, true); + } + + $query = 'SELECT '.implode(', ', $fieldsq).' FROM '; + $query .= $this->db->quoteIdentifier($table_name, true); + + $data = $this->db->queryAll($query, $fields, MDB2_FETCHMODE_ASSOC); + + if (PEAR::isError($data)) { + return $data; + } + + if (!empty($data)) { + $initialization = array(); + $lob_buffer_length = $this->db->getOption('lob_buffer_length'); + foreach ($data as $row) { + $rows = array(); + foreach ($row as $key => $lob) { + if (is_resource($lob)) { + $value = ''; + while (!feof($lob)) { + $value .= fread($lob, $lob_buffer_length); + } + $row[$key] = $value; + } + $rows[] = array('name' => $key, 'group' => array('type' => 'value', 'data' => $row[$key])); + } + $initialization[] = array('type' => 'insert', 'data' => array('field' => $rows)); + } + $database_definition['tables'][$table_name]['initialization'] = $initialization; + } + } + } + + $writer = new $class_name($this->options['valid_types']); + return $writer->dumpDatabase($database_definition, $arguments, $dump); + } + + // }}} + // {{{ writeInitialization() + + /** + * Write initialization and sequences + * + * @param string|array $data data file or data array + * @param string|array $structure structure file or array + * @param array $variables associative array that is passed to the argument + * of the same name to the parseDatabaseDefinitionFile function. (there third + * param) + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function writeInitialization($data, $structure = false, $variables = array()) + { + if ($structure) { + $structure = $this->parseDatabaseDefinition($structure, false, $variables); + if (PEAR::isError($structure)) { + return $structure; + } + } + + $data = $this->parseDatabaseDefinition($data, false, $variables, false, $structure); + if (PEAR::isError($data)) { + return $data; + } + + $previous_database_name = null; + if (!empty($data['name'])) { + $previous_database_name = $this->db->setDatabase($data['name']); + } elseif (!empty($structure['name'])) { + $previous_database_name = $this->db->setDatabase($structure['name']); + } + + if (!empty($data['tables']) && is_array($data['tables'])) { + foreach ($data['tables'] as $table_name => $table) { + if (empty($table['initialization'])) { + continue; + } + $result = $this->initializeTable($table_name, $table); + if (PEAR::isError($result)) { + return $result; + } + } + } + + if (!empty($structure['sequences']) && is_array($structure['sequences'])) { + foreach ($structure['sequences'] as $sequence_name => $sequence) { + if (isset($data['sequences'][$sequence_name]) + || !isset($sequence['on']['table']) + || !isset($data['tables'][$sequence['on']['table']]) + ) { + continue; + } + $result = $this->createSequence($sequence_name, $sequence, true); + if (PEAR::isError($result)) { + return $result; + } + } + } + if (!empty($data['sequences']) && is_array($data['sequences'])) { + foreach ($data['sequences'] as $sequence_name => $sequence) { + $result = $this->createSequence($sequence_name, $sequence, true); + if (PEAR::isError($result)) { + return $result; + } + } + } + + if (isset($previous_database_name)) { + $this->db->setDatabase($previous_database_name); + } + + return MDB2_OK; + } + + // }}} + // {{{ updateDatabase() + + /** + * Compare the correspondent files of two versions of a database schema + * definition: the previously installed and the one that defines the schema + * that is meant to update the database. + * If the specified previous definition file does not exist, this function + * will create the database from the definition specified in the current + * schema file. + * If both files exist, the function assumes that the database was previously + * installed based on the previous schema file and will update it by just + * applying the changes. + * If this function succeeds, the contents of the current schema file are + * copied to replace the previous schema file contents. Any subsequent schema + * changes should only be done on the file specified by the $current_schema_file + * to let this function make a consistent evaluation of the exact changes that + * need to be applied. + * + * @param string|array $current_schema filename or array of the updated database schema definition. + * @param string|array $previous_schema filename or array of the previously installed database schema definition. + * @param array $variables associative array that is passed to the argument of the same + * name to the parseDatabaseDefinitionFile function. (there third param) + * @param bool $disable_query determines if the disable_query option should be set to true + * for the alterDatabase() or createDatabase() call + * @param bool $overwrite_old_schema_file Overwrite? + * + * @return bool|MDB2_Error MDB2_OK or error object + * @access public + */ + function updateDatabase($current_schema, $previous_schema = false, + $variables = array(), $disable_query = false, + $overwrite_old_schema_file = false) + { + $current_definition = $this->parseDatabaseDefinition($current_schema, false, $variables, + $this->options['fail_on_invalid_names']); + + if (PEAR::isError($current_definition)) { + return $current_definition; + } + + $previous_definition = false; + if ($previous_schema) { + $previous_definition = $this->parseDatabaseDefinition($previous_schema, true, $variables, + $this->options['fail_on_invalid_names']); + if (PEAR::isError($previous_definition)) { + return $previous_definition; + } + } + + if ($previous_definition) { + $dbExists = $this->db->databaseExists($current_definition['name']); + if (PEAR::isError($dbExists)) { + return $dbExists; + } + + if (!$dbExists) { + return $this->raiseError(MDB2_SCHEMA_ERROR, null, null, + 'database to update does not exist: '.$current_definition['name']); + } + + $changes = $this->compareDefinitions($current_definition, $previous_definition); + if (PEAR::isError($changes)) { + return $changes; + } + + if (is_array($changes)) { + $this->db->setOption('disable_query', $disable_query); + $result = $this->alterDatabase($current_definition, $previous_definition, $changes); + $this->db->setOption('disable_query', false); + if (PEAR::isError($result)) { + return $result; + } + $copy = true; + if ($this->db->options['debug']) { + $result = $this->dumpDatabaseChanges($changes); + if (PEAR::isError($result)) { + return $result; + } + } + } + } else { + $this->db->setOption('disable_query', $disable_query); + $result = $this->createDatabase($current_definition); + $this->db->setOption('disable_query', false); + if (PEAR::isError($result)) { + return $result; + } + } + + if ($overwrite_old_schema_file + && !$disable_query + && is_string($previous_schema) && is_string($current_schema) + && !copy($current_schema, $previous_schema)) { + + return $this->raiseError(MDB2_SCHEMA_ERROR, null, null, + 'Could not copy the new database definition file to the current file'); + } + + return MDB2_OK; + } + // }}} + // {{{ errorMessage() + + /** + * Return a textual error message for a MDB2 error code + * + * @param int|array $value integer error code, <code>null</code> to get the + * current error code-message map, + * or an array with a new error code-message map + * + * @return string error message, or false if the error code was not recognized + * @access public + */ + function errorMessage($value = null) + { + static $errorMessages; + if (is_array($value)) { + $errorMessages = $value; + return MDB2_OK; + } elseif (!isset($errorMessages)) { + $errorMessages = array( + MDB2_SCHEMA_ERROR => 'unknown error', + MDB2_SCHEMA_ERROR_PARSE => 'schema parse error', + MDB2_SCHEMA_ERROR_VALIDATE => 'schema validation error', + MDB2_SCHEMA_ERROR_INVALID => 'invalid', + MDB2_SCHEMA_ERROR_UNSUPPORTED => 'not supported', + MDB2_SCHEMA_ERROR_WRITER => 'schema writer error', + ); + } + + if (is_null($value)) { + return $errorMessages; + } + + if (PEAR::isError($value)) { + $value = $value->getCode(); + } + + return !empty($errorMessages[$value]) ? + $errorMessages[$value] : $errorMessages[MDB2_SCHEMA_ERROR]; + } + + // }}} + // {{{ raiseError() + + /** + * This method is used to communicate an error and invoke error + * callbacks etc. Basically a wrapper for PEAR::raiseError + * without the message string. + * + * @param int|PEAR_Error $code integer error code or and PEAR_Error instance + * @param int $mode error mode, see PEAR_Error docs + * error level (E_USER_NOTICE etc). If error mode is + * PEAR_ERROR_CALLBACK, this is the callback function, + * either as a function name, or as an array of an + * object and method name. For other error modes this + * parameter is ignored. + * @param array $options Options, depending on the mode, @see PEAR::setErrorHandling + * @param string $userinfo Extra debug information. Defaults to the last + * query and native error code. + * + * @return object a PEAR error object + * @access public + * @see PEAR_Error + */ + function &raiseError($code = null, $mode = null, $options = null, $userinfo = null) + { + $err =& PEAR::raiseError(null, $code, $mode, $options, + $userinfo, 'MDB2_Schema_Error', true); + return $err; + } + + // }}} + // {{{ isError() + + /** + * Tell whether a value is an MDB2_Schema error. + * + * @param mixed $data the value to test + * @param int $code if $data is an error object, return true only if $code is + * a string and $db->getMessage() == $code or + * $code is an integer and $db->getCode() == $code + * + * @return bool true if parameter is an error + * @access public + */ + function isError($data, $code = null) + { + if (is_a($data, 'MDB2_Schema_Error')) { + if (is_null($code)) { + return true; + } elseif (is_string($code)) { + return $data->getMessage() === $code; + } else { + $code = (array)$code; + return in_array($data->getCode(), $code); + } + } + return false; + } + + // }}} +} + +/** + * MDB2_Schema_Error implements a class for reporting portable database error + * messages. + * + * @category Database + * @package MDB2_Schema + * @author Stig Bakken <ssb@fast.no> + * @license BSD http://www.opensource.org/licenses/bsd-license.php + * @link http://pear.php.net/packages/MDB2_Schema + */ +class MDB2_Schema_Error extends PEAR_Error +{ + /** + * MDB2_Schema_Error constructor. + * + * @param mixed $code error code, or string with error message. + * @param int $mode what 'error mode' to operate in + * @param int $level what error level to use for $mode & PEAR_ERROR_TRIGGER + * @param mixed $debuginfo additional debug info, such as the last query + * + * @access public + */ + function MDB2_Schema_Error($code = MDB2_SCHEMA_ERROR, $mode = PEAR_ERROR_RETURN, + $level = E_USER_NOTICE, $debuginfo = null) + { + $this->PEAR_Error('MDB2_Schema Error: ' . MDB2_Schema::errorMessage($code), $code, + $mode, $level, $debuginfo); + } +} +?> |