Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 542 |
|
0.00% |
0 / 56 |
CRAP | |
0.00% |
0 / 1 |
DatabasePostgres | |
0.00% |
0 / 542 |
|
0.00% |
0 / 56 |
22350 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
getType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
open | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
210 | |||
databasesAreIndependent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doSelectDomain | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
makeConnectionString | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
closeConnection | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
doSingleStatementQuery | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 | |||
dumpError | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 | |||
lastInsertId | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
lastError | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
30 | |||
lastErrno | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
estimateRowCount | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
indexInfo | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
indexAttributes | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
30 | |||
indexUnique | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
doInsertSelectNative | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
nextSequenceValue | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getValueTypesForWithClause | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
textFieldSize | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
wasDeadlock | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isConnectionError | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
isQueryTimeoutError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isKnownStatementRollbackError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
duplicateTableStructure | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
30 | |||
truncateTable | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
listTables | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 | |||
pg_array_parse | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
getSoftwareLink | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCurrentSchema | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
getSchemas | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
getSearchPath | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
setSearchPath | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
determineCoreSchema | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
20 | |||
getCoreSchema | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCoreSchemas | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
getServerVersion | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
relationExists | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
42 | |||
tableExists | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
sequenceExists | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
constraintExists | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
schemaExists | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
roleExists | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
fieldInfo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
encodeBlob | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
decodeBlob | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
strencode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addQuotes | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
56 | |||
streamStatementEnd | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
doLockIsFree | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
doLock | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
6 | |||
doUnlock | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
doFlushSession | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
serverIsReadOnly | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
getInsertIdColumnForUpsert | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 | |||
getAttributes | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
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 | |
21 | // Suppress UnusedPluginSuppression because Phan on PHP 7.4 and PHP 8.1 need different suppressions |
22 | // @phan-file-suppress UnusedPluginSuppression,UnusedPluginFileSuppression |
23 | |
24 | namespace Wikimedia\Rdbms; |
25 | |
26 | use RuntimeException; |
27 | use Wikimedia\Rdbms\Platform\PostgresPlatform; |
28 | use Wikimedia\Rdbms\Replication\ReplicationReporter; |
29 | use Wikimedia\WaitConditionLoop; |
30 | |
31 | /** |
32 | * Postgres database abstraction layer. |
33 | * |
34 | * @ingroup Database |
35 | */ |
36 | class DatabasePostgres extends Database { |
37 | /** @var int */ |
38 | private $port; |
39 | /** @var string */ |
40 | private $tempSchema; |
41 | /** @var float|string */ |
42 | private $numericVersion; |
43 | |
44 | /** @var resource|null */ |
45 | private $lastResultHandle; |
46 | |
47 | /** @var PostgresPlatform */ |
48 | protected $platform; |
49 | |
50 | /** |
51 | * @see Database::__construct() |
52 | * @param array $params Additional parameters include: |
53 | * - port: A port to append to the hostname |
54 | */ |
55 | public function __construct( array $params ) { |
56 | $this->port = intval( $params['port'] ?? null ); |
57 | parent::__construct( $params ); |
58 | |
59 | $this->platform = new PostgresPlatform( |
60 | $this, |
61 | $this->logger, |
62 | $this->currentDomain, |
63 | $this->errorLogger |
64 | ); |
65 | $this->replicationReporter = new ReplicationReporter( |
66 | $params['topologyRole'], |
67 | $this->logger, |
68 | $params['srvCache'] |
69 | ); |
70 | } |
71 | |
72 | public function getType() { |
73 | return 'postgres'; |
74 | } |
75 | |
76 | protected function open( $server, $user, $password, $db, $schema, $tablePrefix ) { |
77 | if ( !function_exists( 'pg_connect' ) ) { |
78 | throw $this->newExceptionAfterConnectError( |
79 | "Postgres functions missing, have you compiled PHP with the --with-pgsql\n" . |
80 | "option? (Note: if you recently installed PHP, you may need to restart your\n" . |
81 | "webserver and database)" |
82 | ); |
83 | } |
84 | |
85 | $this->close( __METHOD__ ); |
86 | |
87 | $connectVars = [ |
88 | // A database must be specified in order to connect to Postgres. If $dbName is not |
89 | // specified, then use the standard "postgres" database that should exist by default. |
90 | 'dbname' => ( $db !== null && $db !== '' ) ? $db : 'postgres', |
91 | 'user' => $user, |
92 | 'password' => $password |
93 | ]; |
94 | if ( $server !== null && $server !== '' ) { |
95 | $connectVars['host'] = $server; |
96 | } |
97 | if ( $this->port > 0 ) { |
98 | $connectVars['port'] = $this->port; |
99 | } |
100 | if ( $this->ssl ) { |
101 | $connectVars['sslmode'] = 'require'; |
102 | } |
103 | $connectString = $this->makeConnectionString( $connectVars ); |
104 | |
105 | $this->installErrorHandler(); |
106 | try { |
107 | $this->conn = pg_connect( $connectString, PGSQL_CONNECT_FORCE_NEW ) ?: null; |
108 | } catch ( RuntimeException $e ) { |
109 | $this->restoreErrorHandler(); |
110 | throw $this->newExceptionAfterConnectError( $e->getMessage() ); |
111 | } |
112 | $error = $this->restoreErrorHandler(); |
113 | |
114 | if ( !$this->conn ) { |
115 | throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() ); |
116 | } |
117 | |
118 | try { |
119 | // Since no transaction is active at this point, any SET commands should apply |
120 | // for the entire session (e.g. will not be reverted on transaction rollback). |
121 | // See https://www.postgresql.org/docs/8.3/sql-set.html |
122 | $variables = [ |
123 | 'client_encoding' => 'UTF8', |
124 | 'datestyle' => 'ISO, YMD', |
125 | 'timezone' => 'GMT', |
126 | 'standard_conforming_strings' => 'on', |
127 | 'bytea_output' => 'escape', |
128 | 'client_min_messages' => 'ERROR' |
129 | ]; |
130 | foreach ( $variables as $var => $val ) { |
131 | $sql = 'SET ' . $this->platform->addIdentifierQuotes( $var ) . ' = ' . $this->addQuotes( $val ); |
132 | $query = new Query( $sql, self::QUERY_NO_RETRY | self::QUERY_CHANGE_TRX, 'SET' ); |
133 | $this->query( $query, __METHOD__ ); |
134 | } |
135 | $this->determineCoreSchema( $schema ); |
136 | $this->currentDomain = new DatabaseDomain( $db, $schema, $tablePrefix ); |
137 | $this->platform->setCurrentDomain( $this->currentDomain ); |
138 | } catch ( RuntimeException $e ) { |
139 | throw $this->newExceptionAfterConnectError( $e->getMessage() ); |
140 | } |
141 | } |
142 | |
143 | public function databasesAreIndependent() { |
144 | return true; |
145 | } |
146 | |
147 | public function doSelectDomain( DatabaseDomain $domain ) { |
148 | $database = $domain->getDatabase(); |
149 | if ( $database === null ) { |
150 | // A null database means "don't care" so leave it as is and update the table prefix |
151 | $this->currentDomain = new DatabaseDomain( |
152 | $this->currentDomain->getDatabase(), |
153 | $domain->getSchema() ?? $this->currentDomain->getSchema(), |
154 | $domain->getTablePrefix() |
155 | ); |
156 | $this->platform->setCurrentDomain( $this->currentDomain ); |
157 | } elseif ( $this->getDBname() !== $database ) { |
158 | // Postgres doesn't support selectDB in the same way MySQL does. |
159 | // So if the DB name doesn't match the open connection, open a new one |
160 | $this->open( |
161 | $this->connectionParams[self::CONN_HOST], |
162 | $this->connectionParams[self::CONN_USER], |
163 | $this->connectionParams[self::CONN_PASSWORD], |
164 | $database, |
165 | $domain->getSchema(), |
166 | $domain->getTablePrefix() |
167 | ); |
168 | } else { |
169 | $this->currentDomain = $domain; |
170 | $this->platform->setCurrentDomain( $this->currentDomain ); |
171 | } |
172 | |
173 | return true; |
174 | } |
175 | |
176 | /** |
177 | * @param string[] $vars |
178 | * @return string |
179 | */ |
180 | private function makeConnectionString( $vars ) { |
181 | $s = ''; |
182 | foreach ( $vars as $name => $value ) { |
183 | $s .= "$name='" . str_replace( [ "\\", "'" ], [ "\\\\", "\\'" ], $value ) . "' "; |
184 | } |
185 | |
186 | return $s; |
187 | } |
188 | |
189 | protected function closeConnection() { |
190 | return $this->conn ? pg_close( $this->conn ) : true; |
191 | } |
192 | |
193 | public function doSingleStatementQuery( string $sql ): QueryStatus { |
194 | $conn = $this->getBindingHandle(); |
195 | |
196 | $sql = mb_convert_encoding( $sql, 'UTF-8' ); |
197 | // Clear any previously left over result |
198 | while ( $priorRes = pg_get_result( $conn ) ) { |
199 | pg_free_result( $priorRes ); |
200 | } |
201 | |
202 | if ( pg_send_query( $conn, $sql ) === false ) { |
203 | throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" ); |
204 | } |
205 | |
206 | // Newer PHP versions use PgSql\Result instead of resource variables |
207 | // https://www.php.net/manual/en/function.pg-get-result.php |
208 | $pgRes = pg_get_result( $conn ); |
209 | // Phan on PHP 7.4 and PHP 8.1 need different suppressions |
210 | // @phan-suppress-next-line PhanTypeMismatchProperty,PhanTypeMismatchPropertyProbablyReal |
211 | $this->lastResultHandle = $pgRes; |
212 | $res = pg_result_error( $pgRes ) ? false : $pgRes; |
213 | |
214 | return new QueryStatus( |
215 | // @phan-suppress-next-line PhanTypeMismatchArgument |
216 | is_bool( $res ) ? $res : new PostgresResultWrapper( $this, $conn, $res ), |
217 | $pgRes ? pg_affected_rows( $pgRes ) : 0, |
218 | $this->lastError(), |
219 | $this->lastErrno() |
220 | ); |
221 | } |
222 | |
223 | protected function dumpError() { |
224 | $diags = [ |
225 | PGSQL_DIAG_SEVERITY, |
226 | PGSQL_DIAG_SQLSTATE, |
227 | PGSQL_DIAG_MESSAGE_PRIMARY, |
228 | PGSQL_DIAG_MESSAGE_DETAIL, |
229 | PGSQL_DIAG_MESSAGE_HINT, |
230 | PGSQL_DIAG_STATEMENT_POSITION, |
231 | PGSQL_DIAG_INTERNAL_POSITION, |
232 | PGSQL_DIAG_INTERNAL_QUERY, |
233 | PGSQL_DIAG_CONTEXT, |
234 | PGSQL_DIAG_SOURCE_FILE, |
235 | PGSQL_DIAG_SOURCE_LINE, |
236 | PGSQL_DIAG_SOURCE_FUNCTION |
237 | ]; |
238 | foreach ( $diags as $d ) { |
239 | $this->logger->debug( sprintf( "PgSQL ERROR(%d): %s", |
240 | // @phan-suppress-next-line PhanTypeMismatchArgumentInternal |
241 | $d, pg_result_error_field( $this->lastResultHandle, $d ) ) ); |
242 | } |
243 | } |
244 | |
245 | protected function lastInsertId() { |
246 | // Avoid using query() to prevent unwanted side-effects like changing affected |
247 | // row counts or connection retries. Note that lastval() is connection-specific. |
248 | // Note that this causes "lastval is not yet defined in this session" errors if |
249 | // nextval() was never directly or implicitly triggered (error out any transaction). |
250 | $qs = $this->doSingleStatementQuery( "SELECT lastval() AS id" ); |
251 | |
252 | return $qs->res ? (int)$qs->res->fetchRow()['id'] : 0; |
253 | } |
254 | |
255 | public function lastError() { |
256 | if ( $this->conn ) { |
257 | if ( $this->lastResultHandle ) { |
258 | // @phan-suppress-next-line PhanTypeMismatchArgumentInternal |
259 | return pg_result_error( $this->lastResultHandle ); |
260 | } else { |
261 | return pg_last_error() ?: $this->lastConnectError; |
262 | } |
263 | } |
264 | |
265 | return $this->getLastPHPError() ?: 'No database connection'; |
266 | } |
267 | |
268 | public function lastErrno() { |
269 | if ( $this->lastResultHandle ) { |
270 | // @phan-suppress-next-line PhanTypeMismatchArgumentInternal |
271 | $lastErrno = pg_result_error_field( $this->lastResultHandle, PGSQL_DIAG_SQLSTATE ); |
272 | if ( $lastErrno !== false ) { |
273 | return $lastErrno; |
274 | } |
275 | } |
276 | |
277 | return '00000'; |
278 | } |
279 | |
280 | /** |
281 | * Estimate rows in dataset |
282 | * Returns estimated count, based on EXPLAIN output |
283 | * This is not necessarily an accurate estimate, so use sparingly |
284 | * Returns -1 if count cannot be found |
285 | * Takes same arguments as Database::select() |
286 | * |
287 | * @param string $table |
288 | * @param string $var |
289 | * @param string $conds |
290 | * @param string $fname |
291 | * @param array $options |
292 | * @param array $join_conds |
293 | * @return int |
294 | */ |
295 | public function estimateRowCount( $table, $var = '*', $conds = '', |
296 | $fname = __METHOD__, $options = [], $join_conds = [] |
297 | ): int { |
298 | $conds = $this->platform->normalizeConditions( $conds, $fname ); |
299 | $column = $this->platform->extractSingleFieldFromList( $var ); |
300 | if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) { |
301 | $conds[] = "$column IS NOT NULL"; |
302 | } |
303 | |
304 | $options['EXPLAIN'] = true; |
305 | $res = $this->select( $table, $var, $conds, $fname, $options, $join_conds ); |
306 | $rows = -1; |
307 | if ( $res ) { |
308 | $row = $res->fetchRow(); |
309 | $count = []; |
310 | if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) { |
311 | $rows = (int)$count[1]; |
312 | } |
313 | } |
314 | |
315 | return $rows; |
316 | } |
317 | |
318 | public function indexInfo( $table, $index, $fname = __METHOD__ ) { |
319 | $query = new Query( |
320 | "SELECT indexname FROM pg_indexes WHERE tablename='$table'", |
321 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
322 | 'SELECT' |
323 | ); |
324 | $res = $this->query( $query ); |
325 | if ( !$res ) { |
326 | return null; |
327 | } |
328 | foreach ( $res as $row ) { |
329 | if ( $row->indexname == $this->platform->indexName( $index ) ) { |
330 | return $row; |
331 | } |
332 | } |
333 | |
334 | return false; |
335 | } |
336 | |
337 | public function indexAttributes( $index, $schema = false ) { |
338 | if ( $schema === false ) { |
339 | $schemas = $this->getCoreSchemas(); |
340 | } else { |
341 | $schemas = [ $schema ]; |
342 | } |
343 | |
344 | $eindex = $this->addQuotes( $index ); |
345 | |
346 | $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE; |
347 | foreach ( $schemas as $schema ) { |
348 | $eschema = $this->addQuotes( $schema ); |
349 | /* |
350 | * A subquery would be not needed if we didn't care about the order |
351 | * of attributes, but we do |
352 | */ |
353 | $sql = <<<__INDEXATTR__ |
354 | |
355 | SELECT opcname, |
356 | attname, |
357 | i.indoption[s.g] as option, |
358 | pg_am.amname |
359 | FROM |
360 | (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g |
361 | FROM |
362 | pg_index isub |
363 | JOIN pg_class cis |
364 | ON cis.oid=isub.indexrelid |
365 | JOIN pg_namespace ns |
366 | ON cis.relnamespace = ns.oid |
367 | WHERE cis.relname=$eindex AND ns.nspname=$eschema) AS s, |
368 | pg_attribute, |
369 | pg_opclass opcls, |
370 | pg_am, |
371 | pg_class ci |
372 | JOIN pg_index i |
373 | ON ci.oid=i.indexrelid |
374 | JOIN pg_class ct |
375 | ON ct.oid = i.indrelid |
376 | JOIN pg_namespace n |
377 | ON ci.relnamespace = n.oid |
378 | WHERE |
379 | ci.relname=$eindex AND n.nspname=$eschema |
380 | AND attrelid = ct.oid |
381 | AND i.indkey[s.g] = attnum |
382 | AND i.indclass[s.g] = opcls.oid |
383 | AND pg_am.oid = opcls.opcmethod |
384 | __INDEXATTR__; |
385 | $query = new Query( $sql, $flags, 'SELECT' ); |
386 | $res = $this->query( $query, __METHOD__ ); |
387 | $a = []; |
388 | if ( $res ) { |
389 | foreach ( $res as $row ) { |
390 | $a[] = [ |
391 | $row->attname, |
392 | $row->opcname, |
393 | $row->amname, |
394 | $row->option ]; |
395 | } |
396 | return $a; |
397 | } |
398 | } |
399 | return null; |
400 | } |
401 | |
402 | public function indexUnique( $table, $index, $fname = __METHOD__ ) { |
403 | $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" . |
404 | " AND indexdef LIKE 'CREATE UNIQUE%(" . |
405 | $this->strencode( $this->platform->indexName( $index ) ) . |
406 | ")'"; |
407 | $query = new Query( $sql, self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, 'SELECT' ); |
408 | $res = $this->query( $query, $fname ); |
409 | return $res && $res->numRows() > 0; |
410 | } |
411 | |
412 | /** |
413 | * INSERT SELECT wrapper |
414 | * $varMap must be an associative array of the form [ 'dest1' => 'source1', ... ] |
415 | * Source items may be literals rather then field names, but strings should |
416 | * be quoted with Database::addQuotes() |
417 | * $conds may be "*" to copy the whole table |
418 | * srcTable may be an array of tables. |
419 | * @todo FIXME: Implement this a little better (separate select/insert)? |
420 | * |
421 | * @param string $destTable |
422 | * @param array|string $srcTable |
423 | * @param array $varMap |
424 | * @param array $conds |
425 | * @param string $fname |
426 | * @param array $insertOptions |
427 | * @param array $selectOptions |
428 | * @param array $selectJoinConds |
429 | */ |
430 | protected function doInsertSelectNative( |
431 | $destTable, |
432 | $srcTable, |
433 | array $varMap, |
434 | $conds, |
435 | $fname, |
436 | array $insertOptions, |
437 | array $selectOptions, |
438 | $selectJoinConds |
439 | ) { |
440 | if ( in_array( 'IGNORE', $insertOptions ) ) { |
441 | // Use "ON CONFLICT DO" if we have it for IGNORE |
442 | $destTableEnc = $this->tableName( $destTable ); |
443 | |
444 | $selectSql = $this->selectSQLText( |
445 | $srcTable, |
446 | array_values( $varMap ), |
447 | $conds, |
448 | $fname, |
449 | $selectOptions, |
450 | $selectJoinConds |
451 | ); |
452 | |
453 | $sql = "INSERT INTO $destTableEnc (" . implode( ',', array_keys( $varMap ) ) . ') ' . |
454 | $selectSql . ' ON CONFLICT DO NOTHING'; |
455 | $query = new Query( $sql, self::QUERY_CHANGE_ROWS, 'INSERT', $destTable ); |
456 | $this->query( $query, $fname ); |
457 | } else { |
458 | parent::doInsertSelectNative( $destTable, $srcTable, $varMap, $conds, $fname, |
459 | $insertOptions, $selectOptions, $selectJoinConds ); |
460 | } |
461 | } |
462 | |
463 | public function nextSequenceValue( $seqName ) { |
464 | return new NextSequenceValue; |
465 | } |
466 | |
467 | /** |
468 | * @param string $table |
469 | * @return array<string,string> |
470 | */ |
471 | public function getValueTypesForWithClause( $table ) { |
472 | $typesByColumn = []; |
473 | |
474 | $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE; |
475 | $encTable = $this->addQuotes( $table ); |
476 | foreach ( $this->getCoreSchemas() as $schema ) { |
477 | $encSchema = $this->addQuotes( $schema ); |
478 | $sql = "SELECT column_name,udt_name " . |
479 | "FROM information_schema.columns " . |
480 | "WHERE table_name = $encTable AND table_schema = $encSchema"; |
481 | $query = new Query( $sql, $flags, 'SELECT' ); |
482 | $res = $this->query( $query, __METHOD__ ); |
483 | if ( $res->numRows() ) { |
484 | foreach ( $res as $row ) { |
485 | $typesByColumn[$row->column_name] = $row->udt_name; |
486 | } |
487 | break; |
488 | } |
489 | } |
490 | |
491 | return $typesByColumn; |
492 | } |
493 | |
494 | public function textFieldSize( $table, $field ) { |
495 | wfDeprecated( __METHOD__, '1.43' ); |
496 | $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE; |
497 | $encTable = $this->tableName( $table ); |
498 | $sql = "SELECT t.typname as ftype,a.atttypmod as size |
499 | FROM pg_class c, pg_attribute a, pg_type t |
500 | WHERE relname='$encTable' AND a.attrelid=c.oid AND |
501 | a.atttypid=t.oid and a.attname='$field'"; |
502 | $query = new Query( $sql, $flags, 'SELECT' ); |
503 | $res = $this->query( $query, __METHOD__ ); |
504 | $row = $res->fetchObject(); |
505 | if ( $row->ftype == 'varchar' ) { |
506 | $size = $row->size - 4; |
507 | } else { |
508 | $size = $row->size; |
509 | } |
510 | |
511 | return $size; |
512 | } |
513 | |
514 | public function wasDeadlock() { |
515 | // https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html |
516 | return $this->lastErrno() === '40P01'; |
517 | } |
518 | |
519 | protected function isConnectionError( $errno ) { |
520 | // https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html |
521 | static $codes = [ '08000', '08003', '08006', '08001', '08004', '57P01', '57P03', '53300' ]; |
522 | |
523 | return in_array( $errno, $codes, true ); |
524 | } |
525 | |
526 | protected function isQueryTimeoutError( $errno ) { |
527 | // https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html |
528 | return ( $errno === '57014' ); |
529 | } |
530 | |
531 | protected function isKnownStatementRollbackError( $errno ) { |
532 | return false; // transaction has to be rolled-back from error state |
533 | } |
534 | |
535 | public function duplicateTableStructure( |
536 | $oldName, $newName, $temporary = false, $fname = __METHOD__ |
537 | ) { |
538 | $newNameE = $this->platform->addIdentifierQuotes( $newName ); |
539 | $oldNameE = $this->platform->addIdentifierQuotes( $oldName ); |
540 | |
541 | $temporary = $temporary ? 'TEMPORARY' : ''; |
542 | $query = new Query( |
543 | "CREATE $temporary TABLE $newNameE " . |
544 | "(LIKE $oldNameE INCLUDING DEFAULTS INCLUDING INDEXES)", |
545 | self::QUERY_PSEUDO_PERMANENT | self::QUERY_CHANGE_SCHEMA, |
546 | $temporary ? 'CREATE TEMPORARY' : 'CREATE', |
547 | // Use a dot to avoid double-prefixing in Database::getTempTableWrites() |
548 | '.' . $newName |
549 | ); |
550 | $ret = $this->query( $query, $fname ); |
551 | if ( !$ret ) { |
552 | return $ret; |
553 | } |
554 | |
555 | $sql = 'SELECT attname FROM pg_class c' |
556 | . ' JOIN pg_namespace n ON (n.oid = c.relnamespace)' |
557 | . ' JOIN pg_attribute a ON (a.attrelid = c.oid)' |
558 | . ' JOIN pg_attrdef d ON (c.oid=d.adrelid and a.attnum=d.adnum)' |
559 | . ' WHERE relkind = \'r\'' |
560 | . ' AND nspname = ' . $this->addQuotes( $this->getCoreSchema() ) |
561 | . ' AND relname = ' . $this->addQuotes( $oldName ) |
562 | . ' AND pg_get_expr(adbin, adrelid) LIKE \'nextval(%\''; |
563 | $query = new Query( |
564 | $sql, |
565 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
566 | 'SELECT' |
567 | ); |
568 | |
569 | $res = $this->query( $query, $fname ); |
570 | $row = $res->fetchObject(); |
571 | if ( $row ) { |
572 | $field = $row->attname; |
573 | $newSeq = "{$newName}_{$field}_seq"; |
574 | $fieldE = $this->platform->addIdentifierQuotes( $field ); |
575 | $newSeqE = $this->platform->addIdentifierQuotes( $newSeq ); |
576 | $newSeqQ = $this->addQuotes( $newSeq ); |
577 | $query = new Query( |
578 | "CREATE $temporary SEQUENCE $newSeqE OWNED BY $newNameE.$fieldE", |
579 | self::QUERY_CHANGE_SCHEMA, |
580 | 'CREATE', |
581 | // Do not treat this is as a table modification on top of the CREATE above. |
582 | null |
583 | ); |
584 | $this->query( $query, $fname ); |
585 | $query = new Query( |
586 | "ALTER TABLE $newNameE ALTER COLUMN $fieldE SET DEFAULT nextval({$newSeqQ}::regclass)", |
587 | self::QUERY_CHANGE_SCHEMA, |
588 | 'ALTER', |
589 | // Do not treat this is as a table modification on top of the CREATE above. |
590 | null |
591 | ); |
592 | $this->query( $query, $fname ); |
593 | } |
594 | |
595 | return $ret; |
596 | } |
597 | |
598 | public function truncateTable( $table, $fname = __METHOD__ ) { |
599 | $sql = "TRUNCATE TABLE " . $this->tableName( $table ) . " RESTART IDENTITY"; |
600 | $query = new Query( $sql, self::QUERY_CHANGE_SCHEMA, 'TRUNCATE', $table ); |
601 | $this->query( $query, $fname ); |
602 | } |
603 | |
604 | /** |
605 | * @param string $prefix Only show tables with this prefix, e.g. mw_ |
606 | * @param string $fname Calling function name |
607 | * @return string[] |
608 | * @suppress SecurityCheck-SQLInjection array_map not recognized T204911 |
609 | */ |
610 | public function listTables( $prefix = '', $fname = __METHOD__ ) { |
611 | $eschemas = implode( ',', array_map( [ $this, 'addQuotes' ], $this->getCoreSchemas() ) ); |
612 | $query = new Query( |
613 | "SELECT DISTINCT tablename FROM pg_tables WHERE schemaname IN ($eschemas)", |
614 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
615 | 'SELECT' |
616 | ); |
617 | $result = $this->query( $query, $fname ); |
618 | $endArray = []; |
619 | |
620 | foreach ( $result as $table ) { |
621 | $vars = get_object_vars( $table ); |
622 | $table = array_pop( $vars ); |
623 | if ( $prefix == '' || strpos( $table, $prefix ) === 0 ) { |
624 | $endArray[] = $table; |
625 | } |
626 | } |
627 | |
628 | return $endArray; |
629 | } |
630 | |
631 | /** |
632 | * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12 |
633 | * to https://www.php.net/manual/en/ref.pgsql.php |
634 | * |
635 | * Parsing a postgres array can be a tricky problem, he's my |
636 | * take on this, it handles multi-dimensional arrays plus |
637 | * escaping using a nasty regexp to determine the limits of each |
638 | * data-item. |
639 | * |
640 | * This should really be handled by PHP PostgreSQL module |
641 | * |
642 | * @since 1.19 |
643 | * @param string $text Postgreql array returned in a text form like {a,b} |
644 | * @param string[] &$output |
645 | * @param int|false $limit |
646 | * @param int $offset |
647 | * @return string[] |
648 | */ |
649 | private function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) { |
650 | if ( $limit === false ) { |
651 | $limit = strlen( $text ) - 1; |
652 | $output = []; |
653 | } |
654 | if ( $text == '{}' ) { |
655 | return $output; |
656 | } |
657 | do { |
658 | if ( $text[$offset] != '{' ) { |
659 | preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/", |
660 | $text, $match, 0, $offset ); |
661 | $offset += strlen( $match[0] ); |
662 | $output[] = ( $match[1][0] != '"' |
663 | ? $match[1] |
664 | : stripcslashes( substr( $match[1], 1, -1 ) ) ); |
665 | if ( $match[3] == '},' ) { |
666 | return $output; |
667 | } |
668 | } else { |
669 | $offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 ); |
670 | } |
671 | } while ( $limit > $offset ); |
672 | |
673 | return $output; |
674 | } |
675 | |
676 | public function getSoftwareLink() { |
677 | return '[{{int:version-db-postgres-url}} PostgreSQL]'; |
678 | } |
679 | |
680 | /** |
681 | * Return current schema (executes SELECT current_schema()) |
682 | * Needs transaction |
683 | * |
684 | * @since 1.19 |
685 | * @return string Default schema for the current session |
686 | */ |
687 | public function getCurrentSchema() { |
688 | $query = new Query( |
689 | "SELECT current_schema()", |
690 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
691 | 'SELECT' |
692 | ); |
693 | $res = $this->query( $query, __METHOD__ ); |
694 | $row = $res->fetchRow(); |
695 | |
696 | return $row[0]; |
697 | } |
698 | |
699 | /** |
700 | * Return list of schemas which are accessible without schema name |
701 | * This is list does not contain magic keywords like "$user" |
702 | * Needs transaction |
703 | * |
704 | * @see getSearchPath() |
705 | * @see setSearchPath() |
706 | * @since 1.19 |
707 | * @return array List of actual schemas for the current session |
708 | */ |
709 | public function getSchemas() { |
710 | $query = new Query( |
711 | "SELECT current_schemas(false)", |
712 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
713 | 'SELECT' |
714 | ); |
715 | $res = $this->query( $query, __METHOD__ ); |
716 | $row = $res->fetchRow(); |
717 | $schemas = []; |
718 | |
719 | /* PHP pgsql support does not support array type, "{a,b}" string is returned */ |
720 | |
721 | return $this->pg_array_parse( $row[0], $schemas ); |
722 | } |
723 | |
724 | /** |
725 | * Return search patch for schemas |
726 | * This is different from getSchemas() since it contain magic keywords |
727 | * (like "$user"). |
728 | * Needs transaction |
729 | * |
730 | * @since 1.19 |
731 | * @return array How to search for table names schemas for the current user |
732 | */ |
733 | public function getSearchPath() { |
734 | $query = new Query( |
735 | "SHOW search_path", |
736 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
737 | 'SHOW' |
738 | ); |
739 | $res = $this->query( $query, __METHOD__ ); |
740 | $row = $res->fetchRow(); |
741 | |
742 | /* PostgreSQL returns SHOW values as strings */ |
743 | |
744 | return explode( ",", $row[0] ); |
745 | } |
746 | |
747 | /** |
748 | * Update search_path, values should already be sanitized |
749 | * Values may contain magic keywords like "$user" |
750 | * @since 1.19 |
751 | * |
752 | * @param string[] $search_path List of schemas to be searched by default |
753 | */ |
754 | private function setSearchPath( $search_path ) { |
755 | $query = new Query( |
756 | "SET search_path = " . implode( ", ", $search_path ), |
757 | self::QUERY_CHANGE_TRX, |
758 | 'SET' |
759 | ); |
760 | $this->query( $query, __METHOD__ ); |
761 | } |
762 | |
763 | /** |
764 | * Determine default schema for the current application |
765 | * Adjust this session schema search path if desired schema exists |
766 | * and is not already there. |
767 | * |
768 | * We need to have name of the core schema stored to be able |
769 | * to query database metadata. |
770 | * |
771 | * This will be also called by the installer after the schema is created |
772 | * |
773 | * @since 1.19 |
774 | * |
775 | * @param string|null $desiredSchema |
776 | */ |
777 | public function determineCoreSchema( $desiredSchema ) { |
778 | if ( $this->trxLevel() ) { |
779 | // We do not want the schema selection to change on ROLLBACK or INSERT SELECT. |
780 | // See https://www.postgresql.org/docs/8.3/sql-set.html |
781 | throw new DBUnexpectedError( |
782 | $this, |
783 | __METHOD__ . ": a transaction is currently active" |
784 | ); |
785 | } |
786 | |
787 | if ( $this->schemaExists( $desiredSchema ) ) { |
788 | if ( in_array( $desiredSchema, $this->getSchemas() ) ) { |
789 | $this->platform->setCoreSchema( $desiredSchema ); |
790 | $this->logger->debug( |
791 | "Schema \"" . $desiredSchema . "\" already in the search path\n" ); |
792 | } else { |
793 | // Prepend the desired schema to the search path (T17816) |
794 | $search_path = $this->getSearchPath(); |
795 | array_unshift( $search_path, $this->platform->addIdentifierQuotes( $desiredSchema ) ); |
796 | $this->setSearchPath( $search_path ); |
797 | $this->platform->setCoreSchema( $desiredSchema ); |
798 | $this->logger->debug( |
799 | "Schema \"" . $desiredSchema . "\" added to the search path\n" ); |
800 | } |
801 | } else { |
802 | $this->platform->setCoreSchema( $this->getCurrentSchema() ); |
803 | $this->logger->debug( |
804 | "Schema \"" . $desiredSchema . "\" not found, using current \"" . |
805 | $this->getCoreSchema() . "\"\n" ); |
806 | } |
807 | } |
808 | |
809 | /** |
810 | * Return schema name for core application tables |
811 | * |
812 | * @since 1.19 |
813 | * @return string Core schema name |
814 | */ |
815 | public function getCoreSchema() { |
816 | return $this->platform->getCoreSchema(); |
817 | } |
818 | |
819 | /** |
820 | * Return schema names for temporary tables and core application tables |
821 | * |
822 | * @since 1.31 |
823 | * @return string[] schema names |
824 | */ |
825 | public function getCoreSchemas() { |
826 | if ( $this->tempSchema ) { |
827 | return [ $this->tempSchema, $this->getCoreSchema() ]; |
828 | } |
829 | $query = new Query( |
830 | "SELECT nspname FROM pg_catalog.pg_namespace n WHERE n.oid = pg_my_temp_schema()", |
831 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
832 | 'SELECT' |
833 | ); |
834 | $res = $this->query( $query, __METHOD__ ); |
835 | $row = $res->fetchObject(); |
836 | if ( $row ) { |
837 | $this->tempSchema = $row->nspname; |
838 | return [ $this->tempSchema, $this->getCoreSchema() ]; |
839 | } |
840 | |
841 | return [ $this->getCoreSchema() ]; |
842 | } |
843 | |
844 | public function getServerVersion() { |
845 | if ( !isset( $this->numericVersion ) ) { |
846 | // Works on PG 7.4+ |
847 | $this->numericVersion = pg_version( $this->getBindingHandle() )['server']; |
848 | } |
849 | |
850 | return $this->numericVersion; |
851 | } |
852 | |
853 | /** |
854 | * Query whether a given relation exists (in the given schema, or the |
855 | * default mw one if not given) |
856 | * @param string $table |
857 | * @param array|string $types |
858 | * @param bool|string $schema |
859 | * @return bool |
860 | */ |
861 | private function relationExists( $table, $types, $schema = false ) { |
862 | if ( !is_array( $types ) ) { |
863 | $types = [ $types ]; |
864 | } |
865 | if ( $schema === false ) { |
866 | $schemas = $this->getCoreSchemas(); |
867 | } else { |
868 | $schemas = [ $schema ]; |
869 | } |
870 | $table = $this->tableName( $table, 'raw' ); |
871 | $etable = $this->addQuotes( $table ); |
872 | foreach ( $schemas as $schema ) { |
873 | $eschema = $this->addQuotes( $schema ); |
874 | $sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n " |
875 | . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema " |
876 | . "AND c.relkind IN ('" . implode( "','", $types ) . "')"; |
877 | $query = new Query( |
878 | $sql, |
879 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
880 | 'SELECT' |
881 | ); |
882 | $res = $this->query( $query, __METHOD__ ); |
883 | if ( $res && $res->numRows() ) { |
884 | return true; |
885 | } |
886 | } |
887 | |
888 | return false; |
889 | } |
890 | |
891 | /** |
892 | * For backward compatibility, this function checks both tables and views. |
893 | * @param string $table |
894 | * @param string $fname |
895 | * @param bool|string $schema |
896 | * @return bool |
897 | */ |
898 | public function tableExists( $table, $fname = __METHOD__, $schema = false ) { |
899 | return $this->relationExists( $table, [ 'r', 'v' ], $schema ); |
900 | } |
901 | |
902 | public function sequenceExists( $sequence, $schema = false ) { |
903 | return $this->relationExists( $sequence, 'S', $schema ); |
904 | } |
905 | |
906 | public function constraintExists( $table, $constraint ) { |
907 | foreach ( $this->getCoreSchemas() as $schema ) { |
908 | $sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " . |
909 | "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s", |
910 | $this->addQuotes( $schema ), |
911 | $this->addQuotes( $table ), |
912 | $this->addQuotes( $constraint ) |
913 | ); |
914 | $query = new Query( |
915 | $sql, |
916 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
917 | 'SELECT' |
918 | ); |
919 | $res = $this->query( $query, __METHOD__ ); |
920 | if ( $res && $res->numRows() ) { |
921 | return true; |
922 | } |
923 | } |
924 | return false; |
925 | } |
926 | |
927 | /** |
928 | * Query whether a given schema exists. Returns true if it does, false if it doesn't. |
929 | * @param string|null $schema |
930 | * @return bool |
931 | */ |
932 | public function schemaExists( $schema ) { |
933 | if ( !strlen( $schema ?? '' ) ) { |
934 | return false; // short-circuit |
935 | } |
936 | $query = new Query( |
937 | "SELECT 1 FROM pg_catalog.pg_namespace " . |
938 | "WHERE nspname = " . $this->addQuotes( $schema ) . " LIMIT 1", |
939 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
940 | 'SELECT' |
941 | ); |
942 | $res = $this->query( $query, __METHOD__ ); |
943 | |
944 | return ( $res->numRows() > 0 ); |
945 | } |
946 | |
947 | /** |
948 | * Returns true if a given role (i.e. user) exists, false otherwise. |
949 | * @param string $roleName |
950 | * @return bool |
951 | */ |
952 | public function roleExists( $roleName ) { |
953 | $query = new Query( |
954 | "SELECT 1 FROM pg_catalog.pg_roles " . |
955 | "WHERE rolname = " . $this->addQuotes( $roleName ) . " LIMIT 1", |
956 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
957 | 'SELECT' |
958 | ); |
959 | $res = $this->query( $query, __METHOD__ ); |
960 | |
961 | return ( $res->numRows() > 0 ); |
962 | } |
963 | |
964 | /** |
965 | * @param string $table |
966 | * @param string $field |
967 | * @return PostgresField|null |
968 | */ |
969 | public function fieldInfo( $table, $field ) { |
970 | return PostgresField::fromText( $this, $table, $field ); |
971 | } |
972 | |
973 | public function encodeBlob( $b ) { |
974 | $conn = $this->getBindingHandle(); |
975 | |
976 | return new PostgresBlob( pg_escape_bytea( $conn, $b ) ); |
977 | } |
978 | |
979 | public function decodeBlob( $b ) { |
980 | if ( $b instanceof PostgresBlob ) { |
981 | $b = $b->fetch(); |
982 | } elseif ( $b instanceof Blob ) { |
983 | return $b->fetch(); |
984 | } |
985 | |
986 | return pg_unescape_bytea( $b ); |
987 | } |
988 | |
989 | public function strencode( $s ) { |
990 | // Should not be called by us |
991 | return pg_escape_string( $this->getBindingHandle(), (string)$s ); |
992 | } |
993 | |
994 | public function addQuotes( $s ) { |
995 | $conn = $this->getBindingHandle(); |
996 | |
997 | if ( $s === null ) { |
998 | return 'NULL'; |
999 | } elseif ( is_bool( $s ) ) { |
1000 | return (string)intval( $s ); |
1001 | } elseif ( is_int( $s ) ) { |
1002 | return (string)$s; |
1003 | } elseif ( $s instanceof Blob ) { |
1004 | if ( $s instanceof PostgresBlob ) { |
1005 | $s = $s->fetch(); |
1006 | } else { |
1007 | $s = pg_escape_bytea( $conn, $s->fetch() ); |
1008 | } |
1009 | return "'$s'"; |
1010 | } elseif ( $s instanceof NextSequenceValue ) { |
1011 | return 'DEFAULT'; |
1012 | } |
1013 | |
1014 | return "'" . pg_escape_string( $conn, (string)$s ) . "'"; |
1015 | } |
1016 | |
1017 | public function streamStatementEnd( &$sql, &$newLine ) { |
1018 | # Allow dollar quoting for function declarations |
1019 | if ( str_starts_with( $newLine, '$mw$' ) ) { |
1020 | if ( $this->delimiter ) { |
1021 | $this->delimiter = false; |
1022 | } else { |
1023 | $this->delimiter = ';'; |
1024 | } |
1025 | } |
1026 | |
1027 | return parent::streamStatementEnd( $sql, $newLine ); |
1028 | } |
1029 | |
1030 | public function doLockIsFree( string $lockName, string $method ) { |
1031 | $query = new Query( |
1032 | $this->platform->lockIsFreeSQLText( $lockName ), |
1033 | self::QUERY_CHANGE_LOCKS, |
1034 | 'SELECT' |
1035 | ); |
1036 | $res = $this->query( $query, $method ); |
1037 | $row = $res->fetchObject(); |
1038 | |
1039 | return (bool)$row->unlocked; |
1040 | } |
1041 | |
1042 | public function doLock( string $lockName, string $method, int $timeout ) { |
1043 | $query = new Query( |
1044 | $this->platform->lockSQLText( $lockName, $timeout ), |
1045 | self::QUERY_CHANGE_LOCKS, |
1046 | 'SELECT' |
1047 | ); |
1048 | |
1049 | $acquired = null; |
1050 | $loop = new WaitConditionLoop( |
1051 | function () use ( $query, $method, &$acquired ) { |
1052 | $res = $this->query( $query, $method ); |
1053 | $row = $res->fetchObject(); |
1054 | |
1055 | if ( $row->acquired !== null ) { |
1056 | $acquired = (float)$row->acquired; |
1057 | |
1058 | return WaitConditionLoop::CONDITION_REACHED; |
1059 | } |
1060 | |
1061 | return WaitConditionLoop::CONDITION_CONTINUE; |
1062 | }, |
1063 | $timeout |
1064 | ); |
1065 | $loop->invoke(); |
1066 | |
1067 | return $acquired; |
1068 | } |
1069 | |
1070 | public function doUnlock( string $lockName, string $method ) { |
1071 | $query = new Query( |
1072 | $this->platform->unlockSQLText( $lockName ), |
1073 | self::QUERY_CHANGE_LOCKS, |
1074 | 'SELECT' |
1075 | ); |
1076 | $result = $this->query( $query, $method ); |
1077 | $row = $result->fetchObject(); |
1078 | |
1079 | return (bool)$row->released; |
1080 | } |
1081 | |
1082 | protected function doFlushSession( $fname ) { |
1083 | $flags = self::QUERY_CHANGE_LOCKS | self::QUERY_NO_RETRY; |
1084 | |
1085 | // https://www.postgresql.org/docs/9.1/functions-admin.html |
1086 | $sql = "SELECT pg_advisory_unlock_all()"; |
1087 | $query = new Query( $sql, $flags, 'UNLOCK' ); |
1088 | $qs = $this->executeQuery( $query, __METHOD__, $flags ); |
1089 | if ( $qs->res === false ) { |
1090 | $this->reportQueryError( $qs->message, $qs->code, $sql, $fname, true ); |
1091 | } |
1092 | } |
1093 | |
1094 | public function serverIsReadOnly() { |
1095 | $query = new Query( |
1096 | "SHOW default_transaction_read_only", |
1097 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
1098 | 'SHOW' |
1099 | ); |
1100 | $res = $this->query( $query, __METHOD__ ); |
1101 | $row = $res->fetchObject(); |
1102 | |
1103 | return $row && strtolower( $row->default_transaction_read_only ) === 'on'; |
1104 | } |
1105 | |
1106 | protected function getInsertIdColumnForUpsert( $table ) { |
1107 | $column = null; |
1108 | |
1109 | $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE; |
1110 | $encTable = $this->addQuotes( $this->tableName( $table, 'raw' ) ); |
1111 | foreach ( $this->getCoreSchemas() as $schema ) { |
1112 | $encSchema = $this->addQuotes( $schema ); |
1113 | $query = new Query( |
1114 | "SELECT column_name,data_type,column_default " . |
1115 | "FROM information_schema.columns " . |
1116 | "WHERE table_name = $encTable AND table_schema = $encSchema", |
1117 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
1118 | 'SELECT' |
1119 | ); |
1120 | $res = $this->query( $query, __METHOD__ ); |
1121 | if ( $res->numRows() ) { |
1122 | foreach ( $res as $row ) { |
1123 | if ( |
1124 | $row->column_default !== null && |
1125 | str_starts_with( $row->column_default, "nextval(" ) && |
1126 | in_array( $row->data_type, [ 'integer', 'bigint' ], true ) |
1127 | ) { |
1128 | $column = $row->column_name; |
1129 | } |
1130 | } |
1131 | break; |
1132 | } |
1133 | } |
1134 | |
1135 | return $column; |
1136 | } |
1137 | |
1138 | public static function getAttributes() { |
1139 | return [ self::ATTR_SCHEMAS_AS_TABLE_GROUPS => true ]; |
1140 | } |
1141 | } |