Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU 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
// Moodle 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 General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Database manager instance is responsible for all database structure modifications.
19
 *
20
 * @package    core_ddl
21
 * @copyright  1999 onwards Martin Dougiamas     http://dougiamas.com
22
 *             2001-3001 Eloy Lafuente (stronk7) http://contiento.com
23
 *             2008 Petr Skoda                   http://skodak.org
24
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
/**
30
 * Database manager instance is responsible for all database structure modifications.
31
 *
32
 * It is using db specific generators to find out the correct SQL syntax to do that.
33
 *
34
 * @package    core_ddl
35
 * @copyright  1999 onwards Martin Dougiamas     http://dougiamas.com
36
 *             2001-3001 Eloy Lafuente (stronk7) http://contiento.com
37
 *             2008 Petr Skoda                   http://skodak.org
38
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39
 */
40
class database_manager {
41
 
42
    /** @var moodle_database A moodle_database driver specific instance.*/
43
    protected $mdb;
44
 
45
    /** @var sql_generator A driver specific SQL generator instance. Public because XMLDB editor needs to access it.*/
46
    public $generator;
47
 
48
    /**
49
     * Creates a new database manager instance.
50
     * @param moodle_database $mdb A moodle_database driver specific instance.
51
     * @param sql_generator $generator A driver specific SQL generator instance.
52
     */
53
    public function __construct($mdb, $generator) {
54
        $this->mdb       = $mdb;
55
        $this->generator = $generator;
56
    }
57
 
58
    /**
59
     * Releases all resources
60
     */
61
    public function dispose() {
62
        if ($this->generator) {
63
            $this->generator->dispose();
64
            $this->generator = null;
65
        }
66
        $this->mdb = null;
67
    }
68
 
69
    /**
70
     * This function will execute an array of SQL commands.
71
     *
72
     * @param string[] $sqlarr Array of sql statements to execute.
73
     * @param array|null $tablenames an array of xmldb table names affected by this request.
74
     * @throws ddl_change_structure_exception This exception is thrown if any error is found.
75
     */
76
    protected function execute_sql_arr(array $sqlarr, $tablenames = null) {
77
        $this->mdb->change_database_structure($sqlarr, $tablenames);
78
    }
79
 
80
    /**
81
     * Execute a given sql command string.
82
     *
83
     * @param string $sql The sql string you wish to be executed.
84
     * @throws ddl_change_structure_exception This exception is thrown if any error is found.
85
     */
86
    protected function execute_sql($sql) {
87
        $this->mdb->change_database_structure($sql);
88
    }
89
 
90
    /**
91
     * Given one xmldb_table, check if it exists in DB (true/false).
92
     *
93
     * @param string|xmldb_table $table The table to be searched (string name or xmldb_table instance).
94
     * @return bool True is a table exists, false otherwise.
95
     */
96
    public function table_exists($table) {
97
        if (!is_string($table) and !($table instanceof xmldb_table)) {
98
            throw new ddl_exception('ddlunknownerror', NULL, 'incorrect table parameter!');
99
        }
100
        return $this->generator->table_exists($table);
101
    }
102
 
103
    /**
104
     * Reset a sequence to the id field of a table.
105
     * @param string|xmldb_table $table Name of table.
106
     * @throws ddl_exception thrown upon reset errors.
107
     */
108
    public function reset_sequence($table) {
109
        if (!is_string($table) and !($table instanceof xmldb_table)) {
110
            throw new ddl_exception('ddlunknownerror', NULL, 'incorrect table parameter!');
111
        } else {
112
            if ($table instanceof xmldb_table) {
113
                $tablename = $table->getName();
114
            } else {
115
                $tablename = $table;
116
            }
117
        }
118
 
119
        // Do not test if table exists because it is slow
120
 
121
        if (!$sqlarr = $this->generator->getResetSequenceSQL($table)) {
122
            throw new ddl_exception('ddlunknownerror', null, 'table reset sequence sql not generated');
123
        }
124
 
125
        $this->execute_sql_arr($sqlarr, array($tablename));
126
    }
127
 
128
    /**
129
     * Given one xmldb_field, check if it exists in DB (true/false).
130
     *
131
     * @param string|xmldb_table $table The table to be searched (string name or xmldb_table instance).
132
     * @param string|xmldb_field $field The field to be searched for (string name or xmldb_field instance).
133
     * @return boolean true is exists false otherwise.
134
     * @throws ddl_table_missing_exception
135
     */
136
    public function field_exists($table, $field) {
137
        // Calculate the name of the table
138
        if (is_string($table)) {
139
            $tablename = $table;
140
        } else {
141
            $tablename = $table->getName();
142
        }
143
 
144
        // Check the table exists
145
        if (!$this->table_exists($table)) {
146
            throw new ddl_table_missing_exception($tablename);
147
        }
148
 
149
        if (is_string($field)) {
150
            $fieldname = $field;
151
        } else {
152
            // Calculate the name of the table
153
            $fieldname = $field->getName();
154
        }
155
 
156
        // Get list of fields in table
157
        $columns = $this->mdb->get_columns($tablename);
158
 
159
        $exists = array_key_exists($fieldname,  $columns);
160
 
161
        return $exists;
162
    }
163
 
164
    /**
165
     * Given one xmldb_index, the function returns the name of the index in DB
166
     * of false if it doesn't exist
167
     *
168
     * @param xmldb_table $xmldb_table table to be searched
169
     * @param xmldb_index $xmldb_index the index to be searched
170
     * @param bool $returnall true means return array of all indexes, false means first index only as string
171
     * @return array|string|bool Index name, array of index names or false if no indexes are found.
172
     * @throws ddl_table_missing_exception Thrown when table is not found.
173
     */
174
    public function find_index_name(xmldb_table $xmldb_table, xmldb_index $xmldb_index, $returnall = false) {
175
        // Calculate the name of the table
176
        $tablename = $xmldb_table->getName();
177
 
178
        // Check the table exists
179
        if (!$this->table_exists($xmldb_table)) {
180
            throw new ddl_table_missing_exception($tablename);
181
        }
182
 
183
        // Extract index columns
184
        $indcolumns = $xmldb_index->getFields();
185
 
186
        // Get list of indexes in table
187
        $indexes = $this->mdb->get_indexes($tablename);
188
 
189
        $return = array();
190
 
191
        // Iterate over them looking for columns coincidence
192
        foreach ($indexes as $indexname => $index) {
193
            $columns = $index['columns'];
194
            // Check if index matches queried index
195
            $diferences = array_merge(array_diff($columns, $indcolumns), array_diff($indcolumns, $columns));
196
            // If no differences, we have find the index
197
            if (empty($diferences)) {
198
                if ($returnall) {
199
                    $return[] = $indexname;
200
                } else {
201
                    return $indexname;
202
                }
203
            }
204
        }
205
 
206
        if ($return and $returnall) {
207
            return $return;
208
        }
209
 
210
        // Arriving here, index not found
211
        return false;
212
    }
213
 
214
    /**
215
     * Given one xmldb_index, check if it exists in DB (true/false).
216
     *
217
     * @param xmldb_table $xmldb_table The table to be searched.
218
     * @param xmldb_index $xmldb_index The index to be searched for.
219
     * @return boolean true id index exists, false otherwise.
220
     */
221
    public function index_exists(xmldb_table $xmldb_table, xmldb_index $xmldb_index) {
222
        if (!$this->table_exists($xmldb_table)) {
223
            return false;
224
        }
225
        return ($this->find_index_name($xmldb_table, $xmldb_index) !== false);
226
    }
227
 
228
    /**
229
     * This function IS NOT IMPLEMENTED. ONCE WE'LL BE USING RELATIONAL
230
     * INTEGRITY IT WILL BECOME MORE USEFUL. FOR NOW, JUST CALCULATE "OFFICIAL"
231
     * KEY NAMES WITHOUT ACCESSING TO DB AT ALL.
232
     * Given one xmldb_key, the function returns the name of the key in DB (if exists)
233
     * of false if it doesn't exist
234
     *
235
     * @param xmldb_table $xmldb_table The table to be searched.
236
     * @param xmldb_key $xmldb_key The key to be searched.
237
     * @return string key name if found
238
     */
239
    public function find_key_name(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
240
 
241
        $keycolumns = $xmldb_key->getFields();
242
 
243
        // Get list of keys in table
244
        // first primaries (we aren't going to use this now, because the MetaPrimaryKeys is awful)
245
            //TODO: To implement when we advance in relational integrity
246
        // then uniques (note that Moodle, for now, shouldn't have any UNIQUE KEY for now, but unique indexes)
247
            //TODO: To implement when we advance in relational integrity (note that AdoDB hasn't any MetaXXX for this.
248
        // then foreign (note that Moodle, for now, shouldn't have any FOREIGN KEY for now, but indexes)
249
            //TODO: To implement when we advance in relational integrity (note that AdoDB has one MetaForeignKeys()
250
            //but it's far from perfect.
251
        // TODO: To create the proper functions inside each generator to retrieve all the needed KEY info (name
252
        //       columns, reftable and refcolumns
253
 
254
        // So all we do is to return the official name of the requested key without any confirmation!)
255
        // One exception, hardcoded primary constraint names
256
        if ($this->generator->primary_key_name && $xmldb_key->getType() == XMLDB_KEY_PRIMARY) {
257
            return $this->generator->primary_key_name;
258
        } else {
259
            // Calculate the name suffix
260
            switch ($xmldb_key->getType()) {
261
                case XMLDB_KEY_PRIMARY:
262
                    $suffix = 'pk';
263
                    break;
264
                case XMLDB_KEY_UNIQUE:
265
                    $suffix = 'uk';
266
                    break;
267
                case XMLDB_KEY_FOREIGN_UNIQUE:
268
                case XMLDB_KEY_FOREIGN:
269
                    $suffix = 'fk';
270
                    break;
271
            }
272
            // And simply, return the official name
273
            return $this->generator->getNameForObject($xmldb_table->getName(), implode(', ', $xmldb_key->getFields()), $suffix);
274
        }
275
    }
276
 
277
    /**
278
     * This function will delete all tables found in XMLDB file from db
279
     *
280
     * @param string $file Full path to the XML file to be used.
281
     * @return void
282
     */
283
    public function delete_tables_from_xmldb_file($file) {
284
 
285
        $xmldb_file = new xmldb_file($file);
286
 
287
        if (!$xmldb_file->fileExists()) {
288
            throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist');
289
        }
290
 
291
        $loaded    = $xmldb_file->loadXMLStructure();
292
        $structure = $xmldb_file->getStructure();
293
 
294
        if (!$loaded || !$xmldb_file->isLoaded()) {
295
            // Show info about the error if we can find it
296
            if ($structure) {
297
                if ($errors = $structure->getAllErrors()) {
298
                    throw new ddl_exception('ddlxmlfileerror', null, 'Errors found in XMLDB file: '. implode (', ', $errors));
299
                }
300
            }
301
            throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??');
302
        }
303
 
304
        if ($xmldb_tables = $structure->getTables()) {
305
            // Delete in opposite order, this should help with foreign keys in the future.
306
            $xmldb_tables = array_reverse($xmldb_tables);
307
            foreach($xmldb_tables as $table) {
308
                if ($this->table_exists($table)) {
309
                    $this->drop_table($table);
310
                }
311
            }
312
        }
313
    }
314
 
315
    /**
316
     * This function will drop the table passed as argument
317
     * and all the associated objects (keys, indexes, constraints, sequences, triggers)
318
     * will be dropped too.
319
     *
320
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
321
     * @return void
322
     */
323
    public function drop_table(xmldb_table $xmldb_table) {
324
        // Check table exists
325
        if (!$this->table_exists($xmldb_table)) {
326
            throw new ddl_table_missing_exception($xmldb_table->getName());
327
        }
328
 
329
        if (!$sqlarr = $this->generator->getDropTableSQL($xmldb_table)) {
330
            throw new ddl_exception('ddlunknownerror', null, 'table drop sql not generated');
331
        }
332
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
333
 
334
        $this->generator->cleanup_after_drop($xmldb_table);
335
    }
336
 
337
    /**
338
     * Load an install.xml file, checking that it exists, and that the structure is OK.
339
     * @param string $file the full path to the XMLDB file.
340
     * @return xmldb_file the loaded file.
341
     */
342
    private function load_xmldb_file($file) {
343
        $xmldb_file = new xmldb_file($file);
344
 
345
        if (!$xmldb_file->fileExists()) {
346
            throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist');
347
        }
348
 
349
        $loaded = $xmldb_file->loadXMLStructure();
350
        if (!$loaded || !$xmldb_file->isLoaded()) {
351
            // Show info about the error if we can find it
352
            if ($structure = $xmldb_file->getStructure()) {
353
                if ($errors = $structure->getAllErrors()) {
354
                    throw new ddl_exception('ddlxmlfileerror', null, 'Errors found in XMLDB file: '. implode (', ', $errors));
355
                }
356
            }
357
            throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??');
358
        }
359
 
360
        return $xmldb_file;
361
    }
362
 
363
    /**
364
     * This function will load one entire XMLDB file and call install_from_xmldb_structure.
365
     *
366
     * @param string $file full path to the XML file to be used
367
     * @return void
368
     */
369
    public function install_from_xmldb_file($file) {
370
        $xmldb_file = $this->load_xmldb_file($file);
371
        $xmldb_structure = $xmldb_file->getStructure();
372
        $this->install_from_xmldb_structure($xmldb_structure);
373
    }
374
 
375
    /**
376
     * This function will load one entire XMLDB file and call install_from_xmldb_structure.
377
     *
378
     * @param string $file full path to the XML file to be used
379
     * @param string $tablename the name of the table.
380
     * @param bool $cachestructures boolean to decide if loaded xmldb structures can be safely cached
381
     *             useful for testunits loading the enormous main xml file hundred of times (100x)
382
     */
383
    public function install_one_table_from_xmldb_file($file, $tablename, $cachestructures = false) {
384
 
385
        static $xmldbstructurecache = array(); // To store cached structures
386
        if (!empty($xmldbstructurecache) && array_key_exists($file, $xmldbstructurecache)) {
387
            $xmldb_structure = $xmldbstructurecache[$file];
388
        } else {
389
            $xmldb_file = $this->load_xmldb_file($file);
390
            $xmldb_structure = $xmldb_file->getStructure();
391
            if ($cachestructures) {
392
                $xmldbstructurecache[$file] = $xmldb_structure;
393
            }
394
        }
395
 
396
        $targettable = $xmldb_structure->getTable($tablename);
397
        if (is_null($targettable)) {
398
            throw new ddl_exception('ddlunknowntable', null, 'The table ' . $tablename . ' is not defined in file ' . $file);
399
        }
400
        $targettable->setNext(NULL);
401
        $targettable->setPrevious(NULL);
402
 
403
        $tempstructure = new xmldb_structure('temp');
404
        $tempstructure->addTable($targettable);
405
        $this->install_from_xmldb_structure($tempstructure);
406
    }
407
 
408
    /**
409
     * This function will generate all the needed SQL statements, specific for each
410
     * RDBMS type and, finally, it will execute all those statements against the DB.
411
     *
412
     * @param stdClass $xmldb_structure xmldb_structure object.
413
     * @return void
414
     */
415
    public function install_from_xmldb_structure($xmldb_structure) {
416
 
417
        if (!$sqlarr = $this->generator->getCreateStructureSQL($xmldb_structure)) {
418
            return; // nothing to do
419
        }
420
 
421
        $tablenames = array();
422
        foreach ($xmldb_structure as $xmldb_table) {
423
            if ($xmldb_table instanceof xmldb_table) {
424
                $tablenames[] = $xmldb_table->getName();
425
            }
426
        }
427
        $this->execute_sql_arr($sqlarr, $tablenames);
428
    }
429
 
430
    /**
431
     * This function will create the table passed as argument with all its
432
     * fields/keys/indexes/sequences, everything based in the XMLDB object
433
     *
434
     * @param xmldb_table $xmldb_table Table object (full specs are required).
435
     * @return void
436
     */
437
    public function create_table(xmldb_table $xmldb_table) {
438
        // Check table doesn't exist
439
        if ($this->table_exists($xmldb_table)) {
440
            throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName());
441
        }
442
 
443
        if (!$sqlarr = $this->generator->getCreateTableSQL($xmldb_table)) {
444
            throw new ddl_exception('ddlunknownerror', null, 'table create sql not generated');
445
        }
446
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
447
    }
