]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/pgsqlschema.php
Starting on adapting postgresql schema class to look stuff up in the new drupalish...
[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         foreach ($columns as $row) {
76
77             $name = $row['column_name'];
78             $field = array();
79
80             // ??
81             list($type, $size) = $this->reverseMapType($row['udt_name']);
82             $field['type'] = $type;
83             if ($size !== null) {
84                 $field['size'] = $size;
85             }
86
87             if ($type == 'char' || $type == 'varchar') {
88                 if ($row['character_maximum_length'] !== null) {
89                     $field['length'] = intval($row['character_maximum_length']);
90                 }
91             }
92             if ($type == 'numeric') {
93                 // Other int types may report these values, but they're irrelevant.
94                 // Just ignore them!
95                 if ($row['numeric_precision'] !== null) {
96                     $field['precision'] = intval($row['numeric_precision']);
97                 }
98                 if ($row['numeric_scale'] !== null) {
99                     $field['scale'] = intval($row['numeric_scale']);
100                 }
101             }
102             if ($row['is_nullable'] == 'NO') {
103                 $field['not null'] = true;
104             }
105             if ($row['column_default'] !== null) {
106                 $field['default'] = $row['column_default'];
107                 if ($this->isNumericType($type)) {
108                     $field['default'] = intval($field['default']);
109                 }
110             }
111
112             $def['fields'][$name] = $field;
113         }
114
115         // Pull constraint data from INFORMATION_SCHEMA
116         // @fixme also find multi-val indexes
117         // @fixme distinguish the primary key
118         // @fixme pull foreign key references
119         $keyColumns = $this->fetchMetaInfo($table, 'key_column_usage', 'constraint_name,ordinal_position');
120         $keys = array();
121
122         foreach ($keyColumns as $row) {
123             $keyName = $row['constraint_name'];
124             $keyCol = $row['column_name'];
125             if (!isset($keys[$keyName])) {
126                 $keys[$keyName] = array();
127             }
128             $keys[$keyName][] = $keyCol;
129         }
130
131         foreach ($keys as $keyName => $cols) {
132             $def['unique indexes'][$keyName] = $cols;
133         }
134         return $def;
135     }
136
137     /**
138      * Pull some INFORMATION.SCHEMA data for the given table.
139      *
140      * @param string $table
141      * @return array of arrays
142      */
143     function fetchMetaInfo($table, $infoTable, $orderBy=null)
144     {
145         $query = "SELECT * FROM information_schema.%s " .
146                  "WHERE table_name='%s'";
147         $sql = sprintf($query, $infoTable, $table);
148         if ($orderBy) {
149             $sql .= ' ORDER BY ' . $orderBy;
150         }
151         return $this->fetchQueryData($sql);
152     }
153
154     /**
155      * Creates a table with the given names and columns.
156      *
157      * @param string $name    Name of the table
158      * @param array  $columns Array of ColumnDef objects
159      *                        for new table.
160      *
161      * @return boolean success flag
162      */
163
164     public function createTable($name, $columns)
165     {
166         $uniques = array();
167         $primary = array();
168         $indices = array();
169         $onupdate = array();
170
171         $sql = "CREATE TABLE $name (\n";
172
173         for ($i = 0; $i < count($columns); $i++) {
174
175             $cd =& $columns[$i];
176
177             if ($i > 0) {
178                 $sql .= ",\n";
179             }
180
181             $sql .= $this->_columnSql($cd);
182             switch ($cd->key) {
183             case 'UNI':
184                 $uniques[] = $cd->name;
185                 break;
186             case 'PRI':
187                 $primary[] = $cd->name;
188                 break;
189             case 'MUL':
190                 $indices[] = $cd->name;
191                 break;
192             }
193         }
194
195         if (count($primary) > 0) { // it really should be...
196             $sql .= ",\n PRIMARY KEY (" . implode(',', $primary) . ")";
197         }
198
199         $sql .= "); ";
200
201
202         foreach ($uniques as $u) {
203             $sql .= "\n CREATE index {$name}_{$u}_idx ON {$name} ($u); ";
204         }
205
206         foreach ($indices as $i) {
207             $sql .= "CREATE index {$name}_{$i}_idx ON {$name} ($i)";
208         }
209         $res = $this->conn->query($sql);
210
211         if (PEAR::isError($res)) {
212             throw new Exception($res->getMessage(). ' SQL was '. $sql);
213         }
214
215         return true;
216     }
217
218     /**
219      * Translate the (mostly) mysql-ish column types into somethings more standard
220      * @param string column type
221      *
222      * @return string postgres happy column type
223      */
224     private function _columnTypeTranslation($type) {
225       $map = array(
226       'datetime' => 'timestamp',
227       );
228       if(!empty($map[$type])) {
229         return $map[$type];
230       }
231       return $type;
232     }
233
234     /**
235      * Modifies a column in the schema.
236      *
237      * The name must match an existing column and table.
238      *
239      * @param string    $table     name of the table
240      * @param ColumnDef $columndef new definition of the column.
241      *
242      * @return boolean success flag
243      */
244
245     public function modifyColumn($table, $columndef)
246     {
247         $sql = "ALTER TABLE $table ALTER COLUMN TYPE " .
248           $this->_columnSql($columndef);
249
250         $res = $this->conn->query($sql);
251
252         if (PEAR::isError($res)) {
253             throw new Exception($res->getMessage());
254         }
255
256         return true;
257     }
258
259
260     /**
261      * Ensures that a table exists with the given
262      * name and the given column definitions.
263      *
264      * If the table does not yet exist, it will
265      * create the table. If it does exist, it will
266      * alter the table to match the column definitions.
267      *
268      * @param string $tableName name of the table
269      * @param array  $columns   array of ColumnDef
270      *                          objects for the table
271      *
272      * @return boolean success flag
273      */
274
275     public function ensureTable($tableName, $columns)
276     {
277         // XXX: DB engine portability -> toilet
278
279         try {
280             $td = $this->getTableDef($tableName);
281             
282         } catch (Exception $e) {
283             if (preg_match('/no such table/', $e->getMessage())) {
284                 return $this->createTable($tableName, $columns);
285             } else {
286                 throw $e;
287             }
288         }
289
290         $cur = $this->_names($td->columns);
291         $new = $this->_names($columns);
292
293         $toadd  = array_diff($new, $cur);
294         $todrop = array_diff($cur, $new);
295         $same   = array_intersect($new, $cur);
296         $tomod  = array();
297         foreach ($same as $m) {
298             $curCol = $this->_byName($td->columns, $m);
299             $newCol = $this->_byName($columns, $m);
300             
301
302             if (!$newCol->equals($curCol)) {
303             // BIG GIANT TODO!
304             // stop it detecting different types and trying to modify on every page request
305 //                 $tomod[] = $newCol->name;
306             }
307         }
308         if (count($toadd) + count($todrop) + count($tomod) == 0) {
309             // nothing to do
310             return true;
311         }
312
313         // For efficiency, we want this all in one
314         // query, instead of using our methods.
315
316         $phrase = array();
317
318         foreach ($toadd as $columnName) {
319             $cd = $this->_byName($columns, $columnName);
320
321             $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
322         }
323
324         foreach ($todrop as $columnName) {
325             $phrase[] = 'DROP COLUMN ' . $columnName;
326         }
327
328         foreach ($tomod as $columnName) {
329             $cd = $this->_byName($columns, $columnName);
330
331         /* brute force */
332             $phrase[] = 'DROP COLUMN ' . $columnName;
333             $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
334         }
335
336         $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
337         $res = $this->conn->query($sql);
338
339         if (PEAR::isError($res)) {
340             throw new Exception($res->getMessage());
341         }
342
343         return true;
344     }
345
346     /**
347      * Return the proper SQL for creating or
348      * altering a column.
349      *
350      * Appropriate for use in CREATE TABLE or
351      * ALTER TABLE statements.
352      *
353      * @param string $tableName
354      * @param array $tableDef
355      * @param string $columnName
356      * @param array $cd column to create
357      *
358      * @return string correct SQL for that column
359      */
360
361     function columnSql($name, array $cd)
362     {
363         $line = array();
364         $line[] = parent::_columnSql($cd);
365
366         if ($table['foreign keys'][$name]) {
367             foreach ($table['foreign keys'][$name] as $foreignTable => $foreignColumn) {
368                 $line[] = 'references';
369                 $line[] = $this->quoteId($foreignTable);
370                 $line[] = '(' . $this->quoteId($foreignColumn) . ')';
371             }
372         }
373
374         return implode(' ', $line);
375     }
376
377     function mapType($column)
378     {
379         $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.
380                      'numeric' => 'decimal',
381                      'datetime' => 'timestamp',
382                      'blob' => 'bytea');
383
384         $type = $column['type'];
385         if (isset($map[$type])) {
386             $type = $map[$type];
387         }
388
389         if (!empty($column['size'])) {
390             $size = $column['size'];
391             if ($type == 'integer' &&
392                        in_array($size, array('small', 'big'))) {
393                 $type = $size . 'int';
394             }
395         }
396
397         return $type;
398     }
399
400     // @fixme need name... :P
401     function typeAndSize($column)
402     {
403         if ($column['type'] == 'enum') {
404             $vals = array_map(array($this, 'quote'), $column['enum']);
405             return "text check ($name in " . implode(',', $vals) . ')';
406         } else {
407             return parent::typeAndSize($column);
408         }
409     }
410
411     /**
412      * Map a native type back to an independent type + size
413      *
414      * @param string $type
415      * @return array ($type, $size) -- $size may be null
416      */
417     protected function reverseMapType($type)
418     {
419         $type = strtolower($type);
420         $map = array(
421             'int4' => array('int', null),
422             'int8' => array('int', 'big'),
423             'bytea' => array('blob', null),
424         );
425         if (isset($map[$type])) {
426             return $map[$type];
427         } else {
428             return array($type, null);
429         }
430     }
431
432 }