2009-07-21 00:04:48 +01:00
< ? php
/**
2009-09-23 14:20:04 +01:00
* StatusNet , the distributed open - source microblogging tool
2009-07-21 00:04:48 +01:00
*
* Database schema utilities
*
* PHP version 5
*
* LICENCE : This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < http :// www . gnu . org / licenses />.
*
* @ category Database
2009-09-23 14:20:04 +01:00
* @ package StatusNet
* @ author Evan Prodromou < evan @ status . net >
* @ copyright 2009 StatusNet , Inc .
2009-07-21 00:04:48 +01:00
* @ license http :// www . fsf . org / licensing / licenses / agpl - 3.0 . html GNU Affero General Public License version 3.0
2009-09-23 14:20:04 +01:00
* @ link http :// status . net /
2009-07-21 00:04:48 +01:00
*/
2009-09-23 14:20:04 +01:00
if ( ! defined ( 'STATUSNET' )) {
2009-07-21 00:04:48 +01:00
exit ( 1 );
}
/**
* Class representing the database schema
*
* A class representing the database schema . Can be used to
* manipulate the schema -- especially for plugins and upgrade
* utilities .
*
* @ category Database
2009-09-23 14:20:04 +01:00
* @ package StatusNet
* @ author Evan Prodromou < evan @ status . net >
2009-07-21 00:04:48 +01:00
* @ license http :// www . fsf . org / licensing / licenses / agpl - 3.0 . html GNU Affero General Public License version 3.0
2009-09-23 14:20:04 +01:00
* @ link http :// status . net /
2009-07-21 00:04:48 +01:00
*/
class Schema
{
2009-08-24 16:22:40 +01:00
static $_single = null ;
protected $conn = null ;
2009-07-21 00:04:48 +01:00
2009-10-02 20:02:33 +01:00
/**
* Constructor . Only run once for singleton object .
*/
2009-08-24 16:22:40 +01:00
protected function __construct ()
2009-07-21 00:04:48 +01:00
{
2009-08-24 16:22:40 +01:00
// XXX: there should be an easier way to do this.
$user = new User ();
2009-10-02 20:02:33 +01:00
2009-08-24 16:22:40 +01:00
$this -> conn = $user -> getDatabaseConnection ();
2009-10-02 20:02:33 +01:00
2009-08-24 16:22:40 +01:00
$user -> free ();
2009-10-02 20:02:33 +01:00
2009-08-24 16:22:40 +01:00
unset ( $user );
}
2009-07-21 00:04:48 +01:00
2009-10-02 20:02:33 +01:00
/**
* Main public entry point . Use this to get
* the singleton object .
*
* @ return Schema the ( single ) Schema object
*/
2009-08-24 16:22:40 +01:00
static function get ()
{
if ( empty ( self :: $_single )) {
self :: $_single = new Schema ();
}
return self :: $_single ;
2009-07-21 00:04:48 +01:00
}
2009-10-02 20:02:33 +01:00
/**
* Returns a TableDef object for the table
* in the schema with the given name .
*
* Throws an exception if the table is not found .
*
* @ param string $name Name of the table to get
*
* @ return TableDef tabledef for that table .
*/
2009-07-21 00:04:48 +01:00
public function getTableDef ( $name )
{
2010-01-10 05:21:23 +00:00
if ( common_config ( 'db' , 'type' ) == 'pgsql' ) {
$res = $this -> conn -> query ( " select column_default as default, is_nullable as Null, udt_name as Type, column_name AS Field from INFORMATION_SCHEMA.COLUMNS where table_name = ' $name ' " );
}
else {
$res = $this -> conn -> query ( 'DESCRIBE ' . $name );
}
2009-08-24 16:22:40 +01:00
if ( PEAR :: isError ( $res )) {
throw new Exception ( $res -> getMessage ());
}
$td = new TableDef ();
$td -> name = $name ;
$td -> columns = array ();
$row = array ();
while ( $res -> fetchInto ( $row , DB_FETCHMODE_ASSOC )) {
2010-01-10 05:21:23 +00:00
//lower case the keys, because the php postgres driver is case insentive for column names
foreach ( $row as $k => $v ) {
$row [ strtolower ( $k )] = $row [ $k ];
}
2009-08-24 16:22:40 +01:00
$cd = new ColumnDef ();
2010-01-10 05:21:23 +00:00
$cd -> name = $row [ 'field' ];
2009-08-24 16:22:40 +01:00
2010-01-10 05:21:23 +00:00
$packed = $row [ 'type' ];
2009-08-24 16:22:40 +01:00
if ( preg_match ( '/^(\w+)\((\d+)\)$/' , $packed , $match )) {
$cd -> type = $match [ 1 ];
$cd -> size = $match [ 2 ];
} else {
$cd -> type = $packed ;
}
2010-01-10 05:21:23 +00:00
$cd -> nullable = ( $row [ 'null' ] == 'YES' ) ? true : false ;
2009-08-24 16:22:40 +01:00
$cd -> key = $row [ 'Key' ];
2010-01-10 05:21:23 +00:00
$cd -> default = $row [ 'default' ];
2009-08-24 16:22:40 +01:00
$cd -> extra = $row [ 'Extra' ];
$td -> columns [] = $cd ;
}
return $td ;
2009-07-21 00:04:48 +01:00
}
2009-10-02 20:02:33 +01:00
/**
* Gets a ColumnDef object for a single column .
*
* Throws an exception if the table is not found .
*
* @ param string $table name of the table
* @ param string $column name of the column
*
* @ return ColumnDef definition of the column or null
* if not found .
*/
2009-07-21 00:04:48 +01:00
public function getColumnDef ( $table , $column )
{
2009-09-24 03:24:35 +01:00
$td = $this -> getTableDef ( $table );
foreach ( $td -> columns as $cd ) {
if ( $cd -> name == $column ) {
return $cd ;
}
}
return null ;
2009-07-21 00:04:48 +01:00
}
2009-10-02 20:02:33 +01:00
/**
* Creates a table with the given names and columns .
*
* @ param string $name Name of the table
* @ param array $columns Array of ColumnDef objects
* for new table .
*
* @ return boolean success flag
*/
public function createTable ( $name , $columns )
2009-07-21 00:04:48 +01:00
{
2009-09-24 03:24:35 +01:00
$uniques = array ();
$primary = array ();
$indices = array ();
$sql = " CREATE TABLE $name ( \n " ;
for ( $i = 0 ; $i < count ( $columns ); $i ++ ) {
$cd =& $columns [ $i ];
if ( $i > 0 ) {
$sql .= " , \n " ;
}
$sql .= $this -> _columnSql ( $cd );
switch ( $cd -> key ) {
2009-10-02 20:02:33 +01:00
case 'UNI' :
2009-09-24 03:24:35 +01:00
$uniques [] = $cd -> name ;
break ;
2009-10-02 20:02:33 +01:00
case 'PRI' :
2009-09-24 03:24:35 +01:00
$primary [] = $cd -> name ;
break ;
2009-10-02 20:02:33 +01:00
case 'MUL' :
2009-09-24 03:24:35 +01:00
$indices [] = $cd -> name ;
break ;
}
}
if ( count ( $primary ) > 0 ) { // it really should be...
$sql .= " , \n constraint primary key ( " . implode ( ',' , $primary ) . " ) " ;
}
foreach ( $uniques as $u ) {
$sql .= " , \n unique index { $name } _ { $u } _idx ( $u ) " ;
}
foreach ( $indices as $i ) {
$sql .= " , \n index { $name } _ { $i } _idx ( $i ) " ;
}
$sql .= " ); " ;
2009-12-02 17:47:02 +00:00
$res = $this -> conn -> query ( $sql );
2009-09-24 03:24:35 +01:00
if ( PEAR :: isError ( $res )) {
throw new Exception ( $res -> getMessage ());
}
return true ;
2009-07-21 00:04:48 +01:00
}
2009-10-02 20:02:33 +01:00
/**
* Drops a table from the schema
*
* Throws an exception if the table is not found .
*
* @ param string $name Name of the table to drop
*
* @ return boolean success flag
*/
2009-07-21 00:04:48 +01:00
public function dropTable ( $name )
{
2009-12-02 17:47:02 +00:00
$res = $this -> conn -> query ( " DROP TABLE $name " );
2009-09-24 03:24:35 +01:00
if ( PEAR :: isError ( $res )) {
throw new Exception ( $res -> getMessage ());
}
return true ;
2009-07-21 00:04:48 +01:00
}
2009-10-02 20:02:33 +01:00
/**
* Adds an index to a table .
*
* If no name is provided , a name will be made up based
* on the table name and column names .
*
* Throws an exception on database error , esp . if the table
* does not exist .
*
* @ param string $table Name of the table
* @ param array $columnNames Name of columns to index
* @ param string $name ( Optional ) name of the index
*
* @ return boolean success flag
*/
public function createIndex ( $table , $columnNames , $name = null )
2009-07-21 00:04:48 +01:00
{
2009-10-01 20:00:54 +01:00
if ( ! is_array ( $columnNames )) {
$columnNames = array ( $columnNames );
}
if ( empty ( $name )) {
$name = " $table_ " . implode ( " _ " , $columnNames ) . " _idx " ;
}
2009-12-02 17:47:02 +00:00
$res = $this -> conn -> query ( " ALTER TABLE $table " .
2009-10-02 20:02:33 +01:00
" ADD INDEX $name ( " .
implode ( " , " , $columnNames ) . " ) " );
2009-10-01 20:00:54 +01:00
if ( PEAR :: isError ( $res )) {
throw new Exception ( $res -> getMessage ());
}
return true ;
2009-07-21 00:04:48 +01:00
}
2009-10-02 20:02:33 +01:00
/**
* Drops a named index from a table .
*
* @ param string $table name of the table the index is on .
* @ param string $name name of the index
*
* @ return boolean success flag
*/
2009-10-01 20:00:54 +01:00
public function dropIndex ( $table , $name )
2009-07-21 00:04:48 +01:00
{
2009-12-02 17:47:02 +00:00
$res = $this -> conn -> query ( " ALTER TABLE $table DROP INDEX $name " );
2009-10-01 20:00:54 +01:00
if ( PEAR :: isError ( $res )) {
throw new Exception ( $res -> getMessage ());
}
return true ;
2009-07-21 00:04:48 +01:00
}
2009-10-02 20:02:33 +01:00
/**
* Adds a column to a table
*
* @ param string $table name of the table
* @ param ColumnDef $columndef Definition of the new
* column .
*
* @ return boolean success flag
*/
2009-07-21 00:04:48 +01:00
public function addColumn ( $table , $columndef )
{
2009-10-01 20:00:54 +01:00
$sql = " ALTER TABLE $table ADD COLUMN " . $this -> _columnSql ( $columndef );
2009-12-02 17:47:02 +00:00
$res = $this -> conn -> query ( $sql );
2009-10-01 20:00:54 +01:00
if ( PEAR :: isError ( $res )) {
throw new Exception ( $res -> getMessage ());
}
return true ;
}
2009-10-02 20:02:33 +01:00
/**
* Modifies a column in the schema .
*
* The name must match an existing column and table .
*
* @ param string $table name of the table
* @ param ColumnDef $columndef new definition of the column .
*
* @ return boolean success flag
*/
2009-10-01 20:00:54 +01:00
public function modifyColumn ( $table , $columndef )
{
2009-10-02 20:02:33 +01:00
$sql = " ALTER TABLE $table MODIFY COLUMN " .
$this -> _columnSql ( $columndef );
2009-10-01 20:00:54 +01:00
2009-12-02 17:47:02 +00:00
$res = $this -> conn -> query ( $sql );
2009-10-01 20:00:54 +01:00
if ( PEAR :: isError ( $res )) {
throw new Exception ( $res -> getMessage ());
}
return true ;
}
2009-10-02 20:02:33 +01:00
/**
* Drops a column from a table
*
* The name must match an existing column .
*
* @ param string $table name of the table
* @ param string $columnName name of the column to drop
*
* @ return boolean success flag
*/
2009-10-01 20:00:54 +01:00
public function dropColumn ( $table , $columnName )
{
$sql = " ALTER TABLE $table DROP COLUMN $columnName " ;
2009-12-02 17:47:02 +00:00
$res = $this -> conn -> query ( $sql );
2009-10-01 20:00:54 +01:00
if ( PEAR :: isError ( $res )) {
throw new Exception ( $res -> getMessage ());
}
return true ;
2009-07-21 00:04:48 +01:00
}
2009-10-02 20:02:33 +01:00
/**
* Ensures that a table exists with the given
* name and the given column definitions .
*
* If the table does not yet exist , it will
* create the table . If it does exist , it will
* alter the table to match the column definitions .
*
* @ param string $tableName name of the table
* @ param array $columns array of ColumnDef
* objects for the table
*
* @ return boolean success flag
*/
public function ensureTable ( $tableName , $columns )
2009-07-21 00:04:48 +01:00
{
2009-10-01 20:00:54 +01:00
// XXX: DB engine portability -> toilet
try {
$td = $this -> getTableDef ( $tableName );
} catch ( Exception $e ) {
if ( preg_match ( '/no such table/' , $e -> getMessage ())) {
2009-10-02 20:02:33 +01:00
return $this -> createTable ( $tableName , $columns );
2009-10-01 20:00:54 +01:00
} else {
throw $e ;
}
}
$cur = $this -> _names ( $td -> columns );
$new = $this -> _names ( $columns );
$toadd = array_diff ( $new , $cur );
$todrop = array_diff ( $cur , $new );
2009-10-02 20:02:33 +01:00
$same = array_intersect ( $new , $cur );
$tomod = array ();
2009-10-01 20:43:08 +01:00
2009-10-01 20:00:54 +01:00
foreach ( $same as $m ) {
$curCol = $this -> _byName ( $td -> columns , $m );
$newCol = $this -> _byName ( $columns , $m );
if ( ! $newCol -> equals ( $curCol )) {
$tomod [] = $newCol -> name ;
}
}
if ( count ( $toadd ) + count ( $todrop ) + count ( $tomod ) == 0 ) {
// nothing to do
return true ;
}
// For efficiency, we want this all in one
// query, instead of using our methods.
$phrase = array ();
foreach ( $toadd as $columnName ) {
$cd = $this -> _byName ( $columns , $columnName );
2009-10-02 20:02:33 +01:00
2009-10-01 20:00:54 +01:00
$phrase [] = 'ADD COLUMN ' . $this -> _columnSql ( $cd );
}
foreach ( $todrop as $columnName ) {
$phrase [] = 'DROP COLUMN ' . $columnName ;
}
foreach ( $tomod as $columnName ) {
$cd = $this -> _byName ( $columns , $columnName );
2009-10-02 20:02:33 +01:00
2009-10-01 20:00:54 +01:00
$phrase [] = 'MODIFY COLUMN ' . $this -> _columnSql ( $cd );
}
$sql = 'ALTER TABLE ' . $tableName . ' ' . implode ( ', ' , $phrase );
2009-12-02 17:47:02 +00:00
$res = $this -> conn -> query ( $sql );
2009-10-01 20:00:54 +01:00
if ( PEAR :: isError ( $res )) {
throw new Exception ( $res -> getMessage ());
}
return true ;
2009-07-21 00:04:48 +01:00
}
2009-10-02 20:02:33 +01:00
/**
* Returns the array of names from an array of
* ColumnDef objects .
*
* @ param array $cds array of ColumnDef objects
*
* @ return array strings for name values
*/
private function _names ( $cds )
2009-07-21 00:04:48 +01:00
{
2009-10-01 20:00:54 +01:00
$names = array ();
foreach ( $cds as $cd ) {
$names [] = $cd -> name ;
}
return $names ;
2009-07-21 00:04:48 +01:00
}
2009-10-02 20:02:33 +01:00
/**
* Get a ColumnDef from an array matching
* name .
*
* @ param array $cds Array of ColumnDef objects
* @ param string $name Name of the column
*
* @ return ColumnDef matching item or null if no match .
*/
private function _byName ( $cds , $name )
2009-07-21 00:04:48 +01:00
{
2009-10-01 20:00:54 +01:00
foreach ( $cds as $cd ) {
if ( $cd -> name == $name ) {
return $cd ;
}
2009-07-21 00:04:48 +01:00
}
2009-10-01 20:00:54 +01:00
return null ;
2009-07-21 00:04:48 +01:00
}
2009-09-24 03:24:35 +01:00
2009-10-02 20:02:33 +01:00
/**
* Return the proper SQL for creating or
* altering a column .
*
* Appropriate for use in CREATE TABLE or
* ALTER TABLE statements .
*
* @ param ColumnDef $cd column to create
*
* @ return string correct SQL for that column
*/
private function _columnSql ( $cd )
2009-09-24 03:24:35 +01:00
{
$sql = " { $cd -> name } " ;
if ( ! empty ( $cd -> size )) {
$sql .= " { $cd -> type } ( { $cd -> size } ) " ;
} else {
$sql .= " { $cd -> type } " ;
}
if ( ! empty ( $cd -> default )) {
$sql .= " default { $cd -> default } " ;
} else {
$sql .= ( $cd -> nullable ) ? " null " : " not null " ;
}
2010-01-04 18:30:19 +00:00
if ( ! empty ( $cd -> auto_increment )) {
$sql .= " auto_increment " ;
}
2009-09-24 03:24:35 +01:00
2009-12-09 02:14:48 +00:00
if ( ! empty ( $cd -> extra )) {
$sql .= " { $cd -> extra } " ;
}
2009-09-24 03:24:35 +01:00
return $sql ;
}
2009-07-21 00:04:48 +01:00
}