diff --git a/core/imageboard/search.php b/core/imageboard/search.php index 0377f9cc..b89b70a6 100644 --- a/core/imageboard/search.php +++ b/core/imageboard/search.php @@ -6,6 +6,21 @@ namespace Shimmie2; use GQLA\Query; +/** + * A small chunk of SQL code + parameters, to be used in a larger query + * + * eg + * + * $q = new Querylet("SELECT * FROM images"); + * $q->append(new Querylet(" WHERE id = :id", ["id" => 123])); + * $q->append(new Querylet(" AND rating = :rating", ["rating" => "safe"])); + * $q->append(new Querylet(" ORDER BY id DESC")); + * + * becomes + * + * SELECT * FROM images WHERE id = :id AND rating = :rating ORDER BY id DESC + * ["id" => 123, "rating" => "safe"] + */ class Querylet { /** @@ -25,6 +40,9 @@ class Querylet } } +/** + * When somebody has searched for a tag, "cat", "cute", "-angry", etc + */ class TagCondition { public function __construct( @@ -34,6 +52,10 @@ class TagCondition } } +/** + * When somebody has searched for a specific image property, like "rating:safe", + * "id:123", "width:100", etc + */ class ImgCondition { public function __construct( @@ -45,10 +67,19 @@ class ImgCondition class Search { - /** @var list */ + /** + * The search code is dark and full of horrors, and it's not always clear + * what's going on. This is a list of the steps that the search code took + * to find the images that it returned. + * + * @var list + */ public static array $_search_path = []; /** + * Build a search query for a given set of tags and return + * the results as a PDOStatement (raw SQL rows) + * * @param list $tags */ private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags = []): \FFSPHP\PDOStatement @@ -203,10 +234,13 @@ class Search /** * Turn a human input string into a an abstract search query * + * (This is only public for testing purposes, nobody should be calling this + * directly from outside this class) + * * @param string[] $terms * @return array{0: TagCondition[], 1: ImgCondition[], 2: string} */ - private static function terms_to_conditions(array $terms): array + public static function terms_to_conditions(array $terms): array { global $config; @@ -234,10 +268,13 @@ class Search /** * Turn an abstract search query into an SQL Querylet * + * (This is only public for testing purposes, nobody should be calling this + * directly from outside this class) + * * @param TagCondition[] $tag_conditions * @param ImgCondition[] $img_conditions */ - private static function build_search_querylet( + public static function build_search_querylet( array $tag_conditions, array $img_conditions, ?string $order = null, diff --git a/ext/index/main.php b/ext/index/main.php index 3bf49f95..d1bbfdf3 100644 --- a/ext/index/main.php +++ b/ext/index/main.php @@ -6,6 +6,7 @@ namespace Shimmie2; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\{InputInterface,InputArgument}; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; require_once "config.php"; @@ -149,6 +150,46 @@ class Index extends Extension foreach ($items as $item) { $output->writeln($item->hash); } + return Command::SUCCESS; + }); + $event->app->register('debug:search') + ->addArgument('query', InputArgument::REQUIRED) + ->addOption('count', null, InputOption::VALUE_NONE, 'Generate a count-only query') + ->addOption('page', null, InputOption::VALUE_REQUIRED, 'Page number', default: 1) + ->addOption('limit', null, InputOption::VALUE_REQUIRED, 'Number of results per page', default: 25) + ->setDescription('Show the SQL generated for a given search query') + ->setCode(function (InputInterface $input, OutputInterface $output): int { + $search = Tag::explode($input->getArgument('query'), false); + $page = $input->getOption('page'); + $limit = $input->getOption('limit'); + $count = $input->getOption('count'); + + [$tag_conditions, $img_conditions, $order] = Search::terms_to_conditions($search); + if($count) { + $order = null; + $page = null; + $limit = null; + } + + $q = Search::build_search_querylet( + $tag_conditions, + $img_conditions, + $order, + $limit, + (int)(($page - 1) * $limit), + ); + + $sql_str = $q->sql; + $sql_str = preg_replace("/\s+/", " ", $sql_str); + foreach($q->variables as $key => $val) { + if(is_string($val)) { + $sql_str = str_replace(":$key", "'$val'", $sql_str); + } else { + $sql_str = str_replace(":$key", (string)$val, $sql_str); + } + } + $output->writeln(trim($sql_str)); + return Command::SUCCESS; }); }