Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
60.00% covered (warning)
60.00%
18 / 30
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
UserDatabaseHelper
60.00% covered (warning)
60.00%
18 / 30
33.33% covered (danger)
33.33%
1 / 3
8.30
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 findFirstUserIdForRegistrationTimestamp
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 hasMainspaceEdits
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace GrowthExperiments;
4
5use MediaWiki\User\UserFactory;
6use MediaWiki\User\UserIdentity;
7use Wikimedia\Rdbms\IConnectionProvider;
8use Wikimedia\Rdbms\SelectQueryBuilder;
9
10/**
11 * Helper class for some user-related queries.
12 */
13class UserDatabaseHelper {
14
15    private UserFactory $userFactory;
16    private IConnectionProvider $connectionProvider;
17
18    /**
19     * @param UserFactory $userFactory
20     * @param IConnectionProvider $connectionProvider For the database with the user table.
21     */
22    public function __construct(
23        UserFactory $userFactory,
24        IConnectionProvider $connectionProvider
25    ) {
26        $this->userFactory = $userFactory;
27        $this->connectionProvider = $connectionProvider;
28    }
29
30    /**
31     * Find the first user_id with a registration date >= $registrationDate. On large wikis this
32     * can be a slow operation and should be only used in deferreds and similar
33     * non-performance-sensitive places.
34     *
35     * user_registration is not indexed so filtering or paging based on it is very slow on large
36     * wikis. It is monotonic to a very good approximation though, so once we can find the first
37     * user_id matching the given registration timestamp, we can filter/page by primary key.
38     * @param int|string $registrationTimestamp Registration time in any format known by
39     *   ConvertibleTimestamp.
40     * @return int|null User ID, or null if no user has registered on or after that timestamp.
41     */
42    public function findFirstUserIdForRegistrationTimestamp( $registrationTimestamp ): ?int {
43        $dbr = $this->connectionProvider->getReplicaDatabase();
44        $registrationTimestamp = $dbr->timestamp( $registrationTimestamp );
45        $userId = $dbr->newSelectQueryBuilder()
46            ->field( 'user_id' )
47            ->from( 'user' )
48            ->where( $dbr->expr( 'user_registration', '>=', $registrationTimestamp ) )
49            ->orderBy( 'user_id', SelectQueryBuilder::SORT_ASC )
50            ->caller( __METHOD__ )
51            ->fetchField();
52        return $userId === false ? null : (int)$userId;
53    }
54
55    /**
56     * Performant approximate check for whether the user has any edits in the main namespace.
57     * Will return null if the user's first $limit edits are all not in the main namespace.
58     * @param UserIdentity $userIdentity
59     * @param int $limit
60     * @return bool|null
61     */
62    public function hasMainspaceEdits( UserIdentity $userIdentity, int $limit = 1000 ): ?bool {
63        $user = $this->userFactory->newFromUserIdentity( $userIdentity );
64        $res = $this->connectionProvider->getReplicaDatabase()->newSelectQueryBuilder()
65            ->select( 'page_namespace' )
66            ->from( 'revision' )
67            ->join( 'page', null, 'page_id = rev_page' )
68            ->where( [
69                'rev_actor' => $user->getActorId()
70            ] )
71            // Look at the user's oldest edits - arbitrary choice, other than we want the method to be
72            // deterministic. Can be done efficiently via the rev_actor_timestamp index.
73            ->orderBy( 'rev_timestamp', SelectQueryBuilder::SORT_ASC )
74            ->limit( $limit )
75            ->caller( __METHOD__ )
76            ->fetchFieldValues();
77        $result = array_map( 'intval', $res );
78        if ( in_array( NS_MAIN, $result ) ) {
79            return true;
80        }
81        return count( $result ) === $limit ? null : false;
82    }
83
84}