448
 
449
    /**
450
     * This function will create the temporary table passed as argument with all its
451
     * fields/keys/indexes/sequences, everything based in the XMLDB object
452
     *
453
     * If table already exists ddl_exception will be thrown, please make sure
454
     * the table name does not collide with existing normal table!
455
     *
456
     * @param xmldb_table $xmldb_table Table object (full specs are required).
457
     * @return void
458
     */
459
    public function create_temp_table(xmldb_table $xmldb_table) {
460
 
461
        // Check table doesn't exist
462
        if ($this->table_exists($xmldb_table)) {
463
            throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName());
464
        }
465
 
466
        if (!$sqlarr = $this->generator->getCreateTempTableSQL($xmldb_table)) {
467
            throw new ddl_exception('ddlunknownerror', null, 'temp table create sql not generated');
468
        }
469
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
470
    }
471
 
472
    /**
473
     * This function will drop the temporary table passed as argument with all its
474
     * fields/keys/indexes/sequences, everything based in the XMLDB object
475
     *
476
     * It is recommended to drop temp table when not used anymore.
477
     *
478
     * @deprecated since 2.3, use drop_table() for all table types
479
     * @param xmldb_table $xmldb_table Table object.
480
     * @return void
481
     */
482
    public function drop_temp_table(xmldb_table $xmldb_table) {
483
        debugging('database_manager::drop_temp_table() is deprecated, use database_manager::drop_table() instead');
484
        $this->drop_table($xmldb_table);
485
    }
