From 79b3f8ac45483d9dfa4b9ff0a494ae0201beacec Mon Sep 17 00:00:00 2001 From: Simon Welsh Date: Thu, 6 Sep 2012 19:54:44 +1200 Subject: [PATCH 1/3] API Add exclude() method to SearchFilters that excludes items that match the filter. --- search/filters/EndsWithFilter.php | 63 +++++++++++++++++++++-- search/filters/ExactMatchFilter.php | 60 ++++++++++++++++++++-- search/filters/ExactMatchMultiFilter.php | 56 ++++++++++---------- search/filters/FulltextFilter.php | 12 ++++- search/filters/GreaterThanFilter.php | 19 +++++-- search/filters/LessThanFilter.php | 19 +++++-- search/filters/NegationFilter.php | 9 ++++ search/filters/PartialMatchFilter.php | 40 +++++++++++---- search/filters/SearchFilter.php | 62 ++++++++++++++++++++-- search/filters/StartsWithFilter.php | 65 ++++++++++++++++++++++-- search/filters/StartsWithMultiFilter.php | 42 ++++++++++----- search/filters/SubstringFilter.php | 27 +++++++--- search/filters/WithinRangeFilter.php | 15 ++++-- 13 files changed, 408 insertions(+), 81 deletions(-) diff --git a/search/filters/EndsWithFilter.php b/search/filters/EndsWithFilter.php index 74539da3e..b8737847b 100644 --- a/search/filters/EndsWithFilter.php +++ b/search/filters/EndsWithFilter.php @@ -21,9 +21,9 @@ class EndsWithFilter extends SearchFilter { /** * Applies a match on the trailing characters of a field value. * - * @return unknown + * @return DataQuery */ - public function apply(DataQuery $query) { + protected function applyOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); return $query->where(sprintf( "%s %s '%%%s'", @@ -32,8 +32,65 @@ class EndsWithFilter extends SearchFilter { Convert::raw2sql($this->getValue()) )); } + + /** + * Applies a match on the trailing characters of a field value. + * Matches against one of the many values. + * + * @return DataQuery + */ + protected function applyMany(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $connectives = array(); + foreach($this->getValue() as $value) { + $connectives[] = sprintf( + "%s %s '%%%s'", + $this->getDbName(), + (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + Convert::raw2sql($value) + ); + } + $whereClause = implode(' OR ', $connectives); + return $query->where($whereClause); + } + + /** + * Excludes a match on the trailing characters of a field value. + * + * @return DataQuery + */ + protected function excludeOne(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->where(sprintf( + "%s NOT %s '%%%s'", + $this->getDbName(), + (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + Convert::raw2sql($this->getValue()) + )); + } + + /** + * Excludes a match on the trailing characters of a field value. + * Excludes a field if it matches any of the values. + * + * @return DataQuery + */ + protected function excludeMany(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $connectives = array(); + foreach($this->getValue() as $value) { + $connectives[] = sprintf( + "%s NOT %s '%%%s'", + $this->getDbName(), + (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + Convert::raw2sql($value) + ); + } + $whereClause = implode(' AND ', $connectives); + return $query->where($whereClause); + } public function isEmpty() { - return $this->getValue() == null || $this->getValue() == ''; + return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === ''; } } diff --git a/search/filters/ExactMatchFilter.php b/search/filters/ExactMatchFilter.php index d6fbdc32a..496ab54a9 100644 --- a/search/filters/ExactMatchFilter.php +++ b/search/filters/ExactMatchFilter.php @@ -18,9 +18,9 @@ class ExactMatchFilter extends SearchFilter { /** * Applies an exact match (equals) on a field value. * - * @return unknown + * @return DataQuery */ - public function apply(DataQuery $query) { + protected function applyOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); return $query->where(sprintf( "%s = '%s'", @@ -28,8 +28,62 @@ class ExactMatchFilter extends SearchFilter { Convert::raw2sql($this->getValue()) )); } + + /** + * Applies an exact match (equals) on a field value against multiple + * possible values. + * + * @return DataQuery + */ + protected function applyMany(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $values = array(); + foreach($this->getValue() as $value) { + $values[] = Convert::raw2sql($value); + } + $valueStr = "'" . implode("', '", $values) . "'"; + return $query->where(sprintf( + '%s IN (%s)', + $this->getDbName(), + $valueStr + )); + } + + /** + * Excludes an exact match (equals) on a field value. + * + * @return DataQuery + */ + protected function excludeOne(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->where(sprintf( + "%s != '%s'", + $this->getDbName(), + Convert::raw2sql($this->getValue()) + )); + } + + /** + * Excludes an exact match (equals) on a field value against multiple + * possible values. + * + * @return DataQuery + */ + protected function excludeMany(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $values = array(); + foreach($this->getValue() as $value) { + $values[] = Convert::raw2sql($value); + } + $valueStr = "'" . implode("', '", $values) . "'"; + return $query->where(sprintf( + '%s NOT IN (%s)', + $this->getDbName(), + $valueStr + )); + } public function isEmpty() { - return $this->getValue() == null || $this->getValue() == ''; + return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === ''; } } diff --git a/search/filters/ExactMatchMultiFilter.php b/search/filters/ExactMatchMultiFilter.php index 39856aa28..053b6c1a4 100644 --- a/search/filters/ExactMatchMultiFilter.php +++ b/search/filters/ExactMatchMultiFilter.php @@ -7,46 +7,46 @@ /** * Checks if a value is in a given set. * SQL syntax used: Column IN ('val1','val2') + * @deprecated 3.1 Use ExactMatchFilter instead * - * @todo Add negation (NOT IN)6 * @package framework * @subpackage search */ class ExactMatchMultiFilter extends SearchFilter { + function __construct($fullName, $value = false) { + Deprecation::notice('3.1', 'Use ExactMatchFilter instead.'); + parent::__construct($fullName, $value); + } public function apply(DataQuery $query) { - $this->model = $query->applyRelation($this->relation); - // hack - // PREVIOUS $values = explode(',',$this->getValue()); - $values = array(); - if (is_string($this->getValue())) { + if (!is_array($this->getValue())) { $values = explode(',',$this->getValue()); + } else { + $values = $this->getValue(); } - else { - foreach($this->getValue() as $v) { - $values[] = $v; - } + $filter = new ExactMatchFilter($this->getFullName(), $values); + return $filter->apply($query); + } + + protected function applyOne(DataQuery $query) { + /* NO OP */ + } + + public function exclude(DataQuery $query) { + if (!is_array($this->getValue())) { + $values = explode(',',$this->getValue()); + } else { + $values = $this->getValue(); } - - - if(! $values) return false; - for($i = 0; $i < count($values); $i++) { - if(! is_numeric($values[$i])) { - // @todo Fix string replacement to only replace leading and tailing quotes - $values[$i] = str_replace("'", '', $values[$i]); - $values[$i] = Convert::raw2sql($values[$i]); - } - } - $SQL_valueStr = "'" . implode("','", $values) . "'"; - - return $query->where(sprintf( - "%s IN (%s)", - $this->getDbName(), - $SQL_valueStr - )); + $filter = new ExactMatchFilter($this->getFullName(), $values); + return $filter->exclude($query); + } + + protected function excludeOne(DataQuery $query) { + /* NO OP */ } public function isEmpty() { - return $this->getValue() == null || $this->getValue() == ''; + return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === ''; } } diff --git a/search/filters/FulltextFilter.php b/search/filters/FulltextFilter.php index 118f4fe4f..36b77b4c2 100644 --- a/search/filters/FulltextFilter.php +++ b/search/filters/FulltextFilter.php @@ -27,7 +27,7 @@ */ class FulltextFilter extends SearchFilter { - public function apply(DataQuery $query) { + protected function applyOne(DataQuery $query) { return $query->where(sprintf( "MATCH (%s) AGAINST ('%s')", $this->getDbName(), @@ -35,7 +35,15 @@ class FulltextFilter extends SearchFilter { )); } + protected function excludeOne(DataQuery $query) { + return $query->where(sprintf( + "NOT MATCH (%s) AGAINST ('%s')", + $this->getDbName(), + Convert::raw2sql($this->getValue()) + )); + } + public function isEmpty() { - return $this->getValue() == null || $this->getValue() == ''; + return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === ''; } } diff --git a/search/filters/GreaterThanFilter.php b/search/filters/GreaterThanFilter.php index 8756d908c..a17c3a2f5 100644 --- a/search/filters/GreaterThanFilter.php +++ b/search/filters/GreaterThanFilter.php @@ -10,9 +10,9 @@ class GreaterThanFilter extends SearchFilter { /** - * @return $query + * @return DataQuery */ - public function apply(DataQuery $query) { + protected function applyOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); $value = $this->getDbFormattedValue(); @@ -21,8 +21,21 @@ class GreaterThanFilter extends SearchFilter { return $query->where($filter); } + + /** + * @return DataQuery + */ + protected function excludeOne(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $value = $this->getDbFormattedValue(); + + if(is_numeric($value)) $filter = sprintf("%s <= %s", $this->getDbName(), Convert::raw2sql($value)); + else $filter = sprintf("%s <= '%s'", $this->getDbName(), Convert::raw2sql($value)); + + return $query->where($filter); + } public function isEmpty() { - return $this->getValue() == null || $this->getValue() == ''; + return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === ''; } } diff --git a/search/filters/LessThanFilter.php b/search/filters/LessThanFilter.php index b327d1555..6c9538ab8 100644 --- a/search/filters/LessThanFilter.php +++ b/search/filters/LessThanFilter.php @@ -10,9 +10,9 @@ class LessThanFilter extends SearchFilter { /** - * @return $query + * @return DataQuery */ - public function apply(DataQuery $query) { + protected function applyOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); $value = $this->getDbFormattedValue(); @@ -21,8 +21,21 @@ class LessThanFilter extends SearchFilter { return $query->where($filter); } + + /** + * @return DataQuery + */ + protected function excludeOne(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $value = $this->getDbFormattedValue(); + + if(is_numeric($value)) $filter = sprintf("%s >= %s", $this->getDbName(), Convert::raw2sql($value)); + else $filter = sprintf("%s >= '%s'", $this->getDbName(), Convert::raw2sql($value)); + + return $query->where($filter); + } public function isEmpty() { - return $this->getValue() == null || $this->getValue() == ''; + return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === ''; } } diff --git a/search/filters/NegationFilter.php b/search/filters/NegationFilter.php index 2cec7c41f..b131db4a5 100755 --- a/search/filters/NegationFilter.php +++ b/search/filters/NegationFilter.php @@ -6,6 +6,7 @@ * @subpackage search */ class NegationFilter extends SearchFilter { + // Deprecate this once modifiers are done public function apply(DataQuery $query) { $this->model = $query->applyRelation($this->relation); @@ -16,5 +17,13 @@ class NegationFilter extends SearchFilter { )); } + public function exclude(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->where(sprintf( + "%s = '%s'", + $this->getDbName(), + Convert::raw2sql($this->getValue()) + )); + } } diff --git a/search/filters/PartialMatchFilter.php b/search/filters/PartialMatchFilter.php index 418e1975c..91582f745 100644 --- a/search/filters/PartialMatchFilter.php +++ b/search/filters/PartialMatchFilter.php @@ -12,23 +12,45 @@ */ class PartialMatchFilter extends SearchFilter { - public function apply(DataQuery $query) { + protected function applyOne(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $comparison = (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE'; + $where = sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($this->getValue())); + + return $query->where($where); + } + + protected function applyMany(DataQuery $query) { $this->model = $query->applyRelation($this->relation); $where = array(); $comparison = (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE'; - if(is_array($this->getValue())) { - foreach($this->getValue() as $value) { - $where[]= sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($value)); - } - - } else { - $where[] = sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($this->getValue())); + foreach($this->getValue() as $value) { + $where[]= sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($value)); } return $query->where(implode(' OR ', $where)); } + + protected function excludeOne(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $comparison = (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE'; + $where = sprintf("%s NOT %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($this->getValue())); + + return $query->where($where); + } + + protected function excludeMany(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $where = array(); + $comparison = (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE'; + foreach($this->getValue() as $value) { + $where[]= sprintf("%s NOT %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($value)); + } + + return $query->where(implode(' AND ', $where)); + } public function isEmpty() { - return $this->getValue() == null || $this->getValue() == ''; + return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === ''; } } diff --git a/search/filters/SearchFilter.php b/search/filters/SearchFilter.php index c904fb333..d8859a030 100644 --- a/search/filters/SearchFilter.php +++ b/search/filters/SearchFilter.php @@ -173,10 +173,66 @@ abstract class SearchFilter extends Object { /** * Apply filter criteria to a SQL query. * - * @param SQLQuery $query - * @return SQLQuery + * @param DataQuery $query + * @return DataQuery */ - abstract public function apply(DataQuery $query); + public function apply(DataQuery $query) { + if(is_array($this->value)) { + return $this->applyMany($query); + } else { + return $this->applyOne($query); + } + } + + /** + * Apply filter criteria to a SQL query with a single value. + * + * @param DataQuery $query + * @return DataQuery + */ + abstract protected function applyOne(DataQuery $query); + + /** + * Apply filter criteria to a SQL query with an array of values. + * + * @param DataQuery $query + * @return DataQuery + */ + protected function applyMany(DataQuery $query) { + throw new InvalidArgumentException(get_class($this) . "can't be used to filter by a list of items."); + } + + /** + * Exclude filter criteria from a SQL query. + * + * @param DataQuery $query + * @return DataQuery + */ + public function exclude(DataQuery $query) { + if(is_array($this->value)) { + return $this->excludeMany($query); + } else { + return $this->excludeOne($query); + } + } + + /** + * Exclude filter criteria from a SQL query with a single value. + * + * @param DataQuery $query + * @return DataQuery + */ + abstract protected function excludeOne(DataQuery $query); + + /** + * Exclude filter criteria from a SQL query with an array of values. + * + * @param DataQuery $query + * @return DataQuery + */ + protected function excludeMany(DataQuery $query) { + throw new InvalidArgumentException(get_class($this) . "can't be used to filter by a list of items."); + } /** * Determines if a field has a value, diff --git a/search/filters/StartsWithFilter.php b/search/filters/StartsWithFilter.php index e81dbcbd8..cadb8ac53 100644 --- a/search/filters/StartsWithFilter.php +++ b/search/filters/StartsWithFilter.php @@ -19,11 +19,11 @@ class StartsWithFilter extends SearchFilter { /** - * Applies a substring match on a field value. + * Applies a match on the starting characters of a field value. * - * @return unknown + * @return DataQuery */ - public function apply(DataQuery $query) { + protected function applyOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); return $query->where(sprintf( "%s %s '%s%%'", @@ -32,8 +32,65 @@ class StartsWithFilter extends SearchFilter { Convert::raw2sql($this->getValue()) )); } + + /** + * Applies a match on the starting characters of a field value. + * Matches against one of the many values. + * + * @return DataQuery + */ + protected function applyMany(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $connectives = array(); + foreach($this->getValue() as $value) { + $connectives[] = sprintf( + "%s %s '%s%%'", + $this->getDbName(), + (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + Convert::raw2sql($value) + ); + } + $whereClause = implode(' OR ', $connectives); + return $query->where($whereClause); + } + + /** + * Excludes a match on the starting characters of a field value. + * + * @return DataQuery + */ + protected function excludeOne(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->where(sprintf( + "%s NOT %s '%s%%'", + $this->getDbName(), + (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + Convert::raw2sql($this->getValue()) + )); + } + + /** + * Excludes a match on the starting characters of a field value. + * Excludes a field if it matches any of the values. + * + * @return DataQuery + */ + protected function excludeMany(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $connectives = array(); + foreach($this->getValue() as $value) { + $connectives[] = sprintf( + "%s NOT %s '%s%%'", + $this->getDbName(), + (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + Convert::raw2sql($value) + ); + } + $whereClause = implode(' AND ', $connectives); + return $query->where($whereClause); + } public function isEmpty() { - return $this->getValue() == null || $this->getValue() == ''; + return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === ''; } } diff --git a/search/filters/StartsWithMultiFilter.php b/search/filters/StartsWithMultiFilter.php index 4e36b078d..4a67c8326 100644 --- a/search/filters/StartsWithMultiFilter.php +++ b/search/filters/StartsWithMultiFilter.php @@ -6,29 +6,47 @@ /** * Checks if a value starts with one of the items of in a given set. - * SQL syntax used: Column IN ('val1','val2') + * @deprecated 3.1 Use StartsWithFilter instead * * @todo Add negation (NOT IN)6 * @package framework * @subpackage search */ class StartsWithMultiFilter extends SearchFilter { + function __construct($fullName, $value = false) { + Deprecation::notice('3.1', 'Use StartsWithFilter instead.'); + parent::__construct($fullName, $value); + } public function apply(DataQuery $query) { - $this->model = $query->applyRelation($this->relation); - $values = explode(',', $this->getValue()); - - foreach($values as $value) { - $matches[] = sprintf("%s LIKE '%s%%'", - $this->getDbName(), - Convert::raw2sql(str_replace("'", '', $value)) - ); + if (!is_array($this->getValue())) { + $values = explode(',',$this->getValue()); + } else { + $values = $this->getValue(); } - - return $query->where(implode(" OR ", $matches)); + $filter = new StartsWithFilter($this->getFullName(), $values); + return $filter->apply($query); + } + + protected function applyOne(DataQuery $query) { + /* NO OP */ + } + + public function exclude(DataQuery $query) { + if (!is_array($this->getValue())) { + $values = explode(',',$this->getValue()); + } else { + $values = $this->getValue(); + } + $filter = new StartsWithFilter($this->getFullName(), $values); + return $filter->exclude($query); + } + + protected function excludeOne(DataQuery $query) { + /* NO OP */ } public function isEmpty() { - return $this->getValue() == null || $this->getValue() == ''; + return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === ''; } } diff --git a/search/filters/SubstringFilter.php b/search/filters/SubstringFilter.php index 468042b76..a0b34974f 100755 --- a/search/filters/SubstringFilter.php +++ b/search/filters/SubstringFilter.php @@ -14,20 +14,31 @@ class SubstringFilter extends PartialMatchFilter { public function __construct($fullName, $value = false) { Deprecation::notice('3.0', 'PartialMatchFilter instead.'); - SearchFilter::__construct($fullName, $value); + parent::__construct($fullName, $value); } public function apply(DataQuery $query) { - $this->model = $query->applyRelation($this->relation); - return $query->where(sprintf( - "LOCATE('%s', %s) != 0", - Convert::raw2sql($this->getValue()), - $this->getDbName() - )); + $values = $this->getValue(); + $filter = new PartialMatchFilter($this->getFullName(), $values); + return $filter->apply($query); } + protected function applyOne(DataQuery $query) { + /* NO OP */ + } + + public function exclude(DataQuery $query) { + $values = $this->getValue(); + $filter = new PartialMatchFilter($this->getFullName(), $values); + return $filter->exclude($query); + } + + protected function excludeOne(DataQuery $query) { + /* NO OP */ + } + public function isEmpty() { - return $this->getValue() == null || $this->getValue() == ''; + return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === ''; } } diff --git a/search/filters/WithinRangeFilter.php b/search/filters/WithinRangeFilter.php index 062cb94e8..ca0bb10d4 100644 --- a/search/filters/WithinRangeFilter.php +++ b/search/filters/WithinRangeFilter.php @@ -25,7 +25,7 @@ class WithinRangeFilter extends SearchFilter { $this->max = $max; } - public function apply(DataQuery $query) { + protected function applyOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); return $query->where(sprintf( "%s >= '%s' AND %s <= '%s'", @@ -35,6 +35,15 @@ class WithinRangeFilter extends SearchFilter { Convert::raw2sql($this->max) )); } - -} + protected function excludeOne(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->where(sprintf( + "%s < '%s' OR %s > '%s'", + $this->getDbName(), + Convert::raw2sql($this->min), + $this->getDbName(), + Convert::raw2sql($this->max) + )); + } +} From 2faf7d112d1d0a58724e393d08cecf0790f2e9b1 Mon Sep 17 00:00:00 2001 From: Simon Welsh Date: Thu, 6 Sep 2012 19:54:15 +1200 Subject: [PATCH 2/3] API Allow using SearchFilters in DataList::exclude() --- model/DataList.php | 46 ++++++++++++++++++++++-------------- tests/model/DataListTest.php | 12 ++++++++-- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/model/DataList.php b/model/DataList.php index bf7710168..7bbbbd41d 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -500,27 +500,37 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()'); } - $SQL_Statements = array(); - foreach($whereArguments as $fieldName => $value) { - if($fieldName == 'ID') { - $fieldName = sprintf('"%s"."ID"', ClassInfo::baseDataClass($this->dataClass)); - } else { - $fieldName = '"' . Convert::raw2sql($fieldName) . '"'; + return $this->alterDataQuery(function($query, $list) use ($whereArguments) { + $subquery = $query->disjunctiveGroup(); + + foreach($whereArguments as $field => $value) { + $fieldArgs = explode(':', $field); + $field = array_shift($fieldArgs); + $filterType = array_shift($fieldArgs); + $modifiers = $fieldArgs; + $list->excludeFilterContext($field, $filterType, $modifiers, $value, $subquery); } - - if(is_array($value)){ - $SQL_Statements[] = ($fieldName . ' NOT IN (\''.implode('\',\'', Convert::raw2sql($value)).'\')'); - } else { - $SQL_Statements[] = ($fieldName . ' != \''.Convert::raw2sql($value).'\''); - } - } - - if(!count($SQL_Statements)) return $this; - - return $this->alterDataQuery_30(function($query) use ($SQL_Statements){ - $query->whereAny($SQL_Statements); }); } + + /** + * Translates the comparisator to the sql query + * + * @param string $field - the fieldname in the db + * @param string $comparisators - example StartsWith, relates to a filtercontext + * @param string $value - the value that the filtercontext will use for matching + * @param DataQuery $dataQuery - The (sub)query to add the exclusion clauses to + * @todo Deprecated SearchContexts and pull their functionality into the core of the ORM + */ + private function excludeFilterContext($field, $comparisators, $modifiers, $value, $dataQuery) { + $t = singleton($this->dataClass())->dbObject($field); + $className = "{$comparisators}Filter"; + if(!class_exists($className)){ + throw new InvalidArgumentException('There are no '.$comparisators.' comparisator'); + } + $t = new $className($field, $value, $modifiers); + $t->exclude($dataQuery); + } /** * This method returns a copy of this list that does not contain any DataObjects that exists in $list diff --git a/tests/model/DataListTest.php b/tests/model/DataListTest.php index 29ca2f11c..bfadbb20c 100644 --- a/tests/model/DataListTest.php +++ b/tests/model/DataListTest.php @@ -501,7 +501,7 @@ class DataListTest extends SapphireTest { */ public function testMultipleExclude() { $list = DataObjectTest_TeamComment::get(); - $list->exclude(array('Name'=>'Bob', 'Comment'=>'This is a team comment by Bob')); + $list = $list->exclude(array('Name'=>'Bob', 'Comment'=>'This is a team comment by Bob')); $this->assertEquals(2, $list->count()); } @@ -514,9 +514,17 @@ class DataListTest extends SapphireTest { $list = $list->exclude('Name', 'Bob'); $this->assertContains( - 'WHERE ("Comment" = \'Phil is a unique guy, and comments on team2\') AND ("Name" != \'Bob\')', + 'WHERE ("DataObjectTest_TeamComment"."Comment" = ' + . '\'Phil is a unique guy, and comments on team2\') ' + . 'AND (("DataObjectTest_TeamComment"."Name" != \'Bob\'))', $list->sql()); } + + public function testExcludeWithSearchFilter() { + $list = DataObjectTest_TeamComment::get(); + $list = $list->exclude('Name:LessThan', 'Bob'); + $this->assertContains('WHERE (("DataObjectTest_TeamComment"."Name" >= \'Bob\'))', $list->sql()); + } /** * $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43 From c49f7566c3022734735d2bd78e737c1303439c2a Mon Sep 17 00:00:00 2001 From: Simon Welsh Date: Thu, 20 Sep 2012 19:26:05 +1200 Subject: [PATCH 3/3] API Allow use of :not, :nocase and :case modifiers to SearchFilters. More modifiers can be added to each class as desired. --- docs/en/topics/datamodel.md | 7 +- model/DataList.php | 85 +++++++++-------------- search/filters/EndsWithFilter.php | 30 ++++++-- search/filters/ExactMatchFilter.php | 88 ++++++++++++++++++++---- search/filters/ExactMatchMultiFilter.php | 8 +-- search/filters/NegationFilter.php | 31 +++++---- search/filters/PartialMatchFilter.php | 34 +++++++-- search/filters/SearchFilter.php | 35 +++++++++- search/filters/StartsWithFilter.php | 34 +++++++-- search/filters/StartsWithMultiFilter.php | 8 +-- search/filters/SubstringFilter.php | 8 +-- tests/model/DataListTest.php | 13 +++- 12 files changed, 268 insertions(+), 113 deletions(-) diff --git a/docs/en/topics/datamodel.md b/docs/en/topics/datamodel.md index 912335182..25f1c454a 100755 --- a/docs/en/topics/datamodel.md +++ b/docs/en/topics/datamodel.md @@ -205,13 +205,18 @@ This would be equivalent to a SQL query of ### Search Filter Modifiers -The where clauses showcased in the previous two sections (filter and exclude) specify case-insensitive exact +The where clauses showcased in the previous two sections (filter and exclude) specify exact matches by default. However, there are a number of suffixes that you can put on field names to change this behaviour `":StartsWith"`, `":EndsWith"`, `":PartialMatch"`, `":GreaterThan"`, `":LessThan"`, `":Negation"`. Each of these suffixes is represented in the ORM as a subclass of `[api:SearchFilter]`. Developers can define their own SearchFilters if needing to extend the ORM filter and exclude behaviours. +These suffixes can also take modifiers themselves. The modifiers currently supported are `":not"`, `":nocase"` +and `":case"`. These negate the filter, make it case-insensitive and make it case-sensitive respectively. The +default comparison uses the database's default. For MySQL and MSSQL, this is case-insensitive. For PostgreSQL, +this is case-sensitive. + The following is a query which will return everyone whose first name doesn't start with S, who has logged in since 1/1/2011. diff --git a/model/DataList.php b/model/DataList.php index 7bbbbd41d..dd0a4387f 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -374,38 +374,15 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab * Return a new instance of the list with an added filter */ public function addFilter($filterArray) { - $SQL_Statements = array(); foreach($filterArray as $field => $value) { - if(is_array($value)) { - $customQuery = 'IN (\''.implode('\',\'',Convert::raw2sql($value)).'\')'; - } else { - $customQuery = '= \''.Convert::raw2sql($value).'\''; - } - - if(stristr($field,':')) { - $fieldArgs = explode(':',$field); - $field = array_shift($fieldArgs); - foreach($fieldArgs as $fieldArg){ - $comparisor = $this->applyFilterContext($field, $fieldArg, $value); - } - } else { - if($field == 'ID') { - $field = sprintf('"%s"."ID"', ClassInfo::baseDataClass($this->dataClass)); - } else { - $field = '"' . Convert::raw2sql($field) . '"'; - } - - $SQL_Statements[] = $field . ' ' . $customQuery; - } + $fieldArgs = explode(':', $field); + $field = array_shift($fieldArgs); + $filterType = array_shift($fieldArgs); + $modifiers = $fieldArgs; + $this->applyFilterContext($field, $filterType, $modifiers, $value); } - if(!count($SQL_Statements)) return $this; - - return $this->alterDataQuery_30(function($query) use ($SQL_Statements){ - foreach($SQL_Statements as $SQL_Statement){ - $query->where($SQL_Statement); - } - }); + return $this; } /** @@ -459,16 +436,22 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab * * @param string $field - the fieldname in the db * @param string $comparisators - example StartsWith, relates to a filtercontext + * @param array $modifiers - Modifiers to pass to the filter, ie not,nocase * @param string $value - the value that the filtercontext will use for matching * @todo Deprecated SearchContexts and pull their functionality into the core of the ORM */ - private function applyFilterContext($field, $comparisators, $value) { + private function applyFilterContext($field, $comparisators, $modifiers, $value) { $t = singleton($this->dataClass())->dbObject($field); - $className = "{$comparisators}Filter"; - if(!class_exists($className)){ - throw new InvalidArgumentException('There are no '.$comparisators.' comparisator'); + if($comparisators) { + $className = "{$comparisators}Filter"; + } else { + $className = 'ExactMatchFilter'; } - $t = new $className($field,$value); + if(!class_exists($className)){ + $className = 'ExactMatchFilter'; + array_unshift($modifiers, $comparisators); + } + $t = new $className($field, $value, $modifiers); $t->apply($this->dataQuery()); } @@ -508,29 +491,23 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab $field = array_shift($fieldArgs); $filterType = array_shift($fieldArgs); $modifiers = $fieldArgs; - $list->excludeFilterContext($field, $filterType, $modifiers, $value, $subquery); + + // This is here since PHP 5.3 can't call protected/private methods in a closure. + $t = singleton($list->dataClass())->dbObject($field); + if($filterType) { + $className = "{$filterType}Filter"; + } else { + $className = 'ExactMatchFilter'; + } + if(!class_exists($className)){ + $className = 'ExactMatchFilter'; + array_unshift($modifiers, $filterType); + } + $t = new $className($field, $value, $modifiers); + $t->exclude($subquery); } }); } - - /** - * Translates the comparisator to the sql query - * - * @param string $field - the fieldname in the db - * @param string $comparisators - example StartsWith, relates to a filtercontext - * @param string $value - the value that the filtercontext will use for matching - * @param DataQuery $dataQuery - The (sub)query to add the exclusion clauses to - * @todo Deprecated SearchContexts and pull their functionality into the core of the ORM - */ - private function excludeFilterContext($field, $comparisators, $modifiers, $value, $dataQuery) { - $t = singleton($this->dataClass())->dbObject($field); - $className = "{$comparisators}Filter"; - if(!class_exists($className)){ - throw new InvalidArgumentException('There are no '.$comparisators.' comparisator'); - } - $t = new $className($field, $value, $modifiers); - $t->exclude($dataQuery); - } /** * This method returns a copy of this list that does not contain any DataObjects that exists in $list diff --git a/search/filters/EndsWithFilter.php b/search/filters/EndsWithFilter.php index b8737847b..83444f320 100644 --- a/search/filters/EndsWithFilter.php +++ b/search/filters/EndsWithFilter.php @@ -17,6 +17,28 @@ * @subpackage search */ class EndsWithFilter extends SearchFilter { + protected function comparison($exclude = false) { + $modifiers = $this->getModifiers(); + if(($extras = array_diff($modifiers, array('not', 'nocase', 'case'))) != array()) { + throw new InvalidArgumentException( + get_class($this) . ' does not accept ' . implode(', ', $extras) . ' as modifiers'); + } + if(DB::getConn() instanceof PostgreSQLDatabase) { + if(in_array('case', $modifiers)) { + $comparison = 'LIKE'; + } else { + $comparison = 'ILIKE'; + } + } elseif(in_array('case', $modifiers)) { + $comparison = 'LIKE BINARY'; + } else { + $comparison = 'LIKE'; + } + if($exclude) { + $comparison = 'NOT ' . $comparison; + } + return $comparison; + } /** * Applies a match on the trailing characters of a field value. @@ -28,7 +50,7 @@ class EndsWithFilter extends SearchFilter { return $query->where(sprintf( "%s %s '%%%s'", $this->getDbName(), - (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + $this->comparison(false), Convert::raw2sql($this->getValue()) )); } @@ -46,7 +68,7 @@ class EndsWithFilter extends SearchFilter { $connectives[] = sprintf( "%s %s '%%%s'", $this->getDbName(), - (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + $this->comparison(false), Convert::raw2sql($value) ); } @@ -64,7 +86,7 @@ class EndsWithFilter extends SearchFilter { return $query->where(sprintf( "%s NOT %s '%%%s'", $this->getDbName(), - (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + $this->comparison(true), Convert::raw2sql($this->getValue()) )); } @@ -82,7 +104,7 @@ class EndsWithFilter extends SearchFilter { $connectives[] = sprintf( "%s NOT %s '%%%s'", $this->getDbName(), - (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + $this->comparison(true), Convert::raw2sql($value) ); } diff --git a/search/filters/ExactMatchFilter.php b/search/filters/ExactMatchFilter.php index 496ab54a9..9cf3e9b1f 100644 --- a/search/filters/ExactMatchFilter.php +++ b/search/filters/ExactMatchFilter.php @@ -14,7 +14,35 @@ * @subpackage search */ class ExactMatchFilter extends SearchFilter { - + protected function comparison($exclude = false) { + $modifiers = $this->getModifiers(); + if(($extras = array_diff($modifiers, array('not', 'nocase', 'case'))) != array()) { + throw new InvalidArgumentException( + get_class($this) . ' does not accept ' . implode(', ', $extras) . ' as modifiers'); + } + if(!in_array('case', $modifiers) && !in_array('nocase', $modifiers)) { + if($exclude) { + return '!='; + } else { + return '='; + } + } elseif(DB::getConn() instanceof PostgreSQLDatabase) { + if(in_array('case', $modifiers)) { + $comparison = 'LIKE'; + } else { + $comparison = 'ILIKE'; + } + } elseif(in_array('case', $modifiers)) { + $comparison = 'LIKE BINARY'; + } else { + $comparison = 'LIKE'; + } + if($exclude) { + $comparison = 'NOT ' . $comparison; + } + return $comparison; + } + /** * Applies an exact match (equals) on a field value. * @@ -23,8 +51,9 @@ class ExactMatchFilter extends SearchFilter { protected function applyOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); return $query->where(sprintf( - "%s = '%s'", + "%s %s '%s'", $this->getDbName(), + $this->comparison(false), Convert::raw2sql($this->getValue()) )); } @@ -41,12 +70,26 @@ class ExactMatchFilter extends SearchFilter { foreach($this->getValue() as $value) { $values[] = Convert::raw2sql($value); } - $valueStr = "'" . implode("', '", $values) . "'"; - return $query->where(sprintf( - '%s IN (%s)', - $this->getDbName(), - $valueStr - )); + if($this->comparison(false) == '=') { + // Neither :case nor :nocase + $valueStr = "'" . implode("', '", $values) . "'"; + return $query->where(sprintf( + '%s IN (%s)', + $this->getDbName(), + $valueStr + )); + } else { + foreach($values as &$v) { + $v = sprintf( + "%s %s '%s'", + $this->getDbName(), + $this->comparison(false), + $v + ); + } + $where = implode(' OR ', $values); + return $query->where($where); + } } /** @@ -57,8 +100,9 @@ class ExactMatchFilter extends SearchFilter { protected function excludeOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); return $query->where(sprintf( - "%s != '%s'", + "%s %s '%s'", $this->getDbName(), + $this->comparison(true), Convert::raw2sql($this->getValue()) )); } @@ -75,12 +119,26 @@ class ExactMatchFilter extends SearchFilter { foreach($this->getValue() as $value) { $values[] = Convert::raw2sql($value); } - $valueStr = "'" . implode("', '", $values) . "'"; - return $query->where(sprintf( - '%s NOT IN (%s)', - $this->getDbName(), - $valueStr - )); + if($this->comparison(false) == '=') { + // Neither :case nor :nocase + $valueStr = "'" . implode("', '", $values) . "'"; + return $query->where(sprintf( + '%s NOT IN (%s)', + $this->getDbName(), + $valueStr + )); + } else { + foreach($values as &$v) { + $v = sprintf( + "%s %s '%s'", + $this->getDbName(), + $this->comparison(true), + $v + ); + } + $where = implode(' OR ', $values); + return $query->where($where); + } } public function isEmpty() { diff --git a/search/filters/ExactMatchMultiFilter.php b/search/filters/ExactMatchMultiFilter.php index 053b6c1a4..7edd7cd6f 100644 --- a/search/filters/ExactMatchMultiFilter.php +++ b/search/filters/ExactMatchMultiFilter.php @@ -13,9 +13,9 @@ * @subpackage search */ class ExactMatchMultiFilter extends SearchFilter { - function __construct($fullName, $value = false) { + function __construct($fullName, $value = false, array $modifiers = array()) { Deprecation::notice('3.1', 'Use ExactMatchFilter instead.'); - parent::__construct($fullName, $value); + parent::__construct($fullName, $value, $modifiers); } public function apply(DataQuery $query) { @@ -24,7 +24,7 @@ class ExactMatchMultiFilter extends SearchFilter { } else { $values = $this->getValue(); } - $filter = new ExactMatchFilter($this->getFullName(), $values); + $filter = new ExactMatchFilter($this->getFullName(), $values, $this->getModifiers()); return $filter->apply($query); } @@ -38,7 +38,7 @@ class ExactMatchMultiFilter extends SearchFilter { } else { $values = $this->getValue(); } - $filter = new ExactMatchFilter($this->getFullName(), $values); + $filter = new ExactMatchFilter($this->getFullName(), $values, $this->getModifiers()); return $filter->exclude($query); } diff --git a/search/filters/NegationFilter.php b/search/filters/NegationFilter.php index b131db4a5..9858a750f 100755 --- a/search/filters/NegationFilter.php +++ b/search/filters/NegationFilter.php @@ -2,28 +2,33 @@ /** * Matches on rows where the field is not equal to the given value. * + * @deprecated 3.1 Use ExactMatchFilter:not instead * @package framework * @subpackage search */ class NegationFilter extends SearchFilter { - // Deprecate this once modifiers are done + function __construct($fullName, $value = false, array $modifiers = array()) { + Deprecation::notice('3.1', 'Use ExactMatchFilter:not instead.'); + $modifiers[] = 'not'; + parent::__construct($fullName, $value, $modifiers); + } public function apply(DataQuery $query) { - $this->model = $query->applyRelation($this->relation); - return $query->where(sprintf( - "%s != '%s'", - $this->getDbName(), - Convert::raw2sql($this->getValue()) - )); + $filter = new ExactMatchFilter($this->getFullName(), $this->getValue(), $this->getModifiers()); + return $filter->apply($query); + } + + protected function applyOne(DataQuery $query) { + /* NO OP */ } public function exclude(DataQuery $query) { - $this->model = $query->applyRelation($this->relation); - return $query->where(sprintf( - "%s = '%s'", - $this->getDbName(), - Convert::raw2sql($this->getValue()) - )); + $filter = new ExactMatchFilter($this->getFullName(), $this->getValue(), $this->getModifiers()); + return $filter->exclude($query); + } + + protected function excludeOne(DataQuery $query) { + /* NO OP */ } } diff --git a/search/filters/PartialMatchFilter.php b/search/filters/PartialMatchFilter.php index 91582f745..73a4be1f2 100644 --- a/search/filters/PartialMatchFilter.php +++ b/search/filters/PartialMatchFilter.php @@ -11,10 +11,32 @@ * @subpackage search */ class PartialMatchFilter extends SearchFilter { + protected function comparison($exclude = false) { + $modifiers = $this->getModifiers(); + if(($extras = array_diff($modifiers, array('not', 'nocase', 'case'))) != array()) { + throw new InvalidArgumentException( + get_class($this) . ' does not accept ' . implode(', ', $extras) . ' as modifiers'); + } + if(DB::getConn() instanceof PostgreSQLDatabase) { + if(in_array('case', $modifiers)) { + $comparison = 'LIKE'; + } else { + $comparison = 'ILIKE'; + } + } elseif(in_array('case', $modifiers)) { + $comparison = 'LIKE BINARY'; + } else { + $comparison = 'LIKE'; + } + if($exclude) { + $comparison = 'NOT ' . $comparison; + } + return $comparison; + } protected function applyOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - $comparison = (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE'; + $comparison = $this->comparison(false); $where = sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($this->getValue())); return $query->where($where); @@ -23,7 +45,7 @@ class PartialMatchFilter extends SearchFilter { protected function applyMany(DataQuery $query) { $this->model = $query->applyRelation($this->relation); $where = array(); - $comparison = (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE'; + $comparison = $this->comparison(false); foreach($this->getValue() as $value) { $where[]= sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($value)); } @@ -33,8 +55,8 @@ class PartialMatchFilter extends SearchFilter { protected function excludeOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - $comparison = (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE'; - $where = sprintf("%s NOT %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($this->getValue())); + $comparison = $this->comparison(true); + $where = sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($this->getValue())); return $query->where($where); } @@ -42,9 +64,9 @@ class PartialMatchFilter extends SearchFilter { protected function excludeMany(DataQuery $query) { $this->model = $query->applyRelation($this->relation); $where = array(); - $comparison = (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE'; + $comparison = $this->comparison(true); foreach($this->getValue() as $value) { - $where[]= sprintf("%s NOT %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($value)); + $where[]= sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($value)); } return $query->where(implode(' AND ', $where)); diff --git a/search/filters/SearchFilter.php b/search/filters/SearchFilter.php index d8859a030..243fd9795 100644 --- a/search/filters/SearchFilter.php +++ b/search/filters/SearchFilter.php @@ -30,6 +30,11 @@ abstract class SearchFilter extends Object { */ protected $value; + /** + * @var array + */ + protected $modifiers; + /** * @var string Name of a has-one, has-many or many-many relation (not the classname). * Set in the constructor as part of the name in dot-notation, and used in @@ -43,12 +48,14 @@ abstract class SearchFilter extends Object { * the necessary tables (e.g. "Comments.Name" to join the "Comments" has-many relationship and * search the "Name" column when applying this filter to a SiteTree class). * @param mixed $value + * @param array $modifiers */ - public function __construct($fullName, $value = false) { + public function __construct($fullName, $value = false, array $modifiers = array()) { $this->fullName = $fullName; // sets $this->name and $this->relation $this->addRelation($fullName); $this->value = $value; + $this->modifiers = array_map('strtolower', $modifiers); } /** @@ -95,6 +102,24 @@ abstract class SearchFilter extends Object { public function getValue() { return $this->value; } + + /** + * Set the current modifiers to apply to the filter + * + * @param array $modifiers + */ + public function setModifiers(array $modifiers) { + $this->modifiers = array_map('strtolower', $modifiers); + } + + /** + * Accessor for the current modifiers to apply to the filter. + * + * @return array + */ + public function getModifiers() { + return $this->modifiers; + } /** * The original name of the field. @@ -177,6 +202,10 @@ abstract class SearchFilter extends Object { * @return DataQuery */ public function apply(DataQuery $query) { + if(($key = array_search('not', $this->modifiers)) !== false) { + unset($this->modifiers[$key]); + return $this->exclude($query); + } if(is_array($this->value)) { return $this->applyMany($query); } else { @@ -209,6 +238,10 @@ abstract class SearchFilter extends Object { * @return DataQuery */ public function exclude(DataQuery $query) { + if(($key = array_search('not', $this->modifiers)) !== false) { + unset($this->modifiers[$key]); + return $this->apply($query); + } if(is_array($this->value)) { return $this->excludeMany($query); } else { diff --git a/search/filters/StartsWithFilter.php b/search/filters/StartsWithFilter.php index cadb8ac53..c44086bc4 100644 --- a/search/filters/StartsWithFilter.php +++ b/search/filters/StartsWithFilter.php @@ -17,6 +17,28 @@ * @subpackage search */ class StartsWithFilter extends SearchFilter { + protected function comparison($exclude = false) { + $modifiers = $this->getModifiers(); + if(($extras = array_diff($modifiers, array('not', 'nocase', 'case'))) != array()) { + throw new InvalidArgumentException( + get_class($this) . ' does not accept ' . implode(', ', $extras) . ' as modifiers'); + } + if(DB::getConn() instanceof PostgreSQLDatabase) { + if(in_array('case', $modifiers)) { + $comparison = 'LIKE'; + } else { + $comparison = 'ILIKE'; + } + } elseif(in_array('case', $modifiers)) { + $comparison = 'LIKE BINARY'; + } else { + $comparison = 'LIKE'; + } + if($exclude) { + $comparison = 'NOT ' . $comparison; + } + return $comparison; + } /** * Applies a match on the starting characters of a field value. @@ -28,7 +50,7 @@ class StartsWithFilter extends SearchFilter { return $query->where(sprintf( "%s %s '%s%%'", $this->getDbName(), - (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + $this->comparison(false), Convert::raw2sql($this->getValue()) )); } @@ -46,7 +68,7 @@ class StartsWithFilter extends SearchFilter { $connectives[] = sprintf( "%s %s '%s%%'", $this->getDbName(), - (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + $this->comparison(false), Convert::raw2sql($value) ); } @@ -62,9 +84,9 @@ class StartsWithFilter extends SearchFilter { protected function excludeOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); return $query->where(sprintf( - "%s NOT %s '%s%%'", + "%s %s '%s%%'", $this->getDbName(), - (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + $this->comparison(true), Convert::raw2sql($this->getValue()) )); } @@ -80,9 +102,9 @@ class StartsWithFilter extends SearchFilter { $connectives = array(); foreach($this->getValue() as $value) { $connectives[] = sprintf( - "%s NOT %s '%s%%'", + "%s %s '%s%%'", $this->getDbName(), - (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', + $this->comparison(true), Convert::raw2sql($value) ); } diff --git a/search/filters/StartsWithMultiFilter.php b/search/filters/StartsWithMultiFilter.php index 4a67c8326..f0cd71e95 100644 --- a/search/filters/StartsWithMultiFilter.php +++ b/search/filters/StartsWithMultiFilter.php @@ -13,9 +13,9 @@ * @subpackage search */ class StartsWithMultiFilter extends SearchFilter { - function __construct($fullName, $value = false) { + function __construct($fullName, $value = false, array $modifiers = array()) { Deprecation::notice('3.1', 'Use StartsWithFilter instead.'); - parent::__construct($fullName, $value); + parent::__construct($fullName, $value, $modifiers); } public function apply(DataQuery $query) { @@ -24,7 +24,7 @@ class StartsWithMultiFilter extends SearchFilter { } else { $values = $this->getValue(); } - $filter = new StartsWithFilter($this->getFullName(), $values); + $filter = new StartsWithFilter($this->getFullName(), $values, $this->getModifiers()); return $filter->apply($query); } @@ -38,7 +38,7 @@ class StartsWithMultiFilter extends SearchFilter { } else { $values = $this->getValue(); } - $filter = new StartsWithFilter($this->getFullName(), $values); + $filter = new StartsWithFilter($this->getFullName(), $values, $this->getModifiers()); return $filter->exclude($query); } diff --git a/search/filters/SubstringFilter.php b/search/filters/SubstringFilter.php index a0b34974f..470963443 100755 --- a/search/filters/SubstringFilter.php +++ b/search/filters/SubstringFilter.php @@ -12,14 +12,14 @@ * @subpackage search */ class SubstringFilter extends PartialMatchFilter { - public function __construct($fullName, $value = false) { + public function __construct($fullName, $value = false, array $modifiers = array()) { Deprecation::notice('3.0', 'PartialMatchFilter instead.'); - parent::__construct($fullName, $value); + parent::__construct($fullName, $value, $modifiers); } public function apply(DataQuery $query) { $values = $this->getValue(); - $filter = new PartialMatchFilter($this->getFullName(), $values); + $filter = new PartialMatchFilter($this->getFullName(), $values, $this->getModifiers()); return $filter->apply($query); } @@ -29,7 +29,7 @@ class SubstringFilter extends PartialMatchFilter { public function exclude(DataQuery $query) { $values = $this->getValue(); - $filter = new PartialMatchFilter($this->getFullName(), $values); + $filter = new PartialMatchFilter($this->getFullName(), $values, $this->getModifiers()); return $filter->exclude($query); } diff --git a/tests/model/DataListTest.php b/tests/model/DataListTest.php index bfadbb20c..59b36850d 100644 --- a/tests/model/DataListTest.php +++ b/tests/model/DataListTest.php @@ -442,10 +442,21 @@ class DataListTest extends SapphireTest { $list = $list->filter(array( 'Name'=>array('Bob','Phil'), 'TeamID'=>array($this->idFromFixture('DataObjectTest_Team', 'team1')))); - $this->assertEquals(1, $list->count(), 'There should be one comments'); + $this->assertEquals(1, $list->count(), 'There should be one comment'); $this->assertEquals('Bob', $list->first()->Name, 'Only comment should be from Bob'); } + public function testFilterWithModifiers() { + $list = DataObjectTest_TeamComment::get(); + $nocaseList = $list->filter('Name:nocase', 'bob'); + $this->assertEquals(1, $nocaseList->count(), 'There should be one comment'); + $caseList = $list->filter('Name:case', 'bob'); + $this->assertEquals(0, $caseList->count(), 'There should be no comments'); + $gtList = $list->filter('TeamID:GreaterThan:not', + $this->idFromFixture('DataObjectTest_Team', 'team1')); + $this->assertEquals(2, $gtList->count()); + } + public function testFilterAndExcludeById() { $id = $this->idFromFixture('DataObjectTest_SubTeam', 'subteam1'); $list = DataObjectTest_SubTeam::get()->filter('ID', $id);