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