Search/Indexing

* move fulltext indizes for tables /w ~10k+ rows to separate tables
   > sounds have ~12k but names are effectively incompatible with FTI
 * normalize searchable strings to catch edgecases
   > preparse spell descriptions + buffs
   > move effect text cols of items to new table
 * also fix extended search of creatures, spells & quests
This commit is contained in:
Sarjuuk 2026-03-03 14:58:19 +01:00
parent 2161a7b846
commit 5a230daad6
28 changed files with 648 additions and 165 deletions

View file

@ -144,6 +144,9 @@ abstract class DBTypeList
if ($h = array_filter(array_column($this->queryOpts, 'h')))
$this->queryBase .= ' HAVING '.implode(' AND ', $h);
// fill in locale
$this->queryBase = str_replace(['DB_LOC_I', 'DB_LOC_S'], [Lang::getLocale()->value, '"'.Lang::getLocale()->json().'"'], $this->queryBase);
// without applied LIMIT and ORDER
if ($calcTotal)
$totalQuery = $this->queryBase;
@ -227,12 +230,12 @@ abstract class DBTypeList
$literal = false;
if (is_array($expOrField))
if (is_array($expOrField) && $op != 'MATCH')
$field = $this->resolveCondition($expOrField, $supLink);
else
{
// basic formulas ex: [((minGold + maxGold) / 2), 0, '>']
if (preg_match('/^\([\s\+\-\*\/\w\(\)\.]+\)$/i', strtr($expOrField, ['`' => '', '´' => '', '--' => ''])))
if (is_string($expOrField) && preg_match('/^\([\s\+\-\*\/\w\(\)\.]+\)$/i', strtr($expOrField, ['`' => '', '´' => '', '--' => ''])))
{
$field = preg_replace_callback('/[\w\]*\.?[\w]+/i', $this->setColPrefix(...), $expOrField);
$literal = true;
@ -258,21 +261,20 @@ abstract class DBTypeList
if (!$expr)
return null;
// [[flags, 0x4, '&'], 0] -> (`flags` & 4) = 0
if (is_array($field)) // $field is expression
return [...$field, $expr, $value];
if ($op == 'MATCH' && gettype($value) == 'array')
return ['MATCH(%n)', $field, 'AGAINST(%s IN BOOLEAN MODE)', DB::Aowow()->translate($value)];
if (($op == 'LIKE' || $op == 'NOT LIKE') && gettype($value) == 'string')
return ['%n', $field, $op, '%~like~', $value];
if (is_array($field)) // $field is expression: [[flags, 0x4, '&'], 0] -> (`flags` & 4) = 0
return [...$field, $expr, $value];
return [$literal ? '%SQL' : '%n', $field, $expr, $value];
}
private function setColPrefix(mixed $colName) : ?string
private function setColPrefix(mixed $colName) : null|string|array
{
if (is_array($colName))
$colName = $colName[0];
return array_filter(array_map([$this, 'setColPrefix'], $colName)) ?: null;
// numeric allows for formulas e.g. (1 < 3)
if (Util::checkNumeric($colName))

View file

@ -113,7 +113,7 @@ abstract class Filter
[self::CR_BOOLEAN, <string:colName>, <bool:isString>, null]
[self::CR_FLAG, <string:colName>, <int:testBit>, <bool:matchAny>] # default param2: matchExact
[self::CR_NUMERIC, <string:colName>, <int:NUM_FLAGS>, <bool:addExtraCol>]
[self::CR_STRING, <string:colName>, <int:STR_FLAGS>, null]
[self::CR_STRING, <string:colName>, <int:STR_FLAGS>, <string:fulltextColName]
[self::CR_ENUM, <string:colName>, <bool:ANY_NONE>, <bool:isEnumVal>] # param3 ? crv is val in enum : key in enum
[self::CR_STAFFFLAG, <string:colName>, null, null]
[self::CR_CALLBACK, <string:fnName>, <mixed:param1>, <mixed:param2>]
@ -600,7 +600,13 @@ abstract class Filter
return null;
// if the fulltext token contains invalid chars, should it be sub-tokenized (current behavior) or should the chars just be stripped
$ft = array_filter(explode(' ', preg_replace(self::PATTERN_FT, ' ', $str)), $lenTest);
if ($tok = explode(' ', preg_replace(self::PATTERN_FT, ' ', $str)))
{
$ft = array_filter($tok, $lenTest);
if (count($tok) > 1)
$ft[] = implode('', $tok);
}
// escape manually entered _; entering % should be prohibited
// then replace search wildcards with sql wildcards
@ -646,7 +652,7 @@ abstract class Filter
protected function buildLikeLookup(array $fields, bool $exact = false) : array
{
$qry = [];
foreach ($fields as $field => $col)
foreach ($fields as [$field, $col])
{
$sub = [];
if (!empty($this->inTokens[$field]))
@ -669,17 +675,25 @@ abstract class Filter
protected function buildMatchLookup(array $fields, bool $exact = false) : array
{
if (Lang::getLocale()->isLogographic() && !Cfg::get('LOGOGRAPHIC_FT_SEARCH'))
return $this->buildLikeLookup($fields, $exact);
return [];
$qry = [];
foreach ($fields as $field => $col)
foreach ($fields as [$field, $col])
{
if (!empty($this->ftTokens[$field]))
$qry[] = [$col, $this->ftTokens[$field], 'MATCH'];
$tok = $this->values[$field];
if (self::transformToken($tok))
$qry[] = [$col, $tok];
$qry[] = [$col, array_unique($this->ftTokens[$field]), 'MATCH'];
else
{
$tok = $this->values[$field];
if (self::transformToken($tok))
{
if (!is_array($col))
$qry[] = [$col, $tok];
else
foreach ($col as $c)
$qry[] = [$c, $tok];
}
}
}
return $qry ? [DB::OR, ...$qry] : [];
@ -754,14 +768,19 @@ abstract class Filter
return [[$field, $value, '&'], $value];
}
private function genericString(string $field, ?int $strFlags) : ?array
private function genericString(string $field, ?int $strFlags, ?string $fulltextCol = null) : ?array
{
$strFlags ??= 0x0;
$lkCol = $field;
if ($strFlags & STR_LOCALIZED)
$field .= '_loc'.Lang::getLocale()->value;
$lkCol .= '_loc'.Lang::getLocale()->value;
return $this->buildLikeLookup([$field => $field]);
$lookup = null;
if ($fulltextCol)
$lookup = $this->buildMatchLookup([[$lkCol, $fulltextCol]]);
return $lookup ?: ($field ? $this->buildLikeLookup([[$lkCol, $lkCol]]) : null);
}
private function genericNumeric(string $field, int|float $value, int $op, int $typeCast) : ?array
@ -837,7 +856,7 @@ abstract class Filter
self::CR_FLAG => $this->genericBooleanFlags($colOrFn, $param1, $crs, $param2),
self::CR_STAFFFLAG => $this->genericBooleanFlags($colOrFn, (1 << ($crs - 1)), true),
self::CR_BOOLEAN => $this->genericBoolean($colOrFn, $crs, !empty($param1)),
self::CR_STRING => $this->genericString($colOrFn, $param1),
self::CR_STRING => $this->genericString($colOrFn, $param1, $param2),
self::CR_CALLBACK => $this->{$colOrFn}($cr, $crs, $crv, $param1, $param2),
self::CR_ENUM => $handleEnum($cr, $crs, $colOrFn, $param1, $param2),
self::CR_NYI_PH => $handleNYIPH($crs, $crv, $param1),

View file

@ -162,32 +162,21 @@ class Search
return $qry;
}
private function createMatchLookup(array $fields = []) : array
private function createMatchLookup() : array
{
if ($this->idSearch && $this->included)
return ['id', $this->included];
if (Lang::getLocale()->isLogographic() && !Cfg::get('LOGOGRAPHIC_FT_SEARCH'))
return $this->createLikeLookup($fields);
return $this->createLikeLookup();
// default to name-field
if (!$fields)
$fields[] = 'name_loc'.Lang::getLocale()->value;
$qry = [];
if ($this->fulltext)
$qry = array_map(fn($x) => [$x, $this->fulltext, 'MATCH'], $fields);
return ['nml.nName', $this->fulltext, 'MATCH'];
else if ($strBak = trim($this->query))
if (mb_strlen($strBak) > 2 || Lang::getLocale()->isLogographic())
$qry = array_map(fn($x) => [$x, $strBak], $fields);
return ['name_loc'.Lang::getLocale()->value, $strBak];
// single cnd?
if (count($qry) > 1)
array_unshift($qry, DB::OR);
else if (count($qry) == 1)
$qry = $qry[0];
return $qry;
return [];
}
public function canPerform() : bool

View file

@ -328,9 +328,9 @@ class AchievementListFilter extends Filter
{
$_ = [];
if ($_v['ex'] == 'on')
$_ = $this->buildLikeLookup(['na' => 'name_loc'.Lang::getLocale()->value, 'na' => 'reward_loc'.Lang::getLocale()->value, 'na' => 'description_loc'.Lang::getLocale()->value]);
$_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value], ['na', 'reward_loc'.Lang::getLocale()->value], ['na', 'description_loc'.Lang::getLocale()->value]]);
else
$_ = $this->buildLikeLookup(['na' => 'name_loc'.Lang::getLocale()->value]);
$_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]);
if ($_)
$parts[] = $_;

