Completed
Push — master ( 3184dc...186529 )
by mw
36:28
created

src/SQLStore/QueryEngine/QueryEngine.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace SMW\SQLStore\QueryEngine;
4
5
use RuntimeException;
6
use SMW\DIWikiPage;
7
use SMW\InvalidPredefinedPropertyException;
8
use SMW\Query\DebugOutputFormatter as QueryDebugOutputFormatter;
9
use SMW\Query\Language\Conjunction;
10
use SMW\Query\Language\SomeProperty;
11
use SMW\Query\Language\ThingDescription;
12
use SMWDataItem as DataItem;
13
use SMWPropertyValue as PropertyValue;
14
use SMWQuery as Query;
15
use SMWQueryResult as QueryResult;
16
use SMWSql3SmwIds;
17
use SMWSQLStore3 as SQLStore;
18
use SMW\QueryEngine as QueryEngineInterface;
19
20
/**
21
 * Class that implements query answering for SQLStore.
22
 *
23
 * @license GNU GPL v2+
24
 * @since 2.2
25
 *
26
 * @author Markus Krötzsch
27
 * @author Jeroen De Dauw
28
 * @author mwjames
29
 */
30
class QueryEngine implements QueryEngineInterface {
31
32
	/**
33
	 * @var SQLStore
34
	 */
35
	private $store;
36
37
	/**
38
	 * Query mode copied from given query. Some submethods act differently when
39
	 * in Query::MODE_DEBUG.
40
	 *
41
	 * @var int
42
	 */
43
	private $queryMode;
44
45
	/**
46
	 * Array of generated QuerySegment query descriptions (index => object)
47
	 *
48
	 * @var QuerySegment[]
49
	 */
50
	private $querySegmentList = array();
51
52
	/**
53
	 * Array of sorting requests ("Property_name" => "ASC"/"DESC"). Used during
54
	 * query processing (where these property names are searched while compiling
55
	 * the query conditions).
56
	 *
57
	 * @var string[]
58
	 */
59
	private $sortKeys;
60
61
	/**
62
	 * Local collection of error strings, passed on to callers if possible.
63
	 *
64
	 * @var string[]
65
	 */
66
	private $errors = array();
67
68
	/**
69
	 * @var QuerySegmentListBuilder
70
	 */
71
	private $querySegmentListBuilder = null;
72
73
	/**
74
	 * @var QuerySegmentListProcessor
75
	 */
76
	private $querySegmentListProcessor = null;
77
78
	/**
79
	 * @var EngineOptions
80
	 */
81
	private $engineOptions = null;
82
83
	/**
84
	 * @since 2.2
85
	 *
86
	 * @param SQLStore $parentStore
87
	 * @param QuerySegmentListBuilder $querySegmentListBuilder
88
	 * @param QuerySegmentListProcessor $querySegmentListProcessor
89
	 * @param EngineOptions $engineOptions
90
	 */
91 178
	public function __construct( SQLStore $parentStore, QuerySegmentListBuilder $querySegmentListBuilder, QuerySegmentListProcessor $querySegmentListProcessor, EngineOptions $engineOptions ) {
92 178
		$this->store = $parentStore;
93 178
		$this->querySegmentListBuilder = $querySegmentListBuilder;
94 178
		$this->querySegmentListProcessor = $querySegmentListProcessor;
95 178
		$this->engineOptions = $engineOptions;
96 178
	}
97
98
	/**
99
	 * @since 2.2
100
	 *
101
	 * @return QuerySegmentListBuilder
102
	 */
103 5
	public function getQuerySegmentListBuilder() {
104 5
		return $this->querySegmentListBuilder;
105
	}
106
107
	/**
108
	 * @since 2.2
109
	 *
110
	 * @return QuerySegmentListProcessor
111
	 */
112 5
	public function getQuerySegmentListProcessor() {
113 5
		return $this->querySegmentListProcessor;
114
	}
115
116
	/**
117
	 * The new SQL store's implementation of query answering. This function
118
	 * works in two stages: First, the nested conditions of the given query
119
	 * object are preprocessed to compute an abstract representation of the
120
	 * SQL query that is to be executed. Since query conditions correspond to
121
	 * joins with property tables in most cases, this abstract representation
122
	 * is essentially graph-like description of how property tables are joined.
123
	 * Moreover, this graph is tree-shaped, since all query conditions are
124
	 * tree-shaped. Each part of this abstract query structure is represented
125
	 * by an QuerySegment object in the array m_queries.
126
	 *
127
	 * As a second stage of processing, the thus prepared SQL query is actually
128
	 * executed. Typically, this means that the joins are collapsed into one
129
	 * SQL query to retrieve results. In some cases, such as in dbug mode, the
130
	 * execution might be restricted and not actually perform the whole query.
131
	 *
132
	 * The two-stage process helps to separate tasks, and it also allows for
133
	 * better optimisations: it is left to the execution engine how exactly the
134
	 * query result is to be obtained. For example, one could pre-compute
135
	 * partial suib-results in temporary tables (or even cache them somewhere),
136
	 * instead of passing one large join query to the DB (of course, it might
137
	 * be large only if the configuration of SMW allows it). For some DBMS, a
138
	 * step-wise execution of the query might lead to better performance, since
139
	 * it exploits the tree-structure of the joins, which is important for fast
140
	 * processing -- not all DBMS might be able in seeing this by themselves.
141
	 *
142
	 * @param Query $query
143
	 *
144
	 * @return mixed depends on $query->querymode
145
	 */
146 171
	public function getQueryResult( Query $query ) {
147
148 171
		if ( ( !$this->engineOptions->get( 'smwgIgnoreQueryErrors' ) || $query->getDescription() instanceof ThingDescription ) &&
149 171
		     $query->querymode != Query::MODE_DEBUG &&
150 171
		     count( $query->getErrors() ) > 0 ) {
151 3
			return new QueryResult( $query->getDescription()->getPrintrequests(), $query, array(), $this->store, false );
152
			// NOTE: we check this here to prevent unnecessary work, but we check
153
			// it after query processing below again in case more errors occurred.
154 169
		} elseif ( $query->querymode == Query::MODE_NONE || $query->getLimit() < 1 ) {
155
			// don't query, but return something to printer
156 9
			return new QueryResult( $query->getDescription()->getPrintrequests(), $query, array(), $this->store, true );
157
		}
158
159 164
		$db = $this->store->getConnection( 'mw.db.queryengine' );
160
161 164
		$this->queryMode = $query->querymode;
162 164
		$this->querySegmentList = array();
163
164 164
		$this->errors = array();
165 164
		QuerySegment::$qnum = 0;
166 164
		$this->sortKeys = $query->sortkeys;
167
168
		// Anchor IT_TABLE as root element
169 164
		$rootSegmentNumber = QuerySegment::$qnum;
170 164
		$rootSegment = new QuerySegment();
171 164
		$rootSegment->joinTable = SMWSql3SmwIds::TABLE_NAME;
172 164
		$rootSegment->joinfield = "$rootSegment->alias.smw_id";
173
174 164
		$this->querySegmentListBuilder->addQuerySegment( $rootSegment );
175
176
		// *** First compute abstract representation of the query (compilation) ***//
177 164
		$this->querySegmentListBuilder->setSortKeys( $this->sortKeys );
178 164
		$this->querySegmentListBuilder->getQuerySegmentFrom( $query->getDescription() ); // compile query, build query "plan"
0 ignored issues
show
It seems like $query->getDescription() targeting SMWQuery::getDescription() can also be of type null; however, SMW\SQLStore\QueryEngine...::getQuerySegmentFrom() does only seem to accept object<SMW\Query\Language\Description>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
179
180 164
		$qid = $this->querySegmentListBuilder->getLastQuerySegmentId();
181 164
		$this->querySegmentList = $this->querySegmentListBuilder->getQuerySegmentList();
182 164
		$this->errors = $this->querySegmentListBuilder->getErrors();
183
184 164
		if ( $qid < 0 ) { // no valid/supported condition; ensure that at least only proper pages are delivered
185
			$qid = $rootSegmentNumber;
186
			$q = $this->querySegmentList[$rootSegmentNumber];
187
			$q->where = "$q->alias.smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWIW_OUTDATED ) . " AND $q->alias.smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWREDIIW ) . " AND $q->alias.smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWBORDERIW ) . " AND $q->alias.smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWINTDEFIW );
188
			$this->querySegmentList[$rootSegmentNumber] = $q;
189
		}
