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