View file

@ -85,7 +85,7 @@ class AreaTriggerListFilter extends Filter
// name [str]
if ($_v['na'])
if ($_ = $this->buildLikeLookup(['na' => 'name']))
if ($_ = $this->buildLikeLookup([['na', 'name']]))
$parts[] = $_;
// type [list]

View file

@ -73,7 +73,7 @@ class ArenaTeamListFilter extends Filter
// name [str]
if ($_v['na'])
if ($_ = $this->buildLikeLookup(['na' => 'at.name'], $_v['ex'] == 'on'))
if ($_ = $this->buildLikeLookup([['na', 'at.name']], $_v['ex'] == 'on'))
$parts[] = $_;
// side [list]

View file

@ -17,6 +17,7 @@ class CreatureList extends DBTypeList
protected string $queryBase = 'SELECT ct.*, ct.`id` AS ARRAY_KEY FROM ::creature ct';
public array $queryOpts = array(
'ct' => [['ft', 'qse', 'dct1', 'dct2', 'dct3'], 's' => ', IFNULL(dct1.`id`, IFNULL(dct2.`id`, IFNULL(dct3.`id`, 0))) AS "parentId", IFNULL(dct1.`name_loc0`, IFNULL(dct2.`name_loc0`, IFNULL(dct3.`name_loc0`, ""))) AS "parent_loc0", IFNULL(dct1.`name_loc2`, IFNULL(dct2.`name_loc2`, IFNULL(dct3.`name_loc2`, ""))) AS "parent_loc2", IFNULL(dct1.`name_loc3`, IFNULL(dct2.`name_loc3`, IFNULL(dct3.`name_loc3`, ""))) AS "parent_loc3", IFNULL(dct1.`name_loc4`, IFNULL(dct2.`name_loc4`, IFNULL(dct3.`name_loc4`, ""))) AS "parent_loc4", IFNULL(dct1.`name_loc6`, IFNULL(dct2.`name_loc6`, IFNULL(dct3.`name_loc6`, ""))) AS "parent_loc6", IFNULL(dct1.name_loc8, IFNULL(dct2.`name_loc8`, IFNULL(dct3.`name_loc8`, ""))) AS "parent_loc8", IF(dct1.`difficultyEntry1` = ct.`id`, 1, IF(dct2.`difficultyEntry2` = ct.`id`, 2, IF(dct3.`difficultyEntry3` = ct.`id`, 3, 0))) AS "difficultyMode"'],
'nml' => ['j' => ['::creature_search nml ON nml.`id` = ct.`id` AND nml.`locale` = DB_LOC_I']],
'dct1' => ['j' => ['::creature dct1 ON ct.`cuFlags` & 0x02 AND dct1.`difficultyEntry1` = ct.`id`', true]],
'dct2' => ['j' => ['::creature dct2 ON ct.`cuFlags` & 0x02 AND dct2.`difficultyEntry2` = ct.`id`', true]],
'dct3' => ['j' => ['::creature dct3 ON ct.`cuFlags` & 0x02 AND dct3.`difficultyEntry3` = ct.`id`', true]],
@ -364,15 +365,19 @@ class CreatureListFilter extends Filter
// name [str]
if ($_v['na'])
{
if ($_v['ex'] == 'on')
if ($_ = $this->buildLikeLookup(['na' => 'subname_loc'.Lang::getLocale()->value]))
$parts[] = $_;
$f = [['na', ['nml.nName', 'nml.nSubname']]];
if ($_v['ex'] != 'on')
$f = [['na', 'nml.nName']];
if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildMatchLookup($f))
$parts[] = $_;
else
{
if ($parts)
$parts = [DB::OR, $_, ...$parts];
else
$f = [['na', 'name_loc'.Lang::getLocale()->value], ['na', 'subname_loc'.Lang::getLocale()->value]];
if ($_v['ex'] != 'on')
$f = [$f[0]];
if ($_ = $this->buildLikeLookup($f))
$parts[] = $_;
}
}

