packages/ztd-query-mysql/src/Rewrite/FullText/ExpressionEditor.php

1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Platform\MySql\Rewrite\FullText;
6
7use ZtdQuery\Platform\MySql\Sql\MySqlLexerProfile;
8use ZtdQuery\Sql\SqlToken;
9use ZtdQuery\Sql\SqlTokenStream;
10
11/**
12 * Expression Editor.
13 *
14 * @visibility ZtdQuery\Platform\MySql
15 */
16final class ExpressionEditor
17{
18    /**
19     * @return array{start: int, end: int, replacement: string}|null
20     */
21    public function expressionEdit(string $sql, SqlTokenStream $stream, SqlToken $match): ?array
22    {
23        $columnsOpen = $stream->significantTokenAfter($match);
24        if ($columnsOpen === null) {
25            return null;
26        }
27        $columnsClose = $stream->matchingClosingNestingToken($columnsOpen);
28        if ($columnsClose === null) {
29            return null;
30        }
31        $against = $stream->significantTokenAfter($columnsClose);
32        if ($against === null || !$against->isKeyword('AGAINST')) {
33            return null;
34        }
35        $queryOpen = $stream->significantTokenAfter($against);
36        if ($queryOpen === null) {
37            return null;
38        }
39        $queryClose = $stream->matchingClosingNestingToken($queryOpen);
40        if ($queryClose === null) {
41            return null;
42        }
43        $columnsSql = substr($sql, $columnsOpen->endOffset(), $columnsClose->offset - $columnsOpen->endOffset());
44        $columns = SqlTokenStream::tokenize($columnsSql, MySqlLexerProfile::create())->splitTopLevel();
45        if ($columns === []) {
46            return null;
47        }
48
49        $queryBody = substr($sql, $queryOpen->endOffset(), $queryClose->offset - $queryOpen->endOffset());
50        $querySql = $this->queryExpression($queryBody);
51        if ($querySql === '') {
52            return null;
53        }
54
55        $documentParts = array_map(
56            static fn (string $column): string => "COALESCE(CAST(($column) AS CHAR), '')",
57            $columns,
58        );
59        $document = "LOWER(CONCAT_WS(' ', " . implode(', ', $documentParts) . '))';
60        $needle = "LOWER(NULLIF(TRIM(CAST(($querySql) AS CHAR)), ''))";
61        $replacement = "(CASE WHEN LOCATE($needle, $document) > 0 THEN 1.0 ELSE 0.0 END)";
62
63        return [
64            'start' => $match->offset,
65            'end' => $queryClose->endOffset(),
66            'replacement' => $replacement,
67        ];
68    }
69
70    /**
71     * Query Expression for the supplied MySQL input.
72     */
73    public function queryExpression(string $queryBody): string
74    {
75        $previous = null;
76        foreach (SqlTokenStream::tokenize($queryBody, MySqlLexerProfile::create())->significantTokens() as $token) {
77            if (!$token->isTopLevel()) {
78                continue;
79            }
80            if ($previous !== null && $previous->isKeyword('IN') && ($token->isKeyword('NATURAL') || $token->isKeyword('BOOLEAN'))) {
81                return trim(substr($queryBody, 0, $previous->offset));
82            }
83            if ($previous !== null && $previous->isKeyword('WITH') && $token->isKeyword('QUERY')) {
84                return trim(substr($queryBody, 0, $previous->offset));
85            }
86            $previous = $token;
87        }
88
89        return trim($queryBody);
90    }
91}
92