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