Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.31% covered (success)
92.31%
36 / 39
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
SqlIdGenerator
92.31% covered (success)
92.31%
36 / 39
66.67% covered (warning)
66.67%
2 / 3
8.03
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
 getNewId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 generateNewId
90.91% covered (success)
90.91%
30 / 33
0.00% covered (danger)
0.00%
0 / 1
6.03
1<?php
2
3declare( strict_types = 1 );
4
5namespace EntitySchema\DataAccess;
6
7use EntitySchema\Domain\Storage\IdGenerator;
8use RuntimeException;
9use Wikimedia\Rdbms\IDatabase;
10use Wikimedia\Rdbms\ILoadBalancer;
11
12/**
13 * Unique Id generator implemented using an SQL table.
14 * The table needs to have the fields id_value
15 *
16 * @license GPL-2.0-or-later
17 * based on
18 * @see \Wikibase\Repo\Store\Sql\SqlIdGenerator
19 */
20class SqlIdGenerator implements IdGenerator {
21
22    private ILoadBalancer $loadBalancer;
23
24    private string $tableName;
25
26    /** @var int[] */
27    private array $idsToSkip;
28
29    /**
30     * @param ILoadBalancer $loadBalancer
31     * @param string        $tableName
32     * @param int[]         $idsToSkip
33     */
34    public function __construct( ILoadBalancer $loadBalancer, string $tableName, array $idsToSkip = [] ) {
35        $this->loadBalancer = $loadBalancer;
36        $this->tableName = $tableName;
37        $this->idsToSkip = $idsToSkip;
38    }
39
40    /**
41     * @throws RuntimeException
42     */
43    public function getNewId(): int {
44        $database = $this->loadBalancer->getConnection( DB_PRIMARY );
45
46        $id = $this->generateNewId( $database );
47        return $id;
48    }
49
50    /**
51     * Generates and returns a new ID.
52     *
53     * @param IDatabase $database
54     * @param bool $retry Retry once in case of e.g. race conditions. Defaults to true.
55     *
56     * @throws RuntimeException
57     * @return int
58     */
59    private function generateNewId( IDatabase $database, bool $retry = true ): int {
60        $database->startAtomic( __METHOD__ );
61        $currentId = $database->newSelectQueryBuilder()
62            ->select( [ 'id_value' ] )
63            ->from( $this->tableName )
64            ->forUpdate()
65            ->caller( __METHOD__ )
66            ->fetchRow();
67
68        if ( is_object( $currentId ) ) {
69            $id = $currentId->id_value + 1;
70            $database->newUpdateQueryBuilder()
71                ->update( $this->tableName )
72                ->set( [ 'id_value' => $id ] )
73                ->where( $database::ALL_ROWS ) // there is only one row
74                ->caller( __METHOD__ )->execute();
75            $success = true; // T339346
76        } else {
77            $id = 1;
78
79            $database->newInsertQueryBuilder()
80                ->insertInto( $this->tableName )
81                ->row( [
82                    'id_value' => $id,
83                ] )
84                ->caller( __METHOD__ )
85                ->execute();
86            $success = true; // T339346
87
88            // Retry once, since a race condition on initial insert can cause one to fail.
89            // Race condition is possible due to occurrence of phantom reads is possible
90            // at non serializable transaction isolation level.
91            // @phan-suppress-next-line PhanImpossibleCondition T339346
92            if ( !$success && $retry ) {
93                $id = $this->generateNewId( $database, false );
94                $success = true;
95            }
96        }
97
98        $database->endAtomic( __METHOD__ );
99
100        if ( !$success ) {
101            throw new RuntimeException( 'Could not generate a reliably unique ID.' );
102        }
103
104        if ( in_array( $id, $this->idsToSkip ) ) {
105            $id = $this->generateNewId( $database, $retry );
106        }
107
108        return $id;
109    }
110
111}