1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This is the Postgres database abstraction layer. |
4
|
|
|
* |
5
|
|
|
* This program is free software; you can redistribute it and/or modify |
6
|
|
|
* it under the terms of the GNU General Public License as published by |
7
|
|
|
* the Free Software Foundation; either version 2 of the License, or |
8
|
|
|
* (at your option) any later version. |
9
|
|
|
* |
10
|
|
|
* This program is distributed in the hope that it will be useful, |
11
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
12
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13
|
|
|
* GNU General Public License for more details. |
14
|
|
|
* |
15
|
|
|
* You should have received a copy of the GNU General Public License along |
16
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc., |
17
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18
|
|
|
* http://www.gnu.org/copyleft/gpl.html |
19
|
|
|
* |
20
|
|
|
* @file |
21
|
|
|
* @ingroup Database |
22
|
|
|
*/ |
23
|
|
|
|
24
|
|
|
class PostgresField implements Field { |
25
|
|
|
private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname, |
|
|
|
|
26
|
|
|
$has_default, $default; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @param DatabaseBase $db |
30
|
|
|
* @param string $table |
31
|
|
|
* @param string $field |
32
|
|
|
* @return null|PostgresField |
33
|
|
|
*/ |
34
|
|
|
static function fromText( $db, $table, $field ) { |
35
|
|
|
$q = <<<SQL |
36
|
|
|
SELECT |
37
|
|
|
attnotnull, attlen, conname AS conname, |
38
|
|
|
atthasdef, |
39
|
|
|
adsrc, |
40
|
|
|
COALESCE(condeferred, 'f') AS deferred, |
41
|
|
|
COALESCE(condeferrable, 'f') AS deferrable, |
42
|
|
|
CASE WHEN typname = 'int2' THEN 'smallint' |
43
|
|
|
WHEN typname = 'int4' THEN 'integer' |
44
|
|
|
WHEN typname = 'int8' THEN 'bigint' |
45
|
|
|
WHEN typname = 'bpchar' THEN 'char' |
46
|
|
|
ELSE typname END AS typname |
47
|
|
|
FROM pg_class c |
48
|
|
|
JOIN pg_namespace n ON (n.oid = c.relnamespace) |
49
|
|
|
JOIN pg_attribute a ON (a.attrelid = c.oid) |
50
|
|
|
JOIN pg_type t ON (t.oid = a.atttypid) |
51
|
|
|
LEFT JOIN pg_constraint o ON (o.conrelid = c.oid AND a.attnum = ANY(o.conkey) AND o.contype = 'f') |
52
|
|
|
LEFT JOIN pg_attrdef d on c.oid=d.adrelid and a.attnum=d.adnum |
53
|
|
|
WHERE relkind = 'r' |
54
|
|
|
AND nspname=%s |
55
|
|
|
AND relname=%s |
56
|
|
|
AND attname=%s; |
57
|
|
|
SQL; |
58
|
|
|
|
59
|
|
|
$table = $db->tableName( $table, 'raw' ); |
60
|
|
|
$res = $db->query( |
61
|
|
|
sprintf( $q, |
62
|
|
|
$db->addQuotes( $db->getCoreSchema() ), |
|
|
|
|
63
|
|
|
$db->addQuotes( $table ), |
64
|
|
|
$db->addQuotes( $field ) |
65
|
|
|
) |
66
|
|
|
); |
67
|
|
|
$row = $db->fetchObject( $res ); |
|
|
|
|
68
|
|
|
if ( !$row ) { |
69
|
|
|
return null; |
70
|
|
|
} |
71
|
|
|
$n = new PostgresField; |
72
|
|
|
$n->type = $row->typname; |
73
|
|
|
$n->nullable = ( $row->attnotnull == 'f' ); |
74
|
|
|
$n->name = $field; |
75
|
|
|
$n->tablename = $table; |
76
|
|
|
$n->max_length = $row->attlen; |
77
|
|
|
$n->deferrable = ( $row->deferrable == 't' ); |
78
|
|
|
$n->deferred = ( $row->deferred == 't' ); |
79
|
|
|
$n->conname = $row->conname; |
80
|
|
|
$n->has_default = ( $row->atthasdef === 't' ); |
81
|
|
|
$n->default = $row->adsrc; |
82
|
|
|
|
83
|
|
|
return $n; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
function name() { |
87
|
|
|
return $this->name; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
function tableName() { |
91
|
|
|
return $this->tablename; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
function type() { |
95
|
|
|
return $this->type; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
function isNullable() { |
99
|
|
|
return $this->nullable; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
function maxLength() { |
103
|
|
|
return $this->max_length; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
function is_deferrable() { |
107
|
|
|
return $this->deferrable; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
function is_deferred() { |
111
|
|
|
return $this->deferred; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
function conname() { |
115
|
|
|
return $this->conname; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* @since 1.19 |
120
|
|
|
* @return bool|mixed |
121
|
|
|
*/ |
122
|
|
|
function defaultValue() { |
123
|
|
|
if ( $this->has_default ) { |
124
|
|
|
return $this->default; |
125
|
|
|
} else { |
126
|
|
|
return false; |
127
|
|
|
} |
128
|
|
|
} |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Manage savepoints within a transaction |
133
|
|
|
* @ingroup Database |
134
|
|
|
* @since 1.19 |
135
|
|
|
*/ |
136
|
|
|
class SavepointPostgres { |
137
|
|
|
/** @var DatabasePostgres Establish a savepoint within a transaction */ |
138
|
|
|
protected $dbw; |
139
|
|
|
protected $id; |
140
|
|
|
protected $didbegin; |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* @param DatabaseBase $dbw |
144
|
|
|
* @param int $id |
145
|
|
|
*/ |
146
|
|
|
public function __construct( $dbw, $id ) { |
147
|
|
|
$this->dbw = $dbw; |
|
|
|
|
148
|
|
|
$this->id = $id; |
149
|
|
|
$this->didbegin = false; |
150
|
|
|
/* If we are not in a transaction, we need to be for savepoint trickery */ |
151
|
|
|
if ( !$dbw->trxLevel() ) { |
152
|
|
|
$dbw->begin( "FOR SAVEPOINT", DatabasePostgres::TRANSACTION_INTERNAL ); |
153
|
|
|
$this->didbegin = true; |
154
|
|
|
} |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
public function __destruct() { |
158
|
|
|
if ( $this->didbegin ) { |
159
|
|
|
$this->dbw->rollback(); |
160
|
|
|
$this->didbegin = false; |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
public function commit() { |
165
|
|
|
if ( $this->didbegin ) { |
166
|
|
|
$this->dbw->commit(); |
167
|
|
|
$this->didbegin = false; |
168
|
|
|
} |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
protected function query( $keyword, $msg_ok, $msg_failed ) { |
172
|
|
|
if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) { |
|
|
|
|
173
|
|
|
} else { |
174
|
|
|
wfDebug( sprintf( $msg_failed, $this->id ) ); |
175
|
|
|
} |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
public function savepoint() { |
179
|
|
|
$this->query( "SAVEPOINT", |
180
|
|
|
"Transaction state: savepoint \"%s\" established.\n", |
181
|
|
|
"Transaction state: establishment of savepoint \"%s\" FAILED.\n" |
182
|
|
|
); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
public function release() { |
186
|
|
|
$this->query( "RELEASE", |
187
|
|
|
"Transaction state: savepoint \"%s\" released.\n", |
188
|
|
|
"Transaction state: release of savepoint \"%s\" FAILED.\n" |
189
|
|
|
); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
public function rollback() { |
193
|
|
|
$this->query( "ROLLBACK TO", |
194
|
|
|
"Transaction state: savepoint \"%s\" rolled back.\n", |
195
|
|
|
"Transaction state: rollback of savepoint \"%s\" FAILED.\n" |
196
|
|
|
); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
public function __toString() { |
200
|
|
|
return (string)$this->id; |
201
|
|
|
} |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* @ingroup Database |
206
|
|
|
*/ |
207
|
|
|
class DatabasePostgres extends Database { |
208
|
|
|
/** @var resource */ |
209
|
|
|
protected $mLastResult = null; |
210
|
|
|
|
211
|
|
|
/** @var int The number of rows affected as an integer */ |
212
|
|
|
protected $mAffectedRows = null; |
213
|
|
|
|
214
|
|
|
/** @var int */ |
215
|
|
|
private $mInsertId = null; |
216
|
|
|
|
217
|
|
|
/** @var float|string */ |
218
|
|
|
private $numericVersion = null; |
219
|
|
|
|
220
|
|
|
/** @var string Connect string to open a PostgreSQL connection */ |
221
|
|
|
private $connectString; |
222
|
|
|
|
223
|
|
|
/** @var string */ |
224
|
|
|
private $mCoreSchema; |
225
|
|
|
|
226
|
|
|
function getType() { |
227
|
|
|
return 'postgres'; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
function cascadingDeletes() { |
231
|
|
|
return true; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
function cleanupTriggers() { |
235
|
|
|
return true; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
function strictIPs() { |
239
|
|
|
return true; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
function realTimestamps() { |
243
|
|
|
return true; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
function implicitGroupby() { |
247
|
|
|
return false; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
function implicitOrderby() { |
251
|
|
|
return false; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
function searchableIPs() { |
255
|
|
|
return true; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
function functionalIndexes() { |
259
|
|
|
return true; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
function hasConstraint( $name ) { |
263
|
|
|
$sql = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n " . |
264
|
|
|
"WHERE c.connamespace = n.oid AND conname = '" . |
265
|
|
|
pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . |
266
|
|
|
pg_escape_string( $this->mConn, $this->getCoreSchema() ) . "'"; |
267
|
|
|
$res = $this->doQuery( $sql ); |
268
|
|
|
|
269
|
|
|
return $this->numRows( $res ); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Usually aborts on failure |
274
|
|
|
* @param string $server |
275
|
|
|
* @param string $user |
276
|
|
|
* @param string $password |
277
|
|
|
* @param string $dbName |
278
|
|
|
* @throws DBConnectionError|Exception |
279
|
|
|
* @return DatabaseBase|null |
280
|
|
|
*/ |
281
|
|
|
function open( $server, $user, $password, $dbName ) { |
282
|
|
|
# Test for Postgres support, to avoid suppressed fatal error |
283
|
|
|
if ( !function_exists( 'pg_connect' ) ) { |
284
|
|
|
throw new DBConnectionError( |
285
|
|
|
$this, |
286
|
|
|
"Postgres functions missing, have you compiled PHP with the --with-pgsql\n" . |
287
|
|
|
"option? (Note: if you recently installed PHP, you may need to restart your\n" . |
288
|
|
|
"webserver and database)\n" |
289
|
|
|
); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
global $wgDBport; |
293
|
|
|
|
294
|
|
|
if ( !strlen( $user ) ) { # e.g. the class is being loaded |
295
|
|
|
return null; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
$this->mServer = $server; |
299
|
|
|
$port = $wgDBport; |
300
|
|
|
$this->mUser = $user; |
301
|
|
|
$this->mPassword = $password; |
302
|
|
|
$this->mDBname = $dbName; |
303
|
|
|
|
304
|
|
|
$connectVars = [ |
305
|
|
|
'dbname' => $dbName, |
306
|
|
|
'user' => $user, |
307
|
|
|
'password' => $password |
308
|
|
|
]; |
309
|
|
|
if ( $server != false && $server != '' ) { |
|
|
|
|
310
|
|
|
$connectVars['host'] = $server; |
311
|
|
|
} |
312
|
|
|
if ( $port != false && $port != '' ) { |
313
|
|
|
$connectVars['port'] = $port; |
314
|
|
|
} |
315
|
|
|
if ( $this->mFlags & DBO_SSL ) { |
316
|
|
|
$connectVars['sslmode'] = 1; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
$this->connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW ); |
320
|
|
|
$this->close(); |
321
|
|
|
$this->installErrorHandler(); |
322
|
|
|
|
323
|
|
|
try { |
324
|
|
|
$this->mConn = pg_connect( $this->connectString ); |
325
|
|
|
} catch ( Exception $ex ) { |
326
|
|
|
$this->restoreErrorHandler(); |
327
|
|
|
throw $ex; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
$phpError = $this->restoreErrorHandler(); |
331
|
|
|
|
332
|
|
|
if ( !$this->mConn ) { |
333
|
|
|
wfDebug( "DB connection error\n" ); |
334
|
|
|
wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . |
335
|
|
|
substr( $password, 0, 3 ) . "...\n" ); |
336
|
|
|
wfDebug( $this->lastError() . "\n" ); |
337
|
|
|
throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) ); |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
$this->mOpened = true; |
341
|
|
|
|
342
|
|
|
global $wgCommandLineMode; |
343
|
|
|
# If called from the command-line (e.g. importDump), only show errors |
344
|
|
|
if ( $wgCommandLineMode ) { |
345
|
|
|
$this->doQuery( "SET client_min_messages = 'ERROR'" ); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
$this->query( "SET client_encoding='UTF8'", __METHOD__ ); |
349
|
|
|
$this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ ); |
350
|
|
|
$this->query( "SET timezone = 'GMT'", __METHOD__ ); |
351
|
|
|
$this->query( "SET standard_conforming_strings = on", __METHOD__ ); |
352
|
|
|
if ( $this->getServerVersion() >= 9.0 ) { |
353
|
|
|
$this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127 |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
global $wgDBmwschema; |
357
|
|
|
$this->determineCoreSchema( $wgDBmwschema ); |
358
|
|
|
|
359
|
|
|
return $this->mConn; |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
/** |
363
|
|
|
* Postgres doesn't support selectDB in the same way MySQL does. So if the |
364
|
|
|
* DB name doesn't match the open connection, open a new one |
365
|
|
|
* @param string $db |
366
|
|
|
* @return bool |
367
|
|
|
*/ |
368
|
|
|
function selectDB( $db ) { |
369
|
|
|
if ( $this->mDBname !== $db ) { |
370
|
|
|
return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db ); |
371
|
|
|
} else { |
372
|
|
|
return true; |
373
|
|
|
} |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
function makeConnectionString( $vars ) { |
377
|
|
|
$s = ''; |
378
|
|
|
foreach ( $vars as $name => $value ) { |
379
|
|
|
$s .= "$name='" . str_replace( "'", "\\'", $value ) . "' "; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
return $s; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* Closes a database connection, if it is open |
387
|
|
|
* Returns success, true if already closed |
388
|
|
|
* @return bool |
389
|
|
|
*/ |
390
|
|
|
protected function closeConnection() { |
391
|
|
|
return pg_close( $this->mConn ); |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
public function doQuery( $sql ) { |
395
|
|
|
$sql = mb_convert_encoding( $sql, 'UTF-8' ); |
396
|
|
|
// Clear previously left over PQresult |
397
|
|
|
while ( $res = pg_get_result( $this->mConn ) ) { |
398
|
|
|
pg_free_result( $res ); |
399
|
|
|
} |
400
|
|
|
if ( pg_send_query( $this->mConn, $sql ) === false ) { |
401
|
|
|
throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" ); |
402
|
|
|
} |
403
|
|
|
$this->mLastResult = pg_get_result( $this->mConn ); |
404
|
|
|
$this->mAffectedRows = null; |
405
|
|
|
if ( pg_result_error( $this->mLastResult ) ) { |
406
|
|
|
return false; |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
return $this->mLastResult; |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
protected function dumpError() { |
413
|
|
|
$diags = [ |
414
|
|
|
PGSQL_DIAG_SEVERITY, |
415
|
|
|
PGSQL_DIAG_SQLSTATE, |
416
|
|
|
PGSQL_DIAG_MESSAGE_PRIMARY, |
417
|
|
|
PGSQL_DIAG_MESSAGE_DETAIL, |
418
|
|
|
PGSQL_DIAG_MESSAGE_HINT, |
419
|
|
|
PGSQL_DIAG_STATEMENT_POSITION, |
420
|
|
|
PGSQL_DIAG_INTERNAL_POSITION, |
421
|
|
|
PGSQL_DIAG_INTERNAL_QUERY, |
422
|
|
|
PGSQL_DIAG_CONTEXT, |
423
|
|
|
PGSQL_DIAG_SOURCE_FILE, |
424
|
|
|
PGSQL_DIAG_SOURCE_LINE, |
425
|
|
|
PGSQL_DIAG_SOURCE_FUNCTION |
426
|
|
|
]; |
427
|
|
|
foreach ( $diags as $d ) { |
428
|
|
|
wfDebug( sprintf( "PgSQL ERROR(%d): %s\n", |
429
|
|
|
$d, pg_result_error_field( $this->mLastResult, $d ) ) ); |
430
|
|
|
} |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { |
434
|
|
|
if ( $tempIgnore ) { |
435
|
|
|
/* Check for constraint violation */ |
436
|
|
|
if ( $errno === '23505' ) { |
|
|
|
|
437
|
|
|
parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore ); |
438
|
|
|
|
439
|
|
|
return; |
440
|
|
|
} |
441
|
|
|
} |
442
|
|
|
/* Transaction stays in the ERROR state until rolled back */ |
443
|
|
|
if ( $this->mTrxLevel ) { |
444
|
|
|
$ignore = $this->ignoreErrors( true ); |
445
|
|
|
$this->rollback( __METHOD__ ); |
446
|
|
|
$this->ignoreErrors( $ignore ); |
447
|
|
|
} |
448
|
|
|
parent::reportQueryError( $error, $errno, $sql, $fname, false ); |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
function queryIgnore( $sql, $fname = __METHOD__ ) { |
452
|
|
|
return $this->query( $sql, $fname, true ); |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
/** |
456
|
|
|
* @param stdClass|ResultWrapper $res |
457
|
|
|
* @throws DBUnexpectedError |
458
|
|
|
*/ |
459
|
|
View Code Duplication |
function freeResult( $res ) { |
460
|
|
|
if ( $res instanceof ResultWrapper ) { |
461
|
|
|
$res = $res->result; |
462
|
|
|
} |
463
|
|
|
MediaWiki\suppressWarnings(); |
464
|
|
|
$ok = pg_free_result( $res ); |
465
|
|
|
MediaWiki\restoreWarnings(); |
466
|
|
|
if ( !$ok ) { |
467
|
|
|
throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" ); |
468
|
|
|
} |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
/** |
472
|
|
|
* @param ResultWrapper|stdClass $res |
473
|
|
|
* @return stdClass |
474
|
|
|
* @throws DBUnexpectedError |
475
|
|
|
*/ |
476
|
|
View Code Duplication |
function fetchObject( $res ) { |
477
|
|
|
if ( $res instanceof ResultWrapper ) { |
478
|
|
|
$res = $res->result; |
479
|
|
|
} |
480
|
|
|
MediaWiki\suppressWarnings(); |
481
|
|
|
$row = pg_fetch_object( $res ); |
482
|
|
|
MediaWiki\restoreWarnings(); |
483
|
|
|
# @todo FIXME: HACK HACK HACK HACK debug |
484
|
|
|
|
485
|
|
|
# @todo hashar: not sure if the following test really trigger if the object |
486
|
|
|
# fetching failed. |
487
|
|
|
if ( pg_last_error( $this->mConn ) ) { |
488
|
|
|
throw new DBUnexpectedError( |
489
|
|
|
$this, |
490
|
|
|
'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) |
491
|
|
|
); |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
return $row; |
495
|
|
|
} |
496
|
|
|
|
497
|
|
View Code Duplication |
function fetchRow( $res ) { |
498
|
|
|
if ( $res instanceof ResultWrapper ) { |
499
|
|
|
$res = $res->result; |
500
|
|
|
} |
501
|
|
|
MediaWiki\suppressWarnings(); |
502
|
|
|
$row = pg_fetch_array( $res ); |
503
|
|
|
MediaWiki\restoreWarnings(); |
504
|
|
|
if ( pg_last_error( $this->mConn ) ) { |
505
|
|
|
throw new DBUnexpectedError( |
506
|
|
|
$this, |
507
|
|
|
'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) |
508
|
|
|
); |
509
|
|
|
} |
510
|
|
|
|
511
|
|
|
return $row; |
512
|
|
|
} |
513
|
|
|
|
514
|
|
View Code Duplication |
function numRows( $res ) { |
515
|
|
|
if ( $res instanceof ResultWrapper ) { |
516
|
|
|
$res = $res->result; |
517
|
|
|
} |
518
|
|
|
MediaWiki\suppressWarnings(); |
519
|
|
|
$n = pg_num_rows( $res ); |
520
|
|
|
MediaWiki\restoreWarnings(); |
521
|
|
|
if ( pg_last_error( $this->mConn ) ) { |
522
|
|
|
throw new DBUnexpectedError( |
523
|
|
|
$this, |
524
|
|
|
'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) |
525
|
|
|
); |
526
|
|
|
} |
527
|
|
|
|
528
|
|
|
return $n; |
529
|
|
|
} |
530
|
|
|
|
531
|
|
|
function numFields( $res ) { |
532
|
|
|
if ( $res instanceof ResultWrapper ) { |
533
|
|
|
$res = $res->result; |
534
|
|
|
} |
535
|
|
|
|
536
|
|
|
return pg_num_fields( $res ); |
537
|
|
|
} |
538
|
|
|
|
539
|
|
|
function fieldName( $res, $n ) { |
540
|
|
|
if ( $res instanceof ResultWrapper ) { |
541
|
|
|
$res = $res->result; |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
return pg_field_name( $res, $n ); |
545
|
|
|
} |
546
|
|
|
|
547
|
|
|
/** |
548
|
|
|
* Return the result of the last call to nextSequenceValue(); |
549
|
|
|
* This must be called after nextSequenceValue(). |
550
|
|
|
* |
551
|
|
|
* @return int|null |
552
|
|
|
*/ |
553
|
|
|
function insertId() { |
554
|
|
|
return $this->mInsertId; |
555
|
|
|
} |
556
|
|
|
|
557
|
|
|
/** |
558
|
|
|
* @param mixed $res |
559
|
|
|
* @param int $row |
560
|
|
|
* @return bool |
561
|
|
|
*/ |
562
|
|
|
function dataSeek( $res, $row ) { |
563
|
|
|
if ( $res instanceof ResultWrapper ) { |
564
|
|
|
$res = $res->result; |
565
|
|
|
} |
566
|
|
|
|
567
|
|
|
return pg_result_seek( $res, $row ); |
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
function lastError() { |
571
|
|
|
if ( $this->mConn ) { |
572
|
|
|
if ( $this->mLastResult ) { |
573
|
|
|
return pg_result_error( $this->mLastResult ); |
574
|
|
|
} else { |
575
|
|
|
return pg_last_error(); |
576
|
|
|
} |
577
|
|
|
} else { |
578
|
|
|
return 'No database connection'; |
579
|
|
|
} |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
function lastErrno() { |
583
|
|
|
if ( $this->mLastResult ) { |
584
|
|
|
return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE ); |
585
|
|
|
} else { |
586
|
|
|
return false; |
587
|
|
|
} |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
function affectedRows() { |
591
|
|
|
if ( !is_null( $this->mAffectedRows ) ) { |
592
|
|
|
// Forced result for simulated queries |
593
|
|
|
return $this->mAffectedRows; |
594
|
|
|
} |
595
|
|
|
if ( empty( $this->mLastResult ) ) { |
596
|
|
|
return 0; |
597
|
|
|
} |
598
|
|
|
|
599
|
|
|
return pg_affected_rows( $this->mLastResult ); |
600
|
|
|
} |
601
|
|
|
|
602
|
|
|
/** |
603
|
|
|
* Estimate rows in dataset |
604
|
|
|
* Returns estimated count, based on EXPLAIN output |
605
|
|
|
* This is not necessarily an accurate estimate, so use sparingly |
606
|
|
|
* Returns -1 if count cannot be found |
607
|
|
|
* Takes same arguments as Database::select() |
608
|
|
|
* |
609
|
|
|
* @param string $table |
610
|
|
|
* @param string $vars |
611
|
|
|
* @param string $conds |
612
|
|
|
* @param string $fname |
613
|
|
|
* @param array $options |
614
|
|
|
* @return int |
615
|
|
|
*/ |
616
|
|
View Code Duplication |
function estimateRowCount( $table, $vars = '*', $conds = '', |
617
|
|
|
$fname = __METHOD__, $options = [] |
618
|
|
|
) { |
619
|
|
|
$options['EXPLAIN'] = true; |
620
|
|
|
$res = $this->select( $table, $vars, $conds, $fname, $options ); |
621
|
|
|
$rows = -1; |
622
|
|
|
if ( $res ) { |
623
|
|
|
$row = $this->fetchRow( $res ); |
|
|
|
|
624
|
|
|
$count = []; |
625
|
|
|
if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) { |
626
|
|
|
$rows = (int)$count[1]; |
627
|
|
|
} |
628
|
|
|
} |
629
|
|
|
|
630
|
|
|
return $rows; |
631
|
|
|
} |
632
|
|
|
|
633
|
|
|
/** |
634
|
|
|
* Returns information about an index |
635
|
|
|
* If errors are explicitly ignored, returns NULL on failure |
636
|
|
|
* |
637
|
|
|
* @param string $table |
638
|
|
|
* @param string $index |
639
|
|
|
* @param string $fname |
640
|
|
|
* @return bool|null |
641
|
|
|
*/ |
642
|
|
|
function indexInfo( $table, $index, $fname = __METHOD__ ) { |
643
|
|
|
$sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'"; |
644
|
|
|
$res = $this->query( $sql, $fname ); |
645
|
|
|
if ( !$res ) { |
646
|
|
|
return null; |
647
|
|
|
} |
648
|
|
|
foreach ( $res as $row ) { |
|
|
|
|
649
|
|
|
if ( $row->indexname == $this->indexName( $index ) ) { |
650
|
|
|
return $row; |
651
|
|
|
} |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
return false; |
655
|
|
|
} |
656
|
|
|
|
657
|
|
|
/** |
658
|
|
|
* Returns is of attributes used in index |
659
|
|
|
* |
660
|
|
|
* @since 1.19 |
661
|
|
|
* @param string $index |
662
|
|
|
* @param bool|string $schema |
663
|
|
|
* @return array |
664
|
|
|
*/ |
665
|
|
|
function indexAttributes( $index, $schema = false ) { |
666
|
|
|
if ( $schema === false ) { |
667
|
|
|
$schema = $this->getCoreSchema(); |
668
|
|
|
} |
669
|
|
|
/* |
670
|
|
|
* A subquery would be not needed if we didn't care about the order |
671
|
|
|
* of attributes, but we do |
672
|
|
|
*/ |
673
|
|
|
$sql = <<<__INDEXATTR__ |
674
|
|
|
|
675
|
|
|
SELECT opcname, |
676
|
|
|
attname, |
677
|
|
|
i.indoption[s.g] as option, |
678
|
|
|
pg_am.amname |
679
|
|
|
FROM |
680
|
|
|
(SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g |
681
|
|
|
FROM |
682
|
|
|
pg_index isub |
683
|
|
|
JOIN pg_class cis |
684
|
|
|
ON cis.oid=isub.indexrelid |
685
|
|
|
JOIN pg_namespace ns |
686
|
|
|
ON cis.relnamespace = ns.oid |
687
|
|
|
WHERE cis.relname='$index' AND ns.nspname='$schema') AS s, |
688
|
|
|
pg_attribute, |
689
|
|
|
pg_opclass opcls, |
690
|
|
|
pg_am, |
691
|
|
|
pg_class ci |
692
|
|
|
JOIN pg_index i |
693
|
|
|
ON ci.oid=i.indexrelid |
694
|
|
|
JOIN pg_class ct |
695
|
|
|
ON ct.oid = i.indrelid |
696
|
|
|
JOIN pg_namespace n |
697
|
|
|
ON ci.relnamespace = n.oid |
698
|
|
|
WHERE |
699
|
|
|
ci.relname='$index' AND n.nspname='$schema' |
700
|
|
|
AND attrelid = ct.oid |
701
|
|
|
AND i.indkey[s.g] = attnum |
702
|
|
|
AND i.indclass[s.g] = opcls.oid |
703
|
|
|
AND pg_am.oid = opcls.opcmethod |
704
|
|
|
__INDEXATTR__; |
705
|
|
|
$res = $this->query( $sql, __METHOD__ ); |
706
|
|
|
$a = []; |
707
|
|
|
if ( $res ) { |
708
|
|
|
foreach ( $res as $row ) { |
|
|
|
|
709
|
|
|
$a[] = [ |
710
|
|
|
$row->attname, |
711
|
|
|
$row->opcname, |
712
|
|
|
$row->amname, |
713
|
|
|
$row->option ]; |
714
|
|
|
} |
715
|
|
|
} else { |
716
|
|
|
return null; |
717
|
|
|
} |
718
|
|
|
|
719
|
|
|
return $a; |
720
|
|
|
} |
721
|
|
|
|
722
|
|
|
function indexUnique( $table, $index, $fname = __METHOD__ ) { |
723
|
|
|
$sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" . |
724
|
|
|
" AND indexdef LIKE 'CREATE UNIQUE%(" . |
725
|
|
|
$this->strencode( $this->indexName( $index ) ) . |
726
|
|
|
")'"; |
727
|
|
|
$res = $this->query( $sql, $fname ); |
728
|
|
|
if ( !$res ) { |
729
|
|
|
return null; |
730
|
|
|
} |
731
|
|
|
|
732
|
|
|
return $res->numRows() > 0; |
733
|
|
|
} |
734
|
|
|
|
735
|
|
|
/** |
736
|
|
|
* Change the FOR UPDATE option as necessary based on the join conditions. Then pass |
737
|
|
|
* to the parent function to get the actual SQL text. |
738
|
|
|
* |
739
|
|
|
* In Postgres when using FOR UPDATE, only the main table and tables that are inner joined |
740
|
|
|
* can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to do |
741
|
|
|
* so causes a DB error. This wrapper checks which tables can be locked and adjusts it accordingly. |
742
|
|
|
* |
743
|
|
|
* MySQL uses "ORDER BY NULL" as an optimization hint, but that syntax is illegal in PostgreSQL. |
744
|
|
|
* @see DatabaseBase::selectSQLText |
745
|
|
|
*/ |
746
|
|
|
function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, |
747
|
|
|
$options = [], $join_conds = [] |
748
|
|
|
) { |
749
|
|
|
if ( is_array( $options ) ) { |
750
|
|
|
$forUpdateKey = array_search( 'FOR UPDATE', $options, true ); |
751
|
|
|
if ( $forUpdateKey !== false && $join_conds ) { |
752
|
|
|
unset( $options[$forUpdateKey] ); |
753
|
|
|
|
754
|
|
|
foreach ( $join_conds as $table_cond => $join_cond ) { |
755
|
|
|
if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) { |
756
|
|
|
$options['FOR UPDATE'][] = $table_cond; |
757
|
|
|
} |
758
|
|
|
} |
759
|
|
|
} |
760
|
|
|
|
761
|
|
|
if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) { |
762
|
|
|
unset( $options['ORDER BY'] ); |
763
|
|
|
} |
764
|
|
|
} |
765
|
|
|
|
766
|
|
|
return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); |
767
|
|
|
} |
768
|
|
|
|
769
|
|
|
/** |
770
|
|
|
* INSERT wrapper, inserts an array into a table |
771
|
|
|
* |
772
|
|
|
* $args may be a single associative array, or an array of these with numeric keys, |
773
|
|
|
* for multi-row insert (Postgres version 8.2 and above only). |
774
|
|
|
* |
775
|
|
|
* @param string $table Name of the table to insert to. |
776
|
|
|
* @param array $args Items to insert into the table. |
777
|
|
|
* @param string $fname Name of the function, for profiling |
778
|
|
|
* @param array|string $options String or array. Valid options: IGNORE |
779
|
|
|
* @return bool Success of insert operation. IGNORE always returns true. |
780
|
|
|
*/ |
781
|
|
|
function insert( $table, $args, $fname = __METHOD__, $options = [] ) { |
782
|
|
|
if ( !count( $args ) ) { |
783
|
|
|
return true; |
784
|
|
|
} |
785
|
|
|
|
786
|
|
|
$table = $this->tableName( $table ); |
787
|
|
|
if ( !isset( $this->numericVersion ) ) { |
788
|
|
|
$this->getServerVersion(); |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
if ( !is_array( $options ) ) { |
792
|
|
|
$options = [ $options ]; |
793
|
|
|
} |
794
|
|
|
|
795
|
|
View Code Duplication |
if ( isset( $args[0] ) && is_array( $args[0] ) ) { |
796
|
|
|
$multi = true; |
797
|
|
|
$keys = array_keys( $args[0] ); |
798
|
|
|
} else { |
799
|
|
|
$multi = false; |
800
|
|
|
$keys = array_keys( $args ); |
801
|
|
|
} |
802
|
|
|
|
803
|
|
|
// If IGNORE is set, we use savepoints to emulate mysql's behavior |
804
|
|
|
$savepoint = null; |
805
|
|
View Code Duplication |
if ( in_array( 'IGNORE', $options ) ) { |
806
|
|
|
$savepoint = new SavepointPostgres( $this, 'mw' ); |
807
|
|
|
$olde = error_reporting( 0 ); |
808
|
|
|
// For future use, we may want to track the number of actual inserts |
809
|
|
|
// Right now, insert (all writes) simply return true/false |
810
|
|
|
$numrowsinserted = 0; |
811
|
|
|
} |
812
|
|
|
|
813
|
|
|
$sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES '; |
814
|
|
|
|
815
|
|
|
if ( $multi ) { |
816
|
|
|
if ( $this->numericVersion >= 8.2 && !$savepoint ) { |
817
|
|
|
$first = true; |
818
|
|
View Code Duplication |
foreach ( $args as $row ) { |
819
|
|
|
if ( $first ) { |
820
|
|
|
$first = false; |
821
|
|
|
} else { |
822
|
|
|
$sql .= ','; |
823
|
|
|
} |
824
|
|
|
$sql .= '(' . $this->makeList( $row ) . ')'; |
825
|
|
|
} |
826
|
|
|
$res = (bool)$this->query( $sql, $fname, $savepoint ); |
827
|
|
|
} else { |
828
|
|
|
$res = true; |
829
|
|
|
$origsql = $sql; |
830
|
|
|
foreach ( $args as $row ) { |
831
|
|
|
$tempsql = $origsql; |
832
|
|
|
$tempsql .= '(' . $this->makeList( $row ) . ')'; |
833
|
|
|
|
834
|
|
|
if ( $savepoint ) { |
835
|
|
|
$savepoint->savepoint(); |
836
|
|
|
} |
837
|
|
|
|
838
|
|
|
$tempres = (bool)$this->query( $tempsql, $fname, $savepoint ); |
839
|
|
|
|
840
|
|
View Code Duplication |
if ( $savepoint ) { |
841
|
|
|
$bar = pg_result_error( $this->mLastResult ); |
842
|
|
|
if ( $bar != false ) { |
|
|
|
|
843
|
|
|
$savepoint->rollback(); |
844
|
|
|
} else { |
845
|
|
|
$savepoint->release(); |
846
|
|
|
$numrowsinserted++; |
|
|
|
|
847
|
|
|
} |
848
|
|
|
} |
849
|
|
|
|
850
|
|
|
// If any of them fail, we fail overall for this function call |
851
|
|
|
// Note that this will be ignored if IGNORE is set |
852
|
|
|
if ( !$tempres ) { |
853
|
|
|
$res = false; |
854
|
|
|
} |
855
|
|
|
} |
856
|
|
|
} |
857
|
|
|
} else { |
858
|
|
|
// Not multi, just a lone insert |
859
|
|
|
if ( $savepoint ) { |
860
|
|
|
$savepoint->savepoint(); |
861
|
|
|
} |
862
|
|
|
|
863
|
|
|
$sql .= '(' . $this->makeList( $args ) . ')'; |
864
|
|
|
$res = (bool)$this->query( $sql, $fname, $savepoint ); |
865
|
|
View Code Duplication |
if ( $savepoint ) { |
866
|
|
|
$bar = pg_result_error( $this->mLastResult ); |
867
|
|
|
if ( $bar != false ) { |
|
|
|
|
868
|
|
|
$savepoint->rollback(); |
869
|
|
|
} else { |
870
|
|
|
$savepoint->release(); |
871
|
|
|
$numrowsinserted++; |
872
|
|
|
} |
873
|
|
|
} |
874
|
|
|
} |
875
|
|
|
if ( $savepoint ) { |
876
|
|
|
error_reporting( $olde ); |
|
|
|
|
877
|
|
|
$savepoint->commit(); |
878
|
|
|
|
879
|
|
|
// Set the affected row count for the whole operation |
880
|
|
|
$this->mAffectedRows = $numrowsinserted; |
881
|
|
|
|
882
|
|
|
// IGNORE always returns true |
883
|
|
|
return true; |
884
|
|
|
} |
885
|
|
|
|
886
|
|
|
return $res; |
887
|
|
|
} |
888
|
|
|
|
889
|
|
|
/** |
890
|
|
|
* INSERT SELECT wrapper |
891
|
|
|
* $varMap must be an associative array of the form [ 'dest1' => 'source1', ... ] |
892
|
|
|
* Source items may be literals rather then field names, but strings should |
893
|
|
|
* be quoted with Database::addQuotes() |
894
|
|
|
* $conds may be "*" to copy the whole table |
895
|
|
|
* srcTable may be an array of tables. |
896
|
|
|
* @todo FIXME: Implement this a little better (seperate select/insert)? |
897
|
|
|
* |
898
|
|
|
* @param string $destTable |
899
|
|
|
* @param array|string $srcTable |
900
|
|
|
* @param array $varMap |
901
|
|
|
* @param array $conds |
902
|
|
|
* @param string $fname |
903
|
|
|
* @param array $insertOptions |
904
|
|
|
* @param array $selectOptions |
905
|
|
|
* @return bool |
906
|
|
|
*/ |
907
|
|
|
function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, |
908
|
|
|
$insertOptions = [], $selectOptions = [] ) { |
909
|
|
|
$destTable = $this->tableName( $destTable ); |
910
|
|
|
|
911
|
|
|
if ( !is_array( $insertOptions ) ) { |
912
|
|
|
$insertOptions = [ $insertOptions ]; |
913
|
|
|
} |
914
|
|
|
|
915
|
|
|
/* |
916
|
|
|
* If IGNORE is set, we use savepoints to emulate mysql's behavior |
917
|
|
|
* Ignore LOW PRIORITY option, since it is MySQL-specific |
918
|
|
|
*/ |
919
|
|
|
$savepoint = null; |
920
|
|
View Code Duplication |
if ( in_array( 'IGNORE', $insertOptions ) ) { |
921
|
|
|
$savepoint = new SavepointPostgres( $this, 'mw' ); |
922
|
|
|
$olde = error_reporting( 0 ); |
923
|
|
|
$numrowsinserted = 0; |
924
|
|
|
$savepoint->savepoint(); |
925
|
|
|
} |
926
|
|
|
|
927
|
|
|
if ( !is_array( $selectOptions ) ) { |
928
|
|
|
$selectOptions = [ $selectOptions ]; |
929
|
|
|
} |
930
|
|
|
list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = |
931
|
|
|
$this->makeSelectOptions( $selectOptions ); |
932
|
|
View Code Duplication |
if ( is_array( $srcTable ) ) { |
933
|
|
|
$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) ); |
934
|
|
|
} else { |
935
|
|
|
$srcTable = $this->tableName( $srcTable ); |
936
|
|
|
} |
937
|
|
|
|
938
|
|
|
$sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . |
939
|
|
|
" SELECT $startOpts " . implode( ',', $varMap ) . |
940
|
|
|
" FROM $srcTable $useIndex $ignoreIndex "; |
941
|
|
|
|
942
|
|
|
if ( $conds != '*' ) { |
943
|
|
|
$sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); |
944
|
|
|
} |
945
|
|
|
|
946
|
|
|
$sql .= " $tailOpts"; |
947
|
|
|
|
948
|
|
|
$res = (bool)$this->query( $sql, $fname, $savepoint ); |
949
|
|
|
if ( $savepoint ) { |
950
|
|
|
$bar = pg_result_error( $this->mLastResult ); |
951
|
|
|
if ( $bar != false ) { |
|
|
|
|
952
|
|
|
$savepoint->rollback(); |
953
|
|
|
} else { |
954
|
|
|
$savepoint->release(); |
955
|
|
|
$numrowsinserted++; |
|
|
|
|
956
|
|
|
} |
957
|
|
|
error_reporting( $olde ); |
|
|
|
|
958
|
|
|
$savepoint->commit(); |
959
|
|
|
|
960
|
|
|
// Set the affected row count for the whole operation |
961
|
|
|
$this->mAffectedRows = $numrowsinserted; |
962
|
|
|
|
963
|
|
|
// IGNORE always returns true |
964
|
|
|
return true; |
965
|
|
|
} |
966
|
|
|
|
967
|
|
|
return $res; |
968
|
|
|
} |
969
|
|
|
|
970
|
|
View Code Duplication |
function tableName( $name, $format = 'quoted' ) { |
971
|
|
|
# Replace reserved words with better ones |
972
|
|
|
switch ( $name ) { |
973
|
|
|
case 'user': |
974
|
|
|
return $this->realTableName( 'mwuser', $format ); |
975
|
|
|
case 'text': |
976
|
|
|
return $this->realTableName( 'pagecontent', $format ); |
977
|
|
|
default: |
978
|
|
|
return $this->realTableName( $name, $format ); |
979
|
|
|
} |
980
|
|
|
} |
981
|
|
|
|
982
|
|
|
/* Don't cheat on installer */ |
983
|
|
|
function realTableName( $name, $format = 'quoted' ) { |
984
|
|
|
return parent::tableName( $name, $format ); |
|
|
|
|
985
|
|
|
} |
986
|
|
|
|
987
|
|
|
/** |
988
|
|
|
* Return the next in a sequence, save the value for retrieval via insertId() |
989
|
|
|
* |
990
|
|
|
* @param string $seqName |
991
|
|
|
* @return int|null |
992
|
|
|
*/ |
993
|
|
|
function nextSequenceValue( $seqName ) { |
994
|
|
|
$safeseq = str_replace( "'", "''", $seqName ); |
995
|
|
|
$res = $this->query( "SELECT nextval('$safeseq')" ); |
996
|
|
|
$row = $this->fetchRow( $res ); |
|
|
|
|
997
|
|
|
$this->mInsertId = $row[0]; |
998
|
|
|
|
999
|
|
|
return $this->mInsertId; |
1000
|
|
|
} |
1001
|
|
|
|
1002
|
|
|
/** |
1003
|
|
|
* Return the current value of a sequence. Assumes it has been nextval'ed in this session. |
1004
|
|
|
* |
1005
|
|
|
* @param string $seqName |
1006
|
|
|
* @return int |
1007
|
|
|
*/ |
1008
|
|
|
function currentSequenceValue( $seqName ) { |
1009
|
|
|
$safeseq = str_replace( "'", "''", $seqName ); |
1010
|
|
|
$res = $this->query( "SELECT currval('$safeseq')" ); |
1011
|
|
|
$row = $this->fetchRow( $res ); |
|
|
|
|
1012
|
|
|
$currval = $row[0]; |
1013
|
|
|
|
1014
|
|
|
return $currval; |
1015
|
|
|
} |
1016
|
|
|
|
1017
|
|
|
# Returns the size of a text field, or -1 for "unlimited" |
1018
|
|
|
function textFieldSize( $table, $field ) { |
1019
|
|
|
$table = $this->tableName( $table ); |
1020
|
|
|
$sql = "SELECT t.typname as ftype,a.atttypmod as size |
1021
|
|
|
FROM pg_class c, pg_attribute a, pg_type t |
1022
|
|
|
WHERE relname='$table' AND a.attrelid=c.oid AND |
1023
|
|
|
a.atttypid=t.oid and a.attname='$field'"; |
1024
|
|
|
$res = $this->query( $sql ); |
1025
|
|
|
$row = $this->fetchObject( $res ); |
|
|
|
|
1026
|
|
|
if ( $row->ftype == 'varchar' ) { |
1027
|
|
|
$size = $row->size - 4; |
1028
|
|
|
} else { |
1029
|
|
|
$size = $row->size; |
1030
|
|
|
} |
1031
|
|
|
|
1032
|
|
|
return $size; |
1033
|
|
|
} |
1034
|
|
|
|
1035
|
|
|
function limitResult( $sql, $limit, $offset = false ) { |
1036
|
|
|
return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' ); |
1037
|
|
|
} |
1038
|
|
|
|
1039
|
|
|
function wasDeadlock() { |
1040
|
|
|
return $this->lastErrno() == '40P01'; |
1041
|
|
|
} |
1042
|
|
|
|
1043
|
|
|
function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) { |
1044
|
|
|
$newName = $this->addIdentifierQuotes( $newName ); |
1045
|
|
|
$oldName = $this->addIdentifierQuotes( $oldName ); |
1046
|
|
|
|
1047
|
|
|
return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName " . |
1048
|
|
|
"(LIKE $oldName INCLUDING DEFAULTS)", $fname ); |
1049
|
|
|
} |
1050
|
|
|
|
1051
|
|
|
function listTables( $prefix = null, $fname = __METHOD__ ) { |
1052
|
|
|
$eschema = $this->addQuotes( $this->getCoreSchema() ); |
1053
|
|
|
$result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname ); |
1054
|
|
|
$endArray = []; |
1055
|
|
|
|
1056
|
|
View Code Duplication |
foreach ( $result as $table ) { |
|
|
|
|
1057
|
|
|
$vars = get_object_vars( $table ); |
1058
|
|
|
$table = array_pop( $vars ); |
1059
|
|
|
if ( !$prefix || strpos( $table, $prefix ) === 0 ) { |
|
|
|
|
1060
|
|
|
$endArray[] = $table; |
1061
|
|
|
} |
1062
|
|
|
} |
1063
|
|
|
|
1064
|
|
|
return $endArray; |
1065
|
|
|
} |
1066
|
|
|
|
1067
|
|
|
function timestamp( $ts = 0 ) { |
1068
|
|
|
return wfTimestamp( TS_POSTGRES, $ts ); |
1069
|
|
|
} |
1070
|
|
|
|
1071
|
|
|
/** |
1072
|
|
|
* Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12 |
1073
|
|
|
* to http://www.php.net/manual/en/ref.pgsql.php |
1074
|
|
|
* |
1075
|
|
|
* Parsing a postgres array can be a tricky problem, he's my |
1076
|
|
|
* take on this, it handles multi-dimensional arrays plus |
1077
|
|
|
* escaping using a nasty regexp to determine the limits of each |
1078
|
|
|
* data-item. |
1079
|
|
|
* |
1080
|
|
|
* This should really be handled by PHP PostgreSQL module |
1081
|
|
|
* |
1082
|
|
|
* @since 1.19 |
1083
|
|
|
* @param string $text Postgreql array returned in a text form like {a,b} |
1084
|
|
|
* @param string $output |
1085
|
|
|
* @param int $limit |
1086
|
|
|
* @param int $offset |
1087
|
|
|
* @return string |
1088
|
|
|
*/ |
1089
|
|
|
function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) { |
1090
|
|
|
if ( false === $limit ) { |
1091
|
|
|
$limit = strlen( $text ) - 1; |
1092
|
|
|
$output = []; |
1093
|
|
|
} |
1094
|
|
|
if ( '{}' == $text ) { |
1095
|
|
|
return $output; |
1096
|
|
|
} |
1097
|
|
|
do { |
1098
|
|
|
if ( '{' != $text[$offset] ) { |
1099
|
|
|
preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/", |
1100
|
|
|
$text, $match, 0, $offset ); |
1101
|
|
|
$offset += strlen( $match[0] ); |
1102
|
|
|
$output[] = ( '"' != $match[1][0] |
1103
|
|
|
? $match[1] |
1104
|
|
|
: stripcslashes( substr( $match[1], 1, -1 ) ) ); |
1105
|
|
|
if ( '},' == $match[3] ) { |
1106
|
|
|
return $output; |
1107
|
|
|
} |
1108
|
|
|
} else { |
1109
|
|
|
$offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 ); |
|
|
|
|
1110
|
|
|
} |
1111
|
|
|
} while ( $limit > $offset ); |
1112
|
|
|
|
1113
|
|
|
return $output; |
1114
|
|
|
} |
1115
|
|
|
|
1116
|
|
|
/** |
1117
|
|
|
* Return aggregated value function call |
1118
|
|
|
* @param array $valuedata |
1119
|
|
|
* @param string $valuename |
1120
|
|
|
* @return array |
1121
|
|
|
*/ |
1122
|
|
|
public function aggregateValue( $valuedata, $valuename = 'value' ) { |
1123
|
|
|
return $valuedata; |
1124
|
|
|
} |
1125
|
|
|
|
1126
|
|
|
/** |
1127
|
|
|
* @return string Wikitext of a link to the server software's web site |
1128
|
|
|
*/ |
1129
|
|
|
public function getSoftwareLink() { |
1130
|
|
|
return '[{{int:version-db-postgres-url}} PostgreSQL]'; |
1131
|
|
|
} |
1132
|
|
|
|
1133
|
|
|
/** |
1134
|
|
|
* Return current schema (executes SELECT current_schema()) |
1135
|
|
|
* Needs transaction |
1136
|
|
|
* |
1137
|
|
|
* @since 1.19 |
1138
|
|
|
* @return string Default schema for the current session |
1139
|
|
|
*/ |
1140
|
|
|
function getCurrentSchema() { |
1141
|
|
|
$res = $this->query( "SELECT current_schema()", __METHOD__ ); |
1142
|
|
|
$row = $this->fetchRow( $res ); |
|
|
|
|
1143
|
|
|
|
1144
|
|
|
return $row[0]; |
1145
|
|
|
} |
1146
|
|
|
|
1147
|
|
|
/** |
1148
|
|
|
* Return list of schemas which are accessible without schema name |
1149
|
|
|
* This is list does not contain magic keywords like "$user" |
1150
|
|
|
* Needs transaction |
1151
|
|
|
* |
1152
|
|
|
* @see getSearchPath() |
1153
|
|
|
* @see setSearchPath() |
1154
|
|
|
* @since 1.19 |
1155
|
|
|
* @return array List of actual schemas for the current sesson |
1156
|
|
|
*/ |
1157
|
|
|
function getSchemas() { |
1158
|
|
|
$res = $this->query( "SELECT current_schemas(false)", __METHOD__ ); |
1159
|
|
|
$row = $this->fetchRow( $res ); |
|
|
|
|
1160
|
|
|
$schemas = []; |
1161
|
|
|
|
1162
|
|
|
/* PHP pgsql support does not support array type, "{a,b}" string is returned */ |
1163
|
|
|
|
1164
|
|
|
return $this->pg_array_parse( $row[0], $schemas ); |
1165
|
|
|
} |
1166
|
|
|
|
1167
|
|
|
/** |
1168
|
|
|
* Return search patch for schemas |
1169
|
|
|
* This is different from getSchemas() since it contain magic keywords |
1170
|
|
|
* (like "$user"). |
1171
|
|
|
* Needs transaction |
1172
|
|
|
* |
1173
|
|
|
* @since 1.19 |
1174
|
|
|
* @return array How to search for table names schemas for the current user |
1175
|
|
|
*/ |
1176
|
|
|
function getSearchPath() { |
1177
|
|
|
$res = $this->query( "SHOW search_path", __METHOD__ ); |
1178
|
|
|
$row = $this->fetchRow( $res ); |
|
|
|
|
1179
|
|
|
|
1180
|
|
|
/* PostgreSQL returns SHOW values as strings */ |
1181
|
|
|
|
1182
|
|
|
return explode( ",", $row[0] ); |
1183
|
|
|
} |
1184
|
|
|
|
1185
|
|
|
/** |
1186
|
|
|
* Update search_path, values should already be sanitized |
1187
|
|
|
* Values may contain magic keywords like "$user" |
1188
|
|
|
* @since 1.19 |
1189
|
|
|
* |
1190
|
|
|
* @param array $search_path List of schemas to be searched by default |
1191
|
|
|
*/ |
1192
|
|
|
function setSearchPath( $search_path ) { |
1193
|
|
|
$this->query( "SET search_path = " . implode( ", ", $search_path ) ); |
1194
|
|
|
} |
1195
|
|
|
|
1196
|
|
|
/** |
1197
|
|
|
* Determine default schema for MediaWiki core |
1198
|
|
|
* Adjust this session schema search path if desired schema exists |
1199
|
|
|
* and is not alread there. |
1200
|
|
|
* |
1201
|
|
|
* We need to have name of the core schema stored to be able |
1202
|
|
|
* to query database metadata. |
1203
|
|
|
* |
1204
|
|
|
* This will be also called by the installer after the schema is created |
1205
|
|
|
* |
1206
|
|
|
* @since 1.19 |
1207
|
|
|
* |
1208
|
|
|
* @param string $desiredSchema |
1209
|
|
|
*/ |
1210
|
|
|
function determineCoreSchema( $desiredSchema ) { |
1211
|
|
|
$this->begin( __METHOD__, self::TRANSACTION_INTERNAL ); |
1212
|
|
|
if ( $this->schemaExists( $desiredSchema ) ) { |
1213
|
|
|
if ( in_array( $desiredSchema, $this->getSchemas() ) ) { |
1214
|
|
|
$this->mCoreSchema = $desiredSchema; |
1215
|
|
|
wfDebug( "Schema \"" . $desiredSchema . "\" already in the search path\n" ); |
1216
|
|
|
} else { |
1217
|
|
|
/** |
1218
|
|
|
* Prepend our schema (e.g. 'mediawiki') in front |
1219
|
|
|
* of the search path |
1220
|
|
|
* Fixes bug 15816 |
1221
|
|
|
*/ |
1222
|
|
|
$search_path = $this->getSearchPath(); |
1223
|
|
|
array_unshift( $search_path, |
1224
|
|
|
$this->addIdentifierQuotes( $desiredSchema ) ); |
1225
|
|
|
$this->setSearchPath( $search_path ); |
1226
|
|
|
$this->mCoreSchema = $desiredSchema; |
1227
|
|
|
wfDebug( "Schema \"" . $desiredSchema . "\" added to the search path\n" ); |
1228
|
|
|
} |
1229
|
|
|
} else { |
1230
|
|
|
$this->mCoreSchema = $this->getCurrentSchema(); |
1231
|
|
|
wfDebug( "Schema \"" . $desiredSchema . "\" not found, using current \"" . |
1232
|
|
|
$this->mCoreSchema . "\"\n" ); |
1233
|
|
|
} |
1234
|
|
|
/* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */ |
1235
|
|
|
$this->commit( __METHOD__ ); |
1236
|
|
|
} |
1237
|
|
|
|
1238
|
|
|
/** |
1239
|
|
|
* Return schema name fore core MediaWiki tables |
1240
|
|
|
* |
1241
|
|
|
* @since 1.19 |
1242
|
|
|
* @return string Core schema name |
1243
|
|
|
*/ |
1244
|
|
|
function getCoreSchema() { |
1245
|
|
|
return $this->mCoreSchema; |
1246
|
|
|
} |
1247
|
|
|
|
1248
|
|
|
/** |
1249
|
|
|
* @return string Version information from the database |
1250
|
|
|
*/ |
1251
|
|
|
function getServerVersion() { |
1252
|
|
|
if ( !isset( $this->numericVersion ) ) { |
1253
|
|
|
$versionInfo = pg_version( $this->mConn ); |
1254
|
|
|
if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) { |
1255
|
|
|
// Old client, abort install |
1256
|
|
|
$this->numericVersion = '7.3 or earlier'; |
1257
|
|
|
} elseif ( isset( $versionInfo['server'] ) ) { |
1258
|
|
|
// Normal client |
1259
|
|
|
$this->numericVersion = $versionInfo['server']; |
1260
|
|
|
} else { |
1261
|
|
|
// Bug 16937: broken pgsql extension from PHP<5.3 |
1262
|
|
|
$this->numericVersion = pg_parameter_status( $this->mConn, 'server_version' ); |
1263
|
|
|
} |
1264
|
|
|
} |
1265
|
|
|
|
1266
|
|
|
return $this->numericVersion; |
1267
|
|
|
} |
1268
|
|
|
|
1269
|
|
|
/** |
1270
|
|
|
* Query whether a given relation exists (in the given schema, or the |
1271
|
|
|
* default mw one if not given) |
1272
|
|
|
* @param string $table |
1273
|
|
|
* @param array|string $types |
1274
|
|
|
* @param bool|string $schema |
1275
|
|
|
* @return bool |
1276
|
|
|
*/ |
1277
|
|
|
function relationExists( $table, $types, $schema = false ) { |
1278
|
|
|
if ( !is_array( $types ) ) { |
1279
|
|
|
$types = [ $types ]; |
1280
|
|
|
} |
1281
|
|
|
if ( !$schema ) { |
1282
|
|
|
$schema = $this->getCoreSchema(); |
1283
|
|
|
} |
1284
|
|
|
$table = $this->realTableName( $table, 'raw' ); |
1285
|
|
|
$etable = $this->addQuotes( $table ); |
1286
|
|
|
$eschema = $this->addQuotes( $schema ); |
|
|
|
|
1287
|
|
|
$sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n " |
1288
|
|
|
. "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema " |
1289
|
|
|
. "AND c.relkind IN ('" . implode( "','", $types ) . "')"; |
1290
|
|
|
$res = $this->query( $sql ); |
1291
|
|
|
$count = $res ? $res->numRows() : 0; |
1292
|
|
|
|
1293
|
|
|
return (bool)$count; |
1294
|
|
|
} |
1295
|
|
|
|
1296
|
|
|
/** |
1297
|
|
|
* For backward compatibility, this function checks both tables and |
1298
|
|
|
* views. |
1299
|
|
|
* @param string $table |
1300
|
|
|
* @param string $fname |
1301
|
|
|
* @param bool|string $schema |
1302
|
|
|
* @return bool |
1303
|
|
|
*/ |
1304
|
|
|
function tableExists( $table, $fname = __METHOD__, $schema = false ) { |
1305
|
|
|
return $this->relationExists( $table, [ 'r', 'v' ], $schema ); |
1306
|
|
|
} |
1307
|
|
|
|
1308
|
|
|
function sequenceExists( $sequence, $schema = false ) { |
1309
|
|
|
return $this->relationExists( $sequence, 'S', $schema ); |
1310
|
|
|
} |
1311
|
|
|
|
1312
|
|
View Code Duplication |
function triggerExists( $table, $trigger ) { |
1313
|
|
|
$q = <<<SQL |
1314
|
|
|
SELECT 1 FROM pg_class, pg_namespace, pg_trigger |
1315
|
|
|
WHERE relnamespace=pg_namespace.oid AND relkind='r' |
1316
|
|
|
AND tgrelid=pg_class.oid |
1317
|
|
|
AND nspname=%s AND relname=%s AND tgname=%s |
1318
|
|
|
SQL; |
1319
|
|
|
$res = $this->query( |
1320
|
|
|
sprintf( |
1321
|
|
|
$q, |
1322
|
|
|
$this->addQuotes( $this->getCoreSchema() ), |
1323
|
|
|
$this->addQuotes( $table ), |
1324
|
|
|
$this->addQuotes( $trigger ) |
1325
|
|
|
) |
1326
|
|
|
); |
1327
|
|
|
if ( !$res ) { |
1328
|
|
|
return null; |
1329
|
|
|
} |
1330
|
|
|
$rows = $res->numRows(); |
1331
|
|
|
|
1332
|
|
|
return $rows; |
1333
|
|
|
} |
1334
|
|
|
|
1335
|
|
|
function ruleExists( $table, $rule ) { |
1336
|
|
|
$exists = $this->selectField( 'pg_rules', 'rulename', |
1337
|
|
|
[ |
1338
|
|
|
'rulename' => $rule, |
1339
|
|
|
'tablename' => $table, |
1340
|
|
|
'schemaname' => $this->getCoreSchema() |
1341
|
|
|
] |
1342
|
|
|
); |
1343
|
|
|
|
1344
|
|
|
return $exists === $rule; |
1345
|
|
|
} |
1346
|
|
|
|
1347
|
|
View Code Duplication |
function constraintExists( $table, $constraint ) { |
1348
|
|
|
$sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " . |
1349
|
|
|
"WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s", |
1350
|
|
|
$this->addQuotes( $this->getCoreSchema() ), |
1351
|
|
|
$this->addQuotes( $table ), |
1352
|
|
|
$this->addQuotes( $constraint ) |
1353
|
|
|
); |
1354
|
|
|
$res = $this->query( $sql ); |
1355
|
|
|
if ( !$res ) { |
1356
|
|
|
return null; |
1357
|
|
|
} |
1358
|
|
|
$rows = $res->numRows(); |
1359
|
|
|
|
1360
|
|
|
return $rows; |
1361
|
|
|
} |
1362
|
|
|
|
1363
|
|
|
/** |
1364
|
|
|
* Query whether a given schema exists. Returns true if it does, false if it doesn't. |
1365
|
|
|
* @param string $schema |
1366
|
|
|
* @return bool |
1367
|
|
|
*/ |
1368
|
|
|
function schemaExists( $schema ) { |
1369
|
|
|
$exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1, |
1370
|
|
|
[ 'nspname' => $schema ], __METHOD__ ); |
1371
|
|
|
|
1372
|
|
|
return (bool)$exists; |
1373
|
|
|
} |
1374
|
|
|
|
1375
|
|
|
/** |
1376
|
|
|
* Returns true if a given role (i.e. user) exists, false otherwise. |
1377
|
|
|
* @param string $roleName |
1378
|
|
|
* @return bool |
1379
|
|
|
*/ |
1380
|
|
|
function roleExists( $roleName ) { |
1381
|
|
|
$exists = $this->selectField( '"pg_catalog"."pg_roles"', 1, |
1382
|
|
|
[ 'rolname' => $roleName ], __METHOD__ ); |
1383
|
|
|
|
1384
|
|
|
return (bool)$exists; |
1385
|
|
|
} |
1386
|
|
|
|
1387
|
|
|
/** |
1388
|
|
|
* @var string $table |
1389
|
|
|
* @var string $field |
1390
|
|
|
* @return PostgresField|null |
1391
|
|
|
*/ |
1392
|
|
|
function fieldInfo( $table, $field ) { |
1393
|
|
|
return PostgresField::fromText( $this, $table, $field ); |
1394
|
|
|
} |
1395
|
|
|
|
1396
|
|
|
/** |
1397
|
|
|
* pg_field_type() wrapper |
1398
|
|
|
* @param ResultWrapper|resource $res ResultWrapper or PostgreSQL query result resource |
1399
|
|
|
* @param int $index Field number, starting from 0 |
1400
|
|
|
* @return string |
1401
|
|
|
*/ |
1402
|
|
|
function fieldType( $res, $index ) { |
1403
|
|
|
if ( $res instanceof ResultWrapper ) { |
1404
|
|
|
$res = $res->result; |
1405
|
|
|
} |
1406
|
|
|
|
1407
|
|
|
return pg_field_type( $res, $index ); |
1408
|
|
|
} |
1409
|
|
|
|
1410
|
|
|
/** |
1411
|
|
|
* @param string $b |
1412
|
|
|
* @return Blob |
1413
|
|
|
*/ |
1414
|
|
|
function encodeBlob( $b ) { |
1415
|
|
|
return new PostgresBlob( pg_escape_bytea( $b ) ); |
1416
|
|
|
} |
1417
|
|
|
|
1418
|
|
|
function decodeBlob( $b ) { |
1419
|
|
|
if ( $b instanceof PostgresBlob ) { |
1420
|
|
|
$b = $b->fetch(); |
1421
|
|
|
} elseif ( $b instanceof Blob ) { |
1422
|
|
|
return $b->fetch(); |
1423
|
|
|
} |
1424
|
|
|
|
1425
|
|
|
return pg_unescape_bytea( $b ); |
1426
|
|
|
} |
1427
|
|
|
|
1428
|
|
|
function strencode( $s ) { |
1429
|
|
|
// Should not be called by us |
1430
|
|
|
|
1431
|
|
|
return pg_escape_string( $this->mConn, $s ); |
1432
|
|
|
} |
1433
|
|
|
|
1434
|
|
|
/** |
1435
|
|
|
* @param null|bool|Blob $s |
1436
|
|
|
* @return int|string |
1437
|
|
|
*/ |
1438
|
|
|
function addQuotes( $s ) { |
1439
|
|
|
if ( is_null( $s ) ) { |
1440
|
|
|
return 'NULL'; |
1441
|
|
|
} elseif ( is_bool( $s ) ) { |
1442
|
|
|
return intval( $s ); |
1443
|
|
|
} elseif ( $s instanceof Blob ) { |
1444
|
|
|
if ( $s instanceof PostgresBlob ) { |
1445
|
|
|
$s = $s->fetch(); |
1446
|
|
|
} else { |
1447
|
|
|
$s = pg_escape_bytea( $this->mConn, $s->fetch() ); |
1448
|
|
|
} |
1449
|
|
|
return "'$s'"; |
1450
|
|
|
} |
1451
|
|
|
|
1452
|
|
|
return "'" . pg_escape_string( $this->mConn, $s ) . "'"; |
1453
|
|
|
} |
1454
|
|
|
|
1455
|
|
|
/** |
1456
|
|
|
* Postgres specific version of replaceVars. |
1457
|
|
|
* Calls the parent version in Database.php |
1458
|
|
|
* |
1459
|
|
|
* @param string $ins SQL string, read from a stream (usually tables.sql) |
1460
|
|
|
* @return string SQL string |
1461
|
|
|
*/ |
1462
|
|
|
protected function replaceVars( $ins ) { |
1463
|
|
|
$ins = parent::replaceVars( $ins ); |
1464
|
|
|
|
1465
|
|
|
if ( $this->numericVersion >= 8.3 ) { |
1466
|
|
|
// Thanks for not providing backwards-compatibility, 8.3 |
1467
|
|
|
$ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins ); |
1468
|
|
|
} |
1469
|
|
|
|
1470
|
|
|
if ( $this->numericVersion <= 8.1 ) { // Our minimum version |
1471
|
|
|
$ins = str_replace( 'USING gin', 'USING gist', $ins ); |
1472
|
|
|
} |
1473
|
|
|
|
1474
|
|
|
return $ins; |
1475
|
|
|
} |
1476
|
|
|
|
1477
|
|
|
/** |
1478
|
|
|
* Various select options |
1479
|
|
|
* |
1480
|
|
|
* @param array $options An associative array of options to be turned into |
1481
|
|
|
* an SQL query, valid keys are listed in the function. |
1482
|
|
|
* @return array |
1483
|
|
|
*/ |
1484
|
|
|
function makeSelectOptions( $options ) { |
1485
|
|
|
$preLimitTail = $postLimitTail = ''; |
1486
|
|
|
$startOpts = $useIndex = $ignoreIndex = ''; |
1487
|
|
|
|
1488
|
|
|
$noKeyOptions = []; |
1489
|
|
|
foreach ( $options as $key => $option ) { |
1490
|
|
|
if ( is_numeric( $key ) ) { |
1491
|
|
|
$noKeyOptions[$option] = true; |
1492
|
|
|
} |
1493
|
|
|
} |
1494
|
|
|
|
1495
|
|
|
$preLimitTail .= $this->makeGroupByWithHaving( $options ); |
1496
|
|
|
|
1497
|
|
|
$preLimitTail .= $this->makeOrderBy( $options ); |
1498
|
|
|
|
1499
|
|
|
// if ( isset( $options['LIMIT'] ) ) { |
1500
|
|
|
// $tailOpts .= $this->limitResult( '', $options['LIMIT'], |
1501
|
|
|
// isset( $options['OFFSET'] ) ? $options['OFFSET'] |
1502
|
|
|
// : false ); |
1503
|
|
|
// } |
1504
|
|
|
|
1505
|
|
|
if ( isset( $options['FOR UPDATE'] ) ) { |
1506
|
|
|
$postLimitTail .= ' FOR UPDATE OF ' . |
1507
|
|
|
implode( ', ', array_map( [ &$this, 'tableName' ], $options['FOR UPDATE'] ) ); |
1508
|
|
|
} elseif ( isset( $noKeyOptions['FOR UPDATE'] ) ) { |
1509
|
|
|
$postLimitTail .= ' FOR UPDATE'; |
1510
|
|
|
} |
1511
|
|
|
|
1512
|
|
|
if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) { |
1513
|
|
|
$startOpts .= 'DISTINCT'; |
1514
|
|
|
} |
1515
|
|
|
|
1516
|
|
|
return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ]; |
1517
|
|
|
} |
1518
|
|
|
|
1519
|
|
|
function getDBname() { |
1520
|
|
|
return $this->mDBname; |
1521
|
|
|
} |
1522
|
|
|
|
1523
|
|
|
function getServer() { |
1524
|
|
|
return $this->mServer; |
1525
|
|
|
} |
1526
|
|
|
|
1527
|
|
|
function buildConcat( $stringList ) { |
1528
|
|
|
return implode( ' || ', $stringList ); |
1529
|
|
|
} |
1530
|
|
|
|
1531
|
|
View Code Duplication |
public function buildGroupConcatField( |
1532
|
|
|
$delimiter, $table, $field, $conds = '', $options = [], $join_conds = [] |
1533
|
|
|
) { |
1534
|
|
|
$fld = "array_to_string(array_agg($field)," . $this->addQuotes( $delimiter ) . ')'; |
1535
|
|
|
|
1536
|
|
|
return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')'; |
1537
|
|
|
} |
1538
|
|
|
|
1539
|
|
|
/** |
1540
|
|
|
* @param string $field Field or column to cast |
1541
|
|
|
* @return string |
1542
|
|
|
* @since 1.28 |
1543
|
|
|
*/ |
1544
|
|
|
public function buildStringCast( $field ) { |
1545
|
|
|
return $field . '::text'; |
1546
|
|
|
} |
1547
|
|
|
|
1548
|
|
|
public function getSearchEngine() { |
1549
|
|
|
return 'SearchPostgres'; |
1550
|
|
|
} |
1551
|
|
|
|
1552
|
|
|
public function streamStatementEnd( &$sql, &$newLine ) { |
1553
|
|
|
# Allow dollar quoting for function declarations |
1554
|
|
|
if ( substr( $newLine, 0, 4 ) == '$mw$' ) { |
1555
|
|
|
if ( $this->delimiter ) { |
1556
|
|
|
$this->delimiter = false; |
|
|
|
|
1557
|
|
|
} else { |
1558
|
|
|
$this->delimiter = ';'; |
1559
|
|
|
} |
1560
|
|
|
} |
1561
|
|
|
|
1562
|
|
|
return parent::streamStatementEnd( $sql, $newLine ); |
1563
|
|
|
} |
1564
|
|
|
|
1565
|
|
|
/** |
1566
|
|
|
* Check to see if a named lock is available. This is non-blocking. |
1567
|
|
|
* See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS |
1568
|
|
|
* |
1569
|
|
|
* @param string $lockName Name of lock to poll |
1570
|
|
|
* @param string $method Name of method calling us |
1571
|
|
|
* @return bool |
1572
|
|
|
* @since 1.20 |
1573
|
|
|
*/ |
1574
|
|
View Code Duplication |
public function lockIsFree( $lockName, $method ) { |
1575
|
|
|
$key = $this->addQuotes( $this->bigintFromLockName( $lockName ) ); |
|
|
|
|
1576
|
|
|
$result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key)) |
1577
|
|
|
WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method ); |
1578
|
|
|
$row = $this->fetchObject( $result ); |
|
|
|
|
1579
|
|
|
|
1580
|
|
|
return ( $row->lockstatus === 't' ); |
1581
|
|
|
} |
1582
|
|
|
|
1583
|
|
|
/** |
1584
|
|
|
* See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS |
1585
|
|
|
* @param string $lockName |
1586
|
|
|
* @param string $method |
1587
|
|
|
* @param int $timeout |
1588
|
|
|
* @return bool |
1589
|
|
|
*/ |
1590
|
|
|
public function lock( $lockName, $method, $timeout = 5 ) { |
1591
|
|
|
$key = $this->addQuotes( $this->bigintFromLockName( $lockName ) ); |
|
|
|
|
1592
|
|
|
$loop = new WaitConditionLoop( |
1593
|
|
|
function () use ( $lockName, $key, $timeout, $method ) { |
1594
|
|
|
$res = $this->query( "SELECT pg_try_advisory_lock($key) AS lockstatus", $method ); |
1595
|
|
|
$row = $this->fetchObject( $res ); |
|
|
|
|
1596
|
|
|
if ( $row->lockstatus === 't' ) { |
1597
|
|
|
parent::lock( $lockName, $method, $timeout ); // record |
1598
|
|
|
return true; |
1599
|
|
|
} |
1600
|
|
|
|
1601
|
|
|
return WaitConditionLoop::CONDITION_CONTINUE; |
1602
|
|
|
}, |
1603
|
|
|
$timeout |
1604
|
|
|
); |
1605
|
|
|
|
1606
|
|
|
return ( $loop->invoke() === $loop::CONDITION_REACHED ); |
1607
|
|
|
} |
1608
|
|
|
|
1609
|
|
|
/** |
1610
|
|
|
* See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM |
1611
|
|
|
* PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS |
1612
|
|
|
* @param string $lockName |
1613
|
|
|
* @param string $method |
1614
|
|
|
* @return bool |
1615
|
|
|
*/ |
1616
|
|
View Code Duplication |
public function unlock( $lockName, $method ) { |
1617
|
|
|
$key = $this->addQuotes( $this->bigintFromLockName( $lockName ) ); |
|
|
|
|
1618
|
|
|
$result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method ); |
1619
|
|
|
$row = $this->fetchObject( $result ); |
|
|
|
|
1620
|
|
|
|
1621
|
|
|
if ( $row->lockstatus === 't' ) { |
1622
|
|
|
parent::unlock( $lockName, $method ); // record |
1623
|
|
|
return true; |
1624
|
|
|
} |
1625
|
|
|
|
1626
|
|
|
wfDebug( __METHOD__ . " failed to release lock\n" ); |
1627
|
|
|
|
1628
|
|
|
return false; |
1629
|
|
|
} |
1630
|
|
|
|
1631
|
|
|
/** |
1632
|
|
|
* @param string $lockName |
1633
|
|
|
* @return string Integer |
1634
|
|
|
*/ |
1635
|
|
|
private function bigintFromLockName( $lockName ) { |
1636
|
|
|
return Wikimedia\base_convert( substr( sha1( $lockName ), 0, 15 ), 16, 10 ); |
1637
|
|
|
} |
1638
|
|
|
} // end DatabasePostgres class |
1639
|
|
|
|
1640
|
|
|
class PostgresBlob extends Blob { |
1641
|
|
|
} |
1642
|
|
|
|
Only declaring a single property per statement allows you to later on add doc comments more easily.
It is also recommended by PSR2, so it is a common style that many people expect.