Connection.php 16 KB

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