blob: 15a1ae4da550a60b32a08d70802f5d1551779c47 [file] [log] [blame]
<?php
/**
* File containing the ezcDbSchemaSqliteWriter 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 SQLite as backend.
*
* @package DatabaseSchema
* @version //autogentag//
*/
class ezcDbSchemaSqliteWriter extends ezcDbSchemaCommonSqlWriter
{
/**
* Contains a type map from DbSchema types to SQLite native types.
*
* @var array
*/
private $typeMap = array(
'integer' => 'integer',
'boolean' => 'integer',
'float' => 'real',
'decimal' => 'numeric',
'date' => 'date',
'timestamp' => 'timestamp',
'text' => 'text',
'blob' => 'blob',
'clob' => 'clob'
);
/**
* Returns what type of schema writer this class implements.
*
* This method always returns ezcDbSchema::DATABASE
*
* @return int
*/
public function getWriterType()
{
return ezcDbSchema::DATABASE;
}
/**
* Checks if certain 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, 'DROP COLUMN') || strstr($query, 'CHANGE') ) // detecting DROP COLUMN clause or field CHANGE clause
{
return false;
}
if ( substr( $query, 0, 10 ) == 'DROP TABLE' )
{
$tableName = substr( $query, strlen( 'DROP TABLE ' ) );
$result = $db->query( "SELECT count(*) AS count FROM
(SELECT * FROM sqlite_master UNION ALL
SELECT * FROM sqlite_temp_master)
WHERE type='table' AND tbl_name={$tableName}" )->fetchAll();
if ( $result[0]['count'] == 1 )
{
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 ) as $query )
{
if ( $this->isQueryAllowed( $db, $query ) )
{
$db->exec( $query );
}
else
{
// SQLite don't support SQL clause for removing columns
// perform emulation for this
if ( strstr( $query, 'DROP COLUMN' ) )
{
$db->commit();
$db->beginTransaction();
try
{
preg_match ( "/ALTER TABLE (.*) DROP COLUMN (.*)/" , $query, $matches );
if ( !$matches )
{
throw new ezcDbSchemaSqliteDropFieldException(
"Can't fetch field for droping from SQL query: $query" );
}
$tableName = trim( $matches[1], "'" );
$dropFieldName = trim( $matches[2], "'" );
$this->dropField( $db, $tableName , $dropFieldName );
}
catch ( ezcDbSchemaSqliteDropFieldException $e )
{
}
$db->commit();
$db->beginTransaction();
}
else if ( strstr( $query, 'CHANGE' ) ) // SQLite don't support SQL clause for changing columns
// perform emulation for this
{
$db->commit();
$db->beginTransaction();
try
{
preg_match( "/ALTER TABLE (.*) CHANGE (.*?) (.*?) (.*)/" , $query, $matches );
$tableName = trim( $matches[1], "'" );
$changeFieldName = trim( $matches[2], "'" );
$changeFieldNewName = trim( $matches[3], "'" );
$changeFieldNewType = $matches[4];
$this->changeField( $db, $tableName, $changeFieldName, $changeFieldNewName, $changeFieldNewType );
}
catch ( ezcDbSchemaSqliteDropFieldException $e )
{
}
$db->commit();
$db->beginTransaction();
}
}
}
$db->commit();
}
/**
* Performs changing field in SQLite table.
* (workaround for "ALTER TABLE table CHANGE field fieldDefinition" that not alowed in SQLite ).
*
* @param ezcDbHandler $db
* @param string $tableName
* @param string $changeFieldName
* @param string $changeFieldNewName
* @param string $changeFieldNewDefinition
*/
private function changeField( ezcDbHandler $db, $tableName, $changeFieldName, $changeFieldNewName, $changeFieldNewDefinition )
{
$tmpTableName = $tableName.'_ezcbackup';
$resultArray = $db->query( "PRAGMA TABLE_INFO( '$tableName' )" );
$resultArray->setFetchMode( PDO::FETCH_NUM );
$fieldsDefinitions = array();
$fieldsList = array();
foreach ( $resultArray as $row )
{
$fieldSql = array();
$fieldSql[] = $row[1]; // name
if ( $row[1] == $changeFieldName )
{
// will recreate changed field with new definition
$fieldsDefinitions[] = "'$changeFieldNewName' $changeFieldNewDefinition";
$fieldsList[] = $fieldSql[0];
continue;
}
$fieldSql[] = $row[2]; // type
if ( $row[3] == '99' )
{
$fieldSql[] = 'NOT NULL';
}
$fieldDefault = null;
if ( $row[4] != '' )
{
$fieldSql[]= "DEFAULT '{$row[4]}'";
}
if ( $row[5] =='1' )
{
$fieldSql[] = 'PRIMARY KEY AUTOINCREMENT';
}
// FIXME: unsigned needs to be implemented
$fieldUnsigned = false;
$fieldsDefinitions[] = join ( ' ', $fieldSql );
$fieldsList[] = $fieldSql[0];
}
if ( count( $fieldsDefinitions ) > 0 )
{
$fields = join( ', ', $fieldsDefinitions );
$tmpTableCreateSql = "CREATE TEMPORARY TABLE '$tmpTableName'( $fields );";
$newTableCreateSql = "CREATE TABLE '$tableName'( $fields )" ;
if ( count($fieldsList)>0 )
{
$db->exec( $tmpTableCreateSql );
$db->exec( "INSERT INTO '$tmpTableName' SELECT ". join( ', ', $fieldsList )." FROM '$tableName';" );
$db->exec( "DROP TABLE '$tableName';" );
$db->exec( $newTableCreateSql );
$db->exec( "INSERT INTO '$tableName' SELECT ". join( ', ', $fieldsList )." FROM '$tmpTableName';" );
$db->exec( "DROP TABLE '$tmpTableName';" );
}
else
{
// we had table with one column will drop table and recreate with changed column.
$db->exec( "DROP TABLE '$tableName';" );
$newTableCreateSql = "CREATE TABLE '$tableName'( $changeFieldNewName $changeFieldNewDefinition )" ;
$db->exec( $newTableCreateSql );
}
}
}
/**
* Performs droping field from SQLite table using temporary table
* (workaround for "ALTER TABLE table DROP field" that not alowed in SQLite ).
*
* @param ezcDbHandler $db
* @param string $tableName
* @param string $dropFieldName
*/
private function dropField( $db, $tableName , $dropFieldName )
{
$tmpTableName = $tableName.'_ezcbackup';
$resultArray = $db->query( "PRAGMA TABLE_INFO( $tableName )" );
$resultArray->setFetchMode( PDO::FETCH_NUM );
$fieldsDefinitions = array();
$fieldsList = array();
foreach ( $resultArray as $row )
{
$fieldSql = array();
$fieldSql[] = "'{$row[1]}'"; // name
if ( $row[1] == $dropFieldName )
{
continue; // don't include droped fileld in temporary table
}
$fieldSql[] = $row[2]; // type
if ( $row[3] == '99' )
{
$fieldSql[] = 'NOT NULL';
}
$fieldDefault = null;
if ( $row[4] != '' )
{
$fieldSql[]= "DEFAULT '{$row[4]}'";
}
if ( $row[5] =='1' )
{
$fieldSql[] = 'PRIMARY KEY AUTOINCREMENT';
}
// FIXME: unsigned needs to be implemented
$fieldUnsigned = false;
$fieldsDefinitions[] = join ( ' ', $fieldSql );
$fieldsList[] = $fieldSql[0];
}
$fields = join( ', ', $fieldsDefinitions );
$tmpTableCreateSql = "CREATE TEMPORARY TABLE '$tmpTableName'( $fields );";
$newTableCreateSql = "CREATE TABLE '$tableName'( $fields )" ;
if ( count( $fieldsList ) > 0 )
{
$db->exec( $tmpTableCreateSql );
$db->exec( "INSERT INTO '$tmpTableName' SELECT ". join( ', ', $fieldsList )." FROM '$tableName';" );
$db->exec( "DROP TABLE '$tableName';" );
$db->exec( $newTableCreateSql );
$db->exec( "INSERT INTO '$tableName' SELECT ". join( ', ', $fieldsList )." FROM '$tmpTableName';" );
$db->exec( "DROP TABLE '$tmpTableName';" );
}
else
{
throw new ezcDbSchemaDropAllColumnsException(
"Trying to delete all columns in table: $tableName" );
}
}
/**
* Returns the differences definition in $dbSchema as database specific SQL DDL queries.
*
* @param ezcDbSchemaDiff $dbSchemaDiff
*
* @return array(string)
*/
public function convertDiffToDDL( ezcDbSchemaDiff $dbSchemaDiff )
{
$this->diffSchema = $dbSchemaDiff;
// reset queries
$this->queries = array();
$this->context = array();
$this->generateDiffSchemaAsSql();
return $this->queries;
}
/**
* Adds a "drop table" query for the table $tableName to the internal list of queries.
*
* @todo use DROP IF EXISTS that supported since SQLite 3.3
*
* @param string $tableName
*/
protected function generateDropTableSql( $tableName )
{
// use DROP TABLE and isQueryAllowed() workaround to emulate DROP TABLE IF EXISTS.
$this->queries[] = "DROP TABLE '$tableName'";
}
/**
* 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'";
}
}
/**
* 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})";
}
}
if ( $fieldDefinition->type == 'text' && !$fieldDefinition->length )
{
$typeAddition = "(255)";
}
if ( $fieldDefinition->type == 'boolean' )
{
$typeAddition = '(1)';
}
if ( !isset( $this->typeMap[$fieldDefinition->type] ) )
{
throw new ezcDbSchemaUnsupportedTypeException( 'SQLite', $fieldDefinition->type );
}
$type = $this->typeMap[$fieldDefinition->type];
return "$type$typeAddition";
}
/**
* Returns a "CREATE TABLE" SQL statement part for the table $tableName.
*
* @param string $tableName
* @return string
*/
protected function generateCreateTableSqlStatement( $tableName )
{
return "CREATE TABLE '{$tableName}'";
}
/**
* Adds a "create table" query for the table $tableName with definition $tableDefinition to the internal list of queries.
*
* @param string $tableName
* @param ezcDbSchemaTable $tableDefinition
*/
protected function generateCreateTableSql( $tableName, ezcDbSchemaTable $tableDefinition )
{
$this->context['skip_primary'] = false;
parent::generateCreateTableSql( $tableName, $tableDefinition );
}
/**
* 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 )
{
if ( $fieldDefinition->notNull && $fieldDefinition->default == null )
{
$fieldDefinition->default = $this->generateDefault( $fieldDefinition->type, 0 );
}
$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 )
{
$this->queries[] = "ALTER TABLE '$tableName' CHANGE '$fieldName' " . $this->generateFieldSql( $fieldName, $fieldDefinition );
}
/**
* Adds a "alter table" query to drop the field $fieldName from $tableName.
* will be hooked on execution stage and workaround using temporary
* table will be performed.
*
* @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
* @return string
*/
protected function generateFieldSql( $fieldName, ezcDbSchemaField $fieldDefinition )
{
$sqlDefinition = "'$fieldName' ";
$defList = array();
$type = $this->convertFromGenericType( $fieldDefinition );
$defList[] = $type;
if ( $fieldDefinition->notNull )
{
$defList[] = 'NOT NULL';
}
if ( $fieldDefinition->autoIncrement )
{
$defList[] = "PRIMARY KEY AUTOINCREMENT";
$this->context['skip_primary'] = true;
}
if ( !is_null( $fieldDefinition->default ) && !$fieldDefinition->autoIncrement )
{
$default = $this->generateDefault( $fieldDefinition->type, $fieldDefinition->default );
$defList[] = "DEFAULT $default";
}
$sqlDefinition .= join( ' ', $defList );
return $sqlDefinition;
}
/**
* Adds a "create index" 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;
}
if ( $indexName == 'primary' )
{
$indexName = $tableName.'_pri';
}
$sql = "CREATE UNIQUE INDEX '$indexName' ON '$tableName'";
}
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 revote 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')
{
$indexName = $tableName.'_pri';
}
$this->queries[] = "DROP INDEX '$indexName'";
}
}
?>