]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/pgsqlschema.php
4e24da7501b68f9500eb95f80ef3d1e20e331841
[quix0rs-gnu-social.git] / lib / pgsqlschema.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * Database schema utilities
6  *
7  * PHP version 5
8  *
9  * LICENCE: This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU Affero General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU Affero General Public License for more details.
18  *
19  * You should have received a copy of the GNU Affero General Public License
20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  * @category  Database
23  * @package   StatusNet
24  * @author    Evan Prodromou <evan@status.net>
25  * @copyright 2009 StatusNet, Inc.
26  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
27  * @link      http://status.net/
28  */
29
30 if (!defined('STATUSNET')) {
31     exit(1);
32 }
33
34 /**
35  * Class representing the database schema
36  *
37  * A class representing the database schema. Can be used to
38  * manipulate the schema -- especially for plugins and upgrade
39  * utilities.
40  *
41  * @category Database
42  * @package  StatusNet
43  * @author   Evan Prodromou <evan@status.net>
44  * @author   Brenda Wallace <shiny@cpan.org>
45  * @author   Brion Vibber <brion@status.net>
46  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
47  * @link     http://status.net/
48  */
49
50 class PgsqlSchema extends Schema
51 {
52
53     /**
54      * Returns a table definition array for the table
55      * in the schema with the given name.
56      *
57      * Throws an exception if the table is not found.
58      *
59      * @param string $table Name of the table to get
60      *
61      * @return array tabledef for that table.
62      */
63
64     public function getTableDef($table)
65     {
66         $def = array();
67         $hasKeys = false;
68
69         // Pull column data from INFORMATION_SCHEMA
70         $columns = $this->fetchMetaInfo($table, 'columns', 'ordinal_position');
71         if (count($columns) == 0) {
72             throw new SchemaTableMissingException("No such table: $table");
73         }
74
75         // We'll need to match up fields by ordinal reference
76         $orderedFields = array();
77
78         foreach ($columns as $row) {
79
80             $name = $row['column_name'];
81             $orderedFields[$row['ordinal_position']] = $name;
82
83             $field = array();
84
85             // ??
86             list($type, $size) = $this->reverseMapType($row['udt_name']);
87             $field['type'] = $type;
88             if ($size !== null) {
89                 $field['size'] = $size;
90             }
91
92             if ($type == 'char' || $type == 'varchar') {
93                 if ($row['character_maximum_length'] !== null) {
94                     $field['length'] = intval($row['character_maximum_length']);
95                 }
96             }
97             if ($type == 'numeric') {
98                 // Other int types may report these values, but they're irrelevant.
99                 // Just ignore them!
100                 if ($row['numeric_precision'] !== null) {
101                     $field['precision'] = intval($row['numeric_precision']);
102                 }
103                 if ($row['numeric_scale'] !== null) {
104                     $field['scale'] = intval($row['numeric_scale']);
105                 }
106             }
107             if ($row['is_nullable'] == 'NO') {
108                 $field['not null'] = true;
109             }
110             if ($row['column_default'] !== null) {
111                 $field['default'] = $row['column_default'];
112                 if ($this->isNumericType($type)) {
113                     $field['default'] = intval($field['default']);
114                 }
115             }
116
117             $def['fields'][$name] = $field;
118         }
119
120         // Pulling index info from pg_class & pg_index
121         // This can provide us basic info on primary, unique, and multi-val keys
122         // But... it doesn't list plain constraints or foreign key constraints. :P
123         $indexInfo = $this->getIndexInfo($table);
124         foreach ($indexInfo as $row) {
125             $keyName = $row['key_name'];
126
127             // Dig the column references out!
128             $cols = array();
129             $colPositions = explode(' ', $row['indkey']);
130             foreach ($colPositions as $ord) {
131                 // ordinal_position from above is 1-based
132                 // but values in indkey are 0-based
133                 if ($ord == 0) {
134                     $cols[] = 'FUNCTION'; // @fixme
135                 } else {
136                     $cols[] = $orderedFields[$ord];
137                 }
138             }
139
140             // @fixme foreign keys?
141             // @fixme prefixes?
142             // @fixme funky stuff like fulltext?
143             if ($row['indisprimary'] == 't') {
144                 $def['primary key'] = $cols;
145             } else if ($row['indisunique'] == 't') {
146                 $def['unique keys'][$keyName] = $cols;
147             } else {
148                 $def['indexes'][$keyName] = $cols;
149             }
150         }
151
152         // Pull constraint data from INFORMATION_SCHEMA
153         // @fixme also find multi-val indexes
154         // @fixme distinguish the primary key
155         // @fixme pull foreign key references
156         $keyColumns = $this->fetchMetaInfo($table, 'key_column_usage', 'constraint_name,ordinal_position');
157         $keys = array();
158
159         foreach ($keyColumns as $row) {
160             var_dump($row);
161             $keyName = $row['constraint_name'];
162             $keyCol = $row['column_name'];
163             if (!isset($keys[$keyName])) {
164                 $keys[$keyName] = array();
165             }
166             $keys[$keyName][] = $keyCol;
167         }
168
169         foreach ($keys as $keyName => $cols) {
170             // hack -- is this reliable?
171             if ($keyName == "{$table}_pkey") {
172                 $def['xprimary key'] = $cols;
173             } else if (preg_match("/^{$table}_(.*)_fkey$/", $keyName, $matches)) {
174                 $keys = array_keys($cols);
175                 if (count($cols) == 1 && $cols[$keys[0]] == $keyName) {
176                     $def['foreign keys'][$keyname][$matches[1]] = $keys[0];
177                 }
178                 $def['foreign keys'][$keyName][$matches[1]] = $cols;
179             } else {
180                 $def['xunique indexes'][$keyName] = $cols;
181             }
182         }
183         return $def;
184     }
185
186     /**
187      * Pull some INFORMATION.SCHEMA data for the given table.
188      *
189      * @param string $table
190      * @return array of arrays
191      */
192     function fetchMetaInfo($table, $infoTable, $orderBy=null)
193     {
194         $query = "SELECT * FROM information_schema.%s " .
195                  "WHERE table_name='%s'";
196         $sql = sprintf($query, $infoTable, $table);
197         if ($orderBy) {
198             $sql .= ' ORDER BY ' . $orderBy;
199         }
200         return $this->fetchQueryData($sql);
201     }
202
203     /**
204      * Pull some PG-specific index info
205      * @param string $table
206      * @return array of arrays
207      */
208     function getIndexInfo($table)
209     {
210         $query = 'SELECT ' .
211                  '(SELECT relname FROM pg_class WHERE oid=indexrelid) AS key_name, ' .
212                  '* FROM pg_index ' .
213                  'WHERE indrelid=(SELECT oid FROM pg_class WHERE relname=\'%s\') ' .
214                  'ORDER BY indrelid, indexrelid';
215         $sql = sprintf($query, $table);
216         return $this->fetchQueryData($sql);
217     }
218
219     /**
220      *
221      * Creates a table with the given names and columns.
222      *
223      * @param string $name    Name of the table
224      * @param array  $columns Array of ColumnDef objects
225      *                        for new table.
226      *
227      * @return boolean success flag
228      */
229
230     public function createTable($name, $columns)
231     {
232         $uniques = array();
233         $primary = array();
234         $indices = array();
235         $onupdate = array();
236
237         $sql = "CREATE TABLE $name (\n";
238
239         for ($i = 0; $i < count($columns); $i++) {
240
241             $cd =& $columns[$i];
242
243             if ($i > 0) {
244                 $sql .= ",\n";
245             }
246
247             $sql .= $this->_columnSql($cd);
248             switch ($cd->key) {
249             case 'UNI':
250                 $uniques[] = $cd->name;
251                 break;
252             case 'PRI':
253                 $primary[] = $cd->name;
254                 break;
255             case 'MUL':
256                 $indices[] = $cd->name;
257                 break;
258             }
259         }
260
261         if (count($primary) > 0) { // it really should be...
262             $sql .= ",\n PRIMARY KEY (" . implode(',', $primary) . ")";
263         }
264
265         $sql .= "); ";
266
267
268         foreach ($uniques as $u) {
269             $sql .= "\n CREATE index {$name}_{$u}_idx ON {$name} ($u); ";
270         }
271
272         foreach ($indices as $i) {
273             $sql .= "CREATE index {$name}_{$i}_idx ON {$name} ($i)";
274         }
275         $res = $this->conn->query($sql);
276
277         if (PEAR::isError($res)) {
278             throw new Exception($res->getMessage(). ' SQL was '. $sql);
279         }
280
281         return true;
282     }
283
284     /**
285      * Translate the (mostly) mysql-ish column types into somethings more standard
286      * @param string column type
287      *
288      * @return string postgres happy column type
289      */
290     private function _columnTypeTranslation($type) {
291       $map = array(
292       'datetime' => 'timestamp',
293       );
294       if(!empty($map[$type])) {
295         return $map[$type];
296       }
297       return $type;
298     }
299
300     /**
301      * Modifies a column in the schema.
302      *
303      * The name must match an existing column and table.
304      *
305      * @param string    $table     name of the table
306      * @param ColumnDef $columndef new definition of the column.
307      *
308      * @return boolean success flag
309      */
310
311     public function modifyColumn($table, $columndef)
312     {
313         $sql = "ALTER TABLE $table ALTER COLUMN TYPE " .
314           $this->_columnSql($columndef);
315
316         $res = $this->conn->query($sql);
317
318         if (PEAR::isError($res)) {
319             throw new Exception($res->getMessage());
320         }
321
322         return true;
323     }
324
325     /**
326      * Return the proper SQL for creating or
327      * altering a column.
328      *
329      * Appropriate for use in CREATE TABLE or
330      * ALTER TABLE statements.
331      *
332      * @param string $tableName
333      * @param array $tableDef
334      * @param string $columnName
335      * @param array $cd column to create
336      *
337      * @return string correct SQL for that column
338      */
339
340     function columnSql($name, array $cd)
341     {
342         $line = array();
343         $line[] = parent::_columnSql($cd);
344
345         if ($table['foreign keys'][$name]) {
346             foreach ($table['foreign keys'][$name] as $foreignTable => $foreignColumn) {
347                 $line[] = 'references';
348                 $line[] = $this->quoteId($foreignTable);
349                 $line[] = '(' . $this->quoteId($foreignColumn) . ')';
350             }
351         }
352
353         return implode(' ', $line);
354     }
355
356     /**
357      * Append phrase(s) to an array of partial ALTER TABLE chunks in order
358      * to alter the given column from its old state to a new one.
359      *
360      * @param array $phrase
361      * @param string $columnName
362      * @param array $old previous column definition as found in DB
363      * @param array $cd current column definition
364      */
365     function appendAlterModifyColumn(array &$phrase, $columnName, array $old, array $cd)
366     {
367         $prefix = 'ALTER COLUMN ' . $this->quoteIdentifier($columnName) . ' ';
368
369         $oldType = $this->mapType($old);
370         $newType = $this->mapType($cd);
371         if ($oldType != $newType) {
372             $phrase[] = $prefix . 'TYPE ' . $newType;
373         }
374
375         if (!empty($old['not null']) && empty($cd['not null'])) {
376             $phrase[] = $prefix . 'DROP NOT NULL';
377         } else if (empty($old['not null']) && !empty($cd['not null'])) {
378             $phrase[] = $prefix . 'SET NOT NULL';
379         }
380
381         if (isset($old['default']) && !isset($cd['default'])) {
382             $phrase[] = $prefix . 'DROP DEFAULT';
383         } else if (!isset($old['default']) && isset($cd['default'])) {
384             $phrase[] = $prefix . 'SET DEFAULT ' . $this->quoteDefaultValue($cd);
385         }
386     }
387
388     /**
389      * Quote a db/table/column identifier if necessary.
390      *
391      * @param string $name
392      * @return string
393      */
394     function quoteIdentifier($name)
395     {
396         return '"' . $name . '"';
397     }
398
399     function mapType($column)
400     {
401         $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.
402                      'numeric' => 'decimal',
403                      'datetime' => 'timestamp',
404                      'blob' => 'bytea');
405
406         $type = $column['type'];
407         if (isset($map[$type])) {
408             $type = $map[$type];
409         }
410
411         if (!empty($column['size'])) {
412             $size = $column['size'];
413             if ($type == 'integer' &&
414                        in_array($size, array('small', 'big'))) {
415                 $type = $size . 'int';
416             }
417         }
418
419         return $type;
420     }
421
422     // @fixme need name... :P
423     function typeAndSize($column)
424     {
425         if ($column['type'] == 'enum') {
426             $vals = array_map(array($this, 'quote'), $column['enum']);
427             return "text check ($name in " . implode(',', $vals) . ')';
428         } else {
429             return parent::typeAndSize($column);
430         }
431     }
432
433     /**
434      * Map a native type back to an independent type + size
435      *
436      * @param string $type
437      * @return array ($type, $size) -- $size may be null
438      */
439     protected function reverseMapType($type)
440     {
441         $type = strtolower($type);
442         $map = array(
443             'int4' => array('int', null),
444             'int8' => array('int', 'big'),
445             'bytea' => array('blob', null),
446         );
447         if (isset($map[$type])) {
448             return $map[$type];
449         } else {
450             return array($type, null);
451         }
452     }
453
454 }