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