silverstripe-framework/model/connect/DBQueryBuilder.php
Damian Mooyman d8e9af8af8 API New Database abstraction layer. Ticket #7429
Database abstraction broken up into controller, connector, query builder, and schema manager, each independently configurable via YAML / Injector
Creation of new DBQueryGenerator for database specific generation of SQL
Support for parameterised queries, move of code base to use these over escaped conditions
Refactor of SQLQuery into separate query classes for each of INSERT UPDATE DELETE and SELECT
Support for PDO
Installation process upgraded to use new ORM
SS_DatabaseException created to handle database errors, maintaining details of raw sql and parameter details for user code designed interested in that data.
Renamed DB static methods to conform correctly to naming conventions (e.g. DB::getConn -> DB::get_conn)
3.2 upgrade docs
Performance Optimisation and simplification of code to use more concise API
API Ability for database adapters to register extensions to ConfigureFromEnv.php
2014-07-09 18:04:05 +12:00

333 lines
11 KiB
PHP

<?php
/**
* Builds a SQL query string from a SQLExpression object
*
* @package framework
* @subpackage model
*/
class DBQueryBuilder {
/**
* Determines the line separator to use.
*
* @return string Non-empty whitespace character
*/
public function getSeparator() {
return "\n ";
}
/**
* Builds a sql query with the specified connection
*
* @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();
// Ignore null queries
if($query->isEmpty()) return null;
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 {
user_error("Not implemented: query generation for type " . $query->getType());
}
return $sql;
}
/**
* Builds a query from a SQLSelect expression
*
* @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;
}
/**
* Builds a query from a SQLDelete expression
*
* @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;
}
/**
* Builds a query from a SQLInsert expression
*
* @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();
// Column identifiers
$columns = $query->getColumns();
$sql = "INSERT INTO {$into}{$nl}(" . implode(', ', $columns) . ")";
// Values
$sql .= "{$nl}VALUES";
// 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);
return $sql;
}
/**
* Builds a query from a SQLUpdate expression
*
* @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.
*
* @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 ';
if ($distinct) $text .= 'DISTINCT ';
return $text .= implode(', ', $clauses);
}
/**
* Return the DELETE clause ready for inserting into a query.
*
* @param SQLExpression $query The expression object to build from
* @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';
// 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.
*
* @param SQLExpression $query The expression object to build from
* @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";
// 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.
*
* @param SQLExpression $query The expression object to build from
* @param array $parameters Out parameter for the resulting query parameters
* @return string Completed from part of statement
*/
public function buildFromFragment(SQLExpression $query, array &$parameters) {
$from = $query->getJoins();
$nl = $this->getSeparator();
return "{$nl}FROM " . implode(' ', $from);
}
/**
* Returns the WHERE clauses ready for inserting into a query.
*
* @param SQLExpression $query The expression object to build from
* @param array $parameters Out parameter for the resulting query parameters
* @return string Completed where condition
*/
public function buildWhereFragment(SQLExpression $query, array &$parameters) {
// Get parameterised elements
$where = $query->getWhereParameterised($whereParameters);
if(empty($where)) return '';
// 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.
*
* @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 '';
// Build orders, each with direction considered
$statements = array();
foreach ($orderBy as $clause => $dir) {
$statements[] = trim("$clause $dir");
}
$nl = $this->getSeparator();
return "{$nl}ORDER BY " . implode(', ', $statements);
}
/**
* Returns the GROUP BY clauses ready for inserting into a query.
*
* @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 '';
$nl = $this->getSeparator();
return "{$nl}GROUP BY " . implode(', ', $groupBy);
}
/**
* Returns the HAVING clauses ready for inserting into a query.
*
* @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 '';
// Generate having, considering parameters present
$connective = $query->getConnective();
$parameters = array_merge($parameters, $havingParameters);
$nl = $this->getSeparator();
return "{$nl}HAVING (" . implode("){$nl}{$connective} (", $having) . ")";
}
/**
* Return the LIMIT clause ready for inserting into a query.
*
* @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();
// Ensure limit is given
$limit = $query->getLimit();
if(empty($limit)) return '';
// For literal values return this as the limit SQL
if (!is_array($limit)) {
return "{$nl}LIMIT $limit";
}
// 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)
);
}
// 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;
}
}