]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/pgsqlschema.php
Merge branch 'master' of gitorious.org:statusnet/mainline into testing
[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  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
45  * @link     http://status.net/
46  */
47
48 class PgsqlSchema extends Schema
49 {
50
51     /**
52      * Returns a TableDef object for the table
53      * in the schema with the given name.
54      *
55      * Throws an exception if the table is not found.
56      *
57      * @param string $name Name of the table to get
58      *
59      * @return TableDef tabledef for that table.
60      */
61
62     public function getTableDef($name)
63     {
64         $res = $this->conn->query("select *, column_default as default, is_nullable as Null, udt_name as Type, column_name AS Field from INFORMATION_SCHEMA.COLUMNS where table_name = '$name'");
65
66         if (PEAR::isError($res)) {
67             throw new Exception($res->getMessage());
68         }
69
70         $td = new TableDef();
71
72         $td->name    = $name;
73         $td->columns = array();
74
75         $row = array();
76
77         while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
78 //             var_dump($row);
79             $cd = new ColumnDef();
80
81             $cd->name = $row['field'];
82
83             $packed = $row['type'];
84
85             if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
86                 $cd->type = $match[1];
87                 $cd->size = $match[2];
88             } else {
89                 $cd->type = $packed;
90             }
91
92             $cd->nullable = ($row['null'] == 'YES') ? true : false;
93             $cd->key      = $row['Key'];
94             $cd->default  = $row['default'];
95             $cd->extra    = $row['Extra'];
96
97             $td->columns[] = $cd;
98         }
99         return $td;
100     }
101
102     /**
103      * Gets a ColumnDef object for a single column.
104      *
105      * Throws an exception if the table is not found.
106      *
107      * @param string $table  name of the table
108      * @param string $column name of the column
109      *
110      * @return ColumnDef definition of the column or null
111      *                   if not found.
112      */
113
114     public function getColumnDef($table, $column)
115     {
116         $td = $this->getTableDef($table);
117
118         foreach ($td->columns as $cd) {
119             if ($cd->name == $column) {
120                 return $cd;
121             }
122         }
123
124         return null;
125     }
126
127     /**
128      * Creates a table with the given names and columns.
129      *
130      * @param string $name    Name of the table
131      * @param array  $columns Array of ColumnDef objects
132      *                        for new table.
133      *
134      * @return boolean success flag
135      */
136
137     public function createTable($name, $columns)
138     {
139         $uniques = array();
140         $primary = array();
141         $indices = array();
142
143         $sql = "CREATE TABLE $name (\n";
144
145         for ($i = 0; $i < count($columns); $i++) {
146
147             $cd =& $columns[$i];
148
149             if ($i > 0) {
150                 $sql .= ",\n";
151             }
152
153             $sql .= $this->_columnSql($cd);
154
155             switch ($cd->key) {
156             case 'UNI':
157                 $uniques[] = $cd->name;
158                 break;
159             case 'PRI':
160                 $primary[] = $cd->name;
161                 break;
162             case 'MUL':
163                 $indices[] = $cd->name;
164                 break;
165             }
166         }
167
168         if (count($primary) > 0) { // it really should be...
169             $sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")";
170         }
171
172         foreach ($uniques as $u) {
173             $sql .= ",\nunique index {$name}_{$u}_idx ($u)";
174         }
175
176         foreach ($indices as $i) {
177             $sql .= ",\nindex {$name}_{$i}_idx ($i)";
178         }
179
180         $sql .= "); ";
181
182         $res = $this->conn->query($sql);
183
184         if (PEAR::isError($res)) {
185             throw new Exception($res->getMessage());
186         }
187
188         return true;
189     }
190
191     /**
192      * Drops a table from the schema
193      *
194      * Throws an exception if the table is not found.
195      *
196      * @param string $name Name of the table to drop
197      *
198      * @return boolean success flag
199      */
200
201     public function dropTable($name)
202     {
203         $res = $this->conn->query("DROP TABLE $name");
204
205         if (PEAR::isError($res)) {
206             throw new Exception($res->getMessage());
207         }
208
209         return true;
210     }
211
212     /**
213      * Adds an index to a table.
214      *
215      * If no name is provided, a name will be made up based
216      * on the table name and column names.
217      *
218      * Throws an exception on database error, esp. if the table
219      * does not exist.
220      *
221      * @param string $table       Name of the table
222      * @param array  $columnNames Name of columns to index
223      * @param string $name        (Optional) name of the index
224      *
225      * @return boolean success flag
226      */
227
228     public function createIndex($table, $columnNames, $name=null)
229     {
230         if (!is_array($columnNames)) {
231             $columnNames = array($columnNames);
232         }
233
234         if (empty($name)) {
235             $name = "$table_".implode("_", $columnNames)."_idx";
236         }
237
238         $res = $this->conn->query("ALTER TABLE $table ".
239                                    "ADD INDEX $name (".
240                                    implode(",", $columnNames).")");
241
242         if (PEAR::isError($res)) {
243             throw new Exception($res->getMessage());
244         }
245
246         return true;
247     }
248
249     /**
250      * Drops a named index from a table.
251      *
252      * @param string $table name of the table the index is on.
253      * @param string $name  name of the index
254      *
255      * @return boolean success flag
256      */
257
258     public function dropIndex($table, $name)
259     {
260         $res = $this->conn->query("ALTER TABLE $table DROP INDEX $name");
261
262         if (PEAR::isError($res)) {
263             throw new Exception($res->getMessage());
264         }
265
266         return true;
267     }
268
269     /**
270      * Adds a column to a table
271      *
272      * @param string    $table     name of the table
273      * @param ColumnDef $columndef Definition of the new
274      *                             column.
275      *
276      * @return boolean success flag
277      */
278
279     public function addColumn($table, $columndef)
280     {
281         $sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef);
282
283         $res = $this->conn->query($sql);
284
285         if (PEAR::isError($res)) {
286             throw new Exception($res->getMessage());
287         }
288
289         return true;
290     }
291
292     /**
293      * Modifies a column in the schema.
294      *
295      * The name must match an existing column and table.
296      *
297      * @param string    $table     name of the table
298      * @param ColumnDef $columndef new definition of the column.
299      *
300      * @return boolean success flag
301      */
302
303     public function modifyColumn($table, $columndef)
304     {
305         $sql = "ALTER TABLE $table MODIFY COLUMN " .
306           $this->_columnSql($columndef);
307
308         $res = $this->conn->query($sql);
309
310         if (PEAR::isError($res)) {
311             throw new Exception($res->getMessage());
312         }
313
314         return true;
315     }
316
317     /**
318      * Drops a column from a table
319      *
320      * The name must match an existing column.
321      *
322      * @param string $table      name of the table
323      * @param string $columnName name of the column to drop
324      *
325      * @return boolean success flag
326      */
327
328     public function dropColumn($table, $columnName)
329     {
330         $sql = "ALTER TABLE $table DROP COLUMN $columnName";
331
332         $res = $this->conn->query($sql);
333
334         if (PEAR::isError($res)) {
335             throw new Exception($res->getMessage());
336         }
337
338         return true;
339     }
340
341     /**
342      * Ensures that a table exists with the given
343      * name and the given column definitions.
344      *
345      * If the table does not yet exist, it will
346      * create the table. If it does exist, it will
347      * alter the table to match the column definitions.
348      *
349      * @param string $tableName name of the table
350      * @param array  $columns   array of ColumnDef
351      *                          objects for the table
352      *
353      * @return boolean success flag
354      */
355
356     public function ensureTable($tableName, $columns)
357     {
358         // XXX: DB engine portability -> toilet
359
360         try {
361             $td = $this->getTableDef($tableName);
362         } catch (Exception $e) {
363             if (preg_match('/no such table/', $e->getMessage())) {
364                 return $this->createTable($tableName, $columns);
365             } else {
366                 throw $e;
367             }
368         }
369
370         $cur = $this->_names($td->columns);
371         $new = $this->_names($columns);
372
373         $toadd  = array_diff($new, $cur);
374         $todrop = array_diff($cur, $new);
375         $same   = array_intersect($new, $cur);
376         $tomod  = array();
377
378         foreach ($same as $m) {
379             $curCol = $this->_byName($td->columns, $m);
380             $newCol = $this->_byName($columns, $m);
381
382             if (!$newCol->equals($curCol)) {
383                 $tomod[] = $newCol->name;
384             }
385         }
386
387         if (count($toadd) + count($todrop) + count($tomod) == 0) {
388             // nothing to do
389             return true;
390         }
391
392         // For efficiency, we want this all in one
393         // query, instead of using our methods.
394
395         $phrase = array();
396
397         foreach ($toadd as $columnName) {
398             $cd = $this->_byName($columns, $columnName);
399
400             $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
401         }
402
403         foreach ($todrop as $columnName) {
404             $phrase[] = 'DROP COLUMN ' . $columnName;
405         }
406
407         foreach ($tomod as $columnName) {
408             $cd = $this->_byName($columns, $columnName);
409
410             $phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd);
411         }
412
413         $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
414
415         $res = $this->conn->query($sql);
416
417         if (PEAR::isError($res)) {
418             throw new Exception($res->getMessage());
419         }
420
421         return true;
422     }
423
424     /**
425      * Returns the array of names from an array of
426      * ColumnDef objects.
427      *
428      * @param array $cds array of ColumnDef objects
429      *
430      * @return array strings for name values
431      */
432
433     private function _names($cds)
434     {
435         $names = array();
436
437         foreach ($cds as $cd) {
438             $names[] = $cd->name;
439         }
440
441         return $names;
442     }
443
444     /**
445      * Get a ColumnDef from an array matching
446      * name.
447      *
448      * @param array  $cds  Array of ColumnDef objects
449      * @param string $name Name of the column
450      *
451      * @return ColumnDef matching item or null if no match.
452      */
453
454     private function _byName($cds, $name)
455     {
456         foreach ($cds as $cd) {
457             if ($cd->name == $name) {
458                 return $cd;
459             }
460         }
461
462         return null;
463     }
464
465     /**
466      * Return the proper SQL for creating or
467      * altering a column.
468      *
469      * Appropriate for use in CREATE TABLE or
470      * ALTER TABLE statements.
471      *
472      * @param ColumnDef $cd column to create
473      *
474      * @return string correct SQL for that column
475      */
476
477     private function _columnSql($cd)
478     {
479         $sql = "{$cd->name} ";
480
481         if (!empty($cd->size)) {
482             $sql .= "{$cd->type}({$cd->size}) ";
483         } else {
484             $sql .= "{$cd->type} ";
485         }
486
487         if (!empty($cd->default)) {
488             $sql .= "default {$cd->default} ";
489         } else {
490             $sql .= ($cd->nullable) ? "null " : "not null ";
491         }
492         
493         if (!empty($cd->auto_increment)) {
494             $sql .= " auto_increment ";
495         }
496
497         if (!empty($cd->extra)) {
498             $sql .= "{$cd->extra} ";
499         }
500
501         return $sql;
502     }
503 }