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 * @license GPL-2.0-or-later
7 * @file
8 * @ingroup Maintenance
9 */
10
11namespace MediaWiki\Maintenance;
12
13use MediaWiki\DB\AbstractSchemaValidationError;
14
15// @codeCoverageIgnoreStart
16require_once __DIR__ . '/../Maintenance.php';
17// @codeCoverageIgnoreEnd
18
19abstract class SchemaMaintenance extends Maintenance {
20    public const SUPPORTED_PLATFORMS = [
21        'mysql',
22        'sqlite',
23        'postgres'
24    ];
25
26    public function __construct() {
27        parent::__construct();
28
29        $types = implode( ', ', array_map( static function ( string $type ): string {
30            return "'$type'";
31        }, self::SUPPORTED_PLATFORMS ) );
32
33        $this->addOption(
34            'json',
35            'Path to the json file. Default: tables.json',
36            false,
37            true
38        );
39        $this->addOption(
40            'sql',
41            'Path to output. If --type=all is given, ' .
42            'the output will be placed in a directory named after the dbms. ' .
43            'For mysql, a directory will only be used if it already exists. Default: tables-generated.sql',
44            false,
45            true
46        );
47        $this->addOption(
48            'type',
49            "Can be either $types, or 'all'. Default: mysql",
50            false,
51            true
52        );
53        $this->addOption(
54            'validate',
55            'Validate the schema instead of generating sql files.'
56        );
57    }
58
59    public function canExecuteWithoutLocalSettings(): bool {
60        return true;
61    }
62
63    public function execute() {
64        $platform = $this->getOption( 'type', 'mysql' );
65        $jsonPath = $this->getOption( 'json', MW_INSTALL_PATH . '/sql' );
66
67        if ( $this->hasOption( 'validate' ) ) {
68            try {
69                ( new SchemaGenerator() )->validateAndGetSchema( $jsonPath );
70            } catch ( AbstractSchemaValidationError $e ) {
71                $this->fatalError( $e->getMessage() );
72            }
73
74            $this->output( "Schema is valid.\n" );
75            return;
76        }
77
78        // Allow to specify a folder and use a default name
79        if ( is_dir( $jsonPath ) ) {
80            $jsonPath .= '/tables.json';
81        }
82
83        if ( in_array( $platform, self::SUPPORTED_PLATFORMS, true ) ) {
84            $platforms = [ $platform ];
85        } elseif ( $platform === 'all' ) {
86            $platforms = self::SUPPORTED_PLATFORMS;
87        } else {
88            $this->fatalError( "'$platform' is not a supported platform!" );
89        }
90
91        foreach ( $platforms as $platform ) {
92            $sqlPath = $this->getOption( 'sql', dirname( $jsonPath ) );
93
94            // MediaWiki, and some extensions place mysql .sql files in the directory root, instead of a dedicated
95            // sub directory. If mysql/ doesn't exist, assume that the .sql files should be in the directory root.
96            if (
97                $platform === 'mysql' &&
98                !is_dir( $sqlPath . '/mysql' ) &&
99                !( count( $platforms ) > 1 && is_dir( dirname( $sqlPath ) . '/' . $platform ) )
100            ) {
101                // Allow to specify a folder and build the name from the json filename
102                if ( is_dir( $sqlPath ) ) {
103                    $sqlPath = $this->getSqlPathWithFileName( $jsonPath, $sqlPath );
104                }
105            } else {
106                // Allow to specify a folder and build the name from the json filename
107                if ( is_dir( $sqlPath ) ) {
108                    $sqlPath .= '/' . $platform;
109                    $directory = $sqlPath;
110                    $sqlPath = $this->getSqlPathWithFileName( $jsonPath, $sqlPath );
111                } elseif ( count( $platforms ) > 1 ) {
112                    $directory = dirname( $sqlPath ) . '/' . $platform;
113                    $sqlPath = $directory . '/' . pathinfo( $sqlPath, PATHINFO_FILENAME ) . '.sql';
114                } else {
115                    $directory = false;
116                }
117
118                // The directory for the platform might not exist.
119                if ( $directory !== false && !is_dir( $directory )
120                    && !mkdir( $directory ) && !is_dir( $directory )
121                ) {
122                    $this->error( "Cannot create $directory for $platform" );
123
124                    continue;
125                }
126            }
127
128            $this->writeSchema( $platform, $jsonPath, $sqlPath );
129        }
130    }
131
132    /**
133     * Determine the name of the generated SQL file when only a directory has been provided to --sql.
134     * When --json is given tables.json, tables-generates.sql will be the name, otherwise it will be the name of the
135     * .json file, minus the extension.
136     *
137     * @param string $relativeJsonPath
138     * @param string $sqlPath
139     * @return string
140     */
141    private function getSqlPathWithFileName( string $relativeJsonPath, string $sqlPath ): string {
142        $jsonFilename = pathinfo( $relativeJsonPath, PATHINFO_FILENAME );
143        if ( str_starts_with( $jsonFilename, 'tables' ) ) {
144            $sqlFileName = $jsonFilename . '-generated.sql';
145        } else {
146            $sqlFileName = $jsonFilename . '.sql';
147        }
148
149        return $sqlPath . '/' . $sqlFileName;
150    }
151
152    private function writeSchema(
153        string $platform,
154        string $jsonPath,
155        string $sqlPath
156    ): void {
157        try {
158            $sql = $this->generateSchema( $platform, $jsonPath );
159        } catch ( AbstractSchemaValidationError $e ) {
160            $this->fatalError( $e->getMessage() );
161        }
162
163        // Give a hint, if nothing changed
164        if ( is_readable( $sqlPath ) ) {
165            $oldSql = file_get_contents( $sqlPath );
166            if ( $oldSql === $sql ) {
167                $this->output( "Schema change is unchanged.\n" );
168            }
169        }
170
171        file_put_contents( $sqlPath, $sql );
172        $this->output( 'Schema change generated and written to ' . $sqlPath . "\n" );
173    }
174
175    /**
176     * @throws AbstractSchemaValidationError
177     */
178    abstract protected function generateSchema( string $platform, string $jsonPath ): string;
179}
180
181/** @deprecated class alias since 1.43 */
182class_alias( SchemaMaintenance::class, 'SchemaMaintenance' );