Index: branches/querypage-work/phase3/includes/specials/SpecialBrokenRedirects.php |
— | — | @@ -39,9 +39,33 @@ |
40 | 40 | AND p2.page_namespace IS NULL"; |
41 | 41 | return $sql; |
42 | 42 | } |
| 43 | + |
| 44 | + function getQueryInfo() { |
| 45 | + return array( |
| 46 | + 'tables' => array( 'redirect', 'page AS p1', 'page AS p2' ), |
| 47 | + 'fields' => array( "'{$this->getName()}' AS type", |
| 48 | + 'p1.page_namespace AS namespace', |
| 49 | + 'p1.page_title AS title', |
| 50 | + 'rd_namespace', |
| 51 | + 'rd_title' |
| 52 | + ), |
| 53 | + 'conds' => array( 'rd_namespace >= 0', |
| 54 | + 'p2.page_namespace IS NULL' |
| 55 | + ), |
| 56 | + // TODO test this join |
| 57 | + 'join_conds' => array( 'page AS p1' => array( 'LEFT JOIN', array( |
| 58 | + 'rd_from=p1.page_id', |
| 59 | + ) ), |
| 60 | + 'page AS p2' => array( 'LEFT JOIN', array( |
| 61 | + 'rd_namespace=p2.page_namespace', |
| 62 | + 'rd_title=p2.page_title' |
| 63 | + ) ) |
| 64 | + ) |
| 65 | + ); |
| 66 | + } |
43 | 67 | |
44 | 68 | function getOrder() { |
45 | | - return ''; |
| 69 | + return array (); |
46 | 70 | } |
47 | 71 | |
48 | 72 | function formatResult( $skin, $result ) { |
Index: branches/querypage-work/phase3/includes/specials/SpecialAncientpages.php |
— | — | @@ -20,21 +20,23 @@ |
21 | 21 | |
22 | 22 | function isSyndicated() { return false; } |
23 | 23 | |
24 | | - function getSQL() { |
| 24 | + function getQueryInfo() { |
| 25 | + // FIXME convert timestamps elsewhere |
| 26 | + // Possibly add bool returnsTimestamps() |
| 27 | + // FIXME standardize 'name' AS type ? |
25 | 28 | global $wgDBtype; |
26 | | - $db = wfGetDB( DB_SLAVE ); |
27 | | - $page = $db->tableName( 'page' ); |
28 | | - $revision = $db->tableName( 'revision' ); |
29 | 29 | $epoch = $wgDBtype == 'mysql' ? 'UNIX_TIMESTAMP(rev_timestamp)' : |
30 | | - 'EXTRACT(epoch FROM rev_timestamp)'; |
31 | | - return |
32 | | - "SELECT 'Ancientpages' as type, |
33 | | - page_namespace as namespace, |
34 | | - page_title as title, |
35 | | - $epoch as value |
36 | | - FROM $page, $revision |
37 | | - WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0 |
38 | | - AND page_latest=rev_id"; |
| 30 | + 'EXTRACT(epoch FROM rev_timestamp)'; |
| 31 | + return array( |
| 32 | + 'tables' => array( 'page', 'revision' ), |
| 33 | + 'fields' => array( "'{$this->getName()}' AS type", |
| 34 | + 'page_namespace AS namespace', |
| 35 | + 'page_title AS title', |
| 36 | + "$epoch AS value" ), |
| 37 | + 'conds' => array( 'page_namespace' => NS_MAIN, |
| 38 | + 'page_is_redirect' => 0, |
| 39 | + 'page_latest=rev_id' ) |
| 40 | + ); |
39 | 41 | } |
40 | 42 | |
41 | 43 | function sortDescending() { |
Index: branches/querypage-work/phase3/includes/specials/SpecialDeadendpages.php |
— | — | @@ -47,6 +47,24 @@ |
48 | 48 | "AND page_namespace = 0 " . |
49 | 49 | "AND page_is_redirect = 0"; |
50 | 50 | } |
| 51 | + |
| 52 | + function getQueryInfo() { |
| 53 | + return array( |
| 54 | + 'tables' => array( 'page', 'pagelinks' ), |
| 55 | + 'fields' => array( "'{$this->getName()} AS type", |
| 56 | + 'page_namespace AS namespace', |
| 57 | + 'page_title AS title', |
| 58 | + 'page_title AS value' |
| 59 | + ), |
| 60 | + 'conds' => array( 'pl_from IS NULL', |
| 61 | + 'page_namespace' => NS_MAIN, |
| 62 | + 'page_is_redirect' => 0 |
| 63 | + ), |
| 64 | + 'join_conds' => array( 'pagelinks' => array( 'LEFT JOIN', array( |
| 65 | + 'page_id=pl_from' |
| 66 | + ) ) ) |
| 67 | + ); |
| 68 | + } |
51 | 69 | } |
52 | 70 | |
53 | 71 | /** |
Index: branches/querypage-work/phase3/includes/QueryPage.php |
— | — | @@ -60,7 +60,7 @@ |
61 | 61 | * subclasses derive from it. |
62 | 62 | * @ingroup SpecialPage |
63 | 63 | */ |
64 | | -class QueryPage { |
| 64 | +abstract class QueryPage { |
65 | 65 | /** |
66 | 66 | * Whether or not we want plain listoutput rather than an ordered list |
67 | 67 | * |
— | — | @@ -104,8 +104,14 @@ |
105 | 105 | } |
106 | 106 | |
107 | 107 | /** |
108 | | - * Subclasses return an SQL query here. |
109 | | - * |
| 108 | + * Subclasses return an SQL query here, formatted as an array with the |
| 109 | + * following keys: |
| 110 | + * tables => Table(s) for passing to Database::select() |
| 111 | + * fields => Field(s) for passing to Database::select(), may be * |
| 112 | + * conds => WHERE conditions |
| 113 | + * options => options |
| 114 | + * join_conds => JOIN conditions |
| 115 | + * |
110 | 116 | * Note that the query itself should return the following four columns: |
111 | 117 | * 'type' (your special page's name), 'namespace', 'title', and 'value' |
112 | 118 | * *in that order*. 'value' is used for sorting. |
— | — | @@ -116,10 +122,19 @@ |
117 | 123 | * an integer; non-numeric values are useful only for sorting the initial |
118 | 124 | * query. |
119 | 125 | * |
120 | | - * Don't include an ORDER or LIMIT clause, this will be added. |
| 126 | + * Don't include an ORDER or LIMIT clause, they will be added |
| 127 | + * @return array |
121 | 128 | */ |
122 | | - function getSQL() { |
123 | | - return "SELECT 'sample' as type, 0 as namespace, 'Sample result' as title, 42 as value"; |
| 129 | + abstract function getQueryInfo(); |
| 130 | + |
| 131 | + /** |
| 132 | + * Subclasses return an array of fields to order by here. Don't append |
| 133 | + * DESC to the field names, that'll be done automatically if |
| 134 | + * sortDescending() returns true |
| 135 | + * @return array |
| 136 | + */ |
| 137 | + function getOrderFields() { |
| 138 | + return array('value'); |
124 | 139 | } |
125 | 140 | |
126 | 141 | /** |
— | — | @@ -129,11 +144,6 @@ |
130 | 145 | return true; |
131 | 146 | } |
132 | 147 | |
133 | | - function getOrder() { |
134 | | - return ' ORDER BY value ' . |
135 | | - ($this->sortDescending() ? 'DESC' : ''); |
136 | | - } |
137 | | - |
138 | 148 | /** |
139 | 149 | * Is this query expensive (for some definition of expensive)? Then we |
140 | 150 | * don't let it run in miser mode. $wgDisableQueryPages causes all query |
— | — | @@ -145,7 +155,7 @@ |
146 | 156 | } |
147 | 157 | |
148 | 158 | /** |
149 | | - * Whether or not the output of the page in question is retrived from |
| 159 | + * Whether or not the output of the page in question is retrieved from |
150 | 160 | * the database cache. |
151 | 161 | * |
152 | 162 | * @return bool |
— | — | @@ -167,16 +177,17 @@ |
168 | 178 | * Formats the results of the query for display. The skin is the current |
169 | 179 | * skin; you can use it for making links. The result is a single row of |
170 | 180 | * result data. You should be able to grab SQL results off of it. |
171 | | - * If the function return "false", the line output will be skipped. |
| 181 | + * If the function returns false, the line output will be skipped. |
| 182 | + * @param $skin Skin |
| 183 | + * @param $result object Result row |
| 184 | + * @return mixed String or false to skip |
172 | 185 | */ |
173 | | - function formatResult( $skin, $result ) { |
174 | | - return ''; |
175 | | - } |
| 186 | + abstract function formatResult( $skin, $result ); |
176 | 187 | |
177 | 188 | /** |
178 | 189 | * The content returned by this function will be output before any result |
179 | 190 | */ |
180 | | - function getPageHeader( ) { |
| 191 | + function getPageHeader() { |
181 | 192 | return ''; |
182 | 193 | } |
183 | 194 | |
— | — | @@ -193,15 +204,19 @@ |
194 | 205 | /** |
195 | 206 | * Some special pages (for example SpecialListusers) might not return the |
196 | 207 | * current object formatted, but return the previous one instead. |
197 | | - * Setting this to return true, will call one more time wfFormatResult to |
198 | | - * be sure that the very last result is formatted and shown. |
| 208 | + * Setting this to return true will ensure formatResult() is called |
| 209 | + * one more time to make sure that the very last result is formatted |
| 210 | + * as well. |
199 | 211 | */ |
200 | | - function tryLastResult( ) { |
| 212 | + function tryLastResult() { |
201 | 213 | return false; |
202 | 214 | } |
203 | 215 | |
204 | 216 | /** |
205 | 217 | * Clear the cache and save new results |
| 218 | + * @param $limit mixed Numerical limit or false for no limit |
| 219 | + * @param $ignoreErrors bool If true, don't die on database errors |
| 220 | + * @return mixed Number of rows cached on success, or false on failure |
206 | 221 | */ |
207 | 222 | function recache( $limit, $ignoreErrors = true ) { |
208 | 223 | $fname = get_class( $this ) . '::recache'; |
— | — | @@ -211,8 +226,6 @@ |
212 | 227 | return false; |
213 | 228 | } |
214 | 229 | |
215 | | - $querycache = $dbr->tableName( 'querycache' ); |
216 | | - |
217 | 230 | if ( $ignoreErrors ) { |
218 | 231 | $ignoreW = $dbw->ignoreErrors( true ); |
219 | 232 | $ignoreR = $dbr->ignoreErrors( true ); |
— | — | @@ -221,10 +234,7 @@ |
222 | 235 | # Clear out any old cached data |
223 | 236 | $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname ); |
224 | 237 | # Do query |
225 | | - $sql = $this->getSQL() . $this->getOrder(); |
226 | | - if ( $limit !== false ) |
227 | | - $sql = $dbr->limitResult( $sql, $limit, 0 ); |
228 | | - $res = $dbr->query( $sql, $fname ); |
| 238 | + $res = $this->reallyDoQuery( $limit, false ); |
229 | 239 | $num = false; |
230 | 240 | if ( $res ) { |
231 | 241 | $num = $dbr->numRows( $res ); |
— | — | @@ -248,7 +258,7 @@ |
249 | 259 | if ( !$dbw->insert( 'querycache', $vals, __METHOD__ ) ) { |
250 | 260 | // Set result to false to indicate error |
251 | 261 | $dbr->freeResult( $res ); |
252 | | - $res = false; |
| 262 | + $num = false; |
253 | 263 | } |
254 | 264 | } |
255 | 265 | if ( $res ) { |
— | — | @@ -266,6 +276,68 @@ |
267 | 277 | } |
268 | 278 | return $num; |
269 | 279 | } |
| 280 | + |
| 281 | + /** |
| 282 | + * Run the query and return the result |
| 283 | + * @param $limit mixed Numerical limit or false for no limit |
| 284 | + * @param $offset mixed Numerical offset or false for no offset |
| 285 | + * @return ResultWrapper |
| 286 | + */ |
| 287 | + function reallyDoQuery( $limit, $offset = false ) { |
| 288 | + $fname = get_class( $this ) . "::reallyDoQuery"; |
| 289 | + $query = $this->getQueryInfo(); |
| 290 | + $order = $this->getOrderFields(); |
| 291 | + if( $this->sortDescending() ) { |
| 292 | + foreach( $order as &$field ) { |
| 293 | + $field .= ' DESC'; |
| 294 | + } |
| 295 | + } |
| 296 | + if( !is_array( $query['options'] ) ) { |
| 297 | + $options = array (); |
| 298 | + } |
| 299 | + if( count( $order ) ) { |
| 300 | + $query['options']['ORDER BY'] = implode( ', ', $order ); |
| 301 | + } |
| 302 | + if( $limit !== false) { |
| 303 | + $query['options']['LIMIT'] = intval( $limit ); |
| 304 | + } |
| 305 | + if( $offset !== false) { |
| 306 | + $query['options']['OFFSET'] = intval( $offset ); |
| 307 | + } |
| 308 | + |
| 309 | + $dbr = wfGetDB( DB_SLAVE ); |
| 310 | + $res = $dbr->select( (array)$query['tables'], |
| 311 | + (array)$query['fields'], |
| 312 | + (array)$query['conds'], $fname, |
| 313 | + $query['options'], (array)$query['join_conds'] |
| 314 | + ); |
| 315 | + return $dbr->resultObject( $res ); |
| 316 | + } |
| 317 | + |
| 318 | + /** |
| 319 | + * Fetch the query results from the query cache |
| 320 | + * @param $limit mixed Numerical limit or false for no limit |
| 321 | + * @param $offset mixed Numerical offset or false for no offset |
| 322 | + * @return ResultWrapper |
| 323 | + */ |
| 324 | + function fetchFromCache( $limit, $offset = false ) { |
| 325 | + $dbr = wfGetDB( DB_SLAVE ); |
| 326 | + $options = array (); |
| 327 | + if( $limit !== false ) { |
| 328 | + $options['LIMIT'] = intval( $limit ); |
| 329 | + } |
| 330 | + if( $offset !== false) { |
| 331 | + $options['OFFSET'] = intval( $offset ); |
| 332 | + } |
| 333 | + $res = $dbr->select( 'querycache', array( 'qc_type', |
| 334 | + 'qc_namespace AS namespace', |
| 335 | + 'qc_title AS title', |
| 336 | + 'qc_value AS value' ), |
| 337 | + array( 'qc_type' => $this->getName() ), |
| 338 | + __METHOD__, $options |
| 339 | + ); |
| 340 | + return $dbr->resultObject( $res ); |
| 341 | + } |
270 | 342 | |
271 | 343 | /** |
272 | 344 | * This is the actual workhorse. It does everything needed to make a |
— | — | @@ -287,16 +359,12 @@ |
288 | 360 | |
289 | 361 | $wgOut->setSyndicated( $this->isSyndicated() ); |
290 | 362 | |
| 363 | + //$res = null; |
291 | 364 | if ( !$this->isCached() ) { |
292 | | - $sql = $this->getSQL(); |
| 365 | + $res = $this->reallyDoQuery( $limit, $offset ); |
293 | 366 | } else { |
294 | 367 | # Get the cached result |
295 | | - $querycache = $dbr->tableName( 'querycache' ); |
296 | | - $type = $dbr->strencode( $sname ); |
297 | | - $sql = |
298 | | - "SELECT qc_type as type, qc_namespace as namespace,qc_title as title, qc_value as value |
299 | | - FROM $querycache WHERE qc_type='$type'"; |
300 | | - |
| 368 | + $res = $this->fetchFromCache( $limit, $offset ); |
301 | 369 | if( !$this->listoutput ) { |
302 | 370 | |
303 | 371 | # Fetch the timestamp of this update |
— | — | @@ -323,9 +391,6 @@ |
324 | 392 | |
325 | 393 | } |
326 | 394 | |
327 | | - $sql .= $this->getOrder(); |
328 | | - $sql = $dbr->limitResult($sql, $limit, $offset); |
329 | | - $res = $dbr->query( $sql ); |
330 | 395 | $num = $dbr->numRows($res); |
331 | 396 | |
332 | 397 | $this->preprocessResults( $dbr, $res ); |
— | — | @@ -466,9 +531,7 @@ |
467 | 532 | $feed->outHeader(); |
468 | 533 | |
469 | 534 | $dbr = wfGetDB( DB_SLAVE ); |
470 | | - $sql = $this->getSQL() . $this->getOrder(); |
471 | | - $sql = $dbr->limitResult( $sql, $limit, 0 ); |
472 | | - $res = $dbr->query( $sql, 'QueryPage::doFeed' ); |
| 535 | + $res = $this->reallyDoQuery( $limit, 0 ); |
473 | 536 | while( $obj = $dbr->fetchObject( $res ) ) { |
474 | 537 | $item = $this->feedResult( $obj ); |
475 | 538 | if( $item ) $feed->outItem( $item ); |