486
 
487
    /**
488
     * This function will rename the table passed as argument
489
     * Before renaming the index, the function will check it exists
490
     *
491
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
492
     * @param string $newname New name of the index.
493
     * @return void
494
     */
495
    public function rename_table(xmldb_table $xmldb_table, $newname) {
496
        // Check newname isn't empty
497
        if (!$newname) {
498
            throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
499
        }
500
 
501
        $check = new xmldb_table($newname);
502
 
503
        // Check table already renamed
504
        if (!$this->table_exists($xmldb_table)) {
505
            if ($this->table_exists($check)) {
506
                throw new ddl_exception('ddlunknownerror', null, 'table probably already renamed');
507
            } else {
508
                throw new ddl_table_missing_exception($xmldb_table->getName());
509
            }
510
        }
511
 
512
        // Check new table doesn't exist
513
        if ($this->table_exists($check)) {
514
            throw new ddl_exception('ddltablealreadyexists', $check->getName(), 'can not rename table');
515
        }
516
 
517
        if (!$sqlarr = $this->generator->getRenameTableSQL($xmldb_table, $newname)) {
518
            throw new ddl_exception('ddlunknownerror', null, 'table rename sql not generated');
519
        }
520
 
521
        $this->execute_sql_arr($sqlarr);
522
    }
