Postgres.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Database\Driver;
  17. use Cake\Database\Driver;
  18. use Cake\Database\Expression\FunctionExpression;
  19. use Cake\Database\Expression\IdentifierExpression;
  20. use Cake\Database\Expression\StringExpression;
  21. use Cake\Database\PostgresCompiler;
  22. use Cake\Database\Query;
  23. use Cake\Database\QueryCompiler;
  24. use Cake\Database\Schema\PostgresSchemaDialect;
  25. use Cake\Database\Schema\SchemaDialect;
  26. use PDO;
  27. /**
  28. * Class Postgres
  29. */
  30. class Postgres extends Driver
  31. {
  32. use SqlDialectTrait;
  33. /**
  34. * @inheritDoc
  35. */
  36. protected const MAX_ALIAS_LENGTH = 63;
  37. /**
  38. * Base configuration settings for Postgres driver
  39. *
  40. * @var array<string, mixed>
  41. */
  42. protected array $_baseConfig = [
  43. 'persistent' => true,
  44. 'host' => 'localhost',
  45. 'username' => 'root',
  46. 'password' => '',
  47. 'database' => 'cake',
  48. 'schema' => 'public',
  49. 'port' => 5432,
  50. 'encoding' => 'utf8',
  51. 'timezone' => null,
  52. 'flags' => [],
  53. 'init' => [],
  54. ];
  55. /**
  56. * The schema dialect class for this driver
  57. *
  58. * @var \Cake\Database\Schema\PostgresSchemaDialect|null
  59. */
  60. protected ?PostgresSchemaDialect $_schemaDialect = null;
  61. /**
  62. * String used to start a database identifier quoting to make it safe
  63. *
  64. * @var string
  65. */
  66. protected string $_startQuote = '"';
  67. /**
  68. * String used to end a database identifier quoting to make it safe
  69. *
  70. * @var string
  71. */
  72. protected string $_endQuote = '"';
  73. /**
  74. * @inheritDoc
  75. */
  76. public function connect(): void
  77. {
  78. if (isset($this->_connection)) {
  79. return;
  80. }
  81. $config = $this->_config;
  82. $config['flags'] += [
  83. PDO::ATTR_PERSISTENT => $config['persistent'],
  84. PDO::ATTR_EMULATE_PREPARES => false,
  85. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  86. ];
  87. if (empty($config['unix_socket'])) {
  88. $dsn = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
  89. } else {
  90. $dsn = "pgsql:dbname={$config['database']}";
  91. }
  92. $this->_connection = $this->_connect($dsn, $config);
  93. if (!empty($config['encoding'])) {
  94. $this->setEncoding($config['encoding']);
  95. }
  96. if (!empty($config['schema'])) {
  97. $this->setSchema($config['schema']);
  98. }
  99. if (!empty($config['timezone'])) {
  100. $config['init'][] = sprintf('SET timezone = %s', $this->_connection->quote($config['timezone']));
  101. }
  102. foreach ($config['init'] as $command) {
  103. $this->_connection->exec($command);
  104. }
  105. }
  106. /**
  107. * Returns whether php is able to use this driver for connecting to database
  108. *
  109. * @return bool true if it is valid to use this driver
  110. */
  111. public function enabled(): bool
  112. {
  113. return in_array('pgsql', PDO::getAvailableDrivers(), true);
  114. }
  115. /**
  116. * @inheritDoc
  117. */
  118. public function schemaDialect(): SchemaDialect
  119. {
  120. if ($this->_schemaDialect === null) {
  121. $this->_schemaDialect = new PostgresSchemaDialect($this);
  122. }
  123. return $this->_schemaDialect;
  124. }
  125. /**
  126. * Sets connection encoding
  127. *
  128. * @param string $encoding The encoding to use.
  129. * @return void
  130. */
  131. public function setEncoding(string $encoding): void
  132. {
  133. $this->connect();
  134. $this->_connection->exec('SET NAMES ' . $this->_connection->quote($encoding));
  135. }
  136. /**
  137. * Sets connection default schema, if any relation defined in a query is not fully qualified
  138. * postgres will fallback to looking the relation into defined default schema
  139. *
  140. * @param string $schema The schema names to set `search_path` to.
  141. * @return void
  142. */
  143. public function setSchema(string $schema): void
  144. {
  145. $this->connect();
  146. $this->_connection->exec('SET search_path TO ' . $this->_connection->quote($schema));
  147. }
  148. /**
  149. * @inheritDoc
  150. */
  151. public function disableForeignKeySQL(): string
  152. {
  153. return 'SET CONSTRAINTS ALL DEFERRED';
  154. }
  155. /**
  156. * @inheritDoc
  157. */
  158. public function enableForeignKeySQL(): string
  159. {
  160. return 'SET CONSTRAINTS ALL IMMEDIATE';
  161. }
  162. /**
  163. * @inheritDoc
  164. */
  165. public function supports(string $feature): bool
  166. {
  167. switch ($feature) {
  168. case static::FEATURE_CTE:
  169. case static::FEATURE_JSON:
  170. case static::FEATURE_WINDOW:
  171. return true;
  172. }
  173. return parent::supports($feature);
  174. }
  175. /**
  176. * @inheritDoc
  177. */
  178. public function supportsDynamicConstraints(): bool
  179. {
  180. return true;
  181. }
  182. /**
  183. * @inheritDoc
  184. */
  185. protected function _transformDistinct(Query $query): Query
  186. {
  187. return $query;
  188. }
  189. /**
  190. * @inheritDoc
  191. */
  192. protected function _insertQueryTranslator(Query $query): Query
  193. {
  194. if (!$query->clause('epilog')) {
  195. $query->epilog('RETURNING *');
  196. }
  197. return $query;
  198. }
  199. /**
  200. * @inheritDoc
  201. */
  202. protected function _expressionTranslators(): array
  203. {
  204. return [
  205. IdentifierExpression::class => '_transformIdentifierExpression',
  206. FunctionExpression::class => '_transformFunctionExpression',
  207. StringExpression::class => '_transformStringExpression',
  208. ];
  209. }
  210. /**
  211. * Changes identifer expression into postgresql format.
  212. *
  213. * @param \Cake\Database\Expression\IdentifierExpression $expression The expression to tranform.
  214. * @return void
  215. */
  216. protected function _transformIdentifierExpression(IdentifierExpression $expression): void
  217. {
  218. $collation = $expression->getCollation();
  219. if ($collation) {
  220. // use trim() to work around expression being transformed multiple times
  221. $expression->setCollation('"' . trim($collation, '"') . '"');
  222. }
  223. }
  224. /**
  225. * Receives a FunctionExpression and changes it so that it conforms to this
  226. * SQL dialect.
  227. *
  228. * @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert
  229. * to postgres SQL.
  230. * @return void
  231. */
  232. protected function _transformFunctionExpression(FunctionExpression $expression): void
  233. {
  234. switch ($expression->getName()) {
  235. case 'CONCAT':
  236. // CONCAT function is expressed as exp1 || exp2
  237. $expression->setName('')->setConjunction(' ||');
  238. break;
  239. case 'DATEDIFF':
  240. $expression
  241. ->setName('')
  242. ->setConjunction('-')
  243. ->iterateParts(function ($p) {
  244. if (is_string($p)) {
  245. $p = ['value' => [$p => 'literal'], 'type' => null];
  246. } else {
  247. $p['value'] = [$p['value']];
  248. }
  249. return new FunctionExpression('DATE', $p['value'], [$p['type']]);
  250. });
  251. break;
  252. case 'CURRENT_DATE':
  253. $time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
  254. $expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'date' => 'literal']);
  255. break;
  256. case 'CURRENT_TIME':
  257. $time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
  258. $expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'time' => 'literal']);
  259. break;
  260. case 'NOW':
  261. $expression->setName('LOCALTIMESTAMP')->add([' 0 ' => 'literal']);
  262. break;
  263. case 'RAND':
  264. $expression->setName('RANDOM');
  265. break;
  266. case 'DATE_ADD':
  267. $expression
  268. ->setName('')
  269. ->setConjunction(' + INTERVAL')
  270. ->iterateParts(function ($p, $key) {
  271. if ($key === 1) {
  272. $p = sprintf("'%s'", $p);
  273. }
  274. return $p;
  275. });
  276. break;
  277. case 'DAYOFWEEK':
  278. $expression
  279. ->setName('EXTRACT')
  280. ->setConjunction(' ')
  281. ->add(['DOW FROM' => 'literal'], [], true)
  282. ->add([') + (1' => 'literal']); // Postgres starts on index 0 but Sunday should be 1
  283. break;
  284. }
  285. }
  286. /**
  287. * Changes string expression into postgresql format.
  288. *
  289. * @param \Cake\Database\Expression\StringExpression $expression The string expression to tranform.
  290. * @return void
  291. */
  292. protected function _transformStringExpression(StringExpression $expression): void
  293. {
  294. // use trim() to work around expression being transformed multiple times
  295. $expression->setCollation('"' . trim($expression->getCollation(), '"') . '"');
  296. }
  297. /**
  298. * {@inheritDoc}
  299. *
  300. * @return \Cake\Database\PostgresCompiler
  301. */
  302. public function newCompiler(): QueryCompiler
  303. {
  304. return new PostgresCompiler();
  305. }
  306. }