190
191 164
		if ( isset( $this->querySegmentList[$qid]->joinTable ) && $this->querySegmentList[$qid]->joinTable != SMWSql3SmwIds::TABLE_NAME ) {
192
			// manually make final root query (to retrieve namespace,title):
193 161
			$rootid = $rootSegmentNumber;
194 161
			$qobj = $this->querySegmentList[$rootSegmentNumber];
195 161
			$qobj->components = array( $qid => "$qobj->alias.smw_id" );
196 161
			$qobj->sortfields = $this->querySegmentList[$qid]->sortfields;
197 161
			$this->querySegmentList[$rootSegmentNumber] = $qobj;
198
		} else { // not such a common case, but worth avoiding the additional inner join:
199 7
			$rootid = $qid;
200
		}
201
202
		// var_dump( json_encode( $this->querySegmentList, JSON_PRETTY_PRINT ) );
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
203
		// Include order conditions (may extend query if needed for sorting):
204 164
		if ( $this->engineOptions->get( 'smwgQSortingSupport' ) ) {
205 163
			$this->applyOrderConditions( $rootid );
206
		}
207
208
		// Possibly stop if new errors happened:
209 164
		if ( !$this->engineOptions->get( 'smwgIgnoreQueryErrors' ) &&
210 164
				$query->querymode != Query::MODE_DEBUG &&
211 164
				count( $this->errors ) > 0 ) {
212
			$query->addErrors( $this->errors );
213
			return new QueryResult( $query->getDescription()->getPrintrequests(), $query, array(), $this->store, false );
214
		}
