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