1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-10 16:54:44 +02:00

Various fixes and improvements related to Selectors and the find operators

This commit is contained in:
Ryan Cramer
2020-07-06 14:39:49 -04:00
parent 6acb8028e3
commit 5b285ebc8c
9 changed files with 369 additions and 144 deletions

View File

@@ -37,7 +37,7 @@ class DatabaseQuerySelectFulltext extends Wire {
const maxQueryValueLength = 500; const maxQueryValueLength = 500;
/** /**
* @var DatabaseQuerySelect * @var DatabaseQuerySelect|PageFinderDatabaseQuerySelect
* *
*/ */
protected $query; protected $query;
@@ -69,6 +69,8 @@ class DatabaseQuerySelectFulltext extends Wire {
/** /**
* Is it a NOT operator? * Is it a NOT operator?
* *
* This is not used by PageFinder originating queries, which handles NOT internally.
*
* @var bool * @var bool
* *
*/ */
@@ -82,6 +84,22 @@ class DatabaseQuerySelectFulltext extends Wire {
*/ */
protected $minWordLength = null; protected $minWordLength = null;
/**
* Allow adding 'ORDER BY' to query?
*
* @var bool|null
*
*/
protected $allowOrder = null;
/**
* Allow fulltext searches to fallback to LIKE searches to match stopwords?
*
* @var bool
*
*/
protected $allowStopwords = true;
/** /**
* @var array * @var array
* *
@@ -110,10 +128,11 @@ class DatabaseQuerySelectFulltext extends Wire {
/** /**
* Construct * Construct
* *
* @param DatabaseQuerySelect $query * @param DatabaseQuerySelect|PageFinderDatabaseQuerySelect $query
* *
*/ */
public function __construct(DatabaseQuerySelect $query) { public function __construct(DatabaseQuerySelect $query) {
$query->wire($this);
$this->query = $query; $this->query = $query;
} }
@@ -148,6 +167,40 @@ class DatabaseQuerySelectFulltext extends Wire {
return "$this->tableName.$this->fieldName"; return "$this->tableName.$this->fieldName";
} }
/**
* Get or set whether or not 'ORDER BY' statements are allowed to be added
*
* @param null|bool $allow Specify bool to set or omit to get
* @return bool|null Returns bool when known or null when not yet known
* @since 3.0.162
*
*/
public function allowOrder($allow = null) {
if($allow !== null) $this->allowOrder = $allow ? true : false;
return $this->allowOrder;
}
/**
* Get or set whether fulltext searches can fallback to LIKE searches to match stopwords
*
* @param null|bool $allow Specify bool to set or omit to get
* @return bool
* @since 3.0.162
*
*/
public function allowStopwords($allow = null) {
if($allow !== null) $this->allowStopwords = $allow ? true : false;
return $this->allowStopwords;
}
/**
* @return string
*
*/
protected function matchType() {
return "\n " . ($this->not ? 'NOT MATCH' : 'MATCH');
}
/** /**
* Escape string for use in a MySQL LIKE * Escape string for use in a MySQL LIKE
* *
@@ -204,12 +257,20 @@ class DatabaseQuerySelectFulltext extends Wire {
$this->tableName = $this->database->escapeTable($tableName); $this->tableName = $this->database->escapeTable($tableName);
$this->fieldName = $this->database->escapeCol($fieldName); $this->fieldName = $this->database->escapeCol($fieldName);
$allowOrder = true;
if(strpos($operator, '!') === 0 && $operator !== '!=') { if(strpos($operator, '!') === 0 && $operator !== '!=') {
$this->not = true; $this->not = true;
$operator = ltrim($operator, '!'); $operator = ltrim($operator, '!');
} else {
// disable orderby statements when calling object will be negating whatever we do
$selector = $this->query->selector;
if($selector && $selector instanceof Selector && $selector->not) $allowOrder = false;
} }
// if allowOrder has not been specifically set, then set value now
if($this->allowOrder === null) $this->allowOrder = $allowOrder;
$this->operator = $operator; $this->operator = $operator;
foreach($this->methodOperators as $name => $operators) { foreach($this->methodOperators as $name => $operators) {
@@ -275,7 +336,8 @@ class DatabaseQuerySelectFulltext extends Wire {
* *
*/ */
protected function matchEquals($value) { protected function matchEquals($value) {
$this->query->where("$this->tableField$this->operator?", $value); $op = $this->wire()->database->escapeOperator($this->operator, WireDatabasePDO::operatorTypeComparison);
$this->query->where("$this->tableField$op?", $value);
} }
/** /**
@@ -367,9 +429,10 @@ class DatabaseQuerySelectFulltext extends Wire {
$partial = strpos($operator, '*') !== false; $partial = strpos($operator, '*') !== false;
$partialLast = $operator === '~~='; $partialLast = $operator === '~~=';
$expand = strpos($operator, '+') !== false; $expand = strpos($operator, '+') !== false;
$matchType = $this->not ? 'NOT MATCH' : 'MATCH'; $matchType = $this->matchType();
$scoreField = $this->getScoreFieldName(); $scoreField = $this->getScoreFieldName();
$matchAgainst = ''; $matchAgainst = '';
$wheres = array();
$data = $this->getBooleanModeWords($value, array( $data = $this->getBooleanModeWords($value, array(
'required' => $required, 'required' => $required,
@@ -379,17 +442,26 @@ class DatabaseQuerySelectFulltext extends Wire {
'alternates' => $expand, 'alternates' => $expand,
)); ));
if(empty($data['value'])) {
// query contains no indexable words: force non-match
//$this->query->where('1>2');
//return;
// TEST OUT: title|summary~|+=beer
}
if($expand) { if($expand) {
if(!empty($data['booleanValue'])) { if(!empty($data['booleanValue']) && $this->allowOrder) {
// ensure full matches are above expanded matches // ensure full matches are above expanded matches
$preScoreField = $this->getScoreFieldName(); $preScoreField = $this->getScoreFieldName();
$bindKey = $this->query->bindValueGetKey($data['booleanValue']); $bindKey = $this->query->bindValueGetKey($data['booleanValue']);
$this->query->select("$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE) + 111.1 AS $preScoreField"); $this->query->select("$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE) + 111.1 AS $preScoreField");
$this->query->orderby("$preScoreField DESC"); $this->query->orderby("$preScoreField DESC");
} }
$bindValue = trim($data['value'] . ' ' . implode(' ', $data['altWords'])); if(!empty($data['matchValue'])) {
$bindKey = $this->query->bindValueGetKey($this->escapeAgainst($bindValue)); $bindValue = trim($data['matchValue']);
$matchAgainst = "$matchType($tableField) AGAINST($bindKey WITH QUERY EXPANSION)"; $bindKey = $this->query->bindValueGetKey($this->escapeAgainst($bindValue));
$matchAgainst = "$matchType($tableField) AGAINST($bindKey WITH QUERY EXPANSION)";
}
} else if(!empty($data['booleanValue'])) { } else if(!empty($data['booleanValue'])) {
$bindKey = $this->query->bindValueGetKey($data['booleanValue']); $bindKey = $this->query->bindValueGetKey($data['booleanValue']);
@@ -397,16 +469,26 @@ class DatabaseQuerySelectFulltext extends Wire {
} }
if($matchAgainst) { if($matchAgainst) {
$this->query->where($matchAgainst); $wheres[] = $matchAgainst;
$this->query->select("$matchAgainst AS $scoreField"); // $this->query->where($matchAgainst);
$this->query->orderby("$scoreField DESC"); if($this->allowOrder) {
$this->query->select("$matchAgainst AS $scoreField");
$this->query->orderby("$scoreField DESC");
}
} else if(!$this->allowStopwords) {
// no match possible
// $this->query->where('1>2');
$wheres[] = '1>2';
} }
if(!empty($data['likeWords'])) { if(!empty($data['likeWords'])) {
// stopwords or words that were too short to use fulltext index // stopwords or words that were too short to use fulltext index
$wheres = array();
$likeType = $this->not ? 'NOT RLIKE' : 'RLIKE'; $likeType = $this->not ? 'NOT RLIKE' : 'RLIKE';
$orLikes = array();
$andLikes = array();
foreach($data['likeWords'] as $word) { foreach($data['likeWords'] as $word) {
$isStopword = isset($data['stopWords'][$word]);
if($isStopword && !$this->allowStopwords) continue;
$word = $this->escapeLike($word); $word = $this->escapeLike($word);
if(!strlen($word)) continue; if(!strlen($word)) continue;
$likeValue = '([[:blank:]]|[[:punct:]]|[[:space:]]|>|^)' . preg_quote($word); $likeValue = '([[:blank:]]|[[:punct:]]|[[:space:]]|>|^)' . preg_quote($word);
@@ -417,12 +499,27 @@ class DatabaseQuerySelectFulltext extends Wire {
$likeValue .= '([[:blank:]]|[[:punct:]]|[[:space:]]|<|$)'; $likeValue .= '([[:blank:]]|[[:punct:]]|[[:space:]]|<|$)';
} }
$bindKey = $this->query->bindValueGetKey($likeValue); $bindKey = $this->query->bindValueGetKey($likeValue);
$wheres[] = "($tableField $likeType $bindKey)"; $likeWhere = "($tableField $likeType $bindKey)";
if(!$required || ($isStopword && $expand)) {
$orLikes[] = $likeWhere;
} else {
$andLikes[] = $likeWhere;
}
} }
if(count($wheres)) { $whereLike = '';
$and = $required ? ' AND ' : ' OR '; if(count($orLikes)) {
$this->query->where(implode($and, $wheres)); $whereLike .= '(' . implode(' OR ', $orLikes) . ')';
if(count($andLikes)) $whereLike .= $required ? ' AND ' : ' OR ';
} }
if(count($andLikes)) {
$whereLike .= implode(' AND ', $andLikes);
}
if($whereLike) $wheres[] = $whereLike;
}
if(count($wheres)) {
$and = $required ? ' AND ' : ' OR ';
$this->query->where('(' . implode($and, $wheres) . ')');
} }
} }
@@ -435,7 +532,6 @@ class DatabaseQuerySelectFulltext extends Wire {
protected function matchPhrase($value) { protected function matchPhrase($value) {
$tableField = $this->tableField(); $tableField = $this->tableField();
$not = strpos($this->operator, '!') === 0;
$likeValue = ''; $likeValue = '';
$words = $this->words($value); $words = $this->words($value);
$lastWord = count($words) > 1 ? array_pop($words) : ''; $lastWord = count($words) > 1 ? array_pop($words) : '';
@@ -460,10 +556,10 @@ class DatabaseQuerySelectFulltext extends Wire {
if($lastWord !== '' || !strlen($againstValue)) { if($lastWord !== '' || !strlen($againstValue)) {
// match entire phrase with LIKE as secondary qualifier that includes last word // match entire phrase with LIKE as secondary qualifier that includes last word
// so that we can perform a partial match on the last word only. This is necessary // so that we can perform a partial match on the last word only. This is necessary
// because we cant use partial match qualifiers in or out of quoted phrases // because we cant use partial match qualifiers in or out of quoted phrases.
// if word is indexable let it contribute to final score
$lastWord = strlen($lastWord) ? $this->escapeAgainst($lastWord) : ''; $lastWord = strlen($lastWord) ? $this->escapeAgainst($lastWord) : '';
if(strlen($lastWord) && $this->isIndexableWord($lastWord)) { if(strlen($lastWord) && $this->isIndexableWord($lastWord)) {
// if word is indexable let it contribute to final score
// expand the againstValue to include the last word as a required partial match // expand the againstValue to include the last word as a required partial match
$againstValue = trim("$againstValue +$lastWord*"); $againstValue = trim("$againstValue +$lastWord*");
} }
@@ -473,19 +569,22 @@ class DatabaseQuerySelectFulltext extends Wire {
if(strlen($againstValue)) { if(strlen($againstValue)) {
// use MATCH/AGAINST // use MATCH/AGAINST
$bindKey = $this->query->bindValueGetKey($againstValue); $bindKey = $this->query->bindValueGetKey($againstValue);
$match = $not ? 'NOT MATCH' : 'MATCH'; $matchType = $this->matchType();
$matchAgainst = "$match($tableField) AGAINST($bindKey IN BOOLEAN MODE)"; $matchAgainst = "$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE)";
$scoreField = $this->getScoreFieldName();
$this->query->select("$matchAgainst AS $scoreField");
$this->query->where($matchAgainst); $this->query->where($matchAgainst);
$this->query->orderby("$scoreField DESC");
if($this->allowOrder) {
$scoreField = $this->getScoreFieldName();
$this->query->select("$matchAgainst AS $scoreField");
$this->query->orderby("$scoreField DESC");
}
} }
if(strlen($likeValue)) { if(strlen($likeValue)) {
// LIKE is used as a secondary qualifier to MATCH/AGAINST so that it is // LIKE is used as a secondary qualifier to MATCH/AGAINST so that it is
// performed only on rows already identified from FULLTEXT index, unless // performed only on rows already identified from FULLTEXT index, unless
// no MATCH/AGAINST could be created due to stopwords or too-short words // no MATCH/AGAINST could be created due to stopwords or too-short words
$likeType = $not ? 'NOT RLIKE' : 'RLIKE'; $likeType = $this->not ? 'NOT RLIKE' : 'RLIKE';
$this->query->where("($tableField $likeType ?)", $likeValue); $this->query->where("($tableField $likeType ?)", $likeValue);
} }
} }
@@ -499,8 +598,7 @@ class DatabaseQuerySelectFulltext extends Wire {
protected function matchPhraseExpand($value) { protected function matchPhraseExpand($value) {
$tableField = $this->tableField(); $tableField = $this->tableField();
$not = strpos($this->operator, '!') === 0; $matchType = $this->matchType();
$matchType = $not ? "\nNOT MATCH" : "\nMATCH";
$words = $this->words($value, array('indexable' => true)); $words = $this->words($value, array('indexable' => true));
$wordsAlternates = array(); $wordsAlternates = array();
@@ -509,8 +607,11 @@ class DatabaseQuerySelectFulltext extends Wire {
$againstValue = '+"' . $this->escapeAgainst($value) . '*"'; $againstValue = '+"' . $this->escapeAgainst($value) . '*"';
$bindKey = $this->query->bindValueGetKey($againstValue); $bindKey = $this->query->bindValueGetKey($againstValue);
$matchAgainst = "$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE)"; $matchAgainst = "$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE)";
$this->query->select("$matchAgainst + 333.3 AS $scoreField");
$this->query->orderby("$scoreField DESC"); if($this->allowOrder) {
$this->query->select("$matchAgainst + 333.3 AS $scoreField");
$this->query->orderby("$scoreField DESC");
}
if(!count($words)) { if(!count($words)) {
// no words to work with for query expansion (not likely, unless stopwords or too-short) // no words to work with for query expansion (not likely, unless stopwords or too-short)
@@ -542,19 +643,26 @@ class DatabaseQuerySelectFulltext extends Wire {
} }
$againstValue .= ") "; $againstValue .= ") ";
} }
$bindKey = $this->query->bindValueGetKey(trim($againstValue));
$this->query->select("$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE) + 222.2 AS $scoreField"); if($this->allowOrder && strlen($againstValue)) {
$this->query->orderby("$scoreField DESC"); $bindKey = $this->query->bindValueGetKey(trim($againstValue));
$this->query->select("$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE) + 222.2 AS $scoreField");
$this->query->orderby("$scoreField DESC");
}
// QUERY EXPANSION: regular match/against words with query expansion // QUERY EXPANSION: regular match/against words with query expansion
$words = array_unique(array_merge($words, $wordsAlternates)); $words = array_unique(array_merge($words, $wordsAlternates));
$againstValue = $this->escapeAgainst(implode(' ', $words)); $againstValue = $this->escapeAgainst(implode(' ', $words));
$bindKey = $this->query->bindValueGetKey($againstValue); $bindKey = $this->query->bindValueGetKey($againstValue);
$matchAgainst = "$matchType($tableField) AGAINST($bindKey WITH QUERY EXPANSION)"; $matchAgainst = "$matchType($tableField) AGAINST($bindKey WITH QUERY EXPANSION)";
$scoreField = $this->getScoreFieldName();
$this->query->where($matchAgainst); $this->query->where($matchAgainst);
$scoreField = $this->getScoreFieldName();
$this->query->select("$matchAgainst AS $scoreField"); $this->query->select("$matchAgainst AS $scoreField");
$this->query->orderby("$scoreField DESC");
if($this->allowOrder) {
$this->query->orderby("$scoreField DESC");
}
} }
/** /**
@@ -570,9 +678,9 @@ class DatabaseQuerySelectFulltext extends Wire {
$tableField = $this->tableField(); $tableField = $this->tableField();
$expand = strpos($this->operator, '+') !== false; $expand = strpos($this->operator, '+') !== false;
$matchType = $this->not ? 'NOT MATCH' : 'MATCH'; $matchType = $this->matchType();
if($expand) { if($expand && $this->allowOrder) {
// boolean mode query for sorting purposes // boolean mode query for sorting purposes
$scoreField = $this->getScoreFieldName(); $scoreField = $this->getScoreFieldName();
$data = $this->getBooleanModeWords($value, array( $data = $this->getBooleanModeWords($value, array(
@@ -595,7 +703,7 @@ class DatabaseQuerySelectFulltext extends Wire {
$againstValue = $this->escapeAgainst(implode(' ', $words)); $againstValue = $this->escapeAgainst(implode(' ', $words));
if(!count($words) || !strlen(trim($againstValue))) { if(!count($words) || !strlen(trim($againstValue))) {
// query contains no indexbale words: force non-match // query contains no indexable words: force non-match
if(strlen($value)) $this->query->where('1>2'); if(strlen($value)) $this->query->where('1>2');
return; return;
} }
@@ -603,9 +711,11 @@ class DatabaseQuerySelectFulltext extends Wire {
$bindKey = $this->query->bindValueGetKey($againstValue); $bindKey = $this->query->bindValueGetKey($againstValue);
$againstType = $expand ? 'WITH QUERY EXPANSION' : ''; $againstType = $expand ? 'WITH QUERY EXPANSION' : '';
$where = "$matchType($tableField) AGAINST($bindKey $againstType)"; $where = "$matchType($tableField) AGAINST($bindKey $againstType)";
$this->query->select("$where AS $scoreField");
$this->query->where($where); $this->query->where($where);
$this->query->orderby("$scoreField DESC"); if($this->allowOrder) {
$this->query->select("$where AS $scoreField");
$this->query->orderby("$scoreField DESC");
}
} }
/** /**
@@ -623,7 +733,6 @@ class DatabaseQuerySelectFulltext extends Wire {
// $= Ends with // $= Ends with
$tableField = $this->tableField(); $tableField = $this->tableField();
$not = strpos($this->operator, '!') === 0;
$matchStart = strpos($this->operator, '^') !== false; $matchStart = strpos($this->operator, '^') !== false;
$againstValue = ''; $againstValue = '';
@@ -643,15 +752,17 @@ class DatabaseQuerySelectFulltext extends Wire {
if(strlen($againstValue)) { if(strlen($againstValue)) {
// use MATCH/AGAINST to pre-filter before RLIKE when possible // use MATCH/AGAINST to pre-filter before RLIKE when possible
$bindKey = $this->query->bindValueGetKey($againstValue); $bindKey = $this->query->bindValueGetKey($againstValue);
$match = $not ? 'NOT MATCH' : 'MATCH'; $matchType = $this->matchType();
$matchAgainst = "$match($tableField) AGAINST($bindKey IN BOOLEAN MODE)"; $matchAgainst = "$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE)";
$scoreField = $this->getScoreFieldName(); $scoreField = $this->getScoreFieldName();
$this->query->select("$matchAgainst AS $scoreField");
$this->query->where($matchAgainst); $this->query->where($matchAgainst);
$this->query->orderby("$scoreField DESC"); if($this->allowOrder) {
$this->query->select("$matchAgainst AS $scoreField");
$this->query->orderby("$scoreField DESC");
}
} }
$likeType = $not ? 'NOT RLIKE' : 'RLIKE'; $likeType = $this->not ? 'NOT RLIKE' : 'RLIKE';
$likeValue = preg_quote($value); $likeValue = preg_quote($value);
if($matchStart) { if($matchStart) {
@@ -677,11 +788,14 @@ class DatabaseQuerySelectFulltext extends Wire {
$scoreField = $this->getScoreFieldName(); $scoreField = $this->getScoreFieldName();
$against = $this->getBooleanModeCommands($text); $against = $this->getBooleanModeCommands($text);
$bindKey = $this->query->bindValueGetKey($against); $bindKey = $this->query->bindValueGetKey($against);
$matchAgainst = "MATCH($tableField) AGAINST($bindKey IN BOOLEAN MODE) "; $matchType = $this->matchType();
$select = "$matchAgainst AS $scoreField "; $matchAgainst = "$matchType($tableField) AGAINST($bindKey IN BOOLEAN MODE) ";
$this->query->select($select);
$this->query->orderby("$scoreField DESC");
$this->query->where($matchAgainst); $this->query->where($matchAgainst);
if($this->allowOrder) {
$select = "$matchAgainst AS $scoreField ";
$this->query->select($select);
$this->query->orderby("$scoreField DESC");
}
} }
/** /**
@@ -745,7 +859,7 @@ class DatabaseQuerySelectFulltext extends Wire {
if($this->isStopword($word)) { if($this->isStopword($word)) {
// handle stop-word // handle stop-word
$stopWords[$word] = $word; $stopWords[$word] = $word;
if($useStopwords) $booleanValues[$word] = "$word*"; if($useStopwords && $partial) $booleanValues[$word] = "<$word*";
continue; // do nothing further with stopwords continue; // do nothing further with stopwords
} else if($length < $minWordLength) { } else if($length < $minWordLength) {
@@ -756,7 +870,7 @@ class DatabaseQuerySelectFulltext extends Wire {
} else if($options['partialLess']) { } else if($options['partialLess']) {
// handle regular word and match full word (more weight), or partial word (less weight) // handle regular word and match full word (more weight), or partial word (less weight)
$booleanValues[$word] = $required . "(>$word $word*)"; $booleanValues[$word] = $required ? "+(>$word $word*)" : "$word*";
$goodWords[$word] = $word; $goodWords[$word] = $word;
} else { } else {
@@ -771,6 +885,7 @@ class DatabaseQuerySelectFulltext extends Wire {
if($booleanValue !== $booleanValues[$word]) { if($booleanValue !== $booleanValues[$word]) {
$booleanValues[$word] = $booleanValue; $booleanValues[$word] = $booleanValue;
$altWords = array_merge($altWords, $alternates); $altWords = array_merge($altWords, $alternates);
$allWords = array_merge($allWords, $altWords);
} }
} }
} }
@@ -781,13 +896,23 @@ class DatabaseQuerySelectFulltext extends Wire {
$booleanValues[$lastWord] = $lastRequired . $lastWord . '*'; $booleanValues[$lastWord] = $lastRequired . $lastWord . '*';
} }
if($useStopwords && !$required && count($stopWords) && count($goodWords)) {
// increase weight of non-stopwords
foreach($goodWords as $word) {
$booleanWord = $booleanValues[$word];
if(!in_array($booleanWord[0], array('(', '+', '<', '>', '-', '~', '"'))) {
$booleanValues[$word] = ">$booleanWord";
}
}
}
$badWords = array_merge($stopWords, $shortWords); $badWords = array_merge($stopWords, $shortWords);
if(count($stopWords)) { if(count($stopWords)) {
$numOkayWords = count($goodWords) + count($shortWords); $numOkayWords = count($goodWords) + count($shortWords);
foreach($stopWords as $word) { foreach($stopWords as $word) {
$likeWords[$word] = $word; $likeWords[$word] = $word;
if($numOkayWords) { if($numOkayWords && isset($booleanValues[$word])) {
// make word non-required in boolean query // make word non-required in boolean query
$booleanValues[$word] = ltrim($booleanValues[$word], '+'); $booleanValues[$word] = ltrim($booleanValues[$word], '+');
} else { } else {
@@ -800,6 +925,7 @@ class DatabaseQuerySelectFulltext extends Wire {
return array( return array(
'value' => trim(implode(' ', $allWords)), 'value' => trim(implode(' ', $allWords)),
'matchValue' => trim(implode(' ', $goodWords) . ' ' . implode(' ', $altWords)), // indexable words only
'booleanValue' => trim(implode(' ', $booleanValues)), 'booleanValue' => trim(implode(' ', $booleanValues)),
'booleanWords' => $booleanValues, 'booleanWords' => $booleanValues,
'likeWords' => $likeWords, 'likeWords' => $likeWords,
@@ -840,6 +966,7 @@ class DatabaseQuerySelectFulltext extends Wire {
} }
$alternateWords = array_unique($alternateWords); $alternateWords = array_unique($alternateWords);
$booleanWords = $alternateWords;
// prepare alternate words for inclusion in boolean value and remove any that arent indexable // prepare alternate words for inclusion in boolean value and remove any that arent indexable
foreach($alternateWords as $key => $alternateWord) { foreach($alternateWords as $key => $alternateWord) {
@@ -848,21 +975,24 @@ class DatabaseQuerySelectFulltext extends Wire {
if($alternateWord === $rootWord && $length > 1) { if($alternateWord === $rootWord && $length > 1) {
// root word is always partial match. weight less if there are other alternates to match // root word is always partial match. weight less if there are other alternates to match
$less = count($alternateWords) > 1 && !empty($options['partialLess']) ? '<' : ''; $less = count($booleanWords) > 1 && !empty($options['partialLess']) ? '<' : '';
$alternateWords[$key] = $less . $alternateWord . '*'; $booleanWords[$key] = $less . $alternateWord . '*';
if($length >= $minWordLength && $length >= 3) $alternateWords[] = $less . $alternateWord; if($length >= $minWordLength && $length >= 3) $booleanWords[] = $less . $alternateWord;
unset($alternateWords[$key]);
} else if($length < $minWordLength || $this->isStopword($alternateWord)) { } else if($length < $minWordLength || $this->isStopword($alternateWord)) {
// alternate word not indexable, remove it // alternate word not indexable, remove it
unset($alternateWords[$key]); unset($alternateWords[$key]);
unset($booleanWords[$key]);
} else { } else {
// replace with escaped version // replace with escaped version
$alternateWords[$key] = $alternateWord; $alternateWords[$key] = $alternateWord;
$booleanWords[$key] = $alternateWord;
} }
} }
if(!count($alternateWords)) return array(); if(!count($booleanWords)) return array();
// rebuild boolean value to include alternates: "+(word word)" or "+word" or "" // rebuild boolean value to include alternates: "+(word word)" or "+word" or ""
if($required) $booleanValue = ltrim($booleanValue, '+'); if($required) $booleanValue = ltrim($booleanValue, '+');
@@ -874,7 +1004,7 @@ class DatabaseQuerySelectFulltext extends Wire {
if($booleanValue && strpos($booleanValue, '>') !== 0) $booleanValue = ">$booleanValue"; if($booleanValue && strpos($booleanValue, '>') !== 0) $booleanValue = ">$booleanValue";
// append alternate words // append alternate words
$booleanValue = trim($booleanValue . ' ' . implode(' ', $alternateWords)); $booleanValue = trim($booleanValue . ' ' . implode(' ', $booleanWords));
// package boolean value into parens and optional "+" prefix (indicating required) // package boolean value into parens and optional "+" prefix (indicating required)
$booleanValue = "$required($booleanValue)"; $booleanValue = "$required($booleanValue)";

View File

@@ -13,7 +13,7 @@
* This file is licensed under the MIT license * This file is licensed under the MIT license
* https://processwire.com/about/license/mit/ * https://processwire.com/about/license/mit/
* *
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer * ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* https://processwire.com * https://processwire.com
* *
*/ */
@@ -43,15 +43,29 @@ class FieldSelectorInfo extends Wire {
*/ */
protected $schemaToInput = array(); protected $schemaToInput = array();
/**
* Construct
*
*/
public function __construct() { public function __construct() {
$ftNops = array();
$ftOps = Selectors::getOperators(array(
'compareType' => Selector::compareTypeFind,
'getValueType' => 'operator',
'getIndexType' => 'none',
));
foreach($ftOps as $op) {
$ftNops[] = "!$op";
}
$this->operators = array( $this->operators = array(
'number' => array('=', '!=', '>', '<', '>=', '<=', '=""', '!=""'), 'number' => array('=', '!=', '>', '<', '>=', '<=', '=""', '!=""'),
'text' => array('=', '!=', '%=', '^=', '$=', '=""', '!=""'), 'text' => array('=', '!=', '%=', '%^=', '%$=', '=""', '!=""'),
'fulltext' => array('%=', '*=', '~=', '^=', '$=', '=', '!=', '=""', '!=""', '!%=', '!*=', '!~=', '!^=', '!$='), 'fulltext' => array_merge($ftOps, array('=', '!=', '=""', '!=""'), $ftNops),
'select' => array('=', '!=') 'select' => array('=', '!=')
); );
$this->infoTemplate = array( $this->infoTemplate = array(
// name of the field // name of the field
@@ -68,7 +82,7 @@ class FieldSelectorInfo extends Wire {
'options' => array(), 'options' => array(),
// if field has subfields, this contains array of all above, indexed by subfield name (blank if not applicable) // if field has subfields, this contains array of all above, indexed by subfield name (blank if not applicable)
'subfields' => array(), 'subfields' => array(),
); );
$this->schemaToInput = array( $this->schemaToInput = array(
'TEXT,TINYTEXT,MEDIUMTEXT,LONGTEXT,VARCHAR,CHAR' => 'text', 'TEXT,TINYTEXT,MEDIUMTEXT,LONGTEXT,VARCHAR,CHAR' => 'text',
@@ -76,8 +90,7 @@ class FieldSelectorInfo extends Wire {
'DATE' => 'date', 'DATE' => 'date',
'INT,DECIMAL,FLOAT,DOUBLE' => 'number', 'INT,DECIMAL,FLOAT,DOUBLE' => 'number',
'ENUM,SET' => 'select', 'ENUM,SET' => 'select',
); );
} }
/** /**
@@ -187,21 +200,19 @@ class FieldSelectorInfo extends Wire {
* *
*/ */
public function getOperatorLabels() { public function getOperatorLabels() {
if(empty($this->operatorLabels)) $this->operatorLabels = array( if(!empty($this->operatorLabels)) return $this->operatorLabels;
'=' => $this->_('Equals'), $this->operatorLabels = Selectors::getOperators(array(
'!=' => $this->_('Not Equals'), 'getIndexType' => 'operator',
'>' => $this->_('Greater Than'), 'getValueType' => 'label',
'<' => $this->_('Less Than'), ));
'>=' => $this->_('Greater Than or Equal'), $this->operatorLabels['=""'] = $this->_('Is Empty');
'<=' => $this->_('Less Than or Equal'), $this->operatorLabels['!=""'] = $this->_('Is Not Empty');
'%=' => $this->_('Contains Text'), foreach($this->operators as $operator) {
'*=' => $this->_('Contains Phrase'), if(isset($this->operatorLabels[$operator])) continue;
'~=' => $this->_('Contains Words'), if(strpos($operator, '!') !== 0) continue;
'^=' => $this->_('Starts With'), $op = ltrim($operator, '!');
'$=' => $this->_('Ends With'), $this->operatorLabels[$operator] = sprintf($this->_('Not: %s'), $this->operatorLabels[$op]);
'=""' => $this->_('Is Empty'), }
'!=""' => $this->_('Is Not Empty')
);
return $this->operatorLabels; return $this->operatorLabels;
} }
} }

View File

@@ -693,7 +693,7 @@ abstract class Fieldtype extends WireData implements Module {
* *
* #pw-group-finding * #pw-group-finding
* *
* @param DatabaseQuerySelect $query * @param PageFinderDatabaseQuerySelect $query
* @param string $table The table name to use * @param string $table The table name to use
* @param string $subfield Name of the subfield (typically 'data', unless selector explicitly specified another) * @param string $subfield Name of the subfield (typically 'data', unless selector explicitly specified another)
* @param string $operator The comparison operator * @param string $operator The comparison operator
@@ -711,6 +711,7 @@ abstract class Fieldtype extends WireData implements Module {
$table = $database->escapeTable($table); $table = $database->escapeTable($table);
$subfield = $database->escapeCol($subfield); $subfield = $database->escapeCol($subfield);
$operator = $database->escapeOperator($operator, WireDatabasePDO::operatorTypeComparison);
$query->where("{$table}.{$subfield}{$operator}?", $value); // QA $query->where("{$table}.{$subfield}{$operator}?", $value); // QA
return $query; return $query;
} }

View File

@@ -577,7 +577,6 @@ abstract class FieldtypeMulti extends Fieldtype {
$table = $database->escapeTable($table); $table = $database->escapeTable($table);
// note the Fulltext class can handle non-text values as well (when using non-partial text matching operators) // note the Fulltext class can handle non-text values as well (when using non-partial text matching operators)
$ft = new DatabaseQuerySelectFulltext($query); $ft = new DatabaseQuerySelectFulltext($query);
$this->wire($ft);
$ft->match($table, $col, $operator, $value); $ft->match($table, $col, $operator, $value);
return $query; return $query;
} }
@@ -861,7 +860,7 @@ abstract class FieldtypeMulti extends Fieldtype {
* *
* Possible template method: If overridden, children should NOT call this parent method. * Possible template method: If overridden, children should NOT call this parent method.
* *
* @param DatabaseQuerySelect $query * @param PageFinderDatabaseQuerySelect $query
* @param string $table The table name to use * @param string $table The table name to use
* @param string $subfield Name of the field (typically 'data', unless selector explicitly specified another) * @param string $subfield Name of the field (typically 'data', unless selector explicitly specified another)
* @param string $operator The comparison operator * @param string $operator The comparison operator
@@ -878,12 +877,14 @@ abstract class FieldtypeMulti extends Fieldtype {
$database = $this->wire('database'); $database = $this->wire('database');
$table = $database->escapeTable($table); $table = $database->escapeTable($table);
if($subfield === 'count' && (empty($value) || ctype_digit(ltrim("$value", '-'))) if($subfield === 'count'
&& in_array($operator, array("=", "!=", ">", "<", ">=", "<="))) { && (empty($value) || ctype_digit(ltrim("$value", '-')))
&& $database->isOperator($operator, WireDatabasePDO::operatorTypeComparison)) {
$value = (int) $value; $value = (int) $value;
$t = $table . "_" . $n; $t = $table . "_" . $n;
$c = $database->escapeTable($this->className()) . "_" . $n; $c = $database->escapeTable($this->className()) . "_" . $n;
$operator = $database->escapeOperator($operator);
$query->select("$t.num_$t AS num_$t"); $query->select("$t.num_$t AS num_$t");
$query->leftjoin( $query->leftjoin(

View File

@@ -1585,6 +1585,7 @@ class PageFinder extends Wire {
$q = $this->wire(new DatabaseQuerySelect()); $q = $this->wire(new DatabaseQuerySelect());
} }
/** @var PageFinderDatabaseQuerySelect $q */
$q->set('field', $field); // original field if required by the fieldtype $q->set('field', $field); // original field if required by the fieldtype
$q->set('group', $group); // original group of the field, if required by the fieldtype $q->set('group', $group); // original group of the field, if required by the fieldtype
$q->set('selector', $selector); // original selector if required by the fieldtype $q->set('selector', $selector); // original selector if required by the fieldtype
@@ -3214,3 +3215,14 @@ class PageFinder extends Wire {
} }
} }
/**
* Typehinting class for DatabaseQuerySelect object passed to Fieldtype::getMatchQuery()
*
* @property Field $field Original field
* @property string $group Original group of the field
* @property Selector $selector Original Selector object
* @property Selectors $selectors Original Selectors object
* @property DatabaseQuerySelect $parentQuery Parent database query
*/
abstract class PageFinderDatabaseQuerySelect extends DatabaseQuerySelect { }

View File

@@ -1020,11 +1020,6 @@ class SelectorContainsAnyWordsExpand extends SelectorContainsAnyWords {
$hasAny = true; $hasAny = true;
break; break;
} }
$stem = $textTools->getWordStem($word);
if($stem && stripos($value1, $stem) !== false && preg_match('/\b' . preg_quote($stem) . '/i', $value1)) {
$hasAny = true;
break;
}
$alternates = $textTools->getWordAlternates($word); $alternates = $textTools->getWordAlternates($word);
foreach($alternates as $alternate) { foreach($alternates as $alternate) {
if(stripos($value1, $alternate) && preg_match('/\b' . preg_quote($alternate) . '\b/i', $value1)) { if(stripos($value1, $alternate) && preg_match('/\b' . preg_quote($alternate) . '\b/i', $value1)) {

View File

@@ -357,24 +357,34 @@ class Selectors extends WireArray {
protected function extractOperators(&$str) { protected function extractOperators(&$str) {
$n = 0; $n = 0;
$not = false;
$operator = ''; $operator = '';
$lastOperator = ''; $lastOperator = '';
$operators = array(); $operators = array();
$operatorChars = self::getOperatorChars(); $operatorChars = self::getOperatorChars();
while(isset($str[$n]) && isset($operatorChars[$str[$n]])) { while(isset($str[$n]) && isset($operatorChars[$str[$n]])) {
$operator .= $str[$n]; $c = $str[$n];
if($operator === '!' && $c !== '=') {
// beginning of operator negation thats not "!="
$not = true;
$operator = ltrim($operator, '!');
}
$operator .= $c;
if(self::isOperator($operator)) { if(self::isOperator($operator)) {
$lastOperator = $operator; $lastOperator = $operator;
} else if($lastOperator) { } else if($lastOperator) {
if($not) $lastOperator = "!$lastOperator";
$operators[$lastOperator] = $lastOperator; $operators[$lastOperator] = $lastOperator;
$lastOperator = ''; $lastOperator = '';
$operator = $str[$n]; $operator = $c;
$not = false;
} }
$n++; $n++;
} }
if($lastOperator) { if($lastOperator) {
if($not) $lastOperator = "!$lastOperator";
$operators[$lastOperator] = $lastOperator; $operators[$lastOperator] = $lastOperator;
} }
@@ -383,11 +393,14 @@ class Selectors extends WireArray {
} }
if($operator && !isset($operators[$lastOperator])) { if($operator && !isset($operators[$lastOperator])) {
// leftover characters in $operator, maybe from operator in wrong order
$fail = true; $fail = true;
if(!count($operators)) { if(!count($operators)) {
// check if operator has a typo we can fix // check if operator has a typo we can fix
// isOperator with 2nd argument true allows for and corrects some order mixups
$op = self::isOperator($operator, true); $op = self::isOperator($operator, true);
if($op) { if($op) {
if($not) $op = "!$op";
$operators[$op] = $op; $operators[$op] = $op;
$str = substr($str, $n); $str = substr($str, $n);
$fail = false; $fail = false;
@@ -809,26 +822,28 @@ class Selectors extends WireArray {
* Extract and return operator from end of field name, as used by selector arrays * Extract and return operator from end of field name, as used by selector arrays
* *
* @param string $field * @param string $field
* @return bool|string * @return array
* *
*/ */
protected function getOperatorFromField(&$field) { protected function getOperatorsFromField(&$field) {
$operator = '=';
$operators = array_keys(self::$selectorTypes); $operators = array_keys(self::$selectorTypes);
$operatorsStr = implode('', $operators); $operatorsStr = implode('', $operators);
$op = substr($field, -1); $c = substr($field, -1);
if(strpos($operatorsStr, $op) !== false) { if(ctype_alnum($c)) return array('=');
// extract operator from $field
$op = '';
while(strpos($operatorsStr, $c) !== false && strlen($field)) {
$op = $c . $op;
$field = substr($field, 0, -1); $field = substr($field, 0, -1);
$op2 = substr($field, -1); $c = substr($field, -1);
if(strpos($operatorsStr, $op2) !== false) {
$field = substr($field, 0, -1);
$op = $op2 . $op;
}
$operator = $op;
$field = trim($field);
} }
return $operator;
if(empty($op)) return array('=');
$operators = $this->extractOperators($op);
return $operators;
} }
/** /**
@@ -863,6 +878,7 @@ class Selectors extends WireArray {
foreach($data as $k => $v) { foreach($data as $k => $v) {
$s = $this->makeSelectorArrayItem($k, $v); $s = $this->makeSelectorArrayItem($k, $v);
$selector1 = $this->create($s['field'], $s['operator'], $s['value']); $selector1 = $this->create($s['field'], $s['operator'], $s['value']);
if(!empty($s['altOperators'])) $selector1->altOperators = $s['altOperators'];
$selector2 = $this->create("or$groupCnt", "=", $selector1); $selector2 = $this->create("or$groupCnt", "=", $selector1);
$selector2->quote = '('; $selector2->quote = '(';
$this->add($selector2); $this->add($selector2);
@@ -888,6 +904,7 @@ class Selectors extends WireArray {
if($s['not']) $selector->not = true; if($s['not']) $selector->not = true;
if($s['group']) $selector->group = $s['group']; if($s['group']) $selector->group = $s['group'];
if($s['quote']) $selector->quote = $s['quote']; if($s['quote']) $selector->quote = $s['quote'];
if(!empty($s['altOperators'])) $selector->altOperators = $s['altOperators'];
$this->add($selector); $this->add($selector);
} }
@@ -910,7 +927,7 @@ class Selectors extends WireArray {
$sanitize = 'selectorValue'; $sanitize = 'selectorValue';
$fields = array(); $fields = array();
$values = array(); $values = array();
$operator = '='; $operators = array('=');
$whitelist = null; $whitelist = null;
$not = false; $not = false;
$group = ''; $group = '';
@@ -947,7 +964,7 @@ class Selectors extends WireArray {
if(isset($data['sanitizer']) && !isset($data['sanitize'])) $data['sanitize'] = $data['sanitizer']; // allow alternate if(isset($data['sanitizer']) && !isset($data['sanitize'])) $data['sanitize'] = $data['sanitizer']; // allow alternate
if(isset($data['sanitize'])) $sanitize = $sanitizer->fieldName($data['sanitize']); if(isset($data['sanitize'])) $sanitize = $sanitizer->fieldName($data['sanitize']);
if(!empty($data['operator'])) $operator = $data['operator']; if(!empty($data['operator'])) $operators = $this->extractOperators($data['operator']);
if(!empty($data['not'])) $not = (bool) $data['not']; if(!empty($data['not'])) $not = (bool) $data['not'];
// may use either 'group' or 'or' to specify or-group // may use either 'group' or 'or' to specify or-group
@@ -982,7 +999,7 @@ class Selectors extends WireArray {
// Non-verbose selector, where $key is the field name and $data is the value // Non-verbose selector, where $key is the field name and $data is the value
// The $key field name may have an optional operator appended to it // The $key field name may have an optional operator appended to it
$operator = $this->getOperatorFromField($key); $operators = $this->getOperatorsFromField($key);
$_fields = strpos($key, '|') ? explode('|', $key) : array($key); $_fields = strpos($key, '|') ? explode('|', $key) : array($key);
$_values = is_array($data) ? $data : array($data); $_values = is_array($data) ? $data : array($data);
@@ -997,6 +1014,7 @@ class Selectors extends WireArray {
if(count($data) == 4) { if(count($data) == 4) {
list($field, $operator, $value, $_sanitize) = $data; list($field, $operator, $value, $_sanitize) = $data;
$operators = $this->extractOperators($operator);
if(is_array($_sanitize)) { if(is_array($_sanitize)) {
$whitelist = $_sanitize; $whitelist = $_sanitize;
} else { } else {
@@ -1005,10 +1023,11 @@ class Selectors extends WireArray {
} else if(count($data) == 3) { } else if(count($data) == 3) {
list($field, $operator, $value) = $data; list($field, $operator, $value) = $data;
$operators = $this->extractOperators($operator);
} else if(count($data) == 2) { } else if(count($data) == 2) {
list($field, $value) = $data; list($field, $value) = $data;
$operator = $this->getOperatorFromField($field); $operators = $this->getOperatorsFromField($field);
} }
if(is_array($field)) { if(is_array($field)) {
@@ -1024,8 +1043,10 @@ class Selectors extends WireArray {
} }
// make sure operator is valid // make sure operator is valid
if(!isset(self::$selectorTypes[$operator])) { foreach($operators as $operator) {
throw new WireException("Unrecognized selector operator '$operator'"); if(!isset(self::$selectorTypes[$operator])) {
throw new WireException("Unrecognized selector operator '$operator'");
}
} }
// determine field(s) // determine field(s)
@@ -1088,7 +1109,8 @@ class Selectors extends WireArray {
return array( return array(
'field' => count($fields) > 1 ? $fields : reset($fields), 'field' => count($fields) > 1 ? $fields : reset($fields),
'value' => count($values) > 1 ? $values : reset($values), 'value' => count($values) > 1 ? $values : reset($values),
'operator' => $operator, 'operator' => array_shift($operators),
'altOperators' => $operators,
'not' => $not, 'not' => $not,
'group' => $group, 'group' => $group,
'quote' => $quote, 'quote' => $quote,

View File

@@ -21,6 +21,10 @@
*/ */
class WireDatabasePDO extends Wire implements WireDatabase { class WireDatabasePDO extends Wire implements WireDatabase {
const operatorTypeComparison = 0;
const operatorTypeBitwise = 1;
const operatorTypeAny = 2;
/** /**
* Log of all queries performed in this instance * Log of all queries performed in this instance
* *
@@ -89,6 +93,22 @@ class WireDatabasePDO extends Wire implements WireDatabase {
*/ */
protected $charset = ''; protected $charset = '';
/**
* Regular comparison operators
*
* @var array
*
*/
protected $comparisonOperators = array('=', '<', '>', '>=', '<=', '<>', '!=');
/**
* Bitwise comparison operators
*
* @var array
*
*/
protected $bitwiseOperators = array('&', '~', '&~', '|', '^', '<<', '>>');
/** /**
* Substitute variable names according to engine as used by getVariable() method * Substitute variable names according to engine as used by getVariable() method
* *
@@ -749,46 +769,69 @@ class WireDatabasePDO extends Wire implements WireDatabase {
* ~~~~~ * ~~~~~
* *
* @param string $str 1-2 character operator to test * @param string $str 1-2 character operator to test
* @param bool|null $bitwise NULL=allow all operators, TRUE=allow only bitwise, FALSE=do not allow bitwise (default=NULL) added 3.0.143 * @param bool|null|int $operatorType Specify a WireDatabasePDO::operatorType* constant (3.0.162+), or any one of the following (3.0.143+):
* - `NULL`: allow all operators (default value if not specified)
* - `FALSE`: allow only comparison operators
* - `TRUE`: allow only bitwise operators
* @param bool $get Return the operator rather than true, when valid? (default=false) Added 3.0.162
* @return bool True if valid, false if not * @return bool True if valid, false if not
* *
*/ */
public function isOperator($str, $bitwise = null) { public function isOperator($str, $operatorType = self::operatorTypeAny, $get = false) {
$operators = array('=', '<', '>', '>=', '<=', '<>', '!=');
$bitwiseOperators = array('&', '~', '&~', '|', '^', '<<', '>>');
$len = strlen($str); $len = strlen($str);
if($len > 2 || $len < 1) return false; if($len > 2 || $len < 1) return false;
if($bitwise === null) { if($operatorType === null || $operatorType === self::operatorTypeAny) {
// allow all operators // allow all operators
$operators = array_merge($operators, $bitwiseOperators); $operators = array_merge($this->comparisonOperators, $this->bitwiseOperators);
} else if($bitwise === true) {
} else if($operatorType === true || $operatorType === self::operatorTypeBitwise) {
// allow only bitwise operators // allow only bitwise operators
$operators = $bitwise; $operators = $this->bitwiseOperators;
} else { } else {
// false or unrecognized $bitwise value: allow only regular operators // self::operatorTypeComparison
$operators = $this->comparisonOperators;
} }
return in_array($str, $operators, true); if($get) {
$key = array_search($str, $operators, true);
return $key === false ? false : $operators[$key];
} else {
return in_array($str, $operators, true);
}
} }
/** /**
* Is given word a fulltext stopword to the current database engine? * Is given word a fulltext stopword for database engine?
* *
* @param string $word * @param string $word
* @param string $engine DB engine ('myisam' or 'innodb') or omit for current engine
* @return bool * @return bool
* @since 3.0.160 * @since 3.0.160
* *
*/ */
public function isStopword($word) { public function isStopword($word, $engine = '') {
$engine = $engine === '' ? $this->engine : strtolower($engine);
if($engine === 'myisam') return DatabaseStopwords::has($word);
if($this->stopwordCache === null) $this->getStopwords($engine, true);
return isset($this->stopwordCache[strtolower($word)]);
}
if($this->engine === 'myisam') { /**
return DatabaseStopwords::has($word); * Get all fulltext stopwords for database engine
} *
* @param string $engine Specify DB engine of "myisam" or "innodb" or omit for current DB engine
if($this->stopwordCache === null && $this->engine === 'innodb') { * @param bool $flip Return flipped array where stopwords are array keys rather than values? for isset() use (default=false)
* @return array
*
*/
public function getStopwords($engine = '', $flip = false) {
$engine = $engine === '' ? $this->engine : strtolower($engine);
if($engine === 'myisam') return DatabaseStopwords::getAll();
if($this->stopwordCache === null) { // && $engine === 'innodb') {
$cache = $this->wire()->cache; $cache = $this->wire()->cache;
$stopwords = null; $stopwords = null;
if($cache) { if($cache) {
@@ -804,10 +847,7 @@ class WireDatabasePDO extends Wire implements WireDatabase {
} }
$this->stopwordCache = array_flip($stopwords); $this->stopwordCache = array_flip($stopwords);
} }
return $flip ? $this->stopwordCache : array_keys($this->stopwordCache);
if(!$this->stopwordCache) return false;
return isset($this->stopwordCache[strtolower($word)]);
} }
/** /**
@@ -854,6 +894,20 @@ class WireDatabasePDO extends Wire implements WireDatabase {
return $this->escapeTable($table) . '.' . $this->escapeCol($col); return $this->escapeTable($table) . '.' . $this->escapeCol($col);
} }
/**
* Sanitize comparison operator
*
* @param string $operator
* @param bool|int|null $operatorType Specify a WireDatabasePDO::operatorType* constant (default=operatorTypeComparison)
* @param string $default Default/fallback operator to return if given one is not valid (default='=')
* @return string
*
*/
public function escapeOperator($operator, $operatorType = self::operatorTypeComparison, $default = '=') {
$operator = $this->isOperator($operator, $operatorType, true);
return $operator ? $operator : $default;
}
/** /**
* Escape a string value, same as $db->quote() but without surrounding quotes * Escape a string value, same as $db->quote() but without surrounding quotes
* *

View File

@@ -134,7 +134,7 @@ class FieldtypeText extends Fieldtype {
/** /**
* Update a query to match the text with a fulltext index * Update a query to match the text with a fulltext index
* *
* @param DatabaseQuerySelect $query * @param PageFinderDatabaseQuerySelect $query
* @param string $table * @param string $table
* @param string $subfield * @param string $subfield
* @param string $operator * @param string $operator
@@ -143,8 +143,7 @@ class FieldtypeText extends Fieldtype {
* *
*/ */
public function getMatchQuery($query, $table, $subfield, $operator, $value) { public function getMatchQuery($query, $table, $subfield, $operator, $value) {
/** @var DatabaseQuerySelectFulltext $ft */ $ft = new DatabaseQuerySelectFulltext($query);
$ft = $this->wire(new DatabaseQuerySelectFulltext($query));
$ft->match($table, $subfield, $operator, $value); $ft->match($table, $subfield, $operator, $value);
return $query; return $query;
} }