]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/schema.php
Fixed group representation in Directory plugin, also some ->raw calls
[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         if (!empty($td) && !empty($td->columns)) {
111             foreach ($td->columns as $cd) {
112                 if ($cd->name == $column) {
113                     return $cd;
114                 }
115             }
116         }
117
118         return null;
119     }
120
121     /**
122      * Creates a table with the given names and columns.
123      *
124      * @param string $tableName    Name of the table
125      * @param array  $def          Table definition array listing fields and indexes.
126      *
127      * @return boolean success flag
128      */
129
130     public function createTable($tableName, $def)
131     {
132         $statements = $this->buildCreateTable($tableName, $def);
133         return $this->runSqlSet($statements);
134     }
135
136     /**
137      * Build a set of SQL statements to create a table with the given
138      * name and columns.
139      *
140      * @param string $name    Name of the table
141      * @param array  $def     Table definition array
142      *
143      * @return boolean success flag
144      */
145     public function buildCreateTable($name, $def)
146     {
147         $def = $this->validateDef($name, $def);
148         $def = $this->filterDef($def);
149         $sql = array();
150
151         foreach ($def['fields'] as $col => $colDef) {
152             $this->appendColumnDef($sql, $col, $colDef);
153         }
154
155         // Primary, unique, and foreign keys are constraints, so go within
156         // the CREATE TABLE statement normally.
157         if (!empty($def['primary key'])) {
158             $this->appendPrimaryKeyDef($sql, $def['primary key']);
159         }
160
161         if (!empty($def['unique keys'])) {
162             foreach ($def['unique keys'] as $col => $colDef) {
163                 $this->appendUniqueKeyDef($sql, $col, $colDef);
164             }
165         }
166
167         if (!empty($def['foreign keys'])) {
168             foreach ($def['foreign keys'] as $keyName => $keyDef) {
169                 $this->appendForeignKeyDef($sql, $keyName, $keyDef);
170             }
171         }
172
173         // Wrap the CREATE TABLE around the main body chunks...
174         $statements = array();
175         $statements[] = $this->startCreateTable($name, $def) . "\n" .
176                         implode($sql, ",\n") . "\n" .
177                         $this->endCreateTable($name, $def);
178
179         // Multi-value indexes are advisory and for best portability
180         // should be created as separate statements.
181         if (!empty($def['indexes'])) {
182             foreach ($def['indexes'] as $col => $colDef) {
183                 $this->appendCreateIndex($statements, $name, $col, $colDef);
184             }
185         }
186         if (!empty($def['fulltext indexes'])) {
187             foreach ($def['fulltext indexes'] as $col => $colDef) {
188                 $this->appendCreateFulltextIndex($statements, $name, $col, $colDef);
189             }
190         }
191
192         return $statements;
193     }
194
195     /**
196      * Set up a 'create table' SQL statement.
197      *
198      * @param string $name table name
199      * @param array $def table definition
200      * @param $string
201      */
202     function startCreateTable($name, array $def)
203     {
204         return 'CREATE TABLE ' . $this->quoteIdentifier($name)  . ' (';
205     }
206
207     /**
208      * Close out a 'create table' SQL statement.
209      *
210      * @param string $name table name
211      * @param array $def table definition
212      * @return string
213      */
214     function endCreateTable($name, array $def)
215     {
216         return ')';
217     }
218
219     /**
220      * Append an SQL fragment with a column definition in a CREATE TABLE statement.
221      *
222      * @param array $sql
223      * @param string $name
224      * @param array $def
225      */
226     function appendColumnDef(array &$sql, $name, array $def)
227     {
228         $sql[] = "$name " . $this->columnSql($def);
229     }
230
231     /**
232      * Append an SQL fragment with a constraint definition for a primary
233      * key in a CREATE TABLE statement.
234      *
235      * @param array $sql
236      * @param array $def
237      */
238     function appendPrimaryKeyDef(array &$sql, array $def)
239     {
240         $sql[] = "PRIMARY KEY " . $this->buildIndexList($def);
241     }
242
243     /**
244      * Append an SQL fragment with a constraint definition for a unique
245      * key in a CREATE TABLE statement.
246      *
247      * @param array $sql
248      * @param string $name
249      * @param array $def
250      */
251     function appendUniqueKeyDef(array &$sql, $name, array $def)
252     {
253         $sql[] = "CONSTRAINT $name UNIQUE " . $this->buildIndexList($def);
254     }
255
256     /**
257      * Append an SQL fragment with a constraint definition for a foreign
258      * key in a CREATE TABLE statement.
259      *
260      * @param array $sql
261      * @param string $name
262      * @param array $def
263      */
264     function appendForeignKeyDef(array &$sql, $name, array $def)
265     {
266         if (count($def) != 2) {
267             throw new Exception("Invalid foreign key def for $name: " . var_export($def, true));
268         }
269         list($refTable, $map) = $def;
270         $srcCols = array_keys($map);
271         $refCols = array_values($map);
272         $sql[] = "CONSTRAINT $name FOREIGN KEY " .
273                  $this->buildIndexList($srcCols) .
274                  " REFERENCES " .
275                  $this->quoteIdentifier($refTable) .
276                  " " .
277                  $this->buildIndexList($refCols);
278     }
279
280     /**
281      * Append an SQL statement with an index definition for an advisory
282      * index over one or more columns on a table.
283      *
284      * @param array $statements
285      * @param string $table
286      * @param string $name
287      * @param array $def
288      */
289     function appendCreateIndex(array &$statements, $table, $name, array $def)
290     {
291         $statements[] = "CREATE INDEX $name ON $table " . $this->buildIndexList($def);
292     }
293
294     /**
295      * Append an SQL statement with an index definition for a full-text search
296      * index over one or more columns on a table.
297      *
298      * @param array $statements
299      * @param string $table
300      * @param string $name
301      * @param array $def
302      */
303     function appendCreateFulltextIndex(array &$statements, $table, $name, array $def)
304     {
305         throw new Exception("Fulltext index not supported in this database");
306     }
307
308     /**
309      * Append an SQL statement to drop an index from a table.
310      *
311      * @param array $statements
312      * @param string $table
313      * @param string $name
314      * @param array $def
315      */
316     function appendDropIndex(array &$statements, $table, $name)
317     {
318         $statements[] = "DROP INDEX $name ON " . $this->quoteIdentifier($table);
319     }
320
321     function buildIndexList(array $def)
322     {
323         // @fixme
324         return '(' . implode(',', array_map(array($this, 'buildIndexItem'), $def)) . ')';
325     }
326
327     function buildIndexItem($def)
328     {
329         if (is_array($def)) {
330             list($name, $size) = $def;
331             return $this->quoteIdentifier($name) . '(' . intval($size) . ')';
332         }
333         return $this->quoteIdentifier($def);
334     }
335
336     /**
337      * Drops a table from the schema
338      *
339      * Throws an exception if the table is not found.
340      *
341      * @param string $name Name of the table to drop
342      *
343      * @return boolean success flag
344      */
345
346     public function dropTable($name)
347     {
348         global $_PEAR;
349
350         $res = $this->conn->query("DROP TABLE $name");
351
352         if ($_PEAR->isError($res)) {
353             PEAR_ErrorToPEAR_Exception($res);
354         }
355
356         return true;
357     }
358
359     /**
360      * Adds an index to a table.
361      *
362      * If no name is provided, a name will be made up based
363      * on the table name and column names.
364      *
365      * Throws an exception on database error, esp. if the table
366      * does not exist.
367      *
368      * @param string $table       Name of the table
369      * @param array  $columnNames Name of columns to index
370      * @param string $name        (Optional) name of the index
371      *
372      * @return boolean success flag
373      */
374
375     public function createIndex($table, $columnNames, $name=null)
376     {
377         global $_PEAR;
378
379         if (!is_array($columnNames)) {
380             $columnNames = array($columnNames);
381         }
382
383         if (empty($name)) {
384             $name = "{$table}_".implode("_", $columnNames)."_idx";
385         }
386
387         $res = $this->conn->query("ALTER TABLE $table ".
388                                    "ADD INDEX $name (".
389                                    implode(",", $columnNames).")");
390
391         if ($_PEAR->isError($res)) {
392             PEAR_ErrorToPEAR_Exception($res);
393         }
394
395         return true;
396     }
397
398     /**
399      * Drops a named index from a table.
400      *
401      * @param string $table name of the table the index is on.
402      * @param string $name  name of the index
403      *
404      * @return boolean success flag
405      */
406
407     public function dropIndex($table, $name)
408     {
409         global $_PEAR;
410
411         $res = $this->conn->query("ALTER TABLE $table DROP INDEX $name");
412
413         if ($_PEAR->isError($res)) {
414             PEAR_ErrorToPEAR_Exception($res);
415         }
416
417         return true;
418     }
419
420     /**
421      * Adds a column to a table
422      *
423      * @param string    $table     name of the table
424      * @param ColumnDef $columndef Definition of the new
425      *                             column.
426      *
427      * @return boolean success flag
428      */
429
430     public function addColumn($table, $columndef)
431     {
432         global $_PEAR;
433
434         $sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef);
435
436         $res = $this->conn->query($sql);
437
438         if ($_PEAR->isError($res)) {
439             PEAR_ErrorToPEAR_Exception($res);
440         }
441
442         return true;
443     }
444
445     /**
446      * Modifies a column in the schema.
447      *
448      * The name must match an existing column and table.
449      *
450      * @param string    $table     name of the table
451      * @param ColumnDef $columndef new definition of the column.
452      *
453      * @return boolean success flag
454      */
455
456     public function modifyColumn($table, $columndef)
457     {
458         global $_PEAR;
459
460         $sql = "ALTER TABLE $table MODIFY COLUMN " .
461           $this->_columnSql($columndef);
462
463         $res = $this->conn->query($sql);
464
465         if ($_PEAR->isError($res)) {
466             PEAR_ErrorToPEAR_Exception($res);
467         }
468
469         return true;
470     }
471
472     /**
473      * Drops a column from a table
474      *
475      * The name must match an existing column.
476      *
477      * @param string $table      name of the table
478      * @param string $columnName name of the column to drop
479      *
480      * @return boolean success flag
481      */
482
483     public function dropColumn($table, $columnName)
484     {
485         global $_PEAR;
486
487         $sql = "ALTER TABLE $table DROP COLUMN $columnName";
488
489         $res = $this->conn->query($sql);
490
491         if ($_PEAR->isError($res)) {
492             PEAR_ErrorToPEAR_Exception($res);
493         }
494
495         return true;
496     }
497
498     /**
499      * Ensures that a table exists with the given
500      * name and the given column definitions.
501      *
502      * If the table does not yet exist, it will
503      * create the table. If it does exist, it will
504      * alter the table to match the column definitions.
505      *
506      * @param string $tableName name of the table
507      * @param array  $def       Table definition array
508      *
509      * @return boolean success flag
510      */
511
512     public function ensureTable($tableName, $def)
513     {
514         $statements = $this->buildEnsureTable($tableName, $def);
515         return $this->runSqlSet($statements);
516     }
517
518     /**
519      * Run a given set of SQL commands on the connection in sequence.
520      * Empty input is ok.
521      *
522      * @fixme if multiple statements, wrap in a transaction?
523      * @param array $statements
524      * @return boolean success flag
525      */
526     function runSqlSet(array $statements)
527     {
528         global $_PEAR;
529
530         $ok = true;
531         foreach ($statements as $sql) {
532             if (defined('DEBUG_INSTALLER')) {
533                 echo "<tt>" . htmlspecialchars($sql) . "</tt><br/>\n";
534             }
535             $res = $this->conn->query($sql);
536
537             if ($_PEAR->isError($res)) {
538                 common_debug('PEAR exception on query: '.$sql);
539                 PEAR_ErrorToPEAR_Exception($res);
540             }
541         }
542         return $ok;
543     }
544
545     /**
546      * Check a table's status, and if needed build a set
547      * of SQL statements which change it to be consistent
548      * with the given table definition.
549      *
550      * If the table does not yet exist, statements will
551      * be returned to create the table. If it does exist,
552      * statements will be returned to alter the table to
553      * match the column definitions.
554      *
555      * @param string $tableName name of the table
556      * @param array  $columns   array of ColumnDef
557      *                          objects for the table
558      *
559      * @return array of SQL statements
560      */
561
562     function buildEnsureTable($tableName, array $def)
563     {
564         try {
565             $old = $this->getTableDef($tableName);
566         } catch (SchemaTableMissingException $e) {
567             return $this->buildCreateTable($tableName, $def);
568         }
569
570         // Filter the DB-independent table definition to match the current
571         // database engine's features and limitations.
572         $def = $this->validateDef($tableName, $def);
573         $def = $this->filterDef($def);
574
575         $statements = array();
576         $fields = $this->diffArrays($old, $def, 'fields', array($this, 'columnsEqual'));
577         $uniques = $this->diffArrays($old, $def, 'unique keys');
578         $indexes = $this->diffArrays($old, $def, 'indexes');
579         $foreign = $this->diffArrays($old, $def, 'foreign keys');
580         $fulltext = $this->diffArrays($old, $def, 'fulltext indexes');
581
582         // Drop any obsolete or modified indexes ahead...
583         foreach ($indexes['del'] + $indexes['mod'] as $indexName) {
584             $this->appendDropIndex($statements, $tableName, $indexName);
585         }
586
587         // Drop any obsolete or modified fulltext indexes ahead...
588         foreach ($fulltext['del'] + $fulltext['mod'] as $indexName) {
589             $this->appendDropIndex($statements, $tableName, $indexName);
590         }
591
592         // For efficiency, we want this all in one
593         // query, instead of using our methods.
594
595         $phrase = array();
596
597         foreach ($foreign['del'] + $foreign['mod'] as $keyName) {
598             $this->appendAlterDropForeign($phrase, $keyName);
599         }
600
601         foreach ($uniques['del'] + $uniques['mod'] as $keyName) {
602             $this->appendAlterDropUnique($phrase, $keyName);
603         }
604
605         if (isset($old['primary key']) && (!isset($def['primary key']) || $def['primary key'] != $old['primary key'])) {
606             $this->appendAlterDropPrimary($phrase);
607         }
608
609         foreach ($fields['add'] as $columnName) {
610             $this->appendAlterAddColumn($phrase, $columnName,
611                     $def['fields'][$columnName]);
612         }
613
614         foreach ($fields['mod'] as $columnName) {
615             $this->appendAlterModifyColumn($phrase, $columnName,
616                     $old['fields'][$columnName],
617                     $def['fields'][$columnName]);
618         }
619
620         foreach ($fields['del'] as $columnName) {
621             $this->appendAlterDropColumn($phrase, $columnName);
622         }
623
624         if (isset($def['primary key']) && (!isset($old['primary key']) || $old['primary key'] != $def['primary key'])) {
625             $this->appendAlterAddPrimary($phrase, $def['primary key']);
626         }
627
628         foreach ($uniques['mod'] + $uniques['add'] as $keyName) {
629             $this->appendAlterAddUnique($phrase, $keyName, $def['unique keys'][$keyName]);
630         }
631
632         foreach ($foreign['mod'] + $foreign['add'] as $keyName) {
633             $this->appendAlterAddForeign($phrase, $keyName, $def['foreign keys'][$keyName]);
634         }
635
636         $this->appendAlterExtras($phrase, $tableName, $def);
637
638         if (count($phrase) > 0) {
639             $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(",\n", $phrase);
640             $statements[] = $sql;
641         }
642
643         // Now create any indexes...
644         foreach ($indexes['mod'] + $indexes['add'] as $indexName) {
645             $this->appendCreateIndex($statements, $tableName, $indexName, $def['indexes'][$indexName]);
646         }
647
648         foreach ($fulltext['mod'] + $fulltext['add'] as $indexName) {
649             $colDef = $def['fulltext indexes'][$indexName];
650             $this->appendCreateFulltextIndex($statements, $tableName, $indexName, $colDef);
651         }
652
653         return $statements;
654     }
655
656     function diffArrays($oldDef, $newDef, $section, $compareCallback=null)
657     {
658         $old = isset($oldDef[$section]) ? $oldDef[$section] : array();
659         $new = isset($newDef[$section]) ? $newDef[$section] : array();
660
661         $oldKeys = array_keys($old);
662         $newKeys = array_keys($new);
663
664         $toadd  = array_diff($newKeys, $oldKeys);
665         $todrop = array_diff($oldKeys, $newKeys);
666         $same   = array_intersect($newKeys, $oldKeys);
667         $tomod  = array();
668         $tokeep = array();
669
670         // Find which fields have actually changed definition
671         // in a way that we need to tweak them for this DB type.
672         foreach ($same as $name) {
673             if ($compareCallback) {
674                 $same = call_user_func($compareCallback, $old[$name], $new[$name]);
675             } else {
676                 $same = ($old[$name] == $new[$name]);
677             }
678             if ($same) {
679                 $tokeep[] = $name;
680                 continue;
681             }
682             $tomod[] = $name;
683         }
684         return array('add' => $toadd,
685                      'del' => $todrop,
686                      'mod' => $tomod,
687                      'keep' => $tokeep,
688                      'count' => count($toadd) + count($todrop) + count($tomod));
689     }
690
691     /**
692      * Append phrase(s) to an array of partial ALTER TABLE chunks in order
693      * to add the given column definition to the table.
694      *
695      * @param array $phrase
696      * @param string $columnName
697      * @param array $cd 
698      */
699     function appendAlterAddColumn(array &$phrase, $columnName, array $cd)
700     {
701         $phrase[] = 'ADD COLUMN ' .
702                     $this->quoteIdentifier($columnName) .
703                     ' ' .
704                     $this->columnSql($cd);
705     }
706
707     /**
708      * Append phrase(s) to an array of partial ALTER TABLE chunks in order
709      * to alter the given column from its old state to a new one.
710      *
711      * @param array $phrase
712      * @param string $columnName
713      * @param array $old previous column definition as found in DB
714      * @param array $cd current column definition
715      */
716     function appendAlterModifyColumn(array &$phrase, $columnName, array $old, array $cd)
717     {
718         $phrase[] = 'MODIFY COLUMN ' .
719                     $this->quoteIdentifier($columnName) .
720                     ' ' .
721                     $this->columnSql($cd);
722     }
723
724     /**
725      * Append phrase(s) to an array of partial ALTER TABLE chunks in order
726      * to drop the given column definition from the table.
727      *
728      * @param array $phrase
729      * @param string $columnName
730      */
731     function appendAlterDropColumn(array &$phrase, $columnName)
732     {
733         $phrase[] = 'DROP COLUMN ' . $this->quoteIdentifier($columnName);
734     }
735
736     function appendAlterAddUnique(array &$phrase, $keyName, array $def)
737     {
738         $sql = array();
739         $sql[] = 'ADD';
740         $this->appendUniqueKeyDef($sql, $keyName, $def);
741         $phrase[] = implode(' ', $sql);
742     }
743
744     function appendAlterAddForeign(array &$phrase, $keyName, array $def)
745     {
746         $sql = array();
747         $sql[] = 'ADD';
748         $this->appendForeignKeyDef($sql, $keyName, $def);
749         $phrase[] = implode(' ', $sql);
750     }
751
752     function appendAlterAddPrimary(array &$phrase, array $def)
753     {
754         $sql = array();
755         $sql[] = 'ADD';
756         $this->appendPrimaryKeyDef($sql, $def);
757         $phrase[] = implode(' ', $sql);
758     }
759
760     function appendAlterDropPrimary(array &$phrase)
761     {
762         $phrase[] = 'DROP CONSTRAINT PRIMARY KEY';
763     }
764
765     function appendAlterDropUnique(array &$phrase, $keyName)
766     {
767         $phrase[] = 'DROP CONSTRAINT ' . $keyName;
768     }
769
770     function appendAlterDropForeign(array &$phrase, $keyName)
771     {
772         $phrase[] = 'DROP FOREIGN KEY ' . $keyName;
773     }
774
775     function appendAlterExtras(array &$phrase, $tableName, array $def)
776     {
777         // no-op
778     }
779
780     /**
781      * Quote a db/table/column identifier if necessary.
782      *
783      * @param string $name
784      * @return string
785      */
786     function quoteIdentifier($name)
787     {
788         return $name;
789     }
790
791     function quoteDefaultValue($cd)
792     {
793         if ($cd['type'] == 'datetime' && $cd['default'] == 'CURRENT_TIMESTAMP') {
794             return $cd['default'];
795         } else {
796             return $this->quoteValue($cd['default']);
797         }
798     }
799
800     function quoteValue($val)
801     {
802         return $this->conn->quoteSmart($val);
803     }
804
805     /**
806      * Check if two column definitions are equivalent.
807      * The default implementation checks _everything_ but in many cases
808      * you may be able to discard a bunch of equivalencies.
809      *
810      * @param array $a
811      * @param array $b
812      * @return boolean
813      */
814     function columnsEqual(array $a, array $b)
815     {
816         return !array_diff_assoc($a, $b) && !array_diff_assoc($b, $a);
817     }
818
819     /**
820      * Returns the array of names from an array of
821      * ColumnDef objects.
822      *
823      * @param array $cds array of ColumnDef objects
824      *
825      * @return array strings for name values
826      */
827
828     protected function _names($cds)
829     {
830         $names = array();
831
832         foreach ($cds as $cd) {
833             $names[] = $cd->name;
834         }
835
836         return $names;
837     }
838
839     /**
840      * Get a ColumnDef from an array matching
841      * name.
842      *
843      * @param array  $cds  Array of ColumnDef objects
844      * @param string $name Name of the column
845      *
846      * @return ColumnDef matching item or null if no match.
847      */
848
849     protected function _byName($cds, $name)
850     {
851         foreach ($cds as $cd) {
852             if ($cd->name == $name) {
853                 return $cd;
854             }
855         }
856
857         return null;
858     }
859
860     /**
861      * Return the proper SQL for creating or
862      * altering a column.
863      *
864      * Appropriate for use in CREATE TABLE or
865      * ALTER TABLE statements.
866      *
867      * @param ColumnDef $cd column to create
868      *
869      * @return string correct SQL for that column
870      */
871
872     function columnSql(array $cd)
873     {
874         $line = array();
875         $line[] = $this->typeAndSize($cd);
876
877         if (isset($cd['default'])) {
878             $line[] = 'default';
879             $line[] = $this->quoteDefaultValue($cd);
880         } else if (!empty($cd['not null'])) {
881             // Can't have both not null AND default!
882             $line[] = 'not null';
883         }
884
885         return implode(' ', $line);
886     }
887
888     /**
889      *
890      * @param string $column canonical type name in defs
891      * @return string native DB type name
892      */
893     function mapType($column)
894     {
895         return $column;
896     }
897
898     function typeAndSize($column)
899     {
900         //$type = $this->mapType($column);
901         $type = $column['type'];
902         if (isset($column['size'])) {
903             $type = $column['size'] . $type;
904         }
905         $lengths = array();
906
907         if (isset($column['precision'])) {
908             $lengths[] = $column['precision'];
909             if (isset($column['scale'])) {
910                 $lengths[] = $column['scale'];
911             }
912         } else if (isset($column['length'])) {
913             $lengths[] = $column['length'];
914         }
915
916         if ($lengths) {
917             return $type . '(' . implode(',', $lengths) . ')';
918         } else {
919             return $type;
920         }
921     }
922
923     /**
924      * Convert an old-style set of ColumnDef objects into the current
925      * Drupal-style schema definition array, for backwards compatibility
926      * with plugins written for 0.9.x.
927      *
928      * @param string $tableName
929      * @param array $defs: array of ColumnDef objects
930      * @return array
931      */
932     protected function oldToNew($tableName, array $defs)
933     {
934         $table = array();
935         $prefixes = array(
936             'tiny',
937             'small',
938             'medium',
939             'big',
940         );
941         foreach ($defs as $cd) {
942             $column = array();
943             $column['type'] = $cd->type;
944             foreach ($prefixes as $prefix) {
945                 if (substr($cd->type, 0, strlen($prefix)) == $prefix) {
946                     $column['type'] = substr($cd->type, strlen($prefix));
947                     $column['size'] = $prefix;
948                     break;
949                 }
950             }
951
952             if ($cd->size) {
953                 if ($cd->type == 'varchar' || $cd->type == 'char') {
954                     $column['length'] = $cd->size;
955                 }
956             }
957             if (!$cd->nullable) {
958                 $column['not null'] = true;
959             }
960             if ($cd->auto_increment) {
961                 $column['type'] = 'serial';
962             }
963             if ($cd->default) {
964                 $column['default'] = $cd->default;
965             }
966             $table['fields'][$cd->name] = $column;
967
968             if ($cd->key == 'PRI') {
969                 // If multiple columns are defined as primary key,
970                 // we'll pile them on in sequence.
971                 if (!isset($table['primary key'])) {
972                     $table['primary key'] = array();
973                 }
974                 $table['primary key'][] = $cd->name;
975             } else if ($cd->key == 'MUL') {
976                 // Individual multiple-value indexes are only per-column
977                 // using the old ColumnDef syntax.
978                 $idx = "{$tableName}_{$cd->name}_idx";
979                 $table['indexes'][$idx] = array($cd->name);
980             } else if ($cd->key == 'UNI') {
981                 // Individual unique-value indexes are only per-column
982                 // using the old ColumnDef syntax.
983                 $idx = "{$tableName}_{$cd->name}_idx";
984                 $table['unique keys'][$idx] = array($cd->name);
985             }
986         }
987
988         return $table;
989     }
990
991     /**
992      * Filter the given table definition array to match features available
993      * in this database.
994      *
995      * This lets us strip out unsupported things like comments, foreign keys,
996      * or type variants that we wouldn't get back from getTableDef().
997      *
998      * @param array $tableDef
999      */
1000     function filterDef(array $tableDef)
1001     {
1002         return $tableDef;
1003     }
1004
1005     /**
1006      * Validate a table definition array, checking for basic structure.
1007      *
1008      * If necessary, converts from an old-style array of ColumnDef objects.
1009      *
1010      * @param string $tableName
1011      * @param array $def: table definition array
1012      * @return array validated table definition array
1013      *
1014      * @throws Exception on wildly invalid input
1015      */
1016     function validateDef($tableName, array $def)
1017     {
1018         if (isset($def[0]) && $def[0] instanceof ColumnDef) {
1019             $def = $this->oldToNew($tableName, $def);
1020         }
1021
1022         // A few quick checks :D
1023         if (!isset($def['fields'])) {
1024             throw new Exception("Invalid table definition for $tableName: no fields.");
1025         }
1026
1027         return $def;
1028     }
1029
1030     function isNumericType($type)
1031     {
1032         $type = strtolower($type);
1033         $known = array('int', 'serial', 'numeric');
1034         return in_array($type, $known);
1035     }
1036
1037     /**
1038      * Pull info from the query into a fun-fun array of dooooom
1039      *
1040      * @param string $sql
1041      * @return array of arrays
1042      */
1043     protected function fetchQueryData($sql)
1044     {
1045         global $_PEAR;
1046
1047         $res = $this->conn->query($sql);
1048         if ($_PEAR->isError($res)) {
1049             PEAR_ErrorToPEAR_Exception($res);
1050         }
1051
1052         $out = array();
1053         $row = array();
1054         while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
1055             $out[] = $row;
1056         }
1057         $res->free();
1058
1059         return $out;
1060     }
1061
1062 }
1063
1064 class SchemaTableMissingException extends Exception
1065 {
1066     // no-op
1067 }
1068