MediaWiki master
ChangeTagsCondition.php
Go to the documentation of this file.
1<?php
2
4
6use Psr\Log\LoggerInterface;
7use stdClass;
9
11 private float|int $denseRcSizeThreshold = 10000;
12
13 private ?int $limit = null;
14 private bool $densityThresholdReached = true;
15
16 public function __construct(
17 private ChangeTagsStore $changeTagsStore,
18 private TableStatsProvider $rcStats,
19 private LoggerInterface $logger,
20 private bool $miserMode,
21 ) {
22 }
23
29 public function setLimit( int $limit ) {
30 $this->limit = $limit;
31 }
32
37 public function setDensityThresholdReached( bool $reached ) {
38 $this->densityThresholdReached = $reached;
39 }
40
47 public function setDenseRcSizeThreshold( $threshold ) {
48 $this->denseRcSizeThreshold = $threshold;
49 }
50
52 public function validateValue( $value ) {
53 if ( !is_scalar( $value ) ) {
54 throw new \InvalidArgumentException( "Change tag must be string-like" );
55 }
56 return (string)$value;
57 }
58
60 public function evaluate( stdClass $row, $value ): bool {
61 if ( !$row->ts_tags ) {
62 return false;
63 }
64 return in_array( $value, explode( ',', $row->ts_tags ), true );
65 }
66
68 protected function prepareCapture( IReadableDatabase $dbr, QueryBackend $query ) {
69 $query->fields( [
70 'ts_tags' => $this->changeTagsStore->makeTagSummarySubquery( 'recentchanges' )
71 ] );
72 }
73
75 protected function prepareConds( IReadableDatabase $dbr, QueryBackend $query ) {
76 [ $required, $excluded ] = $this->getUniqueValues();
77 if ( $required === [] ) {
78 $query->forceEmptySet();
79 } elseif ( $required ) {
80 $ids = array_values( $this->changeTagsStore->getTagIdsFromNames( $required ) );
81 if ( !$ids ) {
82 // All tags were invalid, return nothing
83 $query->forceEmptySet();
84 return;
85 }
86
87 $join = $query->joinForConds( 'change_tag' );
88
89 // Workaround for T298225: MySQL's lack of awareness of LIMIT when
90 // choosing the join order.
91 if ( $this->isDenseTagFilter( $dbr, $ids ) ) {
92 $join->straight();
93 } else {
94 $join->reorderable();
95 }
96
97 $query->where( $dbr->expr( 'changetagdisplay.ct_tag_id', '=', $ids ) );
98 if ( count( $ids ) > 1 ) {
99 $query->distinct();
100 }
101 } elseif ( $excluded ) {
102 $ids = array_values( $this->changeTagsStore->getTagIdsFromNames( $excluded ) );
103 if ( !$ids ) {
104 // No valid tags were excluded
105 return;
106 }
107 $query->joinForConds( 'change_tag' )->left()
108 ->on( $dbr->expr( 'changetagdisplay.ct_tag_id', '=', $ids ) );
109 $query->where( $dbr->expr( 'changetagdisplay.ct_tag_id', '=', null ) );
110 }
111 }
112
124 protected function isDenseTagFilter( IReadableDatabase $dbr, array $tagIds ) {
125 if ( !$tagIds
126 // Only on RecentChanges or similar
127 || !$this->densityThresholdReached
128 // Need a limit
129 || !$this->limit
130 // This is a MySQL-specific hack
131 || $dbr->getType() !== 'mysql'
132 // Unnecessary for small wikis
133 || !$this->miserMode
134 ) {
135 return false;
136 }
137
138 $rcSize = $this->rcStats->getIdDelta();
139 if ( $rcSize < $this->denseRcSizeThreshold ) {
140 // RC is too small to worry about
141 return false;
142 }
143 $tagCount = $dbr->newSelectQueryBuilder()
144 ->table( 'change_tag' )
145 ->where( [
146 $dbr->expr( 'ct_rc_id', '>=', $this->rcStats->getMinId() ),
147 'ct_tag_id' => $tagIds
148 ] )
149 ->caller( __METHOD__ )
150 ->estimateRowCount();
151
152 // If we scan recentchanges first, the number of rows examined will be
153 // approximately the limit divided by the proportion of tagged rows,
154 // i.e. $limit / ( $tagCount / $rcSize ). If that's less than $tagCount,
155 // use a straight join. The inequality below is rearranged for
156 // simplicity and to avoid division by zero.
157 $isDense = $this->limit * $rcSize < $tagCount * $tagCount;
158
159 $this->logger->debug( __METHOD__ .
160 ": rcSize = $rcSize, tagCount = $tagCount, limit = {$this->limit} => " .
161 ( $isDense ? 'dense' : 'sparse' ) );
162 return $isDense;
163 }
164}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
Read-write access to the change_tags table.
__construct(private ChangeTagsStore $changeTagsStore, private TableStatsProvider $rcStats, private LoggerInterface $logger, private bool $miserMode,)
prepareConds(IReadableDatabase $dbr, QueryBackend $query)
Add conditions to the query according to the values passed to require() and exclude()....
validateValue( $value)
Validate a value and return its normalized form.mixed
setDenseRcSizeThreshold( $threshold)
Set the minimum size of the recentchanges table at which change tag queries will be conditionally mod...
setLimit(int $limit)
Set the query limit to be used for density heuristics.
evaluate(stdClass $row, $value)
Evaluate the filter condition against a row, determining whether it is true or false....
isDenseTagFilter(IReadableDatabase $dbr, array $tagIds)
Determine whether a tag filter matches a high proportion of the rows in recentchanges.
prepareCapture(IReadableDatabase $dbr, QueryBackend $query)
Cache and provide min/max ID and "size" (ID delta) of a table.
The narrow interface passed to filter modules.
forceEmptySet()
Set a flag forcing the query to return no rows when it is executed.
fields( $fields)
Add fields to the query.
where(IExpression $expr)
Add a condition to the query.
joinForConds(string $table)
Join on the specified table and declare that it will be used to provide fields for the WHERE clause.
distinct()
Flag that the joins will inadvertently duplicate recentchanges rows and that the query will have to d...
A database connection without write operations.
getType()
Get the RDBMS type of the server (e.g.
newSelectQueryBuilder()
Create an empty SelectQueryBuilder which can be used to run queries against this connection.
expr(string $field, string $op, $value)
See Expression::__construct()