215
216
		// *** Now execute the computed query ***//
217 164
		$this->querySegmentListProcessor->setQueryMode( $this->queryMode );
218 164
		$this->querySegmentListProcessor->setQuerySegmentList( $this->querySegmentList );
219
220
		// execute query tree, resolve all dependencies
221 164
		$this->querySegmentListProcessor->doResolveQueryDependenciesById( $rootid );
222
223 164
		$this->applyExtraWhereCondition( $rootid );
224
225 164
		switch ( $query->querymode ) {
226 164
			case Query::MODE_DEBUG:
227 2
				$result = $this->getDebugQueryResult( $query, $rootid );
228 2
			break;
229 162
			case Query::MODE_COUNT:
230 2
				$result = $this->getCountQueryResult( $query, $rootid );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getCountQueryResult($query, $rootid); of type integer|SMWQueryResult adds the type integer to the return on line 240 which is incompatible with the return type declared by the interface SMW\QueryEngine::getQueryResult of type SMWQueryResult|string.
Loading history...
231 2
			break;
232
			default:
233 162
				$result = $this->getInstanceQueryResult( $query, $rootid );
234 162
			break;
235
		}
236
237 164
		$this->querySegmentListProcessor->cleanUp();
238 164
		$query->addErrors( $this->errors );
239
240 164
		return $result;
241
	}
242
243
	/**
244
	 * Using a preprocessed internal query description referenced by $rootid, compute
245
	 * the proper debug output for the given query.
246
	 *
247
	 * @param Query $query
248
	 * @param integer $rootid
249
	 *
250
	 * @return string
251
	 */
