From 7503ecd781ab62e3d57690f486bf7ba79e561e2f Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Thu, 30 Jul 2020 12:04:37 -0400 Subject: [PATCH] Fix issue processwire/processwire-issues#973 --- wire/core/Fieldtype.php | 10 +++++ wire/core/PageFinder.php | 38 +++++++++++++------ .../Fieldtype/FieldtypeDatetime.module | 34 ++++++++++++----- 3 files changed, 60 insertions(+), 22 deletions(-) diff --git a/wire/core/Fieldtype.php b/wire/core/Fieldtype.php index ddba751d..7378f2a6 100644 --- a/wire/core/Fieldtype.php +++ b/wire/core/Fieldtype.php @@ -521,6 +521,16 @@ abstract class Fieldtype extends WireData implements Module { * Example: an integer or text Fieldtype might not consider a "0" to be empty, * whereas a Page reference would. * + * This method is primarily used by the PageFinder::whereEmptyValuePossible() + * method to determine whether to include non-present (null) rows. + * + * 3.0.164+: If given a Selector object for $value, PageFinder is proposing + * handling the empty-value match condition internally rather than calling + * the Fieldtype’s getMatchQuery() method. Return true if this Fieldtype would + * prefer to handle the match, or false if not. Fieldtype modules do not need + * to consider this unless they want to override the default empty value match + * behavior in PageFinder::whereEmptyValuePossible(). + * * #pw-group-finding * * @param Field $field diff --git a/wire/core/PageFinder.php b/wire/core/PageFinder.php index 72d39334..20b6fa82 100644 --- a/wire/core/PageFinder.php +++ b/wire/core/PageFinder.php @@ -1790,14 +1790,15 @@ class PageFinder extends Wire { // look in table that has no pages_id relation back to pages, using the LEFT JOIN / IS NULL trick // OR check for blank value as defined by the fieldtype + static $tableCnt = 0; + + $ft = $field->type; $operator = $selector->operator; $database = $this->wire('database'); - static $tableCnt = 0; $table = $database->escapeTable($field->table); $tableAlias = $table . "__blank" . (++$tableCnt); - $blankValue = $field->type->getBlankValue(new NullPage(), $field); + $blankValue = $ft->getBlankValue(new NullPage(), $field); $blankIsObject = is_object($blankValue); - if($blankIsObject) $blankValue = ''; $whereType = 'OR'; $sql = ''; $operators = array( @@ -1808,29 +1809,42 @@ class PageFinder extends Wire { '>' => '<=', '>=' => '<' ); + + if($blankIsObject) $blankValue = ''; if(!isset($operators[$operator])) return false; if($selector->not) $operator = $operators[$operator]; // reverse - - if($operator == '=') { + + // ask Fieldtype if it would prefer to handle matching this empty value selector + if($ft->isEmptyValue($field, $selector)) { + // fieldtype will handle matching the selector in its getMatchQuery + return false; + + } else if(($operator === '=' || $operator === '!=') && $ft->isEmptyValue($field, $value) && $ft->isEmptyValue($field, '0000-00-00')) { + // matching empty in date, datetime, timestamp column with equals or not-equals condition + // non-presence of row is required in order to match empty/blank (in MySQL 8.x) + $is = $operator === '=' ? 'IS' : 'IS NOT'; + $sql = "$tableAlias.pages_id $is NULL "; + + } else if($operator === '=') { // equals // non-presence of row is equal to value being blank $bindKey = $query->bindValueGetKey($blankValue); - if($field->type->isEmptyValue($field, $value)) { + if($ft->isEmptyValue($field, $value)) { $sql = "$tableAlias.pages_id IS NULL OR ($tableAlias.data=$bindKey"; } else { $sql = "($tableAlias.data=$bindKey"; } - if($value !== "0" && $blankValue !== "0" && !$field->type->isEmptyValue($field, "0")) { + if($value !== "0" && $blankValue !== "0" && !$ft->isEmptyValue($field, "0")) { // if zero is not considered an empty value, exclude it from matching // if the search isn't specifically for a "0" $sql .= " AND $tableAlias.data!='0'"; } $sql .= ")"; - } else if($operator == '!=' || $operator == '<>') { + } else if($operator === '!=' || $operator === '<>') { // not equals // $whereType = 'AND'; - if($value === "0" && !$field->type->isEmptyValue($field, "0")) { + if($value === "0" && !$ft->isEmptyValue($field, "0")) { // may match rows with no value present $sql = "$tableAlias.pages_id IS NULL OR ($tableAlias.data!='0'"; @@ -1840,7 +1854,7 @@ class PageFinder extends Wire { } else { $bindKey = $query->bindValueGetKey($blankValue); $sql = "$tableAlias.pages_id IS NOT NULL AND ($tableAlias.data!=$bindKey"; - if($blankValue !== "0" && !$field->type->isEmptyValue($field, "0")) { + if($blankValue !== "0" && !$ft->isEmptyValue($field, "0")) { $sql .= " OR $tableAlias.data='0'"; } } @@ -1848,7 +1862,7 @@ class PageFinder extends Wire { } else if($operator == '<' || $operator == '<=') { // less than - if($value > 0 && $field->type->isEmptyValue($field, "0")) { + if($value > 0 && $ft->isEmptyValue($field, "0")) { // non-rows can be included as counting for 0 $bindKey = $query->bindValueGetKey($value); $sql = "$tableAlias.pages_id IS NULL OR $tableAlias.data$operator$bindKey"; @@ -1857,7 +1871,7 @@ class PageFinder extends Wire { return false; } } else if($operator == '>' || $operator == '>=') { - if($value < 0 && $field->type->isEmptyValue($field, "0")) { + if($value < 0 && $ft->isEmptyValue($field, "0")) { // non-rows can be included as counting for 0 $bindKey = $query->bindValueGetKey($value); $sql = "$tableAlias.pages_id IS NULL OR $tableAlias.data$operator$bindKey"; diff --git a/wire/modules/Fieldtype/FieldtypeDatetime.module b/wire/modules/Fieldtype/FieldtypeDatetime.module index 1ca79a29..1ea79b15 100644 --- a/wire/modules/Fieldtype/FieldtypeDatetime.module +++ b/wire/modules/Fieldtype/FieldtypeDatetime.module @@ -202,13 +202,28 @@ class FieldtypeDatetime extends Fieldtype { * */ public function isEmptyValue(Field $field, $value) { - return !strlen($value); + + if(is_object($value) && $value instanceof Selector) { + // PageFinder is asking if it should let this Fieldtype handle the operator/value + // combination with potential empty value present in a Selector + $selector = $value; + $op = substr($selector->operator, 0, 1); + // tell PageFinder we will handle greater-than/less-than conditions in our getMatchQuery() + if($op === '>' || $op === '<') return true; + } + + // note: 0000-00-00 intentionally returns true, which is what $value is when PageFinder is testing + // whether the Fieldtype recognizes an empty ISO-8601 date that it will convert to matching null + + $value = trim($value, '-0 '); + + return !strlen($value); } /** * Match a date/time value in the database, as used by PageFinder * - * @param DatabaseQuerySelect $query + * @param PageFinderDatabaseQuerySelect $query * @param string $table * @param string $subfield * @param string $operator @@ -219,10 +234,11 @@ class FieldtypeDatetime extends Fieldtype { */ public function getMatchQuery($query, $table, $subfield, $operator, $value) { - $database = $this->wire('database'); + $database = $this->wire()->database; $intValue = $this->_sanitizeValue($value); $table = $database->escapeTable($table); $subfield = $subfield ? $database->escapeCol($subfield) : 'data'; + $minDT = '1000-01-01 00:00:00'; // $maxDT = '9999-12-31 23:59:59'; if(is_string($value) && in_array($operator, array('%=', '^='))) { // partial date string match @@ -232,9 +248,9 @@ class FieldtypeDatetime extends Fieldtype { if(!ctype_digit(str_replace(array('-', ' '), '', $value))) { throw new WireException("Invalid partial date string '$value' (numbers, hyphens and space only)"); } - $value = $database->escapeStr($value); + $value = str_replace(array('%', '_'), '', $value); $value = $operator === '^=' ? "$value%" : "%$value%"; - $query->where("$table.$subfield LIKE '$value'"); + $query->where("$table.$subfield LIKE ?", $value); } else if(!$database->isOperator($operator)) { // invalid operator @@ -244,21 +260,19 @@ class FieldtypeDatetime extends Fieldtype { // matching a populated value that successfully converted to unix timestamp $dateString = date('Y-m-d H:i:s', $intValue); if($dateString !== false) { - $dateString = $database->escapeStr($dateString); - $query->where("$table.$subfield$operator'$dateString'"); + $query->where("$table.$subfield$operator?", $dateString); } } else { // matching an empty value - $minDT = $database->escapeStr(date('Y-m-d H:i:s', 0)); if(in_array($operator, array('!=', '>', '>='))) { // match NOT empty (!=0, >0) - $query->where("$table.$subfield>='$minDT'"); + $query->where("$table.$subfield>=?", $minDT); } else if(in_array($operator, array('=', '<', '<='))) { // match empty (=0, <0, <=0): match null or value below unix timestamp range // this includes 0000-00-00 when present and used by MySQL version - $query->where("$table.$subfield IS NULL OR $table.$subfield<'$minDT'"); + $query->where("$table.$subfield IS NULL OR $table.$subfield