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