523
 
524
    /**
525
     * This function will add the field to the table passed as arguments
526
     *
527
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
528
     * @param xmldb_field $xmldb_field Index object (full specs are required).
529
     * @return void
530
     */
531
    public function add_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
532
         // Check the field doesn't exist
533
        if ($this->field_exists($xmldb_table, $xmldb_field)) {
534
            throw new ddl_exception('ddlfieldalreadyexists', $xmldb_field->getName());
535
        }
536
 
537
        // If NOT NULL and no default given (we ask the generator about the
538
        // *real* default that will be used) check the table is empty
539
        if ($xmldb_field->getNotNull() && $this->generator->getDefaultValue($xmldb_field) === NULL && $this->mdb->count_records($xmldb_table->getName())) {
540
            throw new ddl_exception('ddlunknownerror', null, 'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
541
                      ' cannot be added. Not null fields added to non empty tables require default value. Create skipped');
542
        }
543
 
544
        if (!$sqlarr = $this->generator->getAddFieldSQL($xmldb_table, $xmldb_field)) {
545
            throw new ddl_exception('ddlunknownerror', null, 'addfield sql not generated');
546
        }
547
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
548
    }
549
 
550
    /**
551
     * This function will drop the field from the table passed as arguments
552
     *
553
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
554
     * @param xmldb_field $xmldb_field Index object (full specs are required).
555
     * @return void
556
     */
557
    public function drop_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
558
        if (!$this->table_exists($xmldb_table)) {
559
            throw new ddl_table_missing_exception($xmldb_table->getName());
560
        }
561
        // Check the field exists
562
        if (!$this->field_exists($xmldb_table, $xmldb_field)) {
563
            throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
564
        }
565
        // Check for dependencies in the DB before performing any action
566
        $this->check_field_dependencies($xmldb_table, $xmldb_field);
567
 
568
        if (!$sqlarr = $this->generator->getDropFieldSQL($xmldb_table, $xmldb_field)) {
569
            throw new ddl_exception('ddlunknownerror', null, 'drop_field sql not generated');
570
        }
571
 
572
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
573
    }
574
 
575
    /**
576
     * This function will change the type of the field in the table passed as arguments
577
     *
578
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
579
     * @param xmldb_field $xmldb_field Index object (full specs are required).
580
     * @return void
581
     */
582
    public function change_field_type(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
583
        if (!$this->table_exists($xmldb_table)) {
584
            throw new ddl_table_missing_exception($xmldb_table->getName());
585
        }
586
        // Check the field exists
587
        if (!$this->field_exists($xmldb_table, $xmldb_field)) {
588
            throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
589
        }
590
        // Check for dependencies in the DB before performing any action
591
        $this->check_field_dependencies($xmldb_table, $xmldb_field);
592
 
593
        if (!$sqlarr = $this->generator->getAlterFieldSQL($xmldb_table, $xmldb_field)) {
594
            return; // probably nothing to do
595
        }
596
 
597
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
598
    }
599
 
600
    /**
601
     * This function will change the precision of the field in the table passed as arguments
602
     *
603
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
604
     * @param xmldb_field $xmldb_field Index object (full specs are required).
605
     * @return void
606
     */
607
    public function change_field_precision(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
608
        // Just a wrapper over change_field_type. Does exactly the same processing
609
        $this->change_field_type($xmldb_table, $xmldb_field);
610
    }
611
 
612
    /**
613
     * This function will change the unsigned/signed of the field in the table passed as arguments
614
     *
615
     * @deprecated since 2.3, only singed numbers are allowed now, migration is automatic
616
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
617
     * @param xmldb_field $xmldb_field Field object (full specs are required).
618
     * @return void
619
     */
620
    public function change_field_unsigned(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
621
        debugging('All unsigned numbers are converted to signed automatically during Moodle upgrade.');
622
        $this->change_field_type($xmldb_table, $xmldb_field);
623
    }
624
 
625
    /**
626
     * This function will change the nullability of the field in the table passed as arguments
627
     *
628
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
629
     * @param xmldb_field $xmldb_field Index object (full specs are required).
630
     * @return void
631
     */
632
    public function change_field_notnull(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
633
        // Just a wrapper over change_field_type. Does exactly the same processing
634
        $this->change_field_type($xmldb_table, $xmldb_field);
635
    }
636
 
637
    /**
638
     * This function will change the default of the field in the table passed as arguments
639
     * One null value in the default field means delete the default
640
     *
641
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
642
     * @param xmldb_field $xmldb_field Index object (full specs are required).
643
     * @return void
644
     */
