|
1 <?php |
|
2 |
|
3 /** |
|
4 * @file |
|
5 * Database interface code for PostgreSQL database servers. |
|
6 */ |
|
7 |
|
8 /** |
|
9 * @addtogroup database |
|
10 * @{ |
|
11 */ |
|
12 |
|
13 /** |
|
14 * The name by which to obtain a lock for retrieving the next insert id. |
|
15 */ |
|
16 define('POSTGRESQL_NEXTID_LOCK', 1000); |
|
17 |
|
18 class DatabaseConnection_pgsql extends DatabaseConnection { |
|
19 |
|
20 public function __construct(array $connection_options = array()) { |
|
21 // This driver defaults to transaction support, except if explicitly passed FALSE. |
|
22 $this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE); |
|
23 |
|
24 // Transactional DDL is always available in PostgreSQL, |
|
25 // but we'll only enable it if standard transactions are. |
|
26 $this->transactionalDDLSupport = $this->transactionSupport; |
|
27 |
|
28 // Default to TCP connection on port 5432. |
|
29 if (empty($connection_options['port'])) { |
|
30 $connection_options['port'] = 5432; |
|
31 } |
|
32 |
|
33 // PostgreSQL in trust mode doesn't require a password to be supplied. |
|
34 if (empty($connection_options['password'])) { |
|
35 $connection_options['password'] = NULL; |
|
36 } |
|
37 // If the password contains a backslash it is treated as an escape character |
|
38 // http://bugs.php.net/bug.php?id=53217 |
|
39 // so backslashes in the password need to be doubled up. |
|
40 // The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords |
|
41 // will break on this doubling up when the bug is fixed, so check the version |
|
42 //elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') { |
|
43 else { |
|
44 $connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']); |
|
45 } |
|
46 |
|
47 $this->connectionOptions = $connection_options; |
|
48 |
|
49 $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port']; |
|
50 |
|
51 // Allow PDO options to be overridden. |
|
52 $connection_options += array( |
|
53 'pdo' => array(), |
|
54 ); |
|
55 $connection_options['pdo'] += array( |
|
56 // Prepared statements are most effective for performance when queries |
|
57 // are recycled (used several times). However, if they are not re-used, |
|
58 // prepared statements become inefficient. Since most of Drupal's |
|
59 // prepared queries are not re-used, it should be faster to emulate |
|
60 // the preparation than to actually ready statements for re-use. If in |
|
61 // doubt, reset to FALSE and measure performance. |
|
62 PDO::ATTR_EMULATE_PREPARES => TRUE, |
|
63 // Convert numeric values to strings when fetching. |
|
64 PDO::ATTR_STRINGIFY_FETCHES => TRUE, |
|
65 ); |
|
66 parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']); |
|
67 |
|
68 // Force PostgreSQL to use the UTF-8 character set by default. |
|
69 $this->exec("SET NAMES 'UTF8'"); |
|
70 |
|
71 // Execute PostgreSQL init_commands. |
|
72 if (isset($connection_options['init_commands'])) { |
|
73 $this->exec(implode('; ', $connection_options['init_commands'])); |
|
74 } |
|
75 } |
|
76 |
|
77 public function prepareQuery($query) { |
|
78 // mapConditionOperator converts LIKE operations to ILIKE for consistency |
|
79 // with MySQL. However, Postgres does not support ILIKE on bytea (blobs) |
|
80 // fields. |
|
81 // To make the ILIKE operator work, we type-cast bytea fields into text. |
|
82 // @todo This workaround only affects bytea fields, but the involved field |
|
83 // types involved in the query are unknown, so there is no way to |
|
84 // conditionally execute this for affected queries only. |
|
85 return parent::prepareQuery(preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE) /i', ' ${1}::text ${2} ', $query)); |
|
86 } |
|
87 |
|
88 public function query($query, array $args = array(), $options = array()) { |
|
89 |
|
90 $options += $this->defaultOptions(); |
|
91 |
|
92 // The PDO PostgreSQL driver has a bug which |
|
93 // doesn't type cast booleans correctly when |
|
94 // parameters are bound using associative |
|
95 // arrays. |
|
96 // See http://bugs.php.net/bug.php?id=48383 |
|
97 foreach ($args as &$value) { |
|
98 if (is_bool($value)) { |
|
99 $value = (int) $value; |
|
100 } |
|
101 } |
|
102 |
|
103 try { |
|
104 if ($query instanceof DatabaseStatementInterface) { |
|
105 $stmt = $query; |
|
106 $stmt->execute(NULL, $options); |
|
107 } |
|
108 else { |
|
109 $this->expandArguments($query, $args); |
|
110 $stmt = $this->prepareQuery($query); |
|
111 $stmt->execute($args, $options); |
|
112 } |
|
113 |
|
114 switch ($options['return']) { |
|
115 case Database::RETURN_STATEMENT: |
|
116 return $stmt; |
|
117 case Database::RETURN_AFFECTED: |
|
118 return $stmt->rowCount(); |
|
119 case Database::RETURN_INSERT_ID: |
|
120 return $this->lastInsertId($options['sequence_name']); |
|
121 case Database::RETURN_NULL: |
|
122 return; |
|
123 default: |
|
124 throw new PDOException('Invalid return directive: ' . $options['return']); |
|
125 } |
|
126 } |
|
127 catch (PDOException $e) { |
|
128 if ($options['throw_exception']) { |
|
129 // Add additional debug information. |
|
130 if ($query instanceof DatabaseStatementInterface) { |
|
131 $e->query_string = $stmt->getQueryString(); |
|
132 } |
|
133 else { |
|
134 $e->query_string = $query; |
|
135 } |
|
136 $e->args = $args; |
|
137 throw $e; |
|
138 } |
|
139 return NULL; |
|
140 } |
|
141 } |
|
142 |
|
143 public function queryRange($query, $from, $count, array $args = array(), array $options = array()) { |
|
144 return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options); |
|
145 } |
|
146 |
|
147 public function queryTemporary($query, array $args = array(), array $options = array()) { |
|
148 $tablename = $this->generateTemporaryTableName(); |
|
149 $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options); |
|
150 return $tablename; |
|
151 } |
|
152 |
|
153 public function driver() { |
|
154 return 'pgsql'; |
|
155 } |
|
156 |
|
157 public function databaseType() { |
|
158 return 'pgsql'; |
|
159 } |
|
160 |
|
161 public function mapConditionOperator($operator) { |
|
162 static $specials; |
|
163 |
|
164 // Function calls not allowed in static declarations, thus this method. |
|
165 if (!isset($specials)) { |
|
166 $specials = array( |
|
167 // In PostgreSQL, 'LIKE' is case-sensitive. For case-insensitive LIKE |
|
168 // statements, we need to use ILIKE instead. |
|
169 'LIKE' => array('operator' => 'ILIKE'), |
|
170 'NOT LIKE' => array('operator' => 'NOT ILIKE'), |
|
171 ); |
|
172 } |
|
173 |
|
174 return isset($specials[$operator]) ? $specials[$operator] : NULL; |
|
175 } |
|
176 |
|
177 /** |
|
178 * Retrieve the next id in a sequence. |
|
179 * |
|
180 * PostgreSQL has built in sequences. We'll use these instead of inserting |
|
181 * and updating a sequences table. |
|
182 */ |
|
183 public function nextId($existing = 0) { |
|
184 |
|
185 // Retrieve the name of the sequence. This information cannot be cached |
|
186 // because the prefix may change, for example, like it does in simpletests. |
|
187 $sequence_name = $this->makeSequenceName('sequences', 'value'); |
|
188 |
|
189 // When PostgreSQL gets a value too small then it will lock the table, |
|
190 // retry the INSERT and if it's still too small then alter the sequence. |
|
191 $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField(); |
|
192 if ($id > $existing) { |
|
193 return $id; |
|
194 } |
|
195 |
|
196 // PostgreSQL advisory locks are simply locks to be used by an |
|
197 // application such as Drupal. This will prevent other Drupal processes |
|
198 // from altering the sequence while we are. |
|
199 $this->query("SELECT pg_advisory_lock(" . POSTGRESQL_NEXTID_LOCK . ")"); |
|
200 |
|
201 // While waiting to obtain the lock, the sequence may have been altered |
|
202 // so lets try again to obtain an adequate value. |
|
203 $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField(); |
|
204 if ($id > $existing) { |
|
205 $this->query("SELECT pg_advisory_unlock(" . POSTGRESQL_NEXTID_LOCK . ")"); |
|
206 return $id; |
|
207 } |
|
208 |
|
209 // Reset the sequence to a higher value than the existing id. |
|
210 $this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1)); |
|
211 |
|
212 // Retrieve the next id. We know this will be as high as we want it. |
|
213 $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField(); |
|
214 |
|
215 $this->query("SELECT pg_advisory_unlock(" . POSTGRESQL_NEXTID_LOCK . ")"); |
|
216 |
|
217 return $id; |
|
218 } |
|
219 |
|
220 public function utf8mb4IsActive() { |
|
221 return TRUE; |
|
222 } |
|
223 |
|
224 public function utf8mb4IsSupported() { |
|
225 return TRUE; |
|
226 } |
|
227 } |
|
228 |
|
229 /** |
|
230 * @} End of "addtogroup database". |
|
231 */ |