View file

@ -249,7 +249,7 @@ class EnchantmentListFilter extends Filter
//string
if ($_v['na'])
if ($_ = $this->buildLikeLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]))
$parts[] = $_;
// type

View file

@ -17,6 +17,7 @@ class GameObjectList extends DBTypeList
protected string $queryBase = 'SELECT o.*, o.`id` AS ARRAY_KEY FROM ::objects o';
protected array $queryOpts = array(
'o' => [['ft', 'qse']],
'nml' => ['j' => ['::objects_search nml ON nml.`id` = o.`id` AND nml.`locale` = DB_LOC_I']],
'ft' => ['j' => ['::factiontemplate ft ON ft.`id` = o.`faction`', true], 's' => ', ft.`factionId`, IFNULL(ft.`A`, 0) AS "A", IFNULL(ft.`H`, 0) AS "H"'],
'qse' => ['j' => ['::quests_startend qse ON qse.`type` = 2 AND qse.`typeId` = o.id', true], 's' => ', IF(MIN(qse.`method`) = 1 OR MAX(qse.`method`) = 3, 1, 0) AS "startsQuests", IF(MIN(qse.`method`) = 2 OR MAX(qse.`method`) = 3, 1, 0) AS "endsQuests"', 'g' => 'o.`id`'],
'qt' => ['j' => '::quests qt ON qse.`questId` = qt.`id`'],
@ -180,8 +181,12 @@ class GameObjectListFilter extends Filter
// name
if ($_v['na'])
if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
{
if ($_ = $this->buildMatchLookup([['na', 'nml.nName']]))
$parts[] = $_;
else if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]))
$parts[] = $_;
}
return $parts;
}

