]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/pgsqlschema.php
PG tweak
[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             /*
87             list($type, $size) = $this->reverseMapType($row['udt_name']);
88             $field['type'] = $type;
89             if ($size !== null) {
90                 $field['size'] = $size;
91             }
92              */
93             $field['type'] = $row['udt_name'];
94
95             if ($type == 'char' || $type == 'varchar') {
96                 if ($row['character_maximum_length'] !== null) {
97                     $field['length'] = intval($row['character_maximum_length']);
98                 }
99             }
100             if ($type == 'numeric') {
101                 // Other int types may report these values, but they're irrelevant.
102                 // Just ignore them!
103                 if ($row['numeric_precision'] !== null) {
104                     $field['precision'] = intval($row['numeric_precision']);
105                 }
106                 if ($row['numeric_scale'] !== null) {
107                     $field['scale'] = intval($row['numeric_scale']);
108                 }
109             }
110             if ($row['is_nullable'] == 'NO') {
111                 $field['not null'] = true;
112             }
113             if ($row['column_default'] !== null) {
114                 $field['default'] = $row['column_default'];
115                 if ($this->isNumericType($type)) {
116                     $field['default'] = intval($field['default']);
117                 }
118             }
119
120             $def['fields'][$name] = $field;
121         }
122
123         // Pulling index info from pg_class & pg_index
124         // This can give us primary & unique key info, but not foreign key constraints
125         // so we exclude them and pick them up later.
126         $indexInfo = $this->getIndexInfo($table);
127         foreach ($indexInfo as $row) {
128             $keyName = $row['key_name'];
129
130             // Dig the column references out!
131             //
132             // These are inconvenient arrays with partial references to the
133             // pg_att table, but since we've already fetched up the column
134             // info on the current table, we can look those up locally.
135             $cols = array();
136             $colPositions = explode(' ', $row['indkey']);
137             foreach ($colPositions as $ord) {
138                 if ($ord == 0) {
139                     $cols[] = 'FUNCTION'; // @fixme
140                 } else {
141                     $cols[] = $orderedFields[$ord];
142                 }
143             }
144
145             $def['indexes'][$keyName] = $cols;
146         }
147
148         // Pull constraint data from INFORMATION_SCHEMA:
149         // Primary key, unique keys, foreign keys
150         $keyColumns = $this->fetchMetaInfo($table, 'key_column_usage', 'constraint_name,ordinal_position');
151         $keys = array();
152
153         foreach ($keyColumns as $row) {
154             $keyName = $row['constraint_name'];
155             $keyCol = $row['column_name'];
156             if (!isset($keys[$keyName])) {
157                 $keys[$keyName] = array();
158             }
159             $keys[$keyName][] = $keyCol;
160         }
161
162         foreach ($keys as $keyName => $cols) {
163             // name hack -- is this reliable?
164             if ($keyName == "{$table}_pkey") {
165                 $def['primary key'] = $cols;
166             } else if (preg_match("/^{$table}_(.*)_fkey$/", $keyName, $matches)) {
167                 $fkey = $this->getForeignKeyInfo($table, $keyName);
168                 $colMap = array_combine($cols, $fkey['col_names']);
169                 $def['foreign keys'][$keyName] = array($fkey['table_name'], $colMap);
170             } else {
171                 $def['unique keys'][$keyName] = $cols;
172             }
173         }
174         return $def;
175     }
176
177     /**
178      * Pull some INFORMATION.SCHEMA data for the given table.
179      *
180      * @param string $table
181      * @return array of arrays
182      */
183     function fetchMetaInfo($table, $infoTable, $orderBy=null)
184     {
185         $query = "SELECT * FROM information_schema.%s " .
186                  "WHERE table_name='%s'";
187         $sql = sprintf($query, $infoTable, $table);
188         if ($orderBy) {
189             $sql .= ' ORDER BY ' . $orderBy;
190         }
191         return $this->fetchQueryData($sql);
192     }
193
194     /**
195      * Pull some PG-specific index info
196      * @param string $table
197      * @return array of arrays
198      */
199     function getIndexInfo($table)
200     {
201         $query = 'SELECT ' .
202                  '(SELECT relname FROM pg_class WHERE oid=indexrelid) AS key_name, ' .
203                  '* FROM pg_index ' .
204                  'WHERE indrelid=(SELECT oid FROM pg_class WHERE relname=\'%s\') ' .
205                  'AND indisprimary=\'f\' AND indisunique=\'f\' ' .
206                  'ORDER BY indrelid, indexrelid';
207         $sql = sprintf($query, $table);
208         return $this->fetchQueryData($sql);
209     }
210
211     /**
212      * Column names from the foreign table can be resolved with a call to getTableColumnNames()
213      * @param <type> $table
214      * @return array array of rows with keys: fkey_name, table_name, table_id, col_names (array of strings)
215      */
216     function getForeignKeyInfo($table, $constraint_name)
217     {
218         // In a sane world, it'd be easier to query the column names directly.
219         // But it's pretty hard to work with arrays such as col_indexes in direct SQL here.
220         $query = 'SELECT ' .
221                  '(SELECT relname FROM pg_class WHERE oid=confrelid) AS table_name, ' .
222                  'confrelid AS table_id, ' .
223                  '(SELECT indkey FROM pg_index WHERE indexrelid=conindid) AS col_indexes ' .
224                  'FROM pg_constraint ' .
225                  'WHERE conrelid=(SELECT oid FROM pg_class WHERE relname=\'%s\') ' .
226                  'AND conname=\'%s\' ' .
227                  'AND contype=\'f\'';
228         $sql = sprintf($query, $table, $constraint_name);
229         $data = $this->fetchQueryData($sql);
230         if (count($data) < 1) {
231             throw new Exception("Could not find foreign key " . $constraint_name . " on table " . $table);
232         }
233
234         $row = $data[0];
235         return array(
236             'table_name' => $row['table_name'],
237             'col_names' => $this->getTableColumnNames($row['table_id'], $row['col_indexes'])
238         );
239     }
240
241     /**
242      *
243      * @param int $table_id
244      * @param array $col_indexes
245      * @return array of strings
246      */
247     function getTableColumnNames($table_id, $col_indexes)
248     {
249         $indexes = array_map('intval', explode(' ', $col_indexes));
250         $query = 'SELECT attnum AS col_index, attname AS col_name ' .
251                  'FROM pg_attribute where attrelid=%d ' .
252                  'AND attnum IN (%s)';
253         $sql = sprintf($query, $table_id, implode(',', $indexes));
254         $data = $this->fetchQueryData($sql);
255
256         $byId = array();
257         foreach ($data as $row) {
258             $byId[$row['col_index']] = $row['col_name'];
259         }
260
261         $out = array();
262         foreach ($indexes as $id) {
263             $out[] = $byId[$id];
264         }
265         return $out;
266     }
267
268     /**
269      * Translate the (mostly) mysql-ish column types into somethings more standard
270      * @param string column type
271      *
272      * @return string postgres happy column type
273      */
274     private function _columnTypeTranslation($type) {
275       $map = array(
276       'datetime' => 'timestamp',
277       );
278       if(!empty($map[$type])) {
279         return $map[$type];
280       }
281       return $type;
282     }
283
284     /**
285      * Return the proper SQL for creating or
286      * altering a column.
287      *
288      * Appropriate for use in CREATE TABLE or
289      * ALTER TABLE statements.
290      *
291      * @param array $cd column to create
292      *
293      * @return string correct SQL for that column
294      */
295
296     function columnSql(array $cd)
297     {
298         $line = array();
299         $line[] = parent::columnSql($cd);
300
301         /*
302         if ($table['foreign keys'][$name]) {
303             foreach ($table['foreign keys'][$name] as $foreignTable => $foreignColumn) {
304                 $line[] = 'references';
305                 $line[] = $this->quoteIdentifier($foreignTable);
306                 $line[] = '(' . $this->quoteIdentifier($foreignColumn) . ')';
307             }
308         }
309         */
310
311         return implode(' ', $line);
312     }
313
314     /**
315      * Append phrase(s) to an array of partial ALTER TABLE chunks in order
316      * to alter the given column from its old state to a new one.
317      *
318      * @param array $phrase
319      * @param string $columnName
320      * @param array $old previous column definition as found in DB
321      * @param array $cd current column definition
322      */
323     function appendAlterModifyColumn(array &$phrase, $columnName, array $old, array $cd)
324     {
325         $prefix = 'ALTER COLUMN ' . $this->quoteIdentifier($columnName) . ' ';
326
327         $oldType = $this->mapType($old);
328         $newType = $this->mapType($cd);
329         if ($oldType != $newType) {
330             $phrase[] = $prefix . 'TYPE ' . $newType;
331         }
332
333         if (!empty($old['not null']) && empty($cd['not null'])) {
334             $phrase[] = $prefix . 'DROP NOT NULL';
335         } else if (empty($old['not null']) && !empty($cd['not null'])) {
336             $phrase[] = $prefix . 'SET NOT NULL';
337         }
338
339         if (isset($old['default']) && !isset($cd['default'])) {
340             $phrase[] = $prefix . 'DROP DEFAULT';
341         } else if (!isset($old['default']) && isset($cd['default'])) {
342             $phrase[] = $prefix . 'SET DEFAULT ' . $this->quoteDefaultValue($cd);
343         }
344     }
345
346     /**
347      * Quote a db/table/column identifier if necessary.
348      *
349      * @param string $name
350      * @return string
351      */
352     function quoteIdentifier($name)
353     {
354         return '"' . $name . '"';
355     }
356
357     function mapType($column)
358     {
359         $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.
360                      'numeric' => 'decimal',
361                      'datetime' => 'timestamp',
362                      'blob' => 'bytea');
363
364         $type = $column['type'];
365         if (isset($map[$type])) {
366             $type = $map[$type];
367         }
368
369         if (!empty($column['size'])) {
370             $size = $column['size'];
371             if ($type == 'int' &&
372                        in_array($size, array('small', 'big'))) {
373                 $type = $size . 'int';
374             }
375         }
376
377         return $type;
378     }
379
380     // @fixme need name... :P
381     function typeAndSize($column)
382     {
383         if ($column['type'] == 'enum') {
384             $vals = array_map(array($this, 'quote'), $column['enum']);
385             return "text check ($name in " . implode(',', $vals) . ')';
386         } else {
387             return parent::typeAndSize($column);
388         }
389     }
390
391     /**
392      * Map a native type back to an independent type + size
393      *
394      * @param string $type
395      * @return array ($type, $size) -- $size may be null
396      */
397     protected function reverseMapType($type)
398     {
399         $type = strtolower($type);
400         $map = array(
401             'int4' => array('int', null),
402             'int8' => array('int', 'big'),
403             'bytea' => array('blob', null),
404         );
405         if (isset($map[$type])) {
406             return $map[$type];
407         } else {
408             return array($type, null);
409         }
410     }
411
412     /**
413      * Filter the given table definition array to match features available
414      * in this database.
415      *
416      * This lets us strip out unsupported things like comments, foreign keys,
417      * or type variants that we wouldn't get back from getTableDef().
418      *
419      * @param array $tableDef
420      */
421     function filterDef(array $tableDef)
422     {
423         foreach ($tableDef['fields'] as $name => &$col) {
424             // No convenient support for field descriptions
425             unset($col['description']);
426
427             /*
428             if (isset($col['size'])) {
429                 // Don't distinguish between tinyint and int.
430                 if ($col['size'] == 'tiny' && $col['type'] == 'int') {
431                     unset($col['size']);
432                 }
433             }
434              */
435             $col['type'] = $this->mapType($col);
436             unset($col['size']);
437         }
438         return $tableDef;
439     }
440
441 }