Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.92% covered (warning)
85.92%
61 / 71
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SqlScoreStorage
85.92% covered (warning)
85.92%
61 / 71
60.00% covered (warning)
60.00%
3 / 5
22.23
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 storeScores
76.67% covered (warning)
76.67%
23 / 30
0.00% covered (danger)
0.00%
0 / 1
7.62
 purgeRows
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 checkModelToKeep
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
7.33
 cleanUpOldScores
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, either version 3 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
15 */
16
17namespace ORES\Storage;
18
19use InvalidArgumentException;
20use Psr\Log\LoggerInterface;
21use RuntimeException;
22use Wikimedia\Rdbms\DBError;
23use Wikimedia\Rdbms\IConnectionProvider;
24
25class SqlScoreStorage implements ScoreStorage {
26
27    private IConnectionProvider $dbProvider;
28    private ModelLookup $modelLookup;
29    private LoggerInterface $logger;
30
31    public function __construct(
32        IConnectionProvider $dbProvider,
33        ModelLookup $modelLookup,
34        LoggerInterface $logger
35    ) {
36        $this->dbProvider = $dbProvider;
37        $this->modelLookup = $modelLookup;
38        $this->logger = $logger;
39    }
40
41    /**
42     * @see ModelLookup::getModelId()
43     *
44     * @param array[] $scores
45     * @param callable|null $errorCallback
46     * @param string[] $modelsToClean
47     */
48    public function storeScores(
49        $scores,
50        ?callable $errorCallback = null,
51        array $modelsToClean = []
52    ) {
53        // TODO: Make $wgOresModelClasses an argument and deprecate the whole config variable
54        global $wgOresModelClasses, $wgOresAggregatedModels;
55
56        if ( $errorCallback === null ) {
57            /**
58             * @param string $mssg
59             * @param int $revision
60             * @return never
61             */
62            $errorCallback = static function ( $mssg, $revision ) {
63                throw new RuntimeException( "Model contains an error for $revision$mssg" );
64            };
65        }
66
67        $dbData = [];
68
69        $scoreParser = new ScoreParser(
70            $this->modelLookup,
71            $wgOresModelClasses,
72            $wgOresAggregatedModels
73        );
74        foreach ( $scores as $revision => $revisionData ) {
75            try {
76                $dbDataPerRevision = $scoreParser->processRevision( $revision, $revisionData );
77            } catch ( InvalidArgumentException $exception ) {
78                call_user_func( $errorCallback, $exception->getMessage(), $revision );
79                continue;
80            }
81
82            $dbData = array_merge( $dbData, $dbDataPerRevision );
83        }
84
85        if ( $dbData ) {
86            try {
87                $this->dbProvider->getPrimaryDatabase()->newInsertQueryBuilder()
88                    ->insertInto( 'ores_classification' )
89                    ->rows( $dbData )
90                    ->ignore()
91                    ->caller( __METHOD__ )
92                    ->execute();
93            } catch ( DBError $exception ) {
94                $this->logger->error(
95                    'Inserting new data into the datbase has failed:' . $exception->getMessage()
96                );
97                return;
98            }
99        }
100
101        if ( $modelsToClean !== [] ) {
102            $this->cleanUpOldScores( $scores, $modelsToClean );
103        }
104    }
105
106    /**
107     * @see ScoreStorage::purgeRows()
108     *
109     * @param int[] $revIds array of revision ids to clean scores
110     */
111    public function purgeRows( array $revIds ) {
112        global $wgOresModels;
113        $modelsToKeep = [];
114        foreach ( $wgOresModels as $model => $modelData ) {
115            $modelId = $this->checkModelToKeep( $model, $modelData );
116            if ( $modelId !== false ) {
117                $modelsToKeep[] = $modelId;
118            }
119        }
120
121        $conditions = [ 'oresc_rev' => $revIds ];
122        if ( $modelsToKeep ) {
123            $conditions[] = 'oresc_model NOT IN (' . implode( ', ', $modelsToKeep ) . ')';
124        }
125
126        $this->dbProvider->getPrimaryDatabase()->newDeleteQueryBuilder()
127            ->deleteFrom( 'ores_classification' )
128            ->where( $conditions )
129            ->caller( __METHOD__ )
130            ->execute();
131    }
132
133    /**
134     * @param string $model name
135     * @param array $modelData
136     * @return int|bool model id to keep, false otherwise
137     */
138    private function checkModelToKeep( $model, array $modelData ) {
139        if ( !isset( $modelData['enabled'] ) || !$modelData['enabled'] ) {
140            return false;
141        }
142        if ( !isset( $modelData['keepForever'] ) || !$modelData['keepForever'] ) {
143            return false;
144        }
145
146        try {
147            $modelId = $this->modelLookup->getModelId( $model );
148        } catch ( InvalidArgumentException $exception ) {
149            $this->logger->warning( "Model {$model} can't be found in the model lookup" );
150            return false;
151        }
152
153        return $modelId;
154    }
155
156    /**
157     * @param array[] $scores
158     * @param string[] $modelsToClean
159     */
160    private function cleanUpOldScores( array $scores, array $modelsToClean ) {
161        $modelIds = [];
162        foreach ( $modelsToClean as $model ) {
163            $modelIds[] = $this->modelLookup->getModelId( $model );
164        }
165
166        $newRevisions = array_keys( $scores );
167
168        $parentIds = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
169            ->select( 'rc_last_oldid' )
170            ->from( 'recentchanges' )
171            ->where( [ 'rc_this_oldid' => $newRevisions ] )
172            ->caller( __METHOD__ )
173            ->fetchFieldValues();
174
175        if ( $parentIds ) {
176            $this->dbProvider->getPrimaryDatabase()->newDeleteQueryBuilder()
177                ->deleteFrom( 'ores_classification' )
178                ->where( [ 'oresc_rev' => $parentIds, 'oresc_model' => $modelIds ] )
179                ->caller( __METHOD__ )
180                ->execute();
181        }
182    }
183
184}