252 2
	private function getDebugQueryResult( Query $query, $rootid ) {
253
254 2
		$qobj = $this->querySegmentList[$rootid];
255 2
		$entries = array();
256
257 2
		$sqlOptions = $this->getSQLOptions( $query, $rootid );
258
259 2
		$entries['SQL Query'] = '';
260 2
		$entries['SQL Explain'] = '';
261
262 2
		$this->doExecuteDebugQueryResult( $qobj, $sqlOptions, $entries );
263 2
		$auxtables = '';
264
265 2
		foreach ( $this->querySegmentListProcessor->getListOfResolvedQueries() as $table => $log ) {
266
			$auxtables .= "<li>Temporary table $table";
267
			foreach ( $log as $q ) {
268
				$auxtables .= "<br />&#160;&#160;<tt>$q</tt>";
269
			}
270
			$auxtables .= '</li>';
271
		}
272
273 2
		if ( $auxtables ) {
274
			$entries['Auxilliary Tables'] = "<ul>$auxtables</ul>";
275
		} else {
276 2
			$entries['Auxilliary Tables'] = 'No auxilliary tables used.';
277
		}
278
279 2
		return QueryDebugOutputFormatter::getStringFrom( 'SQLStore', $entries, $query );
280
	}
281
282 2
	private function doExecuteDebugQueryResult( $qobj, $sqlOptions, &$entries ) {
283
284 2
		if ( !isset( $qobj->joinfield ) || $qobj->joinfield === '' ) {
285 1
			return $entries['SQL Query'] = 'Empty result, no SQL query created.';
286
		}
287
288 1
		$db = $this->store->getConnection( 'mw.db.queryengine' );
289 1
		list( $startOpts, $useIndex, $tailOpts ) = $db->makeSelectOptions( $sqlOptions );
290
291
		$sql = "SELECT DISTINCT ".
292 1
			"$qobj->alias.smw_id AS id," .
293 1
			"$qobj->alias.smw_title AS t," .
294 1
			"$qobj->alias.smw_namespace AS ns," .
295 1
			"$qobj->alias.smw_iw AS iw," .
296 1
			"$qobj->alias.smw_subobject AS so," .
297 1
			"$qobj->alias.smw_sortkey AS sortkey " .
298 1
			"FROM " .
299 1
			$db->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from .
300 1
			( $qobj->where === '' ? '':' WHERE ' ) . $qobj->where . "$tailOpts $startOpts $useIndex ".
301 1
			"LIMIT " . $sqlOptions['LIMIT'] . ' ' .
302 1
			"OFFSET " . $sqlOptions['OFFSET'];
303
304 1
		$res = $db->query(
305 1
			'EXPLAIN '. $sql,
306 1
			__METHOD__
307
		);
308
309 1
		$entries['SQL Explain'] = QueryDebugOutputFormatter::doFormatSQLExplainOutput( $db->getType(), $res );
310 1
		$entries['SQL Query'] = QueryDebugOutputFormatter::doFormatSQLStatement( $sql, $qobj->alias );
311
312 1
		$db->freeResult( $res );
313 1
	}
314
315
	/**
316
	 * Using a preprocessed internal query description referenced by $rootid, compute
317
	 * the proper counting output for the given query.
318
	 *
319
	 * @param Query $query
320
	 * @param integer $rootid
321
	 *
322
	 * @return integer
323
	 */
324 2
	private function getCountQueryResult( Query $query, $rootid ) {
325
326 2
		$queryResult = new QueryResult(
327 2
			$query->getDescription()->getPrintrequests(),
328
			$query,
329 2
			array(),
330 2
			$this->store,
331 2
			false
332
		);
333
334 2
		$queryResult->setCountValue( 0 );
335
336 2
		$qobj = $this->querySegmentList[$rootid];
337
338 2
		if ( $qobj->joinfield === '' ) { // empty result, no query needed
339
			return 0;
340
		}
341
342 2
		$db = $this->store->getConnection( 'mw.db.queryengine' );
343
344 2
		$sql_options = array( 'LIMIT' => $query->getLimit() + 1, 'OFFSET' => $query->getOffset() );
345
346 2
		$res = $db->select(
347 2
			$db->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from,
348 2
			"COUNT(DISTINCT $qobj->alias.smw_id) AS count",
349 2
			$qobj->where,
350 2
			__METHOD__,
351
			$sql_options
352
		);
353
354 2
		$row = $db->fetchObject( $res );
355
356 2
		$count = $row->count;
357 2
		$db->freeResult( $res );
358
359 2
		$queryResult->setCountValue( $count );
360
361 2
		return $queryResult;
362
	}
