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