*/ protected array $_templates = [ 'delete' => 'DELETE', 'where' => ' WHERE %s', 'group' => ' GROUP BY %s ', 'having' => ' HAVING %s ', 'order' => ' %s', 'limit' => ' LIMIT %s', 'offset' => ' OFFSET %s', 'epilog' => ' %s', 'comment' => '/* %s */ ', ]; /** * The list of query clauses to traverse for generating a SELECT statement * * @var array */ protected array $_selectParts = [ 'comment', 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order', 'limit', 'offset', 'union', 'epilog', 'intersect', ]; /** * The list of query clauses to traverse for generating an UPDATE statement * * @var array */ protected array $_updateParts = ['comment', 'with', 'update', 'set', 'where', 'epilog']; /** * The list of query clauses to traverse for generating a DELETE statement * * @var array */ protected array $_deleteParts = ['comment', 'with', 'delete', 'optimizerHint', 'modifier', 'from', 'where', 'epilog']; /** * The list of query clauses to traverse for generating an INSERT statement * * @var array */ protected array $_insertParts = ['comment', 'with', 'insert', 'values', 'epilog']; /** * Indicate whether aliases in SELECT clause need to be always quoted. * * @var bool */ protected bool $_quotedSelectAliases = false; /** * Returns the SQL representation of the provided query after generating * the placeholders for the bound values using the provided generator * * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholders * @return string */ public function compile(Query $query, ValueBinder $binder): string { $sql = ''; $type = $query->type(); $query->traverseParts( $this->_sqlCompiler($sql, $query, $binder), $this->{"_{$type}Parts"}, ); // Propagate bound parameters from sub-queries if the // placeholders can be found in the SQL statement. Only // add new placeholders, as sub-queries may have been executed already. if ($query->getValueBinder() !== $binder) { $existing = $binder->bindings(); foreach ($query->getValueBinder()->bindings() as $binding) { $placeholder = ':' . $binding['placeholder']; if (!isset($existing[$placeholder]) && preg_match('/' . $placeholder . '(?:\W|$)/', $sql) > 0) { $binder->bind($placeholder, $binding['value'], $binding['type']); } } } return $sql; } /** * Returns a closure that can be used to compile a SQL string representation * of this query. * * @param string $sql initial sql string to append to * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return \Closure */ protected function _sqlCompiler(string &$sql, Query $query, ValueBinder $binder): Closure { return function ($part, $partName) use (&$sql, $query, $binder): void { if ( $part === null || ($part === []) || ($part instanceof Countable && count($part) === 0) ) { return; } if ($part instanceof ExpressionInterface) { $part = [$part->sql($binder)]; } if (isset($this->_templates[$partName])) { $part = $this->_stringifyExpressions((array)$part, $binder); $sql .= sprintf($this->_templates[$partName], implode(', ', $part)); return; } $sql .= $this->{'_build' . $partName . 'Part'}($part, $query, $binder); }; } /** * Helper function used to build the string representation of a `WITH` clause, * it constructs the CTE definitions list and generates the `RECURSIVE` * keyword when required. * * @param array<\Cake\Database\Expression\CommonTableExpression> $parts List of CTEs to be transformed to string * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string */ protected function _buildWithPart(array $parts, Query $query, ValueBinder $binder): string { $recursive = false; $expressions = []; foreach ($parts as $cte) { $recursive = $recursive || $cte->isRecursive(); $expressions[] = $cte->sql($binder); } $recursive = $recursive ? 'RECURSIVE ' : ''; return sprintf('WITH %s%s ', $recursive, implode(', ', $expressions)); } /** * Helper function used to build the string representation of a SELECT clause, * it constructs the field list taking care of aliasing and * converting expression objects to string. This function also constructs the * DISTINCT clause for the query. * * @param array $parts list of fields to be transformed to string * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string */ protected function _buildSelectPart(array $parts, Query $query, ValueBinder $binder): string { $driver = $query->getDriver(); $select = 'SELECT%s%s %s%s'; if ( ($query->clause('union') || $query->clause('intersect')) && $driver->supports(DriverFeatureEnum::SET_OPERATIONS_ORDER_BY) ) { $select = '(SELECT%s%s %s%s'; } $hint = $this->_buildOptimizerHintPart($query->clause('optimizerHint'), $query, $binder); $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder); $quoteIdentifiers = $driver->isAutoQuotingEnabled() || $this->_quotedSelectAliases; $normalized = []; $parts = $this->_stringifyExpressions($parts, $binder); foreach ($parts as $k => $p) { if (!is_numeric($k)) { $p .= ' AS '; if ($quoteIdentifiers) { $p .= $driver->quoteIdentifier($k); } else { $p .= $k; } } $normalized[] = $p; } $distinct = $query->clause('distinct'); if ($distinct === true) { $distinct = 'DISTINCT '; } elseif (is_array($distinct)) { $distinct = $this->_stringifyExpressions($distinct, $binder); $distinct = sprintf('DISTINCT ON (%s) ', implode(', ', $distinct)); } return sprintf($select, $hint, $modifiers, $distinct, implode(', ', $normalized)); } /** * Helper function used to build the string representation of a FROM clause, * it constructs the tables list taking care of aliasing and * converting expression objects to string. * * @param array $parts list of tables to be transformed to string * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string */ protected function _buildFromPart(array $parts, Query $query, ValueBinder $binder): string { $select = ' FROM %s'; $normalized = []; $parts = $this->_stringifyExpressions($parts, $binder); foreach ($parts as $k => $p) { if (!is_numeric($k)) { $p = $p . ' ' . $k; } $normalized[] = $p; } return sprintf($select, implode(', ', $normalized)); } /** * Helper function used to build the string representation of multiple JOIN clauses, * it constructs the joins list taking care of aliasing and converting * expression objects to string in both the table to be joined and the conditions * to be used. * * @param array $parts list of joins to be transformed to string * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string */ protected function _buildJoinPart(array $parts, Query $query, ValueBinder $binder): string { $joins = ''; foreach ($parts as $join) { if (!isset($join['table'])) { throw new DatabaseException(sprintf( 'Could not compile join clause for alias `%s`. No table was specified. ' . 'Use the `table` key to define a table.', $join['alias'], )); } if ($join['table'] instanceof ExpressionInterface) { $join['table'] = '(' . $join['table']->sql($binder) . ')'; } $joins .= sprintf(' %s JOIN %s %s', $join['type'], $join['table'], $join['alias']); $condition = ''; if (isset($join['conditions']) && $join['conditions'] instanceof ExpressionInterface) { $condition = $join['conditions']->sql($binder); } if ($condition === '') { $joins .= ' ON 1 = 1'; } else { $joins .= " ON {$condition}"; } } return $joins; } /** * Helper function to build the string representation of a window clause. * * @param array $parts List of windows to be transformed to string * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string */ protected function _buildWindowPart(array $parts, Query $query, ValueBinder $binder): string { $windows = []; foreach ($parts as $window) { /** @var \Cake\Database\Expression\IdentifierExpression $expr */ $expr = $window['name']; /** @var \Cake\Database\Expression\IdentifierExpression $windowExpr */ $windowExpr = $window['window']; $windows[] = $expr->sql($binder) . ' AS (' . $windowExpr->sql($binder) . ')'; } return ' WINDOW ' . implode(', ', $windows); } /** * Helper function to generate SQL for SET expressions. * * @param array $parts List of keys and values to set. * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string */ protected function _buildSetPart(array $parts, Query $query, ValueBinder $binder): string { $set = []; foreach ($parts as $part) { if ($part instanceof ExpressionInterface) { $part = $part->sql($binder); } if (str_starts_with($part, '(')) { $part = substr($part, 1, -1); } $set[] = $part; } return ' SET ' . implode('', $set); } /** * Builds the SQL string for all the `operation` clauses in this query, when dealing * with query objects it will also transform them using their configured SQL * dialect. * * @param string $operation * @param array $parts * @param \Cake\Database\Query $query * @param \Cake\Database\ValueBinder $binder * @return string */ protected function _buildSetOperationPart( string $operation, array $parts, Query $query, ValueBinder $binder, ): string { $setOperationsOrderBy = $query ->getConnection() ->getDriver($query->getConnectionRole()) ->supports(DriverFeatureEnum::SET_OPERATIONS_ORDER_BY); $parts = array_map(function (array $p) use ($binder, $setOperationsOrderBy) { /** @var \Cake\Database\Expression\IdentifierExpression $expr */ $expr = $p['query']; $p['query'] = $expr->sql($binder); $p['query'] = str_starts_with($p['query'], '(') ? trim($p['query'], '()') : $p['query']; $prefix = $p['all'] ? 'ALL ' : ''; if ($setOperationsOrderBy) { return "{$prefix}({$p['query']})"; } return $prefix . $p['query']; }, $parts); if ($setOperationsOrderBy) { return sprintf(")\n{$operation} %s", implode("\n{$operation} ", $parts)); } return sprintf("\n{$operation} %s", implode("\n{$operation} ", $parts)); } /** * Builds the SQL string for all the INTERSECT clauses in this query, when dealing * with query objects it will also transform them using their configured SQL * dialect. * * @param array $parts list of queries to be operated with INTERSECT * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string */ protected function _buildIntersectPart(array $parts, Query $query, ValueBinder $binder): string { return $this->_buildSetOperationPart('INTERSECT', $parts, $query, $binder); } /** * Builds the SQL string for all the UNION clauses in this query, when dealing * with query objects it will also transform them using their configured SQL * dialect. * * @param array $parts list of queries to be operated with UNION * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string */ protected function _buildUnionPart(array $parts, Query $query, ValueBinder $binder): string { return $this->_buildSetOperationPart('UNION', $parts, $query, $binder); } /** * Builds the SQL fragment for INSERT INTO. * * @param array $parts The insert parts. * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string SQL fragment. */ protected function _buildInsertPart(array $parts, Query $query, ValueBinder $binder): string { if (!isset($parts[0])) { throw new DatabaseException( 'Could not compile insert query. No table was specified. ' . 'Use `into()` to define a table.', ); } $table = $parts[0]; $columns = $this->_stringifyExpressions($parts[1], $binder); $hint = $this->_buildOptimizerHintPart($query->clause('optimizerHint'), $query, $binder); $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder); return sprintf('INSERT%s%s INTO %s (%s)', $hint, $modifiers, $table, implode(', ', $columns)); } /** * Builds the SQL fragment for INSERT INTO. * * @param array $parts The values parts. * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string SQL fragment. */ protected function _buildValuesPart(array $parts, Query $query, ValueBinder $binder): string { return implode('', $this->_stringifyExpressions($parts, $binder)); } /** * Builds the SQL fragment for UPDATE. * * @param array $parts The update parts. * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string SQL fragment. */ protected function _buildUpdatePart(array $parts, Query $query, ValueBinder $binder): string { $table = $this->_stringifyExpressions($parts, $binder); $hint = $this->_buildOptimizerHintPart($query->clause('optimizerHint'), $query, $binder); $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder); return sprintf('UPDATE%s%s %s', $hint, $modifiers, implode(',', $table)); } /** * Builds the optimizer hint comment part. * * @param list $parts The optmizer hints * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string Optimizer hint comment */ protected function _buildOptimizerHintPart(array $parts, Query $query, ValueBinder $binder): string { if ($parts === [] || !$query->getDriver()->supports(DriverFeatureEnum::OPTIMIZER_HINT_COMMENT)) { return ''; } return sprintf(' /*+ %s */', implode(' ', $parts)); } /** * Builds the SQL modifier fragment * * @param array $parts The query modifier parts * @param \Cake\Database\Query $query The query that is being compiled * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @return string SQL fragment. */ protected function _buildModifierPart(array $parts, Query $query, ValueBinder $binder): string { if ($parts === []) { return ''; } return ' ' . implode(' ', $this->_stringifyExpressions($parts, $binder, false)); } /** * Helper function used to covert ExpressionInterface objects inside an array * into their string representation. * * @param array $expressions list of strings and ExpressionInterface objects * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder * @param bool $wrap Whether to wrap each expression object with parenthesis * @return array */ protected function _stringifyExpressions(array $expressions, ValueBinder $binder, bool $wrap = true): array { $result = []; foreach ($expressions as $k => $expression) { if ($expression instanceof ExpressionInterface) { $value = $expression->sql($binder); $expression = $wrap ? '(' . $value . ')' : $value; } $result[$k] = $expression; } return $result; } }