363
364
	/**
365
	 * Using a preprocessed internal query description referenced by $rootid,
366
	 * compute the proper result instance output for the given query.
367
	 * @todo The SQL standard requires us to select all fields by which we sort, leading
368
	 * to wrong results regarding the given limit: the user expects limit to be applied to
369
	 * the number of distinct pages, but we can use DISTINCT only to whole rows. Thus, if
370
	 * rows contain sortfields, then pages with multiple values for that field are distinct
371
	 * and appear multiple times in the result. Filtering duplicates in post processing
372
	 * would still allow such duplicates to push aside wanted values, leading to less than
373
	 * "limit" results although there would have been "limit" really distinct results. For
374
	 * this reason, we select sortfields only for POSTGRES. MySQL is able to perform what
375
	 * we want here. It would be nice if we could eliminate the bug in POSTGRES as well.
376
	 *
377
	 * @param Query $query
378
	 * @param integer $rootid
379
	 *
380
	 * @return QueryResult
381
	 */
382 162
	private function getInstanceQueryResult( Query $query, $rootid ) {
383
384 162
		$db = $this->store->getConnection( 'mw.db.queryengine' );
385 162
		$dbType = $db->getType();
386
387 162
		$qobj = $this->querySegmentList[$rootid];
388
389 162
		if ( $qobj->joinfield === '' ) { // empty result, no query needed
390 5
			$result = new QueryResult( $query->getDescription()->getPrintrequests(), $query, array(), $this->store, false );
391 5
			return $result;
392
		}
393
394 161
		$sql_options = $this->getSQLOptions( $query, $rootid );
395
396
		// Selecting those is required in standard SQL (but MySQL does not require it).
397 161
		$sortfields = implode( $qobj->sortfields, ',' );
398
399 161
		$res = $db->select(
400 161
			$db->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from,
401 161
			"DISTINCT $qobj->alias.smw_id AS id,$qobj->alias.smw_title AS t,$qobj->alias.smw_namespace AS ns,$qobj->alias.smw_iw AS iw,$qobj->alias.smw_subobject AS so,$qobj->alias.smw_sortkey AS sortkey" .
402 161
			  ( $dbType == 'postgres' ? ( ( $sortfields ? ',' : '' ) . $sortfields ) : '' ),
403 161
			$qobj->where,
404 161
			__METHOD__,
405
			$sql_options
406
		);
407
408 161
		$qr = array();
409
410 161
		$count = 0; // the number of fetched results ( != number of valid results in array $qr)
411 161
		$missedCount = 0;
412 161
		$dataItemCache = array();
413 161
		$logToTable = array();
414 161
		$hasFurtherResults = false;
415
416 161
		$prs = $query->getDescription()->getPrintrequests();
417
418 161
		$diHandler = $this->store->getDataItemHandlerForDIType( DataItem::TYPE_WIKIPAGE );
419
420 161
		while ( ( $count < $query->getLimit() ) && ( $row = $db->fetchObject( $res ) ) ) {
421 156
			if ( $row->iw === '' || $row->iw{0} != ':' )  {
422
423
				// Catch exception for non-existing predefined properties that
424
				// still registered within non-updated pages (@see bug 48711)
425
				try {
426 156
					$dataItem = $diHandler->dataItemFromDBKeys( array(
427 156
						$row->t,
428 156
						intval( $row->ns ),
429 156
						$row->iw,
430 156
						'',
431 156
						$row->so
432
					) );
433
				} catch ( InvalidPredefinedPropertyException $e ) {
434
					$logToTable[$row->t] = "issue creating a {$row->t} dataitem from a database row";
435
					wfDebugLog( 'smw', __METHOD__ . ' ' . $e->getMessage() . "\n" );
436
					$dataItem = '';
437
				}
438
439 156
				if ( $dataItem instanceof DIWikiPage && !isset( $dataItemCache[$dataItem->getHash()] ) ) {
440 156
					$count++;
441 156
					$dataItemCache[$dataItem->getHash()] = true;
442 156
					$qr[] = $dataItem;
443
					// These IDs are usually needed for displaying the page (esp. if more property values are displayed):
444 156
					$this->store->smwIds->setCache( $row->t, $row->ns, $row->iw, $row->so, $row->id, $row->sortkey );
445
				} else {
446
					$missedCount++;
447 156
					$logToTable[$row->t] = "skip result for {$row->t} existing cache entry / query " . $query->getHash();
448
				}
449
			} else {
450
				$missedCount++;
451
				$logToTable[$row->t] = "skip result for {$row->t} due to an internal `{$row->iw}` pointer / query " . $query->getHash();
452
			}
453
		}
454
455 161
		if ( $db->fetchObject( $res ) ) {
456 5
			$count++;
457
		}
458
459 161
		if ( $logToTable !== array() ) {
460
			wfDebugLog( 'smw', __METHOD__ . ' ' . implode( ',', $logToTable ) . "\n" );
461
		}
462
463 161
		if ( $count > $query->getLimit() || ( $count + $missedCount ) > $query->getLimit() ) {
464 5
			$hasFurtherResults = true;
465
		};
466
467 161
		$db->freeResult( $res );
468 161
		$result = new QueryResult( $prs, $query, $qr, $this->store, $hasFurtherResults );
469
470 161
		return $result;
471
	}
