From f6b5a6d297b3740433d37e4d4fdf68838a6d1f6c Mon Sep 17 00:00:00 2001 From: Morne Alberts Date: Sun, 5 Jan 2025 10:14:53 +0200 Subject: [PATCH] Build Elasticsearch queries --- phpstan-baseline.neon | 18 ++- phpstan.neon | 1 + .../WikibaseFacetedSearchHooks.php | 4 +- .../Query/DelegatingFacetQueryBuilder.php | 31 ++++ .../Search/Query/FacetQueryBuilder.php | 15 ++ .../Search/Query/HasWbFacetFeature.php | 61 ++++++-- .../Search/Query/ItemTypeQueryBuilder.php | 29 ++++ .../Search/Query/ListFacetQueryBuilder.php | 56 +++++++ .../Search/Query/RangeFacetQueryBuilder.php | 50 ++++++ src/WikibaseFacetedSearchExtension.php | 46 +++++- .../Search/Query/HasWbFacetFeatureTest.php | 16 +- .../Search/Query/ItemTypeQueryBuilderTest.php | 57 +++++++ .../Query/ListFacetQueryBuilderTest.php | 147 ++++++++++++++++++ .../Query/RangeFacetQueryBuilderTest.php | 132 ++++++++++++++++ 14 files changed, 645 insertions(+), 18 deletions(-) create mode 100644 src/Persistence/Search/Query/DelegatingFacetQueryBuilder.php create mode 100644 src/Persistence/Search/Query/FacetQueryBuilder.php create mode 100644 src/Persistence/Search/Query/ItemTypeQueryBuilder.php create mode 100644 src/Persistence/Search/Query/ListFacetQueryBuilder.php create mode 100644 src/Persistence/Search/Query/RangeFacetQueryBuilder.php create mode 100644 tests/phpunit/Persistence/Search/Query/ItemTypeQueryBuilderTest.php create mode 100644 tests/phpunit/Persistence/Search/Query/ListFacetQueryBuilderTest.php create mode 100644 tests/phpunit/Persistence/Search/Query/RangeFacetQueryBuilderTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 11f9c46..765647c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -31,7 +31,19 @@ parameters: path: src/Persistence/ConfigDeserializer.php - - message: '#^Cannot cast mixed to string\.$#' - identifier: cast.string + message: '#^Parameter \#2 \$terms of class Elastica\\Query\\Terms constructor expects list\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Persistence/Search/Query/ItemTypeQueryBuilder.php + + - + message: '#^Parameter \#2 \$terms of class Elastica\\Query\\Terms constructor expects list\, array given\.$#' + identifier: argument.type + count: 1 + path: src/Persistence/Search/Query/ListFacetQueryBuilder.php + + - + message: '#^Parameter \#2 \$terms of class Elastica\\Query\\Terms constructor expects list\, array\ given\.$#' + identifier: argument.type count: 1 - path: src/WikibaseFacetedSearchExtension.php + path: src/Persistence/Search/Query/ListFacetQueryBuilder.php diff --git a/phpstan.neon b/phpstan.neon index beaa706..c2d5aa3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -20,6 +20,7 @@ parameters: - src/Persistence/Search/SearchIndexFieldsBuilder.php - src/Persistence/Search/Query/HasWbFacetFeature.php - src/Persistence/Elastic*.php + - src/WikibaseFacetedSearchExtension.php reportUnmatchedIgnoredErrors: false ignoreErrors: - '#no value type specified in iterable type array\.$#' diff --git a/src/EntryPoints/WikibaseFacetedSearchHooks.php b/src/EntryPoints/WikibaseFacetedSearchHooks.php index 58fddcb..81c1b32 100644 --- a/src/EntryPoints/WikibaseFacetedSearchHooks.php +++ b/src/EntryPoints/WikibaseFacetedSearchHooks.php @@ -139,7 +139,9 @@ private static function getConfigPageHtml( string $html ): string { * @param KeywordFeature[] &$extraFeatures */ public static function onCirrusSearchAddQueryFeatures( SearchConfig $config, array &$extraFeatures ): void { - $extraFeatures[] = new HasWbFacetFeature(); + if ( WikibaseFacetedSearchExtension::getInstance()->getConfig()->isComplete() ) { + $extraFeatures[] = WikibaseFacetedSearchExtension::getInstance()->newHasWbFacetFeature(); + } } /** diff --git a/src/Persistence/Search/Query/DelegatingFacetQueryBuilder.php b/src/Persistence/Search/Query/DelegatingFacetQueryBuilder.php new file mode 100644 index 0000000..9e58563 --- /dev/null +++ b/src/Persistence/Search/Query/DelegatingFacetQueryBuilder.php @@ -0,0 +1,31 @@ + + */ + private array $buildersPerType; + + public function addBuilder( FacetType $type, FacetQueryBuilder $builder ): void { + $this->buildersPerType[$type->value] = $builder; + } + + public function buildQuery( FacetConfig $config, PropertyConstraints $state ): ?AbstractQuery { + if ( !array_key_exists( $config->type->value, $this->buildersPerType ) ) { + throw new \RuntimeException( 'No query builder for facet type ' . $config->type->value ); + } + + return $this->buildersPerType[$config->type->value]->buildQuery( $config, $state ); + } + +} diff --git a/src/Persistence/Search/Query/FacetQueryBuilder.php b/src/Persistence/Search/Query/FacetQueryBuilder.php new file mode 100644 index 0000000..fe3d0d7 --- /dev/null +++ b/src/Persistence/Search/Query/FacetQueryBuilder.php @@ -0,0 +1,15 @@ +getItemTypes( $context->getOriginalSearchTerm() ); + + if ( $itemTypes === [] ) { + return [ null, false ]; + } + + $constraints = $this->getConstraintsForValue( $value ); + + if ( $constraints === null ) { + return [ $this->itemTypeQueryBuilder->buildQuery( $itemTypes ), false ]; + } + + foreach ( $itemTypes as $itemTypeId ) { + $facet = $this->config->getConfigForProperty( $itemTypeId, $constraints->propertyId ); + + if ( $facet === null ) { + continue; + } + + return [ $this->facetQueryBuilder->buildQuery( $facet, $constraints ), false ]; + } + return [ null, false ]; } - public function getFilterQuery( KeywordFeatureNode $node, QueryBuildingContext $context ): ?AbstractQuery { - // TODO - return null; + /** + * @return ItemId[] + */ + private function getItemTypes( string $originalQueryString ): array { + return $this->queryStringParser->parse( $originalQueryString )->getItemTypes(); + } + + /** + * @param string $value Keyword value for a single facet + */ + private function getConstraintsForValue( string $value ): ?PropertyConstraints { + $constraints = $this->queryStringParser->parse( "haswbfacet:$value" )->getConstraintsPerProperty(); + + return $constraints === [] ? null : reset( $constraints ); } } diff --git a/src/Persistence/Search/Query/ItemTypeQueryBuilder.php b/src/Persistence/Search/Query/ItemTypeQueryBuilder.php new file mode 100644 index 0000000..ab21643 --- /dev/null +++ b/src/Persistence/Search/Query/ItemTypeQueryBuilder.php @@ -0,0 +1,29 @@ +itemTypeProperty->getSerialization(), + array_map( + fn( ItemId $itemType ) => $itemType->getSerialization(), + $itemTypes + ) + ); + } + +} diff --git a/src/Persistence/Search/Query/ListFacetQueryBuilder.php b/src/Persistence/Search/Query/ListFacetQueryBuilder.php new file mode 100644 index 0000000..627a5b5 --- /dev/null +++ b/src/Persistence/Search/Query/ListFacetQueryBuilder.php @@ -0,0 +1,56 @@ +propertyId->getSerialization(); + + return match ( $this->dataTypeLookup->getDataTypeIdForProperty( $config->propertyId ) ) { + 'string' => $this->buildStringQuery( $name, $state ), + 'wikibase-item' => $this->buildStringQuery( $name, $state ), + 'quantity' => $this->buildQuantityQuery( $name, $state ), + default => null + }; + } + + private function buildStringQuery( string $name, PropertyConstraints $state ): AbstractQuery { + return new Query\Terms( + $name, + $this->getFacetValues( $state ) + ); + } + + private function getFacetValues( PropertyConstraints $state ): array { + if ( $state->getOrSelectedValues() !== [] ) { + return $state->getOrSelectedValues(); + } + + return $state->getAndSelectedValues(); + } + + private function buildQuantityQuery( string $name, PropertyConstraints $state ): AbstractQuery { + return new Query\Terms( + $name, + array_map( + fn( $value ) => (float)$value, + $this->getFacetValues( $state ) + ) + ); + } + +} diff --git a/src/Persistence/Search/Query/RangeFacetQueryBuilder.php b/src/Persistence/Search/Query/RangeFacetQueryBuilder.php new file mode 100644 index 0000000..9b86440 --- /dev/null +++ b/src/Persistence/Search/Query/RangeFacetQueryBuilder.php @@ -0,0 +1,50 @@ +propertyId->getSerialization(); + + return match ( $this->dataTypeLookup->getDataTypeIdForProperty( $config->propertyId ) ) { + 'quantity' => $this->buildQuantityQuery( $name, $state ), + 'time' => $this->buildTimeQuery( $name, $state ), + default => null + }; + } + + private function buildQuantityQuery( string $name, PropertyConstraints $state ): AbstractQuery { + return new Query\Range( + $name, + [ + 'gte' => $state->getInclusiveMinimum(), + 'lte' => $state->getInclusiveMaximum() + ] + ); + } + + private function buildTimeQuery( string $name, PropertyConstraints $state ): AbstractQuery { + return new Query\Range( + $name, + [ + 'gte' => $state->getInclusiveMinimum() === null ? null : (int)$state->getInclusiveMinimum() . '-01-01', + 'lte' => $state->getInclusiveMaximum() === null ? null : (int)$state->getInclusiveMaximum() . '-12-31', + ] + ); + } + +} diff --git a/src/WikibaseFacetedSearchExtension.php b/src/WikibaseFacetedSearchExtension.php index 0e5714d..ddd855f 100644 --- a/src/WikibaseFacetedSearchExtension.php +++ b/src/WikibaseFacetedSearchExtension.php @@ -30,6 +30,11 @@ use ProfessionalWiki\WikibaseFacetedSearch\Persistence\PageContentConfigLookup; use ProfessionalWiki\WikibaseFacetedSearch\Persistence\PageContentFetcher; use ProfessionalWiki\WikibaseFacetedSearch\Persistence\PageItemLookupFactory; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\DelegatingFacetQueryBuilder; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\HasWbFacetFeature; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\ItemTypeQueryBuilder; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\ListFacetQueryBuilder; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\RangeFacetQueryBuilder; use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\SearchIndexFieldsBuilder; use ProfessionalWiki\WikibaseFacetedSearch\Persistence\SitelinkBasedStatementsLookup; use ProfessionalWiki\WikibaseFacetedSearch\Presentation\DelegatingFacetHtmlBuilder; @@ -41,6 +46,7 @@ use TemplateParser; use Title; use Wikibase\DataModel\Services\Lookup\LabelLookup; +use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookup; use Wikibase\Lib\Store\SiteLinkStore; use Wikibase\Repo\WikibaseRepo; @@ -126,10 +132,14 @@ public function newSearchIndexFieldsBuilder( CirrusSearch $engine ): SearchIndex return new SearchIndexFieldsBuilder( engine: $engine, config: $this->getConfig(), - dataTypeLookup: WikibaseRepo::getPropertyDataTypeLookup() + dataTypeLookup: $this->getPropertyDataTypeLookup() ); } + private function getPropertyDataTypeLookup(): PropertyDataTypeLookup { + return WikibaseRepo::getPropertyDataTypeLookup(); + } + public function newStatementsLookup(): StatementsLookup { if ( $this->getConfig()->linkTargetSitelinkSiteId === null ) { return new FromPageStatementsLookup(); @@ -253,4 +263,38 @@ public function getLabelLookup( Language $language ): LabelLookup { return WikibaseRepo::getFallbackLabelDescriptionLookupFactory()->newLabelDescriptionLookup( $language ); } + public function newHasWbFacetFeature(): HasWbFacetFeature { + return new HasWbFacetFeature( + config: $this->getConfig(), + queryStringParser: $this->getQueryStringParser(), + itemTypeQueryBuilder: $this->getItemTypeQueryBuilder(), + facetQueryBuilder: $this->getFacetQueryBuilder() + ); + } + + private function getItemTypeQueryBuilder(): ItemTypeQueryBuilder { + return new ItemTypeQueryBuilder( + itemTypeProperty: $this->getConfig()->getItemTypeProperty() + ); + } + + private function getFacetQueryBuilder(): DelegatingFacetQueryBuilder { + $delegator = new DelegatingFacetQueryBuilder(); + $delegator->addBuilder( FacetType::LIST, $this->newListFacetQueryBuilder() ); + $delegator->addBuilder( FacetType::RANGE, $this->newRangeFacetQueryBuilder() ); + return $delegator; + } + + private function newListFacetQueryBuilder(): ListFacetQueryBuilder { + return new ListFacetQueryBuilder( + dataTypeLookup: $this->getPropertyDataTypeLookup() + ); + } + + private function newRangeFacetQueryBuilder(): RangeFacetQueryBuilder { + return new RangeFacetQueryBuilder( + dataTypeLookup: $this->getPropertyDataTypeLookup() + ); + } + } diff --git a/tests/phpunit/Persistence/Search/Query/HasWbFacetFeatureTest.php b/tests/phpunit/Persistence/Search/Query/HasWbFacetFeatureTest.php index ca7d3a6..98f6d6b 100644 --- a/tests/phpunit/Persistence/Search/Query/HasWbFacetFeatureTest.php +++ b/tests/phpunit/Persistence/Search/Query/HasWbFacetFeatureTest.php @@ -5,8 +5,13 @@ namespace ProfessionalWiki\WikibaseFacetedSearch\Tests\Persistence\Search\Query; use CirrusSearch\Query\KeywordFeatureAssertions; +use ProfessionalWiki\WikibaseFacetedSearch\Application\Config; +use ProfessionalWiki\WikibaseFacetedSearch\Application\QueryStringParser; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\DelegatingFacetQueryBuilder; use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\HasWbFacetFeature; +use ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\ItemTypeQueryBuilder; use ProfessionalWiki\WikibaseFacetedSearch\Tests\WikibaseFacetedSearchIntegrationTest; +use Wikibase\DataModel\Entity\NumericPropertyId; /** * @covers \ProfessionalWiki\WikibaseFacetedSearch\Persistence\Search\Query\HasWbFacetFeature @@ -23,7 +28,7 @@ protected function setUp(): void { * @dataProvider noDataProvider */ public function testNotConsumed( $term ) { - $feature = new HasWbFacetFeature(); + $feature = $this->newHasWbFacetFeature(); $this->getKWAssertions()->assertNotConsumed( $feature, $term ); } @@ -38,6 +43,15 @@ public static function noDataProvider() { ]; } + private function newHasWbFacetFeature(): HasWbFacetFeature { + return new HasWbFacetFeature( + new Config(), + new QueryStringParser( new NumericPropertyId( 'P42' ) ), + new ItemTypeQueryBuilder( new NumericPropertyId( 'P42' ) ), + new DelegatingFacetQueryBuilder() + ); + } + /** * @return KeywordFeatureAssertions */ diff --git a/tests/phpunit/Persistence/Search/Query/ItemTypeQueryBuilderTest.php b/tests/phpunit/Persistence/Search/Query/ItemTypeQueryBuilderTest.php new file mode 100644 index 0000000..7461730 --- /dev/null +++ b/tests/phpunit/Persistence/Search/Query/ItemTypeQueryBuilderTest.php @@ -0,0 +1,57 @@ +newItemTypeQueryBuilder(); + + $query = $itemTypeQueryBuilder->buildQuery( [ + new ItemId( 'Q100' ) + ] ); + + $this->assertEquals( + new Query\Terms( + 'wbfs_P42', + [ 'Q100' ] + ), + $query + ); + } + + private function newItemTypeQueryBuilder(): ItemTypeQueryBuilder { + return new ItemTypeQueryBuilder( new NumericPropertyId( self::ITEM_TYPE_PROPERTY ) ); + } + + public function testBuildsTermsQueryWithMultipleItemTypes(): void { + $itemTypeQueryBuilder = $this->newItemTypeQueryBuilder(); + + $query = $itemTypeQueryBuilder->buildQuery( [ + new ItemId( 'Q100' ), + new ItemId( 'Q200' ) + ] ); + + $this->assertEquals( + new Query\Terms( + 'wbfs_P42', + [ 'Q100', 'Q200' ] + ), + $query + ); + } + +} diff --git a/tests/phpunit/Persistence/Search/Query/ListFacetQueryBuilderTest.php b/tests/phpunit/Persistence/Search/Query/ListFacetQueryBuilderTest.php new file mode 100644 index 0000000..88c7a94 --- /dev/null +++ b/tests/phpunit/Persistence/Search/Query/ListFacetQueryBuilderTest.php @@ -0,0 +1,147 @@ +newFacetQueryBuilder()->buildQuery( + $this->newListFacetConfig( self::STRING_PROPERTY ), + new PropertyConstraints( + new NumericPropertyId( self::STRING_PROPERTY ), + orSelectedValues: [ 'foo', 'bar' ] + ) + ); + + $this->assertIsTermsQueryWithParams( + self::STRING_PROPERTY, + [ 'foo', 'bar' ], + $query + ); + } + + private function newFacetQueryBuilder(): ListFacetQueryBuilder { + return new ListFacetQueryBuilder( + $this->newDataTypeLookup() + ); + } + + private function newDataTypeLookup(): InMemoryDataTypeLookup { + $dataTypeLookup = new InMemoryDataTypeLookup(); + + $types = [ + self::QUANTITY_PROPERTY => 'quantity', + self::STRING_PROPERTY => 'string', + self::TIME_PROPERTY => 'time', + self::ITEM_PROPERTY => 'wikibase-item' + ]; + + foreach ( $types as $pId => $type ) { + $dataTypeLookup->setDataTypeForProperty( + new NumericPropertyId( $pId ), + $type + ); + } + + return $dataTypeLookup; + } + + private function newListFacetConfig( string $propertyId ): FacetConfig { + return new FacetConfig( + new ItemId( 'Q404' ), + new NumericPropertyId( $propertyId ), + FacetType::LIST + ); + } + + private function assertIsTermsQueryWithParams( string $propertyId, array $values, Query\AbstractQuery $query ): void { + $this->assertInstanceOf( Query\Terms::class, $query ); + $this->assertSame( + [ + 'wbfs_' . $propertyId => $values + ], + $query->getParams() + ); + } + + public function testBuildsQueryForItemList(): void { + $query = $this->newFacetQueryBuilder()->buildQuery( + $this->newListFacetConfig( self::ITEM_PROPERTY ), + new PropertyConstraints( + new NumericPropertyId( self::ITEM_PROPERTY ), + orSelectedValues: [ 'Q100', 'Q200' ] + ) + ); + + $this->assertIsTermsQueryWithParams( + self::ITEM_PROPERTY, + [ 'Q100', 'Q200' ], + $query + ); + } + + public function testBuildsQueryForQuantityList(): void { + $query = $this->newFacetQueryBuilder()->buildQuery( + $this->newListFacetConfig( self::QUANTITY_PROPERTY ), + new PropertyConstraints( + new NumericPropertyId( self::QUANTITY_PROPERTY ), + orSelectedValues: [ '42', '100.50', '9001' ] + ) + ); + + $this->assertIsTermsQueryWithParams( + self::QUANTITY_PROPERTY, + [ 42.0, 100.5, 9001.0 ], + $query + ); + } + + public function testDoesNotBuildQueryForDateList(): void { + $query = $this->newFacetQueryBuilder()->buildQuery( + $this->newListFacetConfig( self::TIME_PROPERTY ), + new PropertyConstraints( + new NumericPropertyId( self::TIME_PROPERTY ), + orSelectedValues: [ '2000', '2010' ] + ) + ); + + $this->assertNull( $query ); // TODO: implement date list query + } + + public function testBuildsQueryForStringListWithSingleAndValue(): void { + $query = $this->newFacetQueryBuilder()->buildQuery( + $this->newListFacetConfig( self::STRING_PROPERTY ), + new PropertyConstraints( + new NumericPropertyId( self::STRING_PROPERTY ), + andSelectedValues: [ 'foo' ] + ) + ); + + $this->assertIsTermsQueryWithParams( + self::STRING_PROPERTY, + [ 'foo' ], + $query + ); + } + +} diff --git a/tests/phpunit/Persistence/Search/Query/RangeFacetQueryBuilderTest.php b/tests/phpunit/Persistence/Search/Query/RangeFacetQueryBuilderTest.php new file mode 100644 index 0000000..f2d55c4 --- /dev/null +++ b/tests/phpunit/Persistence/Search/Query/RangeFacetQueryBuilderTest.php @@ -0,0 +1,132 @@ +newFacetQueryBuilder()->buildQuery( + $this->newRangeFacetConfig( self::QUANTITY_PROPERTY ), + new PropertyConstraints( + new NumericPropertyId( self::QUANTITY_PROPERTY ), + min: 42, + max: 9001 + ) + ); + + $this->assertIsRangeQueryWithParams( + self::QUANTITY_PROPERTY, + [ 'gte' => 42.0, 'lte' => 9001.0 ], + $query + ); + } + + private function newFacetQueryBuilder(): RangeFacetQueryBuilder { + return new RangeFacetQueryBuilder( + $this->newDataTypeLookup() + ); + } + + private function newDataTypeLookup(): InMemoryDataTypeLookup { + $dataTypeLookup = new InMemoryDataTypeLookup(); + + $types = [ + self::QUANTITY_PROPERTY => 'quantity', + self::STRING_PROPERTY => 'string', + self::TIME_PROPERTY => 'time', + self::ITEM_PROPERTY => 'wikibase-item' + ]; + + foreach ( $types as $pId => $type ) { + $dataTypeLookup->setDataTypeForProperty( + new NumericPropertyId( $pId ), + $type + ); + } + + return $dataTypeLookup; + } + + private function newRangeFacetConfig( string $propertyId ): FacetConfig { + return new FacetConfig( + new ItemId( 'Q404' ), + new NumericPropertyId( $propertyId ), + FacetType::RANGE + ); + } + + private function assertIsRangeQueryWithParams( string $propertyId, array $values, Query\AbstractQuery $query ): void { + $this->assertInstanceOf( Query\Range::class, $query ); + $this->assertSame( + [ + 'wbfs_' . $propertyId => $values + ], + $query->getParams() + ); + } + + public function testBuildsQueryForDateRange(): void { + $query = $this->newFacetQueryBuilder()->buildQuery( + $this->newRangeFacetConfig( self::TIME_PROPERTY ), + new PropertyConstraints( + new NumericPropertyId( self::TIME_PROPERTY ), + min: 2000, + max: 2010 + ) + ); + + $this->assertEquals( + new Query\Range( + 'wbfs_' . self::TIME_PROPERTY, + [ 'gte' => '2000-01-01', 'lte' => '2010-12-31' ] + ), + $query + ); + } + + public function testDoesNotBuildQueryForStringRange(): void { + $query = $this->newFacetQueryBuilder()->buildQuery( + $this->newRangeFacetConfig( self::STRING_PROPERTY ), + new PropertyConstraints( + new NumericPropertyId( self::STRING_PROPERTY ), + orSelectedValues: [ '2000', '2010' ] + ) + ); + + $this->assertNull( $query ); + } + + public function testDoesNotBuildQueryForItemRange(): void { + $query = $this->newFacetQueryBuilder()->buildQuery( + $this->newRangeFacetConfig( self::ITEM_PROPERTY ), + new PropertyConstraints( + new NumericPropertyId( self::ITEM_PROPERTY ), + orSelectedValues: [ 'Q100', 'Q200' ] + ) + ); + + $this->assertNull( $query ); + } + +}