diff --git a/forms/ManyManyComplexTableField.php b/forms/ManyManyComplexTableField.php index 1c9600c08..d14e9b731 100644 --- a/forms/ManyManyComplexTableField.php +++ b/forms/ManyManyComplexTableField.php @@ -77,7 +77,7 @@ class ManyManyComplexTableField extends HasManyComplexTableField { function getQuery() { $query = parent::getQuery(); - $query->select[] = "CASE WHEN \"{$this->manyManyParentClass}ID\" IS NULL THEN '0' ELSE '1' END AS \"Checked\""; + $query->selectField("CASE WHEN \"{$this->manyManyParentClass}ID\" IS NULL THEN '0' ELSE '1' END", "Checked"); $query->groupby[] = "\"{$this->manyManyParentClass}ID\""; // necessary for Postgres return $query; diff --git a/model/Aggregate.php b/model/Aggregate.php index a3fb965f7..345c33f7c 100644 --- a/model/Aggregate.php +++ b/model/Aggregate.php @@ -78,7 +78,7 @@ class Aggregate extends ViewableData { protected function query($attr) { $singleton = singleton($this->type); $query = $singleton->buildSQL($this->filter); - $query->select = array($attr); + $query->select($attr); $query->orderby = null; $singleton->extend('augmentSQL', $query); return $query; @@ -161,7 +161,7 @@ class Aggregate_Relationship extends Aggregate { $query = $this->object->getManyManyComponentsQuery($this->relationship, $this->filter); } - $query->select = array($attr); + $query->select($attr); $query->groupby = array(); $singleton = singleton($this->type); diff --git a/model/DataQuery.php b/model/DataQuery.php index f56dc523a..30a2ec63f 100644 --- a/model/DataQuery.php +++ b/model/DataQuery.php @@ -205,8 +205,7 @@ class DataQuery { user_error("Bad collision item '$collision'", E_USER_WARNING); } } - $query->select[$k] = "CASE " . implode( " ", $caseClauses) . " ELSE NULL END" - . " AS \"$k\""; + $query->selectField("CASE " . implode( " ", $caseClauses) . " ELSE NULL END", $k); } } @@ -221,8 +220,8 @@ class DataQuery { } } - $query->select[] = "\"$baseClass\".\"ID\""; - $query->select[] = "CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\" ELSE '$baseClass' END AS \"RecordClassName\""; + $query->selectField("\"$baseClass\".\"ID\"", "ID"); + $query->selectField("CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\" ELSE '$baseClass' END", "RecordClassName"); // TODO: Versioned, Translatable, SiteTreeSubsites, etc, could probably be better implemented as subclasses of DataQuery singleton($this->dataClass)->extend('augmentSQL', $query, $this); @@ -274,14 +273,18 @@ class DataQuery { $qualCol = "\"$parts[0]\""; } + // To-do: Remove this if block once SQLQuery::$select has been refactored to store itemisedSelect() + // format internally; then this check can be part of selectField() if(!isset($query->select[$col]) && !in_array($qualCol, $query->select)) { - $query->select[] = $qualCol; + $query->selectField($qualCol); } } else { $qualCol = '"' . implode('"."', $parts) . '"'; + // To-do: Remove this if block once SQLQuery::$select has been refactored to store itemisedSelect() + // format internally; then this check can be part of selectField() if(!in_array($qualCol, $query->select)) { - $query->select[] = $qualCol; + $query->selectField($qualCol); } } } @@ -367,12 +370,12 @@ class DataQuery { if($databaseFields) foreach($databaseFields as $k => $v) { if((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) { // Update $collidingFields if necessary - if(isset($query->select[$k])) { - if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($query->select[$k]); + if($expressionForField = $query->expressionForField($k)) { + if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($expressionForField); $this->collidingFields[$k][] = "\"$tableClass\".\"$k\""; } else { - $query->select[$k] = "\"$tableClass\".\"$k\""; + $query->selectField("\"$tableClass\".\"$k\"", $k); } } } @@ -596,8 +599,12 @@ class DataQuery { */ public function subtract(DataQuery $subtractQuery, $field='ID') { $subSelect= $subtractQuery->getFinalisedQuery(); - $subSelect->select($this->expressionForField($field, $subSelect)); + $fieldExpression = $this->expressionForField($field, $subSelect); + $subSelect->clearSelect(); + $subSelect->selectField($fieldExpression); $this->where($this->expressionForField($field, $this).' NOT IN ('.$subSelect->sql().')'); + + return $this; } /** @@ -607,7 +614,9 @@ class DataQuery { $fieldExpressions = array_map(create_function('$item', "return '\"$table\".\"' . \$item . '\"';"), $fields); - $this->select($fieldExpressions); + $this->query->select($fieldExpressions); + + return $this; } /** @@ -636,7 +645,7 @@ class DataQuery { * Clear the selected fields to start over */ public function clearSelect() { - $this->query->select = array(); + $this->query->clearSelect(); return $this; } @@ -644,8 +653,8 @@ class DataQuery { /** * Select the given field expressions. You must do your own escaping */ - protected function select($fieldExpressions) { - $this->query->select = array_merge($this->query->select, $fieldExpressions); + protected function selectField($fieldExpression, $alias = null) { + $this->query->selectField($fieldExpression, $alias); } //// QUERY PARAMS diff --git a/model/ManyManyList.php b/model/ManyManyList.php index 676018c6f..e3ddd6bb8 100644 --- a/model/ManyManyList.php +++ b/model/ManyManyList.php @@ -133,7 +133,7 @@ class ManyManyList extends RelationList { function removeAll() { $query = $this->dataQuery()->query(); $query->delete = true; - $query->select = array('*'); + $query->select(array('*')); $query->from = array("\"$this->joinTable\""); $query->orderby = null; $query->execute(); diff --git a/model/SQLQuery.php b/model/SQLQuery.php index c723fa98a..47dc9a660 100644 --- a/model/SQLQuery.php +++ b/model/SQLQuery.php @@ -113,6 +113,14 @@ class SQLQuery { $this->limit($limit); } + /** + * Clear the selected fields to start over + */ + function clearSelect() { + $this->select = array(); + return $this; + } + /** * Specify the list of columns to be selected by the query. * @@ -153,9 +161,26 @@ class SQLQuery { $this->select[] = $fields; } } + + /** + * Select an additional field + * + * @param $field The field to select + * @param $alias The alias of that field + */ + public function selectField($field, $alias = null) { + // Let's not put unnecessary aliases into the query + if($alias && preg_match('/"' . preg_quote($alias) . '"$/', $field)) { + $alias = null; + } + + if($alias) $this->select[] = "$field AS \"$alias\""; + else $this->select[] = $field; + } /** - * Return the SQL expression for the given field + * Return the SQL expression for the given field alias. + * Returns null if the given alias doesn't exist. * * @todo This should be refactored after $this->select is changed to make that easier */ @@ -166,6 +191,7 @@ class SQLQuery { else if(preg_match('/"?([^"]*)"?/', $sel, $matches)) $selField = $matches[2]; if($selField == $field) return $sel; } + return null; } /** @@ -542,6 +568,29 @@ class SQLQuery { Deprecation::notice('3.0', 'Please use prepareWhere() instead of getFilter()'); return $this->prepareWhere(); } + + /** + * 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() { + $output = array(); + foreach($this->select as $item) { + $split = preg_split('/ AS /i', $item, 2); + if(isset($split[1])) { + $source = $split[0]; + $alias = $split[1]; + } else { + $source = $item; + $alias = substr($item,strrpos($item,'.')+1); + } + // Drop quoting + $alias = preg_replace('/^"(.*)"$/','\\1', $alias); + $output[$alias] = $source; + } + return $output; + } /** * Returns the WHERE clauses ready for inserting into a query. diff --git a/model/Versioned.php b/model/Versioned.php index 53fb299de..4ab4c450b 100644 --- a/model/Versioned.php +++ b/model/Versioned.php @@ -149,9 +149,9 @@ class Versioned extends DataExtension { // Add all _versions columns foreach(self::$db_for_versions_table as $name => $type) { - $query->select[] = sprintf('"%s_versions"."%s"', $baseTable, $name); + $query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name); } - $query->select[] = sprintf('"%s_versions"."%s" AS "ID"', $baseTable, 'RecordID'); + $query->selectField(sprintf('"%s_versions"."%s"', $baseTable, 'RecordID'), "ID"); if($table != $baseTable) { $query->from[$table] .= " AND \"{$table}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\""; @@ -195,9 +195,9 @@ class Versioned extends DataExtension { // Add all _versions columns foreach(self::$db_for_versions_table as $name => $type) { - $query->selectMore(sprintf('"%s_versions"."%s"', $baseTable, $name)); + $query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name); } - $query->selectMore(sprintf('"%s_versions"."%s" AS "ID"', $baseTable, 'RecordID')); + $query->selectField(sprintf('"%s_versions"."%s"', $baseTable, 'RecordID'), "ID"); // latest_version has one more step // Return latest version instances, regardless of whether they are on a particular stage @@ -722,7 +722,7 @@ class Versioned extends DataExtension { // Add all _versions columns foreach(self::$db_for_versions_table as $name => $type) { - $query->select[] = sprintf('"%s_versions"."%s"', $baseTable, $name); + $query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name); } $query->where[] = "\"{$baseTable}_versions\".\"RecordID\" = '{$this->owner->ID}'"; diff --git a/model/fieldtypes/Money.php b/model/fieldtypes/Money.php index 6ec94cf7c..963608b6a 100644 --- a/model/fieldtypes/Money.php +++ b/model/fieldtypes/Money.php @@ -96,8 +96,8 @@ class Money extends DBField implements CompositeDBField { function addToQuery(&$query) { parent::addToQuery($query); - $query->select[] = sprintf('"%sAmount"', $this->name); - $query->select[] = sprintf('"%sCurrency"', $this->name); + $query->selectField(sprintf('"%sAmount"', $this->name)); + $query->selectField(sprintf('"%sCurrency"', $this->name)); } function setValue($value, $record = null, $markChanged = true) { diff --git a/tests/model/SQLQueryTest.php b/tests/model/SQLQueryTest.php index 5aaf80686..3591f95af 100755 --- a/tests/model/SQLQueryTest.php +++ b/tests/model/SQLQueryTest.php @@ -23,14 +23,14 @@ class SQLQueryTest extends SapphireTest { function testSelectFromUserSpecifiedFields() { $query = new SQLQuery(); - $query->select = array("Name", "Title", "Description"); + $query->select(array("Name", "Title", "Description")); $query->from[] = "MyTable"; $this->assertEquals("SELECT Name, Title, Description FROM MyTable", $query->sql()); } function testSelectWithWhereClauseFilter() { $query = new SQLQuery(); - $query->select = array("Name","Meta"); + $query->select(array("Name","Meta")); $query->from[] = "MyTable"; $query->where[] = "Name = 'Name'"; $query->where[] = "Meta = 'Test'";