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