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