View file

@ -114,7 +114,7 @@ class GuildListFilter extends Filter
// name [str]
if ($_v['na'])
if ($_ = $this->buildLikeLookup(['na' => 'g.name'], $_v['ex'] == 'on'))
if ($_ = $this->buildLikeLookup([['na', 'g.name']], $_v['ex'] == 'on'))
$parts[] = $_;
// side [list]

View file

@ -145,7 +145,7 @@ class IconListFilter extends Filter
//string
if ($_v['na'])
if ($_ = $this->buildLikeLookup(['na' => 'name']))
if ($_ = $this->buildLikeLookup([['na', 'name']]))
$parts[] = $_;
return $parts;

View file

@ -28,6 +28,7 @@ class ItemList extends DBTypeList
protected string $queryBase = 'SELECT i.*, i.`block` AS "tplBlock", i.`armor` AS tplArmor, i.`dmgMin1` AS "tplDmgMin1", i.`dmgMax1` AS "tplDmgMax1", i.`id` AS ARRAY_KEY, i.`id` AS "id" FROM ::items i';
protected array $queryOpts = array( // 3 => Type::ITEM
'i' => [['is', 'src', 'ic'], 'o' => 'i.`quality` DESC, i.`itemLevel` DESC'],
'nml' => ['j' => ['::items_search nml ON nml.`id` = i.`id` AND nml.`locale` = DB_LOC_I']],
'ic' => ['j' => ['::icons `ic` ON `ic`.`id` = `i`.`iconId`', true], 's' => ', ic.`name` AS "iconString"'],
'is' => ['j' => ['::item_stats `is` ON `is`.`type` = 3 AND `is`.`typeId` = `i`.`id`', true], 's' => ', `is`.*'],
's' => ['j' => ['::spell `s` ON `s`.`effect1CreateItemId` = `i`.`id`', true], 'g' => 'i.`id`'],
@ -1917,10 +1918,10 @@ class ItemListFilter extends Filter
101 => [parent::CR_NUMERIC, 'is.rgdhastertng', NUM_CAST_INT, true ], // rgdhastertng
102 => [parent::CR_NUMERIC, 'is.splhastertng', NUM_CAST_INT, true ], // splhastertng
103 => [parent::CR_NUMERIC, 'is.hastertng', NUM_CAST_INT, true ], // hastertng
104 => [parent::CR_STRING, 'description', STR_LOCALIZED ], // flavortext
104 => [parent::CR_STRING, 'description', STR_LOCALIZED, 'nml.nDescription'], // flavortext
105 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_DUNGEON_DROP, 1 ], // dropsinnormal [heroicdungeon-any]
106 => [parent::CR_CALLBACK, 'cbDropsInInstance', SRC_FLAG_DUNGEON_DROP, 2 ], // dropsinheroic [heroicdungeon-any]
107 => [parent::CR_STRING, 'effects', STR_LOCALIZED ], // effecttext [str]
107 => [parent::CR_STRING, '', STR_LOCALIZED, 'nml.nEffects' ], // effecttext [str]
109 => [parent::CR_CALLBACK, 'cbArmorBonus', null, null ], // armorbonus [op] [int]
111 => [parent::CR_NUMERIC, 'requiredSkillRank', NUM_CAST_INT, true ], // reqskillrank
113 => [parent::CR_FLAG, 'cuFlags', CUSTOM_HAS_SCREENSHOT ], // hasscreenshots
@ -2095,8 +2096,12 @@ class ItemListFilter extends Filter
// name
if ($_v['na'])
if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
{
if ($_ = $this->buildMatchLookup([['na', 'nml.nName']]))
$parts[] = $_;
else if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]))
$parts[] = $_;
}
// usable-by (not excluded by requiredClass && armor or weapons match mask from ::classes)
if ($_v['ub'])

