From c615c4eb917586b7011f3922baeb5efea69e5ac1 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Mon, 21 Mar 2011 21:37:55 +1300 Subject: [PATCH] ENHANCEMENT: First cut of running SearchContext through DataList/DataQuery. Note that the eventual goal is probably to ditch SearchContext entirely. --- core/model/DataQuery.php | 81 ++++++++++++++++++++- search/SearchContext.php | 46 ++++-------- search/filters/EndsWithFilter.php | 6 +- search/filters/ExactMatchFilter.php | 6 +- search/filters/ExactMatchMultiFilter.php | 7 +- search/filters/FulltextFilter.php | 2 +- search/filters/GreaterThanFilter.php | 6 +- search/filters/LessThanFilter.php | 6 +- search/filters/NegationFilter.php | 4 +- search/filters/PartialMatchFilter.php | 6 +- search/filters/SearchFilter.php | 93 +----------------------- search/filters/StartsWithFilter.php | 6 +- search/filters/StartsWithMultiFilter.php | 6 +- search/filters/SubstringFilter.php | 4 +- tests/model/SQLQueryTest.php | 2 + 15 files changed, 129 insertions(+), 152 deletions(-) diff --git a/core/model/DataQuery.php b/core/model/DataQuery.php index 534db3568..bcea0d23e 100644 --- a/core/model/DataQuery.php +++ b/core/model/DataQuery.php @@ -352,7 +352,86 @@ class DataQuery { return $this; } } - + + /** + * Traverse the relationship fields, and add the table + * mappings to the query object state. This has to be called + * in any overloaded {@link SearchFilter->apply()} methods manually. + * + * @param $relation The array/dot-syntax relation to follow + * @return The model class of the related item + */ + function applyRelation($relation) { + // NO-OP + if(!$relation) return $this->dataClass; + + if(is_string($relation)) $relation = explode(".", $relation); + + $modelClass = $this->dataClass; + + foreach($relation as $rel) { + $model = singleton($modelClass); + if ($component = $model->has_one($rel)) { + if(!$this->query->isJoinedTo($component)) { + $foreignKey = $model->getReverseAssociation($component); + $this->query->leftJoin($component, "\"$component\".\"ID\" = \"{$modelClass}\".\"{$foreignKey}ID\""); + + /** + * add join clause to the component's ancestry classes so that the search filter could search on its + * ancester fields. + */ + $ancestry = ClassInfo::ancestry($component, true); + if(!empty($ancestry)){ + $ancestry = array_reverse($ancestry); + foreach($ancestry as $ancestor){ + if($ancestor != $component){ + $this->query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\""); + $component=$ancestor; + } + } + } + } + $modelClass = $component; + + } elseif ($component = $model->has_many($rel)) { + if(!$this->query->isJoinedTo($component)) { + $ancestry = $model->getClassAncestry(); + $foreignKey = $model->getRemoteJoinField($rel); + $this->query->leftJoin($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. + */ + $ancestry = ClassInfo::ancestry($component, true); + if(!empty($ancestry)){ + $ancestry = array_reverse($ancestry); + foreach($ancestry as $ancestor){ + if($ancestor != $component){ + $this->query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\""); + $component=$ancestor; + } + } + } + } + $modelClass = $component; + + } elseif ($component = $model->many_many($rel)) { + 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\""); + if(ClassInfo::hasTable($componentClass)) { + $this->query->leftJoin($componentClass, "\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\""); + } + $modelClass = $componentClass; + + } + } + + return $modelClass; + } + /** * Select the given fields from the given table */ diff --git a/search/SearchContext.php b/search/SearchContext.php index 54a4e8a73..ca4be07c2 100644 --- a/search/SearchContext.php +++ b/search/SearchContext.php @@ -115,19 +115,17 @@ class SearchContext extends Object { * @return SQLQuery */ public function getQuery($searchParams, $sort = false, $limit = false, $existingQuery = null) { - $model = singleton($this->modelClass); - - if($existingQuery) { - $query = $existingQuery; - } else { - $query = $model->extendedSQL(); - } - - $SQL_limit = Convert::raw2sql($limit); - $query->limit($SQL_limit); - - $SQL_sort = (!empty($sort)) ? Convert::raw2sql($sort) : singleton($this->modelClass)->stat('default_sort'); - $query->orderby($SQL_sort); + if($existingQuery) { + if(!($existingQuery instanceof DataList)) throw new InvalidArgumentException("existingQuery must be DataList"); + if($existingQuery->dataClass() != $this->modelClass) throw new InvalidArgumentException("existingQuery's dataClass is " . $existingQuery->dataClass() . ", $this->modelClass expected."); + $query = $existingQuery; + + } else { + $query = DataList::create($this->modelClass); + } + + $query->limit($limit); + $query->sort($sort); // hack to work with $searchParems when it's an Object $searchParamArray = array(); @@ -143,15 +141,12 @@ class SearchContext extends Object { $filter->setModel($this->modelClass); $filter->setValue($value); if(! $filter->isEmpty()) { - $filter->apply($query); + $filter->apply($query->dataQuery()); } } } - $query->connective = $this->connective; - $query->distinct = true; - - $model->extend('augmentSQL', $query); + if($this->connective != "AND") throw new Exception("SearchContext connective '$this->connective' not supported after ORM-rewrite."); return $query; } @@ -168,18 +163,9 @@ class SearchContext extends Object { */ public function getResults($searchParams, $sort = false, $limit = false) { $searchParams = array_filter($searchParams, array($this,'clearEmptySearchFields')); - - $query = $this->getQuery($searchParams, $sort, $limit); - - // use if a raw SQL query is needed - $results = new DataObjectSet(); - foreach($query->execute() as $row) { - $className = $row['RecordClassName']; - $results->push(new $className($row)); - } - return $results; - // - //return DataObject::get($this->modelClass, $query->getFilter(), "", "", $limit); + + // getQuery actually returns a DataList + return $this->getQuery($searchParams, $sort, $limit); } /** diff --git a/search/filters/EndsWithFilter.php b/search/filters/EndsWithFilter.php index 056353ede..3cc8eb360 100644 --- a/search/filters/EndsWithFilter.php +++ b/search/filters/EndsWithFilter.php @@ -23,9 +23,9 @@ class EndsWithFilter extends SearchFilter { * * @return unknown */ - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - $query->where($this->getDbName() . " LIKE '%" . Convert::raw2sql($this->getValue()) . "'"); + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $query->filter($this->getDbName() . " LIKE '%" . Convert::raw2sql($this->getValue()) . "'"); } public function isEmpty() { diff --git a/search/filters/ExactMatchFilter.php b/search/filters/ExactMatchFilter.php index 161c016aa..9dcd28ba2 100644 --- a/search/filters/ExactMatchFilter.php +++ b/search/filters/ExactMatchFilter.php @@ -20,9 +20,9 @@ class ExactMatchFilter extends SearchFilter { * * @return unknown */ - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - return $query->where(sprintf( + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->filter(sprintf( "%s = '%s'", $this->getDbName(), Convert::raw2sql($this->getValue()) diff --git a/search/filters/ExactMatchMultiFilter.php b/search/filters/ExactMatchMultiFilter.php index e85d410ea..9b35f9106 100644 --- a/search/filters/ExactMatchMultiFilter.php +++ b/search/filters/ExactMatchMultiFilter.php @@ -14,9 +14,8 @@ */ class ExactMatchMultiFilter extends SearchFilter { - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); // hack // PREVIOUS $values = explode(',',$this->getValue()); $values = array(); @@ -40,7 +39,7 @@ class ExactMatchMultiFilter extends SearchFilter { } $SQL_valueStr = "'" . implode("','", $values) . "'"; - return $query->where(sprintf( + return $query->filter(sprintf( "%s IN (%s)", $this->getDbName(), $SQL_valueStr diff --git a/search/filters/FulltextFilter.php b/search/filters/FulltextFilter.php index 83e4538c1..0401331f0 100644 --- a/search/filters/FulltextFilter.php +++ b/search/filters/FulltextFilter.php @@ -27,7 +27,7 @@ */ class FulltextFilter extends SearchFilter { - public function apply(SQLQuery $query) { + public function apply(DataQuery $query) { $query->where(sprintf( "MATCH (%s) AGAINST ('%s')", $this->getDbName(), diff --git a/search/filters/GreaterThanFilter.php b/search/filters/GreaterThanFilter.php index 20c95e24d..0556c211d 100644 --- a/search/filters/GreaterThanFilter.php +++ b/search/filters/GreaterThanFilter.php @@ -12,9 +12,9 @@ class GreaterThanFilter extends SearchFilter { /** * @return $query */ - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - return $query->where(sprintf( + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->filter(sprintf( "%s > '%s'", $this->getDbName(), Convert::raw2sql($this->getDbFormattedValue()) diff --git a/search/filters/LessThanFilter.php b/search/filters/LessThanFilter.php index 9b522ef51..b79d29a65 100644 --- a/search/filters/LessThanFilter.php +++ b/search/filters/LessThanFilter.php @@ -12,9 +12,9 @@ class LessThanFilter extends SearchFilter { /** * @return $query */ - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - return $query->where(sprintf( + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->filter(sprintf( "%s < '%s'", $this->getDbName(), Convert::raw2sql($this->getDbFormattedValue()) diff --git a/search/filters/NegationFilter.php b/search/filters/NegationFilter.php index 73ad02ecd..9d092936a 100644 --- a/search/filters/NegationFilter.php +++ b/search/filters/NegationFilter.php @@ -7,8 +7,8 @@ */ class NegationFilter extends SearchFilter { - public function apply(SQLQuery $query) { - return $query->where(sprintf( + public function apply(DataQuery $query) { + return $query->filter(sprintf( "%s != '%s'", $this->getDbName(), Convert::raw2sql($this->getValue()) diff --git a/search/filters/PartialMatchFilter.php b/search/filters/PartialMatchFilter.php index bad845f5c..cf97d48fb 100644 --- a/search/filters/PartialMatchFilter.php +++ b/search/filters/PartialMatchFilter.php @@ -12,9 +12,9 @@ */ class PartialMatchFilter extends SearchFilter { - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - return $query->where(sprintf( + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->filter(sprintf( "%s LIKE '%%%s%%'", $this->getDbName(), Convert::raw2sql($this->getValue()) diff --git a/search/filters/SearchFilter.php b/search/filters/SearchFilter.php index d6f4989c6..615ab7c1b 100755 --- a/search/filters/SearchFilter.php +++ b/search/filters/SearchFilter.php @@ -149,96 +149,7 @@ abstract class SearchFilter extends Object { $dbField->setValue($this->value); return $dbField->RAW(); } - - /** - * Traverse the relationship fields, and add the table - * mappings to the query object state. This has to be called - * in any overloaded {@link SearchFilter->apply()} methods manually. - * - * @todo try to make this implicitly triggered so it doesn't have to be manually called in child filters - * @param SQLQuery $query - * @return SQLQuery - */ - function applyRelation($query) { - if (is_array($this->relation)) { - foreach($this->relation as $rel) { - $model = singleton($this->model); - if ($component = $model->has_one($rel)) { - if(!$query->isJoinedTo($component)) { - $foreignKey = $model->getReverseAssociation($component); - $query->leftJoin($component, "\"$component\".\"ID\" = \"{$this->model}\".\"{$foreignKey}ID\""); - - /** - * add join clause to the component's ancestry classes so that the search filter could search on its - * ancester fields. - */ - $ancestry = ClassInfo::ancestry($component, true); - if(!empty($ancestry)){ - $ancestry = array_reverse($ancestry); - foreach($ancestry as $ancestor){ - if($ancestor != $component){ - $query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\""); - $component=$ancestor; - } - } - } - } - $this->model = $component; - } elseif ($component = $model->has_many($rel)) { - if(!$query->isJoinedTo($component)) { - $ancestry = $model->getClassAncestry(); - $foreignKey = $model->getRemoteJoinField($rel); - $query->leftJoin($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. - */ - $ancestry = ClassInfo::ancestry($component, true); - if(!empty($ancestry)){ - $ancestry = array_reverse($ancestry); - foreach($ancestry as $ancestor){ - if($ancestor != $component){ - $query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\""); - $component=$ancestor; - } - } - } - } - $this->model = $component; - } elseif ($component = $model->many_many($rel)) { - list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component; - $parentBaseClass = ClassInfo::baseDataClass($parentClass); - $componentBaseClass = ClassInfo::baseDataClass($componentClass); - $query->innerJoin($relationTable, "\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\""); - $query->leftJoin($componentBaseClass, "\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\""); - if(ClassInfo::hasTable($componentClass)) { - $query->leftJoin($componentClass, "\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\""); - } - $this->model = $componentClass; - - // Experimental support for user-defined relationships via a "(relName)Query" method - // This will likely be dropped in 2.4 for a system that makes use of Lazy Data Lists. - } elseif($model->hasMethod($rel.'Query')) { - // Get the query representing the join - it should have "$ID" in the filter - $newQuery = $model->{"{$rel}Query"}(); - if($newQuery) { - // Get the table to join to - //DATABASE ABSTRACTION: I don't think we need this line anymore: - $newModel = str_replace('`','',array_shift($newQuery->from)); - // Get the filter to use on the join - $ancestry = $model->getClassAncestry(); - $newFilter = "(" . str_replace('$ID', "\"{$ancestry[0]}\".\"ID\"" , implode(") AND (", $newQuery->where) ) . ")"; - $query->leftJoin($newModel, $newFilter); - $this->model = $newModel; - } else { - $this->name = "NULL"; - return; - } - } - } - } - return $query; - } + /** * Apply filter criteria to a SQL query. @@ -246,7 +157,7 @@ abstract class SearchFilter extends Object { * @param SQLQuery $query * @return SQLQuery */ - abstract public function apply(SQLQuery $query); + abstract public function apply(DataQuery $query); /** * Determines if a field has a value, diff --git a/search/filters/StartsWithFilter.php b/search/filters/StartsWithFilter.php index c6ada3c7c..fda662316 100644 --- a/search/filters/StartsWithFilter.php +++ b/search/filters/StartsWithFilter.php @@ -23,9 +23,9 @@ class StartsWithFilter extends SearchFilter { * * @return unknown */ - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - $query->where($this->getDbName() . " LIKE '" . Convert::raw2sql($this->getValue()) . "%'"); + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $query->filter($this->getDbName() . " LIKE '" . Convert::raw2sql($this->getValue()) . "%'"); } public function isEmpty() { diff --git a/search/filters/StartsWithMultiFilter.php b/search/filters/StartsWithMultiFilter.php index ed105a6cf..22f4f4dcf 100644 --- a/search/filters/StartsWithMultiFilter.php +++ b/search/filters/StartsWithMultiFilter.php @@ -14,8 +14,8 @@ */ class StartsWithMultiFilter extends SearchFilter { - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); $values = explode(',', $this->getValue()); foreach($values as $value) { @@ -25,7 +25,7 @@ class StartsWithMultiFilter extends SearchFilter { ); } - return $query->where(implode(" OR ", $matches)); + return $query->filter(implode(" OR ", $matches)); } public function isEmpty() { diff --git a/search/filters/SubstringFilter.php b/search/filters/SubstringFilter.php index 7b72b46b4..a8c345d64 100644 --- a/search/filters/SubstringFilter.php +++ b/search/filters/SubstringFilter.php @@ -12,8 +12,8 @@ */ class SubstringFilter extends SearchFilter { - public function apply(SQLQuery $query) { - return $query->where(sprintf( + public function apply(DataQuery $query) { + return $query->filter(sprintf( "LOCATE('%s', %s) != 0", Convert::raw2sql($this->getValue()), $this->getDbName() diff --git a/tests/model/SQLQueryTest.php b/tests/model/SQLQueryTest.php index 44ae1a4a1..89e16fcbb 100644 --- a/tests/model/SQLQueryTest.php +++ b/tests/model/SQLQueryTest.php @@ -65,6 +65,7 @@ class SQLQueryTest extends SapphireTest { } function testSelectWithPredicateFilters() { + /* this is no longer part of this $query = new SQLQuery(); $query->select(array("Name"))->from("SQLQueryTest_DO"); @@ -77,6 +78,7 @@ class SQLQueryTest extends SapphireTest { $match->apply($query); $this->assertEquals("SELECT Name FROM SQLQueryTest_DO WHERE (\"SQLQueryTest_DO\".\"Name\" = 'Value') AND (\"SQLQueryTest_DO\".\"Meta\" LIKE '%Value%')", $query->sql()); + */ } function testSelectWithLimitClause() {