46fb30062211c890b0d15f3b163d7194726793cc
[tine20] / tine20 / Setup / Backend / Pgsql.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Setup
6  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
7  * @author      Fl├ívio Gomes da Silva Lisboa <flavio.lisboa@serpro.gov.br>
8  * @copyright   Copyright (c) 2011-2011 Metaways Infosystems GmbH (http://www.metaways.de)
9  *
10  */
11
12 /**
13  * setup backend class for PostgreSQL 8.3 +
14  * based on class Setup_Backend_Mysql
15  * @package     Setup
16  */
17 class Setup_Backend_Pgsql extends Setup_Backend_Abstract
18 {
19         /**
20          * Define how database agnostic data types get mapped to postgresql data types
21          * @todo reviews data type
22          * @var array
23          */
24         protected $_typeMappings = array(
25         'integer' => array(
26                 'lengthTypes' => array(
27                                 4 => 'smallint',
28                                 19 => 'integer',
29                                 64 => 'bigint'),
30             'defaultType' => 'integer',
31                 'defaultLength' => self::INTEGER_DEFAULT_LENGTH),            
32         'boolean' => array(
33             'defaultType' => 'NUMERIC',
34             'defaultScale' => 0,
35             'defaultLength' => 1),
36                 'text' => array(
37             'lengthTypes' => array(
38                                 256 => 'character varying', //@todo this should be 255 indeed but we have 256 in our setup.xml files
39                                 65535 => 'character varying',
40                                 16777215 => 'character varying',
41                                 4294967295 => 'character varying'),
42             'defaultType' => 'text',
43             'defaultLength' => null),
44         'float' => array(
45             'defaultType' => 'double precision',
46                 'defaultLength' => null ),
47         'decimal' => array(
48             'defaultType' => 'numeric',
49                 'defaultLength' => null ),
50         'datetime' => array(
51             'defaultType' => 'timestamp with time zone',
52                 'defaultLength' => null ),
53         'time' => array(
54             'defaultType' => 'time with timezone',
55                 'defaultLength' => null ),
56         'date' => array(
57             'defaultType' => 'date',
58                 'defaultLength' => null ),
59         'blob' => array(
60             'defaultType' => 'text',
61                 'defaultLength' => null ),
62         'clob' => array(
63             'defaultType' => 'text',
64                 'defaultLength' => null ),
65         'enum' => array(
66             'defaultType' => 'enum',
67                         'defaultLength' => null     )
68         );
69         
70         public function getFalseValue()
71         {
72                 return 'false';
73         }
74         
75         public function getTrueValue()
76         {
77                 return 'true';
78         }       
79         
80         /**
81          * Generates an SQL CREATE STATEMENT
82          * @param Setup_Backend_Schema_Table_Abstract $_table
83          * @return array CREATE TABLE statement, enum types, indexes
84          * @throws Setup_Exception_NotFound
85          */
86         public function getCreateStatement(Setup_Backend_Schema_Table_Abstract  $_table)
87         {
88                 
89                 $enums = array();
90                 $statement = "CREATE TABLE " . SQL_TABLE_PREFIX . $_table->name . " (\n";
91                 $statementSnippets = array();
92                  
93                 foreach ($_table->fields as $field) {
94                         if (isset($field->name)) {
95                                 // getFieldDeclarations() adds 'unsigned' that it doesn't exist in PortgreSQL
96                                 if (isset($field->unsigned)) $field->unsigned = false;
97                                 $fieldDeclarations = $this->getFieldDeclarations($field,$_table->name);
98                                 // identifies enum types and creates declaration
99                                 $position = strpos($fieldDeclarations,'enum');
100                                 if ($position !== false)
101                                 {
102                                         $enums = $this->_getCreateTypeStatements($field,$fieldDeclarations,$_table->name, $position, $enums);                                   
103                                         // replaces enum function with enum type
104                                         $fieldDeclarations = substr($fieldDeclarations, 0, $position) . $this->_getLastEnumType($enums) . ' NOT NULL';
105                                 }
106                                 // removes length of integer between parenthesis 
107                                 $fieldDeclarations = preg_replace('/integer\([0-9][0-9]\)/', 'integer', $fieldDeclarations);
108                                 $fieldDeclarations = preg_replace('/smallint\([0-9][0-9]\)/', 'smallint', $fieldDeclarations);
109                                 $fieldDeclarations = preg_replace('/bigint\([0-9][0-9]\)/', 'bigint', $fieldDeclarations);
110                                 // replaces integer auto_increment with serial
111                                 $fieldDeclarations = str_replace('integer NOT NULL auto_increment', 'serial', $fieldDeclarations);
112                                 $statementSnippets[] = $fieldDeclarations;
113                         }
114                 }
115
116                 $createIndexStatement = '';
117                 
118                 foreach ($_table->indices as $index) {
119                         if ($index->foreign) {
120                                 $statementSnippets[] = $this->getForeignKeyDeclarations($index);
121                         } else {
122                                 $statementSnippet = $this->getIndexDeclarations($index,$_table->name);
123                                 if (strpos($statementSnippet, 'CREATE INDEX')!==false)
124                                 {
125                                         $createIndexStatement = $statementSnippet;
126                                 }
127                                 else
128                                 {
129                                         $statementSnippets[] = $statementSnippet;
130                                 }
131                         }
132                 }
133
134                 $statement .= implode(",\n", $statementSnippets) . "\n)";
135
136                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . print_r($enums,true) . "\n" .  $statement . "\n" . $createIndexStatement);
137
138                 return array($statement,$enums,$createIndexStatement);
139         }
140         
141         /**
142          * 
143          * Considers an associated array with one single dimension 
144          * @param array $associatedArray
145          */
146         private function _getLastEnumType(array $associatedArray)
147         {
148                 $values = array_keys($associatedArray);
149                 $values = $values[count($values)-1];
150                 return $values;
151         }
152         
153         /**
154          *      /**
155          * 
156          * creates CREATE TYPE statement for enum types
157          * @param string $field object with info about table field
158          * @param string $fieldDeclarations original field declaration
159          * @param string $tableName table name
160          * @param string $endOfEnum position in $fieldDeclarations where begins enumeration 
161          * @param array $enums
162          */
163         private function _getCreateTypeStatements($field, $fieldDeclarations,$tableName, $endOfEnum, array $enums)
164         {
165                 $createTypeStatement = 'CREATE TYPE ' . $tableName . '_enum_' . $field->name . ' AS '. substr($fieldDeclarations, $endOfEnum);
166                 $enumType = $tableName . '_enum_' . $field->name;
167                 $strlength = strpos($createTypeStatement,')');
168                 $createTypeStatement = substr($createTypeStatement, 0, $strlength + 1) . ';';
169                 $enums[$enumType] = $createTypeStatement;
170                 return $enums;
171         }
172
173         /**
174          * Get schema of existing table
175          *
176          * @param String $_tableName
177          *
178          * @return Setup_Backend_Schema_Table_Pgsql
179          */
180         public function getExistingSchema($_tableName)
181         {
182                 // Get common table information
183                 $select = $this->_db->select()
184                 ->from('information_schema.tables')
185                 ->where($this->_db->quoteIdentifier('TABLE_SCHEMA') . ' = ?', $this->_config->database->dbname)
186                 ->where($this->_db->quoteIdentifier('TABLE_NAME') . ' = ?',  SQL_TABLE_PREFIX . $_tableName);
187
188
189                 $stmt = $select->query();
190                 $tableInfo = $stmt->fetchObject();
191
192                 //$existingTable = new Setup_Backend_Schema_Table($tableInfo);
193                 $existingTable = Setup_Backend_Schema_Table_Factory::factory('Pgsql', $tableInfo);
194                 // get field informations
195                 $select = $this->_db->select()
196                 ->from('information_schema.COLUMNS')
197                 ->where($this->_db->quoteIdentifier('TABLE_NAME') . ' = ?', SQL_TABLE_PREFIX .  $_tableName);
198
199                 $stmt = $select->query();
200                 $tableColumns = $stmt->fetchAll();
201
202                 foreach ($tableColumns as $tableColumn) {
203                         $field = Setup_Backend_Schema_Field_Factory::factory('Pgsql', $tableColumn);
204                         $existingTable->addField($field);
205
206                         if ($field->primary === 'true' || $field->unique === 'true' || $field->mul === 'true') {
207                                 $index = Setup_Backend_Schema_Index_Factory::factory('Pgsql', $tableColumn);
208
209                                 // get foreign keys
210                                 $select = $this->_db->select()
211                                 ->from('information_schema.KEY_COLUMN_USAGE')
212                                 ->where($this->_db->quoteIdentifier('TABLE_NAME') . ' = ?', SQL_TABLE_PREFIX .  $_tableName)
213                                 ->where($this->_db->quoteIdentifier('COLUMN_NAME') . ' = ?', $tableColumn['COLUMN_NAME']);
214
215                                 $stmt = $select->query();
216                                 $keyUsage = $stmt->fetchAll();
217
218                                 foreach ($keyUsage as $keyUse) {
219                                         if ($keyUse['REFERENCED_TABLE_NAME'] != NULL) {
220                                                 $index->setForeignKey($keyUse);
221                                         }
222                                 }
223                                 $existingTable->addIndex($index);
224                         }
225                 }
226
227                 return $existingTable;
228         }
229
230         /**
231          * add column/field to database table
232          *
233          * @param string tableName
234          * @param Setup_Backend_Schema_Field_Abstract declaration
235          * @param int position of future column
236          */
237         public function addCol($_tableName, Setup_Backend_Schema_Field_Abstract $_declaration, $_position = NULL)
238         {
239                 $statement = "ALTER TABLE '" . SQL_TABLE_PREFIX . $_tableName . "' ADD COLUMN " ;
240
241                 $statement .= $this->getFieldDeclarations($_declaration);
242
243                 if ($_position !== NULL) {
244                         if ($_position == 0) {
245                                 $statement .= ' FIRST ';
246                         } else {
247                                 $before = $this->execQuery('DESCRIBE \'' . SQL_TABLE_PREFIX . $_tableName . '\' ');
248                                 $statement .= ' AFTER \'' . $before[$_position]['Field'] . '\'';
249                         }
250                 }
251
252                 $this->execQueryVoid($statement);
253         }
254
255         /**
256          * rename or redefines column/field in database table
257          *
258          * @param string tableName
259          * @param Setup_Backend_Schema_Field_Abstract declaration
260          * @param string old column/field name
261          */
262         public function alterCol($_tableName, Setup_Backend_Schema_Field_Abstract $_declaration, $_oldName = NULL)
263         {
264                 $statement = "ALTER TABLE '" . SQL_TABLE_PREFIX . $_tableName . "' CHANGE COLUMN " ;
265
266                 if ($_oldName === NULL) {
267                         $oldName = $_declaration->name;
268                 } else {
269                         $oldName = $_oldName;
270                 }
271
272                 $statement .= " '" . $oldName .  "' " . $this->getFieldDeclarations($_declaration);
273                 $this->execQueryVoid($statement);
274         }
275
276         /**
277          * add a key to database table
278          *
279          * @param string tableName
280          * @param Setup_Backend_Schema_Index_Abstract declaration
281          */
282         public function addIndex($_tableName ,  Setup_Backend_Schema_Index_Abstract $_declaration)
283         {
284                 $statement = "ALTER TABLE '" . SQL_TABLE_PREFIX . $_tableName . "' ADD "
285                 . $this->getIndexDeclarations($_declaration);
286                 $this->execQueryVoid($statement);
287         }
288
289         /**
290          * create the right pgsql-statement-snippet for keys.
291          * return constraints to add to create table statement or
292          * return create index statement
293          * @param   Setup_Backend_Schema_Index_Abstract $_key
294          * @param String | optional $_tableName [is not used in this Backend (PgSQL)]
295          * @return  string
296          * @throws  Setup_Exception_NotFound
297          */
298         public function getIndexDeclarations(Setup_Backend_Schema_Index_Abstract $_key, $_tableName = '')
299         {
300                 $isNotIndex = false;
301                 
302                 $keys = array();
303
304                 $indexes = str_replace('-', ',', $_key->name);
305                 $snippet = 'CREATE INDEX  ' . $_tableName . '_' . $_key->name . ' ON ' . SQL_TABLE_PREFIX . $_tableName . "($indexes);";
306                 $snippet = str_replace('-', '_', $snippet);
307                 if (!empty($_key->primary)) {
308                         $pkey = $_tableName . '_pkey';
309                         $pkey = str_replace('-', '_', $pkey);
310                         $snippet = " CONSTRAINT $pkey PRIMARY KEY ";
311                         $isNotIndex = true;
312                 } else if (!empty($_key->unique)) {
313                         $unique = $_tableName . '_' . $_key->name . '_' . 'key';
314                         $unique = str_replace('-', '_', $unique);
315                         $snippet = "CONSTRAINT $unique UNIQUE " ;
316                         $isNotIndex = true;
317                 }
318
319                 foreach ($_key->field as $keyfield) {
320                         $key = (string)$keyfield;
321                         $keys[] = $key;
322                 }
323
324                 if (empty($keys)) {
325                         throw new Setup_Exception_NotFound('no keys for index found');
326                 }
327
328                 if ($isNotIndex)
329                 {
330                         $snippet .= ' (' . implode(",", $keys) . ')';
331                 }
332
333                 return $snippet;
334         }
335
336         /**
337          *  create the right mysql-statement-snippet for foreign keys
338          *
339          * @param object $_key the xml index definition
340          * @return string
341          */
342         public function getForeignKeyDeclarations(Setup_Backend_Schema_Index_Abstract $_key)
343         {
344                 $snippet = '  CONSTRAINT ' . SQL_TABLE_PREFIX . $_key->referenceTable . '_' . $_key->field . ' FOREIGN KEY ';
345                 $snippet .= '(' . $_key->field . ") REFERENCES " . SQL_TABLE_PREFIX
346                 . $_key->referenceTable .
347                     " (" . $_key->referenceField . ")";
348
349                 if (!empty($_key->referenceOnDelete)) {
350                         $snippet .= " ON DELETE " . strtoupper($_key->referenceOnDelete);
351                 }
352                 if (!empty($_key->referenceOnUpdate)) {
353                         $snippet .= " ON UPDATE " . strtoupper($_key->referenceOnUpdate);
354                 }               
355                 
356                 return $snippet;
357         }
358
359         /**
360          * enable/disabled foreign key checks
361          *
362          * @param integer|string|boolean $_value
363          */
364         public function setForeignKeyChecks($_value)
365         {
366                 if ($_value == 0 || $_value == 1) {
367                         $this->_db->query("SET FOREIGN_KEY_CHECKS=" . $_value);
368                 }
369         }
370         
371         /**
372      * takes the xml stream and creates a table
373      *
374      * @param object $_table xml stream
375      */ 
376     public function createTable(Setup_Backend_Schema_Table_Abstract  $_table)
377     {
378         // receives an array where 0 is CREATE STATEMENT, 1 are CREATE TYPE statements and 2 is CREATE INDEX statement 
379         $statements = $this->getCreateStatement($_table);
380         
381         try {
382                         // creates enum types before creates declaration
383                         foreach($statements[1] as $enumType => $createTypeStatement)
384                         {
385                                 $this->execQueryVoid('DROP TYPE IF EXISTS '. $enumType . ' CASCADE;');
386                                 $this->execQueryVoid($createTypeStatement);
387                         }
388         
389                 $this->execQueryVoid($statements[0]);
390                 
391                 if (!empty($statements[2])) $this->execQueryVoid($statements[2]);
392         }
393         catch (Exception $e)
394         {
395                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Exception: ' . $e->getMessage() . ' Trace: ' . $e->getTraceAsString());
396         }
397     }
398         
399 }
400