2010-01-30 14:45:10 +09:00
< ? php
/**
* StatusNet , the distributed open - source microblogging tool
*
* 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
* @ package StatusNet
* @ author Evan Prodromou < evan @ status . net >
* @ copyright 2009 StatusNet , Inc .
* @ license http :// www . fsf . org / licensing / licenses / agpl - 3.0 . html GNU Affero General Public License version 3.0
* @ link http :// status . net /
*/
if ( ! defined ( 'STATUSNET' )) {
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
* @ package StatusNet
* @ author Evan Prodromou < evan @ status . net >
2010-05-15 11:56:40 +09:00
* @ author Brenda Wallace < shiny @ cpan . org >
2010-10-09 08:36:32 +09:00
* @ author Brion Vibber < brion @ status . net >
2010-01-30 14:45:10 +09:00
* @ license http :// www . fsf . org / licensing / licenses / agpl - 3.0 . html GNU Affero General Public License version 3.0
* @ link http :// status . net /
*/
class PgsqlSchema extends Schema
{
/**
2010-10-09 08:36:32 +09:00
* Returns a table definition array for the table
2010-01-30 14:45:10 +09:00
* in the schema with the given name .
*
* Throws an exception if the table is not found .
*
2010-10-09 08:36:32 +09:00
* @ param string $table Name of the table to get
2010-01-30 14:45:10 +09:00
*
2010-10-09 08:36:32 +09:00
* @ return array tabledef for that table .
2010-01-30 14:45:10 +09:00
*/
2010-10-09 08:36:32 +09:00
public function getTableDef ( $table )
2010-01-30 14:45:10 +09:00
{
2010-10-09 08:36:32 +09:00
$def = array ();
$hasKeys = false ;
2010-01-30 14:45:10 +09:00
2010-10-09 08:36:32 +09:00
// Pull column data from INFORMATION_SCHEMA
$columns = $this -> fetchMetaInfo ( $table , 'columns' , 'ordinal_position' );
if ( count ( $columns ) == 0 ) {
throw new SchemaTableMissingException ( " No such table: $table " );
2010-01-30 14:45:10 +09:00
}
2010-10-12 06:15:02 +09:00
// We'll need to match up fields by ordinal reference
$orderedFields = array ();
2010-10-09 08:36:32 +09:00
foreach ( $columns as $row ) {
2010-01-30 14:45:10 +09:00
2010-10-09 08:36:32 +09:00
$name = $row [ 'column_name' ];
2010-10-12 06:15:02 +09:00
$orderedFields [ $row [ 'ordinal_position' ]] = $name ;
2010-10-09 08:36:32 +09:00
$field = array ();
2010-01-30 14:45:10 +09:00
2010-10-09 08:36:32 +09:00
// ??
list ( $type , $size ) = $this -> reverseMapType ( $row [ 'udt_name' ]);
$field [ 'type' ] = $type ;
if ( $size !== null ) {
$field [ 'size' ] = $size ;
}
2010-01-30 14:45:10 +09:00
2010-10-09 08:36:32 +09:00
if ( $type == 'char' || $type == 'varchar' ) {
if ( $row [ 'character_maximum_length' ] !== null ) {
$field [ 'length' ] = intval ( $row [ 'character_maximum_length' ]);
}
}
if ( $type == 'numeric' ) {
// Other int types may report these values, but they're irrelevant.
// Just ignore them!
if ( $row [ 'numeric_precision' ] !== null ) {
$field [ 'precision' ] = intval ( $row [ 'numeric_precision' ]);
}
if ( $row [ 'numeric_scale' ] !== null ) {
$field [ 'scale' ] = intval ( $row [ 'numeric_scale' ]);
}
}
if ( $row [ 'is_nullable' ] == 'NO' ) {
$field [ 'not null' ] = true ;
}
if ( $row [ 'column_default' ] !== null ) {
$field [ 'default' ] = $row [ 'column_default' ];
if ( $this -> isNumericType ( $type )) {
$field [ 'default' ] = intval ( $field [ 'default' ]);
}
}
2010-01-30 14:45:10 +09:00
2010-10-09 08:36:32 +09:00
$def [ 'fields' ][ $name ] = $field ;
}
2010-01-30 14:45:10 +09:00
2010-10-12 06:15:02 +09:00
// Pulling index info from pg_class & pg_index
// This can provide us basic info on primary, unique, and multi-val keys
// But... it doesn't list plain constraints or foreign key constraints. :P
$indexInfo = $this -> getIndexInfo ( $table );
foreach ( $indexInfo as $row ) {
$keyName = $row [ 'key_name' ];
// Dig the column references out!
$cols = array ();
$colPositions = explode ( ' ' , $row [ 'indkey' ]);
foreach ( $colPositions as $ord ) {
// ordinal_position from above is 1-based
// but values in indkey are 0-based
if ( $ord == 0 ) {
$cols [] = 'FUNCTION' ; // @fixme
} else {
$cols [] = $orderedFields [ $ord ];
}
}
// @fixme foreign keys?
// @fixme prefixes?
// @fixme funky stuff like fulltext?
if ( $row [ 'indisprimary' ] == 't' ) {
$def [ 'primary key' ] = $cols ;
} else if ( $row [ 'indisunique' ] == 't' ) {
$def [ 'unique keys' ][ $keyName ] = $cols ;
} else {
$def [ 'indexes' ][ $keyName ] = $cols ;
}
}
2010-10-09 08:36:32 +09:00
// Pull constraint data from INFORMATION_SCHEMA
// @fixme also find multi-val indexes
// @fixme distinguish the primary key
// @fixme pull foreign key references
$keyColumns = $this -> fetchMetaInfo ( $table , 'key_column_usage' , 'constraint_name,ordinal_position' );
$keys = array ();
foreach ( $keyColumns as $row ) {
2010-10-12 06:15:02 +09:00
var_dump ( $row );
2010-10-09 08:36:32 +09:00
$keyName = $row [ 'constraint_name' ];
$keyCol = $row [ 'column_name' ];
if ( ! isset ( $keys [ $keyName ])) {
$keys [ $keyName ] = array ();
2010-01-30 14:45:10 +09:00
}
2010-10-09 08:36:32 +09:00
$keys [ $keyName ][] = $keyCol ;
}
2010-01-30 14:45:10 +09:00
2010-10-09 08:36:32 +09:00
foreach ( $keys as $keyName => $cols ) {
2010-10-12 06:15:02 +09:00
// hack -- is this reliable?
if ( $keyName == " { $table } _pkey " ) {
$def [ 'xprimary key' ] = $cols ;
} else if ( preg_match ( " /^ { $table } _(.*)_fkey $ / " , $keyName , $matches )) {
$keys = array_keys ( $cols );
if ( count ( $cols ) == 1 && $cols [ $keys [ 0 ]] == $keyName ) {
$def [ 'foreign keys' ][ $keyname ][ $matches [ 1 ]] = $keys [ 0 ];
}
$def [ 'foreign keys' ][ $keyName ][ $matches [ 1 ]] = $cols ;
} else {
$def [ 'xunique indexes' ][ $keyName ] = $cols ;
}
2010-10-09 08:36:32 +09:00
}
return $def ;
}
2010-01-30 14:45:10 +09:00
2010-10-09 08:36:32 +09:00
/**
* Pull some INFORMATION . SCHEMA data for the given table .
*
* @ param string $table
* @ return array of arrays
*/
function fetchMetaInfo ( $table , $infoTable , $orderBy = null )
{
$query = " SELECT * FROM information_schema.%s " .
" WHERE table_name='%s' " ;
$sql = sprintf ( $query , $infoTable , $table );
if ( $orderBy ) {
$sql .= ' ORDER BY ' . $orderBy ;
2010-01-30 14:45:10 +09:00
}
2010-10-09 08:36:32 +09:00
return $this -> fetchQueryData ( $sql );
2010-01-30 14:45:10 +09:00
}
/**
2010-10-12 06:15:02 +09:00
* Pull some PG - specific index info
* @ param string $table
* @ return array of arrays
*/
function getIndexInfo ( $table )
{
$query = 'SELECT ' .
'(SELECT relname FROM pg_class WHERE oid=indexrelid) AS key_name, ' .
'* FROM pg_index ' .
'WHERE indrelid=(SELECT oid FROM pg_class WHERE relname=\'%s\') ' .
'ORDER BY indrelid, indexrelid' ;
$sql = sprintf ( $query , $table );
return $this -> fetchQueryData ( $sql );
}
/**
*
2010-01-30 14:45:10 +09: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 )
{
$uniques = array ();
$primary = array ();
$indices = array ();
2010-05-17 10:50:37 +09:00
$onupdate = array ();
2010-01-30 14:45:10 +09:00
$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 ) {
case 'UNI' :
$uniques [] = $cd -> name ;
break ;
case 'PRI' :
$primary [] = $cd -> name ;
break ;
case 'MUL' :
$indices [] = $cd -> name ;
break ;
}
}
if ( count ( $primary ) > 0 ) { // it really should be...
2010-05-15 12:31:54 +09:00
$sql .= " , \n PRIMARY KEY ( " . implode ( ',' , $primary ) . " ) " ;
2010-01-30 14:45:10 +09:00
}
$sql .= " ); " ;
2010-03-10 17:54:30 +09:00
foreach ( $uniques as $u ) {
$sql .= " \n CREATE index { $name } _ { $u } _idx ON { $name } ( $u ); " ;
}
2010-05-15 12:14:11 +09:00
foreach ( $indices as $i ) {
2010-05-15 12:31:54 +09:00
$sql .= " CREATE index { $name } _ { $i } _idx ON { $name } ( $i ) " ;
2010-05-15 12:14:11 +09:00
}
2010-01-30 14:45:10 +09:00
$res = $this -> conn -> query ( $sql );
if ( PEAR :: isError ( $res )) {
2010-05-15 11:56:40 +09:00
throw new Exception ( $res -> getMessage () . ' SQL was ' . $sql );
2010-01-30 14:45:10 +09:00
}
return true ;
}
2010-03-10 18:02:56 +09:00
/**
* Translate the ( mostly ) mysql - ish column types into somethings more standard
* @ param string column type
*
* @ return string postgres happy column type
*/
private function _columnTypeTranslation ( $type ) {
$map = array (
2010-05-15 11:56:40 +09:00
'datetime' => 'timestamp' ,
2010-03-10 18:02:56 +09:00
);
if ( ! empty ( $map [ $type ])) {
return $map [ $type ];
}
return $type ;
}
2010-01-30 14:45:10 +09: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
*/
public function modifyColumn ( $table , $columndef )
{
2010-05-17 10:50:37 +09:00
$sql = " ALTER TABLE $table ALTER COLUMN TYPE " .
2010-01-30 14:45:10 +09:00
$this -> _columnSql ( $columndef );
$res = $this -> conn -> query ( $sql );
if ( PEAR :: isError ( $res )) {
throw new Exception ( $res -> getMessage ());
}
return true ;
}
/**
2010-08-17 08:31:18 +09:00
* Return the proper SQL for creating or
* altering a column .
2010-01-30 14:45:10 +09:00
*
2010-08-17 08:31:18 +09:00
* Appropriate for use in CREATE TABLE or
* ALTER TABLE statements .
2010-01-30 14:45:10 +09:00
*
2010-08-17 08:31:18 +09:00
* @ param string $tableName
* @ param array $tableDef
* @ param string $columnName
* @ param array $cd column to create
2010-01-30 14:45:10 +09:00
*
2010-08-17 08:31:18 +09:00
* @ return string correct SQL for that column
2010-01-30 14:45:10 +09:00
*/
2010-08-17 08:31:18 +09:00
function columnSql ( $name , array $cd )
2010-01-30 14:45:10 +09:00
{
2010-08-17 08:31:18 +09:00
$line = array ();
$line [] = parent :: _columnSql ( $cd );
if ( $table [ 'foreign keys' ][ $name ]) {
foreach ( $table [ 'foreign keys' ][ $name ] as $foreignTable => $foreignColumn ) {
$line [] = 'references' ;
$line [] = $this -> quoteId ( $foreignTable );
$line [] = '(' . $this -> quoteId ( $foreignColumn ) . ')' ;
2010-01-30 14:45:10 +09:00
}
}
2010-08-17 08:31:18 +09:00
return implode ( ' ' , $line );
2010-01-30 14:45:10 +09:00
}
2010-10-08 10:33:02 +09:00
/**
* Append phrase ( s ) to an array of partial ALTER TABLE chunks in order
* to alter the given column from its old state to a new one .
*
* @ param array $phrase
* @ param string $columnName
* @ param array $old previous column definition as found in DB
* @ param array $cd current column definition
*/
function appendAlterModifyColumn ( array & $phrase , $columnName , array $old , array $cd )
{
$prefix = 'ALTER COLUMN ' . $this -> quoteIdentifier ( $columnName ) . ' ' ;
$oldType = $this -> mapType ( $old );
$newType = $this -> mapType ( $cd );
if ( $oldType != $newType ) {
2010-10-12 05:05:44 +09:00
$phrase [] = $prefix . 'TYPE ' . $newType ;
2010-10-08 10:33:02 +09:00
}
if ( ! empty ( $old [ 'not null' ]) && empty ( $cd [ 'not null' ])) {
2010-10-12 05:05:44 +09:00
$phrase [] = $prefix . 'DROP NOT NULL' ;
2010-10-08 10:33:02 +09:00
} else if ( empty ( $old [ 'not null' ]) && ! empty ( $cd [ 'not null' ])) {
2010-10-12 05:05:44 +09:00
$phrase [] = $prefix . 'SET NOT NULL' ;
2010-10-08 10:33:02 +09:00
}
if ( isset ( $old [ 'default' ]) && ! isset ( $cd [ 'default' ])) {
2010-10-12 05:05:44 +09:00
$phrase [] = $prefix . 'DROP DEFAULT' ;
2010-10-08 10:33:02 +09:00
} else if ( ! isset ( $old [ 'default' ]) && isset ( $cd [ 'default' ])) {
2010-10-12 05:05:44 +09:00
$phrase [] = $prefix . 'SET DEFAULT ' . $this -> quoteDefaultValue ( $cd );
2010-10-08 10:33:02 +09:00
}
}
/**
* Quote a db / table / column identifier if necessary .
*
* @ param string $name
* @ return string
*/
function quoteIdentifier ( $name )
{
return '"' . $name . '"' ;
}
2010-08-17 08:31:18 +09:00
function mapType ( $column )
2010-01-30 14:45:10 +09:00
{
2010-08-17 08:31:18 +09:00
$map = array ( 'serial' => 'bigserial' , // FIXME: creates the wrong name for the sequence for some internal sequence-lookup function, so better fix this to do the real 'create sequence' dance.
'numeric' => 'decimal' ,
'datetime' => 'timestamp' ,
'blob' => 'bytea' );
$type = $column [ 'type' ];
if ( isset ( $map [ $type ])) {
$type = $map [ $type ];
2010-05-15 11:56:40 +09:00
}
2010-08-17 08:31:18 +09:00
if ( ! empty ( $column [ 'size' ])) {
$size = $column [ 'size' ];
if ( $type == 'integer' &&
in_array ( $size , array ( 'small' , 'big' ))) {
$type = $size . 'int' ;
}
2010-01-30 14:45:10 +09:00
}
2010-08-17 08:31:18 +09:00
return $type ;
}
// @fixme need name... :P
function typeAndSize ( $column )
{
if ( $column [ 'type' ] == 'enum' ) {
$vals = array_map ( array ( $this , 'quote' ), $column [ 'enum' ]);
return " text check ( $name in " . implode ( ',' , $vals ) . ')' ;
2010-01-30 14:45:10 +09:00
} else {
2010-08-17 08:31:18 +09:00
return parent :: typeAndSize ( $column );
2010-01-30 14:45:10 +09:00
}
}
2010-08-17 08:31:18 +09:00
2010-10-09 08:36:32 +09:00
/**
* Map a native type back to an independent type + size
*
* @ param string $type
* @ return array ( $type , $size ) -- $size may be null
*/
protected function reverseMapType ( $type )
{
$type = strtolower ( $type );
$map = array (
'int4' => array ( 'int' , null ),
'int8' => array ( 'int' , 'big' ),
'bytea' => array ( 'blob' , null ),
);
if ( isset ( $map [ $type ])) {
return $map [ $type ];
} else {
return array ( $type , null );
}
}
2010-01-30 14:45:10 +09:00
}