Refactored DB Forge

- PDO subdrivers are isolated from each other now.
- Added compatibility for pretty much all of the features, for every DB platform.
- Unified the way that stuff works in general.
- Fixes issue #1005.
diff --git a/system/database/drivers/postgre/postgre_forge.php b/system/database/drivers/postgre/postgre_forge.php
index b08fa51..ad26763 100644
--- a/system/database/drivers/postgre/postgre_forge.php
+++ b/system/database/drivers/postgre/postgre_forge.php
@@ -36,189 +36,150 @@
 class CI_DB_postgre_forge extends CI_DB_forge {
 
 	/**
-	 * DROP TABLE statement
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'INT2'		=> 'INTEGER',
+		'SMALLINT'	=> 'INTEGER',
+		'INT'		=> 'BIGINT',
+		'INT4'		=> 'BIGINT',
+		'INTEGER'	=> 'BIGINT',
+		'INT8'		=> 'NUMERIC',
+		'BIGINT'	=> 'NUMERIC',
+		'REAL'		=> 'DOUBLE PRECISION',
+		'FLOAT'		=> 'DOUBLE PRECISION'
+	);
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
 	 *
 	 * @var	string
 	 */
-	protected $_drop_table	= 'DROP TABLE IF EXISTS %s CASCADE';
+	protected $_null		= 'NULL';
 
 	// --------------------------------------------------------------------
 
 	/**
-	 * Process Fields
+	 * Class constructor
 	 *
-	 * @param	mixed	$fields
-	 * @param	array	$primary_keys
-	 * @return	string
+	 * @return	void
 	 */
-	protected function _process_fields($fields, $primary_keys = array())
+	public function __construct()
 	{
-		$sql = '';
-		$current_field_count = 0;
+		parent::__construct();
 
-		foreach ($fields as $field => $attributes)
+		if (version_compare($this->db->version(), '9.0', '>'))
 		{
-			// Numeric field names aren't allowed in databases, so if the key is
-			// numeric, we know it was assigned by PHP and the developer manually
-			// entered the field information, so we'll simply add it to the list
-			if (is_numeric($field))
-			{
-				$sql .= "\n\t".$attributes;
-			}
-			else
-			{
-				$sql .= "\n\t".$this->db->escape_identifiers($field);
-
-				$attributes = array_change_key_case($attributes, CASE_UPPER);
-				$is_unsigned = ( ! empty($attributes['UNSIGNED']) && $attributes['UNSIGNED'] === TRUE);
-
-				// Convert datatypes to be PostgreSQL-compatible
-				switch (strtoupper($attributes['TYPE']))
-				{
-					case 'TINYINT':
-						$attributes['TYPE'] = 'SMALLINT';
-						break;
-					case 'SMALLINT':
-						$attributes['TYPE'] = ($is_unsigned) ? 'INTEGER' : 'SMALLINT';
-						break;
-					case 'MEDIUMINT':
-						$attributes['TYPE'] = 'INTEGER';
-						break;
-					case 'INT':
-						$attributes['TYPE'] = ($is_unsigned) ? 'BIGINT' : 'INTEGER';
-						break;
-					case 'BIGINT':
-						$attributes['TYPE'] = ($is_unsigned) ? 'NUMERIC' : 'BIGINT';
-						break;
-					case 'DOUBLE':
-						$attributes['TYPE'] = 'DOUBLE PRECISION';
-						break;
-					case 'DATETIME':
-						$attributes['TYPE'] = 'TIMESTAMP';
-						break;
-					case 'LONGTEXT':
-						$attributes['TYPE'] = 'TEXT';
-						break;
-					case 'BLOB':
-						$attributes['TYPE'] = 'BYTEA';
-						break;
-					default:
-						break;
-				}
-
-				// If this is an auto-incrementing primary key, use the serial data type instead
-				$sql .= (in_array($field, $primary_keys) && ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE)
-					? ' SERIAL' : ' '.$attributes['TYPE'];
-
-				// Modified to prevent constraints with integer data types
-				if ( ! empty($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') === FALSE)
-				{
-					$sql .= '('.$attributes['CONSTRAINT'].')';
-				}
-
-				if (isset($attributes['DEFAULT']))
-				{
-					$sql .= " DEFAULT '".$attributes['DEFAULT']."'";
-				}
-
-				$sql .= ( ! empty($attributes['NULL']) && $attributes['NULL'] === TRUE)
-					? ' NULL' : ' NOT NULL';
-
-				// Added new attribute to create unique fields. Also works with MySQL
-				if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE)
-				{
-					$sql .= ' UNIQUE';
-				}
-			}
-
-			// don't add a comma on the end of the last field
-			if (++$current_field_count < count($fields))
-			{
-				$sql .= ',';
-			}
+			$this->create_table_if = 'CREATE TABLE IF NOT EXISTS';
 		}
-
-		return $sql;
 	}
 
 	// --------------------------------------------------------------------
 
 	/**
-	 * Create Table
+	 * ALTER TABLE
 	 *
-	 * @param	string	the table name
-	 * @param	array	the fields
-	 * @param	mixed	primary key(s)
-	 * @param	mixed	key(s)
-	 * @param	bool	should 'IF NOT EXISTS' be added to the SQL
-	 * @return	bool
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
 	 */
-	protected function _create_table($table, $fields, $primary_keys, $keys, $if_not_exists)
-	{
-		$sql = 'CREATE TABLE ';
-
-		// PostgreSQL doesn't support IF NOT EXISTS syntax so we check if table exists manually
-		if ($if_not_exists === TRUE && $this->db->table_exists($table))
-		{
-			return TRUE;
-		}
-
-		$sql .= $this->db->escape_identifiers($table).' ('.$this->_process_fields($fields, $primary_keys);
-
-		if (count($primary_keys) > 0)
-		{
-			$sql .= ",\n\tPRIMARY KEY (".implode(', ', $this->db->escape_identifiers($primary_keys)).')';
-		}
-
-		$sql .= "\n);";
-
-		if (is_array($keys) && count($keys) > 0)
-		{
-			foreach ($keys as $key)
-			{
-				$key = is_array($key)
-					? $this->db->escape_identifiers($key)
-					: array($this->db->escape_identifiers($key));
-
-				foreach ($key as $field)
-				{
-					$sql .= "\nCREATE INDEX ".$this->db->escape_identifiers($table.'_'.str_replace(array('"', "'"), '', $field).'_index')
-						.' ON '.$this->db->escape_identifiers($table).' ('.$this->db->escape_identifiers($field).');';
-				}
-			}
-		}
-
-		return $sql;
-	}
-
-	// --------------------------------------------------------------------
-
-	/**
-	 * Alter table query
-	 *
-	 * Generates a platform-specific query so that a table can be altered
-	 * Called by add_column(), drop_column(), and column_alter(),
-	 *
-	 * @param	string	$alter_type	the ALTER type (ADD, DROP, CHANGE)
-	 * @param	string	$table		the table name
-	 * @param	string	$fields		the column definition
-	 * @param	string	$after_field
-	 * @return	string
-	 */
-	protected function _alter_table($alter_type, $table, $fields, $after_field = '')
+	protected function _alter_table($alter_type, $table, $field)
  	{
- 		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' '.$alter_type.' ';
+		if (in_array($alter_type, array('DROP', 'ADD'), TRUE))
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
 
- 		// DROP has everything it needs now.
- 		if ($alter_type === 'DROP')
- 		{
- 			return $sql.$this->db->escape_identifiers($fields);
- 		}
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table);
+		$sqls = array();
+		for ($i = 0, $c = count($field), $sql .= $alter_type.' '; $i < $c; $i++)
+		{
+			if ($field[$i]['_literal'] !== FALSE)
+			{
+				return FALSE;
+			}
 
- 		return $sql.$this->_process_fields($fields)
-			.($after_field !== '' ? ' AFTER '.$this->db->escape_identifiers($after_field) : '');
+			if (version_compare($this->db->version(), '8', '>=') && isset($field[$i]['type']))
+			{
+				$sqls[] = $sql.' TYPE '.$field[$i]['type'].$field[$i]['length'];
+			}
+
+			if ( ! empty($field[$i]['default']))
+			{
+				$sqls[] = $sql.' ALTER '.$this->db->escape_identifiers($field[$i]['name'])
+					.' SET '.$field[$i]['default'];
+			}
+
+			if (isset($field[$i]['null']))
+			{
+				$sqls[] = $sql.' ALTER '.$this->db->escape_identifiers($field[$i]['name'])
+					.($field[$i]['null'] === TRUE ? ' DROP NOT NULL' : ' SET NOT NULL');
+			}
+
+			if ( ! empty($field[$i]['new_name']))
+			{
+				$sqls[] = $sql.' RENAME '.$this->db->escape_identifiers($field[$i]['name'])
+					.' TO '.$this->db->escape_identifiers($field[$i]['new_name']);
+			}
+		}
+
+		return $sqls;
  	}
 
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		// Reset field lenghts for data types that don't support it
+		if (isset($attributes['CONSTRAINT']) && stripos($attributes['TYPE'], 'int') !== FALSE)
+		{
+			$attributes['CONSTRAINT'] = NULL;
+		}
+
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'TINYINT':
+				$attributes['TYPE'] = 'SMALLINT';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE)
+		{
+			$field['type'] = ($field['type'] === 'NUMERIC')
+						? 'BIGSERIAL'
+						: 'SERIAL';
+		}
+	}
+
 }
 
 /* End of file postgre_forge.php */