├── src ├── PackagePrivate │ └── Doctrine │ │ ├── TermType.php │ │ ├── TableNames.php │ │ ├── DoctrineItemTermStore.php │ │ ├── DoctrinePropertyTermStore.php │ │ ├── NormalizedStore.php │ │ └── DoctrineSchemaCreator.php └── DoctrineTermStore.php ├── LICENSE ├── tests ├── Unit │ └── Doctrine │ │ └── TableNamesTest.php └── Integration │ └── Doctrine │ ├── DoctrineTermStoreTest.php │ ├── DoctrineItemTermStoreTest.php │ └── DoctrinePropertyTermStoreTest.php └── README.md /src/PackagePrivate/Doctrine/TermType.php: -------------------------------------------------------------------------------- 1 | prefixIsSafe( $tableNamePrefix ) ) { 17 | throw new \InvalidArgumentException( 'Table name prefix contains forbidden characters' ); 18 | } 19 | $this->tableNamePrefix = $tableNamePrefix; 20 | } 21 | 22 | private function prefixIsSafe( $prefix ) { 23 | $withoutUnderscores = str_replace( '_', '', $prefix ); 24 | return $withoutUnderscores === '' || ctype_alnum( $withoutUnderscores ); 25 | } 26 | 27 | public function itemTerms() { 28 | return $this->tableNamePrefix . self::ITEM_TERMS; 29 | } 30 | 31 | public function propertyTerms() { 32 | return $this->tableNamePrefix . self::PROPERTY_TERMS; 33 | } 34 | 35 | public function termInLanguage() { 36 | return $this->tableNamePrefix . self::TERM_IN_LANGUAGE; 37 | } 38 | 39 | public function textInLanguage() { 40 | return $this->tableNamePrefix . self::TEXT_IN_LANGUAGE; 41 | } 42 | 43 | public function text() { 44 | return $this->tableNamePrefix . self::TEXT; 45 | } 46 | 47 | public function prefix( $string ) { 48 | return $this->tableNamePrefix . $string; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Wikimedia Deutschland e.V. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /tests/Unit/Doctrine/TableNamesTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 15 | 'wbt_text', 16 | ( new TableNames( '' ) )->text() 17 | ); 18 | } 19 | 20 | public function testTextTableGetsPrefixed() { 21 | $this->assertSame( 22 | 'prefix_wbt_text', 23 | ( new TableNames( 'prefix_' ) )->text() 24 | ); 25 | } 26 | 27 | public function testTextInLanguageTableGetsPrefixed() { 28 | $this->assertSame( 29 | 'prefix_wbt_text_in_lang', 30 | ( new TableNames( 'prefix_' ) )->textInLanguage() 31 | ); 32 | } 33 | 34 | public function testTermInLanguageTableGetsPrefixed() { 35 | $this->assertSame( 36 | 'prefix_wbt_term_in_lang', 37 | ( new TableNames( 'prefix_' ) )->termInLanguage() 38 | ); 39 | } 40 | 41 | public function testPropertyTermsTableGetsPrefixed() { 42 | $this->assertSame( 43 | 'prefix_wbt_property_terms', 44 | ( new TableNames( 'prefix_' ) )->propertyTerms() 45 | ); 46 | } 47 | 48 | public function testItemTermsTableGetsPrefixed() { 49 | $this->assertSame( 50 | 'prefix_wbt_item_terms', 51 | ( new TableNames( 'prefix_' ) )->itemTerms() 52 | ); 53 | } 54 | 55 | /** 56 | * @dataProvider invalidPrefixProvider 57 | */ 58 | public function testOnlyAlphaNumericPrefixesAreAllowed( string $prefix ) { 59 | $this->expectException( \InvalidArgumentException::class ); 60 | new TableNames( $prefix ); 61 | } 62 | 63 | public function invalidPrefixProvider() { 64 | yield [ '-' ]; 65 | yield [ ' ' ]; 66 | yield [ 'abc!' ]; 67 | yield [ 'abc!abc' ]; 68 | yield [ '%&$' ]; 69 | yield [ '"' ]; 70 | yield [ "'" ]; 71 | yield [ '\\' ]; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wikibase Doctrine TermStore 2 | 3 | [![Build Status](https://travis-ci.org/wmde/doctrine-term-store.svg?branch=master)](https://travis-ci.org/wmde/doctrine-term-store) 4 | [![Latest Stable Version](https://poser.pugx.org/wikibase/doctrine-term-store/version.png)](https://packagist.org/packages/wikibase/doctrine-term-store) 5 | [![Download count](https://poser.pugx.org/wikibase/doctrine-term-store/d/total.png)](https://packagist.org/packages/wikibase/doctrine-term-store) 6 | 7 | [Doctrine DBAL](https://www.doctrine-project.org/projects/dbal.html) implementation of 8 | [Wikibase TermStore](https://github.com/wmde/wikibase-term-store). 9 | 10 | ## Usage 11 | 12 | The public entry point of the package is `DoctrineTermStore`, which is used to construct all services. 13 | 14 | ```php 15 | $termStore = new DoctrineTermStore( /* config */ ); 16 | ``` 17 | 18 | Getting terms: 19 | 20 | ```php 21 | $fingerprint = $termStore->newPropertyTermStore()->getTerms( $propertyId ); 22 | ``` 23 | 24 | Schema creation: 25 | 26 | ```php 27 | $termStore->install(); 28 | ``` 29 | 30 | ## Installation 31 | 32 | To use the Wikibase TermStore library in your project, simply add a dependency on wikibase/doctrine-term-store 33 | to your project's `composer.json` file. Here is a minimal example of a `composer.json` 34 | file that just defines a dependency on wikibase/doctrine-term-store 1.x: 35 | 36 | ```json 37 | { 38 | "require": { 39 | "wikibase/doctrine-term-store": "~1.0" 40 | } 41 | } 42 | ``` 43 | 44 | ## Development 45 | 46 | Start by installing the project dependencies by executing 47 | 48 | composer update 49 | 50 | You can run the tests by executing 51 | 52 | make test 53 | 54 | You can run the style checks by executing 55 | 56 | make cs 57 | 58 | To run all CI checks, execute 59 | 60 | make ci 61 | 62 | You can also invoke PHPUnit directly to pass it arguments, as follows 63 | 64 | vendor/bin/phpunit --filter SomeClassNameOrFilter 65 | -------------------------------------------------------------------------------- /src/DoctrineTermStore.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 23 | $this->tableNames = new TableNames( $tableNamePrefix ); 24 | } 25 | 26 | public function install( MessageReporter $reporter = null ) { 27 | ( new DoctrineSchemaCreator( 28 | $this->connection->getSchemaManager(), 29 | $this->tableNames, 30 | $reporter === null ? new NullMessageReporter() : $reporter 31 | ) )->createSchema(); 32 | } 33 | 34 | /** 35 | * CAUTION! This drops all tables part of the term store! 36 | */ 37 | public function uninstall( MessageReporter $reporter = null ) { 38 | if ( $reporter === null ) { 39 | $reporter = new NullMessageReporter(); 40 | } 41 | 42 | $schema = $this->connection->getSchemaManager(); 43 | 44 | if ( $schema->tablesExist( $this->tableNames->itemTerms() ) ) { 45 | $reporter->reportMessage( 'Uninstalling Wikibase Term Store: removing tables' ); 46 | 47 | $schema->dropTable( $this->tableNames->itemTerms() ); 48 | $schema->dropTable( $this->tableNames->propertyTerms() ); 49 | $schema->dropTable( $this->tableNames->termInLanguage() ); 50 | $schema->dropTable( $this->tableNames->textInLanguage() ); 51 | $schema->dropTable( $this->tableNames->text() ); 52 | } 53 | else { 54 | $reporter->reportMessage( 'Wikibase Term Store is not installed so will not be removed' ); 55 | } 56 | } 57 | 58 | public function newPropertyTermStore(): PropertyTermStore { 59 | return new DoctrinePropertyTermStore( $this->connection, $this->tableNames ); 60 | } 61 | 62 | public function newItemTermStore(): ItemTermStore { 63 | return new DoctrineItemTermStore( $this->connection, $this->tableNames ); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/PackagePrivate/Doctrine/DoctrineItemTermStore.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 22 | $this->tableNames = $tableNames; 23 | $this->normalizedStore = new NormalizedStore( $connection, $tableNames ); 24 | } 25 | 26 | public function storeTerms( ItemId $itemId, Fingerprint $terms ) { 27 | try { 28 | $this->deleteTerms( $itemId ); 29 | $this->insertTerms( $itemId, $terms ); 30 | } 31 | catch ( DBALException $ex ) { 32 | throw new TermStoreException( $ex->getMessage(), $ex->getCode() ); 33 | } 34 | } 35 | 36 | private function insertTerms( ItemId $itemId, Fingerprint $terms ) { 37 | foreach ( $terms->getLabels() as $term ) { 38 | $this->insertTerm( $itemId, $term, TermType::LABEL ); 39 | } 40 | 41 | foreach ( $terms->getDescriptions() as $term ) { 42 | $this->insertTerm( $itemId, $term, TermType::DESCRIPTION ); 43 | } 44 | 45 | foreach ( $terms->getAliasGroups() as $aliasGroup ) { 46 | foreach ( $aliasGroup->getAliases() as $alias ) { 47 | $this->insertTerm( 48 | $itemId, 49 | new Term( $aliasGroup->getLanguageCode(), $alias ), 50 | TermType::ALIAS 51 | ); 52 | } 53 | } 54 | } 55 | 56 | private function insertTerm( ItemId $itemId, Term $term, $termType ) { 57 | $this->connection->insert( 58 | $this->tableNames->itemTerms(), 59 | [ 60 | 'wbit_item_id' => $itemId->getNumericId(), 61 | 'wbit_term_in_lang_id' => $this->normalizedStore->acquireTermInLanguageId( $term, $termType ), 62 | ] 63 | ); 64 | } 65 | 66 | public function deleteTerms( ItemId $itemId ) { 67 | try { 68 | $this->connection->delete( 69 | $this->tableNames->itemTerms(), 70 | [ 'wbit_item_id' => $itemId->getNumericId() ], 71 | [ \PDO::PARAM_INT ] 72 | ); 73 | } 74 | catch ( DBALException $ex ) { 75 | throw new TermStoreException( $ex->getMessage(), $ex->getCode() ); 76 | } 77 | } 78 | 79 | public function getTerms( ItemId $itemId ): Fingerprint { 80 | try { 81 | return $this->normalizedStore->getFingerprint( 82 | $this->newGetTermsStatement( $itemId ) 83 | ); 84 | } 85 | catch ( DBALException $ex ) { 86 | throw new TermStoreException( $ex->getMessage(), $ex->getCode() ); 87 | } 88 | } 89 | 90 | private function newGetTermsStatement( ItemId $itemId ): Statement { 91 | $sql = <<tableNames->itemTerms()} 93 | INNER JOIN {$this->tableNames->termInLanguage()} ON {$this->tableNames->itemTerms()}.wbit_term_in_lang_id = {$this->tableNames->termInLanguage()}.wbtl_id 94 | INNER JOIN {$this->tableNames->textInLanguage()} ON {$this->tableNames->termInLanguage()}.wbtl_text_in_lang_id = {$this->tableNames->textInLanguage()}.wbxl_id 95 | INNER JOIN {$this->tableNames->text()} ON {$this->tableNames->textInLanguage()}.wbxl_text_id = {$this->tableNames->text()}.wbx_id 96 | WHERE wbit_item_id = ? 97 | EOT; 98 | 99 | return $this->connection->executeQuery( 100 | $sql, 101 | [ 102 | $itemId->getNumericId() 103 | ], 104 | [ 105 | \PDO::PARAM_INT 106 | ] 107 | ); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/PackagePrivate/Doctrine/DoctrinePropertyTermStore.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 22 | $this->tableNames = $tableNames; 23 | $this->normalizedStore = new NormalizedStore( $connection, $tableNames ); 24 | } 25 | 26 | public function storeTerms( PropertyId $propertyId, Fingerprint $terms ) { 27 | try { 28 | $this->deleteTerms( $propertyId ); 29 | $this->insertTerms( $propertyId, $terms ); 30 | } 31 | catch ( DBALException $ex ) { 32 | throw new TermStoreException( $ex->getMessage(), $ex->getCode() ); 33 | } 34 | } 35 | 36 | private function insertTerms( PropertyId $propertyId, Fingerprint $terms ) { 37 | foreach ( $terms->getLabels() as $term ) { 38 | $this->insertTerm( $propertyId, $term, TermType::LABEL ); 39 | } 40 | 41 | foreach ( $terms->getDescriptions() as $term ) { 42 | $this->insertTerm( $propertyId, $term, TermType::DESCRIPTION ); 43 | } 44 | 45 | foreach ( $terms->getAliasGroups() as $aliasGroup ) { 46 | foreach ( $aliasGroup->getAliases() as $alias ) { 47 | $this->insertTerm( 48 | $propertyId, 49 | new Term( $aliasGroup->getLanguageCode(), $alias ), 50 | TermType::ALIAS 51 | ); 52 | } 53 | } 54 | } 55 | 56 | private function insertTerm( PropertyId $propertyId, Term $term, $termType ) { 57 | $this->connection->insert( 58 | $this->tableNames->propertyTerms(), 59 | [ 60 | 'wbpt_property_id' => $propertyId->getNumericId(), 61 | 'wbpt_term_in_lang_id' => $this->normalizedStore->acquireTermInLanguageId( $term, $termType ), 62 | ] 63 | ); 64 | } 65 | 66 | public function deleteTerms( PropertyId $propertyId ) { 67 | try { 68 | $this->connection->delete( 69 | $this->tableNames->propertyTerms(), 70 | [ 'wbpt_property_id' => $propertyId->getNumericId() ], 71 | [ \PDO::PARAM_INT ] 72 | ); 73 | } 74 | catch ( DBALException $ex ) { 75 | throw new TermStoreException( $ex->getMessage(), $ex->getCode() ); 76 | } 77 | } 78 | 79 | public function getTerms( PropertyId $propertyId ): Fingerprint { 80 | try { 81 | return $this->normalizedStore->getFingerprint( 82 | $this->newGetTermsStatement( $propertyId ) 83 | ); 84 | } 85 | catch ( DBALException $ex ) { 86 | throw new TermStoreException( $ex->getMessage(), $ex->getCode() ); 87 | } 88 | } 89 | 90 | private function newGetTermsStatement( PropertyId $propertyId ): Statement { 91 | $sql = <<tableNames->propertyTerms()} 93 | INNER JOIN {$this->tableNames->termInLanguage()} ON {$this->tableNames->propertyTerms()}.wbpt_term_in_lang_id = {$this->tableNames->termInLanguage()}.wbtl_id 94 | INNER JOIN {$this->tableNames->textInLanguage()} ON {$this->tableNames->termInLanguage()}.wbtl_text_in_lang_id = {$this->tableNames->textInLanguage()}.wbxl_id 95 | INNER JOIN {$this->tableNames->text()} ON {$this->tableNames->textInLanguage()}.wbxl_text_id = {$this->tableNames->text()}.wbx_id 96 | WHERE wbpt_property_id = ? 97 | EOT; 98 | 99 | return $this->connection->executeQuery( 100 | $sql, 101 | [ 102 | $propertyId->getNumericId() 103 | ], 104 | [ 105 | \PDO::PARAM_INT 106 | ] 107 | ); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/PackagePrivate/Doctrine/NormalizedStore.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 17 | $this->tableNames = $tableNames; 18 | } 19 | 20 | public function acquireTermInLanguageId( Term $term, $termType ) { 21 | $textInLanguageId = $this->acquireTextInLanguageId( $term ); 22 | 23 | $id = $this->findExistingTermInLanguageId( $termType, $textInLanguageId ); 24 | 25 | if ( $id !== false ) { 26 | return $id; 27 | } 28 | 29 | $this->insertTermInLanguageRecord( $termType, $textInLanguageId ); 30 | 31 | return $this->connection->lastInsertId(); 32 | } 33 | 34 | private function findExistingTermInLanguageId( $termType, $textInLanguageId ) { 35 | $record = $this->connection->executeQuery( 36 | 'SELECT wbtl_id FROM ' . $this->tableNames->termInLanguage() . ' WHERE wbtl_type_id = ? AND wbtl_text_in_lang_id = ?', 37 | [ $termType, $textInLanguageId ], 38 | [ \PDO::PARAM_INT, \PDO::PARAM_INT ] 39 | )->fetch(); 40 | 41 | return is_array( $record ) ? $record['wbtl_id'] : false; 42 | } 43 | 44 | private function insertTermInLanguageRecord( $termType, $textInLanguageId ) { 45 | $this->connection->insert( 46 | $this->tableNames->termInLanguage(), 47 | [ 48 | 'wbtl_type_id' => $termType, 49 | 'wbtl_text_in_lang_id ' => $textInLanguageId, 50 | ] 51 | ); 52 | } 53 | 54 | private function acquireTextInLanguageId( Term $term ) { 55 | $textId = $this->acquireTextId( $term ); 56 | 57 | $id = $this->findExistingTextInLanguageId( $term, $textId ); 58 | 59 | if ( $id !== false ) { 60 | return $id; 61 | } 62 | 63 | $this->insertTextInLanguageRecord( $term, $textId ); 64 | 65 | return $this->connection->lastInsertId(); 66 | } 67 | 68 | private function findExistingTextInLanguageId( Term $term, $textId ) { 69 | $record = $this->connection->executeQuery( 70 | 'SELECT wbxl_id FROM ' . $this->tableNames->textInLanguage() . ' WHERE wbxl_language = ? AND wbxl_text_id = ?', 71 | [ $term->getLanguageCode(), $textId ], 72 | [ \PDO::PARAM_STR, \PDO::PARAM_INT ] 73 | )->fetch(); 74 | 75 | return is_array( $record ) ? $record['wbxl_id'] : false; 76 | } 77 | 78 | private function insertTextInLanguageRecord( Term $term, $textId ) { 79 | $this->connection->insert( 80 | $this->tableNames->textInLanguage(), 81 | [ 82 | 'wbxl_language' => $term->getLanguageCode(), 83 | 'wbxl_text_id' => $textId, 84 | ] 85 | ); 86 | } 87 | 88 | private function acquireTextId( Term $term ) { 89 | $id = $this->findExistingTextId( $term ); 90 | 91 | if ( $id !== false ) { 92 | return $id; 93 | } 94 | 95 | $this->insertTextRecord( $term ); 96 | 97 | return $this->connection->lastInsertId(); 98 | } 99 | 100 | private function findExistingTextId( Term $term ) { 101 | $record = $this->connection->executeQuery( 102 | 'SELECT wbx_id FROM ' . $this->tableNames->text() . ' WHERE wbx_text = ?', 103 | [ $term->getText() ], 104 | [ \PDO::PARAM_STR ] 105 | )->fetch(); 106 | 107 | return is_array( $record ) ? $record['wbx_id'] : false; 108 | } 109 | 110 | private function insertTextRecord( Term $term ) { 111 | $this->connection->insert( 112 | $this->tableNames->text(), 113 | [ 114 | 'wbx_text' => $term->getText(), 115 | ] 116 | ); 117 | } 118 | 119 | public function getFingerprint( Statement $statement ): Fingerprint { 120 | return $this->recordsToFingerprint( 121 | $statement->fetchAll( \PDO::FETCH_OBJ ) 122 | ); 123 | } 124 | 125 | private function recordsToFingerprint( array $termRecords ): Fingerprint { 126 | $fingerprint = new Fingerprint(); 127 | 128 | $aliasGroups = []; 129 | 130 | foreach ( $termRecords as $term ) { 131 | switch ( $term->wbtl_type_id ) { 132 | case TermType::LABEL: 133 | $fingerprint->setLabel( $term->wbxl_language, $term->wbx_text ); 134 | break; 135 | case TermType::DESCRIPTION: 136 | $fingerprint->setDescription( $term->wbxl_language, $term->wbx_text ); 137 | break; 138 | case TermType::ALIAS: 139 | if ( !array_key_exists( $term->wbxl_language, $aliasGroups ) ) { 140 | $aliasGroups[$term->wbxl_language] = []; 141 | } 142 | 143 | $aliasGroups[$term->wbxl_language][] = $term->wbx_text; 144 | 145 | break; 146 | } 147 | } 148 | 149 | foreach ( $aliasGroups as $language => $aliases ) { 150 | $fingerprint->setAliasGroup( $language, $aliases ); 151 | } 152 | 153 | return $fingerprint; 154 | } 155 | 156 | } -------------------------------------------------------------------------------- /src/PackagePrivate/Doctrine/DoctrineSchemaCreator.php: -------------------------------------------------------------------------------- 1 | schemaManager = $schemaManager; 18 | $this->tableNames = $tableNames; 19 | $this->reporter = $reporter; 20 | } 21 | 22 | public function createSchema() { 23 | if ( $this->schemaManager->tablesExist( $this->tableNames->itemTerms() ) ) { 24 | $this->reporter->reportMessage( 'Wikibase Term Store is already installed' ); 25 | } 26 | else { 27 | $this->reporter->reportMessage( 'Installing Wikibase Term Store' ); 28 | 29 | $this->reporter->reportMessage( 'Creating table ' . $this->tableNames->itemTerms() ); 30 | $this->schemaManager->createTable( $this->newItemTermsTable() ); 31 | 32 | $this->reporter->reportMessage( 'Creating table ' . $this->tableNames->propertyTerms() ); 33 | $this->schemaManager->createTable( $this->newPropertyTermsTable() ); 34 | 35 | $this->reporter->reportMessage( 'Creating table ' . $this->tableNames->termInLanguage() ); 36 | $this->schemaManager->createTable( $this->newTermInLangTable() ); 37 | 38 | $this->reporter->reportMessage( 'Creating table ' . $this->tableNames->textInLanguage() ); 39 | $this->schemaManager->createTable( $this->newTextInLangTable() ); 40 | 41 | $this->reporter->reportMessage( 'Creating table ' . $this->tableNames->text() ); 42 | $this->schemaManager->createTable( $this->newTextTable() ); 43 | 44 | $this->reporter->reportMessage( 'Finished installing Wikibase Term Store' ); 45 | } 46 | } 47 | 48 | private function newItemTermsTable(): Table { 49 | $table = new Table( $this->tableNames->itemTerms() ); 50 | 51 | $table->addColumn( 'wbit_id', Type::BIGINT, [ 'autoincrement' => true, 'unsigned' => true ] ); 52 | $table->addColumn( 'wbit_item_id', Type::INTEGER, [ 'unsigned' => true ] ); 53 | $table->addColumn( 'wbit_term_in_lang_id', Type::INTEGER, [ 'unsigned' => true ] ); 54 | 55 | $table->setPrimaryKey( [ 'wbit_id' ] ); 56 | $table->addIndex( [ 'wbit_item_id' ], $this->tableNames->prefix( 'wbt_item_terms_item_id' ) ); 57 | $table->addIndex( [ 'wbit_term_in_lang_id' ], $this->tableNames->prefix( 'wbt_item_terms_term_in_lang_id' ) ); 58 | 59 | return $table; 60 | } 61 | 62 | private function newPropertyTermsTable(): Table { 63 | $table = new Table( $this->tableNames->propertyTerms() ); 64 | 65 | $table->addColumn( 'wbpt_id', Type::INTEGER, [ 'autoincrement' => true, 'unsigned' => true ] ); 66 | $table->addColumn( 'wbpt_property_id', Type::INTEGER, [ 'unsigned' => true ] ); 67 | $table->addColumn( 'wbpt_term_in_lang_id', Type::INTEGER, [ 'unsigned' => true ] ); 68 | 69 | $table->setPrimaryKey( [ 'wbpt_id' ] ); 70 | $table->addIndex( [ 'wbpt_property_id' ], $this->tableNames->prefix( 'wbt_property_terms_property_id' ) ); 71 | $table->addIndex( [ 'wbpt_term_in_lang_id' ], $this->tableNames->prefix( 'wbt_property_terms_term_in_lang_id' ) ); 72 | 73 | return $table; 74 | } 75 | 76 | private function newTermInLangTable(): Table { 77 | $table = new Table( $this->tableNames->termInLanguage() ); 78 | 79 | $table->addColumn( 'wbtl_id', Type::INTEGER, [ 'autoincrement' => true, 'unsigned' => true ] ); 80 | $table->addColumn( 'wbtl_type_id', Type::INTEGER, [ 'unsigned' => true ] ); 81 | $table->addColumn( 'wbtl_text_in_lang_id', Type::INTEGER, [ 'unsigned' => true ] ); 82 | 83 | $table->setPrimaryKey( [ 'wbtl_id' ] ); 84 | $table->addIndex( [ 'wbtl_type_id' ], $this->tableNames->prefix( 'wbt_term_in_lang_type_id' ) ); 85 | $table->addIndex( [ 'wbtl_text_in_lang_id' ], $this->tableNames->prefix( 'wbt_term_in_lang_text_in_lang_id' ) ); 86 | 87 | return $table; 88 | } 89 | 90 | private function newTextInLangTable(): Table { 91 | $table = new Table( $this->tableNames->textInLanguage() ); 92 | 93 | $table->addColumn( 'wbxl_id', Type::INTEGER, [ 'autoincrement' => true, 'unsigned' => true ] ); 94 | $table->addColumn( 'wbxl_language', Type::BINARY, [ 'length' => 10 ] ); 95 | $table->addColumn( 'wbxl_text_id', Type::INTEGER, [ 'unsigned' => true ] ); 96 | 97 | $table->setPrimaryKey( [ 'wbxl_id' ] ); 98 | $table->addIndex( [ 'wbxl_language' ], $this->tableNames->prefix( 'wbt_text_in_lang_language' ) ); 99 | $table->addIndex( [ 'wbxl_text_id' ], $this->tableNames->prefix( 'wbt_text_in_lang_text_id' ) ); 100 | 101 | return $table; 102 | } 103 | 104 | private function newTextTable(): Table { 105 | $table = new Table( $this->tableNames->text() ); 106 | 107 | $table->addColumn( 'wbx_id', Type::INTEGER, [ 'autoincrement' => true, 'unsigned' => true ] ); 108 | $table->addColumn( 'wbx_text', Type::BINARY, [ 'length' => 255 ] ); 109 | 110 | $table->setPrimaryKey( [ 'wbx_id' ] ); 111 | $table->addUniqueIndex( [ 'wbx_text' ], $this->tableNames->prefix( 'wbt_text_text' ) ); 112 | 113 | return $table; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /tests/Integration/Doctrine/DoctrineTermStoreTest.php: -------------------------------------------------------------------------------- 1 | connection = DriverManager::getConnection( [ 33 | 'driver' => 'pdo_sqlite', 34 | 'memory' => true, 35 | ] ); 36 | 37 | $this->tableNames = new TableNames( 'prefix_' ); 38 | } 39 | 40 | public function testInstallCreatesTables() { 41 | $this->newTermStore()->install(); 42 | 43 | $this->assertTableExists( $this->tableNames->itemTerms() ); 44 | $this->assertTableExists( $this->tableNames->propertyTerms() ); 45 | $this->assertTableExists( $this->tableNames->termInLanguage() ); 46 | $this->assertTableExists( $this->tableNames->textInLanguage() ); 47 | $this->assertTableExists( $this->tableNames->text() ); 48 | } 49 | 50 | private function newTermStore(): DoctrineTermStore { 51 | return new DoctrineTermStore( $this->connection, self::PREFIX ); 52 | } 53 | 54 | private function assertTableExists( string $tableName ) { 55 | $this->assertTrue( 56 | $this->connection->getSchemaManager()->tablesExist( $tableName ), 57 | 'Table "' . $tableName . '" should exist' 58 | ); 59 | } 60 | 61 | public function testInstallCreatesItemTermsColumns() { 62 | $this->newTermStore()->install(); 63 | 64 | $columns = $this->connection->getSchemaManager()->listTableColumns( $this->tableNames->itemTerms() ); 65 | 66 | $this->assertTrue( $columns['wbit_id']->getAutoincrement(), 'id column should have auto increment' ); 67 | $this->assertTrue( $columns['wbit_id']->getNotnull(), 'id column should not be nullable' ); 68 | $this->assertContains( 69 | $columns['wbit_id']->getType()->getName(), 70 | [ Type::BIGINT, Type::INTEGER ], 71 | 'id column should have BIGINT or INTEGER type' 72 | ); 73 | } 74 | 75 | public function testInstallCreatesItemTermsIndexes() { 76 | $this->newTermStore()->install(); 77 | 78 | $table = $this->connection->getSchemaManager()->listTableDetails( $this->tableNames->itemTerms() ); 79 | 80 | $this->assertSame( 81 | [ 'wbit_id' ], 82 | $table->getPrimaryKey()->getColumns(), 83 | 'primary key should be on id column' 84 | ); 85 | 86 | $this->assertSame( 87 | [ 'wbit_item_id' ], 88 | $table->getIndex( 'prefix_wbt_item_terms_item_id' )->getColumns(), 89 | 'prefix_wbt_item_terms_item_id index should exist' 90 | ); 91 | 92 | $this->assertSame( 93 | [ 'wbit_term_in_lang_id' ], 94 | $table->getIndex( 'prefix_wbt_item_terms_term_in_lang_id' )->getColumns(), 95 | 'prefix_wbt_item_terms_term_in_lang_id index should exist' 96 | ); 97 | } 98 | 99 | public function testUninstallDropsTables() { 100 | $this->newTermStore()->install(); 101 | $this->newTermStore()->uninstall(); 102 | 103 | $this->assertTableDoesNotExist( $this->tableNames->itemTerms() ); 104 | $this->assertTableDoesNotExist( $this->tableNames->propertyTerms() ); 105 | $this->assertTableDoesNotExist( $this->tableNames->termInLanguage() ); 106 | $this->assertTableDoesNotExist( $this->tableNames->textInLanguage() ); 107 | $this->assertTableDoesNotExist( $this->tableNames->text() ); 108 | } 109 | 110 | private function assertTableDoesNotExist( string $tableName ) { 111 | $this->assertFalse( 112 | $this->connection->getSchemaManager()->tablesExist( $tableName ), 113 | 'Table "' . $tableName . '" should NOT exist' 114 | ); 115 | } 116 | 117 | public function testCanInstallMultipleStoresInOneDatabaseUsingDifferentPrefixes() { 118 | ( new DoctrineTermStore( $this->connection, 'one_' ) )->install(); 119 | ( new DoctrineTermStore( $this->connection, 'two_' ) )->install(); 120 | 121 | $this->assertTableExists( ( new TableNames( 'one_' ) )->itemTerms() ); 122 | $this->assertTableExists( ( new TableNames( 'two_' ) )->itemTerms() ); 123 | } 124 | 125 | public function testCanInstallMultipleTimes() { 126 | $store = $this->newTermStore(); 127 | 128 | $store->install(); 129 | $store->install(); 130 | 131 | $this->assertTableExists( $this->tableNames->itemTerms() ); 132 | } 133 | 134 | public function testFreshInstallationReportsProgress() { 135 | $messageReporter = new SpyMessageReporter(); 136 | 137 | $this->newTermStore()->install( $messageReporter ); 138 | 139 | $this->assertContains( 140 | 'Installing Wikibase Term Store', 141 | $messageReporter->getMessages() 142 | ); 143 | 144 | $this->assertContains( 145 | 'Finished installing Wikibase Term Store', 146 | $messageReporter->getMessages() 147 | ); 148 | } 149 | 150 | public function testNullInstallationReportsAlreadyInstalled() { 151 | $messageReporter = new SpyMessageReporter(); 152 | 153 | $store = $this->newTermStore(); 154 | 155 | $store->install(); 156 | $store->install( $messageReporter ); 157 | 158 | $this->assertSame( 159 | [ 160 | 'Wikibase Term Store is already installed' 161 | ], 162 | $messageReporter->getMessages() 163 | ); 164 | } 165 | 166 | public function testUninstallReportsProgress() { 167 | $messageReporter = new SpyMessageReporter(); 168 | 169 | $store = $this->newTermStore(); 170 | 171 | $store->install(); 172 | $store->uninstall( $messageReporter ); 173 | 174 | $this->assertSame( 175 | [ 176 | 'Uninstalling Wikibase Term Store: removing tables' 177 | ], 178 | $messageReporter->getMessages() 179 | ); 180 | } 181 | 182 | public function testUninstallReportsNothingToDoWhenNotInstalled() { 183 | $messageReporter = new SpyMessageReporter(); 184 | 185 | $this->newTermStore()->uninstall( $messageReporter ); 186 | 187 | $this->assertSame( 188 | [ 189 | 'Wikibase Term Store is not installed so will not be removed' 190 | ], 191 | $messageReporter->getMessages() 192 | ); 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /tests/Integration/Doctrine/DoctrineItemTermStoreTest.php: -------------------------------------------------------------------------------- 1 | connection = DriverManager::getConnection( [ 44 | 'driver' => 'pdo_sqlite', 45 | 'memory' => true, 46 | ] ); 47 | 48 | $factory = new DoctrineTermStore( $this->connection, 'prefix_' ); 49 | 50 | $factory->install(); 51 | 52 | $this->store = $factory->newItemTermStore(); 53 | $this->tableNames = new TableNames( 'prefix_' ); 54 | } 55 | 56 | public function testWhenItemIsNotStored_getTermsReturnsEmptyFingerprint() { 57 | $this->assertEquals( 58 | new Fingerprint(), 59 | $this->store->getTerms( new ItemId( self::UNKNOWN_ITEM_ID ) ) 60 | ); 61 | } 62 | 63 | /** 64 | * @dataProvider fingerprintProvider 65 | */ 66 | public function testFingerprintRoundtrip( Fingerprint $fingerprint ) { 67 | $itemId = new ItemId( 'Q1' ); 68 | 69 | $this->store->storeTerms( $itemId, $fingerprint ); 70 | 71 | $this->assertEquals( 72 | $fingerprint, 73 | $this->store->getTerms( $itemId ) 74 | ); 75 | } 76 | 77 | public function fingerprintProvider(): \Iterator { 78 | yield 'one label' => [ 79 | new Fingerprint( 80 | new TermList( [ new Term( 'en', 'EnglishLabel' ) ] ) 81 | ) 82 | ]; 83 | 84 | yield 'one description' => [ 85 | new Fingerprint( 86 | null, 87 | new TermList( [ new Term( 'de', 'ZeGermanDescription' ) ] ) 88 | ) 89 | ]; 90 | 91 | yield 'one alias' => [ 92 | new Fingerprint( 93 | null, 94 | null, 95 | new AliasGroupList( [ 96 | new AliasGroup( 'fr', [ 'LeFrenchAlias' ] ) 97 | ] ) 98 | ) 99 | ]; 100 | 101 | yield 'multiple terms' => [ 102 | $this->newFingerprintWithManyTerms() 103 | ]; 104 | } 105 | 106 | private function newFingerprintWithManyTerms(): Fingerprint { 107 | return new Fingerprint( 108 | new TermList( [ 109 | new Term( 'en', 'EnglishLabel' ), 110 | new Term( 'de', 'ZeGermanLabel' ), 111 | new Term( 'fr', 'LeFrenchLabel' ), 112 | ] ), 113 | new TermList( [ 114 | new Term( 'en', 'EnglishDescription' ), 115 | new Term( 'de', 'ZeGermanDescription' ), 116 | ] ), 117 | new AliasGroupList( [ 118 | new AliasGroup( 'fr', [ 'LeFrenchAlias', 'LaFrenchAlias' ] ), 119 | new AliasGroup( 'en', [ 'EnglishAlias' ] ), 120 | ] ) 121 | ); 122 | } 123 | 124 | public function testOnlyTermsOfTheItemAreReturned() { 125 | $itemId = new ItemId( 'Q1' ); 126 | $terms = new Fingerprint( 127 | new TermList( [ 128 | new Term( 'de', 'ZeGermanLabel' ), 129 | ] ), 130 | new TermList( [ 131 | new Term( 'de', 'ZeGermanDescription' ), 132 | ] ), 133 | new AliasGroupList( [ 134 | new AliasGroup( 'de', [ 'ZeGermanAlias' ] ), 135 | ] ) 136 | ); 137 | 138 | $this->store->storeTerms( 139 | $itemId, 140 | $terms 141 | ); 142 | 143 | $this->store->storeTerms( 144 | new ItemId( 'Q2' ), 145 | new Fingerprint( 146 | new TermList( [ 147 | new Term( 'en', 'EnglishLabel' ), 148 | ] ), 149 | new TermList( [ 150 | new Term( 'en', 'EnglishDescription' ), 151 | ] ), 152 | new AliasGroupList( [ 153 | new AliasGroup( 'en', [ 'EnglishAlias' ] ), 154 | ] ) 155 | ) 156 | ); 157 | 158 | $this->assertEquals( 159 | $terms, 160 | $this->store->getTerms( $itemId ) 161 | ); 162 | } 163 | 164 | public function testDeletionRemovesReturnsOfTarget() { 165 | $itemId = new ItemId( 'Q1' ); 166 | 167 | $this->store->storeTerms( 168 | $itemId, 169 | $this->newFingerprintWithManyTerms() 170 | ); 171 | 172 | $this->store->deleteTerms( $itemId ); 173 | 174 | $this->assertEquals( 175 | new Fingerprint(), 176 | $this->store->getTerms( $itemId ) 177 | ); 178 | } 179 | 180 | public function testDeletionOnlyRemovesTargetTerms() { 181 | $itemId = new ItemId( 'Q1' ); 182 | 183 | $this->store->storeTerms( 184 | $itemId, 185 | $this->newFingerprintWithManyTerms() 186 | ); 187 | 188 | $this->store->deleteTerms( new ItemId( 'Q2' ) ); 189 | 190 | $this->assertEquals( 191 | $this->newFingerprintWithManyTerms(), 192 | $this->store->getTerms( $itemId ) 193 | ); 194 | } 195 | 196 | public function testStoreTermsUsesExistingRecordsOfOtherItems() { 197 | $fingerprint = new Fingerprint( 198 | new TermList( [ 199 | new Term( 'en', 'EnglishLabel' ), 200 | ] ) 201 | ); 202 | 203 | $this->store->storeTerms( new ItemId( 'Q1' ), $fingerprint ); 204 | $this->store->storeTerms( new ItemId( 'Q2' ), $fingerprint ); 205 | 206 | $this->assertEquals( 207 | $fingerprint, 208 | $this->store->getTerms( new ItemId( 'Q2' ) ) 209 | ); 210 | 211 | $this->assertTableRowCount( 1, $this->tableNames->text() ); 212 | $this->assertTableRowCount( 1, $this->tableNames->textInLanguage() ); 213 | $this->assertTableRowCount( 1, $this->tableNames->termInLanguage() ); 214 | } 215 | 216 | private function assertTableRowCount( $expectedCount, $tableName ) { 217 | $this->assertSame( 218 | (string)$expectedCount, 219 | $this->connection->executeQuery( 'SELECT count(*) as records FROM ' . $tableName )->fetchColumn(), 220 | 'Table ' . $tableName . ' should contain ' . (string)$expectedCount . ' records' 221 | ); 222 | } 223 | 224 | public function testStoreTermsRemovesOldItemTerms() { 225 | $itemId = new ItemId( 'Q1' ); 226 | 227 | $oldFingerprint = new Fingerprint( 228 | new TermList( [ 229 | new Term( 'en', 'EnglishLabel' ), 230 | new Term( 'de', 'ZeGermanLabel' ), 231 | ] ) 232 | ); 233 | 234 | $newFingerprint = new Fingerprint( 235 | new TermList( [ 236 | new Term( 'en', 'EnglishLabel' ), 237 | new Term( 'fr', 'LeFrenchLabel' ), 238 | ] ) 239 | ); 240 | 241 | $this->store->storeTerms( $itemId, $oldFingerprint ); 242 | $this->store->storeTerms( $itemId, $newFingerprint ); 243 | 244 | $this->assertEquals( 245 | $newFingerprint, 246 | $this->store->getTerms( $itemId ) 247 | ); 248 | } 249 | 250 | public function testGetTermsThrowsExceptionOnInfrastructureFailure() { 251 | $store = $this->newStoreWithThrowingConnection(); 252 | 253 | $this->expectException( TermStoreException::class ); 254 | $store->getTerms( new ItemId( 'Q1' ) ); 255 | } 256 | 257 | private function newStoreWithThrowingConnection(): ItemTermStore { 258 | return ( new DoctrineTermStore( $this->newThrowingDoctrineConnection(), '' ) )->newItemTermStore(); 259 | } 260 | 261 | private function newThrowingDoctrineConnection(): Connection { 262 | $connection = $this->createMock( Connection::class ); 263 | 264 | $connection->method( $this->anything() ) 265 | ->willThrowException( new DBALException() ); 266 | 267 | return $connection; 268 | } 269 | 270 | public function testStoreTermsThrowsExceptionOnInfrastructureFailure() { 271 | $store = $this->newStoreWithThrowingConnection(); 272 | 273 | $this->expectException( TermStoreException::class ); 274 | $store->storeTerms( new ItemId( 'Q1' ), $this->newFingerprintWithManyTerms() ); 275 | } 276 | 277 | public function testDeleteTermsThrowsExceptionOnInfrastructureFailure() { 278 | $store = $this->newStoreWithThrowingConnection(); 279 | 280 | $this->expectException( TermStoreException::class ); 281 | $store->deleteTerms( new ItemId( 'Q1' ) ); 282 | } 283 | 284 | } 285 | -------------------------------------------------------------------------------- /tests/Integration/Doctrine/DoctrinePropertyTermStoreTest.php: -------------------------------------------------------------------------------- 1 | connection = DriverManager::getConnection( [ 44 | 'driver' => 'pdo_sqlite', 45 | 'memory' => true, 46 | ] ); 47 | 48 | $factory = new DoctrineTermStore( $this->connection, 'prefix_' ); 49 | 50 | $factory->install(); 51 | 52 | $this->store = $factory->newPropertyTermStore(); 53 | $this->tableNames = new TableNames( 'prefix_' ); 54 | } 55 | 56 | public function testWhenPropertyIsNotStored_getTermsReturnsEmptyFingerprint() { 57 | $this->assertEquals( 58 | new Fingerprint(), 59 | $this->store->getTerms( new PropertyId( self::UNKNOWN_PROPERTY_ID ) ) 60 | ); 61 | } 62 | 63 | /** 64 | * @dataProvider fingerprintProvider 65 | */ 66 | public function testFingerprintRoundtrip( Fingerprint $fingerprint ) { 67 | $propertyId = new PropertyId( 'P1' ); 68 | 69 | $this->store->storeTerms( $propertyId, $fingerprint ); 70 | 71 | $this->assertEquals( 72 | $fingerprint, 73 | $this->store->getTerms( $propertyId ) 74 | ); 75 | } 76 | 77 | public function fingerprintProvider(): \Iterator { 78 | yield 'one label' => [ 79 | new Fingerprint( 80 | new TermList( [ new Term( 'en', 'EnglishLabel' ) ] ) 81 | ) 82 | ]; 83 | 84 | yield 'one description' => [ 85 | new Fingerprint( 86 | null, 87 | new TermList( [ new Term( 'de', 'ZeGermanDescription' ) ] ) 88 | ) 89 | ]; 90 | 91 | yield 'one alias' => [ 92 | new Fingerprint( 93 | null, 94 | null, 95 | new AliasGroupList( [ 96 | new AliasGroup( 'fr', [ 'LeFrenchAlias' ] ) 97 | ] ) 98 | ) 99 | ]; 100 | 101 | yield 'multiple terms' => [ 102 | $this->newFingerprintWithManyTerms() 103 | ]; 104 | } 105 | 106 | private function newFingerprintWithManyTerms(): Fingerprint { 107 | return new Fingerprint( 108 | new TermList( [ 109 | new Term( 'en', 'EnglishLabel' ), 110 | new Term( 'de', 'ZeGermanLabel' ), 111 | new Term( 'fr', 'LeFrenchLabel' ), 112 | ] ), 113 | new TermList( [ 114 | new Term( 'en', 'EnglishDescription' ), 115 | new Term( 'de', 'ZeGermanDescription' ), 116 | ] ), 117 | new AliasGroupList( [ 118 | new AliasGroup( 'fr', [ 'LeFrenchAlias', 'LaFrenchAlias' ] ), 119 | new AliasGroup( 'en', [ 'EnglishAlias' ] ), 120 | ] ) 121 | ); 122 | } 123 | 124 | public function testOnlyTermsOfThePropertyAreReturned() { 125 | $propertyId = new PropertyId( 'P1' ); 126 | $terms = new Fingerprint( 127 | new TermList( [ 128 | new Term( 'de', 'ZeGermanLabel' ), 129 | ] ), 130 | new TermList( [ 131 | new Term( 'de', 'ZeGermanDescription' ), 132 | ] ), 133 | new AliasGroupList( [ 134 | new AliasGroup( 'de', [ 'ZeGermanAlias' ] ), 135 | ] ) 136 | ); 137 | 138 | $this->store->storeTerms( 139 | $propertyId, 140 | $terms 141 | ); 142 | 143 | $this->store->storeTerms( 144 | new PropertyId( 'P2' ), 145 | new Fingerprint( 146 | new TermList( [ 147 | new Term( 'en', 'EnglishLabel' ), 148 | ] ), 149 | new TermList( [ 150 | new Term( 'en', 'EnglishDescription' ), 151 | ] ), 152 | new AliasGroupList( [ 153 | new AliasGroup( 'en', [ 'EnglishAlias' ] ), 154 | ] ) 155 | ) 156 | ); 157 | 158 | $this->assertEquals( 159 | $terms, 160 | $this->store->getTerms( $propertyId ) 161 | ); 162 | } 163 | 164 | public function testDeletionRemovesReturnsOfTarget() { 165 | $propertyId = new PropertyId( 'P1' ); 166 | 167 | $this->store->storeTerms( 168 | $propertyId, 169 | $this->newFingerprintWithManyTerms() 170 | ); 171 | 172 | $this->store->deleteTerms( $propertyId ); 173 | 174 | $this->assertEquals( 175 | new Fingerprint(), 176 | $this->store->getTerms( $propertyId ) 177 | ); 178 | } 179 | 180 | public function testDeletionOnlyRemovesTargetTerms() { 181 | $propertyId = new PropertyId( 'P1' ); 182 | 183 | $this->store->storeTerms( 184 | $propertyId, 185 | $this->newFingerprintWithManyTerms() 186 | ); 187 | 188 | $this->store->deleteTerms( new PropertyId( 'P2' ) ); 189 | 190 | $this->assertEquals( 191 | $this->newFingerprintWithManyTerms(), 192 | $this->store->getTerms( $propertyId ) 193 | ); 194 | } 195 | 196 | public function testStoreTermsUsesExistingRecordsOfOtherProperties() { 197 | $fingerprint = new Fingerprint( 198 | new TermList( [ 199 | new Term( 'en', 'EnglishLabel' ), 200 | ] ) 201 | ); 202 | 203 | $this->store->storeTerms( new PropertyId( 'P1' ), $fingerprint ); 204 | $this->store->storeTerms( new PropertyId( 'P2' ), $fingerprint ); 205 | 206 | $this->assertEquals( 207 | $fingerprint, 208 | $this->store->getTerms( new PropertyId( 'P2' ) ) 209 | ); 210 | 211 | $this->assertTableRowCount( 1, $this->tableNames->text() ); 212 | $this->assertTableRowCount( 1, $this->tableNames->textInLanguage() ); 213 | $this->assertTableRowCount( 1, $this->tableNames->termInLanguage() ); 214 | } 215 | 216 | private function assertTableRowCount( $expectedCount, $tableName ) { 217 | $this->assertSame( 218 | (string)$expectedCount, 219 | $this->connection->executeQuery( 'SELECT count(*) as records FROM ' . $tableName )->fetchColumn(), 220 | 'Table ' . $tableName . ' should contain ' . (string)$expectedCount . ' records' 221 | ); 222 | } 223 | 224 | public function testStoreTermsRemovesOldPropertyTerms() { 225 | $propertyId = new PropertyId( 'P1' ); 226 | 227 | $oldFingerprint = new Fingerprint( 228 | new TermList( [ 229 | new Term( 'en', 'EnglishLabel' ), 230 | new Term( 'de', 'ZeGermanLabel' ), 231 | ] ) 232 | ); 233 | 234 | $newFingerprint = new Fingerprint( 235 | new TermList( [ 236 | new Term( 'en', 'EnglishLabel' ), 237 | new Term( 'fr', 'LeFrenchLabel' ), 238 | ] ) 239 | ); 240 | 241 | $this->store->storeTerms( $propertyId, $oldFingerprint ); 242 | $this->store->storeTerms( $propertyId, $newFingerprint ); 243 | 244 | $this->assertEquals( 245 | $newFingerprint, 246 | $this->store->getTerms( $propertyId ) 247 | ); 248 | } 249 | 250 | public function testGetTermsThrowsExceptionOnInfrastructureFailure() { 251 | $store = $this->newStoreWithThrowingConnection(); 252 | 253 | $this->expectException( TermStoreException::class ); 254 | $store->getTerms( new PropertyId( 'P1' ) ); 255 | } 256 | 257 | private function newStoreWithThrowingConnection(): PropertyTermStore { 258 | return ( new DoctrineTermStore( $this->newThrowingDoctrineConnection(), '' ) )->newPropertyTermStore(); 259 | } 260 | 261 | private function newThrowingDoctrineConnection(): Connection { 262 | $connection = $this->createMock( Connection::class ); 263 | 264 | $connection->method( $this->anything() ) 265 | ->willThrowException( new DBALException() ); 266 | 267 | return $connection; 268 | } 269 | 270 | public function testStoreTermsThrowsExceptionOnInfrastructureFailure() { 271 | $store = $this->newStoreWithThrowingConnection(); 272 | 273 | $this->expectException( TermStoreException::class ); 274 | $store->storeTerms( new PropertyId( 'P1' ), $this->newFingerprintWithManyTerms() ); 275 | } 276 | 277 | public function testDeleteTermsThrowsExceptionOnInfrastructureFailure() { 278 | $store = $this->newStoreWithThrowingConnection(); 279 | 280 | $this->expectException( TermStoreException::class ); 281 | $store->deleteTerms( new PropertyId( 'P1' ) ); 282 | } 283 | 284 | public function testDeduplicationWorksWhenOnlyTextIsTheSame() { 285 | $text = '某实体所取得的纪录'; 286 | 287 | $this->store->storeTerms( 288 | new PropertyId( 'P1' ), 289 | new Fingerprint( 290 | new TermList( [ 291 | new Term( 'en', $text ), 292 | ] ) 293 | ) 294 | ); 295 | 296 | $this->store->storeTerms( 297 | new PropertyId( 'P2' ), 298 | new Fingerprint( 299 | new TermList(), 300 | new TermList( [ 301 | new Term( 'de', $text ), 302 | ] ) 303 | ) 304 | ); 305 | 306 | $this->assertTableRowCount( 1, $this->tableNames->text() ); 307 | $this->assertTableRowCount( 2, $this->tableNames->textInLanguage() ); 308 | $this->assertTableRowCount( 2, $this->tableNames->termInLanguage() ); 309 | } 310 | 311 | } 312 | --------------------------------------------------------------------------------