645
    public function change_field_default(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
646
        if (!$this->table_exists($xmldb_table)) {
647
            throw new ddl_table_missing_exception($xmldb_table->getName());
648
        }
649
        // Check the field exists
650
        if (!$this->field_exists($xmldb_table, $xmldb_field)) {
651
            throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
652
        }
653
        // Check for dependencies in the DB before performing any action
654
        $this->check_field_dependencies($xmldb_table, $xmldb_field);
655
 
656
        if (!$sqlarr = $this->generator->getModifyDefaultSQL($xmldb_table, $xmldb_field)) {
657
            return; //Empty array = nothing to do = no error
658
        }
659
 
660
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
661
    }
662
 
663
    /**
664
     * This function will rename the field in the table passed as arguments
665
     * Before renaming the field, the function will check it exists
666
     *
667
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
668
     * @param xmldb_field $xmldb_field Index object (full specs are required).
669
     * @param string $newname New name of the field.
670
     * @return void
671
     */
672
    public function rename_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field, $newname) {
673
        if (empty($newname)) {
674
            throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
675
        }
676
 
677
        if (!$this->table_exists($xmldb_table)) {
678
            throw new ddl_table_missing_exception($xmldb_table->getName());
679
        }
680
 
681
        // Check the field exists
682
        if (!$this->field_exists($xmldb_table, $xmldb_field)) {
683
            throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
684
        }
685
 
686
        // Check we have included full field specs
687
        if (!$xmldb_field->getType()) {
688
            throw new ddl_exception('ddlunknownerror', null,
689
                      'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
690
                      ' must contain full specs. Rename skipped');
691
        }
692
 
693
        // Check field isn't id. Renaming over that field is not allowed
694
        if ($xmldb_field->getName() == 'id') {
695
            throw new ddl_exception('ddlunknownerror', null,
696
                      'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
697
                      ' cannot be renamed. Rename skipped');
698
        }
699
 
700
        if (!$sqlarr = $this->generator->getRenameFieldSQL($xmldb_table, $xmldb_field, $newname)) {
701
            return; //Empty array = nothing to do = no error
702
        }
703
 
704
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
705
    }
706
 
707
    /**
708
     * This function will check, for the given table and field, if there there is any dependency
709
     * preventing the field to be modified. It's used by all the public methods that perform any
710
     * DDL change on fields, throwing one ddl_dependency_exception if dependencies are found.
711
     *
712
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
713
     * @param xmldb_field $xmldb_field Index object (full specs are required).
714
     * @return void
715
     * @throws ddl_dependency_exception|ddl_field_missing_exception|ddl_table_missing_exception if dependency not met.
716
     */
717
    private function check_field_dependencies(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
718
 
719
        // Check the table exists
720
        if (!$this->table_exists($xmldb_table)) {
721
            throw new ddl_table_missing_exception($xmldb_table->getName());
722
        }
723
 
724
        // Check the field exists
725
        if (!$this->field_exists($xmldb_table, $xmldb_field)) {
726
            throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
727
        }
728
 
729
        // Check the field isn't in use by any index in the table
730
        if ($indexes = $this->mdb->get_indexes($xmldb_table->getName(), false)) {
731
            foreach ($indexes as $indexname => $index) {
732
                $columns = $index['columns'];
733
                if (in_array($xmldb_field->getName(), $columns)) {
734
                    throw new ddl_dependency_exception('column', $xmldb_table->getName() . '->' . $xmldb_field->getName(),
735
                                                       'index', $indexname . ' (' . implode(', ', $columns)  . ')');
736
                }
737
            }
738
        }
739
    }
740
 
741
    /**
742
     * This function will create the key in the table passed as arguments
743
     *
744
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
745
     * @param xmldb_key $xmldb_key Index object (full specs are required).
746
     * @return void
747
     */
748
    public function add_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
749
 
750
        if ($xmldb_key->getType() == XMLDB_KEY_PRIMARY) { // Prevent PRIMARY to be added (only in create table, being serious  :-P)
751
            throw new ddl_exception('ddlunknownerror', null, 'Primary Keys can be added at table create time only');
752
        }
753
 
754
        if (!$sqlarr = $this->generator->getAddKeySQL($xmldb_table, $xmldb_key)) {
755
            return; //Empty array = nothing to do = no error
756
        }
757
 
758
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
759
    }
760
 
761
    /**
762
     * This function will drop the key in the table passed as arguments
763
     *
764
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
765
     * @param xmldb_key $xmldb_key Key object (full specs are required).
766
     * @return void
767
     */
768
    public function drop_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
769
        if ($xmldb_key->getType() == XMLDB_KEY_PRIMARY) { // Prevent PRIMARY to be dropped (only in drop table, being serious  :-P)
770
            throw new ddl_exception('ddlunknownerror', null, 'Primary Keys can be deleted at table drop time only');
771
        }
772
 
773
        if (!$sqlarr = $this->generator->getDropKeySQL($xmldb_table, $xmldb_key)) {
774
            return; //Empty array = nothing to do = no error
775
        }
776
 
777
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
778
    }
779
 
780
    /**
781
     * This function will rename the key in the table passed as arguments
782
     * Experimental. Shouldn't be used at all in normal installation/upgrade!
783
     *
784
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
785
     * @param xmldb_key $xmldb_key key object (full specs are required).
786
     * @param string $newname New name of the key.
787
     * @return void
788
     */
789
    public function rename_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key, $newname) {
790
        debugging('rename_key() is one experimental feature. You must not use it in production!', DEBUG_DEVELOPER);
791
 
792
        // Check newname isn't empty
793
        if (!$newname) {
794
            throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
795
        }
796
 
797
        if (!$sqlarr = $this->generator->getRenameKeySQL($xmldb_table, $xmldb_key, $newname)) {
798
            throw new ddl_exception('ddlunknownerror', null, 'Some DBs do not support key renaming (MySQL, PostgreSQL, MsSQL). Rename skipped');
799
        }
800
 
801
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
802
    }
