'number', 'boolean' => 'char', 'float' => 'float', 'decimal' => 'number', 'date' => 'date', 'timestamp' => 'timestamp', 'text' => 'varchar2', 'blob' => 'blob', 'clob' => 'clob' ); /** * Checks if query allowed. * * Perform testing if table exist for DROP TABLE query * to avoid stoping execution while try to drop not existent table. * * @param ezcDbHandler $db * @param string $query * * @return boolean false if query should not be executed. */ public function isQueryAllowed( ezcDbHandler $db, $query ) { if ( strstr( $query, 'AUTO_INCREMENT' ) ) // detect AUTO_INCREMENT and return imediately. Will process later. { return false; } if ( substr( $query, 0, 10 ) == 'DROP TABLE' ) { $tableName = substr($query, strlen( 'DROP TABLE "' ), -1 ); // get table name without quotes $result = $db->query( "SELECT count( table_name ) AS count FROM user_tables WHERE table_name='$tableName'" )->fetchAll(); if ( $result[0]['count'] == 1 ) { $sequences = $db->query( "SELECT sequence_name FROM user_sequences" )->fetchAll(); array_walk( $sequences, create_function( '&$item,$key', '$item = $item[0];' ) ); foreach ( $sequences as $sequenceName ) { // try to drop sequences related to dropped table. if ( substr( $sequenceName, 0, strlen($tableName) ) == $tableName ) { $db->query( "DROP SEQUENCE \"{$sequenceName}\"" ); } } return true; } else { return false; } } return true; } /** * Returns what type of schema difference writer this class implements. * * This method always returns ezcDbSchema::DATABASE * * @return int */ public function getDiffWriterType() { return ezcDbSchema::DATABASE; } /** * Applies the differences defined in $dbSchemaDiff to the database referenced by $db. * * This method uses {@link convertDiffToDDL} to create SQL for the * differences and then executes the returned SQL statements on the * database handler $db. * * @todo check for failed transaction * * @param ezcDbHandler $db * @param ezcDbSchemaDiff $dbSchemaDiff */ public function applyDiffToDb( ezcDbHandler $db, ezcDbSchemaDiff $dbSchemaDiff ) { $db->beginTransaction(); foreach ( $this->convertDiffToDDL( $dbSchemaDiff, $db ) as $query ) { if ( $this->isQueryAllowed( $db, $query ) ) { $db->exec( $query ); } else { if ( strstr($query, 'AUTO_INCREMENT') ) // detect AUTO_INCREMENT and emulate it by adding sequence and trigger { $db->commit(); $db->beginTransaction(); if ( preg_match ( "/ALTER TABLE (.*) MODIFY (.*?) (.*) AUTO_INCREMENT/" , $query, $matches ) ) { $tableName = trim( $matches[1], '"' ); $autoIncrementFieldName = trim( $matches[2], '"' ); $autoIncrementFieldType = trim( $matches[3], '"' ); $this->addAutoIncrementField( $db, $tableName, $autoIncrementFieldName, $autoIncrementFieldType ); } } $db->commit(); $db->beginTransaction(); } } $db->commit(); } /** * Performs changing field in Oracle table. * (workaround for "ALTER TABLE table MODIFY field fieldType AUTO_INCREMENT " that not alowed in Oracle ). * * @param ezcDbHandler $db * @param string $tableName * @param string $autoIncrementFieldName * @param string $autoIncrementFieldType */ private function addAutoIncrementField( $db, $tableName, $autoIncrementFieldName, $autoIncrementFieldType ) { // fetching field info from Oracle, getting column position of autoincrement field // @apichange This code piece would become orphan, with the new // implementation. We still need it to drop the old sequences. // Remove until --END-- to not take care of them. $resultArray = $db->query( "SELECT a.column_name AS field, " . " a.column_id AS field_pos " . "FROM user_tab_columns a " . "WHERE a.table_name = '{$tableName}' AND a.column_name = '{$autoIncrementFieldName}'" . "ORDER BY a.column_id" ); $resultArray->setFetchMode( PDO::FETCH_ASSOC ); if ( count( $resultArray) != 1 ) { return; } $result = $resultArray->fetch(); $fieldPos = $result['field_pos']; // emulation of autoincrement through adding sequence, trigger and constraint $oldName = ezcDbSchemaOracleHelper::generateSuffixCompositeIdentName( $tableName, $fieldPos, "seq" ); $oldNameTrigger = ezcDbSchemaOracleHelper::generateSuffixCompositeIdentName( $tableName, $fieldPos, "trg" ); $sequence = $db->query( "SELECT sequence_name FROM user_sequences WHERE sequence_name = '{$oldName}'" )->fetchAll(); if ( count( $sequence) > 0 ) { // assuming that if the seq exists, the trigger exists too $db->query( "DROP SEQUENCE \"{$oldName}\"" ); $db->query( "DROP TRIGGER \"{$oldNameTrigger}\"" ); } // --END-- // New sequence names, using field names $newName = ezcDbSchemaOracleHelper::generateSuffixCompositeIdentName( $tableName, $fieldPos, "seq" ); $newNameTrigger = ezcDbSchemaOracleHelper::generateSuffixCompositeIdentName( $tableName, $fieldPos, "seq" ); // Emulation of autoincrement through adding sequence, trigger and constraint $sequences = $db->query( "SELECT sequence_name FROM user_sequences WHERE sequence_name = '{$newName}'" )->fetchAll(); if ( count( $sequences ) > 0 ) { $db->query( "DROP SEQUENCE \"{$newName}\"" ); } $db->exec( "CREATE SEQUENCE \"{$newName}\" start with 1 increment by 1 nomaxvalue" ); $db->exec( "CREATE OR REPLACE TRIGGER \"{$newNameTrigger}\" ". "before insert on \"{$tableName}\" for each row ". "begin ". "select \"{$newName}\".nextval into :new.\"{$autoIncrementFieldName}\" from dual; ". "end;" ); $constraintName = ezcDbSchemaOracleHelper::generateSuffixedIdentName( array( $tableName ), "pkey" ); $constraint = $db->query( "SELECT constraint_name FROM user_cons_columns WHERE constraint_name = '{$constraintName}'" )->fetchAll(); if ( count( $constraint) > 0 ) { $db->query( "ALTER TABLE \"$tableName\" DROP CONSTRAINT \"{$constraintName}\"" ); } $db->exec( "ALTER TABLE \"{$tableName}\" ADD CONSTRAINT \"{$constraintName}\" PRIMARY KEY ( \"{$autoIncrementFieldName}\" )" ); $this->context['skip_primary'] = true; } /** * Returns the differences definition in $dbSchema as database specific SQL DDL queries. * * @param ezcDbSchemaDiff $dbSchemaDiff * @param ezcDbHandler $db * * @return array(string) */ public function convertDiffToDDL( ezcDbSchemaDiff $dbSchemaDiff, ezcDbHandler $db = null ) { $this->diffSchema = $dbSchemaDiff; // reset queries $this->queries = array(); $this->context = array(); // Find sequences which require explicit drop statesments, see bug // #16222 if ( $db !== null ) { $this->generateAdditionalDropSequenceStatements( $dbSchemaDiff, $db ); } $this->generateDiffSchemaAsSql(); return $this->queries; } /** * Generate additional drop sequence statements * * Some sequences might not be dropped automatically, this method generates * additional DROP SEQUENCE queries for those. * * Since Oracle only allows sequence identifiers up to 30 characters * sequences for long table / column names may be shortened. In this case * the sequence name does not started with the table name any more, thus * does not get dropped together with the table automatically. * * This method requires a DB connection to check which sequences have been * defined in the database, because the information about fields is not * available otherwise. * * @param ezcDbSchemaDiff $dbSchemaDiff * @param ezcDbHandler $db * @return void */ protected function generateAdditionalDropSequenceStatements( ezcDbSchemaDiff $dbSchemaDiff, ezcDbHandler $db ) { $reader = new ezcDbSchemaOracleReader(); $schema = $reader->loadFromDb( $db )->getSchema(); foreach ( $dbSchemaDiff->removedTables as $table => $true ) { foreach ( $schema[$table]->fields as $name => $field ) { if ( $field->autoIncrement !== true ) { continue; } $seqName = ezcDbSchemaOracleHelper::generateSuffixCompositeIdentName( $table, $name, "seq" ); if ( strpos( $seqName, $table ) !== 0 ) { $this->queries[] = "DROP SEQUENCE \"$seqName\""; } } } } /** * Adds a "drop table" query for the table $tableName to the internal list of queries. * * @param string $tableName */ protected function generateDropTableSql( $tableName ) { $this->queries[] = "DROP TABLE \"$tableName\""; } /** * Converts the generic field type contained in $fieldDefinition to a database specific field definition. * * @param ezcDbSchemaField $fieldDefinition * @return string */ protected function convertFromGenericType( ezcDbSchemaField $fieldDefinition ) { $typeAddition = ''; if ( in_array( $fieldDefinition->type, array( 'decimal', 'text' ) ) ) { if ( $fieldDefinition->length !== false && $fieldDefinition->length !== 0 ) { $typeAddition = "({$fieldDefinition->length})"; } else { $typeAddition = "(4000)"; // default length for varchar2 in Oracle } } if ( $fieldDefinition->type == 'boolean' ) { $typeAddition = "(1)"; if ( $fieldDefinition->default ) { $fieldDefinition->default = ( $fieldDefinition->default == 'true' ) ? '1': '0'; } } if ( !isset( $this->typeMap[$fieldDefinition->type] ) ) { throw new ezcDbSchemaUnsupportedTypeException( 'Oracle', $fieldDefinition->type ); } $type = $this->typeMap[$fieldDefinition->type]; return "$type$typeAddition"; } /** * Adds a "create table" query for the table $tableName with * definition $tableDefinition to the internal list of queries. * * Adds additional CREATE queries for sequences and triggers * to implement autoincrement fields that not supported in Oracle directly. * * @param string $tableName * @param ezcDbSchemaTable $tableDefinition */ protected function generateCreateTableSql( $tableName, ezcDbSchemaTable $tableDefinition ) { $sql = ''; $sql .= "CREATE TABLE \"{$tableName}\" (\n"; $this->context['skip_primary'] = false; // dump fields $fieldsSQL = array(); $autoincrementSQL = array(); $fieldCounter = 1; foreach ( $tableDefinition->fields as $fieldName => $fieldDefinition ) { $fieldsSQL[] = "\t" . $this->generateFieldSql( $fieldName, $fieldDefinition ); if ( $fieldDefinition->autoIncrement && !$this->context['skip_primary'] ) { $sequenceName = ezcDbSchemaOracleHelper::generateSuffixCompositeIdentName( $tableName, $fieldName, "seq" ); $triggerName = ezcDbSchemaOracleHelper::generateSuffixCompositeIdentName( $tableName, $fieldName, "trg" ); $constraintName = ezcDbSchemaOracleHelper::generateSuffixedIdentName( array( $tableName ), "pkey" ); $autoincrementSQL[] = "CREATE SEQUENCE \"{$sequenceName}\" start with 1 increment by 1 nomaxvalue"; $autoincrementSQL[] = "CREATE OR REPLACE TRIGGER \"{$triggerName}\" ". "before insert on \"{$tableName}\" for each row ". "begin ". "select \"{$sequenceName}\".nextval into :new.\"{$fieldName}\" from dual; ". "end;"; $autoincrementSQL[] = "ALTER TABLE \"{$tableName}\" ADD CONSTRAINT \"{$constraintName}\" PRIMARY KEY ( \"{$fieldName}\" )"; $this->context['skip_primary'] = true; } $fieldCounter++; } $sql .= join( ",\n", $fieldsSQL ); $sql .= "\n)"; $this->queries[] = $sql; if ( count( $autoincrementSQL ) > 0 ) // adding autoincrement emulation queries if exists { $this->queries = array_merge( $this->queries, $autoincrementSQL ); } // dump indexes foreach ( $tableDefinition->indexes as $indexName => $indexDefinition) { $fieldsSQL[] = $this->generateAddIndexSql( $tableName, $indexName, $indexDefinition ); } } /** * Generates queries to upgrade a the table $tableName with the differences in $tableDiff. * * This method generates queries to migrate a table to a new version * with the changes that are stored in the $tableDiff property. It * will call different subfunctions for the different types of changes, and * those functions will add queries to the internal list of queries that is * stored in $this->queries. * * @param string $tableName * @param ezcDbSchemaTableDiff $tableDiff */ protected function generateDiffSchemaTableAsSql( $tableName, ezcDbSchemaTableDiff $tableDiff ) { $this->context['skip_primary'] = false; parent::generateDiffSchemaTableAsSql( $tableName, $tableDiff ); } /** * Adds a "alter table" query to add the field $fieldName to $tableName with the definition $fieldDefinition. * * @param string $tableName * @param string $fieldName * @param ezcDbSchemaField $fieldDefinition */ protected function generateAddFieldSql( $tableName, $fieldName, ezcDbSchemaField $fieldDefinition ) { $this->queries[] = "ALTER TABLE \"$tableName\" ADD " . $this->generateFieldSql( $fieldName, $fieldDefinition ); } /** * Adds a "alter table" query to change the field $fieldName to $tableName with the definition $fieldDefinition. * * @param string $tableName * @param string $fieldName * @param ezcDbSchemaField $fieldDefinition */ protected function generateChangeFieldSql( $tableName, $fieldName, ezcDbSchemaField $fieldDefinition ) { if ( !$fieldDefinition->autoIncrement ) { $this->queries[] = "ALTER TABLE \"$tableName\" MODIFY " . $this->generateFieldSql( $fieldName, $fieldDefinition ); } else { // mark query to make autoincrement emulation when executing $this->queries[] = "ALTER TABLE \"$tableName\" MODIFY " . $this->generateFieldSql( $fieldName, $fieldDefinition ) . " AUTO_INCREMENT"; } } /** * Adds a "alter table" query to drop the field $fieldName from $tableName. * * @param string $tableName * @param string $fieldName */ protected function generateDropFieldSql( $tableName, $fieldName ) { $this->queries[] = "ALTER TABLE \"$tableName\" DROP COLUMN \"$fieldName\""; } /** * Returns a column definition for $fieldName with definition $fieldDefinition. * * @param string $fieldName * @param ezcDbSchemaField $fieldDefinition * @param string $autoincrementField * @return string */ protected function generateFieldSql( $fieldName, ezcDbSchemaField $fieldDefinition ) { $sqlDefinition = '"'.$fieldName.'" '; $defList = array(); $type = $this->convertFromGenericType( $fieldDefinition ); $defList[] = $type; if ( !is_null( $fieldDefinition->default ) && !$fieldDefinition->autoIncrement ) { $default = $this->generateDefault( $fieldDefinition->type, $fieldDefinition->default ); $defList[] = "DEFAULT $default"; } if ( $fieldDefinition->notNull ) { $defList[] = 'NOT NULL'; } $sqlDefinition .= join( ' ', $defList ); return $sqlDefinition; } /** * Returns an appropriate default value for $type with $value. * * @param string $type * @param mixed $value * @return string */ protected function generateDefault( $type, $value ) { switch ( $type ) { case 'boolean': return ( $value && $value != 'false' ) ? '1' : '0'; case 'integer': return (int) $value; case 'float': case 'decimal': return (float) $value; default: return "'$value'"; } } /** * Adds a "alter table" query to add the index $indexName to the table $tableName with definition $indexDefinition to the internal list of queries * * @param string $tableName * @param string $indexName * @param ezcDbSchemaIndex $indexDefinition */ protected function generateAddIndexSql( $tableName, $indexName, ezcDbSchemaIndex $indexDefinition ) { $sql = ""; if ( $indexDefinition->primary ) { if ( $this->context['skip_primary'] ) { return; } $sql = "ALTER TABLE \"$tableName\" ADD CONSTRAINT \"{$tableName}_pkey\" PRIMARY KEY"; } else if ( $indexDefinition->unique ) { $sql = "CREATE UNIQUE INDEX \"$indexName\" ON \"$tableName\""; } else { $sql = "CREATE INDEX \"$indexName\" ON \"$tableName\""; } $sql .= " ( "; $indexFieldSql = array(); foreach ( $indexDefinition->indexFields as $indexFieldName => $dummy ) { $indexFieldSql[] = "\"$indexFieldName\""; } $sql .= join( ', ', $indexFieldSql ) . " )"; $this->queries[] = $sql; } /** * Adds a "alter table" query to remote the index $indexName from the table $tableName to the internal list of queries. * * @param string $tableName * @param string $indexName */ protected function generateDropIndexSql( $tableName, $indexName ) { if ( $indexName == 'primary' ) // handling primary indexes { $constraintName = ezcDbSchemaOracleHelper::generateSuffixedIdentName( array( $tableName ), "pkey"); $this->queries[] = "ALTER TABLE \"$tableName\" DROP CONSTRAINT \"{$constraintName}\""; } else { $this->queries[] = "DROP INDEX \"$indexName\""; } } } ?>