diff --git a/wire/core/DatabaseQuery.php b/wire/core/DatabaseQuery.php index c82b32d8..d18cf783 100644 --- a/wire/core/DatabaseQuery.php +++ b/wire/core/DatabaseQuery.php @@ -120,8 +120,7 @@ abstract class DatabaseQuery extends WireData { * Get or set a bind option * * @param string|bool $optionName One of 'prefix' or 'global', boolean true to get/set all - * @param null|int|string|array $optionValue Omit when getting, Specify option value to set, or array when setting - * all + * @param null|int|string|array $optionValue Omit when getting, Specify option value to set, or array when setting all * @return string|int|array * @since 3.0.157 * diff --git a/wire/core/DatabaseStopwords.php b/wire/core/DatabaseStopwords.php index 4435329e..ed0e8c26 100644 --- a/wire/core/DatabaseStopwords.php +++ b/wire/core/DatabaseStopwords.php @@ -3,7 +3,7 @@ /** * ProcessWire DatabaseStopwords * - * MySQL stopwords, primarily for use with filtering fulltext queries + * MySQL stopwords, primarily for use with filtering fulltext queries (MyISAM only) * * ProcessWire 3.x, Copyright 2016 by Ryan Cramer * https://processwire.com @@ -49,8 +49,8 @@ class DatabaseStopwords { "various", "very", "via", "viz", "vs", "want", "wants", "was", "wasn't", "way", "we", "we'd", "we'll", "we're", "we've", "welcome", "well", "went", "were", "weren't", "what", "what's", "whatever", "when", "whence", "whenever", "where", "where's", "whereafter", "whereas", "whereby", "wherein", "whereupon", "wherever", "whether", "which", "while", "whither", "who", "who's", "whoever", "whole", "whom", "whose", "why", "will", "willing", "wish", "with", "within", - "without", "won't", "wonder", "would", "would", "wouldn't", "yes", "yet", "you", "you'd", "you'll", "you're", "you've", "your", "yours", "yourself", - "yourselves", "zero"); + "without", "won't", "wonder", "would", "wouldn't", "yes", "yet", "you", "you'd", "you'll", "you're", "you've", "your", "yours", "yourself", "yourselves", "zero" + ); /** * Get all stopwords diff --git a/wire/core/Fieldtype.php b/wire/core/Fieldtype.php index 0410ce18..ccdb9f7a 100644 --- a/wire/core/Fieldtype.php +++ b/wire/core/Fieldtype.php @@ -711,9 +711,7 @@ abstract class Fieldtype extends WireData implements Module { $table = $database->escapeTable($table); $subfield = $database->escapeCol($subfield); - $quoteValue = $database->quote($value); - - $query->where("{$table}.{$subfield}{$operator}$quoteValue"); // QA + $query->where("{$table}.{$subfield}{$operator}?", $value); // QA return $query; } diff --git a/wire/core/Page.php b/wire/core/Page.php index c69c41f3..53340f31 100644 --- a/wire/core/Page.php +++ b/wire/core/Page.php @@ -99,6 +99,7 @@ * @property Page|null $_cloning Internal runtime use, contains Page being cloned (source), when this Page is the new copy (target). #pw-internal * @property bool|null $_hasAutogenName Internal runtime use, set by Pages class when page as auto-generated name. #pw-internal * @property bool|null $_forceSaveParents Internal runtime/debugging use, force a page to refresh its pages_parents DB entries on save(). #pw-internal + * @property float|null $_pfscore Internal PageFinder fulltext match score when page found/loaded from relevant query. #pw-internal * * Methods added by PageRender.module: * ----------------------------------- diff --git a/wire/core/PageFinder.php b/wire/core/PageFinder.php index fd35d6c1..2266f1b9 100644 --- a/wire/core/PageFinder.php +++ b/wire/core/PageFinder.php @@ -639,7 +639,7 @@ class PageFinder extends Wire { $score += $v; unset($row[$k]); } - $row['score'] = $score; // @todo do we need this anymore? + $row['score'] = $score; $matches[] = $row; } else if($options['returnAllCols']) { @@ -2298,7 +2298,9 @@ class PageFinder extends Wire { // the following fields are defined in each iteration here because they may be modified in the loop $table = "pages"; $operator = $selector->operator; - $isPartialOperator = in_array($operator, array('%=', '^=', '$=', '%^=', '%$=', '*=')); + $compareType = $selectors::getSelectorByOperator($operator, 'compareType'); + $isPartialOperator = ($compareType & Selector::compareTypeFind); + $subfield = ''; $IDs = array(); // populated in special cases where we can just match parent IDs $sql = ''; @@ -3054,7 +3056,10 @@ class PageFinder extends Wire { /** @var PageFinder $finder */ $finder = $this->wire(new PageFinder()); - $ids = $finder->findIDs($ownerSelectors); + $ids = array(); + foreach($finder->findIDs($ownerSelectors) as $id) { + $ids[] = (int) $id; + } if($this->isRepeaterFieldtype($ownerField->type)) { // Repeater diff --git a/wire/core/PagesLoader.php b/wire/core/PagesLoader.php index 4f58a892..e219ab10 100644 --- a/wire/core/PagesLoader.php +++ b/wire/core/PagesLoader.php @@ -346,7 +346,7 @@ class PagesLoader extends Wire { // [ pageID => [ all pages columns ] ] $pagesInfo = $pageFinder->findVerboseIDs($selectors, $options); } else { - // [ [ 'id' => 3, 'templates_id' => 2, 'parent_id' => 1 ] + // [ [ 'id' => 3, 'templates_id' => 2, 'parent_id' => 1, 'score' => 1.123 ] $pagesInfo = $pageFinder->find($selectors, $options); } @@ -398,13 +398,16 @@ class PagesLoader extends Wire { $parent_id = $pageFinder->getParentID(); $idsSorted = array(); $idsByTemplate = array(); + $scores = array(); // organize the pages by template ID foreach($pagesInfo as $page) { - $tpl_id = $page['templates_id']; + $tpl_id = (int) $page['templates_id']; + $id = (int) $page['id']; if(!isset($idsByTemplate[$tpl_id])) $idsByTemplate[$tpl_id] = array(); - $idsByTemplate[$tpl_id][] = $page['id']; - $idsSorted[] = $page['id']; + $idsByTemplate[$tpl_id][] = $id; + $idsSorted[] = $id; + if(!empty($page['score'])) $scores[$id] = (float) $page['score']; } if(count($idsByTemplate) > 1) { @@ -440,6 +443,12 @@ class PagesLoader extends Wire { $sortsAfter = $pageFinder->getSortsAfter(); if(count($sortsAfter)) $pages->sort($sortsAfter); + + if(count($scores)) { + foreach($pages as $page) { + if(isset($scores[$page->id])) $page->setQuietly('_pfscore', $scores[$page->id]); + } + } } else { $pages = $this->pages->newPageArray($loadOptions); diff --git a/wire/core/ProcessWire.php b/wire/core/ProcessWire.php index bd7e1f88..fb11a8cd 100644 --- a/wire/core/ProcessWire.php +++ b/wire/core/ProcessWire.php @@ -495,7 +495,9 @@ class ProcessWire extends Wire { /** @var WireCache $cache */ $cache = $this->wire('cache', new WireCache(), true); - $cache->preload($config->preloadCacheNames); + $cacheNames = $config->preloadCacheNames; + if($database->getEngine() === 'innodb') $cacheNames[] = 'InnoDB.stopwords'; + $cache->preload($cacheNames); $modules = null; try { diff --git a/wire/core/WireDatabasePDO.php b/wire/core/WireDatabasePDO.php index 27e84ef1..e259f2fc 100644 --- a/wire/core/WireDatabasePDO.php +++ b/wire/core/WireDatabasePDO.php @@ -122,6 +122,14 @@ class WireDatabasePDO extends Wire implements WireDatabase { */ protected $variableCache = array(); + /** + * Cached InnoDB stopwords (keys are the stopwords and values are irrelevant) + * + * @var array|null Becomes array once loaded + * + */ + protected $stopwordCache = null; + /** * Create a new PDO instance from ProcessWire $config API variable * @@ -766,6 +774,42 @@ class WireDatabasePDO extends Wire implements WireDatabase { return in_array($str, $operators, true); } + /** + * Is given word a fulltext stopword to the current database engine? + * + * @param string $word + * @return bool + * @since 3.0.160 + * + */ + public function isStopword($word) { + + if($this->engine === 'myisam') { + return DatabaseStopwords::has($word); + } + + if($this->stopwordCache === null && $this->engine === 'innodb') { + $cache = $this->wire()->cache; + $stopwords = null; + if($cache) { + $stopwords = $cache->get('InnoDB.stopwords'); + if($stopwords) $stopwords = explode(',', $stopwords); + } + if(!$stopwords) { + $query = $this->prepare('SELECT value FROM INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD'); + $query->execute(); + $stopwords = $query->fetchAll(\PDO::FETCH_COLUMN, 0); + $query->closeCursor(); + if($cache) $cache->save('InnoDB.stopwords', implode(',', $stopwords), WireCache::expireDaily); + } + $this->stopwordCache = array_flip($stopwords); + } + + if(!$this->stopwordCache) return false; + + return isset($this->stopwordCache[strtolower($word)]); + } + /** * Sanitize a table name for _a-zA-Z0-9 * @@ -930,6 +974,28 @@ class WireDatabasePDO extends Wire implements WireDatabase { return $value; } + /** + * Get current database engine (lowercase) + * + * @return string + * @since 3.0.160 + * + */ + public function getEngine() { + return $this->engine; + } + + /** + * Get current database charset (lowercase) + * + * @return string + * @since 3.0.160 + * + */ + public function getCharset() { + return $this->charset; + } + /** * Retrieve new instance of WireDatabaseBackups ready to use with this connection * diff --git a/wire/core/WireDatabasePDOStatement.php b/wire/core/WireDatabasePDOStatement.php index 6173c856..239bdd2f 100644 --- a/wire/core/WireDatabasePDOStatement.php +++ b/wire/core/WireDatabasePDOStatement.php @@ -129,15 +129,27 @@ class WireDatabasePDOStatement extends \PDOStatement { * * @param array|null $input_parameters * @return bool + * @throws \PDOException * */ public function execute($input_parameters = NULL) { $timer = Debug::startTimer(); - $result = parent::execute($input_parameters); + $exception = null; + + try { + $result = parent::execute($input_parameters); + } catch(\PDOException $e) { + $exception = $e; + $result = false; + } + $timer = Debug::stopTimer($timer, 'ms'); - if(!$this->database) return $result; + if(!$this->database) { + if($exception) throw $exception; + return $result; + } if(is_array($input_parameters)) { foreach($input_parameters as $key => $value) { @@ -146,6 +158,7 @@ class WireDatabasePDOStatement extends \PDOStatement { } $debugNote = trim("$this->debugNote [$timer]"); + if($exception) $debugNote .= ' FAIL SQLSTATE[' . $exception->getCode() . ']'; if($this->debugParamsQty) { $sql = $this->queryString; @@ -168,6 +181,8 @@ class WireDatabasePDOStatement extends \PDOStatement { $this->database->queryLog($this->queryString, $debugNote); } + if($exception) throw $exception; + return $result; } diff --git a/wire/modules/Fieldtype/FieldtypeFile.module b/wire/modules/Fieldtype/FieldtypeFile.module index 5d23e413..65d63b47 100644 --- a/wire/modules/Fieldtype/FieldtypeFile.module +++ b/wire/modules/Fieldtype/FieldtypeFile.module @@ -913,8 +913,8 @@ class FieldtypeFile extends FieldtypeMulti implements ConfigurableModule { } else if($operator === '!=' && ctype_alnum("$value")) { $operator = '!~='; // ok - } else if($operator === '~=' || $operator === '!~=' || $operator === '%=' || $operator === '*=') { - // ok + } else if(Selectors::getSelectorByOperator($operator, 'compareType') & Selector::compareTypeFind) { + // ok, text finding operators } else { throw new PageFinderSyntaxException("Operator $operator is not supported by $this in selector: $selector");