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