* All manually built LIKE queries in the core are replaced with a wrapper function Database::buildLike()
* This function automatically performs all escaping, so Database::escapeLike() is now almost never used
dummy
* (bug 21161) Changing $wgCacheEpoch now always invalidates file cache
* (bug 20268) Fixed row count estimation on SQLite backend
+* (bug 20275) Fixed LIKE queries on SQLite backend
== API changes in 1.16 ==
'LBFactory' => 'includes/db/LBFactory.php',
'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php',
'LBFactory_Simple' => 'includes/db/LBFactory.php',
+ 'LikeMatch' => 'includes/db/Database.php',
'LoadBalancer' => 'includes/db/LoadBalancer.php',
'LoadMonitor' => 'includes/db/LoadMonitor.php',
'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php',
$options = array();
$db =& $this->getDBOptions( $options );
$conds = array(
- "ipb_range_start LIKE '$range%'",
+ 'ipb_range_start' . $db->buildLike( $range, $db->anyString() ),
"ipb_range_start <= '$iaddr'",
"ipb_range_end >= '$iaddr'"
);
* @static
* @param $filterEntry String: domainparts
* @param $prot String: protocol
+ * @deprecated Use makeLikeArray() and pass result to Database::buildLike() instead
*/
public static function makeLike( $filterEntry , $prot = 'http://' ) {
+ wfDeprecated( __METHOD__ );
$db = wfGetDB( DB_MASTER );
if ( substr( $filterEntry, 0, 2 ) == '*.' ) {
$subdomains = true;
}
return $like;
}
+
+ /**
+ * Make an array to be used for calls to Database::like(), which will match the specified
+ * string. There are several kinds of filter entry:
+ * *.domain.com - Produces http://com.domain.%, matches domain.com
+ * and www.domain.com
+ * domain.com - Produces http://com.domain./%, matches domain.com
+ * or domain.com/ but not www.domain.com
+ * *.domain.com/x - Produces http://com.domain.%/x%, matches
+ * www.domain.com/xy
+ * domain.com/x - Produces http://com.domain./x%, matches
+ * domain.com/xy but not www.domain.com/xy
+ *
+ * Asterisks in any other location are considered invalid.
+ *
+ * @static
+ * @param $filterEntry String: domainparts
+ * @param $prot String: protocol
+ */
+ public static function makeLikeArray( $filterEntry , $prot = 'http://' ) {
+ $db = wfGetDB( DB_MASTER );
+ if ( substr( $filterEntry, 0, 2 ) == '*.' ) {
+ $subdomains = true;
+ $filterEntry = substr( $filterEntry, 2 );
+ if ( $filterEntry == '' ) {
+ // We don't want to make a clause that will match everything,
+ // that could be dangerous
+ return false;
+ }
+ } else {
+ $subdomains = false;
+ }
+ // No stray asterisks, that could cause confusion
+ // It's not simple or efficient to handle it properly so we don't
+ // handle it at all.
+ if ( strpos( $filterEntry, '*' ) !== false ) {
+ return false;
+ }
+ $slash = strpos( $filterEntry, '/' );
+ if ( $slash !== false ) {
+ $path = substr( $filterEntry, $slash );
+ $host = substr( $filterEntry, 0, $slash );
+ } else {
+ $path = '/';
+ $host = $filterEntry;
+ }
+ // Reverse the labels in the hostname, convert to lower case
+ // For emails reverse domainpart only
+ if ( $prot == 'mailto:' && strpos($host, '@') ) {
+ // complete email adress
+ $mailparts = explode( '@', $host );
+ $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) );
+ $host = $domainpart . '@' . $mailparts[0];
+ $like = array( "$prot$host", $db->anyString() );
+ } elseif ( $prot == 'mailto:' ) {
+ // domainpart of email adress only. do not add '.'
+ $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) );
+ $like = array( "$prot$host", $db->anyString() );
+ } else {
+ $host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) );
+ if ( substr( $host, -1, 1 ) !== '.' ) {
+ $host .= '.';
+ }
+ $like = array( "$prot$host" );
+
+ if ( $subdomains ) {
+ $like[] = $db->anyString();
+ }
+ if ( !$subdomains || $path !== '/' ) {
+ $like[] = $path;
+ $like[] = $db->anyString();
+ }
+ }
+ return $like;
+ }
+
+ /**
+ * Filters an array returned by makeLikeArray(), removing everything past first pattern placeholder.
+ * @static
+ * @param $arr array: array to filter
+ * @return filtered array
+ */
+ public static function keepOneWildcard( $arr ) {
+ if( !is_array( $arr ) ) {
+ return $arr;
+ }
+
+ foreach( $arr as $key => $value ) {
+ if ( $value instanceof LikeMatch ) {
+ return array_slice( $arr, 0, $key + 1 );
+ }
+ }
+
+ return $arr;
+ }
}
$this->title = $title->getPrefixedText();
$ns = $title->getNamespace();
+ $db = $this->mDb;
+
# Using the (log_namespace, log_title, log_timestamp) index with a
# range scan (LIKE) on the first two parts, instead of simple equality,
# makes it unusable for sorting. Sorted retrieval using another index
# log entries for even the busiest pages, so it can be safely scanned
# in full to satisfy an impossible condition on user or similar.
if( $pattern && !$wgMiserMode ) {
- # use escapeLike to avoid expensive search patterns like 't%st%'
- $safetitle = $this->mDb->escapeLike( $title->getDBkey() );
$this->mConds['log_namespace'] = $ns;
- $this->mConds[] = "log_title LIKE '$safetitle%'";
+ $this->mConds[] = 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() );
$this->pattern = $pattern;
} else {
$this->mConds['log_namespace'] = $ns;
}
// Paranoia: avoid brute force searches (bug 17342)
if( !$wgUser->isAllowed( 'deletedhistory' ) ) {
- $this->mConds[] = $this->mDb->bitAnd('log_deleted', LogPage::DELETED_ACTION) . ' = 0';
+ $this->mConds[] = $db->bitAnd('log_deleted', LogPage::DELETED_ACTION) . ' = 0';
} else if( !$wgUser->isAllowed( 'suppressrevision' ) ) {
- $this->mConds[] = $this->mDb->bitAnd('log_deleted', LogPage::SUPPRESSED_ACTION) .
+ $this->mConds[] = $db->bitAnd('log_deleted', LogPage::SUPPRESSED_ACTION) .
' != ' . LogPage::SUPPRESSED_ACTION;
}
}
# database or in code.
if ( $code !== $wgContLanguageCode ) {
# Messages for particular language
- $escapedCode = $dbr->escapeLike( $code );
- $conds[] = "page_title like '%%/$escapedCode'";
+ $conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), "/$code" );
} else {
# Effectively disallows use of '/' character in NS_MEDIAWIKI for uses
# other than language code.
- $conds[] = "page_title not like '%%/%%'";
+ $conds[] = 'page_title NOT' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() );
}
}
$dbr = wfGetDB( DB_SLAVE );
$conds['page_namespace'] = $this->getNamespace();
- $conds[] = 'page_title LIKE ' . $dbr->addQuotes(
- $dbr->escapeLike( $this->getDBkey() ) . '/%' );
+ $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
$options = array();
if( $limit > -1 )
$options['LIMIT'] = $limit;
$from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from']));
$this->addWhereRange('cat_title', $dir, $from, null);
if (isset ($params['prefix']))
- $this->addWhere("cat_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'");
+ $this->addWhere('cat_title' . $db->buildLike( $this->titlePartToKey($params['prefix']), $db->anyString() ) );
$this->addOption('LIMIT', $params['limit']+1);
$this->addOption('ORDER BY', 'cat_title' . ($params['dir'] == 'descending' ? ' DESC' : ''));
if (!is_null($params['from']))
$this->addWhere('pl_title>=' . $db->addQuotes($this->titlePartToKey($params['from'])));
if (isset ($params['prefix']))
- $this->addWhere("pl_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'");
+ $this->addWhere('pl_title' . $db->buildLike( $this->titlePartToKey($params['prefix']), $db->anyString() ) );
$this->addFields(array (
'pl_title',
$this->addWhere('u1.user_name >= ' . $db->addQuotes($this->keyToTitle($params['from'])));
if (!is_null($params['prefix']))
- $this->addWhere('u1.user_name LIKE "' . $db->escapeLike($this->keyToTitle( $params['prefix'])) . '%"');
+ $this->addWhere('u1.user_name' . $db->buildLike($this->keyToTitle($params['prefix']), $db->anyString()));
if (!is_null($params['group'])) {
// Filter only users that belong to a given group
$from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from']));
$this->addWhereRange('img_name', $dir, $from, null);
if (isset ($params['prefix']))
- $this->addWhere("img_name LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'");
+ $this->addWhere('img_name' . $db->buildLike( $this->titlePartToKey($params['prefix']), $db->anyString() ) );
if (isset ($params['minsize'])) {
$this->addWhere('img_size>=' . intval($params['minsize']));
$from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from']));
$this->addWhereRange('page_title', $dir, $from, null);
if (isset ($params['prefix']))
- $this->addWhere("page_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'");
+ $this->addWhere('page_title' . $db->buildLike($this->titlePartToKey($params['prefix']), $db->->anyString()));
if (is_null($resultPageSet)) {
$selectFields = array (
else
$lower = $upper = IP::toHex($params['ip']);
$prefix = substr($lower, 0, 4);
+
+ $db = $this->getDB();
$this->addWhere(array(
- "ipb_range_start LIKE '$prefix%'",
+ 'ipb_range_start' . $db->buildLike($prefix, $db->anyString()),
"ipb_range_start <= '$lower'",
"ipb_range_end >= '$upper'"
));
if(is_null($protocol))
$protocol = 'http://';
- $likeQuery = LinkFilter::makeLike($query, $protocol);
+ $likeQuery = LinkFilter::makeLikeArray($query, $protocol);
if (!$likeQuery)
$this->dieUsage('Invalid query', 'bad_query');
- $likeQuery = substr($likeQuery, 0, strpos($likeQuery,'%')+1);
- $this->addWhere('el_index LIKE ' . $db->addQuotes( $likeQuery ));
+
+ $likeQuery = LinkFilter::keepOneWildcard($likeQuery);
+ $this->addWhere('el_index ' . $db->buildLike( $likeQuery ));
}
else if(!is_null($protocol))
- $this->addWhere('el_index LIKE ' . $db->addQuotes( "$protocol%" ));
+ $this->addWhere('el_index ' . $db->buildLike( "$protocol", $db->anyString() ));
$prop = array_flip($params['prop']);
$fld_ids = isset($prop['ids']);
$this->addWhere($this->getDB()->bitAnd('rev_deleted',Revision::DELETED_USER) . ' = 0');
// We only want pages by the specified users.
if($this->prefixMode)
- $this->addWhere("rev_user_text LIKE '" . $this->getDB()->escapeLike($this->userprefix) . "%'");
+ $this->addWhere('rev_user_text' . $this->getDB()->buildLike($this->userprefix, $this->getDB()->anyString()));
else
$this->addWhereFld('rev_user_text', $this->usernames);
// ... and in the specified timeframe.
}
/**
- * Escape string for safe LIKE usage
+ * Escape string for safe LIKE usage.
+ * WARNING: you should almost never use this function directly,
+ * instead use buildLike() that escapes everything automatically
*/
function escapeLike( $s ) {
$s = str_replace( '\\', '\\\\', $s );
return $s;
}
+ /**
+ * LIKE statement wrapper, receives a variable-length argument list with parts of pattern to match
+ * containing either string literals that will be escaped or tokens returned by anyChar() or anyString().
+ * Alternatively, the function could be provided with an array of aforementioned parameters.
+ *
+ * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns a LIKE clause that searches
+ * for subpages of 'My page title'.
+ * Alternatively: $pattern = array( 'My_page_title/', $dbr->anyString() ); $query .= $dbr->buildLike( $pattern );
+ *
+ * @ return String: fully built LIKE statement
+ */
+ function buildLike() {
+ $params = func_get_args();
+ if (count($params) > 0 && is_array($params[0])) {
+ $params = $params[0];
+ }
+
+ $s = '';
+ foreach( $params as $value) {
+ if( $value instanceof LikeMatch ) {
+ $s .= $value->toString();
+ } else {
+ $s .= $this->escapeLike( $value );
+ }
+ }
+ return " LIKE '" . $s . "' ";
+ }
+
+ /**
+ * Returns a token for buildLike() that denotes a '_' to be used in a LIKE query
+ */
+ function anyChar() {
+ return new LikeMatch( '_' );
+ }
+
+ /**
+ * Returns a token for buildLike() that denotes a '%' to be used in a LIKE query
+ */
+ function anyString() {
+ return new LikeMatch( '%' );
+ }
+
/**
* Returns an appropriately quoted sequence value for inserting a new row.
* MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
return $this->current() !== false;
}
}
+
+/**
+ * Used by DatabaseBase::buildLike() to represent characters that have special meaning in SQL LIKE clauses
+ * and thus need no escaping. Don't instantiate it manually, use Database::anyChar() and anyString() instead.
+ */
+class LikeMatch {
+ private $str;
+
+ public function __construct( $s ) {
+ $this->str = $s;
+ }
+
+ public function toString() {
+ return $this->str;
+ }
+}
\ No newline at end of file
return $s;
}
+ function buildLike() {
+ $params = func_get_args();
+ if ( count( $params ) > 0 && is_array( $params[0] ) ) {
+ $params = $params[0];
+ }
+ return parent::buildLike( $params ) . "ESCAPE '\' ";
+ }
+
/**
* How lagged is this slave?
*/
$ext = File::normalizeExtension($ext);
$inuse = $dbw->selectField( 'oldimage', '1',
array( 'oi_sha1' => $sha1,
- "oi_archive_name LIKE '%.{$ext}'",
+ 'oi_archive_name ' . $dbw->buildLike( $dbw->anyString(), ".$ext" ),
$dbw->bitAnd('oi_deleted', File::DELETED_FILE) => File::DELETED_FILE ),
__METHOD__, array( 'FOR UPDATE' ) );
}
// Fixme -- encapsulate this sort of query-building.
$dbr = wfGetDB( DB_SLAVE );
$encIp = $dbr->addQuotes( IP::sanitizeIP($this->ip) );
- $encRange = $dbr->addQuotes( "$range%" );
$encAddr = $dbr->addQuotes( $iaddr );
$conds[] = "(ipb_address = $encIp) OR
- (ipb_range_start LIKE $encRange AND
+ (ipb_range_start" . $dbr->buildLike( $range, $dbr->anyString() ) . " AND
ipb_range_start <= $encAddr
AND ipb_range_end >= $encAddr)";
} else {
*/
static function mungeQuery( $query , $prot ) {
$field = 'el_index';
- $rv = LinkFilter::makeLike( $query , $prot );
+ $rv = LinkFilter::makeLikeArray( $query , $prot );
if ($rv === false) {
//makeLike doesn't handle wildcard in IP, so we'll have to munge here.
if (preg_match('/^(:?[0-9]{1,3}\.)+\*\s*$|^(:?[0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]*\*\s*$/', $query)) {
- $rv = $prot . rtrim($query, " \t*") . '%';
+ $rv = array( $prot . rtrim($query, " \t*"), $dbr->anyString() );
$field = 'el_to';
}
}
/* strip everything past first wildcard, so that index-based-only lookup would be done */
list( $munged, $clause ) = self::mungeQuery( $this->mQuery, $this->mProt );
- $stripped = substr($munged,0,strpos($munged,'%')+1);
- $encSearch = $dbr->addQuotes( $stripped );
+ $stripped = LinkFilter::keepOneWildcard( $munged );
+ $like = $dbr->buildLike( $stripped );
$encSQL = '';
if ( isset ($this->mNs) && !$wgMiserMode )
$externallinks $use_index
WHERE
page_id=el_from
- AND $clause LIKE $encSearch
+ AND $clause $like
$encSQL";
}
$nt = Title::newFromUrl( $search );
if( $nt ) {
$dbr = wfGetDB( DB_SLAVE );
- $m = $dbr->strencode( strtolower( $nt->getDBkey() ) );
- $m = str_replace( "%", "\\%", $m );
- $m = str_replace( "_", "\\_", $m );
- $this->mQueryConds = array( "LOWER(img_name) LIKE '%{$m}%'" );
+ $this->mQueryConds = array( 'LOWER(img_name)' . $dbr->buildLike( $dbr->anyString(),
+ strtolower( $nt->getDBkey() ), $dbr->anyString() ) );
}
}
)
) ) {
$conds = array(
- 'page_title LIKE '.$dbr->addQuotes( $dbr->escapeLike( $ot->getDBkey() ) . '/%' )
+ 'page_title' . $dbr->buildLike( $ot->getDBkey() . '/', $dbr->anyString() )
.' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
);
$conds['page_namespace'] = array();
if ( $wpIlMatch != '' && !$wgMiserMode) {
$nt = Title::newFromUrl( $wpIlMatch );
if( $nt ) {
- $m = $dbr->escapeLike( strtolower( $nt->getDBkey() ) );
- $where[] = "LOWER(img_name) LIKE '%{$m}%'";
+ $where[] = 'LOWER(img_name) ' . $dbr->buildLike( $dbr->anyString(), strtolower( $nt->getDBkey() ), $dbr->anyString() );
$searchpar['wpIlMatch'] = $wpIlMatch;
}
}
array( 'page_namespace', 'page_title', 'page_is_redirect' ),
array(
'page_namespace' => $namespace,
- 'page_title LIKE \'' . $dbr->escapeLike( $prefixKey ) .'%\'',
+ 'page_title' . $dbr->buildLike( $prefixKey, $dbr->anyString() ),
'page_title >= ' . $dbr->addQuotes( $fromKey ),
),
__METHOD__,
$title = Title::newFromText( $prefix );
if( $title ) {
$ns = $title->getNamespace();
- $encPrefix = $dbr->escapeLike( $title->getDBkey() );
+ $prefix = $title->getDBkey();
} else {
// Prolly won't work too good
// @todo handle bare namespace names cleanly?
$ns = 0;
- $encPrefix = $dbr->escapeLike( $prefix );
}
$conds = array(
'ar_namespace' => $ns,
- "ar_title LIKE '$encPrefix%'",
+ 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
);
return self::listPages( $dbr, $conds );
}
function getSQL() {
$dbr = wfGetDB( DB_SLAVE );
list( $page, $langlinks ) = $dbr->tableNamesN( 'page', 'langlinks' );
- $prefix = $this->prefix ? "AND page_title LIKE '" . $dbr->escapeLike( $this->prefix ) . "%'" : '';
+ $prefix = $this->prefix ? 'AND page_title' . $dbr->buildLike( $this->prefix , $dbr->anyString() ) : '';
return
"SELECT 'Withoutinterwiki' AS type,
page_namespace AS namespace,
$wgUser->addToDatabase();
}
$spec = $this->getArg();
- $like = LinkFilter::makeLike( $spec );
+ $like = LinkFilter::makeLikeArray( $spec );
if ( !$like ) {
$this->error( "Not a valid hostname specification: $spec", true );
}
$dbr = wfGetDB( DB_SLAVE, array(), $wikiID );
$count = $dbr->selectField( 'externallinks', 'COUNT(*)',
- array( 'el_index LIKE ' . $dbr->addQuotes( $like ) ), __METHOD__ );
+ array( 'el_index' . $dbr->buildLike( $like ) ), __METHOD__ );
if ( $count ) {
$found = true;
passthru( "php cleanupSpam.php --wiki='$wikiID' $spec | sed 's/^/$wikiID: /'" );
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'externallinks', array( 'DISTINCT el_from' ),
- array( 'el_index LIKE ' . $dbr->addQuotes( $like ) ), __METHOD__ );
+ array( 'el_index' . $dbr->buildLike( $like ) ), __METHOD__ );
$count = $dbr->numRows( $res );
$this->output( "Found $count articles containing $spec\n" );
foreach ( $res as $row ) {
while (1) {
wfWaitForSlaves( 2 );
$db->commit();
- $encServer = $db->escapeLike( $wgServer );
- $q="DELETE /* deleteSelfExternals */ FROM externallinks WHERE el_to LIKE '$encServer/%' LIMIT 1000\n";
+ $q = $db->limitResult( "DELETE /* deleteSelfExternals */ FROM externallinks WHERE el_to"
+ . $db->buildLike( $wgServer . '/', $db->anyString() ), 1000 );
print "Deleting a batch\n";
$db->query($q);
if (!$db->affectedRows()) exit(0);
$table = $this->db->tableName( $page );
$prefix = $this->db->strencode( $name );
- $likeprefix = str_replace( '_', '\\_', $prefix);
$encNamespace = $this->db->addQuotes( $ns );
$titleSql = "TRIM(LEADING '$prefix:' FROM {$page}_title)";
$titleSql AS title
FROM {$table}
WHERE {$page}_namespace=0
- AND {$page}_title LIKE '$likeprefix:%'";
+ AND {$page}_title " . $this->db->buildLike( $name . ':', $this-db->anyString() );
$result = $this->db->query( $sql, __METHOD__ );
# overwriting bulk storage concat rows. Don't compress external references, because
# the script doesn't yet delete rows from external storage.
$conds = array(
- "old_flags NOT LIKE '%object%' AND old_flags NOT LIKE '%external%'");
+ 'old_flags NOT ' . $dbr->buildLike( MATCH_STRING, 'object', MATCH_STRING ) . ' AND old_flags NOT '
+ . $dbr->buildLike( MATCH_STRING, 'external', MATCH_STRING ) );
if ( $beginDate ) {
if ( !preg_match( '/^\d{14}$/', $beginDate ) ) {
$res = $dbr->select( 'text', array( 'old_id', 'old_flags', 'old_text' ),
array(
"old_id BETWEEN $blockStart AND $blockEnd",
- "old_flags NOT LIKE '%external%'",
+ 'old_flags NOT ' . $dbr->buildLike( $dbr->anyString(), 'external', $dbr->anyString() ),
), $fname );
while ( $row = $dbr->fetchObject( $res ) ) {
# Resolve stubs
# Get the (maybe) external row
$externalRow = $dbr->selectRow( 'text', array( 'old_text' ),
- array( 'old_id' => $stub->mOldId, "old_flags LIKE '%external%'" ),
+ array( 'old_id' => $stub->mOldId, 'old_flags' . $dbr->buildLike( $dbr->anyString(), 'external', $dbr->anyString() ) ),
$fname
);
if ( $this->textClause != '' ) {
$this->textClause .= ' OR ';
}
- $this->textClause .= 'old_text LIKE ' . $dbr->addQuotes( $dbr->escapeLike( "DB://$cluster/" ) . '%' );
+ $this->textClause .= 'old_text' . $dbr->buildLike( "DB://$cluster/", $dbr->anyString() );
}
}
return $this->textClause;
'rev_id > ' . $dbr->addQuotes( $startId ),
'rev_text_id=old_id',
$textClause,
- "old_flags LIKE '%external%'",
+ 'old_flags ' . $dbr->buildLike( $dbr->anyString(), 'external', $dbr->anyString() ),
),
__METHOD__,
array(
array(
'old_id>' . $dbr->addQuotes( $startId ),
$textClause,
- "old_flags LIKE '%external%'",
+ 'old_flags ' . $dbr->buildLike( $dbr->anyString(), 'external', $dbr->anyString() ),
'bt_text_id IS NULL'
),
__METHOD__,