Skip to content

Commit

Permalink
Support search using regex
Browse files Browse the repository at this point in the history
When user searches a term starting and ending with a forward slash,
the items will be matched against the regular expression between the slashes.

The syntax of the regular expressions is database-engine specific:

* MySQL uses POSIX extended https://dev.mysql.com/doc/refman/5.7/en/regexp.html
* PostgreSQL uses POSIX extended with some extensions https://www.sqlite.org/lang_expr.html#the_like_glob_regexp_and_match_operators
* SQLite uses PHP’s PCRE https://www.postgresql.org/docs/12/functions-matching.html#FUNCTIONS-POSIX-REGEXP, https://www.php.net/manual/en/book.pcre.php
  • Loading branch information
jtojnar committed Jul 14, 2020
1 parent 664481d commit 83f78e5
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 4 deletions.
27 changes: 27 additions & 0 deletions src/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,33 @@
'constructParams' => $dbParams
]));

// Define regexp function for SQLite
if ($f3->get('db_type') === 'sqlite') {
$dice->addRule(DB\SQL::class, [
'call' => [
[
// DB\SQL uses PDO instance through composition
// and forwards calls of non-existent methods to it.
// But Dice can only call existing methods.
// Let’s walk around these limitations by directly
// calling the __call magic method.
'__call',
[
// https://www.sqlite.org/lang_expr.html#the_like_glob_regexp_and_match_operators
'sqliteCreateFunction',
[
'regexp',
function($pattern, $text) {
return preg_match('/' . addcslashes($pattern, '/') . '/', $text);
},
2
]
]
],
]
]);
}

// Fallback rule
$dice->addRule('*', $substitutions);

Expand Down
19 changes: 19 additions & 0 deletions src/daos/StatementsInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ public static function isTrue($column);
*/
public static function isFalse($column);

/**
* Combine expressions using OR operator.
*
* @param string ...$exprs expressions to combine
*
* @return string combined expression
*/
public static function or(...$exprs);

/**
* check if CSV column matches a value.
*
Expand Down Expand Up @@ -112,4 +121,14 @@ public static function ensureRowTypes(array $rows, array $expectedRowTypes);
* @return string
*/
public static function csvRow(array $a);

/**
* Match a value to a regular expression.
*
* @param string $value value to match
* @param string $regex regular expression
*
* @return string expression for matching
*/
public static function matchesRegex($value, $regex);
}
11 changes: 8 additions & 3 deletions src/daos/mysql/Items.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,14 @@ public function get($options = []) {

// search
if (isset($options['search']) && strlen($options['search']) > 0) {
$search = implode('%', \helpers\Search::splitTerms($options['search']));
$params[':search'] = $params[':search2'] = $params[':search3'] = ['%' . $search . '%', \PDO::PARAM_STR];
$where[] = '(items.title LIKE :search OR items.content LIKE :search2 OR sources.title LIKE :search3) ';
if (preg_match('#^/(?P<regex>.+)/$#', $options['search'], $matches)) {
$params[':search'] = $params[':search2'] = $params[':search3'] = [$matches['regex'], \PDO::PARAM_STR];
$where[] = $stmt::or($stmt::matchesRegex('items.title', ':search'), $stmt::matchesRegex('items.content', ':search2'), $stmt::matchesRegex('sources.title', ':search3'));
} else {
$search = implode('%', \helpers\Search::splitTerms($options['search']));
$params[':search'] = $params[':search2'] = $params[':search3'] = ['%' . $search . '%', \PDO::PARAM_STR];
$where[] = '(items.title LIKE :search OR items.content LIKE :search2 OR sources.title LIKE :search3) ';
}
}

// tag filter
Expand Down
24 changes: 24 additions & 0 deletions src/daos/mysql/Statements.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ public static function isFalse($column) {
return "$column=0";
}

/**
* Combine expressions using OR operator.
*
* @param string ...$exprs expressions to combine
*
* @return string combined expression
*/
public static function or(...$exprs) {
return '(' . implode(' OR ', $exprs) . ')';
}

/**
* check if CSV column matches a value.
*
Expand Down Expand Up @@ -199,4 +210,17 @@ public static function csvRow(array $a) {

return implode(',', $filtered);
}

/**
* Match a value to a regular expression.
*
* @param string $value value to match
* @param string $regex regular expression
*
* @return string expression for matching
*/
public static function matchesRegex($value, $regex) {
// https://dev.mysql.com/doc/refman/5.7/en/regexp.html
return $value . ' REGEXP ' . $regex;
}
}
13 changes: 13 additions & 0 deletions src/daos/pgsql/Statements.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,17 @@ public static function ensureRowTypes(array $rows, array $expectedRowTypes) {

return $rows;
}

/**
* Match a value to a regular expression.
*
* @param string $value value to match
* @param string $regex regular expression
*
* @return string expression for matching
*/
public static function matchesRegex($value, $regex) {
// https://www.postgresql.org/docs/12/functions-matching.html#FUNCTIONS-POSIX-REGEXP
return $value . ' ~ ' . $regex;
}
}
13 changes: 13 additions & 0 deletions src/daos/sqlite/Statements.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,17 @@ public static function datetime($datestr) {

return $date->format('Y-m-d H:i:s');
}

/**
* Match a value to a regular expression.
*
* @param string $value value to match
* @param string $regex regular expression
*
* @return string expression for matching
*/
public static function matchesRegex($value, $regex) {
// https://www.sqlite.org/lang_expr.html#the_like_glob_regexp_and_match_operators
return $value . ' REGEXP ' . $regex;
}
}
2 changes: 1 addition & 1 deletion src/helpers/ViewHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static function highlight($content, $searchWords) {
}

foreach ($searchWords as $word) {
$content = preg_replace('/(?!<[^<>])(' . $word . ')(?![^<>]*>)/i', '<span class="found">$0</span>', $content);
$content = preg_replace('/(?!<[^<>])(' . preg_quote($word, '/') . ')(?![^<>]*>)/i', '<span class="found">$0</span>', $content);
}

return $content;
Expand Down

0 comments on commit 83f78e5

Please sign in to comment.