Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 528 |
|
0.00% |
0 / 52 |
CRAP | |
0.00% |
0 / 1 |
DatabasePostgres | |
0.00% |
0 / 528 |
|
0.00% |
0 / 52 |
20306 | |
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 / 21 |
|
0.00% |
0 / 1 |
20 | |||
indexAttributes | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
30 | |||
doInsertSelectNative | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
getValueTypesForWithClause | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
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 / 19 |
|
0.00% |
0 / 1 |
30 | |||
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 / 22 |
|
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( $domain ); |
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 | // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition |
199 | while ( $priorRes = pg_get_result( $conn ) ) { |
200 | pg_free_result( $priorRes ); |
201 | } |
202 | |
203 | if ( pg_send_query( $conn, $sql ) === false ) { |
204 | throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" ); |
205 | } |
206 | |
207 | // Newer PHP versions use PgSql\Result instead of resource variables |
208 | // https://www.php.net/manual/en/function.pg-get-result.php |
209 | $pgRes = pg_get_result( $conn ); |
210 | // Phan on PHP 7.4 and PHP 8.1 need different suppressions |
211 | // @phan-suppress-next-line PhanTypeMismatchProperty,PhanTypeMismatchPropertyProbablyReal |
212 | $this->lastResultHandle = $pgRes; |
213 | $res = pg_result_error( $pgRes ) ? false : $pgRes; |
214 | |
215 | return new QueryStatus( |
216 | // @phan-suppress-next-line PhanTypeMismatchArgument |
217 | is_bool( $res ) ? $res : new PostgresResultWrapper( $this, $conn, $res ), |
218 | $pgRes ? pg_affected_rows( $pgRes ) : 0, |
219 | $this->lastError(), |
220 | $this->lastErrno() |
221 | ); |
222 | } |
223 | |
224 | protected function dumpError() { |
225 | $diags = [ |
226 | PGSQL_DIAG_SEVERITY, |
227 | PGSQL_DIAG_SQLSTATE, |
228 | PGSQL_DIAG_MESSAGE_PRIMARY, |
229 | PGSQL_DIAG_MESSAGE_DETAIL, |
230 | PGSQL_DIAG_MESSAGE_HINT, |
231 | PGSQL_DIAG_STATEMENT_POSITION, |
232 | PGSQL_DIAG_INTERNAL_POSITION, |
233 | PGSQL_DIAG_INTERNAL_QUERY, |
234 | PGSQL_DIAG_CONTEXT, |
235 | PGSQL_DIAG_SOURCE_FILE, |
236 | PGSQL_DIAG_SOURCE_LINE, |
237 | PGSQL_DIAG_SOURCE_FUNCTION |
238 | ]; |
239 | foreach ( $diags as $d ) { |
240 | $this->logger->debug( sprintf( "PgSQL ERROR(%d): %s", |
241 | // @phan-suppress-next-line PhanTypeMismatchArgumentInternal |
242 | $d, pg_result_error_field( $this->lastResultHandle, $d ) ) ); |
243 | } |
244 | } |
245 | |
246 | protected function lastInsertId() { |
247 | // Avoid using query() to prevent unwanted side-effects like changing affected |
248 | // row counts or connection retries. Note that lastval() is connection-specific. |
249 | // Note that this causes "lastval is not yet defined in this session" errors if |
250 | // nextval() was never directly or implicitly triggered (error out any transaction). |
251 | $qs = $this->doSingleStatementQuery( "SELECT lastval() AS id" ); |
252 | |
253 | return $qs->res ? (int)$qs->res->fetchRow()['id'] : 0; |
254 | } |
255 | |
256 | public function lastError() { |
257 | if ( $this->conn ) { |
258 | if ( $this->lastResultHandle ) { |
259 | // @phan-suppress-next-line PhanTypeMismatchArgumentInternal |
260 | return pg_result_error( $this->lastResultHandle ); |
261 | } else { |
262 | return pg_last_error() ?: $this->lastConnectError; |
263 | } |
264 | } |
265 | |
266 | return $this->getLastPHPError() ?: 'No database connection'; |
267 | } |
268 | |
269 | public function lastErrno() { |
270 | if ( $this->lastResultHandle ) { |
271 | // @phan-suppress-next-line PhanTypeMismatchArgumentInternal |
272 | $lastErrno = pg_result_error_field( $this->lastResultHandle, PGSQL_DIAG_SQLSTATE ); |
273 | if ( $lastErrno !== false ) { |
274 | return $lastErrno; |
275 | } |
276 | } |
277 | |
278 | return '00000'; |
279 | } |
280 | |
281 | public function estimateRowCount( $table, $var = '*', $conds = '', |
282 | $fname = __METHOD__, $options = [], $join_conds = [] |
283 | ): int { |
284 | $conds = $this->platform->normalizeConditions( $conds, $fname ); |
285 | $column = $this->platform->extractSingleFieldFromList( $var ); |
286 | if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) { |
287 | $conds[] = "$column IS NOT NULL"; |
288 | } |
289 | |
290 | $options['EXPLAIN'] = true; |
291 | $res = $this->select( $table, $var, $conds, $fname, $options, $join_conds ); |
292 | $rows = -1; |
293 | if ( $res ) { |
294 | $row = $res->fetchRow(); |
295 | $count = []; |
296 | if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) { |
297 | $rows = (int)$count[1]; |
298 | } |
299 | } |
300 | |
301 | return $rows; |
302 | } |
303 | |
304 | public function indexInfo( $table, $index, $fname = __METHOD__ ) { |
305 | $components = $this->platform->qualifiedTableComponents( $table ); |
306 | if ( count( $components ) === 1 ) { |
307 | $schema = $this->getCoreSchema(); |
308 | $tableComponent = $components[0]; |
309 | } elseif ( count( $components ) === 2 ) { |
310 | [ $schema, $tableComponent ] = $components; |
311 | } else { |
312 | [ , $schema, $tableComponent ] = $components; |
313 | } |
314 | $encSchema = $this->addQuotes( $schema ); |
315 | $encTable = $this->addQuotes( $tableComponent ); |
316 | $encIndex = $this->addQuotes( $this->platform->indexName( $index ) ); |
317 | $query = new Query( |
318 | "SELECT indexname,indexdef FROM pg_indexes " . |
319 | "WHERE schemaname=$encSchema AND tablename=$encTable AND indexname=$encIndex", |
320 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
321 | 'SELECT' |
322 | ); |
323 | $res = $this->query( $query ); |
324 | $row = $res->fetchObject(); |
325 | |
326 | if ( $row ) { |
327 | return [ 'unique' => ( strpos( $row->indexdef, 'CREATE UNIQUE ' ) === 0 ) ]; |
328 | } |
329 | |
330 | return false; |
331 | } |
332 | |
333 | public function indexAttributes( $index, $schema = false ) { |
334 | if ( $schema === false ) { |
335 | $schemas = $this->getCoreSchemas(); |
336 | } else { |
337 | $schemas = [ $schema ]; |
338 | } |
339 | |
340 | $eindex = $this->addQuotes( $index ); |
341 | |
342 | $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE; |
343 | foreach ( $schemas as $schema ) { |
344 | $eschema = $this->addQuotes( $schema ); |
345 | /* |
346 | * A subquery would be not needed if we didn't care about the order |
347 | * of attributes, but we do |
348 | */ |
349 | $sql = <<<__INDEXATTR__ |
350 | |
351 | SELECT opcname, |
352 | attname, |
353 | i.indoption[s.g] as option, |
354 | pg_am.amname |
355 | FROM |
356 | (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g |
357 | FROM |
358 | pg_index isub |
359 | JOIN pg_class cis |
360 | ON cis.oid=isub.indexrelid |
361 | JOIN pg_namespace ns |
362 | ON cis.relnamespace = ns.oid |
363 | WHERE cis.relname=$eindex AND ns.nspname=$eschema) AS s, |
364 | pg_attribute, |
365 | pg_opclass opcls, |
366 | pg_am, |
367 | pg_class ci |
368 | JOIN pg_index i |
369 | ON ci.oid=i.indexrelid |
370 | JOIN pg_class ct |
371 | ON ct.oid = i.indrelid |
372 | JOIN pg_namespace n |
373 | ON ci.relnamespace = n.oid |
374 | WHERE |
375 | ci.relname=$eindex AND n.nspname=$eschema |
376 | AND attrelid = ct.oid |
377 | AND i.indkey[s.g] = attnum |
378 | AND i.indclass[s.g] = opcls.oid |
379 | AND pg_am.oid = opcls.opcmethod |
380 | __INDEXATTR__; |
381 | $query = new Query( $sql, $flags, 'SELECT' ); |
382 | $res = $this->query( $query, __METHOD__ ); |
383 | $a = []; |
384 | if ( $res ) { |
385 | foreach ( $res as $row ) { |
386 | $a[] = [ |
387 | $row->attname, |
388 | $row->opcname, |
389 | $row->amname, |
390 | $row->option ]; |
391 | } |
392 | return $a; |
393 | } |
394 | } |
395 | return null; |
396 | } |
397 | |
398 | protected function doInsertSelectNative( |
399 | $destTable, |
400 | $srcTable, |
401 | array $varMap, |
402 | $conds, |
403 | $fname, |
404 | array $insertOptions, |
405 | array $selectOptions, |
406 | $selectJoinConds |
407 | ) { |
408 | if ( in_array( 'IGNORE', $insertOptions ) ) { |
409 | // Use "ON CONFLICT DO" if we have it for IGNORE |
410 | $destTableEnc = $this->tableName( $destTable ); |
411 | |
412 | $selectSql = $this->selectSQLText( |
413 | $srcTable, |
414 | array_values( $varMap ), |
415 | $conds, |
416 | $fname, |
417 | $selectOptions, |
418 | $selectJoinConds |
419 | ); |
420 | |
421 | $sql = "INSERT INTO $destTableEnc (" . implode( ',', array_keys( $varMap ) ) . ') ' . |
422 | $selectSql . ' ON CONFLICT DO NOTHING'; |
423 | $query = new Query( $sql, self::QUERY_CHANGE_ROWS, 'INSERT', $destTable ); |
424 | $this->query( $query, $fname ); |
425 | } else { |
426 | parent::doInsertSelectNative( $destTable, $srcTable, $varMap, $conds, $fname, |
427 | $insertOptions, $selectOptions, $selectJoinConds ); |
428 | } |
429 | } |
430 | |
431 | public function getValueTypesForWithClause( $table ) { |
432 | $typesByColumn = []; |
433 | |
434 | $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE; |
435 | $encTable = $this->addQuotes( $table ); |
436 | foreach ( $this->getCoreSchemas() as $schema ) { |
437 | $encSchema = $this->addQuotes( $schema ); |
438 | $sql = "SELECT column_name,udt_name " . |
439 | "FROM information_schema.columns " . |
440 | "WHERE table_name = $encTable AND table_schema = $encSchema"; |
441 | $query = new Query( $sql, $flags, 'SELECT' ); |
442 | $res = $this->query( $query, __METHOD__ ); |
443 | if ( $res->numRows() ) { |
444 | foreach ( $res as $row ) { |
445 | $typesByColumn[$row->column_name] = $row->udt_name; |
446 | } |
447 | break; |
448 | } |
449 | } |
450 | |
451 | return $typesByColumn; |
452 | } |
453 | |
454 | protected function isConnectionError( $errno ) { |
455 | // https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html |
456 | static $codes = [ '08000', '08003', '08006', '08001', '08004', '57P01', '57P03', '53300' ]; |
457 | |
458 | return in_array( $errno, $codes, true ); |
459 | } |
460 | |
461 | protected function isQueryTimeoutError( $errno ) { |
462 | // https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html |
463 | return ( $errno === '57014' ); |
464 | } |
465 | |
466 | protected function isKnownStatementRollbackError( $errno ) { |
467 | return false; // transaction has to be rolled-back from error state |
468 | } |
469 | |
470 | public function duplicateTableStructure( |
471 | $oldName, $newName, $temporary = false, $fname = __METHOD__ |
472 | ) { |
473 | $newNameE = $this->platform->addIdentifierQuotes( $newName ); |
474 | $oldNameE = $this->platform->addIdentifierQuotes( $oldName ); |
475 | |
476 | $temporary = $temporary ? 'TEMPORARY' : ''; |
477 | $query = new Query( |
478 | "CREATE $temporary TABLE $newNameE " . |
479 | "(LIKE $oldNameE INCLUDING DEFAULTS INCLUDING INDEXES)", |
480 | self::QUERY_PSEUDO_PERMANENT | self::QUERY_CHANGE_SCHEMA, |
481 | $temporary ? 'CREATE TEMPORARY' : 'CREATE', |
482 | // Use a dot to avoid double-prefixing in Database::getTempTableWrites() |
483 | '.' . $newName |
484 | ); |
485 | $ret = $this->query( $query, $fname ); |
486 | if ( !$ret ) { |
487 | return $ret; |
488 | } |
489 | |
490 | $sql = 'SELECT attname FROM pg_class c' |
491 | . ' JOIN pg_namespace n ON (n.oid = c.relnamespace)' |
492 | . ' JOIN pg_attribute a ON (a.attrelid = c.oid)' |
493 | . ' JOIN pg_attrdef d ON (c.oid=d.adrelid and a.attnum=d.adnum)' |
494 | . ' WHERE relkind = \'r\'' |
495 | . ' AND nspname = ' . $this->addQuotes( $this->getCoreSchema() ) |
496 | . ' AND relname = ' . $this->addQuotes( $oldName ) |
497 | . ' AND pg_get_expr(adbin, adrelid) LIKE \'nextval(%\''; |
498 | $query = new Query( |
499 | $sql, |
500 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
501 | 'SELECT' |
502 | ); |
503 | |
504 | $res = $this->query( $query, $fname ); |
505 | $row = $res->fetchObject(); |
506 | if ( $row ) { |
507 | $field = $row->attname; |
508 | $newSeq = "{$newName}_{$field}_seq"; |
509 | $fieldE = $this->platform->addIdentifierQuotes( $field ); |
510 | $newSeqE = $this->platform->addIdentifierQuotes( $newSeq ); |
511 | $newSeqQ = $this->addQuotes( $newSeq ); |
512 | $query = new Query( |
513 | "CREATE $temporary SEQUENCE $newSeqE OWNED BY $newNameE.$fieldE", |
514 | self::QUERY_CHANGE_SCHEMA, |
515 | 'CREATE', |
516 | // Do not treat this is as a table modification on top of the CREATE above. |
517 | null |
518 | ); |
519 | $this->query( $query, $fname ); |
520 | $query = new Query( |
521 | "ALTER TABLE $newNameE ALTER COLUMN $fieldE SET DEFAULT nextval({$newSeqQ}::regclass)", |
522 | self::QUERY_CHANGE_SCHEMA, |
523 | 'ALTER', |
524 | // Do not treat this is as a table modification on top of the CREATE above. |
525 | null |
526 | ); |
527 | $this->query( $query, $fname ); |
528 | } |
529 | |
530 | return $ret; |
531 | } |
532 | |
533 | public function truncateTable( $table, $fname = __METHOD__ ) { |
534 | $sql = "TRUNCATE TABLE " . $this->tableName( $table ) . " RESTART IDENTITY"; |
535 | $query = new Query( $sql, self::QUERY_CHANGE_SCHEMA, 'TRUNCATE', $table ); |
536 | $this->query( $query, $fname ); |
537 | } |
538 | |
539 | /** |
540 | * @param string $prefix Only show tables with this prefix, e.g. mw_ |
541 | * @param string $fname Calling function name |
542 | * @return string[] |
543 | * @suppress SecurityCheck-SQLInjection array_map not recognized T204911 |
544 | */ |
545 | public function listTables( $prefix = '', $fname = __METHOD__ ) { |
546 | $eschemas = implode( ',', array_map( [ $this, 'addQuotes' ], $this->getCoreSchemas() ) ); |
547 | $query = new Query( |
548 | "SELECT DISTINCT tablename FROM pg_tables WHERE schemaname IN ($eschemas)", |
549 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
550 | 'SELECT' |
551 | ); |
552 | $result = $this->query( $query, $fname ); |
553 | $endArray = []; |
554 | |
555 | foreach ( $result as $table ) { |
556 | $vars = get_object_vars( $table ); |
557 | $table = array_pop( $vars ); |
558 | if ( $prefix == '' || strpos( $table, $prefix ) === 0 ) { |
559 | $endArray[] = $table; |
560 | } |
561 | } |
562 | |
563 | return $endArray; |
564 | } |
565 | |
566 | /** |
567 | * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12 |
568 | * to https://www.php.net/manual/en/ref.pgsql.php |
569 | * |
570 | * Parsing a postgres array can be a tricky problem, he's my |
571 | * take on this, it handles multi-dimensional arrays plus |
572 | * escaping using a nasty regexp to determine the limits of each |
573 | * data-item. |
574 | * |
575 | * This should really be handled by PHP PostgreSQL module |
576 | * |
577 | * @since 1.19 |
578 | * @param string $text Postgreql array returned in a text form like {a,b} |
579 | * @param string[] &$output |
580 | * @param int|false $limit |
581 | * @param int $offset |
582 | * @return string[] |
583 | */ |
584 | private function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) { |
585 | if ( $limit === false ) { |
586 | $limit = strlen( $text ) - 1; |
587 | $output = []; |
588 | } |
589 | if ( $text == '{}' ) { |
590 | return $output; |
591 | } |
592 | do { |
593 | if ( $text[$offset] != '{' ) { |
594 | preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/", |
595 | $text, $match, 0, $offset ); |
596 | $offset += strlen( $match[0] ); |
597 | $output[] = ( $match[1][0] != '"' |
598 | ? $match[1] |
599 | : stripcslashes( substr( $match[1], 1, -1 ) ) ); |
600 | if ( $match[3] == '},' ) { |
601 | return $output; |
602 | } |
603 | } else { |
604 | $offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 ); |
605 | } |
606 | } while ( $limit > $offset ); |
607 | |
608 | return $output; |
609 | } |
610 | |
611 | public function getSoftwareLink() { |
612 | return '[{{int:version-db-postgres-url}} PostgreSQL]'; |
613 | } |
614 | |
615 | /** |
616 | * Return current schema (executes SELECT current_schema()) |
617 | * Needs transaction |
618 | * |
619 | * @since 1.19 |
620 | * @return string Default schema for the current session |
621 | */ |
622 | public function getCurrentSchema() { |
623 | $query = new Query( |
624 | "SELECT current_schema()", |
625 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
626 | 'SELECT' |
627 | ); |
628 | $res = $this->query( $query, __METHOD__ ); |
629 | $row = $res->fetchRow(); |
630 | |
631 | return $row[0]; |
632 | } |
633 | |
634 | /** |
635 | * Return list of schemas which are accessible without schema name |
636 | * This is list does not contain magic keywords like "$user" |
637 | * Needs transaction |
638 | * |
639 | * @see getSearchPath() |
640 | * @see setSearchPath() |
641 | * @since 1.19 |
642 | * @return array List of actual schemas for the current session |
643 | */ |
644 | public function getSchemas() { |
645 | $query = new Query( |
646 | "SELECT current_schemas(false)", |
647 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
648 | 'SELECT' |
649 | ); |
650 | $res = $this->query( $query, __METHOD__ ); |
651 | $row = $res->fetchRow(); |
652 | $schemas = []; |
653 | |
654 | /* PHP pgsql support does not support array type, "{a,b}" string is returned */ |
655 | |
656 | return $this->pg_array_parse( $row[0], $schemas ); |
657 | } |
658 | |
659 | /** |
660 | * Return search patch for schemas |
661 | * This is different from getSchemas() since it contain magic keywords |
662 | * (like "$user"). |
663 | * Needs transaction |
664 | * |
665 | * @since 1.19 |
666 | * @return array How to search for table names schemas for the current user |
667 | */ |
668 | public function getSearchPath() { |
669 | $query = new Query( |
670 | "SHOW search_path", |
671 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
672 | 'SHOW' |
673 | ); |
674 | $res = $this->query( $query, __METHOD__ ); |
675 | $row = $res->fetchRow(); |
676 | |
677 | /* PostgreSQL returns SHOW values as strings */ |
678 | |
679 | return explode( ",", $row[0] ); |
680 | } |
681 | |
682 | /** |
683 | * Update search_path, values should already be sanitized |
684 | * Values may contain magic keywords like "$user" |
685 | * @since 1.19 |
686 | * |
687 | * @param string[] $search_path List of schemas to be searched by default |
688 | */ |
689 | private function setSearchPath( $search_path ) { |
690 | $query = new Query( |
691 | "SET search_path = " . implode( ", ", $search_path ), |
692 | self::QUERY_CHANGE_TRX, |
693 | 'SET' |
694 | ); |
695 | $this->query( $query, __METHOD__ ); |
696 | } |
697 | |
698 | /** |
699 | * Determine default schema for the current application |
700 | * Adjust this session schema search path if desired schema exists |
701 | * and is not already there. |
702 | * |
703 | * We need to have name of the core schema stored to be able |
704 | * to query database metadata. |
705 | * |
706 | * This will be also called by the installer after the schema is created |
707 | * |
708 | * @since 1.19 |
709 | * |
710 | * @param string|null $desiredSchema |
711 | */ |
712 | public function determineCoreSchema( $desiredSchema ) { |
713 | if ( $this->trxLevel() ) { |
714 | // We do not want the schema selection to change on ROLLBACK or INSERT SELECT. |
715 | // See https://www.postgresql.org/docs/8.3/sql-set.html |
716 | throw new DBUnexpectedError( |
717 | $this, |
718 | __METHOD__ . ": a transaction is currently active" |
719 | ); |
720 | } |
721 | |
722 | if ( $this->schemaExists( $desiredSchema ) ) { |
723 | if ( in_array( $desiredSchema, $this->getSchemas() ) ) { |
724 | $this->platform->setCoreSchema( $desiredSchema ); |
725 | $this->logger->debug( |
726 | "Schema \"" . $desiredSchema . "\" already in the search path\n" ); |
727 | } else { |
728 | // Prepend the desired schema to the search path (T17816) |
729 | $search_path = $this->getSearchPath(); |
730 | array_unshift( $search_path, $this->platform->addIdentifierQuotes( $desiredSchema ) ); |
731 | $this->setSearchPath( $search_path ); |
732 | $this->platform->setCoreSchema( $desiredSchema ); |
733 | $this->logger->debug( |
734 | "Schema \"" . $desiredSchema . "\" added to the search path\n" ); |
735 | } |
736 | } else { |
737 | $this->platform->setCoreSchema( $this->getCurrentSchema() ); |
738 | $this->logger->debug( |
739 | "Schema \"" . $desiredSchema . "\" not found, using current \"" . |
740 | $this->getCoreSchema() . "\"\n" ); |
741 | } |
742 | } |
743 | |
744 | /** |
745 | * Return schema name for core application tables |
746 | * |
747 | * @since 1.19 |
748 | * @return string Core schema name |
749 | */ |
750 | public function getCoreSchema() { |
751 | return $this->platform->getCoreSchema(); |
752 | } |
753 | |
754 | /** |
755 | * Return schema names for temporary tables and core application tables |
756 | * |
757 | * @since 1.31 |
758 | * @return string[] schema names |
759 | */ |
760 | public function getCoreSchemas() { |
761 | if ( $this->tempSchema ) { |
762 | return [ $this->tempSchema, $this->getCoreSchema() ]; |
763 | } |
764 | $query = new Query( |
765 | "SELECT nspname FROM pg_catalog.pg_namespace n WHERE n.oid = pg_my_temp_schema()", |
766 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
767 | 'SELECT' |
768 | ); |
769 | $res = $this->query( $query, __METHOD__ ); |
770 | $row = $res->fetchObject(); |
771 | if ( $row ) { |
772 | $this->tempSchema = $row->nspname; |
773 | return [ $this->tempSchema, $this->getCoreSchema() ]; |
774 | } |
775 | |
776 | return [ $this->getCoreSchema() ]; |
777 | } |
778 | |
779 | public function getServerVersion() { |
780 | if ( !isset( $this->numericVersion ) ) { |
781 | // Works on PG 7.4+ |
782 | $this->numericVersion = pg_version( $this->getBindingHandle() )['server']; |
783 | } |
784 | |
785 | return $this->numericVersion; |
786 | } |
787 | |
788 | /** |
789 | * Query whether a given relation exists (in the given schema, or the |
790 | * default mw one if not given) |
791 | * @param string $table |
792 | * @param array|string $types |
793 | * @return bool |
794 | */ |
795 | private function relationExists( $table, $types ) { |
796 | if ( !is_array( $types ) ) { |
797 | $types = [ $types ]; |
798 | } |
799 | $schemas = $this->getCoreSchemas(); |
800 | $components = $this->platform->qualifiedTableComponents( $table ); |
801 | $etable = $this->addQuotes( end( $components ) ); |
802 | foreach ( $schemas as $schema ) { |
803 | $eschema = $this->addQuotes( $schema ); |
804 | $sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n " |
805 | . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema " |
806 | . "AND c.relkind IN ('" . implode( "','", $types ) . "')"; |
807 | $query = new Query( |
808 | $sql, |
809 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
810 | 'SELECT' |
811 | ); |
812 | $res = $this->query( $query, __METHOD__ ); |
813 | if ( $res && $res->numRows() ) { |
814 | return true; |
815 | } |
816 | } |
817 | |
818 | return false; |
819 | } |
820 | |
821 | public function tableExists( $table, $fname = __METHOD__ ) { |
822 | return $this->relationExists( $table, [ 'r', 'v' ] ); |
823 | } |
824 | |
825 | public function sequenceExists( $sequence ) { |
826 | return $this->relationExists( $sequence, 'S' ); |
827 | } |
828 | |
829 | public function constraintExists( $table, $constraint ) { |
830 | foreach ( $this->getCoreSchemas() as $schema ) { |
831 | $sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " . |
832 | "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s", |
833 | $this->addQuotes( $schema ), |
834 | $this->addQuotes( $table ), |
835 | $this->addQuotes( $constraint ) |
836 | ); |
837 | $query = new Query( |
838 | $sql, |
839 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
840 | 'SELECT' |
841 | ); |
842 | $res = $this->query( $query, __METHOD__ ); |
843 | if ( $res && $res->numRows() ) { |
844 | return true; |
845 | } |
846 | } |
847 | return false; |
848 | } |
849 | |
850 | /** |
851 | * Query whether a given schema exists. Returns true if it does, false if it doesn't. |
852 | * @param string|null $schema |
853 | * @return bool |
854 | */ |
855 | public function schemaExists( $schema ) { |
856 | if ( !strlen( $schema ?? '' ) ) { |
857 | return false; // short-circuit |
858 | } |
859 | $query = new Query( |
860 | "SELECT 1 FROM pg_catalog.pg_namespace " . |
861 | "WHERE nspname = " . $this->addQuotes( $schema ) . " LIMIT 1", |
862 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
863 | 'SELECT' |
864 | ); |
865 | $res = $this->query( $query, __METHOD__ ); |
866 | |
867 | return ( $res->numRows() > 0 ); |
868 | } |
869 | |
870 | /** |
871 | * Returns true if a given role (i.e. user) exists, false otherwise. |
872 | * @param string $roleName |
873 | * @return bool |
874 | */ |
875 | public function roleExists( $roleName ) { |
876 | $query = new Query( |
877 | "SELECT 1 FROM pg_catalog.pg_roles " . |
878 | "WHERE rolname = " . $this->addQuotes( $roleName ) . " LIMIT 1", |
879 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
880 | 'SELECT' |
881 | ); |
882 | $res = $this->query( $query, __METHOD__ ); |
883 | |
884 | return ( $res->numRows() > 0 ); |
885 | } |
886 | |
887 | /** |
888 | * @param string $table |
889 | * @param string $field |
890 | * @return PostgresField|null |
891 | */ |
892 | public function fieldInfo( $table, $field ) { |
893 | return PostgresField::fromText( $this, $table, $field ); |
894 | } |
895 | |
896 | public function encodeBlob( $b ) { |
897 | $conn = $this->getBindingHandle(); |
898 | |
899 | return new PostgresBlob( pg_escape_bytea( $conn, $b ) ); |
900 | } |
901 | |
902 | public function decodeBlob( $b ) { |
903 | if ( $b instanceof PostgresBlob ) { |
904 | $b = $b->fetch(); |
905 | } elseif ( $b instanceof Blob ) { |
906 | return $b->fetch(); |
907 | } |
908 | |
909 | return pg_unescape_bytea( $b ); |
910 | } |
911 | |
912 | public function strencode( $s ) { |
913 | // Should not be called by us |
914 | return pg_escape_string( $this->getBindingHandle(), (string)$s ); |
915 | } |
916 | |
917 | public function addQuotes( $s ) { |
918 | if ( $s instanceof RawSQLValue ) { |
919 | return $s->toSql(); |
920 | } |
921 | $conn = $this->getBindingHandle(); |
922 | |
923 | if ( $s === null ) { |
924 | return 'NULL'; |
925 | } elseif ( is_bool( $s ) ) { |
926 | return (string)intval( $s ); |
927 | } elseif ( is_int( $s ) ) { |
928 | return (string)$s; |
929 | } elseif ( $s instanceof Blob ) { |
930 | if ( $s instanceof PostgresBlob ) { |
931 | $s = $s->fetch(); |
932 | } else { |
933 | $s = pg_escape_bytea( $conn, $s->fetch() ); |
934 | } |
935 | return "'$s'"; |
936 | } |
937 | |
938 | return "'" . pg_escape_string( $conn, (string)$s ) . "'"; |
939 | } |
940 | |
941 | public function streamStatementEnd( &$sql, &$newLine ) { |
942 | # Allow dollar quoting for function declarations |
943 | if ( str_starts_with( $newLine, '$mw$' ) ) { |
944 | if ( $this->delimiter ) { |
945 | $this->delimiter = false; |
946 | } else { |
947 | $this->delimiter = ';'; |
948 | } |
949 | } |
950 | |
951 | return parent::streamStatementEnd( $sql, $newLine ); |
952 | } |
953 | |
954 | public function doLockIsFree( string $lockName, string $method ) { |
955 | $query = new Query( |
956 | $this->platform->lockIsFreeSQLText( $lockName ), |
957 | self::QUERY_CHANGE_LOCKS, |
958 | 'SELECT' |
959 | ); |
960 | $res = $this->query( $query, $method ); |
961 | $row = $res->fetchObject(); |
962 | |
963 | return (bool)$row->unlocked; |
964 | } |
965 | |
966 | public function doLock( string $lockName, string $method, int $timeout ) { |
967 | $query = new Query( |
968 | $this->platform->lockSQLText( $lockName, $timeout ), |
969 | self::QUERY_CHANGE_LOCKS, |
970 | 'SELECT' |
971 | ); |
972 | |
973 | $acquired = null; |
974 | $loop = new WaitConditionLoop( |
975 | function () use ( $query, $method, &$acquired ) { |
976 | $res = $this->query( $query, $method ); |
977 | $row = $res->fetchObject(); |
978 | |
979 | if ( $row->acquired !== null ) { |
980 | $acquired = (float)$row->acquired; |
981 | |
982 | return WaitConditionLoop::CONDITION_REACHED; |
983 | } |
984 | |
985 | return WaitConditionLoop::CONDITION_CONTINUE; |
986 | }, |
987 | $timeout |
988 | ); |
989 | $loop->invoke(); |
990 | |
991 | return $acquired; |
992 | } |
993 | |
994 | public function doUnlock( string $lockName, string $method ) { |
995 | $query = new Query( |
996 | $this->platform->unlockSQLText( $lockName ), |
997 | self::QUERY_CHANGE_LOCKS, |
998 | 'SELECT' |
999 | ); |
1000 | $result = $this->query( $query, $method ); |
1001 | $row = $result->fetchObject(); |
1002 | |
1003 | return (bool)$row->released; |
1004 | } |
1005 | |
1006 | protected function doFlushSession( $fname ) { |
1007 | $flags = self::QUERY_CHANGE_LOCKS | self::QUERY_NO_RETRY; |
1008 | |
1009 | // https://www.postgresql.org/docs/9.1/functions-admin.html |
1010 | $sql = "SELECT pg_advisory_unlock_all()"; |
1011 | $query = new Query( $sql, $flags, 'UNLOCK' ); |
1012 | $qs = $this->executeQuery( $query, __METHOD__, $flags ); |
1013 | if ( $qs->res === false ) { |
1014 | $this->reportQueryError( $qs->message, $qs->code, $sql, $fname, true ); |
1015 | } |
1016 | } |
1017 | |
1018 | public function serverIsReadOnly() { |
1019 | $query = new Query( |
1020 | "SHOW default_transaction_read_only", |
1021 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
1022 | 'SHOW' |
1023 | ); |
1024 | $res = $this->query( $query, __METHOD__ ); |
1025 | $row = $res->fetchObject(); |
1026 | |
1027 | return $row && strtolower( $row->default_transaction_read_only ) === 'on'; |
1028 | } |
1029 | |
1030 | protected function getInsertIdColumnForUpsert( $table ) { |
1031 | $column = null; |
1032 | |
1033 | $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE; |
1034 | $components = $this->platform->qualifiedTableComponents( $table ); |
1035 | $encTable = $this->addQuotes( end( $components ) ); |
1036 | foreach ( $this->getCoreSchemas() as $schema ) { |
1037 | $encSchema = $this->addQuotes( $schema ); |
1038 | $query = new Query( |
1039 | "SELECT column_name,data_type,column_default " . |
1040 | "FROM information_schema.columns " . |
1041 | "WHERE table_name = $encTable AND table_schema = $encSchema", |
1042 | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, |
1043 | 'SELECT' |
1044 | ); |
1045 | $res = $this->query( $query, __METHOD__ ); |
1046 | if ( $res->numRows() ) { |
1047 | foreach ( $res as $row ) { |
1048 | if ( |
1049 | $row->column_default !== null && |
1050 | str_starts_with( $row->column_default, "nextval(" ) && |
1051 | in_array( $row->data_type, [ 'integer', 'bigint' ], true ) |
1052 | ) { |
1053 | $column = $row->column_name; |
1054 | } |
1055 | } |
1056 | break; |
1057 | } |
1058 | } |
1059 | |
1060 | return $column; |
1061 | } |
1062 | |
1063 | public static function getAttributes() { |
1064 | return [ self::ATTR_SCHEMAS_AS_TABLE_GROUPS => true ]; |
1065 | } |
1066 | } |