803
 
804
    /**
805
     * This function will create the index in the table passed as arguments
806
     * Before creating the index, the function will check it doesn't exists
807
     *
808
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
809
     * @param xmldb_index $xmldb_intex Index object (full specs are required).
810
     * @return void
811
     */
812
    public function add_index($xmldb_table, $xmldb_intex) {
813
        if (!$this->table_exists($xmldb_table)) {
814
            throw new ddl_table_missing_exception($xmldb_table->getName());
815
        }
816
 
817
        // Check index doesn't exist
818
        if ($this->index_exists($xmldb_table, $xmldb_intex)) {
819
            throw new ddl_exception('ddlunknownerror', null,
820
                      'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .
821
                      ' already exists. Create skipped');
822
        }
823
 
824
        if (!$sqlarr = $this->generator->getAddIndexSQL($xmldb_table, $xmldb_intex)) {
825
            throw new ddl_exception('ddlunknownerror', null, 'add_index sql not generated');
826
        }
827
 
828
        try {
829
            $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
830
        } catch (ddl_change_structure_exception $e) {
831
            // There could be a problem with the index length related to the row format of the table.
832
            // If we are using utf8mb4 and the row format is 'compact' or 'redundant' then we need to change it over to
833
            // 'compressed' or 'dynamic'.
834
            if (method_exists($this->mdb, 'convert_table_row_format')) {
835
                $this->mdb->convert_table_row_format($xmldb_table->getName());
836
                $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
837
            } else {
838
                // It's some other problem that we are currently not handling.
839
                throw $e;
840
            }
841
        }
842
    }
843
 
844
    /**
845
     * This function will drop the index in the table passed as arguments
846
     * Before dropping the index, the function will check it exists
847
     *
848
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
849
     * @param xmldb_index $xmldb_intex Index object (full specs are required).
850
     * @return void
851
     */
852
    public function drop_index($xmldb_table, $xmldb_intex) {
853
        if (!$this->table_exists($xmldb_table)) {
854
            throw new ddl_table_missing_exception($xmldb_table->getName());
855
        }
856
 
857
        // Check index exists
858
        if (!$this->index_exists($xmldb_table, $xmldb_intex)) {
859
            throw new ddl_exception('ddlunknownerror', null,
860
                      'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .
861
                      ' does not exist. Drop skipped');
862
        }
863
 
864
        if (!$sqlarr = $this->generator->getDropIndexSQL($xmldb_table, $xmldb_intex)) {
865
            throw new ddl_exception('ddlunknownerror', null, 'drop_index sql not generated');
866
        }
867
 
868
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
869
    }
870
 
871
    /**
872
     * This function will rename the index in the table passed as arguments
873
     * Before renaming the index, the function will check it exists
874
     * Experimental. Shouldn't be used at all!
875
     *
876
     * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
877
     * @param xmldb_index $xmldb_intex Index object (full specs are required).
878
     * @param string $newname New name of the index.
879
     * @return void
880
     */
881
    public function rename_index($xmldb_table, $xmldb_intex, $newname) {
882
        debugging('rename_index() is one experimental feature. You must not use it in production!', DEBUG_DEVELOPER);
883
 
884
        // Check newname isn't empty
885
        if (!$newname) {
886
            throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
887
        }
888
 
889
        // Check index exists
890
        if (!$this->index_exists($xmldb_table, $xmldb_intex)) {
891
            throw new ddl_exception('ddlunknownerror', null,
892
                      'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .
893
                      ' does not exist. Rename skipped');
894
        }
895
 
896
        if (!$sqlarr = $this->generator->getRenameIndexSQL($xmldb_table, $xmldb_intex, $newname)) {
897
            throw new ddl_exception('ddlunknownerror', null, 'Some DBs do not support index renaming (MySQL). Rename skipped');
898
        }
899
 
900
        $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
901
    }
902
 
903
    /**
904
     * Get the list of install.xml files.
905
     *
906
     * @return array
907
     */
908
    public function get_install_xml_files(): array {
909
        global $CFG;
910
        require_once($CFG->libdir.'/adminlib.php');
911
 
912
        $files = [];
913
        $dbdirs = get_db_directories();
914
        foreach ($dbdirs as $dbdir) {
915
            $filename = "{$dbdir}/install.xml";
916
            if (file_exists($filename)) {
917
                $files[] = $filename;
918
            }
919
        }
920
 
921
        return $files;
922
    }
923
 
924
    /**
925
     * Reads the install.xml files for Moodle core and modules and returns an array of
926
     * xmldb_structure object with xmldb_table from these files.
927
     * @return xmldb_structure schema from install.xml files
928
     */
929
    public function get_install_xml_schema() {
930
        global $CFG;
931
        require_once($CFG->libdir.'/adminlib.php');
932
 
933
        $schema = new xmldb_structure('export');
934
        $schema->setVersion($CFG->version);
935
 
936
        foreach ($this->get_install_xml_files() as $filename) {
937
            $xmldb_file = new xmldb_file($filename);
938
            if (!$xmldb_file->loadXMLStructure()) {
939
                continue;
940
            }
941
            $structure = $xmldb_file->getStructure();
942
            $tables = $structure->getTables();
943
            foreach ($tables as $table) {
944
                $table->setPrevious(null);
945
                $table->setNext(null);
946
                $schema->addTable($table);
947
            }
948
        }
949
        return $schema;
950
    }
951
 
952
    /**
953
     * Checks the database schema against a schema specified by an xmldb_structure object
954
     * @param xmldb_structure $schema export schema describing all known tables
955
     * @param array $options
956
     * @return array keyed by table name with array of difference messages as values
957
     */
