diff --git a/model/Database.php b/model/Database.php index f439da84c..7ede4e631 100644 --- a/model/Database.php +++ b/model/Database.php @@ -883,6 +883,18 @@ abstract class SS_Database { return "'" . Convert::raw2sql($string) . "'"; } + /** + * Generate a WHERE clause for text matching. + * + * @param String $field Quoted field name + * @param String $value Escaped search. Can include percentage wildcards. + * @param boolean $exact Exact matches or wildcard support. + * @param boolean $negate Negate the clause. + * @param boolean $caseSensitive Perform case sensitive search. + * @return String SQL + */ + abstract public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = false); + /** * function to return an SQL datetime expression that can be used with the adapter in use * used for querying a datetime in a certain format diff --git a/model/MySQLDatabase.php b/model/MySQLDatabase.php index 43b74d12d..c0069bead 100644 --- a/model/MySQLDatabase.php +++ b/model/MySQLDatabase.php @@ -1056,6 +1056,28 @@ class MySQLDatabase extends SS_Database { $this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN;'); } + /** + * Generate a WHERE clause for text matching. + * + * @param String $field Quoted field name + * @param String $value Escaped search. Can include percentage wildcards. + * @param boolean $exact Exact matches or wildcard support. + * @param boolean $negate Negate the clause. + * @param boolean $caseSensitive Enforce case sensitivity if TRUE or FALSE. + * Stick with default collation if set to NULL. + * @return String SQL + */ + public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null) { + if($exact && $caseSensitive === null) { + $comp = ($negate) ? '!=' : '='; + } else { + $comp = ($caseSensitive) ? 'LIKE BINARY' : 'LIKE'; + if($negate) $comp = 'NOT ' . $comp; + } + + return sprintf("%s %s '%s'", $field, $comp, $value); + } + /** * function to return an SQL datetime expression that can be used with MySQL * used for querying a datetime in a certain format diff --git a/search/filters/EndsWithFilter.php b/search/filters/EndsWithFilter.php index 83444f320..05bf258ed 100644 --- a/search/filters/EndsWithFilter.php +++ b/search/filters/EndsWithFilter.php @@ -17,27 +17,14 @@ * @subpackage search */ class EndsWithFilter extends SearchFilter { - protected function comparison($exclude = false) { - $modifiers = $this->getModifiers(); + + public function setModifiers(array $modifiers) { 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; + + parent::setModifiers($modifiers); } /** @@ -47,12 +34,15 @@ class EndsWithFilter extends SearchFilter { */ protected function applyOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - return $query->where(sprintf( - "%s %s '%%%s'", + $modifiers = $this->getModifiers(); + $where = DB::getConn()->comparisonClause( $this->getDbName(), - $this->comparison(false), - Convert::raw2sql($this->getValue()) - )); + '%' . Convert::raw2sql($this->getValue()), + false, // exact? + false, // negate? + $this->getCaseSensitive() + ); + return $query->where($where); } /** @@ -63,13 +53,15 @@ class EndsWithFilter extends SearchFilter { */ protected function applyMany(DataQuery $query) { $this->model = $query->applyRelation($this->relation); + $modifiers = $this->getModifiers(); $connectives = array(); foreach($this->getValue() as $value) { - $connectives[] = sprintf( - "%s %s '%%%s'", + $connectives[] = DB::getConn()->comparisonClause( $this->getDbName(), - $this->comparison(false), - Convert::raw2sql($value) + '%' . Convert::raw2sql($value), + false, // exact? + false, // negate? + $this->getCaseSensitive() ); } $whereClause = implode(' OR ', $connectives); @@ -83,12 +75,15 @@ class EndsWithFilter extends SearchFilter { */ protected function excludeOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - return $query->where(sprintf( - "%s NOT %s '%%%s'", + $modifiers = $this->getModifiers(); + $where = DB::getConn()->comparisonClause( $this->getDbName(), - $this->comparison(true), - Convert::raw2sql($this->getValue()) - )); + '%' . Convert::raw2sql($this->getValue()), + false, // exact? + true, // negate? + $this->getCaseSensitive() + ); + return $query->where($where); } /** @@ -99,13 +94,15 @@ class EndsWithFilter extends SearchFilter { */ protected function excludeMany(DataQuery $query) { $this->model = $query->applyRelation($this->relation); + $modifiers = $this->getModifiers(); $connectives = array(); foreach($this->getValue() as $value) { - $connectives[] = sprintf( - "%s NOT %s '%%%s'", + $connectives[] = DB::getConn()->comparisonClause( $this->getDbName(), - $this->comparison(true), - Convert::raw2sql($value) + '%' . Convert::raw2sql($value), + false, // exact? + true, // negate? + $this->getCaseSensitive() ); } $whereClause = implode(' AND ', $connectives); diff --git a/search/filters/ExactMatchFilter.php b/search/filters/ExactMatchFilter.php index 9cf3e9b1f..f47e8d0c9 100644 --- a/search/filters/ExactMatchFilter.php +++ b/search/filters/ExactMatchFilter.php @@ -14,33 +14,14 @@ * @subpackage search */ class ExactMatchFilter extends SearchFilter { - protected function comparison($exclude = false) { - $modifiers = $this->getModifiers(); + + public function setModifiers(array $modifiers) { 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; + + parent::setModifiers($modifiers); } /** @@ -50,12 +31,15 @@ class ExactMatchFilter extends SearchFilter { */ protected function applyOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - return $query->where(sprintf( - "%s %s '%s'", + $modifiers = $this->getModifiers(); + $where = DB::getConn()->comparisonClause( $this->getDbName(), - $this->comparison(false), - Convert::raw2sql($this->getValue()) - )); + Convert::raw2sql($this->getValue()), + true, // exact? + false, // negate? + $this->getCaseSensitive() + ); + return $query->where($where); } /** @@ -66,12 +50,12 @@ class ExactMatchFilter extends SearchFilter { */ protected function applyMany(DataQuery $query) { $this->model = $query->applyRelation($this->relation); + $modifiers = $this->getModifiers(); $values = array(); foreach($this->getValue() as $value) { $values[] = Convert::raw2sql($value); } - if($this->comparison(false) == '=') { - // Neither :case nor :nocase + if(!in_array('case', $modifiers) && !in_array('nocase', $modifiers)) { $valueStr = "'" . implode("', '", $values) . "'"; return $query->where(sprintf( '%s IN (%s)', @@ -80,11 +64,12 @@ class ExactMatchFilter extends SearchFilter { )); } else { foreach($values as &$v) { - $v = sprintf( - "%s %s '%s'", + $v = DB::getConn()->comparisonClause( $this->getDbName(), - $this->comparison(false), - $v + $v, + true, // exact? + false, // negate? + $this->getCaseSensitive() ); } $where = implode(' OR ', $values); @@ -99,12 +84,15 @@ class ExactMatchFilter extends SearchFilter { */ protected function excludeOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - return $query->where(sprintf( - "%s %s '%s'", + $modifiers = $this->getModifiers(); + $where = DB::getConn()->comparisonClause( $this->getDbName(), - $this->comparison(true), - Convert::raw2sql($this->getValue()) - )); + Convert::raw2sql($this->getValue()), + true, // exact? + true, // negate? + $this->getCaseSensitive() + ); + return $query->where($where); } /** @@ -115,12 +103,12 @@ class ExactMatchFilter extends SearchFilter { */ protected function excludeMany(DataQuery $query) { $this->model = $query->applyRelation($this->relation); + $modifiers = $this->getModifiers(); $values = array(); foreach($this->getValue() as $value) { $values[] = Convert::raw2sql($value); } - if($this->comparison(false) == '=') { - // Neither :case nor :nocase + if(!in_array('case', $modifiers) && !in_array('nocase', $modifiers)) { $valueStr = "'" . implode("', '", $values) . "'"; return $query->where(sprintf( '%s NOT IN (%s)', @@ -129,11 +117,12 @@ class ExactMatchFilter extends SearchFilter { )); } else { foreach($values as &$v) { - $v = sprintf( - "%s %s '%s'", + $v = DB::getConn()->comparisonClause( $this->getDbName(), - $this->comparison(true), - $v + $v, + true, // exact? + true, // negate? + $this->getCaseSensitive() ); } $where = implode(' OR ', $values); diff --git a/search/filters/PartialMatchFilter.php b/search/filters/PartialMatchFilter.php index 73a4be1f2..2a884797c 100644 --- a/search/filters/PartialMatchFilter.php +++ b/search/filters/PartialMatchFilter.php @@ -11,33 +11,26 @@ * @subpackage search */ class PartialMatchFilter extends SearchFilter { - protected function comparison($exclude = false) { - $modifiers = $this->getModifiers(); + + public function setModifiers(array $modifiers) { 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; + + parent::setModifiers($modifiers); } protected function applyOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - $comparison = $this->comparison(false); - $where = sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($this->getValue())); + $modifiers = $this->getModifiers(); + $where = DB::getConn()->comparisonClause( + $this->getDbName(), + '%' . Convert::raw2sql($this->getValue()) . '%', + false, // exact? + false, // negate? + $this->getCaseSensitive() + ); return $query->where($where); } @@ -45,9 +38,15 @@ class PartialMatchFilter extends SearchFilter { protected function applyMany(DataQuery $query) { $this->model = $query->applyRelation($this->relation); $where = array(); - $comparison = $this->comparison(false); + $modifiers = $this->getModifiers(); foreach($this->getValue() as $value) { - $where[]= sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($value)); + $where[]= DB::getConn()->comparisonClause( + $this->getDbName(), + '%' . Convert::raw2sql($value) . '%', + false, // exact? + false, // negate? + $this->getCaseSensitive() + ); } return $query->where(implode(' OR ', $where)); @@ -55,8 +54,14 @@ class PartialMatchFilter extends SearchFilter { protected function excludeOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - $comparison = $this->comparison(true); - $where = sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($this->getValue())); + $modifiers = $this->getModifiers(); + $where = DB::getConn()->comparisonClause( + $this->getDbName(), + '%' . Convert::raw2sql($this->getValue()) . '%', + false, // exact? + true, // negate? + $this->getCaseSensitive() + ); return $query->where($where); } @@ -64,9 +69,15 @@ class PartialMatchFilter extends SearchFilter { protected function excludeMany(DataQuery $query) { $this->model = $query->applyRelation($this->relation); $where = array(); - $comparison = $this->comparison(true); + $modifiers = $this->getModifiers(); foreach($this->getValue() as $value) { - $where[]= sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($value)); + $where[]= DB::getConn()->comparisonClause( + $this->getDbName(), + '%' . Convert::raw2sql($value) . '%', + false, // exact? + true, // negate? + $this->getCaseSensitive() + ); } return $query->where(implode(' AND ', $where)); diff --git a/search/filters/SearchFilter.php b/search/filters/SearchFilter.php index 243fd9795..24fc729fe 100644 --- a/search/filters/SearchFilter.php +++ b/search/filters/SearchFilter.php @@ -55,7 +55,7 @@ abstract class SearchFilter extends Object { // sets $this->name and $this->relation $this->addRelation($fullName); $this->value = $value; - $this->modifiers = array_map('strtolower', $modifiers); + $this->setModifiers($modifiers); } /** @@ -278,5 +278,17 @@ abstract class SearchFilter extends Object { public function isEmpty() { return false; } - -} + + /** + * Determines case sensitivity based on {@link getModifiers()}. + * + * @return Mixed TRUE or FALSE to enforce sensitivity, NULL to use field collation. + */ + protected function getCaseSensitive() { + $modifiers = $this->getModifiers(); + if(in_array('case', $modifiers)) return true; + else if(in_array('nocase', $modifiers)) return false; + else return null; + } + +} \ No newline at end of file diff --git a/search/filters/StartsWithFilter.php b/search/filters/StartsWithFilter.php index c44086bc4..cdf445139 100644 --- a/search/filters/StartsWithFilter.php +++ b/search/filters/StartsWithFilter.php @@ -17,27 +17,14 @@ * @subpackage search */ class StartsWithFilter extends SearchFilter { - protected function comparison($exclude = false) { - $modifiers = $this->getModifiers(); + + public function setModifiers(array $modifiers) { 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; + + parent::setModifiers($modifiers); } /** @@ -47,12 +34,15 @@ class StartsWithFilter extends SearchFilter { */ protected function applyOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - return $query->where(sprintf( - "%s %s '%s%%'", + $modifiers = $this->getModifiers(); + $where = DB::getConn()->comparisonClause( $this->getDbName(), - $this->comparison(false), - Convert::raw2sql($this->getValue()) - )); + Convert::raw2sql($this->getValue()) . '%', + false, // exact? + false, // negate? + $this->getCaseSensitive() + ); + return $query->where($where); } /** @@ -63,13 +53,15 @@ class StartsWithFilter extends SearchFilter { */ protected function applyMany(DataQuery $query) { $this->model = $query->applyRelation($this->relation); + $modifiers = $this->getModifiers(); $connectives = array(); foreach($this->getValue() as $value) { - $connectives[] = sprintf( - "%s %s '%s%%'", + $connectives[] = DB::getConn()->comparisonClause( $this->getDbName(), - $this->comparison(false), - Convert::raw2sql($value) + Convert::raw2sql($value) . '%', + false, // exact? + false, // negate? + $this->getCaseSensitive() ); } $whereClause = implode(' OR ', $connectives); @@ -83,12 +75,15 @@ class StartsWithFilter extends SearchFilter { */ protected function excludeOne(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - return $query->where(sprintf( - "%s %s '%s%%'", + $modifiers = $this->getModifiers(); + $where = DB::getConn()->comparisonClause( $this->getDbName(), - $this->comparison(true), - Convert::raw2sql($this->getValue()) - )); + Convert::raw2sql($this->getValue()) . '%', + false, // exact? + true, // negate? + $this->getCaseSensitive() + ); + return $query->where($where); } /** @@ -99,13 +94,15 @@ class StartsWithFilter extends SearchFilter { */ protected function excludeMany(DataQuery $query) { $this->model = $query->applyRelation($this->relation); + $modifiers = $this->getModifiers(); $connectives = array(); foreach($this->getValue() as $value) { - $connectives[] = sprintf( - "%s %s '%s%%'", + $connectives[] = DB::getConn()->comparisonClause( $this->getDbName(), - $this->comparison(true), - Convert::raw2sql($value) + Convert::raw2sql($value) . '%', + false, // exact? + true, // negate? + $this->getCaseSensitive() ); } $whereClause = implode(' AND ', $connectives);