2013-06-21 00:32:08 +02:00
|
|
|
<?php
|
|
|
|
|
2016-06-15 06:03:16 +02:00
|
|
|
namespace SilverStripe\ORM\Connect;
|
|
|
|
|
|
|
|
use InvalidArgumentException;
|
|
|
|
use SilverStripe\ORM\Queries\SQLExpression;
|
|
|
|
use SilverStripe\ORM\Queries\SQLSelect;
|
|
|
|
use SilverStripe\ORM\Queries\SQLDelete;
|
|
|
|
use SilverStripe\ORM\Queries\SQLInsert;
|
|
|
|
use SilverStripe\ORM\Queries\SQLUpdate;
|
|
|
|
use SilverStripe\ORM\Queries\SQLConditionalExpression;
|
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Builds a SQL query string from a SQLExpression object
|
|
|
|
*/
|
|
|
|
class DBQueryBuilder {
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Determines the line separator to use.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @return string Non-empty whitespace character
|
|
|
|
*/
|
|
|
|
public function getSeparator() {
|
|
|
|
return "\n ";
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Builds a sql query with the specified connection
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param SQLExpression $query The expression object to build from
|
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string The resulting SQL as a string
|
|
|
|
*/
|
|
|
|
public function buildSQL(SQLExpression $query, &$parameters) {
|
|
|
|
$sql = null;
|
|
|
|
$parameters = array();
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Ignore null queries
|
|
|
|
if($query->isEmpty()) return null;
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
if($query instanceof SQLSelect) {
|
|
|
|
$sql = $this->buildSelectQuery($query, $parameters);
|
|
|
|
} elseif($query instanceof SQLDelete) {
|
|
|
|
$sql = $this->buildDeleteQuery($query, $parameters);
|
|
|
|
} elseif($query instanceof SQLInsert) {
|
|
|
|
$sql = $this->buildInsertQuery($query, $parameters);
|
|
|
|
} elseif($query instanceof SQLUpdate) {
|
|
|
|
$sql = $this->buildUpdateQuery($query, $parameters);
|
|
|
|
} else {
|
2016-08-19 00:51:35 +02:00
|
|
|
throw new InvalidArgumentException(
|
|
|
|
"Not implemented: query generation for type " . get_class($query)
|
|
|
|
);
|
2013-06-21 00:32:08 +02:00
|
|
|
}
|
|
|
|
return $sql;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Builds a query from a SQLSelect expression
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param SQLSelect $query The expression object to build from
|
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string Completed SQL string
|
|
|
|
*/
|
|
|
|
protected function buildSelectQuery(SQLSelect $query, array &$parameters) {
|
|
|
|
$sql = $this->buildSelectFragment($query, $parameters);
|
|
|
|
$sql .= $this->buildFromFragment($query, $parameters);
|
|
|
|
$sql .= $this->buildWhereFragment($query, $parameters);
|
|
|
|
$sql .= $this->buildGroupByFragment($query, $parameters);
|
|
|
|
$sql .= $this->buildHavingFragment($query, $parameters);
|
|
|
|
$sql .= $this->buildOrderByFragment($query, $parameters);
|
|
|
|
$sql .= $this->buildLimitFragment($query, $parameters);
|
|
|
|
return $sql;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Builds a query from a SQLDelete expression
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param SQLDelete $query The expression object to build from
|
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string Completed SQL string
|
|
|
|
*/
|
|
|
|
protected function buildDeleteQuery(SQLDelete $query, array &$parameters) {
|
|
|
|
$sql = $this->buildDeleteFragment($query, $parameters);
|
|
|
|
$sql .= $this->buildFromFragment($query, $parameters);
|
|
|
|
$sql .= $this->buildWhereFragment($query, $parameters);
|
|
|
|
return $sql;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Builds a query from a SQLInsert expression
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param SQLInsert $query The expression object to build from
|
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string Completed SQL string
|
|
|
|
*/
|
|
|
|
protected function buildInsertQuery(SQLInsert $query, array &$parameters) {
|
|
|
|
$nl = $this->getSeparator();
|
|
|
|
$into = $query->getInto();
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Column identifiers
|
|
|
|
$columns = $query->getColumns();
|
|
|
|
$sql = "INSERT INTO {$into}{$nl}(" . implode(', ', $columns) . ")";
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Values
|
|
|
|
$sql .= "{$nl}VALUES";
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Build all rows
|
|
|
|
$rowParts = array();
|
|
|
|
foreach($query->getRows() as $row) {
|
|
|
|
// Build all columns in this row
|
|
|
|
$assignments = $row->getAssignments();
|
|
|
|
// Join SET components together, considering parameters
|
|
|
|
$parts = array();
|
|
|
|
foreach($columns as $column) {
|
|
|
|
// Check if this column has a value for this row
|
|
|
|
if(isset($assignments[$column])) {
|
|
|
|
// Assigment is a single item array, expand with a loop here
|
|
|
|
foreach($assignments[$column] as $assignmentSQL => $assignmentParameters) {
|
|
|
|
$parts[] = $assignmentSQL;
|
|
|
|
$parameters = array_merge($parameters, $assignmentParameters);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This row is missing a value for a column used by another row
|
|
|
|
$parts[] = '?';
|
|
|
|
$parameters[] = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$rowParts[] = '(' . implode(', ', $parts) . ')';
|
|
|
|
}
|
|
|
|
$sql .= $nl . implode(",$nl", $rowParts);
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
return $sql;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Builds a query from a SQLUpdate expression
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param SQLUpdate $query The expression object to build from
|
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string Completed SQL string
|
|
|
|
*/
|
|
|
|
protected function buildUpdateQuery(SQLUpdate $query, array &$parameters) {
|
|
|
|
$sql = $this->buildUpdateFragment($query, $parameters);
|
|
|
|
$sql .= $this->buildWhereFragment($query, $parameters);
|
|
|
|
return $sql;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the SELECT clauses ready for inserting into a query.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param SQLSelect $query The expression object to build from
|
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string Completed select part of statement
|
|
|
|
*/
|
|
|
|
protected function buildSelectFragment(SQLSelect $query, array &$parameters) {
|
|
|
|
$distinct = $query->getDistinct();
|
|
|
|
$select = $query->getSelect();
|
|
|
|
$clauses = array();
|
|
|
|
|
|
|
|
foreach ($select as $alias => $field) {
|
|
|
|
// Don't include redundant aliases.
|
|
|
|
$fieldAlias = "\"{$alias}\"";
|
|
|
|
if ($alias === $field || substr($field, -strlen($fieldAlias)) === $fieldAlias) {
|
|
|
|
$clauses[] = $field;
|
|
|
|
} else {
|
|
|
|
$clauses[] = "$field AS $fieldAlias";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$text = 'SELECT ';
|
2016-08-19 00:51:35 +02:00
|
|
|
if ($distinct) {
|
|
|
|
$text .= 'DISTINCT ';
|
|
|
|
}
|
|
|
|
return $text . implode(', ', $clauses);
|
2013-06-21 00:32:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the DELETE clause ready for inserting into a query.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2016-08-19 00:51:35 +02:00
|
|
|
* @param SQLDelete $query The expression object to build from
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string Completed delete part of statement
|
|
|
|
*/
|
|
|
|
public function buildDeleteFragment(SQLDelete $query, array &$parameters) {
|
|
|
|
$text = 'DELETE';
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// If doing a multiple table delete then list the target deletion tables here
|
|
|
|
// Note that some schemas don't support multiple table deletion
|
|
|
|
$delete = $query->getDelete();
|
|
|
|
if(!empty($delete)) {
|
|
|
|
$text .= ' ' . implode(', ', $delete);
|
|
|
|
}
|
|
|
|
return $text;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the UPDATE clause ready for inserting into a query.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2016-08-19 00:51:35 +02:00
|
|
|
* @param SQLUpdate $query The expression object to build from
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string Completed from part of statement
|
|
|
|
*/
|
|
|
|
public function buildUpdateFragment(SQLUpdate $query, array &$parameters) {
|
|
|
|
$table = $query->getTable();
|
|
|
|
$text = "UPDATE $table";
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Join SET components together, considering parameters
|
|
|
|
$parts = array();
|
|
|
|
foreach($query->getAssignments() as $column => $assignment) {
|
|
|
|
// Assigment is a single item array, expand with a loop here
|
|
|
|
foreach($assignment as $assignmentSQL => $assignmentParameters) {
|
|
|
|
$parts[] = "$column = $assignmentSQL";
|
|
|
|
$parameters = array_merge($parameters, $assignmentParameters);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$nl = $this->getSeparator();
|
|
|
|
$text .= "{$nl}SET " . implode(', ', $parts);
|
|
|
|
return $text;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the FROM clause ready for inserting into a query.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2016-08-19 00:51:35 +02:00
|
|
|
* @param SQLConditionalExpression $query The expression object to build from
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string Completed from part of statement
|
|
|
|
*/
|
2014-09-15 02:27:55 +02:00
|
|
|
public function buildFromFragment(SQLConditionalExpression $query, array &$parameters) {
|
|
|
|
$from = $query->getJoins($joinParameters);
|
|
|
|
$parameters = array_merge($parameters, $joinParameters);
|
2013-06-21 00:32:08 +02:00
|
|
|
$nl = $this->getSeparator();
|
|
|
|
return "{$nl}FROM " . implode(' ', $from);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the WHERE clauses ready for inserting into a query.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2016-08-19 00:51:35 +02:00
|
|
|
* @param SQLConditionalExpression $query The expression object to build from
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string Completed where condition
|
|
|
|
*/
|
2014-09-15 02:27:55 +02:00
|
|
|
public function buildWhereFragment(SQLConditionalExpression $query, array &$parameters) {
|
2013-06-21 00:32:08 +02:00
|
|
|
// Get parameterised elements
|
|
|
|
$where = $query->getWhereParameterised($whereParameters);
|
|
|
|
if(empty($where)) return '';
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Join conditions
|
|
|
|
$connective = $query->getConnective();
|
|
|
|
$parameters = array_merge($parameters, $whereParameters);
|
|
|
|
$nl = $this->getSeparator();
|
|
|
|
return "{$nl}WHERE (" . implode("){$nl}{$connective} (", $where) . ")";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the ORDER BY clauses ready for inserting into a query.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param SQLSelect $query The expression object to build from
|
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string Completed order by part of statement
|
|
|
|
*/
|
|
|
|
public function buildOrderByFragment(SQLSelect $query, array &$parameters) {
|
|
|
|
$orderBy = $query->getOrderBy();
|
|
|
|
if(empty($orderBy)) return '';
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Build orders, each with direction considered
|
|
|
|
$statements = array();
|
|
|
|
foreach ($orderBy as $clause => $dir) {
|
|
|
|
$statements[] = trim("$clause $dir");
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
$nl = $this->getSeparator();
|
|
|
|
return "{$nl}ORDER BY " . implode(', ', $statements);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the GROUP BY clauses ready for inserting into a query.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param SQLSelect $query The expression object to build from
|
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string Completed group part of statement
|
|
|
|
*/
|
|
|
|
public function buildGroupByFragment(SQLSelect $query, array &$parameters) {
|
|
|
|
$groupBy = $query->getGroupBy();
|
|
|
|
if(empty($groupBy)) return '';
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
$nl = $this->getSeparator();
|
|
|
|
return "{$nl}GROUP BY " . implode(', ', $groupBy);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the HAVING clauses ready for inserting into a query.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param SQLSelect $query The expression object to build from
|
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function buildHavingFragment(SQLSelect $query, array &$parameters) {
|
|
|
|
$having = $query->getHavingParameterised($havingParameters);
|
|
|
|
if(empty($having)) return '';
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Generate having, considering parameters present
|
|
|
|
$connective = $query->getConnective();
|
2014-08-15 08:53:05 +02:00
|
|
|
$parameters = array_merge($parameters, $havingParameters);
|
|
|
|
$nl = $this->getSeparator();
|
2013-06-21 00:32:08 +02:00
|
|
|
return "{$nl}HAVING (" . implode("){$nl}{$connective} (", $having) . ")";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the LIMIT clause ready for inserting into a query.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param SQLSelect $query The expression object to build from
|
|
|
|
* @param array $parameters Out parameter for the resulting query parameters
|
|
|
|
* @return string The finalised limit SQL fragment
|
|
|
|
*/
|
|
|
|
public function buildLimitFragment(SQLSelect $query, array &$parameters) {
|
|
|
|
$nl = $this->getSeparator();
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Ensure limit is given
|
|
|
|
$limit = $query->getLimit();
|
|
|
|
if(empty($limit)) return '';
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// For literal values return this as the limit SQL
|
|
|
|
if (!is_array($limit)) {
|
|
|
|
return "{$nl}LIMIT $limit";
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Assert that the array version provides the 'limit' key
|
|
|
|
if (!isset($limit['limit']) || !is_numeric($limit['limit'])) {
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
'DBQueryBuilder::buildLimitSQL(): Wrong format for $limit: '. var_export($limit, true)
|
|
|
|
);
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Format the array limit, given an optional start key
|
|
|
|
$clause = "{$nl}LIMIT {$limit['limit']}";
|
|
|
|
if(isset($limit['start']) && is_numeric($limit['start']) && $limit['start'] !== 0) {
|
|
|
|
$clause .= " OFFSET {$limit['start']}";
|
|
|
|
}
|
|
|
|
return $clause;
|
|
|
|
}
|
|
|
|
}
|