Version 1
[yaffs-website] / web / core / lib / Drupal / Core / Database / Driver / pgsql / Connection.php
1 <?php
2
3 namespace Drupal\Core\Database\Driver\pgsql;
4
5 use Drupal\Core\Database\Database;
6 use Drupal\Core\Database\Connection as DatabaseConnection;
7 use Drupal\Core\Database\DatabaseAccessDeniedException;
8 use Drupal\Core\Database\DatabaseNotFoundException;
9
10 /**
11  * @addtogroup database
12  * @{
13  */
14
15 /**
16  * PostgreSQL implementation of \Drupal\Core\Database\Connection.
17  */
18 class Connection extends DatabaseConnection {
19
20   /**
21    * The name by which to obtain a lock for retrieve the next insert id.
22    */
23   const POSTGRESQL_NEXTID_LOCK = 1000;
24
25   /**
26    * Error code for "Unknown database" error.
27    */
28   const DATABASE_NOT_FOUND = 7;
29
30   /**
31    * Error code for "Connection failure" errors.
32    *
33    * Technically this is an internal error code that will only be shown in the
34    * PDOException message. It will need to get extracted.
35    */
36   const CONNECTION_FAILURE = '08006';
37
38   /**
39    * A map of condition operators to PostgreSQL operators.
40    *
41    * In PostgreSQL, 'LIKE' is case-sensitive. ILKE should be used for
42    * case-insensitive statements.
43    */
44   protected static $postgresqlConditionOperatorMap = [
45     'LIKE' => ['operator' => 'ILIKE'],
46     'LIKE BINARY' => ['operator' => 'LIKE'],
47     'NOT LIKE' => ['operator' => 'NOT ILIKE'],
48     'REGEXP' => ['operator' => '~*'],
49   ];
50
51   /**
52    * The list of PostgreSQL reserved key words.
53    *
54    * @see http://www.postgresql.org/docs/9.4/static/sql-keywords-appendix.html
55    */
56   protected $postgresqlReservedKeyWords = ['all', 'analyse', 'analyze', 'and',
57   'any', 'array', 'as', 'asc', 'asymmetric', 'authorization', 'binary', 'both',
58   'case', 'cast', 'check', 'collate', 'collation', 'column', 'concurrently',
59   'constraint', 'create', 'cross', 'current_catalog', 'current_date',
60   'current_role', 'current_schema', 'current_time', 'current_timestamp',
61   'current_user', 'default', 'deferrable', 'desc', 'distinct', 'do', 'else',
62   'end', 'except', 'false', 'fetch', 'for', 'foreign', 'freeze', 'from', 'full',
63   'grant', 'group', 'having', 'ilike', 'in', 'initially', 'inner', 'intersect',
64   'into', 'is', 'isnull', 'join', 'lateral', 'leading', 'left', 'like', 'limit',
65   'localtime', 'localtimestamp', 'natural', 'not', 'notnull', 'null', 'offset',
66   'on', 'only', 'or', 'order', 'outer', 'over', 'overlaps', 'placing',
67   'primary', 'references', 'returning', 'right', 'select', 'session_user',
68   'similar', 'some', 'symmetric', 'table', 'then', 'to', 'trailing', 'true',
69   'union', 'unique', 'user', 'using', 'variadic', 'verbose', 'when', 'where',
70   'window', 'with'];
71
72   /**
73    * Constructs a connection object.
74    */
75   public function __construct(\PDO $connection, array $connection_options) {
76     parent::__construct($connection, $connection_options);
77
78     // This driver defaults to transaction support, except if explicitly passed FALSE.
79     $this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
80
81     // Transactional DDL is always available in PostgreSQL,
82     // but we'll only enable it if standard transactions are.
83     $this->transactionalDDLSupport = $this->transactionSupport;
84
85     $this->connectionOptions = $connection_options;
86
87     // Force PostgreSQL to use the UTF-8 character set by default.
88     $this->connection->exec("SET NAMES 'UTF8'");
89
90     // Execute PostgreSQL init_commands.
91     if (isset($connection_options['init_commands'])) {
92       $this->connection->exec(implode('; ', $connection_options['init_commands']));
93     }
94   }
95
96   /**
97    * {@inheritdoc}
98    */
99   public static function open(array &$connection_options = []) {
100     // Default to TCP connection on port 5432.
101     if (empty($connection_options['port'])) {
102       $connection_options['port'] = 5432;
103     }
104
105     // PostgreSQL in trust mode doesn't require a password to be supplied.
106     if (empty($connection_options['password'])) {
107       $connection_options['password'] = NULL;
108     }
109     // If the password contains a backslash it is treated as an escape character
110     // http://bugs.php.net/bug.php?id=53217
111     // so backslashes in the password need to be doubled up.
112     // The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords
113     // will break on this doubling up when the bug is fixed, so check the version
114     //elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') {
115     else {
116       $connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']);
117     }
118
119     $connection_options['database'] = (!empty($connection_options['database']) ? $connection_options['database'] : 'template1');
120     $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port'];
121
122     // Allow PDO options to be overridden.
123     $connection_options += [
124       'pdo' => [],
125     ];
126     $connection_options['pdo'] += [
127       \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
128       // Prepared statements are most effective for performance when queries
129       // are recycled (used several times). However, if they are not re-used,
130       // prepared statements become inefficient. Since most of Drupal's
131       // prepared queries are not re-used, it should be faster to emulate
132       // the preparation than to actually ready statements for re-use. If in
133       // doubt, reset to FALSE and measure performance.
134       \PDO::ATTR_EMULATE_PREPARES => TRUE,
135       // Convert numeric values to strings when fetching.
136       \PDO::ATTR_STRINGIFY_FETCHES => TRUE,
137     ];
138
139     try {
140       $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
141     }
142     catch (\PDOException $e) {
143       if (static::getSQLState($e) == static::CONNECTION_FAILURE) {
144         if (strpos($e->getMessage(), 'password authentication failed for user') !== FALSE) {
145           throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e);
146         }
147         elseif (strpos($e->getMessage(), 'database') !== FALSE && strpos($e->getMessage(), 'does not exist') !== FALSE) {
148           throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
149         }
150       }
151       throw $e;
152     }
153
154     return $pdo;
155   }
156
157   /**
158    * {@inheritdoc}
159    */
160   public function query($query, array $args = [], $options = []) {
161     $options += $this->defaultOptions();
162
163     // The PDO PostgreSQL driver has a bug which doesn't type cast booleans
164     // correctly when parameters are bound using associative arrays.
165     // @see http://bugs.php.net/bug.php?id=48383
166     foreach ($args as &$value) {
167       if (is_bool($value)) {
168         $value = (int) $value;
169       }
170     }
171
172     // We need to wrap queries with a savepoint if:
173     // - Currently in a transaction.
174     // - A 'mimic_implicit_commit' does not exist already.
175     // - The query is not a savepoint query.
176     $wrap_with_savepoint = $this->inTransaction() &&
177       !isset($this->transactionLayers['mimic_implicit_commit']) &&
178       !(is_string($query) && (
179         stripos($query, 'ROLLBACK TO SAVEPOINT ') === 0 ||
180         stripos($query, 'RELEASE SAVEPOINT ') === 0 ||
181         stripos($query, 'SAVEPOINT ') === 0
182       )
183     );
184     if ($wrap_with_savepoint) {
185       // Create a savepoint so we can rollback a failed query. This is so we can
186       // mimic MySQL and SQLite transactions which don't fail if a single query
187       // fails. This is important for tables that are created on demand. For
188       // example, \Drupal\Core\Cache\DatabaseBackend.
189       $this->addSavepoint();
190       try {
191         $return = parent::query($query, $args, $options);
192         $this->releaseSavepoint();
193       }
194       catch (\Exception $e) {
195         $this->rollbackSavepoint();
196         throw $e;
197       }
198     }
199     else {
200       $return = parent::query($query, $args, $options);
201     }
202
203     return $return;
204   }
205
206   public function prepareQuery($query) {
207     // mapConditionOperator converts some operations (LIKE, REGEXP, etc.) to
208     // PostgreSQL equivalents (ILIKE, ~*, etc.). However PostgreSQL doesn't
209     // automatically cast the fields to the right type for these operators,
210     // so we need to alter the query and add the type-cast.
211     return parent::prepareQuery(preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE|~\*) /i', ' ${1}::text ${2} ', $query));
212   }
213
214   public function queryRange($query, $from, $count, array $args = [], array $options = []) {
215     return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options);
216   }
217
218   public function queryTemporary($query, array $args = [], array $options = []) {
219     $tablename = $this->generateTemporaryTableName();
220     $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options);
221     return $tablename;
222   }
223
224   /**
225    * {@inheritdoc}
226    */
227   public function escapeField($field) {
228     $escaped = parent::escapeField($field);
229
230     // Remove any invalid start character.
231     $escaped = preg_replace('/^[^A-Za-z0-9_]/', '', $escaped);
232
233     // The pgsql database driver does not support field names that contain
234     // periods (supported by PostgreSQL server) because this method may be
235     // called by a field with a table alias as part of SQL conditions or
236     // order by statements. This will consider a period as a table alias
237     // identifier, and split the string at the first period.
238     if (preg_match('/^([A-Za-z0-9_]+)"?[.]"?([A-Za-z0-9_.]+)/', $escaped, $parts)) {
239       $table = $parts[1];
240       $column = $parts[2];
241
242       // Use escape alias because escapeField may contain multiple periods that
243       // need to be escaped.
244       $escaped = $this->escapeTable($table) . '.' . $this->escapeAlias($column);
245     }
246     else {
247       $escaped = $this->doEscape($escaped);
248     }
249
250     return $escaped;
251   }
252
253   /**
254    * {@inheritdoc}
255    */
256   public function escapeAlias($field) {
257     $escaped = preg_replace('/[^A-Za-z0-9_]+/', '', $field);
258     $escaped = $this->doEscape($escaped);
259     return $escaped;
260   }
261
262   /**
263    * {@inheritdoc}
264    */
265   public function escapeTable($table) {
266     $escaped = parent::escapeTable($table);
267
268     // Ensure that each part (database, schema and table) of the table name is
269     // properly and independently escaped.
270     $parts = explode('.', $escaped);
271     $parts = array_map([$this, 'doEscape'], $parts);
272     $escaped = implode('.', $parts);
273
274     return $escaped;
275   }
276
277   /**
278    * Escape a string if needed.
279    *
280    * @param $string
281    *   The string to escape.
282    * @return string
283    *   The escaped string.
284    */
285   protected function doEscape($string) {
286     // Quote identifier to make it case-sensitive.
287     if (preg_match('/[A-Z]/', $string)) {
288       $string = '"' . $string . '"';
289     }
290     elseif (in_array(strtolower($string), $this->postgresqlReservedKeyWords)) {
291       // Quote the string for PostgreSQL reserved key words.
292       $string = '"' . $string . '"';
293     }
294     return $string;
295   }
296
297   public function driver() {
298     return 'pgsql';
299   }
300
301   public function databaseType() {
302     return 'pgsql';
303   }
304
305   /**
306    * Overrides \Drupal\Core\Database\Connection::createDatabase().
307    *
308    * @param string $database
309    *   The name of the database to create.
310    *
311    * @throws \Drupal\Core\Database\DatabaseNotFoundException
312    */
313   public function createDatabase($database) {
314     // Escape the database name.
315     $database = Database::getConnection()->escapeDatabase($database);
316
317     // If the PECL intl extension is installed, use it to determine the proper
318     // locale.  Otherwise, fall back to en_US.
319     if (class_exists('Locale')) {
320       $locale = \Locale::getDefault();
321     }
322     else {
323       $locale = 'en_US';
324     }
325
326     try {
327       // Create the database and set it as active.
328       $this->connection->exec("CREATE DATABASE $database WITH TEMPLATE template0 ENCODING='utf8' LC_CTYPE='$locale.utf8' LC_COLLATE='$locale.utf8'");
329     }
330     catch (\Exception $e) {
331       throw new DatabaseNotFoundException($e->getMessage());
332     }
333   }
334
335   public function mapConditionOperator($operator) {
336     return isset(static::$postgresqlConditionOperatorMap[$operator]) ? static::$postgresqlConditionOperatorMap[$operator] : NULL;
337   }
338
339   /**
340    * Retrieve a the next id in a sequence.
341    *
342    * PostgreSQL has built in sequences. We'll use these instead of inserting
343    * and updating a sequences table.
344    */
345   public function nextId($existing = 0) {
346
347     // Retrieve the name of the sequence. This information cannot be cached
348     // because the prefix may change, for example, like it does in simpletests.
349     $sequence_name = $this->makeSequenceName('sequences', 'value');
350
351     // When PostgreSQL gets a value too small then it will lock the table,
352     // retry the INSERT and if it's still too small then alter the sequence.
353     $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
354     if ($id > $existing) {
355       return $id;
356     }
357
358     // PostgreSQL advisory locks are simply locks to be used by an
359     // application such as Drupal. This will prevent other Drupal processes
360     // from altering the sequence while we are.
361     $this->query("SELECT pg_advisory_lock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
362
363     // While waiting to obtain the lock, the sequence may have been altered
364     // so lets try again to obtain an adequate value.
365     $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
366     if ($id > $existing) {
367       $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
368       return $id;
369     }
370
371     // Reset the sequence to a higher value than the existing id.
372     $this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1));
373
374     // Retrieve the next id. We know this will be as high as we want it.
375     $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
376
377     $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
378
379     return $id;
380   }
381
382   /**
383    * {@inheritdoc}
384    */
385   public function getFullQualifiedTableName($table) {
386     $options = $this->getConnectionOptions();
387     $prefix = $this->tablePrefix($table);
388
389     // The fully qualified table name in PostgreSQL is in the form of
390     // <database>.<schema>.<table>, so we have to include the 'public' schema in
391     // the return value.
392     return $options['database'] . '.public.' . $prefix . $table;
393   }
394
395   /**
396    * Add a new savepoint with an unique name.
397    *
398    * The main use for this method is to mimic InnoDB functionality, which
399    * provides an inherent savepoint before any query in a transaction.
400    *
401    * @param $savepoint_name
402    *   A string representing the savepoint name. By default,
403    *   "mimic_implicit_commit" is used.
404    *
405    * @see Drupal\Core\Database\Connection::pushTransaction()
406    */
407   public function addSavepoint($savepoint_name = 'mimic_implicit_commit') {
408     if ($this->inTransaction()) {
409       $this->pushTransaction($savepoint_name);
410     }
411   }
412
413   /**
414    * Release a savepoint by name.
415    *
416    * @param $savepoint_name
417    *   A string representing the savepoint name. By default,
418    *   "mimic_implicit_commit" is used.
419    *
420    * @see Drupal\Core\Database\Connection::popTransaction()
421    */
422   public function releaseSavepoint($savepoint_name = 'mimic_implicit_commit') {
423     if (isset($this->transactionLayers[$savepoint_name])) {
424       $this->popTransaction($savepoint_name);
425     }
426   }
427
428   /**
429    * Rollback a savepoint by name if it exists.
430    *
431    * @param $savepoint_name
432    *   A string representing the savepoint name. By default,
433    *   "mimic_implicit_commit" is used.
434    */
435   public function rollbackSavepoint($savepoint_name = 'mimic_implicit_commit') {
436     if (isset($this->transactionLayers[$savepoint_name])) {
437       $this->rollBack($savepoint_name);
438     }
439   }
440
441   /**
442    * {@inheritdoc}
443    */
444   public function upsert($table, array $options = []) {
445     // Use the (faster) native Upsert implementation for PostgreSQL >= 9.5.
446     if (version_compare($this->version(), '9.5', '>=')) {
447       $class = $this->getDriverClass('NativeUpsert');
448     }
449     else {
450       $class = $this->getDriverClass('Upsert');
451     }
452
453     return new $class($this, $table, $options);
454   }
455
456 }
457
458 /**
459  * @} End of "addtogroup database".
460  */