PageRenderTime 40ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 1ms

/web/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php

https://gitlab.com/mohamed_hussein/prodt
PHP | 375 lines | 172 code | 51 blank | 152 comment | 19 complexity | 61511363ff747b801a6484a641eec78c MD5 | raw file
  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. use Drupal\Core\Database\StatementInterface;
  8. use Drupal\Core\Database\StatementWrapper;
  9. // cSpell:ignore ilike nextval
  10. /**
  11. * @addtogroup database
  12. * @{
  13. */
  14. /**
  15. * PostgreSQL implementation of \Drupal\Core\Database\Connection.
  16. */
  17. class Connection extends DatabaseConnection {
  18. /**
  19. * The name by which to obtain a lock for retrieve the next insert id.
  20. */
  21. const POSTGRESQL_NEXTID_LOCK = 1000;
  22. /**
  23. * Error code for "Unknown database" error.
  24. */
  25. const DATABASE_NOT_FOUND = 7;
  26. /**
  27. * Error code for "Connection failure" errors.
  28. *
  29. * Technically this is an internal error code that will only be shown in the
  30. * PDOException message. It will need to get extracted.
  31. */
  32. const CONNECTION_FAILURE = '08006';
  33. /**
  34. * {@inheritdoc}
  35. */
  36. protected $statementClass = NULL;
  37. /**
  38. * {@inheritdoc}
  39. */
  40. protected $statementWrapperClass = StatementWrapper::class;
  41. /**
  42. * A map of condition operators to PostgreSQL operators.
  43. *
  44. * In PostgreSQL, 'LIKE' is case-sensitive. ILIKE should be used for
  45. * case-insensitive statements.
  46. */
  47. protected static $postgresqlConditionOperatorMap = [
  48. 'LIKE' => ['operator' => 'ILIKE'],
  49. 'LIKE BINARY' => ['operator' => 'LIKE'],
  50. 'NOT LIKE' => ['operator' => 'NOT ILIKE'],
  51. 'REGEXP' => ['operator' => '~*'],
  52. 'NOT REGEXP' => ['operator' => '!~*'],
  53. ];
  54. /**
  55. * {@inheritdoc}
  56. */
  57. protected $transactionalDDLSupport = TRUE;
  58. /**
  59. * {@inheritdoc}
  60. */
  61. protected $identifierQuotes = ['"', '"'];
  62. /**
  63. * Constructs a connection object.
  64. */
  65. public function __construct(\PDO $connection, array $connection_options) {
  66. parent::__construct($connection, $connection_options);
  67. // Force PostgreSQL to use the UTF-8 character set by default.
  68. $this->connection->exec("SET NAMES 'UTF8'");
  69. // Execute PostgreSQL init_commands.
  70. if (isset($connection_options['init_commands'])) {
  71. $this->connection->exec(implode('; ', $connection_options['init_commands']));
  72. }
  73. }
  74. /**
  75. * {@inheritdoc}
  76. */
  77. public static function open(array &$connection_options = []) {
  78. // Default to TCP connection on port 5432.
  79. if (empty($connection_options['port'])) {
  80. $connection_options['port'] = 5432;
  81. }
  82. // PostgreSQL in trust mode doesn't require a password to be supplied.
  83. if (empty($connection_options['password'])) {
  84. $connection_options['password'] = NULL;
  85. }
  86. // If the password contains a backslash it is treated as an escape character
  87. // http://bugs.php.net/bug.php?id=53217
  88. // so backslashes in the password need to be doubled up.
  89. // The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords
  90. // will break on this doubling up when the bug is fixed, so check the version
  91. // elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') {
  92. else {
  93. $connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']);
  94. }
  95. $connection_options['database'] = (!empty($connection_options['database']) ? $connection_options['database'] : 'template1');
  96. $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port'];
  97. // Allow PDO options to be overridden.
  98. $connection_options += [
  99. 'pdo' => [],
  100. ];
  101. $connection_options['pdo'] += [
  102. \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
  103. // Prepared statements are most effective for performance when queries
  104. // are recycled (used several times). However, if they are not re-used,
  105. // prepared statements become inefficient. Since most of Drupal's
  106. // prepared queries are not re-used, it should be faster to emulate
  107. // the preparation than to actually ready statements for re-use. If in
  108. // doubt, reset to FALSE and measure performance.
  109. \PDO::ATTR_EMULATE_PREPARES => TRUE,
  110. // Convert numeric values to strings when fetching.
  111. \PDO::ATTR_STRINGIFY_FETCHES => TRUE,
  112. ];
  113. try {
  114. $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
  115. }
  116. catch (\PDOException $e) {
  117. if (static::getSQLState($e) == static::CONNECTION_FAILURE) {
  118. if (strpos($e->getMessage(), 'password authentication failed for user') !== FALSE) {
  119. throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e);
  120. }
  121. elseif (strpos($e->getMessage(), 'database') !== FALSE && strpos($e->getMessage(), 'does not exist') !== FALSE) {
  122. throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
  123. }
  124. }
  125. throw $e;
  126. }
  127. return $pdo;
  128. }
  129. /**
  130. * {@inheritdoc}
  131. */
  132. public function query($query, array $args = [], $options = []) {
  133. $options += $this->defaultOptions();
  134. // The PDO PostgreSQL driver has a bug which doesn't type cast booleans
  135. // correctly when parameters are bound using associative arrays.
  136. // @see http://bugs.php.net/bug.php?id=48383
  137. foreach ($args as &$value) {
  138. if (is_bool($value)) {
  139. $value = (int) $value;
  140. }
  141. }
  142. // We need to wrap queries with a savepoint if:
  143. // - Currently in a transaction.
  144. // - A 'mimic_implicit_commit' does not exist already.
  145. // - The query is not a savepoint query.
  146. $wrap_with_savepoint = $this->inTransaction() &&
  147. !isset($this->transactionLayers['mimic_implicit_commit']) &&
  148. !(is_string($query) && (
  149. stripos($query, 'ROLLBACK TO SAVEPOINT ') === 0 ||
  150. stripos($query, 'RELEASE SAVEPOINT ') === 0 ||
  151. stripos($query, 'SAVEPOINT ') === 0
  152. )
  153. );
  154. if ($wrap_with_savepoint) {
  155. // Create a savepoint so we can rollback a failed query. This is so we can
  156. // mimic MySQL and SQLite transactions which don't fail if a single query
  157. // fails. This is important for tables that are created on demand. For
  158. // example, \Drupal\Core\Cache\DatabaseBackend.
  159. $this->addSavepoint();
  160. try {
  161. $return = parent::query($query, $args, $options);
  162. $this->releaseSavepoint();
  163. }
  164. catch (\Exception $e) {
  165. $this->rollbackSavepoint();
  166. throw $e;
  167. }
  168. }
  169. else {
  170. $return = parent::query($query, $args, $options);
  171. }
  172. return $return;
  173. }
  174. /**
  175. * {@inheritdoc}
  176. */
  177. public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface {
  178. // mapConditionOperator converts some operations (LIKE, REGEXP, etc.) to
  179. // PostgreSQL equivalents (ILIKE, ~*, etc.). However PostgreSQL doesn't
  180. // automatically cast the fields to the right type for these operators,
  181. // so we need to alter the query and add the type-cast.
  182. $query = preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE|~\*|!~\*) /i', ' ${1}::text ${2} ', $query);
  183. return parent::prepareStatement($query, $options, $allow_row_count);
  184. }
  185. public function queryRange($query, $from, $count, array $args = [], array $options = []) {
  186. return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options);
  187. }
  188. /**
  189. * {@inheritdoc}
  190. */
  191. public function queryTemporary($query, array $args = [], array $options = []) {
  192. @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED);
  193. $tablename = $this->generateTemporaryTableName();
  194. $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options);
  195. return $tablename;
  196. }
  197. public function driver() {
  198. return 'pgsql';
  199. }
  200. public function databaseType() {
  201. return 'pgsql';
  202. }
  203. /**
  204. * Overrides \Drupal\Core\Database\Connection::createDatabase().
  205. *
  206. * @param string $database
  207. * The name of the database to create.
  208. *
  209. * @throws \Drupal\Core\Database\DatabaseNotFoundException
  210. */
  211. public function createDatabase($database) {
  212. // Escape the database name.
  213. $database = Database::getConnection()->escapeDatabase($database);
  214. // If the PECL intl extension is installed, use it to determine the proper
  215. // locale. Otherwise, fall back to en_US.
  216. if (class_exists('Locale')) {
  217. $locale = \Locale::getDefault();
  218. }
  219. else {
  220. $locale = 'en_US';
  221. }
  222. try {
  223. // Create the database and set it as active.
  224. $this->connection->exec("CREATE DATABASE $database WITH TEMPLATE template0 ENCODING='utf8' LC_CTYPE='$locale.utf8' LC_COLLATE='$locale.utf8'");
  225. }
  226. catch (\Exception $e) {
  227. throw new DatabaseNotFoundException($e->getMessage());
  228. }
  229. }
  230. public function mapConditionOperator($operator) {
  231. return static::$postgresqlConditionOperatorMap[$operator] ?? NULL;
  232. }
  233. /**
  234. * Retrieve a the next id in a sequence.
  235. *
  236. * PostgreSQL has built in sequences. We'll use these instead of inserting
  237. * and updating a sequences table.
  238. */
  239. public function nextId($existing = 0) {
  240. // Retrieve the name of the sequence. This information cannot be cached
  241. // because the prefix may change, for example, like it does in tests.
  242. $sequence_name = $this->makeSequenceName('sequences', 'value');
  243. // When PostgreSQL gets a value too small then it will lock the table,
  244. // retry the INSERT and if it's still too small then alter the sequence.
  245. $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
  246. if ($id > $existing) {
  247. return $id;
  248. }
  249. // PostgreSQL advisory locks are simply locks to be used by an
  250. // application such as Drupal. This will prevent other Drupal processes
  251. // from altering the sequence while we are.
  252. $this->query("SELECT pg_advisory_lock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
  253. // While waiting to obtain the lock, the sequence may have been altered
  254. // so lets try again to obtain an adequate value.
  255. $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
  256. if ($id > $existing) {
  257. $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
  258. return $id;
  259. }
  260. // Reset the sequence to a higher value than the existing id.
  261. $this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1));
  262. // Retrieve the next id. We know this will be as high as we want it.
  263. $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
  264. $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
  265. return $id;
  266. }
  267. /**
  268. * {@inheritdoc}
  269. */
  270. public function getFullQualifiedTableName($table) {
  271. $options = $this->getConnectionOptions();
  272. $prefix = $this->tablePrefix($table);
  273. // The fully qualified table name in PostgreSQL is in the form of
  274. // <database>.<schema>.<table>, so we have to include the 'public' schema in
  275. // the return value.
  276. return $options['database'] . '.public.' . $prefix . $table;
  277. }
  278. /**
  279. * Add a new savepoint with a unique name.
  280. *
  281. * The main use for this method is to mimic InnoDB functionality, which
  282. * provides an inherent savepoint before any query in a transaction.
  283. *
  284. * @param $savepoint_name
  285. * A string representing the savepoint name. By default,
  286. * "mimic_implicit_commit" is used.
  287. *
  288. * @see Drupal\Core\Database\Connection::pushTransaction()
  289. */
  290. public function addSavepoint($savepoint_name = 'mimic_implicit_commit') {
  291. if ($this->inTransaction()) {
  292. $this->pushTransaction($savepoint_name);
  293. }
  294. }
  295. /**
  296. * Release a savepoint by name.
  297. *
  298. * @param $savepoint_name
  299. * A string representing the savepoint name. By default,
  300. * "mimic_implicit_commit" is used.
  301. *
  302. * @see Drupal\Core\Database\Connection::popTransaction()
  303. */
  304. public function releaseSavepoint($savepoint_name = 'mimic_implicit_commit') {
  305. if (isset($this->transactionLayers[$savepoint_name])) {
  306. $this->popTransaction($savepoint_name);
  307. }
  308. }
  309. /**
  310. * Rollback a savepoint by name if it exists.
  311. *
  312. * @param $savepoint_name
  313. * A string representing the savepoint name. By default,
  314. * "mimic_implicit_commit" is used.
  315. */
  316. public function rollbackSavepoint($savepoint_name = 'mimic_implicit_commit') {
  317. if (isset($this->transactionLayers[$savepoint_name])) {
  318. $this->rollBack($savepoint_name);
  319. }
  320. }
  321. }
  322. /**
  323. * @} End of "addtogroup database".
  324. */