Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
9.09% covered (danger)
9.09%
10 / 110
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
PostgresPlatform
9.09% covered (danger)
9.09%
10 / 110
0.00% covered (danger)
0.00%
0 / 20
2327.73
0.00% covered (danger)
0.00%
0 / 1
 limitResult
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 buildConcat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 timestamp
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 buildStringCast
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 implicitOrderby
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCoreSchema
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setCoreSchema
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 selectSQLText
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
240
 makeSelectOptions
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 getDatabaseAndTableIdentifier
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 relationSchemaQualifier
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 buildGroupConcatField
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 makeInsertLists
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
90
 makeInsertNonConflictingVerbAndOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeUpdateOptionsArray
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 isTransactableQuery
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 lockSQLText
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 lockIsFreeSQLText
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 unlockSQLText
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 bigintFromLockName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
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 */
20namespace Wikimedia\Rdbms\Platform;
21
22use Wikimedia\Rdbms\DBLanguageError;
23use Wikimedia\Rdbms\Query;
24use Wikimedia\Timestamp\ConvertibleTimestamp;
25
26/**
27 * @since 1.39
28 * @see ISQLPlatform
29 */
30class PostgresPlatform extends SQLPlatform {
31    /** @var string */
32    private $coreSchema;
33
34    public function limitResult( $sql, $limit, $offset = false ) {
35        return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' );
36    }
37
38    public function buildConcat( $stringList ) {
39        return implode( ' || ', $stringList );
40    }
41
42    public function timestamp( $ts = 0 ) {
43        $ct = new ConvertibleTimestamp( $ts );
44
45        return $ct->getTimestamp( TS_POSTGRES );
46    }
47
48    public function buildStringCast( $field ) {
49        return $field . '::text';
50    }
51
52    public function implicitOrderby() {
53        return false;
54    }
55
56    public function getCoreSchema(): string {
57        return $this->coreSchema;
58    }
59
60    public function setCoreSchema( string $coreSchema ): void {
61        $this->coreSchema = $coreSchema;
62    }
63
64    public function selectSQLText(
65        $tables, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
66    ) {
67        if ( is_string( $options ) ) {
68            $options = [ $options ];
69        }
70
71        // Change the FOR UPDATE option as necessary based on the join conditions. Then pass
72        // to the parent function to get the actual SQL text.
73        // In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
74        // can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to
75        // do so causes a DB error. This wrapper checks which tables can be locked and adjusts it
76        // accordingly.
77        // MySQL uses "ORDER BY NULL" as an optimization hint, but that is illegal in PostgreSQL.
78        if ( is_array( $options ) ) {
79            $forUpdateKey = array_search( 'FOR UPDATE', $options, true );
80            if ( $forUpdateKey !== false && $join_conds ) {
81                unset( $options[$forUpdateKey] );
82                $options['FOR UPDATE'] = [];
83
84                $toCheck = $tables;
85                reset( $toCheck );
86                while ( $toCheck ) {
87                    $alias = key( $toCheck );
88                    $name = $toCheck[$alias];
89                    unset( $toCheck[$alias] );
90
91                    $hasAlias = !is_numeric( $alias );
92                    if ( !$hasAlias && is_string( $name ) ) {
93                        $alias = $name;
94                    }
95
96                    if ( !isset( $join_conds[$alias] ) ||
97                        !preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_conds[$alias][0] )
98                    ) {
99                        if ( is_array( $name ) ) {
100                            // It's a parenthesized group, process all the tables inside the group.
101                            $toCheck = array_merge( $toCheck, $name );
102                        } else {
103                            // Quote alias names so $this->tableName() won't mangle them
104                            $options['FOR UPDATE'][] = $hasAlias ?
105                                $this->addIdentifierQuotes( $alias ) : $alias;
106                        }
107                    }
108                }
109            }
110
111            if (
112                isset( $options['ORDER BY'] ) &&
113                ( $options['ORDER BY'] == 'NULL' || $options['ORDER BY'] == [ 'NULL' ] )
114            ) {
115                unset( $options['ORDER BY'] );
116            }
117        }
118
119        return parent::selectSQLText( $tables, $vars, $conds, $fname, $options, $join_conds );
120    }
121
122    protected function makeSelectOptions( array $options ) {
123        $preLimitTail = $postLimitTail = '';
124        $startOpts = '';
125
126        $noKeyOptions = [];
127        foreach ( $options as $key => $option ) {
128            if ( is_numeric( $key ) ) {
129                $noKeyOptions[$option] = true;
130            }
131        }
132
133        $preLimitTail .= $this->makeGroupByWithHaving( $options );
134
135        $preLimitTail .= $this->makeOrderBy( $options );
136
137        if ( isset( $options['FOR UPDATE'] ) ) {
138            $postLimitTail .= ' FOR UPDATE OF ' .
139                implode( ', ', array_map( [ $this, 'tableName' ], $options['FOR UPDATE'] ) );
140        } elseif ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
141            $postLimitTail .= ' FOR UPDATE';
142        }
143
144        if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
145            $startOpts .= 'DISTINCT';
146        }
147
148        return [ $startOpts, $preLimitTail, $postLimitTail ];
149    }
150
151    public function getDatabaseAndTableIdentifier( string $table ) {
152        $components = $this->qualifiedTableComponents( $table );
153        switch ( count( $components ) ) {
154            case 1:
155                return [ $this->currentDomain->getDatabase(), $components[0] ];
156            case 2:
157                return [ $this->currentDomain->getDatabase(), $components[1] ];
158            case 3:
159                return [ $components[0], $components[2] ];
160            default:
161                throw new DBLanguageError( 'Too many table components' );
162        }
163    }
164
165    protected function relationSchemaQualifier() {
166        if ( $this->coreSchema === $this->currentDomain->getSchema() ) {
167            // The schema to be used is now in the search path; no need for explicit qualification
168            return '';
169        }
170
171        return parent::relationSchemaQualifier();
172    }
173
174    public function buildGroupConcatField(
175        $delim, $tables, $field, $conds = '', $join_conds = []
176    ) {
177        $fld = "array_to_string(array_agg($field)," . $this->quoter->addQuotes( $delim ) . ')';
178
179        return '(' . $this->selectSQLText( $tables, $fld, $conds, static::CALLER_SUBQUERY, [], $join_conds ) . ')';
180    }
181
182    public function makeInsertLists( array $rows, $aliasPrefix = '', array $typeByColumn = [] ) {
183        $firstRow = $rows[0];
184        if ( !is_array( $firstRow ) || !$firstRow ) {
185            throw new DBLanguageError( 'Got an empty row list or empty row' );
186        }
187        // List of columns that define the value tuple ordering
188        $tupleColumns = array_keys( $firstRow );
189
190        $valueTuples = [];
191        foreach ( $rows as $row ) {
192            $rowColumns = array_keys( $row );
193            // VALUES(...) requires a uniform correspondence of (column => value)
194            if ( $rowColumns !== $tupleColumns ) {
195                throw new DBLanguageError(
196                    'Got row columns (' . implode( ', ', $rowColumns ) . ') ' .
197                    'instead of expected (' . implode( ', ', $tupleColumns ) . ')'
198                );
199            }
200            // Make the value tuple that defines this row
201            $typedRowValues = [];
202            foreach ( $row as $column => $value ) {
203                $type = $typeByColumn[$column] ?? null;
204                if ( $value === null ) {
205                    $typedRowValues[] = 'NULL';
206                } elseif ( $type !== null ) {
207                    $typedRowValues[] = $this->quoter->addQuotes( $value ) . '::' . $type;
208                } else {
209                    $typedRowValues[] = $this->quoter->addQuotes( $value );
210                }
211            }
212            $valueTuples[] = '(' . implode( ',', $typedRowValues ) . ')';
213        }
214
215        $magicAliasFields = [];
216        foreach ( $tupleColumns as $column ) {
217            $magicAliasFields[] = $aliasPrefix . $column;
218        }
219
220        return [
221            $this->makeList( $tupleColumns, self::LIST_NAMES ),
222            implode( ',', $valueTuples ),
223            $this->makeList( $magicAliasFields, self::LIST_NAMES )
224        ];
225    }
226
227    protected function makeInsertNonConflictingVerbAndOptions() {
228        return [ 'INSERT INTO', 'ON CONFLICT DO NOTHING' ];
229    }
230
231    protected function makeUpdateOptionsArray( $options ) {
232        $options = $this->normalizeOptions( $options );
233        // PostgreSQL doesn't support anything like "ignore" for UPDATE.
234        $options = array_diff( $options, [ 'IGNORE' ] );
235
236        return parent::makeUpdateOptionsArray( $options );
237    }
238
239    public function isTransactableQuery( Query $sql ) {
240        return parent::isTransactableQuery( $sql ) &&
241            !preg_match( '/^SELECT\s+pg_(try_|)advisory_\w+\(/', $sql->getSQL() );
242    }
243
244    public function lockSQLText( $lockName, $timeout ) {
245        // http://www.postgresql.org/docs/9.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
246        $key = $this->quoter->addQuotes( $this->bigintFromLockName( $lockName ) );
247        return "SELECT (CASE WHEN pg_try_advisory_lock($key" .
248            "THEN EXTRACT(epoch from clock_timestamp()) " .
249            "ELSE NULL " .
250            "END) AS acquired";
251    }
252
253    public function lockIsFreeSQLText( $lockName ) {
254        // http://www.postgresql.org/docs/9.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
255        $key = $this->quoter->addQuotes( $this->bigintFromLockName( $lockName ) );
256        return "SELECT (CASE(pg_try_advisory_lock($key))
257            WHEN FALSE THEN FALSE ELSE pg_advisory_unlock($key) END) AS unlocked";
258    }
259
260    public function unlockSQLText( $lockName ) {
261        // http://www.postgresql.org/docs/9.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
262        $key = $this->quoter->addQuotes( $this->bigintFromLockName( $lockName ) );
263        return "SELECT pg_advisory_unlock($key) AS released";
264    }
265
266    /**
267     * @param string $lockName
268     * @return string Integer
269     */
270    private function bigintFromLockName( $lockName ) {
271        return \Wikimedia\base_convert( substr( sha1( $lockName ), 0, 15 ), 16, 10 );
272    }
273}