MediaWiki  master
SchemaMaintenance.php
Go to the documentation of this file.
1 <?php
2 
27 
28 require_once __DIR__ . '/../Maintenance.php';
29 
30 abstract class SchemaMaintenance extends Maintenance {
31  public const SUPPORTED_PLATFORMS = [
32  'mysql',
33  'sqlite',
34  'postgres'
35  ];
36 
41  protected $scriptName;
42 
43  public function __construct() {
44  parent::__construct();
45 
46  $types = implode( ', ', array_map( static function ( string $type ): string {
47  return "'$type'";
49 
50  $this->addOption(
51  'json',
52  'Path to the json file. Default: tables.json',
53  false,
54  true
55  );
56  $this->addOption(
57  'sql',
58  'Path to output. If --type=all is given, ' .
59  'the output will be placed in a directory named after the dbms. ' .
60  'For mysql, a directory will only be used if it already exists. Default: tables-generated.sql',
61  false,
62  true
63  );
64  $this->addOption(
65  'type',
66  "Can be either $types, or 'all'. Default: mysql",
67  false,
68  true
69  );
70  $this->addOption(
71  'validate',
72  'Validate the schema instead of generating sql files.'
73  );
74  }
75 
76  public function execute() {
77  global $IP;
78 
79  $platform = $this->getOption( 'type', 'mysql' );
80  $jsonPath = $this->getOption( 'json', dirname( __DIR__ ) );
81 
82  $installPath = $IP;
83 
84  // For windows
85  if ( DIRECTORY_SEPARATOR === '\\' ) {
86  $installPath = strtr( $installPath, '\\', '/' );
87  $jsonPath = strtr( $jsonPath, '\\', '/' );
88  }
89 
90  if ( $this->hasOption( 'validate' ) ) {
91  $this->getSchema( $jsonPath );
92 
93  return;
94  }
95 
96  // Allow to specify a folder and use a default name
97  if ( is_dir( $jsonPath ) ) {
98  $jsonPath .= '/tables.json';
99  }
100 
101  $relativeJsonPath = str_replace(
102  [ "$installPath/extensions/", "$installPath/" ],
103  '',
104  $jsonPath
105  );
106 
107  if ( in_array( $platform, self::SUPPORTED_PLATFORMS, true ) ) {
108  $platforms = [ $platform ];
109  } elseif ( $platform === 'all' ) {
110  $platforms = self::SUPPORTED_PLATFORMS;
111  } else {
112  $this->fatalError( "'$platform' is not a supported platform!" );
113  }
114 
115  foreach ( $platforms as $platform ) {
116  $sqlPath = $this->getOption( 'sql', dirname( $jsonPath ) );
117 
118  // MediaWiki, and some extensions place mysql .sql files in the directory root, instead of a dedicated
119  // sub directory. If mysql/ doesn't exist, assume that the .sql files should be in the directory root.
120  if ( $platform === 'mysql' && !is_dir( $sqlPath . '/mysql' ) ) {
121  // Allow to specify a folder and build the name from the json filename
122  if ( is_dir( $sqlPath ) ) {
123  $sqlPath = $this->getSqlPathWithFileName( $relativeJsonPath, $sqlPath );
124  }
125  } else {
126  // Allow to specify a folder and build the name from the json filename
127  if ( is_dir( $sqlPath ) ) {
128  $sqlPath .= '/' . $platform;
129  $directory = $sqlPath;
130  $sqlPath = $this->getSqlPathWithFileName( $relativeJsonPath, $sqlPath );
131  } elseif ( count( $platforms ) > 1 ) {
132  $directory = dirname( $sqlPath ) . '/' . $platform;
133  $sqlPath = $directory . '/' . pathinfo( $sqlPath, PATHINFO_FILENAME ) . '.sql';
134  } else {
135  $directory = false;
136  }
137 
138  // The directory for the platform might not exist.
139  if ( $directory !== false && !is_dir( $directory )
140  && !mkdir( $directory ) && !is_dir( $directory )
141  ) {
142  $this->error( "Cannot create $directory for $platform" );
143 
144  continue;
145  }
146  }
147 
148  $this->writeSchema( $platform, $jsonPath, $relativeJsonPath, $sqlPath );
149  }
150  }
151 
161  private function getSqlPathWithFileName( string $relativeJsonPath, string $sqlPath ): string {
162  $jsonFilename = pathinfo( $relativeJsonPath, PATHINFO_FILENAME );
163  if ( str_starts_with( $jsonFilename, 'tables' ) ) {
164  $sqlFileName = $jsonFilename . '-generated.sql';
165  } else {
166  $sqlFileName = $jsonFilename . '.sql';
167  }
168 
169  return $sqlPath . '/' . $sqlFileName;
170  }
171 
172  private function writeSchema(
173  string $platform,
174  string $jsonPath,
175  string $relativeJsonPath,
176  string $sqlPath
177  ): void {
178  $abstractSchemaChange = $this->getSchema( $jsonPath );
179 
180  $sql =
181  "-- This file is automatically generated using maintenance/$this->scriptName.\n" .
182  "-- Source: $relativeJsonPath\n" .
183  "-- Do not modify this file directly.\n" .
184  "-- See https://www.mediawiki.org/wiki/Manual:Schema_changes\n";
185 
186  $sql .= $this->generateSchema( $platform, $abstractSchemaChange );
187 
188  // Give a hint, if nothing changed
189  if ( is_readable( $sqlPath ) ) {
190  $oldSql = file_get_contents( $sqlPath );
191  if ( $oldSql === $sql ) {
192  $this->output( "Schema change is unchanged.\n" );
193  }
194  }
195 
196  file_put_contents( $sqlPath, $sql );
197  $this->output( 'Schema change generated and written to ' . $sqlPath . "\n" );
198  }
199 
200  abstract protected function generateSchema( string $platform, array $schema ): string;
201 
208  private function getSchema( string $jsonPath ): array {
209  $json = file_get_contents( $jsonPath );
210 
211  if ( !$json ) {
212  $this->fatalError(
213  "'$jsonPath' does not exist!\n"
214  );
215  }
216 
217  $abstractSchema = json_decode( $json, true );
218 
219  if ( json_last_error() !== JSON_ERROR_NONE ) {
220  $this->fatalError(
221  "'$jsonPath' seems to be invalid json. Check the syntax and try again!\n" . json_last_error_msg()
222  );
223  }
224 
225  $validator = new AbstractSchemaValidator( function ( string $msg ): void {
226  $this->fatalError( $msg );
227  } );
228  try {
229  $validator->validate( $jsonPath );
230  } catch ( AbstractSchemaValidationError $e ) {
231  $this->fatalError( $e->getMessage() );
232  }
233 
234  return $abstractSchema;
235  }
236 }
if(!defined( 'MEDIAWIKI')) if(ini_get( 'mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition: Setup.php:93
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
Definition: Maintenance.php:66
error( $err, $die=0)
Throw an error to the user.
hasOption( $name)
Checks to see if a particular option was set.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
getOption( $name, $default=null)
Get an option, or return the default.
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
Validate abstract schema json files against their JSON schema.
execute()
Do the actual work.
string $scriptName
Name of the script.
generateSchema(string $platform, array $schema)
__construct()
Default constructor.