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