packages/ztd-query-sqlite/src/Rewrite/Index/IndexHintTokens.php
1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Platform\Sqlite\Rewrite\Index;
6
7use ZtdQuery\Sql\SqlToken;
8use ZtdQuery\Sql\SqlTokenKind;
9
10/**
11 * Locates index hints following a table reference and optional alias.
12 *
13 * @visibility ZtdQuery\Platform\Sqlite
14 */
15final class IndexHintTokens
16{
17 /**
18 * @param list<SqlToken> $tokens
19 * @return array{start: int, end: int}|null
20 */
21 public static function hintRange(array $tokens, int $index): ?array
22 {
23 $keyword = $tokens[$index] ?? null;
24 if ($keyword === null) {
25 return null;
26 }
27
28 if ($keyword->isKeyword('NOT')) {
29 $indexed = $tokens[$index + 1] ?? null;
30 if ($indexed === null) {
31 return null;
32 }
33 if (!$indexed->isKeyword('INDEXED')) {
34 return null;
35 }
36
37 return ['start' => $keyword->offset, 'end' => $indexed->endOffset()];
38 }
39
40 if (!$keyword->isKeyword('INDEXED')) {
41 return null;
42 }
43 $by = $tokens[$index + 1] ?? null;
44 if ($by === null) {
45 return null;
46 }
47 if (!$by->isKeyword('BY')) {
48 return null;
49 }
50 $nameEnd = self::identifierEndIndex($tokens, $index + 2);
51 if ($nameEnd === null) {
52 return null;
53 }
54
55 return [
56 'start' => $keyword->offset,
57 'end' => $tokens[$nameEnd - 1]->endOffset(),
58 ];
59 }
60
61 /**
62 * @param list<SqlToken> $tokens
63 */
64 public static function tokenIndexAtOrAfter(array $tokens, int $offset): int
65 {
66 foreach ($tokens as $index => $token) {
67 if ($token->offset >= $offset) {
68 return $index;
69 }
70 }
71
72 return count($tokens);
73 }
74
75 /**
76 * @param list<SqlToken> $tokens
77 */
78 public static function skipAlias(array $tokens, int $index): int
79 {
80 $candidate = $tokens[$index] ?? null;
81 if ($candidate === null) {
82 return $index;
83 }
84 if ($candidate->isKeyword('AS')) {
85 return self::identifierEndIndex($tokens, $index + 1) ?? $index;
86 }
87
88 if (self::isSourceBoundary($candidate)) {
89 return $index;
90 }
91
92 return self::identifierEndIndex($tokens, $index) ?? $index;
93 }
94
95 /**
96 * Locates index hints following a table reference and optional alias.
97 */
98 public static function isSourceBoundary(SqlToken $token): bool
99 {
100 foreach ([
101 'INDEXED', 'NOT', 'WHERE', 'GROUP', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET',
102 'JOIN', 'LEFT', 'RIGHT', 'FULL', 'INNER', 'CROSS', 'NATURAL', 'ON', 'USING',
103 'UNION', 'INTERSECT', 'EXCEPT', 'RETURNING', 'WINDOW', 'FOR',
104 ] as $keyword) {
105 if ($token->isKeyword($keyword)) {
106 return true;
107 }
108 }
109
110 return false;
111 }
112
113 /**
114 * @param list<SqlToken> $tokens
115 */
116 public static function identifierEndIndex(array $tokens, int $index): ?int
117 {
118 $token = $tokens[$index] ?? null;
119 if ($token === null) {
120 return null;
121 }
122 if ($token->kind === SqlTokenKind::Word || $token->kind === SqlTokenKind::QuotedIdentifier) {
123 return $index + 1;
124 }
125 if ($token->kind !== SqlTokenKind::Symbol) {
126 return null;
127 }
128 if ($token->text !== '[') {
129 return null;
130 }
131
132 $endIndex = $index;
133 while (true) {
134 $endIndex++;
135 $endToken = $tokens[$endIndex] ?? null;
136 if ($endToken === null) {
137 return null;
138 }
139 if ($endToken->kind !== SqlTokenKind::Symbol) {
140 continue;
141 }
142 if ($endToken->text !== ']') {
143 continue;
144 }
145
146 return $endIndex + 1;
147 }
148 }
149}
150