View file

@ -199,7 +199,7 @@ class ItemsetListFilter extends Filter
// name [str]
if ($_v['na'])
if ($_ = $this->buildLikeLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildLikeLookup([['na', 'name_loc'.Lang::getLocale()->value]]))
$parts[] = $_;
// quality [enum]

View file

@ -297,7 +297,7 @@ class ProfileListFilter extends Filter
$this->{$prop}['_na'] = array_map(Util::ucWords(...), $this->{$prop}['na']);
};
$parts[] = $this->buildLikeLookup(['na' => $k.'.name', '_na' => $k.'.name'], $_v['ex'] == 'on');
$parts[] = $this->buildLikeLookup([['na', $k.'.name'], ['_na', $k.'.name']], $_v['ex'] == 'on');
}
// side [list]
@ -415,7 +415,7 @@ class ProfileListFilter extends Filter
{
$n = preg_replace(parent::PATTERN_NAME, '', $crv);
if ($this->tokenizeString($cr, $n))
if ($_ = $this->buildLikeLookup([$cr => 'at.name']))
if ($_ = $this->buildLikeLookup([[$cr, 'at.name']]))
return [DB::AND, ['at.type', $size], $_];
return null;

View file

@ -18,6 +18,7 @@ class QuestList extends DBTypeList
protected string $queryBase = 'SELECT q.*, q.`id` AS ARRAY_KEY FROM ::quests q';
protected array $queryOpts = array(
'q' => [],
'nml' => ['j' => '::quests_search nml ON nml.`id` = q.`id` AND nml.`locale` = DB_LOC_I'],
'rsc' => ['j' => '::spell rsc ON q.`rewardSpellCast` = rsc.`id`'], // limit rewardSpellCasts
'qse' => ['j' => '::quests_startend qse ON q.`id` = qse.`questId`', 's' => ', qse.`method`'], // groupConcat..?
'e' => ['j' => ['::events e ON e.`id` = q.`eventId`', true], 's' => ', e.`holidayId`']
@ -506,15 +507,19 @@ class QuestListFilter extends Filter
// name
if ($_v['na'])
{
if ($_v['ex'] == 'on')
if ($_ = $this->buildLikeLookup(['na' => 'objectives_loc'.Lang::getLocale()->value, 'na' => 'details_loc'.Lang::getLocale()->value]))
$parts[] = $_;
$f = [['na', ['nml.nName', 'nml.nObjectives', 'nml.nDetails']]];
if ($_v['ex'] != 'on')
$f = [['na', 'nml.nName']];
if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildMatchLookup($f))
$parts[] = $_;
else
{
if ($parts)
$parts[0][] = $_;
else
$f = [['na', 'name_loc'.Lang::getLocale()->value], ['na', 'objectives_loc'.Lang::getLocale()->value], ['na', 'details_loc'.Lang::getLocale()->value]];
if ($_v['ex'] != 'on')
$f = [$f[0]];
if ($_ = $this->buildLikeLookup($f))
$parts[] = $_;
}
}

View file

@ -115,7 +115,7 @@ class SoundListFilter extends Filter
// name [str]
if ($_v['na'])
if ($_ = $this->buildLikeLookup(['na' => 'name']))
if ($_ = $this->buildLikeLookup([['na', 'name']]))
$parts[] = $_;
// type [list]

View file

@ -105,6 +105,7 @@ class SpellList extends DBTypeList
protected string $queryBase = 'SELECT s.*, s.`id` AS ARRAY_KEY FROM ::spell s';
protected array $queryOpts = array(
's' => [['src', 'sr', 'ic', 'ica']], // 6: Type::SPELL
'nml' => ['j' => ['::spell_search nml ON nml.`id` = s.`id` AND nml.`locale` = DB_LOC_I']],
'ic' => ['j' => ['::icons ic ON ic.`id` = s.`iconId`', true], 's' => ', ic.`name` AS "iconString"'],
'ica' => ['j' => ['::icons ica ON ica.`id` = s.`iconIdAlt`', true], 's' => ', ica.`name` AS "iconStringAlt"'],
'sr' => ['j' => ['::spellrange sr ON sr.`id` = s.`rangeId`'], 's' => ', sr.`rangeMinHostile`, sr.`rangeMinFriend`, sr.`rangeMaxHostile`, sr.`rangeMaxFriend`, sr.`name_loc0` AS "rangeText_loc0", sr.`name_loc2` AS "rangeText_loc2", sr.`name_loc3` AS "rangeText_loc3", sr.`name_loc4` AS "rangeText_loc4", sr.`name_loc6` AS "rangeText_loc6", sr.`name_loc8` AS "rangeText_loc8"'],
@ -2603,18 +2604,22 @@ class SpellListFilter extends Filter
$parts = [];
$_v = &$this->values;
//string (extended)
// string (extended)
if ($_v['na'])
{
if ($_v['ex'] == 'on')
if ($_ = $this->buildLikeLookup(['na' => 'buff_loc'.Lang::getLocale()->value, 'na' => 'description_loc'.Lang::getLocale()->value]))
$parts[] = $_;
$f = [['na', ['nml.nName', 'nml.nBuff', 'nml.nDescription']]];
if ($_v['ex'] != 'on')
$f = [['na', 'nml.nName']];
if ($_ = $this->buildMatchLookup(['na' => 'name_loc'.Lang::getLocale()->value]))
if ($_ = $this->buildMatchLookup($f))
$parts[] = $_;
else
{
if ($parts)
$parts[0][] = $_;
else
$f = [['na', 'name_loc'.Lang::getLocale()->value], ['na', 'buff_loc'.Lang::getLocale()->value], ['na', 'description_loc'.Lang::getLocale()->value]];
if ($_v['ex'] != 'on')
$f = [$f[0]];
if ($_ = $this->buildLikeLookup($f))
$parts[] = $_;
}
}

View file

@ -7,7 +7,7 @@ mb_substitute_character('none'); // drop invalid char
error_reporting(E_ALL);
mysqli_report(MYSQLI_REPORT_ERROR);
define('AOWOW_REVISION', 46);
define('AOWOW_REVISION', 47);
define('OS_WIN', substr(PHP_OS, 0, 3) == 'WIN'); // OS_WIN as per compile info of php
define('CLI', PHP_SAPI === 'cli');
define('CLI_HAS_E', CLI && // WIN10 and later usually support ANSI escape sequences