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