Add validation for abstract schema
This adds an option to the schema generating maintenance scripts to validate abstract schemas and schema changes and a structure test to validate exisiting schemas and schema changes. Schemas are also validated when generating. The validation for the schema doesn't impose limits on table, index or column names as I couldn't find any reliable conventions for them. The structure tests only cover MediaWiki itself as there is no convention on where extensions store their abstract schema. Ideally, auto detection would be possible for sql/, but for now extensions have to define their own (thankfully trivial) tests. A couple of invalid definitions were fixed thanks to these tests. I aimed to be thorough, but not all parts of the abstract schema are completely clear, and Doctrine's documentation is not complete. As a result, not everything has a description field. Bug: T298320 Change-Id: I681d265317d4d1584869142ebb23d4098c06885f
This commit is contained in:
parent
c85a2a8c24
commit
de0c4819d1
11 changed files with 561 additions and 4 deletions
22
docs/abstract-schema-changes.schema.json
Normal file
22
docs/abstract-schema-changes.schema.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/schema#",
|
||||
"description": "MediaWiki abstract database schema schema",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"comment": {
|
||||
"type": "string",
|
||||
"description": "Comment describing the schema change"
|
||||
},
|
||||
"before": {
|
||||
"type": "object",
|
||||
"description": "Schema before the change",
|
||||
"$ref": "abstract-schema-table.json"
|
||||
},
|
||||
"after": {
|
||||
"type": "object",
|
||||
"description": "Schema after the change",
|
||||
"$ref": "abstract-schema-table.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
208
docs/abstract-schema-table.json
Normal file
208
docs/abstract-schema-table.json
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/schema#",
|
||||
"description": "Abstract description of a mediawiki database table",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the table"
|
||||
},
|
||||
"comment": {
|
||||
"type": "string",
|
||||
"description": "Comment describing the table"
|
||||
},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"additionalItems": false,
|
||||
"description": "Columns",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the column"
|
||||
},
|
||||
"comment": {
|
||||
"type": "string",
|
||||
"description": "Comment describing the column"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Data type of the column",
|
||||
"enum": [
|
||||
"bigint",
|
||||
"binary",
|
||||
"blob",
|
||||
"datetimetz",
|
||||
"float",
|
||||
"integer",
|
||||
"mwenum",
|
||||
"mwtimestamp",
|
||||
"mwtinyint",
|
||||
"smallint",
|
||||
"string",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"description": "Additional options",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"autoincrement": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if the field should use an autoincremented value if no value was provided",
|
||||
"default": false
|
||||
},
|
||||
"default": {
|
||||
"type": [
|
||||
"number",
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "The default value of the column if no value was specified",
|
||||
"default": null
|
||||
},
|
||||
"fixed": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if the column should have a fixed length",
|
||||
"default": false
|
||||
},
|
||||
"length": {
|
||||
"type": "number",
|
||||
"description": "Length of the field.",
|
||||
"default": null,
|
||||
"minimum": 0
|
||||
},
|
||||
"notnull": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates whether the column is nullable or not",
|
||||
"default": true
|
||||
},
|
||||
"unsigned": {
|
||||
"type": "boolean",
|
||||
"description": "If the column should be an unsigned integer",
|
||||
"default": false
|
||||
},
|
||||
"PlatformOptions": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CustomSchemaOptions": {
|
||||
"type": "object",
|
||||
"description": "Custom schema options",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"allowInfinite": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"doublePrecision": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enum_values": {
|
||||
"type": "array",
|
||||
"description": "Values to use with type 'mwenum'",
|
||||
"additionalItems": false,
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"type"
|
||||
]
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"type": "array",
|
||||
"additionalItems": false,
|
||||
"description": "Indexes",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Index name"
|
||||
},
|
||||
"comment": {
|
||||
"type": "string",
|
||||
"description": "Comment describing the index"
|
||||
},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"additionalItems": false,
|
||||
"description": "Columns used by the index",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"unique": {
|
||||
"type": "boolean",
|
||||
"description": "If the index is unique"
|
||||
},
|
||||
"flags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lengths": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"columns"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pk": {
|
||||
"type": "array",
|
||||
"additionalItems": false,
|
||||
"description": "Array of column names used in the primary key",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"table_options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"columns",
|
||||
"indexes"
|
||||
]
|
||||
}
|
||||
10
docs/abstract-schema.schema.json
Normal file
10
docs/abstract-schema.schema.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/schema#",
|
||||
"description": "MediaWiki abstract database schema schema",
|
||||
"type": "array",
|
||||
"additionalItems": false,
|
||||
"items": {
|
||||
"$ref": "abstract-schema-table.json"
|
||||
},
|
||||
"minItems": 1
|
||||
}
|
||||
31
includes/db/AbstractSchemaValidationError.php
Normal file
31
includes/db/AbstractSchemaValidationError.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
namespace MediaWiki\DB;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @newable
|
||||
* @since 1.38
|
||||
*/
|
||||
class AbstractSchemaValidationError extends Exception {
|
||||
}
|
||||
117
includes/db/AbstractSchemaValidator.php
Normal file
117
includes/db/AbstractSchemaValidator.php
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
namespace MediaWiki\DB;
|
||||
|
||||
use JsonSchema\Validator;
|
||||
use Seld\JsonLint\DuplicateKeyException;
|
||||
use Seld\JsonLint\JsonParser;
|
||||
use Seld\JsonLint\ParsingException;
|
||||
use function class_exists;
|
||||
use function file_get_contents;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
|
||||
/**
|
||||
* Validate abstract schema json files against their JSON schema.
|
||||
*
|
||||
* This is used for static validation from the command-line via
|
||||
* generateSchemaSql.php, generateSchemaChangeSql, and the PHPUnit structure test suite
|
||||
* (AbstractSchemaValidationTest).
|
||||
*
|
||||
* The files are normally read by the generateSchemaSql.php and generateSchemaSqlChange.php maintenance scripts.
|
||||
*
|
||||
* @since 1.38
|
||||
*/
|
||||
class AbstractSchemaValidator {
|
||||
/**
|
||||
* @var callable(string):void
|
||||
*/
|
||||
private $missingDepCallback;
|
||||
|
||||
/**
|
||||
* @param callable(string):void $missingDepCallback
|
||||
*/
|
||||
public function __construct( callable $missingDepCallback ) {
|
||||
$this->missingDepCallback = $missingDepCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
* @return bool
|
||||
*/
|
||||
public function checkDependencies(): bool {
|
||||
if ( !class_exists( Validator::class ) ) {
|
||||
( $this->missingDepCallback )(
|
||||
'The JsonSchema library cannot be found, please install it through composer.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !class_exists( JsonParser::class ) ) {
|
||||
( $this->missingDepCallback )(
|
||||
'The JSON lint library cannot be found, please install it through composer.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path file to validate
|
||||
* @return bool true if passes validation
|
||||
* @throws AbstractSchemaValidationError on any failure
|
||||
*/
|
||||
public function validate( string $path ): bool {
|
||||
$contents = file_get_contents( $path );
|
||||
$jsonParser = new JsonParser();
|
||||
try {
|
||||
$data = $jsonParser->parse( $contents, JsonParser::DETECT_KEY_CONFLICTS );
|
||||
} catch ( DuplicateKeyException $e ) {
|
||||
throw new AbstractSchemaValidationError( $e->getMessage(), $e->getCode(), $e );
|
||||
} catch ( ParsingException $e ) {
|
||||
throw new AbstractSchemaValidationError( "$path is not valid JSON", $e->getCode(), $e );
|
||||
}
|
||||
|
||||
// Regular schema's are arrays, schema changes are objects.
|
||||
if ( is_array( $data ) ) {
|
||||
$schemaPath = __DIR__ . '/../../docs/abstract-schema.schema.json';
|
||||
} elseif ( is_object( $data ) ) {
|
||||
$schemaPath = __DIR__ . '/../../docs/abstract-schema-changes.schema.json';
|
||||
} else {
|
||||
throw new AbstractSchemaValidationError( "$path is not a supported JSON object" );
|
||||
}
|
||||
|
||||
$validator = new Validator;
|
||||
$validator->check( $data, (object)[ '$ref' => 'file://' . $schemaPath ] );
|
||||
if ( $validator->isValid() ) {
|
||||
// All good.
|
||||
return true;
|
||||
}
|
||||
|
||||
$out = "$path did not pass validation.\n";
|
||||
foreach ( $validator->getErrors() as $error ) {
|
||||
$out .= "[{$error['property']}] {$error['message']}\n";
|
||||
}
|
||||
throw new AbstractSchemaValidationError( $out );
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,9 @@
|
|||
* @ingroup Maintenance
|
||||
*/
|
||||
|
||||
use MediaWiki\DB\AbstractSchemaValidationError;
|
||||
use MediaWiki\DB\AbstractSchemaValidator;
|
||||
|
||||
require_once __DIR__ . '/../Maintenance.php';
|
||||
|
||||
abstract class SchemaMaintenance extends Maintenance {
|
||||
|
|
@ -64,6 +67,10 @@ abstract class SchemaMaintenance extends Maintenance {
|
|||
false,
|
||||
true
|
||||
);
|
||||
$this->addOption(
|
||||
'validate',
|
||||
'Validate the schema instead of generating sql files.'
|
||||
);
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
|
|
@ -80,6 +87,12 @@ abstract class SchemaMaintenance extends Maintenance {
|
|||
$jsonPath = strtr( $jsonPath, '\\', '/' );
|
||||
}
|
||||
|
||||
if ( $this->hasOption( 'validate' ) ) {
|
||||
$this->getSchema( $jsonPath );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow to specify a folder and use a default name
|
||||
if ( is_dir( $jsonPath ) ) {
|
||||
$jsonPath .= '/tables.json';
|
||||
|
|
@ -205,6 +218,15 @@ abstract class SchemaMaintenance extends Maintenance {
|
|||
);
|
||||
}
|
||||
|
||||
$validator = new AbstractSchemaValidator( function ( string $msg ): void {
|
||||
$this->fatalError( $msg );
|
||||
} );
|
||||
try {
|
||||
$validator->validate( $jsonPath );
|
||||
} catch ( AbstractSchemaValidationError $e ) {
|
||||
$this->fatalError( $e->getMessage() );
|
||||
}
|
||||
|
||||
return $abstractSchema;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3699,7 +3699,6 @@
|
|||
{
|
||||
"name": "si_text",
|
||||
"columns": [ "si_text" ],
|
||||
"fulltext": true,
|
||||
"unique": false,
|
||||
"flags": [
|
||||
"fulltext"
|
||||
|
|
|
|||
1
tests/phpunit/data/db/notschema.txt
Normal file
1
tests/phpunit/data/db/notschema.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
This is definitely not JSON and most certainly not an abstract schema.
|
||||
|
|
@ -7,19 +7,19 @@
|
|||
"name": "actor_id",
|
||||
"comment": "Unique ID to identify each actor",
|
||||
"type": "bigint",
|
||||
"options": { "Unsigned": true, "Notnull": true }
|
||||
"options": { "unsigned": true, "notnull": true }
|
||||
},
|
||||
{
|
||||
"name": "actor_user",
|
||||
"comment": "Key to user.user_id, or NULL for anonymous edits",
|
||||
"type": "integer",
|
||||
"options": { "Unsigned": true }
|
||||
"options": { "unsigned": true }
|
||||
},
|
||||
{
|
||||
"name": "actor_name",
|
||||
"comment": "Text username or IP address",
|
||||
"type": "string",
|
||||
"options": { "Length": 255, "Notnull": true }
|
||||
"options": { "length": 255, "notnull": true }
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
|
|
|
|||
81
tests/phpunit/structure/AbstractSchemaValidationTest.php
Normal file
81
tests/phpunit/structure/AbstractSchemaValidationTest.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
/**
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*/
|
||||
|
||||
use MediaWiki\DB\AbstractSchemaValidationError;
|
||||
use MediaWiki\DB\AbstractSchemaValidator;
|
||||
|
||||
/**
|
||||
* Validates all abstract schemas against the abstract-schema schemas in the docs/ folder.
|
||||
*/
|
||||
class AbstractSchemaValidationTest extends PHPUnit\Framework\TestCase {
|
||||
use MediaWikiCoversValidator;
|
||||
|
||||
/**
|
||||
* @var AbstractSchemaValidator
|
||||
*/
|
||||
protected $validator;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->validator = new AbstractSchemaValidator( [ $this, 'markTestSkipped' ] );
|
||||
$this->validator->checkDependencies();
|
||||
}
|
||||
|
||||
public static function provideSchemas(): array {
|
||||
return [
|
||||
'maintenance/tables.json' => [ __DIR__ . '/../../../maintenance/tables.json' ]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSchemas
|
||||
* @param string $path Path to tables.json file
|
||||
*/
|
||||
public function testSchemasPassValidation( string $path ): void {
|
||||
try {
|
||||
$this->validator->validate( $path );
|
||||
// All good
|
||||
$this->assertTrue( true );
|
||||
} catch ( AbstractSchemaValidationError $e ) {
|
||||
$this->fail( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
public static function provideSchemaChanges(): Generator {
|
||||
foreach ( glob( __DIR__ . '/../../../maintenance/abstractSchemaChanges/*.json' ) as $schemaChange ) {
|
||||
$fileName = pathinfo( $schemaChange, PATHINFO_BASENAME );
|
||||
|
||||
yield $fileName => [ $schemaChange ];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSchemaChanges
|
||||
* @param string $path Path to tables.json file
|
||||
*/
|
||||
public function testSchemaChangesPassValidation( string $path ): void {
|
||||
try {
|
||||
$this->validator->validate( $path );
|
||||
// All good
|
||||
$this->assertTrue( true );
|
||||
} catch ( AbstractSchemaValidationError $e ) {
|
||||
$this->fail( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
/**
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
*/
|
||||
|
||||
use MediaWiki\DB\AbstractSchemaValidationError;
|
||||
use MediaWiki\DB\AbstractSchemaValidator;
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\DB\AbstractSchemaValidator
|
||||
*/
|
||||
class AbstractSchemaValidatorTest extends MediaWikiUnitTestCase {
|
||||
|
||||
/**
|
||||
* @dataProvider provideValidate
|
||||
* @param string $file
|
||||
* @param string|true $expected
|
||||
*/
|
||||
public function testValidate( string $file, $expected ): void {
|
||||
// If a dependency is missing, skip this test.
|
||||
$validator = new AbstractSchemaValidator( function ( $msg ) {
|
||||
$this->markTestSkipped( $msg );
|
||||
} );
|
||||
|
||||
if ( is_string( $expected ) ) {
|
||||
$this->expectException( AbstractSchemaValidationError::class );
|
||||
$this->expectExceptionMessage( $expected );
|
||||
}
|
||||
|
||||
$dir = __DIR__ . '/../../../data/db/';
|
||||
$this->assertSame(
|
||||
$expected,
|
||||
$validator->validate( $dir . $file )
|
||||
);
|
||||
}
|
||||
|
||||
public static function provideValidate(): array {
|
||||
return [
|
||||
[
|
||||
'tables.json',
|
||||
true
|
||||
],
|
||||
[
|
||||
'patch-drop-ct_tag.json',
|
||||
true
|
||||
],
|
||||
[
|
||||
'notschema.txt',
|
||||
'notschema.txt is not valid JSON'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue