Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 101 |
|
0.00% |
0 / 19 |
CRAP | |
0.00% |
0 / 1 |
PostgresPlatform | |
0.00% |
0 / 101 |
|
0.00% |
0 / 19 |
2550 | |
0.00% |
0 / 1 |
limitResult | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
buildConcat | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
timestamp | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
buildStringCast | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
implicitOrderby | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCoreSchema | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setCoreSchema | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
selectSQLText | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
240 | |||
makeSelectOptions | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
56 | |||
relationSchemaQualifier | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
buildGroupConcatField | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
makeInsertLists | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
90 | |||
makeInsertNonConflictingVerbAndOptions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
makeUpdateOptionsArray | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
isTransactableQuery | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
lockSQLText | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
lockIsFreeSQLText | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
unlockSQLText | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
bigintFromLockName | |
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 | namespace Wikimedia\Rdbms\Platform; |
21 | |
22 | use Wikimedia\Rdbms\DBLanguageError; |
23 | use Wikimedia\Rdbms\Query; |
24 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
25 | |
26 | /** |
27 | * @since 1.39 |
28 | * @see ISQLPlatform |
29 | */ |
30 | class 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 | $table, $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 = $table; |
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( $table, $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 | protected function relationSchemaQualifier() { |
152 | if ( $this->coreSchema === $this->currentDomain->getSchema() ) { |
153 | // The schema to be used is now in the search path; no need for explicit qualification |
154 | return ''; |
155 | } |
156 | |
157 | return parent::relationSchemaQualifier(); |
158 | } |
159 | |
160 | public function buildGroupConcatField( |
161 | $delim, $table, $field, $conds = '', $join_conds = [] |
162 | ) { |
163 | $fld = "array_to_string(array_agg($field)," . $this->quoter->addQuotes( $delim ) . ')'; |
164 | |
165 | return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')'; |
166 | } |
167 | |
168 | public function makeInsertLists( array $rows, $aliasPrefix = '', array $typeByColumn = [] ) { |
169 | $firstRow = $rows[0]; |
170 | if ( !is_array( $firstRow ) || !$firstRow ) { |
171 | throw new DBLanguageError( 'Got an empty row list or empty row' ); |
172 | } |
173 | // List of columns that define the value tuple ordering |
174 | $tupleColumns = array_keys( $firstRow ); |
175 | |
176 | $valueTuples = []; |
177 | foreach ( $rows as $row ) { |
178 | $rowColumns = array_keys( $row ); |
179 | // VALUES(...) requires a uniform correspondence of (column => value) |
180 | if ( $rowColumns !== $tupleColumns ) { |
181 | throw new DBLanguageError( |
182 | 'Got row columns (' . implode( ', ', $rowColumns ) . ') ' . |
183 | 'instead of expected (' . implode( ', ', $tupleColumns ) . ')' |
184 | ); |
185 | } |
186 | // Make the value tuple that defines this row |
187 | $typedRowValues = []; |
188 | foreach ( $row as $column => $value ) { |
189 | $type = $typeByColumn[$column] ?? null; |
190 | if ( $value === null ) { |
191 | $typedRowValues[] = 'NULL'; |
192 | } elseif ( $type !== null ) { |
193 | $typedRowValues[] = $this->quoter->addQuotes( $value ) . '::' . $type; |
194 | } else { |
195 | $typedRowValues[] = $this->quoter->addQuotes( $value ); |
196 | } |
197 | } |
198 | $valueTuples[] = '(' . implode( ',', $typedRowValues ) . ')'; |
199 | } |
200 | |
201 | $magicAliasFields = []; |
202 | foreach ( $tupleColumns as $column ) { |
203 | $magicAliasFields[] = $aliasPrefix . $column; |
204 | } |
205 | |
206 | return [ |
207 | $this->makeList( $tupleColumns, self::LIST_NAMES ), |
208 | implode( ',', $valueTuples ), |
209 | $this->makeList( $magicAliasFields, self::LIST_NAMES ) |
210 | ]; |
211 | } |
212 | |
213 | protected function makeInsertNonConflictingVerbAndOptions() { |
214 | return [ 'INSERT INTO', 'ON CONFLICT DO NOTHING' ]; |
215 | } |
216 | |
217 | protected function makeUpdateOptionsArray( $options ) { |
218 | $options = $this->normalizeOptions( $options ); |
219 | // PostgreSQL doesn't support anything like "ignore" for UPDATE. |
220 | $options = array_diff( $options, [ 'IGNORE' ] ); |
221 | |
222 | return parent::makeUpdateOptionsArray( $options ); |
223 | } |
224 | |
225 | public function isTransactableQuery( Query $sql ) { |
226 | return parent::isTransactableQuery( $sql ) && |
227 | !preg_match( '/^SELECT\s+pg_(try_|)advisory_\w+\(/', $sql->getSQL() ); |
228 | } |
229 | |
230 | public function lockSQLText( $lockName, $timeout ) { |
231 | // http://www.postgresql.org/docs/9.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS |
232 | $key = $this->quoter->addQuotes( $this->bigintFromLockName( $lockName ) ); |
233 | return "SELECT (CASE WHEN pg_try_advisory_lock($key) " . |
234 | "THEN EXTRACT(epoch from clock_timestamp()) " . |
235 | "ELSE NULL " . |
236 | "END) AS acquired"; |
237 | } |
238 | |
239 | public function lockIsFreeSQLText( $lockName ) { |
240 | // http://www.postgresql.org/docs/9.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS |
241 | $key = $this->quoter->addQuotes( $this->bigintFromLockName( $lockName ) ); |
242 | return "SELECT (CASE(pg_try_advisory_lock($key)) |
243 | WHEN FALSE THEN FALSE ELSE pg_advisory_unlock($key) END) AS unlocked"; |
244 | } |
245 | |
246 | public function unlockSQLText( $lockName ) { |
247 | // http://www.postgresql.org/docs/9.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS |
248 | $key = $this->quoter->addQuotes( $this->bigintFromLockName( $lockName ) ); |
249 | return "SELECT pg_advisory_unlock($key) AS released"; |
250 | } |
251 | |
252 | /** |
253 | * @param string $lockName |
254 | * @return string Integer |
255 | */ |
256 | private function bigintFromLockName( $lockName ) { |
257 | return \Wikimedia\base_convert( substr( sha1( $lockName ), 0, 15 ), 16, 10 ); |
258 | } |
259 | } |