472
473
	/**
474
	 * This function modifies the given query object at $qid to account for all ordering conditions
475
	 * in the Query $query. It is always required that $qid is the id of a query that joins with
476
	 * SMW IDs table so that the field alias.smw_title is $available for default sorting.
477
	 *
478
	 * @param integer $qid
479
	 */
480 163
	private function applyOrderConditions( $qid ) {
481 163
		$qobj = $this->querySegmentList[$qid];
482
483 163
		$extraProperties = $this->collectedRequiredExtraPropertyDescriptions( $qobj );
484
485 163
		if ( count( $extraProperties ) > 0 ) {
486 5
			$this->compileAccordingConditionsAndHackThemIntoQobj( $extraProperties, $qobj, $qid );
487
		}
488 163
	}
489
490 164
	private function applyExtraWhereCondition( $qid ) {
491
492 164
		$db = $this->store->getConnection( 'mw.db.queryengine' );
493
494 164
		if ( !isset( $this->querySegmentList[$qid] ) ) {
495 1
			return null;
496
		}
497
498 163
		$qobj = $this->querySegmentList[$qid];
499
500
		// Filter elements that should never appear in a result set
501
		$extraWhereCondition = array(
502 163
			'del'  => "$qobj->alias.smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWIW_OUTDATED ) . " AND $qobj->alias.smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWDELETEIW ),
503 163
			'redi' => "$qobj->alias.smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWREDIIW )
504
		);
505
506 163
		if ( strpos( $qobj->where, SMW_SQL3_SMWIW_OUTDATED ) === false ) {
507 163
			$qobj->where .= $qobj->where === '' ? $extraWhereCondition['del'] : " AND " . $extraWhereCondition['del'];
508
		}
509
510 163
		if ( strpos( $qobj->where, SMW_SQL3_SMWREDIIW ) === false ) {
511 163
			$qobj->where .= $qobj->where === '' ? $extraWhereCondition['redi'] : " AND " . $extraWhereCondition['redi'];
512
		}
513
514 163
		$this->querySegmentList[$qid] = $qobj;
515 163
	}
516
517 163
	private function collectedRequiredExtraPropertyDescriptions( $qobj ) {
518 163
		$extraProperties = array();
519
520 163
		foreach ( $this->sortKeys as $propkey => $order ) {
521
522 89
			if ( !is_string( $propkey ) ) {
523
				throw new RuntimeException( "Expected a string value as sortkey" );
524
			}
525
526 89
			if ( !array_key_exists( $propkey, $qobj->sortfields ) ) { // Find missing property to sort by.
527 81
				if ( $propkey === '' ) { // Sort by first result column (page titles).
528 77
					$qobj->sortfields[$propkey] = "$qobj->alias.smw_sortkey";
529 7
				} elseif ( $propkey === '#' ) { // Sort by first result column (page titles).
530
					// PHP7 showed a rather erratic behaviour where in cases
531
					// the sortkey contains the same string for comparison, the
532
					// result returned from the DB was mixed in order therefore
533
					// using # as indicator to search for additional fields if
534
					// no specific property is given (see test cases in #1534)
535 2
					$qobj->sortfields[$propkey] = "$qobj->alias.smw_sortkey,$qobj->alias.smw_title,$qobj->alias.smw_subobject";
536
				} else { // Try to extend query.
537 5
					$sortprop = PropertyValue::makeUserProperty( $propkey );
538
539 5
					if ( $sortprop->isValid() ) {
540 89
						$extraProperties[] = new SomeProperty( $sortprop->getDataItem(), new ThingDescription() );
0 ignored issues
show
$sortprop->getDataItem() of type object<SMWDataItem> is not a sub-type of object<SMW\DIProperty>. It seems like you assume a child class of the class SMWDataItem to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
541
					}
542
				}
543
			}
544
		}
545
546 163
		return $extraProperties;
547
	}
