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