2016-03-21 11:51:16 +00:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
|
*
|
|
|
|
|
* @file
|
|
|
|
|
*/
|
|
|
|
|
|
2023-02-23 17:56:12 +00:00
|
|
|
use MediaWiki\MediaWikiServices;
|
2017-02-24 16:17:16 +00:00
|
|
|
use Wikimedia\Rdbms\DBQueryError;
|
2020-01-10 00:00:51 +00:00
|
|
|
use Wikimedia\Rdbms\IDatabase;
|
2021-05-15 08:09:28 +00:00
|
|
|
use Wikimedia\ScopedCallback;
|
2017-02-10 18:09:05 +00:00
|
|
|
|
2016-03-21 11:51:16 +00:00
|
|
|
/**
|
|
|
|
|
* LCStore implementation which uses the standard DB functions to store data.
|
language: Add missing `@ingroup`, subgroup "Languages" and ungroup files
== Ungroup file blocks
Remove `@ingroup` from `@file` blocks and keep only the class block.
This matches similar changes previously applied to API, Skins, Profile,
and ResourceLoader.
This helps make the API documentation easier to navigate.
E.g. Modules -> Language in the sidebar of
<https://doc.wikimedia.org/mediawiki-core/master/php/> as well as
<https://doc.wikimedia.org/mediawiki-core/master/php/group__Language.html>
These are currently cluttered with tons of duplicate entries for files
and classes both. We only need to group files that aren't also
documented as a class (e.g. message files, entry points, other scripts
or files that we mainly consider a data file). This has the helpful
side-effect that we don't encourage duplication of the class
description (or worse, place useful docs only in the file block), and
makes the class files consistently start with a mentally ignorable
block. Basically, unless there's something other than a class, don't
describe or group the file itself.
== Missing group
Various classes in this subtree were missing the `Language` group,
or were using different group from before T225756.
== Subgroup
For ease of navigation, move Converter subclasses to a group called
"Languages", which for documentation purposes is a subgroup of
"Language". The next commit does the same for Messages* files,
and Language subclasses (done separately for ease of review).
Change-Id: I301f471f86ba2dee924fece29a16dc3c20b5bebe
2022-06-22 22:37:31 +00:00
|
|
|
*
|
|
|
|
|
* @ingroup Language
|
2016-03-21 11:51:16 +00:00
|
|
|
*/
|
|
|
|
|
class LCStoreDB implements LCStore {
|
2022-02-26 07:54:16 +00:00
|
|
|
/** @var string|null Language code */
|
2019-09-09 22:50:33 +00:00
|
|
|
private $code;
|
|
|
|
|
/** @var array Server configuration map */
|
|
|
|
|
private $server;
|
|
|
|
|
|
|
|
|
|
/** @var array Rows buffered for insertion */
|
|
|
|
|
private $batch = [];
|
|
|
|
|
|
2018-07-05 11:58:48 +00:00
|
|
|
/** @var IDatabase|null */
|
2016-03-21 11:51:16 +00:00
|
|
|
private $dbw;
|
2023-10-02 12:56:25 +00:00
|
|
|
/** @var bool Whether write batch was recently written */
|
2019-09-09 22:50:33 +00:00
|
|
|
private $writesDone = false;
|
2023-10-02 12:56:25 +00:00
|
|
|
/** @var bool Whether the DB is read-only or otherwise unavailable for writing */
|
2016-03-21 11:51:16 +00:00
|
|
|
private $readOnly = false;
|
2018-07-05 11:58:48 +00:00
|
|
|
|
|
|
|
|
public function __construct( $params ) {
|
|
|
|
|
$this->server = $params['server'] ?? [];
|
|
|
|
|
}
|
2016-03-21 11:51:16 +00:00
|
|
|
|
|
|
|
|
public function get( $code, $key ) {
|
2018-07-05 11:58:48 +00:00
|
|
|
if ( $this->server || $this->writesDone ) {
|
|
|
|
|
// If a server configuration map is specified, always used that connection
|
|
|
|
|
// for reads and writes. Otherwise, if writes occurred in finishWrite(), make
|
|
|
|
|
// sure those changes are always visible.
|
|
|
|
|
$db = $this->getWriteConnection();
|
2016-03-21 11:51:16 +00:00
|
|
|
} else {
|
2024-02-12 19:39:24 +00:00
|
|
|
$db = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
|
2016-03-21 11:51:16 +00:00
|
|
|
}
|
|
|
|
|
|
2023-07-25 13:37:41 +00:00
|
|
|
$value = $db->newSelectQueryBuilder()
|
|
|
|
|
->select( 'lc_value' )
|
|
|
|
|
->from( 'l10n_cache' )
|
|
|
|
|
->where( [ 'lc_lang' => $code, 'lc_key' => $key ] )
|
|
|
|
|
->caller( __METHOD__ )->fetchField();
|
2016-03-21 11:51:16 +00:00
|
|
|
|
|
|
|
|
return ( $value !== false ) ? unserialize( $db->decodeBlob( $value ) ) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function startWrite( $code ) {
|
|
|
|
|
if ( $this->readOnly ) {
|
|
|
|
|
return;
|
|
|
|
|
} elseif ( !$code ) {
|
2023-06-09 22:25:07 +00:00
|
|
|
throw new InvalidArgumentException( __METHOD__ . ": Invalid language \"$code\"" );
|
2016-03-21 11:51:16 +00:00
|
|
|
}
|
|
|
|
|
|
2018-07-05 11:58:48 +00:00
|
|
|
$dbw = $this->getWriteConnection();
|
|
|
|
|
$this->readOnly = $dbw->isReadOnly();
|
2016-03-21 11:51:16 +00:00
|
|
|
|
2019-09-09 22:50:33 +00:00
|
|
|
$this->code = $code;
|
2016-03-21 11:51:16 +00:00
|
|
|
$this->batch = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function finishWrite() {
|
|
|
|
|
if ( $this->readOnly ) {
|
|
|
|
|
return;
|
2020-01-09 23:48:34 +00:00
|
|
|
} elseif ( $this->code === null ) {
|
2023-06-09 22:25:07 +00:00
|
|
|
throw new LogicException( __CLASS__ . ': must call startWrite() before finishWrite()' );
|
2016-03-21 11:51:16 +00:00
|
|
|
}
|
|
|
|
|
|
2021-05-15 08:09:28 +00:00
|
|
|
$scope = Profiler::instance()->getTransactionProfiler()->silenceForScope();
|
|
|
|
|
$dbw = $this->getWriteConnection();
|
|
|
|
|
$dbw->startAtomic( __METHOD__ );
|
2016-03-21 11:51:16 +00:00
|
|
|
try {
|
2023-06-20 19:31:41 +00:00
|
|
|
$dbw->newDeleteQueryBuilder()
|
In query builders, use insertInto() and deleteFrom() instead of insert() and delete()
The design principle for SelectQueryBuilder was to make the chained
builder calls look as much like SQL as possible, so that developers
could leverage their knowledge of SQL to understand what the query
builder is doing.
That's why SelectQueryBuilder::select() takes a list of fields, and by
the same principle, it makes sense for UpdateQueryBuilder::update() to
take a table. However with "insert" and "delete", the SQL designers
chose to add prepositions "into" and "from", and I think it makes sense
to follow that here.
In terms of natural language, we update a table, but we don't delete a
table, or insert a table. We delete rows from a table, or insert rows
into a table. The table is not the object of the verb.
So, add insertInto() as an alias for insert(), and add deleteFrom() as
an alias for delete(). Use the new methods in MW core callers where
PHPStorm knows the type.
Change-Id: Idb327a54a57a0fb2288ea067472c1e9727016000
2023-09-08 00:06:59 +00:00
|
|
|
->deleteFrom( 'l10n_cache' )
|
2023-06-20 19:31:41 +00:00
|
|
|
->where( [ 'lc_lang' => $this->code ] )
|
|
|
|
|
->caller( __METHOD__ )->execute();
|
2021-05-15 08:09:28 +00:00
|
|
|
foreach ( array_chunk( $this->batch, 500 ) as $rows ) {
|
2023-08-03 20:05:22 +00:00
|
|
|
$dbw->newInsertQueryBuilder()
|
In query builders, use insertInto() and deleteFrom() instead of insert() and delete()
The design principle for SelectQueryBuilder was to make the chained
builder calls look as much like SQL as possible, so that developers
could leverage their knowledge of SQL to understand what the query
builder is doing.
That's why SelectQueryBuilder::select() takes a list of fields, and by
the same principle, it makes sense for UpdateQueryBuilder::update() to
take a table. However with "insert" and "delete", the SQL designers
chose to add prepositions "into" and "from", and I think it makes sense
to follow that here.
In terms of natural language, we update a table, but we don't delete a
table, or insert a table. We delete rows from a table, or insert rows
into a table. The table is not the object of the verb.
So, add insertInto() as an alias for insert(), and add deleteFrom() as
an alias for delete(). Use the new methods in MW core callers where
PHPStorm knows the type.
Change-Id: Idb327a54a57a0fb2288ea067472c1e9727016000
2023-09-08 00:06:59 +00:00
|
|
|
->insertInto( 'l10n_cache' )
|
2023-08-03 20:05:22 +00:00
|
|
|
->rows( $rows )
|
|
|
|
|
->caller( __METHOD__ )->execute();
|
2021-05-15 08:09:28 +00:00
|
|
|
}
|
|
|
|
|
$this->writesDone = true;
|
|
|
|
|
} catch ( DBQueryError $e ) {
|
2024-06-04 13:55:18 +00:00
|
|
|
if ( $dbw->isReadOnly() ) {
|
2023-10-02 12:56:25 +00:00
|
|
|
$this->readOnly = true; // just avoid site downtime
|
2021-05-15 08:09:28 +00:00
|
|
|
} else {
|
|
|
|
|
throw $e;
|
2016-03-21 11:51:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
2021-05-15 08:09:28 +00:00
|
|
|
$dbw->endAtomic( __METHOD__ );
|
|
|
|
|
ScopedCallback::consume( $scope );
|
2016-03-21 11:51:16 +00:00
|
|
|
|
2019-09-09 22:50:33 +00:00
|
|
|
$this->code = null;
|
2016-03-21 11:51:16 +00:00
|
|
|
$this->batch = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function set( $key, $value ) {
|
|
|
|
|
if ( $this->readOnly ) {
|
|
|
|
|
return;
|
2020-01-09 23:48:34 +00:00
|
|
|
} elseif ( $this->code === null ) {
|
2023-06-09 22:25:07 +00:00
|
|
|
throw new LogicException( __CLASS__ . ': must call startWrite() before set()' );
|
2016-03-21 11:51:16 +00:00
|
|
|
}
|
|
|
|
|
|
2018-07-05 11:58:48 +00:00
|
|
|
$dbw = $this->getWriteConnection();
|
|
|
|
|
|
2016-03-21 11:51:16 +00:00
|
|
|
$this->batch[] = [
|
2019-09-09 22:50:33 +00:00
|
|
|
'lc_lang' => $this->code,
|
2016-03-21 11:51:16 +00:00
|
|
|
'lc_key' => $key,
|
2018-07-05 11:58:48 +00:00
|
|
|
'lc_value' => $dbw->encodeBlob( serialize( $value ) )
|
2016-03-21 11:51:16 +00:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-05 11:58:48 +00:00
|
|
|
/**
|
|
|
|
|
* @return IDatabase
|
|
|
|
|
*/
|
|
|
|
|
private function getWriteConnection() {
|
|
|
|
|
if ( !$this->dbw ) {
|
|
|
|
|
if ( $this->server ) {
|
2023-02-23 17:56:12 +00:00
|
|
|
$dbFactory = MediaWikiServices::getInstance()->getDatabaseFactory();
|
|
|
|
|
$this->dbw = $dbFactory->create( $this->server['type'], $this->server );
|
2018-07-05 11:58:48 +00:00
|
|
|
if ( !$this->dbw ) {
|
2023-06-09 22:25:07 +00:00
|
|
|
throw new RuntimeException( __CLASS__ . ': failed to obtain a DB connection' );
|
2018-07-05 11:58:48 +00:00
|
|
|
}
|
|
|
|
|
} else {
|
2024-01-22 21:27:45 +00:00
|
|
|
$this->dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
|
2018-07-05 11:58:48 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->dbw;
|
|
|
|
|
}
|
2016-03-21 11:51:16 +00:00
|
|
|
}
|