1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Wikibase\TermStore\MediaWiki\PackagePrivate\Util; |
4
|
|
|
|
5
|
|
|
use AppendIterator; |
6
|
|
|
use ArrayIterator; |
7
|
|
|
use Psr\Log\LoggerInterface; |
8
|
|
|
use Psr\Log\NullLogger; |
9
|
|
|
use Wikimedia\Rdbms\DBQueryError; |
10
|
|
|
use Wikimedia\Rdbms\IDatabase; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Allows acquiring ids of records in database table, |
14
|
|
|
* by inspecting a given read-only replica database to initially |
15
|
|
|
* find existing records with their ids, and insert non-existing |
16
|
|
|
* records into a read-write master databas and getting those |
17
|
|
|
* ids as well from the master database after insertion. |
18
|
|
|
*/ |
19
|
|
|
class ReplicaMasterAwareRecordIdsAcquirer { |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* @var IDatabase $dbMaster |
23
|
|
|
*/ |
24
|
|
|
private $dbMaster; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var IDatabase $dbReplica |
28
|
|
|
*/ |
29
|
|
|
private $dbReplica; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var string $table |
33
|
|
|
*/ |
34
|
|
|
private $table; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var string $idColumn |
38
|
|
|
*/ |
39
|
|
|
private $idColumn; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var LoggerInterface|null $logger |
43
|
|
|
*/ |
44
|
|
|
private $logger; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @param IDatabase $dbMaster master database to insert non-existing records into |
48
|
|
|
* @param IDatabase $dbReplica replica database to initially query existing records in |
49
|
|
|
* @param string $table the name of the table this acquirer is for |
50
|
|
|
* @param string $idColumn the name of the column that contains the desired ids |
51
|
|
|
* @param LoggerInterface $logger |
52
|
|
|
*/ |
53
|
|
|
public function __construct( |
54
|
|
|
IDatabase $dbMaster, |
55
|
|
|
IDatabase $dbReplica, |
56
|
|
|
$table, |
57
|
|
|
$idColumn, |
58
|
|
|
LoggerInterface $logger = null |
59
|
|
|
) { |
60
|
|
|
$this->dbMaster = $dbMaster; |
61
|
|
|
$this->dbReplica = $dbReplica; |
62
|
|
|
$this->table = $table; |
63
|
|
|
$this->idColumn = $idColumn; |
64
|
|
|
$this->logger = $logger ?? new NullLogger(); |
|
|
|
|
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* Acquire ids of needed records in the table, inserting non-existing |
69
|
|
|
* ones into master database. |
70
|
|
|
* |
71
|
|
|
* Note 1: this function assumes that all records given in $neededRecords specify |
72
|
|
|
* the same columns. If some records specify less, more or different columns than |
73
|
|
|
* the first one does, the behavior is not defined. |
74
|
|
|
* |
75
|
|
|
* Note 2: this function assumes that all records given in $neededRecords have |
76
|
|
|
* their values as strings. If some values are of different type (e.g. integer ids) |
77
|
|
|
* this can cause infinite loops due to mismatch in identifying records selected in |
78
|
|
|
* database with their corresponding needed records. The first element keys will be |
79
|
|
|
* used as the set of columns to select in database and to provide back in the returned array. |
80
|
|
|
* |
81
|
|
|
* @param array $neededRecords array of records to be looked-up or inserted. |
82
|
|
|
* Each entry in this array should an associative array of column => value pairs. |
83
|
|
|
* Example: |
84
|
|
|
* [ |
85
|
|
|
* [ 'columnA' => 'valueA1', 'columnB' => 'valueB1' ], |
86
|
|
|
* [ 'columnA' => 'valueA2', 'columnB' => 'valueB2' ], |
87
|
|
|
* ... |
88
|
|
|
* ] |
89
|
|
|
* |
90
|
|
|
* @return array the array of input recrods along with their ids |
91
|
|
|
* Example: |
92
|
|
|
* [ |
93
|
|
|
* [ 'columnA' => 'valueA1', 'columnB' => 'valueB1', 'idColumn' => '1' ], |
94
|
|
|
* [ 'columnA' => 'valueA2', 'columnB' => 'valueB2', 'idColumn' => '2' ], |
95
|
|
|
* ... |
96
|
|
|
* ] |
97
|
|
|
*/ |
98
|
|
|
public function acquireIds( array $neededRecords ) { |
99
|
|
|
$existingRecords = $this->findExistingRecords( $this->dbReplica, $neededRecords ); |
100
|
|
|
$neededRecords = $this->filterNonExistingRecords( $neededRecords, $existingRecords ); |
101
|
|
|
|
102
|
|
|
while ( !empty( $neededRecords ) ) { |
103
|
|
|
$this->insertNonExistingRecordsIntoMaster( $neededRecords ); |
104
|
|
|
$existingRecords = array_merge( |
105
|
|
|
$existingRecords, |
106
|
|
|
$this->findExistingRecords( $this->dbMaster, $neededRecords ) |
107
|
|
|
); |
108
|
|
|
$neededRecords = $this->filterNonExistingRecords( $neededRecords, $existingRecords ); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
return $existingRecords; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
private function findExistingRecords( IDatabase $db, array $neededRecords ): array { |
115
|
|
|
$recordsSelectConditions = array_map( function ( $record ) use ( $db ) { |
116
|
|
|
return $db->makeList( $record, IDatabase::LIST_AND ); |
117
|
|
|
}, $neededRecords ); |
118
|
|
|
|
119
|
|
|
/* |
120
|
|
|
* Todo, related to Note 1 on self::acquireIds(): |
121
|
|
|
* this class can allow for specifying a different set of columns to select |
122
|
|
|
* and return back from self::acquireIds(). This set of columns can be added as |
123
|
|
|
* an optional argument to self::acquireIds() for instance, the current solution |
124
|
|
|
* in here can be a fallback when that isn't given. |
125
|
|
|
*/ |
126
|
|
|
$selectColumns = array_keys( $neededRecords[0] ); |
127
|
|
|
$selectColumns[] = $this->idColumn; |
128
|
|
|
|
129
|
|
|
$existingRows = $db->select( |
130
|
|
|
$this->table, |
131
|
|
|
$selectColumns, |
132
|
|
|
$db->makeList( $recordsSelectConditions, IDatabase::LIST_OR ) |
133
|
|
|
); |
134
|
|
|
|
135
|
|
|
$existingRecords = []; |
136
|
|
|
foreach ( $existingRows as $row ) { |
137
|
|
|
$existingRecord = []; |
138
|
|
|
foreach ( $selectColumns as $column ) { |
139
|
|
|
$existingRecord[$column] = $row->$column; |
140
|
|
|
} |
141
|
|
|
$existingRecords[] = $existingRecord; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
return $existingRecords; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
private function insertNonExistingRecordsIntoMaster( array $neededRecords ) { |
148
|
|
|
$uniqueRecords = []; |
149
|
|
|
foreach ( $neededRecords as $record ) { |
150
|
|
|
$recordHash = $this->calcRecordHash( $record ); |
151
|
|
|
$uniqueRecords[$recordHash] = $record; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
try { |
155
|
|
|
$this->dbMaster->insert( $this->table, array_values( $uniqueRecords ) ); |
156
|
|
|
} catch ( DBQueryError $dbError ) { |
|
|
|
|
157
|
|
|
$this->logger->info( |
158
|
|
|
'{method}: Inserting records into {table} failed: {exception}', |
159
|
|
|
[ |
160
|
|
|
'method' => __METHOD__, |
161
|
|
|
'exception' => $dbError, |
162
|
|
|
'table' => $this->table, |
163
|
|
|
'records' => $uniqueRecords |
164
|
|
|
] |
165
|
|
|
); |
166
|
|
|
} |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
private function filterNonExistingRecords( $neededRecords, $existingRecords ): array { |
170
|
|
|
$existingRecordsHashes = []; |
171
|
|
|
foreach ( $existingRecords as $record ) { |
172
|
|
|
unset( $record[$this->idColumn] ); |
173
|
|
|
$recordHash = $this->calcRecordHash( $record ); |
174
|
|
|
$existingRecordsHashes[$recordHash] = true; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
$nonExistingRecords = []; |
178
|
|
|
foreach ( $neededRecords as $record ) { |
179
|
|
|
$recordHash = $this->calcRecordHash( $record ); |
180
|
|
|
|
181
|
|
|
if ( !isset( $existingRecordsHashes[$recordHash] ) ) { |
182
|
|
|
$nonExistingRecords[] = $record; |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
return $nonExistingRecords; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Implementation detail, related to Note 2 on self::acquireIds(): |
191
|
|
|
* this function relies on the fact that the given set of needed records will have |
192
|
|
|
* all values as strings in order to produce hashes that match up correctly with |
193
|
|
|
* selected records in database, because database selection will always return |
194
|
|
|
* values as strings. |
195
|
|
|
*/ |
196
|
|
|
private function calcRecordHash( array $record ) { |
197
|
|
|
ksort( $record ); |
198
|
|
|
return md5( serialize( $record ) ); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.