*/ protected array $_baseConfig = [ 'persistent' => true, 'host' => 'localhost', 'username' => 'root', 'password' => '', 'database' => 'cake', 'schema' => 'public', 'port' => 5432, 'encoding' => 'utf8', 'timezone' => null, 'flags' => [], 'init' => [], 'ssl_key' => null, 'ssl_cert' => null, 'ssl_ca' => null, 'ssl' => false, 'ssl_mode' => null, ]; /** * String used to start a database identifier quoting to make it safe * * @var string */ protected string $_startQuote = '"'; /** * String used to end a database identifier quoting to make it safe * * @var string */ protected string $_endQuote = '"'; /** * @inheritDoc */ public function connect(): void { if ($this->pdo !== null) { return; } $config = $this->_config; $config['flags'] += [ PDO::ATTR_PERSISTENT => $config['persistent'], PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, ]; if (empty($config['unix_socket'])) { $dsn = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}"; } else { $dsn = "pgsql:dbname={$config['database']}"; } if ($this->_config['ssl']) { if ($this->_config['ssl_mode']) { $dsn .= ';sslmode=' . $this->_config['ssl_mode']; } else { $dsn .= ';sslmode=allow'; } if ($this->_config['ssl_key']) { $dsn .= ';sslkey=' . $this->_config['ssl_key']; } if ($this->_config['ssl_cert']) { $dsn .= ';sslcert=' . $this->_config['ssl_cert']; } if ($this->_config['ssl_ca']) { $dsn .= ';sslrootcert=' . $this->_config['ssl_ca']; } } $this->pdo = $this->createPdo($dsn, $config); if (!empty($config['encoding'])) { $this->setEncoding($config['encoding']); } if (!empty($config['schema'])) { $this->setSchema($config['schema']); } if (!empty($config['timezone'])) { $config['init'][] = sprintf('SET timezone = %s', $this->getPdo()->quote($config['timezone'])); } foreach ($config['init'] as $command) { /** @phpstan-ignore-next-line */ $this->pdo->exec($command); } } /** * Returns whether php is able to use this driver for connecting to database * * @return bool true if it is valid to use this driver */ public function enabled(): bool { return in_array('pgsql', PDO::getAvailableDrivers(), true); } /** * @inheritDoc */ public function schemaDialect(): SchemaDialect { return $this->_schemaDialect ?? ($this->_schemaDialect = new PostgresSchemaDialect($this)); } /** * Sets connection encoding * * @param string $encoding The encoding to use. * @return void */ public function setEncoding(string $encoding): void { $pdo = $this->getPdo(); $pdo->exec('SET NAMES ' . $pdo->quote($encoding)); } /** * Sets connection default schema, if any relation defined in a query is not fully qualified * postgres will fallback to looking the relation into defined default schema * * @param string $schema The schema names to set `search_path` to. * @return void */ public function setSchema(string $schema): void { $pdo = $this->getPdo(); $pdo->exec('SET search_path TO ' . $pdo->quote($schema)); } /** * Get the SQL for disabling foreign keys. * * @return string */ public function disableForeignKeySQL(): string { return 'SET CONSTRAINTS ALL DEFERRED'; } /** * @inheritDoc */ public function enableForeignKeySQL(): string { return 'SET CONSTRAINTS ALL IMMEDIATE'; } /** * @inheritDoc */ public function supports(DriverFeatureEnum $feature): bool { return match ($feature) { DriverFeatureEnum::CTE, DriverFeatureEnum::JSON, DriverFeatureEnum::SAVEPOINT, DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS, DriverFeatureEnum::WINDOW => true, DriverFeatureEnum::INTERSECT => true, DriverFeatureEnum::INTERSECT_ALL => true, DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => true, DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION => false, DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => true, DriverFeatureEnum::CHECK_CONSTRAINTS => true, }; } /** * @inheritDoc */ protected function _transformDistinct(SelectQuery $query): SelectQuery { return $query; } /** * @inheritDoc */ protected function _insertQueryTranslator(InsertQuery $query): InsertQuery { if (!$query->clause('epilog')) { $query->epilog('RETURNING *'); } return $query; } /** * @inheritDoc */ protected function _expressionTranslators(): array { return [ IdentifierExpression::class => '_transformIdentifierExpression', FunctionExpression::class => '_transformFunctionExpression', StringExpression::class => '_transformStringExpression', ]; } /** * Changes identifer expression into postgresql format. * * @param \Cake\Database\Expression\IdentifierExpression $expression The expression to transform. * @return void */ protected function _transformIdentifierExpression(IdentifierExpression $expression): void { $collation = $expression->getCollation(); if ($collation) { // use trim() to work around expression being transformed multiple times $expression->setCollation('"' . trim($collation, '"') . '"'); } } /** * Receives a FunctionExpression and changes it so that it conforms to this * SQL dialect. * * @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert * to postgres SQL. * @return void */ protected function _transformFunctionExpression(FunctionExpression $expression): void { switch ($expression->getName()) { case 'CONCAT': // CONCAT function is expressed as exp1 || exp2 $expression->setName('')->setConjunction(' ||'); break; case 'DATEDIFF': $expression ->setName('') ->setConjunction('-') ->iterateParts(function ($p) { if (is_string($p)) { $p = ['value' => [$p => 'literal'], 'type' => null]; } else { $p['value'] = [$p['value']]; } return new FunctionExpression('DATE', $p['value'], [$p['type']]); }); break; case 'CURRENT_DATE': $time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']); $expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'date' => 'literal']); break; case 'CURRENT_TIME': $time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']); $expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'time' => 'literal']); break; case 'NOW': $expression->setName('LOCALTIMESTAMP')->add([' 0 ' => 'literal']); break; case 'RAND': $expression->setName('RANDOM'); break; case 'DATE_ADD': $expression ->setName('') ->setConjunction(' + INTERVAL') ->iterateParts(function ($p, $key) { if ($key === 1) { return sprintf("'%s'", $p); } return $p; }); break; case 'DAYOFWEEK': $expression ->setName('EXTRACT') ->setConjunction(' ') ->add(['DOW FROM' => 'literal'], [], true) ->add([') + (1' => 'literal']); // Postgres starts on index 0 but Sunday should be 1 break; case 'JSON_VALUE': $expression->setName('JSONB_PATH_QUERY') ->iterateParts(function ($p, $key) { if ($key === 0) { $p = sprintf('%s::jsonb', $p); } elseif ($key === 1) { $p = sprintf("'%s'::jsonpath", $this->quoteIdentifier($p['value'])); } return $p; }); break; } } /** * Changes string expression into postgresql format. * * @param \Cake\Database\Expression\StringExpression $expression The string expression to transform. * @return void */ protected function _transformStringExpression(StringExpression $expression): void { // use trim() to work around expression being transformed multiple times $expression->setCollation('"' . trim($expression->getCollation(), '"') . '"'); } /** * {@inheritDoc} * * @return \Cake\Database\PostgresCompiler */ public function newCompiler(): QueryCompiler { return new PostgresCompiler(); } }