blob: 116c7ab9dd2060afb4729ac3364dfddcade430cb [file] [log] [blame]
<?php
/**
* File containing the ezcDbSchemaOracleWriter class.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @package DatabaseSchema
* @version //autogentag//
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
*/
/**
* Handler for storing database schemas and applying differences that uses Oracle as backend.
*
* @package DatabaseSchema
* @version //autogentag//
*/
class ezcDbSchemaOracleWriter extends ezcDbSchemaCommonSqlWriter implements ezcDbSchemaDbWriter, ezcDbSchemaDiffDbWriter
{
/**
* Contains a type map from DbSchema types to Oracle native types.
*
* @var array
*/
private $typeMap = array(
'integer' => '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\"";
}
}
}
?>