958
    public function check_database_schema(xmldb_structure $schema, array $options = null) {
959
        $alloptions = array(
960
            'extratables' => true,
961
            'missingtables' => true,
962
            'extracolumns' => true,
963
            'missingcolumns' => true,
964
            'changedcolumns' => true,
965
            'missingindexes' => true,
966
            'extraindexes' => true
967
        );
968
 
969
        $typesmap = array(
970
            'I' => XMLDB_TYPE_INTEGER,
971
            'R' => XMLDB_TYPE_INTEGER,
972
            'N' => XMLDB_TYPE_NUMBER,
973
            'F' => XMLDB_TYPE_NUMBER, // Nobody should be using floats!
974
            'C' => XMLDB_TYPE_CHAR,
975
            'X' => XMLDB_TYPE_TEXT,
976
            'B' => XMLDB_TYPE_BINARY,
977
            'T' => XMLDB_TYPE_TIMESTAMP,
978
            'D' => XMLDB_TYPE_DATETIME,
979
        );
980
 
981
        $options = (array)$options;
982
        $options = array_merge($alloptions, $options);
983
 
984
        // Note: the error descriptions are not supposed to be localised,
985
        //       it is intended for developers and skilled admins only.
986
        $errors = array();
987
 
988
        /** @var string[] $dbtables */
989
        $dbtables = $this->mdb->get_tables(false);
990
        /** @var xmldb_table[] $tables */
991
        $tables = $schema->getTables();
992
 
993
        foreach ($tables as $table) {
994
            $tablename = $table->getName();
995
 
996
            if ($options['missingtables']) {
997
                // Missing tables are a fatal problem.
998
                if (empty($dbtables[$tablename])) {
999
                    $errors[$tablename][] = "table is missing";
1000
                    continue;
1001
                }
1002
            }
1003
 
1004
            /** @var database_column_info[] $dbfields */
1005
            $dbfields = $this->mdb->get_columns($tablename, false);
1006
            $dbindexes = $this->mdb->get_indexes($tablename);
1007
            /** @var xmldb_field[] $fields */
1008
            $fields = $table->getFields();
1009
 
1010
            foreach ($fields as $field) {
1011
                $fieldname = $field->getName();
1012
                if (empty($dbfields[$fieldname])) {
1013
                    if ($options['missingcolumns']) {
1014
                        // Missing columns are a fatal problem.
1015
                        $errors[$tablename][] = "column '$fieldname' is missing";
1016
                    }
1017
                } else if ($options['changedcolumns']) {
1018
                    $dbfield = $dbfields[$fieldname];
1019
 
1020
                    if (!isset($typesmap[$dbfield->meta_type])) {
1021
                        $errors[$tablename][] = "column '$fieldname' has unsupported type '$dbfield->meta_type'";
1022
                    } else {
1023
                        $dbtype = $typesmap[$dbfield->meta_type];
1024
                        $type = $field->getType();
1025
                        if ($type == XMLDB_TYPE_FLOAT) {
1026
                            $type = XMLDB_TYPE_NUMBER;
1027
                        }
1028
                        if ($type != $dbtype) {
1029
                            if ($expected = array_search($type, $typesmap)) {
1030
                                $errors[$tablename][] = "column '$fieldname' has incorrect type '$dbfield->meta_type', expected '$expected'";
1031
                            } else {
1032
                                $errors[$tablename][] = "column '$fieldname' has incorrect type '$dbfield->meta_type'";
1033
                            }
1034
                        } else {
1035
                            if ($field->getNotNull() != $dbfield->not_null) {
1036
                                if ($field->getNotNull()) {
1037
                                    $errors[$tablename][] = "column '$fieldname' should be NOT NULL ($dbfield->meta_type)";
1038
                                } else {
1039
                                    $errors[$tablename][] = "column '$fieldname' should allow NULL ($dbfield->meta_type)";
1040
                                }
1041
                            }
1042
                            switch ($dbtype) {
1043
                                case XMLDB_TYPE_TEXT:
1044
                                case XMLDB_TYPE_BINARY:
1045
                                    // No length check necessary - there is one size only now.
1046
                                    break;
1047
 
1048
                                case XMLDB_TYPE_NUMBER:
1049
                                    $lengthmismatch = $field->getLength() != $dbfield->max_length;
1050
                                    $decimalmismatch = $field->getDecimals() != $dbfield->scale;
1051
                                    // Do not use floats in any new code, they are deprecated in XMLDB editor!
1052
                                    if ($field->getType() != XMLDB_TYPE_FLOAT && ($lengthmismatch || $decimalmismatch)) {
1053
                                        $size = "({$field->getLength()},{$field->getDecimals()})";
1054
                                        $dbsize = "($dbfield->max_length,$dbfield->scale)";
1055
                                        $errors[$tablename][] = "column '$fieldname' size is $dbsize,".
1056
                                            " expected $size ($dbfield->meta_type)";
1057
                                    }
1058
                                    break;
1059
 
1060
                                case XMLDB_TYPE_CHAR:
1061
                                    // This is not critical, but they should ideally match.
1062
                                    if ($field->getLength() != $dbfield->max_length) {
1063
                                        $errors[$tablename][] = "column '$fieldname' length is $dbfield->max_length,".
1064
                                            " expected {$field->getLength()} ($dbfield->meta_type)";
1065
                                    }
1066
                                    break;
1067
 
1068
                                case XMLDB_TYPE_INTEGER:
1069
                                    // Integers may be bigger in some DBs.
1070
                                    $length = $field->getLength();
1071
                                    if ($length > 18) {
1072
                                        // Integers are not supposed to be bigger than 18.
1073
                                        $length = 18;
1074
                                    }
1075
                                    if ($length > $dbfield->max_length) {
1076
                                        $errors[$tablename][] = "column '$fieldname' length is $dbfield->max_length,".
1077
                                            " expected at least {$field->getLength()} ($dbfield->meta_type)";
1078
                                    }
1079
                                    break;
1080
 
1081
                                case XMLDB_TYPE_TIMESTAMP:
1082
                                    $errors[$tablename][] = "column '$fieldname' is a timestamp,".
1083
                                        " this type is not supported ($dbfield->meta_type)";
1084
                                    continue 2;
1085
 
1086
                                case XMLDB_TYPE_DATETIME:
1087
                                    $errors[$tablename][] = "column '$fieldname' is a datetime,".
1088
                                        "this type is not supported ($dbfield->meta_type)";
1089
                                    continue 2;
1090
 
1091
                                default:
1092
                                    // Report all other unsupported types as problems.
1093
                                    $errors[$tablename][] = "column '$fieldname' has unknown type ($dbfield->meta_type)";
1094
                                    continue 2;
1095
 
1096
                            }
1097
 
1098
                            // Note: The empty string defaults are a bit messy...
1099
                            if ($field->getDefault() != $dbfield->default_value) {
1100
                                $default = is_null($field->getDefault()) ? 'NULL' : $field->getDefault();
1101
                                $dbdefault = is_null($dbfield->default_value) ? 'NULL' : $dbfield->default_value;
1102
                                $errors[$tablename][] = "column '$fieldname' has default '$dbdefault', expected '$default' ($dbfield->meta_type)";
1103
                            }
1104
                        }
1105
                    }
1106
                }
1107
                unset($dbfields[$fieldname]);
1108
            }
1109
 
1110
            // Check for missing indexes/keys.
1111
            if ($options['missingindexes']) {
1112
                // Check the foreign keys.
1113
                if ($keys = $table->getKeys()) {
1114
                    foreach ($keys as $key) {
1115
                        // Primary keys are skipped.
1116
                        if ($key->getType() == XMLDB_KEY_PRIMARY) {
1117
                            continue;
1118
                        }
1119
 
1120
                        $keyname = $key->getName();
1121
 
1122
                        // Create the interim index.
1123
                        $index = new xmldb_index('anyname');
1124
                        $index->setFields($key->getFields());
1125
                        switch ($key->getType()) {
1126
                            case XMLDB_KEY_UNIQUE:
1127
                            case XMLDB_KEY_FOREIGN_UNIQUE:
1128
                                $index->setUnique(true);
1129
                                break;
1130
                            case XMLDB_KEY_FOREIGN:
1131
                                $index->setUnique(false);
1132
                                break;
1133
                        }
1134
                        if (!$this->index_exists($table, $index)) {
1135
                            $errors[$tablename][] = $this->get_missing_index_error($table, $index, $keyname);
1136
                        } else {
1137
                            $this->remove_index_from_dbindex($dbindexes, $index);
1138
                        }
1139
                    }
1140
                }
1141
 
1142
                // Check the indexes.
1143
                if ($indexes = $table->getIndexes()) {
1144
                    foreach ($indexes as $index) {
1145
                        if (!$this->index_exists($table, $index)) {
1146
                            $errors[$tablename][] = $this->get_missing_index_error($table, $index, $index->getName());
1147
                        } else {
1148
                            $this->remove_index_from_dbindex($dbindexes, $index);
1149
                        }
1150
                    }
1151
                }
1152
            }
1153
 
1154
            // Check if we should show the extra indexes.
1155
            if ($options['extraindexes']) {
1156
                // Hack - skip for table 'search_simpledb_index' as this plugin adds indexes dynamically on install
1157
                // which are not included in install.xml. See search/engine/simpledb/db/install.php.
1158
                if ($tablename != 'search_simpledb_index') {
1159
                    foreach ($dbindexes as $indexname => $index) {
1160
                        $errors[$tablename][] = "Unexpected index '$indexname'.";
1161
                    }
1162
                }
1163
            }
1164
 
1165
            // Check for extra columns (indicates unsupported hacks) - modify install.xml if you want to pass validation.
1166
            foreach ($dbfields as $fieldname => $dbfield) {
1167
                if ($options['extracolumns']) {
1168
                    $errors[$tablename][] = "column '$fieldname' is not expected ($dbfield->meta_type)";
1169
                }
1170
            }
1171
            unset($dbtables[$tablename]);
1172
        }
1173
 
1174
        if ($options['extratables']) {
1175
            // Look for unsupported tables - local custom tables should be in /local/xxxx/db/install.xml file.
1176
            // If there is no prefix, we can not say if table is ours, sorry.
1177
            if ($this->generator->prefix !== '') {
1178
                foreach ($dbtables as $tablename => $unused) {
1179
                    if (strpos($tablename, 'pma_') === 0) {
1180
                        // Ignore phpmyadmin tables.
1181
                        continue;
1182
                    }
1183
                    if (strpos($tablename, 'test') === 0) {
1184
                        // Legacy simple test db tables need to be eventually removed,
1185
                        // report them as problems!
1186
                        $errors[$tablename][] = "table is not expected (it may be a leftover after Simpletest unit tests)";
1187
                    } else {
1188
                        $errors[$tablename][] = "table is not expected";
1189
                    }
1190
                }
1191
            }
1192
        }
1193
 
1194
        return $errors;
1195
    }
1196
 
1197
    /**
1198
     * Returns a string describing the missing index error.
1199
     *
1200
     * @param xmldb_table $table
1201
     * @param xmldb_index $index
1202
     * @param string $indexname
1203
     * @return string
1204
     */
1205
    private function get_missing_index_error(xmldb_table $table, xmldb_index $index, string $indexname): string {
1206
        $sqlarr = $this->generator->getAddIndexSQL($table, $index);
1207
        $sqlarr = $this->generator->getEndedStatements($sqlarr);
1208
        $sqltoadd = reset($sqlarr);
1209
 
1210
        return "Missing index '" . $indexname . "' " . "(" . $index->readableInfo() . "). \n" . $sqltoadd;
1211
    }
1212
 
1213
    /**
1214
     * Removes an index from the array $dbindexes if it is found.
1215
     *
1216
     * @param array $dbindexes
1217
     * @param xmldb_index $index
1218
     */
1219
    private function remove_index_from_dbindex(array &$dbindexes, xmldb_index $index) {
1220
        foreach ($dbindexes as $key => $dbindex) {
1221
            if ($dbindex['columns'] == $index->getFields()) {
1222
                unset($dbindexes[$key]);
1223
            }
1224
        }
1225
    }
1226
}