Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.67% covered (success)
93.67%
74 / 79
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SchemaMaintenance
94.87% covered (success)
94.87%
74 / 78
60.00% covered (warning)
60.00%
3 / 5
26.09
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
1
 canExecuteWithoutLocalSettings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
91.43% covered (success)
91.43%
32 / 35
0.00% covered (danger)
0.00%
0 / 1
18.20
 getSqlPathWithFileName
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 writeSchema
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 generateSchema
n/a
0 / 0
n/a
0 / 0
0
1<?php
2
3/**
4 * Base script for schema maintenance
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 *
21 * @file
22 * @ingroup Maintenance
23 */
24
25namespace MediaWiki\Maintenance;
26
27use MediaWiki\DB\AbstractSchemaValidationError;
28
29// @codeCoverageIgnoreStart
30require_once __DIR__ . '/../Maintenance.php';
31// @codeCoverageIgnoreEnd
32
33abstract class SchemaMaintenance extends Maintenance {
34    public const SUPPORTED_PLATFORMS = [
35        'mysql',
36        'sqlite',
37        'postgres'
38    ];
39
40    public function __construct() {
41        parent::__construct();
42
43        $types = implode( ', ', array_map( static function ( string $type ): string {
44            return "'$type'";
45        }, self::SUPPORTED_PLATFORMS ) );
46
47        $this->addOption(
48            'json',
49            'Path to the json file. Default: tables.json',
50            false,
51            true
52        );
53        $this->addOption(
54            'sql',
55            'Path to output. If --type=all is given, ' .
56            'the output will be placed in a directory named after the dbms. ' .
57            'For mysql, a directory will only be used if it already exists. Default: tables-generated.sql',
58            false,
59            true
60        );
61        $this->addOption(
62            'type',
63            "Can be either $types, or 'all'. Default: mysql",
64            false,
65            true
66        );
67        $this->addOption(
68            'validate',
69            'Validate the schema instead of generating sql files.'
70        );
71    }
72
73    public function canExecuteWithoutLocalSettings(): bool {
74        return true;
75    }
76
77    public function execute() {
78        $platform = $this->getOption( 'type', 'mysql' );
79        $jsonPath = $this->getOption( 'json', '../../sql' );
80
81        if ( $this->hasOption( 'validate' ) ) {
82            try {
83                ( new SchemaGenerator() )->validateAndGetSchema( $jsonPath );
84            } catch ( AbstractSchemaValidationError $e ) {
85                $this->fatalError( $e->getMessage() );
86            }
87
88            $this->output( "Schema is valid.\n" );
89            return;
90        }
91
92        // Allow to specify a folder and use a default name
93        if ( is_dir( $jsonPath ) ) {
94            $jsonPath .= '/tables.json';
95        }
96
97        if ( in_array( $platform, self::SUPPORTED_PLATFORMS, true ) ) {
98            $platforms = [ $platform ];
99        } elseif ( $platform === 'all' ) {
100            $platforms = self::SUPPORTED_PLATFORMS;
101        } else {
102            $this->fatalError( "'$platform' is not a supported platform!" );
103        }
104
105        foreach ( $platforms as $platform ) {
106            $sqlPath = $this->getOption( 'sql', dirname( $jsonPath ) );
107
108            // MediaWiki, and some extensions place mysql .sql files in the directory root, instead of a dedicated
109            // sub directory. If mysql/ doesn't exist, assume that the .sql files should be in the directory root.
110            if (
111                $platform === 'mysql' &&
112                !is_dir( $sqlPath . '/mysql' ) &&
113                !( count( $platforms ) > 1 && is_dir( dirname( $sqlPath ) . '/' . $platform ) )
114            ) {
115                // Allow to specify a folder and build the name from the json filename
116                if ( is_dir( $sqlPath ) ) {
117                    $sqlPath = $this->getSqlPathWithFileName( $jsonPath, $sqlPath );
118                }
119            } else {
120                // Allow to specify a folder and build the name from the json filename
121                if ( is_dir( $sqlPath ) ) {
122                    $sqlPath .= '/' . $platform;
123                    $directory = $sqlPath;
124                    $sqlPath = $this->getSqlPathWithFileName( $jsonPath, $sqlPath );
125                } elseif ( count( $platforms ) > 1 ) {
126                    $directory = dirname( $sqlPath ) . '/' . $platform;
127                    $sqlPath = $directory . '/' . pathinfo( $sqlPath, PATHINFO_FILENAME ) . '.sql';
128                } else {
129                    $directory = false;
130                }
131
132                // The directory for the platform might not exist.
133                if ( $directory !== false && !is_dir( $directory )
134                    && !mkdir( $directory ) && !is_dir( $directory )
135                ) {
136                    $this->error( "Cannot create $directory for $platform" );
137
138                    continue;
139                }
140            }
141
142            $this->writeSchema( $platform, $jsonPath, $sqlPath );
143        }
144    }
145
146    /**
147     * Determine the name of the generated SQL file when only a directory has been provided to --sql.
148     * When --json is given tables.json, tables-generates.sql will be the name, otherwise it will be the name of the
149     * .json file, minus the extension.
150     *
151     * @param string $relativeJsonPath
152     * @param string $sqlPath
153     * @return string
154     */
155    private function getSqlPathWithFileName( string $relativeJsonPath, string $sqlPath ): string {
156        $jsonFilename = pathinfo( $relativeJsonPath, PATHINFO_FILENAME );
157        if ( str_starts_with( $jsonFilename, 'tables' ) ) {
158            $sqlFileName = $jsonFilename . '-generated.sql';
159        } else {
160            $sqlFileName = $jsonFilename . '.sql';
161        }
162
163        return $sqlPath . '/' . $sqlFileName;
164    }
165
166    private function writeSchema(
167        string $platform,
168        string $jsonPath,
169        string $sqlPath
170    ): void {
171        try {
172            $sql = $this->generateSchema( $platform, $jsonPath );
173        } catch ( AbstractSchemaValidationError $e ) {
174            $this->fatalError( $e->getMessage() );
175        }
176
177        // Give a hint, if nothing changed
178        if ( is_readable( $sqlPath ) ) {
179            $oldSql = file_get_contents( $sqlPath );
180            if ( $oldSql === $sql ) {
181                $this->output( "Schema change is unchanged.\n" );
182            }
183        }
184
185        file_put_contents( $sqlPath, $sql );
186        $this->output( 'Schema change generated and written to ' . $sqlPath . "\n" );
187    }
188
189    /**
190     * @throws AbstractSchemaValidationError
191     */
192    abstract protected function generateSchema( string $platform, string $jsonPath ): string;
193}
194
195/** @deprecated class alias since 1.43 */
196class_alias( SchemaMaintenance::class, 'SchemaMaintenance' );