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