packages/ztd-query-core/src/Sql/Profile/SqlSymbolProfile.php

1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Sql\Profile;
6
7use ZtdQuery\Exception\InvalidDefinitionException;
8use ZtdQuery\Sql\LexicalPattern;
9
10/**
11 * How one dialect spells what is written plainly.
12 *
13 * Which characters may spell an identifier, how far a number runs, what
14 * nests, what brackets, and what ends a statement or separates a list are
15 * all asked here, because they are the shapes a scanner tells apart without
16 * knowing what any of them mean.
17 */
18final class SqlSymbolProfile
19{
20    /** @var array{non-empty-string, non-empty-string}|null */
21    private readonly ?array $bracketPair;
22
23    /** @var array{non-empty-string, non-empty-string} */
24    private readonly array $nestingPair;
25
26    /**
27     * @param string $numericLiteralPattern Pattern a number is written as
28     * @param string $identifierStartPattern Pattern the first character of an identifier matches
29     * @param string $identifierPartPattern Pattern every later character of an identifier matches
30     * @param array{string, string}|null $bracketPair Opening and closing bracket, or null where the dialect has none
31     * @param array{string, string} $nestingPair Opening and closing delimiter that nest
32     * @param string $statementDelimiter Single character that ends a statement
33     * @param string $listDelimiter Single character that separates list items
34     * @param LexicalPattern $patterns Reads a regular expression against a position
35     *
36     * @throws InvalidDefinitionException When a pattern is unreadable, a delimiter is empty, or a single-character delimiter is not one character
37     */
38    public function __construct(
39        private readonly string $numericLiteralPattern,
40        private readonly string $identifierStartPattern,
41        private readonly string $identifierPartPattern,
42        ?array $bracketPair,
43        array $nestingPair,
44        private readonly string $statementDelimiter,
45        private readonly string $listDelimiter,
46        private readonly LexicalPattern $patterns = new LexicalPattern(),
47    ) {
48        $this->patterns->assertValid($this->numericLiteralPattern);
49        $this->patterns->assertValid($this->identifierStartPattern);
50        $this->patterns->assertValid($this->identifierPartPattern);
51        if ($bracketPair !== null && ($bracketPair[0] === '' || $bracketPair[1] === '')) {
52            throw new InvalidDefinitionException('Bracket delimiters must not be empty.');
53        }
54        /** @var array{non-empty-string, non-empty-string}|null $bracketPair */
55        $this->bracketPair = $bracketPair;
56        if ($nestingPair[0] === '' || $nestingPair[1] === '') {
57            throw new InvalidDefinitionException('Nesting delimiters must not be empty.');
58        }
59        /** @var array{non-empty-string, non-empty-string} $nestingPair */
60        $this->nestingPair = $nestingPair;
61        if (strlen($this->statementDelimiter) !== 1 || strlen($this->listDelimiter) !== 1) {
62            throw new InvalidDefinitionException('Statement and list delimiters must be single characters.');
63        }
64    }
65
66    /**
67     * Answers how long the number starting here is.
68     *
69     * @param string $sql Statement being scanned
70     * @param int $offset Position to look at
71     *
72     * @return int Its length, or zero when no number starts there
73     */
74    public function numberLengthAt(string $sql, int $offset): int
75    {
76        $match = $this->patterns->matchAt($this->numericLiteralPattern, $sql, $offset);
77
78        return $match === null ? 0 : strlen($match);
79    }
80
81    /**
82     * Reports whether an identifier may begin with this character.
83     *
84     * @param string $character Character to test
85     *
86     * @return bool True when it may
87     */
88    public function isIdentifierStart(string $character): bool
89    {
90        return $this->patterns->matchesCharacter($this->identifierStartPattern, $character);
91    }
92
93    /**
94     * Reports whether an identifier may continue with this character.
95     *
96     * @param string $character Character to test
97     *
98     * @return bool True when it may
99     */
100    public function isIdentifierPart(string $character): bool
101    {
102        return $this->patterns->matchesCharacter($this->identifierPartPattern, $character);
103    }
104
105    /**
106     * Reports whether this character opens a bracket.
107     *
108     * @param string $character Character to test
109     *
110     * @return bool True when it does, and false where the dialect brackets nothing
111     */
112    public function isBracketOpening(string $character): bool
113    {
114        return $this->bracketPair !== null && $character === $this->bracketPair[0];
115    }
116
117    /**
118     * Reports whether this character closes a bracket.
119     *
120     * @param string $character Character to test
121     *
122     * @return bool True when it does, and false where the dialect brackets nothing
123     */
124    public function isBracketClosing(string $character): bool
125    {
126        return $this->bracketPair !== null && $character === $this->bracketPair[1];
127    }
128
129    /**
130     * Reports whether this character opens a nesting.
131     *
132     * @param string $character Character to test
133     *
134     * @return bool True when it does
135     */
136    public function isNestingOpening(string $character): bool
137    {
138        return $character === $this->nestingPair[0];
139    }
140
141    /**
142     * Reports whether this character closes a nesting.
143     *
144     * @param string $character Character to test
145     *
146     * @return bool True when it does
147     */
148    public function isNestingClosing(string $character): bool
149    {
150        return $character === $this->nestingPair[1];
151    }
152
153    /**
154     * Reports whether this symbol ends a statement.
155     *
156     * @param string $symbol Symbol to test
157     *
158     * @return bool True when it does
159     */
160    public function isStatementDelimiter(string $symbol): bool
161    {
162        return $symbol === $this->statementDelimiter;
163    }
164
165    /**
166     * Answers the character that separates list items.
167     *
168     * @return string The separator
169     */
170    public function listDelimiter(): string
171    {
172        return $this->listDelimiter;
173    }
174}
175