Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SqlModuleDependencyStore
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 8
552
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 retrieveMulti
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 storeMulti
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
 remove
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 fetchDependencyBlobs
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 getReplicaDb
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getPrimaryDb
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getEntityNameComponents
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
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 2 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 along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace Wikimedia\DependencyStore;
22
23use InvalidArgumentException;
24use Wikimedia\Rdbms\IDatabase;
25use Wikimedia\Rdbms\ILoadBalancer;
26use Wikimedia\Rdbms\IReadableDatabase;
27use Wikimedia\Rdbms\OrExpressionGroup;
28
29/**
30 * Track per-module file dependencies in the core module_deps table
31 *
32 * Wiki farms that are too big for maintenance/update.php, can clean up
33 * unneeded data for modules that no longer exist after a MW upgrade,
34 * by running maintenance/cleanupRemovedModules.php.
35 *
36 * To force a rebuild and incurr a small penalty in browser cache churn,
37 * run maintenance/purgeModuleDeps.php instead.
38 *
39 * @internal For use by ResourceLoader\Module only
40 * @since 1.35
41 */
42class SqlModuleDependencyStore extends DependencyStore {
43    /** @var ILoadBalancer */
44    private $lb;
45
46    /**
47     * @param ILoadBalancer $lb Storage backend
48     */
49    public function __construct( ILoadBalancer $lb ) {
50        $this->lb = $lb;
51    }
52
53    public function retrieveMulti( $type, array $entities ) {
54        $dbr = $this->getReplicaDb();
55
56        $depsBlobByEntity = $this->fetchDependencyBlobs( $entities, $dbr );
57
58        $storedPathsByEntity = [];
59        foreach ( $depsBlobByEntity as $entity => $depsBlob ) {
60            $storedPathsByEntity[$entity] = json_decode( $depsBlob, true );
61        }
62
63        $results = [];
64        foreach ( $entities as $entity ) {
65            $paths = $storedPathsByEntity[$entity] ?? [];
66            $results[$entity] = $this->newEntityDependencies( $paths, null );
67        }
68
69        return $results;
70    }
71
72    public function storeMulti( $type, array $dataByEntity, $ttl ) {
73        // Avoid opening a primary DB connection when it's not needed.
74        // ResourceLoader::saveModuleDependenciesInternal calls this method unconditionally
75        // with empty values most of the time.
76        if ( !$dataByEntity ) {
77            return;
78        }
79
80        $dbw = $this->getPrimaryDb();
81        $depsBlobByEntity = $this->fetchDependencyBlobs( array_keys( $dataByEntity ), $dbw );
82
83        $rows = [];
84        foreach ( $dataByEntity as $entity => $data ) {
85            [ $module, $variant ] = $this->getEntityNameComponents( $entity );
86            if ( !is_array( $data[self::KEY_PATHS] ) ) {
87                throw new InvalidArgumentException( "Invalid entry for '$entity'" );
88            }
89
90            // Normalize the list by removing duplicates and sortings
91            $paths = array_values( array_unique( $data[self::KEY_PATHS] ) );
92            sort( $paths, SORT_STRING );
93            $blob = json_encode( $paths, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
94
95            $existingBlob = $depsBlobByEntity[$entity] ?? null;
96            if ( $blob !== $existingBlob ) {
97                $rows[] = [
98                    'md_module' => $module,
99                    'md_skin' => $variant,
100                    'md_deps' => $blob
101                ];
102            }
103        }
104
105        // @TODO: use a single query with VALUES()/aliases support in DB wrapper
106        // See https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html
107        foreach ( $rows as $row ) {
108            $dbw->newInsertQueryBuilder()
109                ->insertInto( 'module_deps' )
110                ->row( $row )
111                ->onDuplicateKeyUpdate()
112                ->uniqueIndexFields( [ 'md_module', 'md_skin' ] )
113                ->set( [ 'md_deps' => $row['md_deps'] ] )
114                ->caller( __METHOD__ )->execute();
115        }
116    }
117
118    public function remove( $type, $entities ) {
119        // Avoid opening a primary DB connection when it's not needed.
120        // ResourceLoader::saveModuleDependenciesInternal calls this method unconditionally
121        // with empty values most of the time.
122        if ( !$entities ) {
123            return;
124        }
125
126        $dbw = $this->getPrimaryDb();
127        $disjunctionConds = [];
128        foreach ( (array)$entities as $entity ) {
129            [ $module, $variant ] = $this->getEntityNameComponents( $entity );
130            $disjunctionConds[] = $dbw
131                ->expr( 'md_skin', '=', $variant )
132                ->and( 'md_module', '=', $module );
133        }
134
135        if ( $disjunctionConds ) {
136            $dbw->newDeleteQueryBuilder()
137                ->deleteFrom( 'module_deps' )
138                ->where( new OrExpressionGroup( ...$disjunctionConds ) )
139                ->caller( __METHOD__ )->execute();
140        }
141    }
142
143    /**
144     * @param string[] $entities
145     * @param IReadableDatabase $db
146     * @return string[]
147     */
148    private function fetchDependencyBlobs( array $entities, IReadableDatabase $db ) {
149        $modulesByVariant = [];
150        foreach ( $entities as $entity ) {
151            [ $module, $variant ] = $this->getEntityNameComponents( $entity );
152            $modulesByVariant[$variant][] = $module;
153        }
154
155        $disjunctionConds = [];
156        foreach ( $modulesByVariant as $variant => $modules ) {
157            $disjunctionConds[] = $db
158                ->expr( 'md_skin', '=', $variant )
159                ->and( 'md_module', '=', $modules );
160        }
161
162        $depsBlobByEntity = [];
163
164        if ( $disjunctionConds ) {
165            $res = $db->newSelectQueryBuilder()
166                ->select( [ 'md_module', 'md_skin', 'md_deps' ] )
167                ->from( 'module_deps' )
168                ->where( new OrExpressionGroup( ...$disjunctionConds ) )
169                ->caller( __METHOD__ )->fetchResultSet();
170
171            foreach ( $res as $row ) {
172                $entity = "{$row->md_module}|{$row->md_skin}";
173                $depsBlobByEntity[$entity] = $row->md_deps;
174            }
175        }
176
177        return $depsBlobByEntity;
178    }
179
180    /**
181     * @return IReadableDatabase
182     */
183    private function getReplicaDb() {
184        return $this->lb
185            ->getConnection( DB_REPLICA, [], false, ( $this->lb )::CONN_TRX_AUTOCOMMIT );
186    }
187
188    /**
189     * @return IDatabase
190     */
191    private function getPrimaryDb() {
192        return $this->lb
193            ->getConnection( DB_PRIMARY, [], false, ( $this->lb )::CONN_TRX_AUTOCOMMIT );
194    }
195
196    /**
197     * @param string $entity
198     * @return string[]
199     */
200    private function getEntityNameComponents( $entity ) {
201        $parts = explode( '|', $entity, 2 );
202        if ( count( $parts ) !== 2 ) {
203            throw new InvalidArgumentException( "Invalid module entity '$entity'" );
204        }
205
206        return $parts;
207    }
208}