From 051d9de4823bf8548f235e75332c3d2e948ce89b Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Thu, 3 May 2012 19:34:16 +1200 Subject: [PATCH] API CHANGE Deprecated internal access to SQLQuery properties, update core code to reflect these changes. Using set/add instead in accessor methods, and use Database::sql*ToString() to allow easier per-database adapter SQL overloading. --- core/PaginatedList.php | 6 +- docs/en/changelogs/3.0.0.md | 21 + model/DataList.php | 2 +- model/DataObject.php | 6 +- model/DataQuery.php | 105 ++--- model/Database.php | 135 ++++-- model/ManyManyList.php | 13 +- model/MySQLDatabase.php | 6 +- model/SQLQuery.php | 672 +++++++++++++++++++----------- model/Versioned.php | 22 +- tests/model/DbDatetimeTest.php | 12 +- tests/model/PaginatedListTest.php | 6 +- tests/model/SQLQueryTest.php | 167 ++++---- 13 files changed, 719 insertions(+), 454 deletions(-) diff --git a/core/PaginatedList.php b/core/PaginatedList.php index af7060c8b..195541162 100644 --- a/core/PaginatedList.php +++ b/core/PaginatedList.php @@ -137,9 +137,9 @@ class PaginatedList extends SS_ListDecorator { * @param SQLQuery $query */ public function setPaginationFromQuery(SQLQuery $query) { - if ($query->limit) { - $this->setPageLength($query->limit['limit']); - $this->setPageStart($query->limit['start']); + if ($limit = $query->getLimit()) { + $this->setPageLength($limit['limit']); + $this->setPageStart($limit['start']); $this->setTotalItems($query->unlimitedRowCount()); } } diff --git a/docs/en/changelogs/3.0.0.md b/docs/en/changelogs/3.0.0.md index 63c9b1fac..f1d949f5d 100644 --- a/docs/en/changelogs/3.0.0.md +++ b/docs/en/changelogs/3.0.0.md @@ -182,6 +182,27 @@ The abstract `RelationList` class and its implementations `ManyManyList` and `Ha are replacing the `ComponentSet` API, which is only relevant if you have instanciated these manually. Relations are retrieved through the same way (e.g. `$myMember->Groups()`). +### `SQLQuery` changes ### + +`SQLQuery` has been changed so direct access to internal properties `$from`, `$select`, `$orderby` is +now deprecated. + +Instead, there are now methods you can call which allow you to get and set SQL clauses instead. + + * `$from` getter is `getFrom()` and setters `setFrom()` and `addFrom()` + * `$select` getter is `getSelect()` and setters `setSelect()` and `addSelect()` + * `$where` getter is `getWhere()` and setter `setWhere()` and `addWhere()` + * `$orderby` getter is `getOrderBy()` and setter `setOrderBy()` and `addOrderBy()` + * `$groupby` getter is `getGroupBy()` and setter `getGroupBy()` and `addGroupBy()` + * `$having` getter is `getHaving()` and setter `setHaving()` and `addHaving()` + * `$limit` getter is `getLimit()` and setter `setLimit()` + * `$distinct` getter is `getDistinct()` and setter `setDistinct()` + * `$delete` getter is `getDelete()` and setter `setDelete()` + * `$connective` getter is `getConnective()` and settter `setConnective()` + + * `innerJoin()` has been renamed to `addInnerJoin()` + * `leftJoin()` has been renamed to `addLeftJoin()` + ### InnoDB driver for existing and new tables on MySQL (instead of MyISAM) [innodb]### SilverStripe has traditionally created all MySQL tables with the MyISAM storage driver, diff --git a/model/DataList.php b/model/DataList.php index 1c6f16bab..9f919e447 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -201,7 +201,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab return $this; } - + /** * Filter the list to include items with these charactaristics * diff --git a/model/DataObject.php b/model/DataObject.php index 58f09a244..24156ea5a 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -1216,9 +1216,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // obviously, that means getting requireTable() to configure cascading deletes ;-) $srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query(); foreach($srcQuery->queriedTables() as $table) { - $query = new SQLQuery("*", array('"'.$table.'"')); - $query->where("\"ID\" = $this->ID"); - $query->delete = true; + $query = new SQLQuery("*", array('"' . $table . '"')); + $query->setWhere("\"ID\" = $this->ID"); + $query->setDelete(true); $query->execute(); } // Remove this item out of any caches diff --git a/model/DataQuery.php b/model/DataQuery.php index 93e2e7461..3c8cf50f7 100644 --- a/model/DataQuery.php +++ b/model/DataQuery.php @@ -58,7 +58,7 @@ class DataQuery { * Return the {@link DataObject} class that is being queried. */ function dataClass() { - return $this->dataClass; + return $this->dataClass; } /** @@ -75,15 +75,22 @@ class DataQuery { */ function removeFilterOn($fieldExpression) { $matched = false; - foreach($this->query->where as $i=>$item) { - if(strpos($item, $fieldExpression) !== false) { - unset($this->query->where[$i]); + + $where = $this->query->getWhere(); + foreach($where as $i => $clause) { + if(strpos($clause, $fieldExpression) !== false) { + unset($where[$i]); $matched = true; } } - - if(!$matched) throw new InvalidArgumentException("Couldn't find $fieldExpression in the query filter."); - + + // set the entire where clause back, but clear the original one first + if($matched) { + $this->query->setWhere($where); + } else { + throw new InvalidArgumentException("Couldn't find $fieldExpression in the query filter."); + } + return $this; } @@ -108,13 +115,13 @@ class DataQuery { // Build our intial query $this->query = new SQLQuery(array()); - $this->query->distinct = true; + $this->query->setDistinct(true); if($sort = singleton($this->dataClass)->stat('default_sort')) { $this->sort($sort); } - $this->query->from("\"$baseClass\""); + $this->query->setFrom("\"$baseClass\""); singleton($this->dataClass)->extend('augmentDataQueryCreation', $this->query, $this); } @@ -140,7 +147,7 @@ class DataQuery { if($queriedColumns) { $tableClasses = ClassInfo::dataClassesFor($this->dataClass); - foreach ($query->where as $where) { + foreach ($query->getWhere() as $where) { // Check for just the column, in the form '"Column" = ?' and the form '"Table"."Column"' = ? if (preg_match('/^"([^"]+)"/', $where, $matches) || preg_match('/^"([^"]+)"\."[^"]+"/', $where, $matches)) { @@ -179,7 +186,7 @@ class DataQuery { } if ($joinTable) { - $query->leftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"") ; + $query->addLeftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"") ; } } @@ -208,7 +215,7 @@ class DataQuery { // Get the ClassName values to filter to $classNames = ClassInfo::subclassesFor($this->dataClass); if(!$classNames) user_error("DataList::create() Can't find data sub-classes for '$callerClass'"); - $query->where[] = "\"$baseClass\".\"ClassName\" IN ('" . implode("','", $classNames) . "')"; + $query->addWhere("\"$baseClass\".\"ClassName\" IN ('" . implode("','", $classNames) . "')"); } } @@ -233,9 +240,7 @@ class DataQuery { $tableClasses = ClassInfo::dataClassesFor($this->dataClass); $baseClass = array_shift($tableClasses); - if($query->orderby) { - $orderby = $query->getOrderBy(); - + if($orderby = $query->getOrderBy()) { foreach($orderby as $k => $dir) { // don't touch functions in the ORDER BY or function calls // selected as fields @@ -270,23 +275,24 @@ class DataQuery { $qualCol = "\"$parts[0]\""; } - // To-do: Remove this if block once SQLQuery::$select has been refactored to store itemisedSelect() + // To-do: Remove this if block once SQLQuery::$select has been refactored to store getSelect() // format internally; then this check can be part of selectField() - if(!isset($query->select[$col]) && !in_array($qualCol, $query->select)) { + $selects = $query->getSelect(); + if(!isset($selects[$col]) && !in_array($qualCol, $selects)) { $query->selectField($qualCol); } } else { $qualCol = '"' . implode('"."', $parts) . '"'; - // To-do: Remove this if block once SQLQuery::$select has been refactored to store itemisedSelect() + // To-do: Remove this if block once SQLQuery::$select has been refactored to store getSelect() // format internally; then this check can be part of selectField() - if(!in_array($qualCol, $query->select)) { + if(!in_array($qualCol, $query->getSelect())) { $query->selectField($qualCol); } } } - $query->orderby = $orderby; + $query->setOrderBy($orderby); } } @@ -390,7 +396,7 @@ class DataQuery { function having($having) { if($having) { $clone = $this; - $clone->query->having[] = $having; + $clone->query->addHaving($having); return $clone; } else { return $this; @@ -403,7 +409,7 @@ class DataQuery { function where($filter) { if($filter) { $clone = $this; - $clone->query->where($filter); + $clone->query->addWhere($filter); return $clone; } else { return $this; @@ -436,7 +442,11 @@ class DataQuery { */ function sort($sort = null, $direction = null, $clear = true) { $clone = $this; - $clone->query->orderby($sort, $direction, $clear); + if($clear) { + $clone->query->setOrderBy($sort, $direction); + } else { + $clone->query->addOrderBy($sort, $direction); + } return $clone; } @@ -458,7 +468,7 @@ class DataQuery { */ function limit($limit, $offset = 0) { $clone = $this; - $clone->query->limit($limit, $offset); + $clone->query->setLimit($limit, $offset); return $clone; } @@ -470,9 +480,13 @@ class DataQuery { Deprecation::notice('3.0', 'Use innerJoin() or leftJoin() instead.'); if($join) { $clone = $this; - $clone->query->from[] = $join; + $clone->query->addFrom($join); // TODO: This needs to be resolved for all databases - if(DB::getConn() instanceof MySQLDatabase) $clone->query->groupby[] = reset($clone->query->from) . ".\"ID\""; + + if(DB::getConn() instanceof MySQLDatabase) { + $from = $clone->query->getFrom(); + $clone->query->setGroupBy(reset($from) . ".\"ID\""); + } return $clone; } else { return $this; @@ -487,7 +501,7 @@ class DataQuery { public function innerJoin($table, $onClause, $alias = null) { if($table) { $clone = $this; - $clone->query->innerJoin($table, $onClause, $alias); + $clone->query->addInnerJoin($table, $onClause, $alias); return $clone; } else { return $this; @@ -502,7 +516,7 @@ class DataQuery { public function leftJoin($table, $onClause, $alias = null) { if($table) { $clone = $this; - $clone->query->leftJoin($table, $onClause, $alias); + $clone->query->addLeftJoin($table, $onClause, $alias); return $clone; } else { return $this; @@ -530,7 +544,7 @@ class DataQuery { if ($component = $model->has_one($rel)) { if(!$this->query->isJoinedTo($component)) { $foreignKey = $model->getReverseAssociation($component); - $this->query->leftJoin($component, "\"$component\".\"ID\" = \"{$modelClass}\".\"{$foreignKey}ID\""); + $this->query->addLeftJoin($component, "\"$component\".\"ID\" = \"{$modelClass}\".\"{$foreignKey}ID\""); /** * add join clause to the component's ancestry classes so that the search filter could search on its @@ -541,7 +555,7 @@ class DataQuery { $ancestry = array_reverse($ancestry); foreach($ancestry as $ancestor){ if($ancestor != $component){ - $this->query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\""); + $this->query->addInnerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\""); $component=$ancestor; } } @@ -553,7 +567,7 @@ class DataQuery { if(!$this->query->isJoinedTo($component)) { $ancestry = $model->getClassAncestry(); $foreignKey = $model->getRemoteJoinField($rel); - $this->query->leftJoin($component, "\"$component\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\""); + $this->query->addLeftJoin($component, "\"$component\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\""); /** * add join clause to the component's ancestry classes so that the search filter could search on its * ancestor fields. @@ -563,7 +577,7 @@ class DataQuery { $ancestry = array_reverse($ancestry); foreach($ancestry as $ancestor){ if($ancestor != $component){ - $this->query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\""); + $this->query->addInnerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\""); $component=$ancestor; } } @@ -575,10 +589,10 @@ class DataQuery { list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component; $parentBaseClass = ClassInfo::baseDataClass($parentClass); $componentBaseClass = ClassInfo::baseDataClass($componentClass); - $this->query->innerJoin($relationTable, "\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\""); - $this->query->leftJoin($componentBaseClass, "\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\""); + $this->query->addInnerJoin($relationTable, "\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\""); + $this->query->addLeftJoin($componentBaseClass, "\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\""); if(ClassInfo::hasTable($componentClass)) { - $this->query->leftJoin($componentClass, "\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\""); + $this->query->addLeftJoin($componentClass, "\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\""); } $modelClass = $componentClass; @@ -597,7 +611,7 @@ class DataQuery { public function subtract(DataQuery $subtractQuery, $field='ID') { $subSelect= $subtractQuery->getFinalisedQuery(); $fieldExpression = $this->expressionForField($field, $subSelect); - $subSelect->clearSelect(); + $subSelect->setSelect(array()); $subSelect->selectField($fieldExpression, $field); $this->where($this->expressionForField($field, $this).' NOT IN ('.$subSelect->sql().')'); @@ -611,7 +625,7 @@ class DataQuery { $fieldExpressions = array_map(create_function('$item', "return '\"$table\".\"' . \$item . '\"';"), $fields); - $this->query->select($fieldExpressions); + $this->query->setSelect($fieldExpressions); return $this; } @@ -621,9 +635,9 @@ class DataQuery { */ public function column($field = 'ID') { $query = $this->getFinalisedQuery(array($field)); - $originalSelect = $query->itemisedSelect(); + $originalSelect = $query->getSelect(); $fieldExpression = $this->expressionForField($field, $query); - $query->clearSelect(); + $query->setSelect(array()); $query->selectField($fieldExpression, $field); $this->ensureSelectContainsOrderbyColumns($query, $originalSelect); @@ -637,17 +651,8 @@ class DataQuery { return "\"$baseClass\".\"ID\""; } else { - return $query->expressionForField($field); - } - } - - /** - * Clear the selected fields to start over - */ - public function clearSelect() { - $this->query->clearSelect(); - - return $this; + return $query->expressionForField($field); + } } /** diff --git a/model/Database.php b/model/Database.php index c1d7c338e..d52617d0f 100644 --- a/model/Database.php +++ b/model/Database.php @@ -705,7 +705,7 @@ abstract class SS_Database { break; case "deleted": $color = "red"; - break; + break; case "changed": $color = "blue"; break; @@ -721,42 +721,119 @@ abstract class SS_Database { } /** - * Convert a SQLQuery object into a SQL statement + * Returns the SELECT clauses ready for inserting into a query. + * @param array $select Select columns + * @param boolean $distinct Distinct select? + * @return string */ - public function sqlQueryToString(SQLQuery $sqlQuery) { - $distinct = $sqlQuery->distinct ? "DISTINCT " : ""; - - if($sqlQuery->delete) { - $text = "DELETE "; - } else { - $text = "SELECT $distinct" . $sqlQuery->prepareSelect(); + public function sqlSelectToString($select, $distinct = false) { + $clauses = array(); + + foreach($select as $alias => $field) { + // Don't include redundant aliases. + if($alias === $field || preg_match('/"' . preg_quote($alias) . '"$/', $field)) $clauses[] = $field; + else $clauses[] = "$field AS \"$alias\""; } - - if($sqlQuery->from) $text .= " FROM " . implode(" ", $sqlQuery->from); - if($sqlQuery->where) $text .= " WHERE (" . $sqlQuery->prepareWhere(). ")"; - if($sqlQuery->groupby) $text .= " GROUP BY " . $sqlQuery->prepareGroupBy(); - if($sqlQuery->having) $text .= " HAVING ( " .$sqlQuery->prepareHaving() . " )"; - if($sqlQuery->orderby) $text .= " ORDER BY " . $sqlQuery->prepareOrderBy(); - if($sqlQuery->limit) { - $limit = $sqlQuery->limit; - // Pass limit as array or SQL string value - if(is_array($limit)) { - if(!array_key_exists('limit',$limit)) throw new InvalidArgumentException('SQLQuery::limit(): Wrong format for $limit: ' . var_export($limit, true)); + $text = 'SELECT '; + if($distinct) $text .= 'DISTINCT '; + return $text .= implode(', ', $clauses); + } - if(isset($limit['start']) && is_numeric($limit['start']) && isset($limit['limit']) && is_numeric($limit['limit'])) { - $combinedLimit = $limit['start'] ? "$limit[limit] OFFSET $limit[start]" : "$limit[limit]"; - } elseif(isset($limit['limit']) && is_numeric($limit['limit'])) { - $combinedLimit = (int)$limit['limit']; - } else { - $combinedLimit = false; - } - if(!empty($combinedLimit)) $text .= " LIMIT " . $combinedLimit; + /** + * Return the FROM clause ready for inserting into a query. + * @return string + */ + public function sqlFromToString($from) { + return ' FROM ' . implode(' ', $from); + } + /** + * Returns the WHERE clauses ready for inserting into a query. + * @return string + */ + public function sqlWhereToString($where, $connective) { + return ' WHERE (' . implode(") {$connective} (" , $where) . ')'; + } + + /** + * Returns the ORDER BY clauses ready for inserting into a query. + * @return string + */ + public function sqlOrderByToString($orderby) { + $statements = array(); + + foreach($orderby as $clause => $dir) { + $statements[] = trim($clause . ' ' . $dir); + } + + return ' ORDER BY ' . implode(', ', $statements); + } + + /** + * Returns the GROUP BY clauses ready for inserting into a query. + * @return string + */ + public function sqlGroupByToString($groupby) { + return ' GROUP BY ' . implode(', ', $groupby); + } + + /** + * Returns the HAVING clauses ready for inserting into a query. + * @return string + */ + public function sqlHavingToString($having) { + return ' HAVING ( ' . implode(' ) AND ( ', $having); + } + + /** + * Return the LIMIT clause ready for inserting into a query. + * @return string + */ + public function sqlLimitToString($limit) { + $clause = ''; + + // Pass limit as array or SQL string value + if(is_array($limit)) { + if(!array_key_exists('limit', $limit)) throw new InvalidArgumentException('Database::sqlLimitToString(): Wrong format for $limit: ' . var_export($limit, true)); + + if(isset($limit['start']) && is_numeric($limit['start']) && isset($limit['limit']) && is_numeric($limit['limit'])) { + $combinedLimit = $limit['start'] ? "$limit[limit] OFFSET $limit[start]" : "$limit[limit]"; + } elseif(isset($limit['limit']) && is_numeric($limit['limit'])) { + $combinedLimit = (int) $limit['limit']; } else { - $text .= " LIMIT " . $sqlQuery->limit; + $combinedLimit = false; } + if(!empty($combinedLimit)) $clause .= ' LIMIT ' . $combinedLimit; + } else { + $clause .= ' LIMIT ' . $limit; } + + return $clause; + } + + /** + * Convert a SQLQuery object into a SQL statement + * @param $query SQLQuery + */ + public function sqlQueryToString(SQLQuery $query) { + if($query->getDelete()) { + $text = 'DELETE '; + } else { + $text = $this->sqlSelectToString($query->getSelect(), $query->getDistinct()); + } + + if($query->getFrom()) $text .= $this->sqlFromToString($query->getFrom()); + if($query->getWhere()) $text .= $this->sqlWhereToString($query->getWhere(), $query->getConnective()); + + // these clauses only make sense in SELECT queries, not DELETE + if(!$query->getDelete()) { + if($query->getGroupBy()) $text .= $this->sqlGroupByToString($query->getGroupBy()); + if($query->getHaving()) $text .= $this->sqlHavingToString($query->getHaving()) . ' )'; + if($query->getOrderBy()) $text .= $this->sqlOrderByToString($query->getOrderBy()); + if($query->getLimit()) $text .= $this->sqlLimitToString($query->getLimit()); + } + return $text; } diff --git a/model/ManyManyList.php b/model/ManyManyList.php index e3ddd6bb8..4a24fe1dc 100644 --- a/model/ManyManyList.php +++ b/model/ManyManyList.php @@ -115,15 +115,15 @@ class ManyManyList extends RelationList { if(!is_numeric($itemID)) throw new InvalidArgumentException("ManyManyList::removeById() expecting an ID"); $query = new SQLQuery("*", array("\"$this->joinTable\"")); - $query->delete = true; + $query->setDelete(true); if($filter = $this->foreignIDFilter()) { - $query->where($filter); + $query->setWhere($filter); } else { user_error("Can't call ManyManyList::remove() until a foreign ID is set", E_USER_WARNING); } - $query->where("\"$this->localKey\" = {$itemID}"); + $query->addWhere("\"$this->localKey\" = {$itemID}"); $query->execute(); } @@ -132,10 +132,9 @@ class ManyManyList extends RelationList { */ function removeAll() { $query = $this->dataQuery()->query(); - $query->delete = true; - $query->select(array('*')); - $query->from = array("\"$this->joinTable\""); - $query->orderby = null; + $query->setDelete(true); + $query->setSelect(array('*')); + $query->setFrom("\"$this->joinTable\""); $query->execute(); } diff --git a/model/MySQLDatabase.php b/model/MySQLDatabase.php index 0bcda0a13..f35e85a71 100644 --- a/model/MySQLDatabase.php +++ b/model/MySQLDatabase.php @@ -855,9 +855,9 @@ class MySQLDatabase extends SS_Database { $query = $list->dataQuery()->query(); // There's no need to do all that joining - $query->from = array(str_replace(array('"','`'),'',$baseClasses[$class]) => $baseClasses[$class]); - $query->select($select[$class]); - $query->orderby = null; + $query->setFrom(array(str_replace(array('"','`'), '', $baseClasses[$class]) => $baseClasses[$class])); + $query->setSelect($select[$class]); + $query->setOrderBy(array()); $querySQLs[] = $query->sql(); $totalCount += $query->unlimitedRowCount(); diff --git a/model/SQLQuery.php b/model/SQLQuery.php index 2e49cdb99..b1c9a3dd3 100644 --- a/model/SQLQuery.php +++ b/model/SQLQuery.php @@ -17,21 +17,21 @@ class SQLQuery { * * @var array */ - public $select = array(); + protected $select = array(); /** * An array of join clauses. The first one is just the table name. * * @var array */ - public $from = array(); + protected $from = array(); /** * An array of filters. * * @var array */ - public $where = array(); + protected $where = array(); /** * An array of order by clauses, functions. Stores as an associative @@ -39,55 +39,63 @@ class SQLQuery { * * @var string */ - public $orderby = array(); + protected $orderby = array(); /** * An array of fields to group by. * * @var array */ - public $groupby = array(); + protected $groupby = array(); /** * An array of having clauses. * * @var array */ - public $having = array(); + protected $having = array(); /** * A limit clause. * * @var string */ - public $limit; + protected $limit; /** * If this is true DISTINCT will be added to the SQL. * @var boolean */ - public $distinct = false; + protected $distinct = false; /** * If this is true, this statement will delete rather than select. * * @var boolean */ - public $delete = false; + protected $delete = false; /** * The logical connective used to join WHERE clauses. Defaults to AND. * * @var string */ - public $connective = 'AND'; + protected $connective = 'AND'; /** * Keep an internal register of find/replace pairs to execute when it's time to actually get the * query SQL. + * @var array */ - private $replacementsOld = array(), $replacementsNew = array(); - + protected $replacementsOld = array(); + + /** + * Keep an internal register of find/replace pairs to execute when it's time to actually get the + * query SQL. + * @var array + */ + protected $replacementsNew = array(); + /** * Construct a new SQLQuery. * @@ -103,64 +111,91 @@ class SQLQuery { * by this stage. */ function __construct($select = "*", $from = array(), $where = "", $orderby = "", $groupby = "", $having = "", $limit = "") { - $this->select($select); + $this->setSelect($select); // @todo $this->from = is_array($from) ? $from : array(str_replace(array('"','`'),'',$from) => $from); - $this->where($where); - $this->orderby($orderby); - $this->groupby($groupby); - $this->having($having); - $this->limit($limit); + $this->setWhere($where); + $this->setOrderBy($orderby); + $this->setGroupBy($groupby); + $this->setHaving($having); + $this->setLimit($limit); } - - /** - * Clear the selected fields to start over - */ - function clearSelect() { - $this->select = array(); - return $this; + + function __get($field) { + if(strtolower($field) == 'select') Deprecation::notice('3.0', 'Please use getSlect() instead'); + if(strtolower($field) == 'from') Deprecation::notice('3.0', 'Please use getFrom() instead'); + if(strtolower($field) == 'groupby') Deprecation::notice('3.0', 'Please use getGroupBy() instead'); + if(strtolower($field) == 'orderby') Deprecation::notice('3.0', 'Please use getOrderBy() instead'); + if(strtolower($field) == 'having') Deprecation::notice('3.0', 'Please use getHaving() instead'); + if(strtolower($field) == 'limit') Deprecation::notice('3.0', 'Please use getLimit() instead'); + if(strtolower($field) == 'delete') Deprecation::notice('3.0', 'Please use getDelete() instead'); + if(strtolower($field) == 'connective') Deprecation::notice('3.0', 'Please use getConnective() instead'); + if(strtolower($field) == 'distinct') Deprecation::notice('3.0', 'Please use getDistinct() instead'); + + return $this->$field; } - + + function __set($field, $value) { + if(strtolower($field) == 'select') Deprecation::notice('3.0', 'Please use setSelect() or addSelect() instead'); + if(strtolower($field) == 'from') Deprecation::notice('3.0', 'Please use setFrom() or addFrom() instead'); + if(strtolower($field) == 'groupby') Deprecation::notice('3.0', 'Please use setGroupBy() or addGroupBy() instead'); + if(strtolower($field) == 'orderby') Deprecation::notice('3.0', 'Please use setOrderBy() or addOrderBy() instead'); + if(strtolower($field) == 'having') Deprecation::notice('3.0', 'Please use setHaving() or addHaving() instead'); + if(strtolower($field) == 'limit') Deprecation::notice('3.0', 'Please use setLimit() instead'); + if(strtolower($field) == 'delete') Deprecation::notice('3.0', 'Please use setDelete() instead'); + if(strtolower($field) == 'connective') Deprecation::notice('3.0', 'Please use setConnective() instead'); + if(strtolower($field) == 'distinct') Deprecation::notice('3.0', 'Please use setDistinct() instead'); + + return $this->$field = $value; + } + /** - * Specify the list of columns to be selected by the query. + * Set the list of columns to be selected by the query. * * * // pass fields to select as single parameter array * $query->select(array("Col1","Col2"))->from("MyTable"); - * + * * // pass fields to select as multiple parameters * $query->select("Col1", "Col2")->from("MyTable"); * - * - * @param mixed $fields + * + * @param string|array $fields + * @param boolean $clear Clear existing select fields? * @return SQLQuery */ - public function select($fields) { + public function setSelect($fields) { + $this->select = array(); + if (func_num_args() > 1) { + $fields = func_get_args(); + } else if(!is_array($fields)) { + $fields = array($fields); + } + return $this->addSelect($fields); + } + + /** + * Add to the list of columns to be selected by the query. + * + * + * // pass fields to select as single parameter array + * $query->select(array("Col1","Col2"))->from("MyTable"); + * + * // pass fields to select as multiple parameters + * $query->select("Col1", "Col2")->from("MyTable"); + * + * + * @param string|array $fields + * @param boolean $clear Clear existing select fields? + * @return SQLQuery + */ + public function addSelect($fields) { if (func_num_args() > 1) { $fields = func_get_args(); } else if(!is_array($fields)) { $fields = array($fields); } - $this->select = array(); - $this->selectMore($fields); - - return $this; - } - - /** - * Add addition columns to the select clause - * - * @param array|string - */ - public function selectMore($fields) { - if (func_num_args() > 1) { - $fields = func_get_args(); - } else if(!is_array($fields)) { - $fields = array($fields); - } - - $this->select = array(); foreach($fields as $idx => $field) { if(preg_match('/^(.*) +AS +"?([^"]*)"?/i', $field, $matches)) { Deprecation::notice("3.0", "Use selectField() to specify column aliases"); @@ -169,8 +204,15 @@ class SQLQuery { $this->selectField($field, is_numeric($idx) ? null : $idx); } } + + return $this; } - + + public function select($fields) { + Deprecation::notice('3.0', 'Please use setSelect() or addSelect() instead!'); + $this->setSelect($fields); + } + /** * Select an additional field * @@ -194,68 +236,107 @@ class SQLQuery { } /** - * Specify the target table to select from. - * + * Set table for the SELECT clause. + * * * $query->from("MyTable"); // SELECT * FROM MyTable * * - * @param string $table - * @return SQLQuery This instance + * @param string|array $from + * @return SQLQuery */ - public function from($table) { - $this->from[str_replace(array('"','`'),'',$table)] = $table; - + public function setFrom($from) { + $this->from = array(); + return $this->addFrom($from); + } + + /** + * Add a table to the SELECT clause. + * + * + * $query->from("MyTable"); // SELECT * FROM MyTable + * + * + * @param string|array $from + * @return SQLQuery + */ + public function addFrom($from) { + if(is_array($from)) { + $this->from = array_merge($this->from, $from); + } elseif(!empty($from)) { + $this->from[str_replace(array('"','`'), '', $from)] = $from; + } + return $this; } + + public function from($from) { + Deprecation::notice('3.0', 'Please use setFrom() or addFrom() instead!'); + return $this->setFrom($from); + } /** * Add a LEFT JOIN criteria to the FROM clause. - * - * @param String $table Table name (unquoted) - * @param String $onPredicate The "ON" SQL fragment in a "LEFT JOIN ... AS ... ON ..." statement. + * + * @param string $table Table name (unquoted) + * @param string $onPredicate The "ON" SQL fragment in a "LEFT JOIN ... AS ... ON ..." statement. * Needs to be valid (quoted) SQL. - * @param String $tableAlias Optional alias which makes it easier to identify and replace joins later on - * @return SQLQuery This instance + * @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on + * @return SQLQuery */ - public function leftJoin($table, $onPredicate, $tableAlias=null) { - if( !$tableAlias ) { - $tableAlias = $table; - } + public function addLeftJoin($table, $onPredicate, $tableAlias = null) { + if(!$tableAlias) $tableAlias = $table; $this->from[$tableAlias] = array('type' => 'LEFT', 'table' => $table, 'filter' => array($onPredicate)); return $this; } - - /** - * Add an INNER JOIN criteria to the FROM clause. - * - * @param String $table Table name (unquoted) - * @param String $onPredicate The "ON" SQL fragment in a "LEFT JOIN ... AS ... ON ..." statement. - * Needs to be valid (quoted) SQL. - * @param String $tableAlias Optional alias which makes it easier to identify and replace joins later on - * @return SQLQuery This instance - */ - public function innerJoin($table, $onPredicate, $tableAlias=null) { - if( !$tableAlias ) { - $tableAlias = $table; - } - $this->from[$tableAlias] = array('type' => 'INNER', 'table' => $table, 'filter' => array($onPredicate)); - return $this; - } - - /** - * Add an additional filter (part of the ON clause) on a join - */ - public function addFilterToJoin($tableAlias, $filter) { - $this->from[$tableAlias]['filter'][] = $filter; + + public function leftjoin($table, $onPredicate, $tableAlias = null) { + Deprecation::notice('3.0', 'Please use addLeftJoin() instead!'); + $this->addLeftJoin($table, $onPredicate, $tableAlias); } /** - * Replace the existing filter (ON clause) on a join + * Add an INNER JOIN criteria to the FROM clause. + * + * @param string $table Table name (unquoted) + * @param string $onPredicate The "ON" SQL fragment in an "INNER JOIN ... AS ... ON ..." statement. + * Needs to be valid (quoted) SQL. + * @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on + * @return SQLQuery */ - public function setJoinFilter($tableAlias, $filter) { - if(is_string($this->from[$tableAlias])) {Debug::message($tableAlias); Debug::dump($this->from);} - $this->from[$tableAlias]['filter'] = array($filter); + public function addInnerJoin($table, $onPredicate, $tableAlias = null) { + if(!$tableAlias) $tableAlias = $table; + $this->from[$tableAlias] = array('type' => 'INNER', 'table' => $table, 'filter' => array($onPredicate)); + return $this; + } + + public function innerjoin($table, $onPredicate, $tableAlias = null) { + Deprecation::notice('3.0', 'Please use addInnerJoin() instead!'); + return $this->addInnerJoin($table, $onPredicate, $tableAlias); + } + + /** + * Add an additional filter (part of the ON clause) on a join. + * + * @param string $table Table to join on from the original join + * @param string $filter The "ON" SQL fragment + * @return SQLQuery + */ + public function addFilterToJoin($table, $filter) { + $this->from[$table]['filter'][] = $filter; + return $this; + } + + /** + * Set the filter (part of the ON clause) on a join. + * + * @param string $table Table to join on from the original join + * @param string $filter The "ON" SQL fragment + * @return SQLQuery + */ + public function setJoinFilter($table, $filter) { + $this->from[$table]['filter'] = array($filter); + return $this; } /** @@ -285,6 +366,62 @@ class SQLQuery { return $tables; } + /** + * Set distinct property. + * @param boolean $value + */ + public function setDistinct($value) { + $this->distinct = $value; + } + + /** + * Get the distinct property. + * @return boolean + */ + public function getDistinct() { + return $this->distinct; + } + + /** + * Set the delete property. + * @param boolean $value + */ + public function setDelete($value) { + $this->delete = $value; + } + + /** + * Get the delete property. + * @return boolean + */ + public function getDelete() { + return $this->delete; + } + + /** + * Set the connective property. + * @param boolean $value + */ + public function setConnective($value) { + $this->connective = $value; + } + + /** + * Get the connective property. + * @return string + */ + public function getConnective() { + return $this->connective; + } + + /** + * Get limit clause + * @return string + */ + public function getLimit() { + return $this->limit; + } + /** * Pass LIMIT clause either as SQL snippet or in array format. * Internally, limit will always be stored as a map containing the keys 'start' and 'limit' @@ -292,7 +429,7 @@ class SQLQuery { * @param string|array $limit * @return SQLQuery This instance */ - public function limit($limit, $offset = 0) { + public function setLimit($limit, $offset = 0) { if($limit && is_numeric($limit)) { $this->limit = array( 'start' => $offset, @@ -312,9 +449,14 @@ class SQLQuery { return $this; } - + + public function limit($limit, $offset = 0) { + Deprecation::notice('3.0', 'Please use setLimit() instead!'); + return $this->setLimit($limit, $offset); + } + /** - * Pass ORDER BY clause either as SQL snippet or in array format. + * Set ORDER BY clause either as SQL snippet or in array format. * * @example $sql->orderby("Column"); * @example $sql->orderby("Column DESC"); @@ -322,24 +464,39 @@ class SQLQuery { * @example $sql->orderby("Column", "DESC"); * @example $sql->orderby(array("Column" => "ASC", "ColumnTwo" => "DESC")); * - * @param string|array $orderby - * @param string $dir - * @param bool $clear remove existing order by clauses + * @param string|array $orderby Clauses to add + * @param string $dir Sort direction, ASC or DESC * * @return SQLQuery */ - public function orderby($clauses = null, $direction = null, $clear = true) { - if($clear) $this->orderby = array(); + public function setOrderBy($clauses = null, $direction = null) { + $this->orderby = array(); + return $this->addOrderBy($clauses, $direction); + } + /** + * Add ORDER BY clause either as SQL snippet or in array format. + * + * @example $sql->orderby("Column"); + * @example $sql->orderby("Column DESC"); + * @example $sql->orderby("Column DESC, ColumnTwo ASC"); + * @example $sql->orderby("Column", "DESC"); + * @example $sql->orderby(array("Column" => "ASC", "ColumnTwo" => "DESC")); + * + * @param string|array $orderby Clauses to add + * @param string $dir Sort direction, ASC or DESC + * + * @return SQLQuery + */ + public function addOrderBy($clauses = null, $direction = null) { if(!$clauses) { return $this; } if(is_string($clauses)) { - if(strpos($clauses, "(") !== false) { + if(strpos($clauses, "(") !== false) { $sort = preg_split("/,(?![^()]*+\\))/", $clauses); - } - else { + } else { $sort = explode(",", $clauses); } @@ -356,15 +513,13 @@ class SQLQuery { if(!is_numeric($key)) { $column = trim($key); $columnDir = strtoupper(trim($value)); - } - else { + } else { list($column, $columnDir) = $this->getDirectionFromString($value); } - + $this->orderby[$column] = $columnDir; } - } - else { + } else { user_error('SQLQuery::orderby() incorrect format for $orderby', E_USER_WARNING); } @@ -385,15 +540,20 @@ class SQLQuery { $column = "_SortColumn{$i}"; $this->selectField($clause, $column); - $this->orderby('"' . $column . '"', $dir, false); + $this->addOrderBy('"' . $column . '"', $dir); $i++; } } } - + return $this; } - + + public function orderby($clauses = null, $direction = null) { + Deprecation::notice('3.0', 'Please use setOrderBy() instead!'); + return $this->setOrderBy($clauses, $direction); + } + /** * Extract the direction part of a single-column order by clause. * @@ -412,29 +572,27 @@ class SQLQuery { /** * Returns the current order by as array if not already. To handle legacy - * statements which are stored as strings. Without clauses and directions, + * statements which are stored as strings. Without clauses and directions, * convert the orderby clause to something readable. * - * @todo When $orderby is a private variable and all orderby statements - * set through - * * @return array */ public function getOrderBy() { $orderby = $this->orderby; - + if(!$orderby) $orderby = array(); + if(!is_array($orderby)) { // spilt by any commas not within brackets - $orderby = preg_split("/,(?![^()]*+\\))/", $orderby); + $orderby = preg_split('/,(?![^()]*+\\))/', $orderby); } - + foreach($orderby as $k => $v) { - if(strpos($v, " ") !== false) { + if(strpos($v, ' ') !== false) { unset($orderby[$k]); - $rule = explode(" ", trim($v)); + $rule = explode(' ', trim($v)); $clause = $rule[0]; - $dir = (isset($rule[1])) ? $rule[1] : "ASC"; + $dir = (isset($rule[1])) ? $rule[1] : 'ASC'; $orderby[$clause] = $dir; } @@ -445,91 +603,142 @@ class SQLQuery { /** * Reverses the order by clause by replacing ASC or DESC references in the - * current order by with it's corollary. + * current order by with it's corollary. * * @return SQLQuery */ public function reverseOrderBy() { $order = $this->getOrderBy(); - $this->orderby = array(); - - foreach($order as $clause => $dir) { - $dir = (strtoupper($dir) == "DESC") ? "ASC" : "DESC"; - $this->orderby($clause, $dir, false); + foreach($order as $clause => $dir) { + $dir = (strtoupper($dir) == 'DESC') ? 'ASC' : 'DESC'; + $this->addOrderBy($clause, $dir); } - + return $this; } + /** + * Set a GROUP BY clause. + * + * @param string|array $groupby + * @return SQLQuery + */ + public function setGroupBy($groupby) { + $this->groupby = array(); + return $this->addGroupBy($groupby); + } + /** * Add a GROUP BY clause. * * @param string|array $groupby * @return SQLQuery */ - public function groupby($groupby) { + public function addGroupBy($groupby) { if(is_array($groupby)) { - $this->groupby = array_merge($this->groupby, $groupby); + $this->groupby = array_merge($this->groupby, $groupby); } elseif(!empty($groupby)) { $this->groupby[] = $groupby; } + + return $this; + } + + public function groupby($where) { + Deprecation::notice('3.0', 'Please use setGroupBy() or addHaving() instead!'); + return $this->setGroupBy($where); + } + + /** + * Set a HAVING clause. + * + * @param string|array $having + * @return SQLQuery + */ + public function setHaving($having) { + $this->having = array(); + return $this->addHaving($having); + } + + /** + * Add a HAVING clause + * + * @param string|array $having + * @return SQLQuery + */ + public function addHaving($having) { + if(is_array($having)) { + $this->having = array_merge($this->having, $having); + } elseif(!empty($having)) { + $this->having[] = $having; + } + + return $this; + } + + public function having($having) { + Deprecation::notice('3.0', 'Please use setHaving() or addHaving() instead!'); + return $this->setHaving($having); + } + + /** + * Set a WHERE clause. + * + * There are two different ways of doing this: + * + * + * // the entire predicate as a single string + * $query->where("Column = 'Value'"); + * + * // multiple predicates as an array + * $query->where(array("Column = 'Value'", "Column != 'Value'")); + * + * + * @param string|array $where Predicate(s) to set + * @return SQLQuery + */ + public function setWhere($where) { + $this->where = array(); + + $args = func_get_args(); + if(isset($args[1])) { + Deprecation::notice('3.0', 'Multiple arguments to where is deprecated. Pleas use where("Column = Something") syntax instead'); + } + + return $this->addWhere($where); + } + + /** + * Add a WHERE predicate. + * + * There are two different ways of doing this: + * + * + * // the entire predicate as a single string + * $query->where("Column = 'Value'"); + * + * // multiple predicates as an array + * $query->where(array("Column = 'Value'", "Column != 'Value'")); + * + * + * @param string|array $where Predicate(s) to set + * @return SQLQuery + */ + public function addWhere($where) { + if(is_array($where)) { + $this->where = array_merge($this->where, $where); + } elseif(!empty($where)) { + $this->where[] = $where; + } return $this; } - /** - * Add a HAVING clause. - * - * @param string|array $having - * @return SQLQuery - */ - public function having($having) { - if(is_array($having)) { - $this->having = array_merge($this->having, $having); - } elseif(!empty($having)) { - $this->having[] = $having; - } - - return $this; - } - - /** - * Apply a predicate filter to the where clause. - * - * Accepts a variable length of arguments, which represent - * different ways of formatting a predicate in a where clause: - * - * - * // the entire predicate as a single string - * $query->where("Column = 'Value'"); - * - * // an exact match predicate with a key value pair - * $query->where("Column", "Value"); - * - * // a predicate with user defined operator - * $query->where("Column", "!=", "Value"); - * - * - */ - public function where() { - $args = func_get_args(); - if (func_num_args() == 3) { - $filter = "{$args[0]} {$args[1]} '{$args[2]}'"; - } elseif (func_num_args() == 2) { - $filter = "{$args[0]} = '{$args[1]}'"; - } else { - $filter = $args[0]; - } - - if(is_array($filter)) { - $this->where = array_merge($this->where,$filter); - } elseif(!empty($filter)) { - $this->where[] = $filter; - } - - return $this; + public function where($where) { + Deprecation::notice('3.0', 'Please use setWhere() or addWhere() instead!'); + return $this->setWhere($where); } /** @@ -538,7 +747,7 @@ class SQLQuery { function whereAny($filters) { if(is_string($filters)) $filters = func_get_args(); $clause = implode(" OR ", $filters); - return $this->where($clause); + return $this->setWhere($clause); } /** @@ -579,71 +788,48 @@ class SQLQuery { Deprecation::notice('3.0', 'Please use prepareWhere() instead of getFilter()'); return $this->prepareWhere(); } - + + /** + * Return a list of FROM clauses used internally. + * @return array + */ + public function getFrom() { + return $this->from; + } + + /** + * Return a list of HAVING clauses used internally. + * @return array + */ + public function getHaving() { + return $this->having; + } + + /** + * Return a list of GROUP BY clauses used internally. + * @return array + */ + public function getGroupBy() { + return $this->groupby; + } + + /** + * Return a list of WHERE clauses used internally. + * @return array + */ + public function getWhere() { + return $this->where; + } + /** * Return an itemised select list as a map, where keys are the aliases, and values are the column sources. * Aliases will always be provided (if the alias is implicit, the alias value will be inferred), and won't be quoted. * E.g., 'Title' => '"SiteTree"."Title"'. */ - public function itemisedSelect() { + public function getSelect() { return $this->select; } - /** - * Returns the WHERE clauses ready for inserting into a query. - * @return string - */ - public function prepareSelect() { - $clauses = array(); - foreach($this->select as $alias => $field) { - // Don't include redundant aliases. - if($alias === $field || preg_match('/"' . preg_quote($alias) . '"$/', $field)) $clauses[] = $field; - else $clauses[] = "$field AS \"$alias\""; - } - return implode(", ", $clauses); - } - - /** - * Returns the WHERE clauses ready for inserting into a query. - * @return string - */ - public function prepareWhere() { - return ($this->where) ? implode(") {$this->connective} (" , $this->where) : ''; - } - - /** - * Returns the ORDER BY clauses ready for inserting into a query. - * @return string - */ - public function prepareOrderBy() { - $statments = array(); - - if($order = $this->getOrderBy()) { - foreach($order as $clause => $dir) { - $statements[] = trim($clause . ' '. $dir); - } - } - - return implode(", ", $statements); - } - - /** - * Returns the GROUP BY clauses ready for inserting into a query. - * @return string - */ - public function prepareGroupBy() { - return implode(", ", $this->groupby); - } - - /** - * Returns the HAVING clauses ready for inserting into a query. - * @return string - */ - public function prepareHaving() { - return implode(" ) AND ( ", $sqlQuery->having); - } - - /** * Generate the SQL statement for this query. * @@ -842,7 +1028,7 @@ class SQLQuery { function firstRow() { $query = clone $this; $offset = $this->limit ? $this->limit['start'] : 0; - $query->limit(1, $offset); + $query->setLimit(1, $offset); return $query; } @@ -852,7 +1038,7 @@ class SQLQuery { function lastRow() { $query = clone $this; $offset = $this->limit ? $this->limit['start'] : 0; - $query->limit(1, $this->count() + $offset - 1); + $query->setLimit(1, $this->count() + $offset - 1); return $query; } diff --git a/model/Versioned.php b/model/Versioned.php index 21c21aa00..9af453a86 100644 --- a/model/Versioned.php +++ b/model/Versioned.php @@ -143,7 +143,7 @@ class Versioned extends DataExtension { // Reading a specific data from the archive case 'archive': $date = $dataQuery->getQueryParam('Versioned.date'); - foreach($query->from as $table => $dummy) { + foreach($query->getFrom() as $table => $dummy) { $query->renameTable($table, $table . '_versions'); $query->replaceText("\"$table\".\"ID\"", "\"$table\".\"RecordID\""); @@ -154,7 +154,7 @@ class Versioned extends DataExtension { $query->selectField(sprintf('"%s_versions"."%s"', $baseTable, 'RecordID'), "ID"); if($table != $baseTable) { - $query->from[$table] .= " AND \"{$table}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\""; + $query->from(array($table => " AND \"{$table}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\"")); } } @@ -169,7 +169,7 @@ class Versioned extends DataExtension { case 'stage': $stage = $dataQuery->getQueryParam('Versioned.stage'); if($stage && ($stage != $this->defaultStage)) { - foreach($query->from as $table => $dummy) { + foreach($query->getFrom() as $table => $dummy) { // Only rewrite table names that are actually part of the subclass tree // This helps prevent rewriting of other tables that get joined in, in // particular, many_many tables @@ -186,7 +186,7 @@ class Versioned extends DataExtension { // Return all version instances case 'all_versions': case 'latest_versions': - foreach($query->from as $alias => $join) { + foreach($query->getFrom() as $alias => $join) { if($alias != $baseTable) { $query->setJoinFilter($alias, "\"$alias\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$alias\".\"Version\" = \"{$baseTable}_versions\".\"Version\""); } @@ -204,7 +204,7 @@ class Versioned extends DataExtension { // This provides "show all, including deleted" functonality if($dataQuery->getQueryParam('Versioned.mode') == 'latest_versions') { $archiveTable = self::requireArchiveTempTable($baseTable); - $query->innerJoin($archiveTable, "\"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\""); + $query->addInnerJoin($archiveTable, "\"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\""); } break; @@ -711,11 +711,11 @@ class Versioned extends DataExtension { $query = $list->dataQuery()->query(); - foreach($query->from as $table => $tableJoin) { + foreach($query->getFrom() as $table => $tableJoin) { if(is_string($tableJoin) && $tableJoin[0] == '"') { $baseTable = str_replace('"','',$tableJoin); } elseif(is_string($tableJoin) && substr($tableJoin,0,5) != 'INNER') { - $query->from[$table] = "LEFT JOIN \"$table\" ON \"$table\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\""; + $query->setFrom(array($table => "LEFT JOIN \"$table\" ON \"$table\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\"")); } $query->renameTable($table, $table . '_versions'); } @@ -725,12 +725,12 @@ class Versioned extends DataExtension { $query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name); } - $query->where[] = "\"{$baseTable}_versions\".\"RecordID\" = '{$this->owner->ID}'"; - $query->orderby = ($sort) ? $sort : "\"{$baseTable}_versions\".\"LastEdited\" DESC, \"{$baseTable}_versions\".\"Version\" DESC"; - + $query->addWhere("\"{$baseTable}_versions\".\"RecordID\" = '{$this->owner->ID}'"); + $query->setOrderBy(($sort) ? $sort : "\"{$baseTable}_versions\".\"LastEdited\" DESC, \"{$baseTable}_versions\".\"Version\" DESC"); + $records = $query->execute(); $versions = new ArrayList(); - + foreach($records as $record) { $versions->push(new Versioned_Version($record)); } diff --git a/tests/model/DbDatetimeTest.php b/tests/model/DbDatetimeTest.php index 2bd10df22..295b8fae3 100644 --- a/tests/model/DbDatetimeTest.php +++ b/tests/model/DbDatetimeTest.php @@ -91,9 +91,9 @@ class DbDatetimeTest extends FunctionalTest { $this->matchesRoughly($result, date('Y-m-d H:i:s', strtotime('+1 Day', $this->getDbNow())), 'tomorrow'); $query = new SQLQuery(); - $query->select(array("test" => $this->adapter->datetimeIntervalClause('"Created"', '-15 Minutes'))) - ->from('"DbDateTimeTest_Team"') - ->limit(1); + $query->setSelect(array("test" => $this->adapter->datetimeIntervalClause('"Created"', '-15 Minutes'))) + ->setFrom('"DbDateTimeTest_Team"') + ->setLimit(1); $result = $query->execute()->value(); $this->matchesRoughly($result, date('Y-m-d H:i:s', strtotime(Dataobject::get_one('DbDateTimeTest_Team')->Created) - 900), '15 Minutes before creating fixture'); @@ -116,9 +116,9 @@ class DbDatetimeTest extends FunctionalTest { $this->matchesRoughly($result, -45 * 60, 'now - 45 minutes ahead'); $query = new SQLQuery(); - $query->select(array("test" => $this->adapter->datetimeDifferenceClause('"LastEdited"', '"Created"'))) - ->from('"DbDateTimeTest_Team"') - ->limit(1); + $query->setSelect(array("test" => $this->adapter->datetimeDifferenceClause('"LastEdited"', '"Created"'))) + ->setFrom('"DbDateTimeTest_Team"') + ->setLimit(1); $result = $query->execute()->value(); $lastedited = Dataobject::get_one('DbDateTimeTest_Team')->LastEdited; diff --git a/tests/model/PaginatedListTest.php b/tests/model/PaginatedListTest.php index 4efab1c06..64a1f7c00 100644 --- a/tests/model/PaginatedListTest.php +++ b/tests/model/PaginatedListTest.php @@ -34,10 +34,12 @@ class PaginatedListTest extends SapphireTest { public function testSetPaginationFromQuery() { $query = $this->getMock('SQLQuery'); - $query->limit = array('limit' => 15, 'start' => 30); + $query->expects($this->once()) + ->method('getLimit') + ->will($this->returnValue(array('limit' => 15, 'start' => 30))); $query->expects($this->once()) ->method('unlimitedRowCount') - ->will($this->returnValue(100)); + ->will($this->returnValue(100)); $list = new PaginatedList(new ArrayList()); $list->setPaginationFromQuery($query); diff --git a/tests/model/SQLQueryTest.php b/tests/model/SQLQueryTest.php index 08a67e2f0..e23b5022b 100755 --- a/tests/model/SQLQueryTest.php +++ b/tests/model/SQLQueryTest.php @@ -15,25 +15,25 @@ class SQLQueryTest extends SapphireTest { function testSelectFromBasicTable() { $query = new SQLQuery(); - $query->from[] = "MyTable"; + $query->setFrom('MyTable'); $this->assertEquals("SELECT * FROM MyTable", $query->sql()); - $query->from[] = "MyJoin"; + $query->addFrom('MyJoin'); $this->assertEquals("SELECT * FROM MyTable MyJoin", $query->sql()); } function testSelectFromUserSpecifiedFields() { $query = new SQLQuery(); - $query->select(array("Name", "Title", "Description")); - $query->from[] = "MyTable"; + $query->setSelect(array("Name", "Title", "Description")); + $query->setFrom("MyTable"); $this->assertEquals("SELECT Name, Title, Description FROM MyTable", $query->sql()); } function testSelectWithWhereClauseFilter() { $query = new SQLQuery(); - $query->select(array("Name","Meta")); - $query->from[] = "MyTable"; - $query->where[] = "Name = 'Name'"; - $query->where[] = "Meta = 'Test'"; + $query->setSelect(array("Name","Meta")); + $query->setFrom("MyTable"); + $query->setWhere("Name = 'Name'"); + $query->addWhere("Meta = 'Test'"); $this->assertEquals("SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test')", $query->sql()); } @@ -46,132 +46,115 @@ class SQLQueryTest extends SapphireTest { function testSelectWithChainedMethods() { $query = new SQLQuery(); - $query->select("Name","Meta")->from("MyTable")->where("Name", "Name")->where("Meta", "Test"); + $query->setSelect("Name","Meta")->setFrom("MyTable")->setWhere("Name = 'Name'")->addWhere("Meta = 'Test'"); $this->assertEquals("SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test')", $query->sql()); } function testCanSortBy() { $query = new SQLQuery(); - $query->select("Name","Meta")->from("MyTable")->where("Name", "Name")->where("Meta", "Test"); + $query->setSelect("Name","Meta")->setFrom("MyTable")->setWhere("Name = 'Name'")->addWhere("Meta = 'Test'"); $this->assertTrue($query->canSortBy('Name ASC')); $this->assertTrue($query->canSortBy('Name')); } function testSelectWithChainedFilterParameters() { $query = new SQLQuery(); - $query->select(array("Name","Meta"))->from("MyTable"); - $query->where("Name = 'Name'")->where("Meta","Test")->where("Beta", "!=", "Gamma"); - $this->assertEquals("SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test') AND (Beta != 'Gamma')", $query->sql()); - } - - function testSelectWithPredicateFilters() { - /* this is no longer part of this - $query = new SQLQuery(); - $query->select(array("Name"))->from("SQLQueryTest_DO"); - - $match = new ExactMatchFilter("Name", "Value"); - $match->setModel('SQLQueryTest_DO'); - $match->apply($query); - - $match = new PartialMatchFilter("Meta", "Value"); - $match->setModel('SQLQueryTest_DO'); - $match->apply($query); - - $this->assertEquals("SELECT Name FROM SQLQueryTest_DO WHERE (\"SQLQueryTest_DO\".\"Name\" = 'Value') AND (\"SQLQueryTest_DO\".\"Meta\" LIKE '%Value%')", $query->sql()); - */ + $query->setSelect(array("Name","Meta"))->setFrom("MyTable"); + $query->setWhere("Name = 'Name'")->addWhere("Meta = 'Test'")->addWhere("Beta != 'Gamma'"); + $this->assertEquals("SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test') AND (Beta != 'Gamma')", $query->sql()); } function testSelectWithLimitClause() { - // These are MySQL specific :-S - if(DB::getConn() instanceof MySQLDatabase) { - // numeric limit - $query = new SQLQuery(); - $query->from[] = "MyTable"; - $query->limit(99); - $this->assertEquals("SELECT * FROM MyTable LIMIT 99", $query->sql()); - - // array limit with start (MySQL specific) - $query = new SQLQuery(); - $query->from[] = "MyTable"; - $query->limit(99, 97); - $this->assertEquals("SELECT * FROM MyTable LIMIT 99 OFFSET 97", $query->sql()); + if(!(DB::getConn() instanceof MySQLDatabase || DB::getConn() instanceof SQLite3Database || DB::getConn() instanceof PostgreSQLDatabase)) { + $this->markTestIncomplete(); } + + $query = new SQLQuery(); + $query->setFrom("MyTable"); + $query->setLimit(99); + $this->assertEquals("SELECT * FROM MyTable LIMIT 99", $query->sql()); + + // array limit with start (MySQL specific) + $query = new SQLQuery(); + $query->setFrom("MyTable"); + $query->setLimit(99, 97); + $this->assertEquals("SELECT * FROM MyTable LIMIT 99 OFFSET 97", $query->sql()); } function testSelectWithOrderbyClause() { $query = new SQLQuery(); - $query->from[] = "MyTable"; - $query->orderby('MyName'); + $query->setFrom("MyTable"); + $query->setOrderBy('MyName'); $this->assertEquals('SELECT * FROM MyTable ORDER BY MyName ASC', $query->sql()); $query = new SQLQuery(); - $query->from[] = "MyTable"; - $query->orderby('MyName desc'); + $query->setFrom("MyTable"); + $query->setOrderBy('MyName desc'); $this->assertEquals('SELECT * FROM MyTable ORDER BY MyName DESC', $query->sql()); $query = new SQLQuery(); - $query->from[] = "MyTable"; - $query->orderby('MyName ASC, Color DESC'); + $query->setFrom("MyTable"); + $query->setOrderBy('MyName ASC, Color DESC'); $this->assertEquals('SELECT * FROM MyTable ORDER BY MyName ASC, Color DESC', $query->sql()); $query = new SQLQuery(); - $query->from[] = "MyTable"; - $query->orderby('MyName ASC, Color'); + $query->setFrom("MyTable"); + $query->setOrderBy('MyName ASC, Color'); $this->assertEquals('SELECT * FROM MyTable ORDER BY MyName ASC, Color ASC', $query->sql()); $query = new SQLQuery(); - $query->from[] = "MyTable"; - $query->orderby(array('MyName' => 'desc')); + $query->setFrom("MyTable"); + $query->setOrderBy(array('MyName' => 'desc')); $this->assertEquals('SELECT * FROM MyTable ORDER BY MyName DESC', $query->sql()); $query = new SQLQuery(); - $query->from[] = "MyTable"; - $query->orderby(array('MyName' => 'desc', 'Color')); + $query->setFrom("MyTable"); + $query->setOrderBy(array('MyName' => 'desc', 'Color')); $this->assertEquals('SELECT * FROM MyTable ORDER BY MyName DESC, Color ASC', $query->sql()); $query = new SQLQuery(); - $query->from[] = "MyTable"; - $query->orderby('implode("MyName","Color")'); + $query->setFrom("MyTable"); + $query->setOrderBy('implode("MyName","Color")'); $this->assertEquals('SELECT *, implode("MyName","Color") AS "_SortColumn0" FROM MyTable ORDER BY "_SortColumn0" ASC', $query->sql()); $query = new SQLQuery(); - $query->from[] = "MyTable"; - $query->orderby('implode("MyName","Color") DESC'); + $query->setFrom("MyTable"); + $query->setOrderBy('implode("MyName","Color") DESC'); $this->assertEquals('SELECT *, implode("MyName","Color") AS "_SortColumn0" FROM MyTable ORDER BY "_SortColumn0" DESC', $query->sql()); $query = new SQLQuery(); - $query->from[] = "MyTable"; - $query->orderby('RAND()'); + $query->setFrom("MyTable"); + $query->setOrderBy('RAND()'); $this->assertEquals('SELECT *, RAND() AS "_SortColumn0" FROM MyTable ORDER BY "_SortColumn0" ASC', $query->sql()); } public function testReverseOrderBy() { $query = new SQLQuery(); - $query->from('MyTable'); + $query->setFrom('MyTable'); // default is ASC - $query->orderby("Name"); + $query->setOrderBy("Name"); $query->reverseOrderBy(); $this->assertEquals('SELECT * FROM MyTable ORDER BY Name DESC',$query->sql()); - $query->orderby("Name DESC"); + $query->setOrderBy("Name DESC"); $query->reverseOrderBy(); $this->assertEquals('SELECT * FROM MyTable ORDER BY Name ASC',$query->sql()); - $query->orderby(array("Name" => "ASC")); + $query->setOrderBy(array("Name" => "ASC")); $query->reverseOrderBy(); $this->assertEquals('SELECT * FROM MyTable ORDER BY Name DESC',$query->sql()); - $query->orderby(array("Name" => 'DESC', 'Color' => 'asc')); + $query->setOrderBy(array("Name" => 'DESC', 'Color' => 'asc')); $query->reverseOrderBy(); $this->assertEquals('SELECT * FROM MyTable ORDER BY Name ASC, Color DESC',$query->sql()); - $query->orderby('implode("MyName","Color") DESC'); + $query->setOrderBy('implode("MyName","Color") DESC'); $query->reverseOrderBy(); $this->assertEquals('SELECT *, implode("MyName","Color") AS "_SortColumn0" FROM MyTable ORDER BY "_SortColumn0" ASC',$query->sql()); @@ -179,50 +162,42 @@ class SQLQueryTest extends SapphireTest { function testFiltersOnID() { $query = new SQLQuery(); - $query->where[] = "ID = 5"; + $query->setWhere("ID = 5"); $this->assertTrue( $query->filtersOnID(), "filtersOnID() is true with simple unquoted column name" ); $query = new SQLQuery(); - $query->where[] = "ID=5"; + $query->setWhere("ID=5"); $this->assertTrue( $query->filtersOnID(), "filtersOnID() is true with simple unquoted column name and no spaces in equals sign" ); - /* + $query = new SQLQuery(); - $query->where[] = "Foo='Bar' AND ID=5"; - $this->assertTrue( - $query->filtersOnID(), - "filtersOnID() is true with combined SQL statements" - ); - */ - - $query = new SQLQuery(); - $query->where[] = "Identifier = 5"; + $query->setWhere("Identifier = 5"); $this->assertFalse( $query->filtersOnID(), "filtersOnID() is false with custom column name (starting with 'id')" ); $query = new SQLQuery(); - $query->where[] = "ParentID = 5"; + $query->setWhere("ParentID = 5"); $this->assertFalse( $query->filtersOnID(), "filtersOnID() is false with column name ending in 'ID'" ); $query = new SQLQuery(); - $query->where[] = "MyTable.ID = 5"; + $query->setWhere("MyTable.ID = 5"); $this->assertTrue( $query->filtersOnID(), "filtersOnID() is true with table and column name" ); $query = new SQLQuery(); - $query->where[] = "MyTable.ID= 5"; + $query->setWhere("MyTable.ID = 5"); $this->assertTrue( $query->filtersOnID(), "filtersOnID() is true with table and quoted column name " @@ -231,28 +206,28 @@ class SQLQueryTest extends SapphireTest { function testFiltersOnFK() { $query = new SQLQuery(); - $query->where[] = "ID = 5"; + $query->setWhere("ID = 5"); $this->assertFalse( $query->filtersOnFK(), "filtersOnFK() is true with simple unquoted column name" ); $query = new SQLQuery(); - $query->where[] = "Identifier = 5"; + $query->setWhere("Identifier = 5"); $this->assertFalse( $query->filtersOnFK(), "filtersOnFK() is false with custom column name (starting with 'id')" ); $query = new SQLQuery(); - $query->where[] = "MyTable.ParentID = 5"; + $query->setWhere("MyTable.ParentID = 5"); $this->assertTrue( $query->filtersOnFK(), "filtersOnFK() is true with table and column name" ); $query = new SQLQuery(); - $query->where[] = "MyTable.`ParentID`= 5"; + $query->setWhere("MyTable.`ParentID`= 5"); $this->assertTrue( $query->filtersOnFK(), "filtersOnFK() is true with table and quoted column name " @@ -261,36 +236,36 @@ class SQLQueryTest extends SapphireTest { public function testInnerJoin() { $query = new SQLQuery(); - $query->from( 'MyTable' ); - $query->innerJoin( 'MyOtherTable', 'MyOtherTable.ID = 2' ); - $query->leftJoin( 'MyLastTable', 'MyOtherTable.ID = MyLastTable.ID' ); + $query->setFrom('MyTable'); + $query->addInnerJoin('MyOtherTable', 'MyOtherTable.ID = 2'); + $query->addLeftJoin('MyLastTable', 'MyOtherTable.ID = MyLastTable.ID'); - $this->assertEquals( 'SELECT * FROM MyTable '. + $this->assertEquals('SELECT * FROM MyTable '. 'INNER JOIN "MyOtherTable" ON MyOtherTable.ID = 2 '. 'LEFT JOIN "MyLastTable" ON MyOtherTable.ID = MyLastTable.ID', $query->sql() ); $query = new SQLQuery(); - $query->from( 'MyTable' ); - $query->innerJoin( 'MyOtherTable', 'MyOtherTable.ID = 2', 'table1' ); - $query->leftJoin( 'MyLastTable', 'MyOtherTable.ID = MyLastTable.ID', 'table2' ); + $query->setFrom('MyTable'); + $query->addInnerJoin('MyOtherTable', 'MyOtherTable.ID = 2', 'table1'); + $query->addLeftJoin('MyLastTable', 'MyOtherTable.ID = MyLastTable.ID', 'table2'); - $this->assertEquals( 'SELECT * FROM MyTable '. + $this->assertEquals('SELECT * FROM MyTable '. 'INNER JOIN "MyOtherTable" AS "table1" ON MyOtherTable.ID = 2 '. 'LEFT JOIN "MyLastTable" AS "table2" ON MyOtherTable.ID = MyLastTable.ID', $query->sql() ); } - public function testWhereAny() { $query = new SQLQuery(); - $query->from( 'MyTable' ); + $query->setFrom('MyTable'); $query->whereAny(array("Monkey = 'Chimp'", "Color = 'Brown'")); $this->assertEquals("SELECT * FROM MyTable WHERE (Monkey = 'Chimp' OR Color = 'Brown')",$query->sql()); } + } class SQLQueryTest_DO extends DataObject implements TestOnly {