548
549 5
	private function compileAccordingConditionsAndHackThemIntoQobj( array $extraProperties, $qobj, $qid ) {
550 5
		$this->querySegmentListBuilder->setSortKeys( $this->sortKeys );
551 5
		$this->querySegmentListBuilder->getQuerySegmentFrom( new Conjunction( $extraProperties ) );
552
553 5
		$newQuerySegmentId = $this->querySegmentListBuilder->getLastQuerySegmentId();
554 5
		$this->querySegmentList = $this->querySegmentListBuilder->getQuerySegmentList();
555 5
		$this->errors = $this->querySegmentListBuilder->getErrors();
556
557 5
		$newQuerySegment = $this->querySegmentList[$newQuerySegmentId]; // This is always an QuerySegment::Q_CONJUNCTION ...
558
559 5
		foreach ( $newQuerySegment->components as $cid => $field ) { // ... so just re-wire its dependencies
560 5
			$qobj->components[$cid] = $qobj->joinfield;
561 5
			$qobj->sortfields = array_merge( $qobj->sortfields, $this->querySegmentList[$cid]->sortfields );
562
		}
563
564 5
		$this->querySegmentList[$qid] = $qobj;
565 5
	}
566
567
	/**
568
	 * Get a SQL option array for the given query and preprocessed query object at given id.
569
	 *
570
	 * @param Query $query
571
	 * @param integer $rootId
572
	 *
573
	 * @return array
574
	 */
575 163
	private function getSQLOptions( Query $query, $rootId ) {
576
577 163
		$result = array( 'LIMIT' => $query->getLimit() + 5, 'OFFSET' => $query->getOffset() );
578
579
		// Build ORDER BY options using discovered sorting fields.
580 163
		if ( $this->engineOptions->get( 'smwgQSortingSupport' ) ) {
581 162
			$qobj = $this->querySegmentList[$rootId];
582 162
			$type = $this->store->getConnection( 'mw.db.queryengine' )->getType();
583
584 162
			foreach ( $this->sortKeys as $propkey => $order ) {
585
586 89
				if ( !is_string( $propkey ) ) {
587
					throw new RuntimeException( "Expected a string value as sortkey" );
588
				}
589
590
				// #835
591
				// SELECT DISTINCT and ORDER BY RANDOM causes an issue for postgres
592
				// Disable RANDOM support for postgres
593 89
				if ( $type === 'postgres' ) {
594
					$this->engineOptions->set( 'smwgQRandSortingSupport', false );
595
				}
596
597 89
				if ( ( $order != 'RANDOM' ) && array_key_exists( $propkey, $qobj->sortfields ) ) { // Field was successfully added.
598 88
					$result['ORDER BY'] = ( array_key_exists( 'ORDER BY', $result ) ? $result['ORDER BY'] . ', ' : '' ) . $qobj->sortfields[$propkey] . " $order ";
599 1
				} elseif ( ( $order == 'RANDOM' ) && $this->engineOptions->get( 'smwgQRandSortingSupport' ) ) {
600 89
					$result['ORDER BY'] = ( array_key_exists( 'ORDER BY', $result ) ? $result['ORDER BY'] . ', ' : '' ) . ' RAND() ';
601
				}
602
			}
603
		}
604 163
		return $result;
605
	}
606
607
}
608