packages/ztd-query-sqlite/src/Rewrite/FullText/SqliteFullTextSearchRewriter.php

1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Platform\Sqlite\Rewrite\FullText;
6
7use ZtdQuery\Sql\SqlTokenStream;
8
9/**
10 * Replaces FTS virtual-table operators with expressions executable over shadow CTE rows.
11 *
12 * @phpstan-type TableContext array{viewSql: string}|array{columns: array<int, string>}
13 */
14final class SqliteFullTextSearchRewriter
15{
16    private \ZtdQuery\Platform\Sqlite\Sql\SqliteIdentifierQuoter $quoter;
17    private \ZtdQuery\Platform\Sqlite\Sql\SqliteParser $parser;
18
19    /**
20     * Binds the dependencies used by this operation.
21     */
22    public function __construct()
23    {
24        $this->quoter = new \ZtdQuery\Platform\Sqlite\Sql\SqliteIdentifierQuoter();
25        $this->parser = new \ZtdQuery\Platform\Sqlite\Sql\SqliteParser();
26    }
27
28    /**
29     * @param array<string, TableContext> $tables
30     */
31    public function rewrite(string $sql, array $tables): string
32    {
33        $stream = SqlTokenStream::tokenize($sql, \ZtdQuery\Platform\Sqlite\Sql\SqliteLexerProfile::create());
34        /**
35         * @var list<array{start: int, end: int, replacement: string}> $edits
36         */
37        $edits = [];
38
39        foreach ($stream->significantTokens() as $operator) {
40            $edit = (new MatchExpressionRewriter($this->parser, $this->quoter))->expressionEdit($stream, $operator, $tables);
41            if ($edit === null) {
42                continue;
43            }
44            $edits[] = $edit;
45        }
46
47        usort($edits, static fn (array $left, array $right): int => $right['start'] <=> $left['start']);
48        foreach ($edits as $edit) {
49            $sql = substr_replace($sql, $edit['replacement'], $edit['start'], $edit['end'] - $edit['start']);
50        }
51
52        return $sql;
53    }
54
55}
56