packages/sql-semantics-mysql/src/TypeRules.php
1<?php
2
3declare(strict_types=1);
4
5namespace SqlSemantics\Platform\MySql;
6
7use SqlParser\Lexer\Token;
8use SqlParser\Parser\Node;
9use SqlSemantics\Core\Ast\Tree;
10use SqlSemantics\Core\Binding\TypeResolution;
11use SqlSemantics\Core\Dialect;
12use SqlSemantics\Core\Model\Expression;
13use SqlSemantics\Core\Policy\TypeRules as Contract;
14use SqlSemantics\Core\SemanticException;
15use SqlSemantics\Core\Type\TypeDescriptor;
16
17/**
18 * MySql TypeRules implementation.
19 *
20 * @visibility SqlSemantics
21 */
22final class TypeRules implements Contract
23{
24 /**
25 * Retains the language identity used in semantic output.
26 */
27 public function __construct(private readonly Dialect $dialect)
28 {
29 }
30
31 /**
32 * Reads a declared type, including table-dependent storage rules and modifiers.
33 */
34 public function read(Node $node, ?Node $table = null): TypeDescriptor
35 {
36 $tokens = $node->tokens();
37 $words = [];
38 $modifiers = [];
39 $inModifiers = false;
40 foreach ($tokens as $token) {
41 if ($token->text === '(') {
42 $inModifiers = true;
43 } elseif ($token->text === ')') {
44 $inModifiers = false;
45 } elseif ($token->text !== ',') {
46 if ($inModifiers) {
47 $modifiers[] = $token->text;
48 } else {
49 $words[] = $token->text;
50 }
51 }
52 }
53 $name = implode(' ', $words);
54 $canonical = $this->canonical(strtoupper($name)) ?? $name;
55 return new TypeDescriptor($this->dialect, $canonical, $modifiers);
56 }
57
58 /**
59 * Resolves the built-in aliases modeled for this dialect.
60 */
61 public function canonical(string $name): ?string
62 {
63 return match ($name) {
64 'INT', 'INTEGER', 'INT4' => 'integer',
65 'SMALLINT', 'INT2' => 'smallint',
66 'BIGINT', 'INT8' => 'bigint',
67 'DEC', 'DECIMAL', 'NUMERIC' => 'numeric',
68 'REAL' => 'double precision',
69 'FLOAT4' => 'real',
70 'DOUBLE', 'DOUBLE PRECISION', 'FLOAT8' => 'double precision',
71 'BOOL', 'BOOLEAN' => 'tinyint',
72 'VARCHAR', 'CHARACTER VARYING', 'CHAR VARYING' => 'varchar',
73 'CHAR', 'CHARACTER' => 'char',
74 'TEXT', 'DATE', 'TIME', 'TIMESTAMP', 'JSON' => strtolower($name),
75 'TINYINT', 'MEDIUMINT', 'DATETIME', 'BLOB' => strtolower($name),
76 'UUID', 'BYTEA', 'JSONB', 'TIMESTAMPTZ', 'TIMETZ', 'INTERVAL' => null,
77 default => null,
78 };
79 }
80
81 /**
82 * Computes storage affinity from a declaration name.
83 */
84 public function affinity(string $name): string
85 {
86 if (str_contains($name, 'INT')) {
87 return 'integer';
88 }
89 if (str_contains($name, 'CHAR') || str_contains($name, 'CLOB') || str_contains($name, 'TEXT')) {
90 return 'text';
91 }
92 if ($name === '' || str_contains($name, 'BLOB')) {
93 return 'blob';
94 }
95 if (str_contains($name, 'REAL') || str_contains($name, 'FLOA') || str_contains($name, 'DOUB')) {
96 return 'real';
97 }
98 return 'numeric';
99 }
100
101 /**
102 * Classifies a literal's lexical category without converting its contents.
103 */
104 public function typeName(Token $token): ?string
105 {
106 $name = $token->name;
107 $number = str_replace('_', '', $token->text);
108 if (in_array($name, ['ICONST', 'FCONST', 'INTEGER', 'NUM', 'LONG_NUM', 'ULONGLONG_NUM'], true) && preg_match('/^0[xob]/i', $number) === 1) {
109 Tree::unsupported($token, 'non-decimal numeric literal');
110 }
111 $text = strtoupper($token->text);
112 return match (true) {
113 in_array($name, ['ICONST', 'NUM', 'INTEGER'], true) => $this->integer($number),
114 $name === 'LONG_NUM' => 'bigint',
115 $name === 'ULONGLONG_NUM' => 'bigint unsigned',
116 $name === 'FCONST' => ctype_digit($number) ? $this->integer($number) : 'numeric',
117 $name === 'DECIMAL_NUM' => 'numeric',
118 in_array($name, ['FLOAT_NUM', 'FLOAT'], true) => 'double precision',
119 in_array($name, ['SCONST', 'USCONST', 'TEXT_STRING', 'STRING'], true) => 'text',
120 in_array($name, ['NULL_P', 'NULL_SYM', 'NULL'], true) => 'unknown',
121 in_array($text, ['TRUE', 'FALSE'], true) && !in_array($name, ['IDENT', 'IDENT_QUOTED', 'ID'], true) => 'integer',
122 default => null,
123 };
124 }
125
126 /**
127 * Chooses a MySql integer width from its decimal spelling.
128 */
129 public function integer(string $text): string
130 {
131 $digits = ltrim($text, '0');
132 return 'integer';
133 }
134
135 /**
136 * @param list<Expression> $expressions
137 * @throws SemanticException
138 */
139 public function common(array $expressions, Node|Token $source): TypeDescriptor
140 {
141 $types = [];
142 foreach ($expressions as $expression) {
143 if ($expression->type->name !== 'unknown') {
144 $types[] = $expression->type;
145 }
146 }
147 if ($types === []) {
148 return new TypeDescriptor($this->dialect, 'unknown');
149 }
150 $names = array_values(array_unique(array_map(static fn (TypeDescriptor $type): string => $type->name, $types)));
151 if (count($names) === 1) {
152 return new TypeDescriptor($this->dialect, $types[0]->name, affinity: $types[0]->affinity);
153 }
154 $numeric = ['smallint', 'integer', 'bigint', 'numeric', 'real', 'double precision'];
155 if (array_diff($names, $numeric) === []) {
156 $rank = 0;
157 foreach ($numeric as $index => $name) {
158 if (in_array($name, $names, true)) {
159 $rank = $index;
160 }
161 }
162 return new TypeDescriptor($this->dialect, $numeric[$rank]);
163 }
164 if (array_diff($names, ['varchar', 'text', 'char']) === []) {
165 return new TypeDescriptor($this->dialect, 'text');
166 }
167 throw new SemanticException('unsupported-coercion', 'Cannot establish a common type for: ' . implode(', ', $names), $source);
168 }
169
170 /**
171 * Returns the dialect result type of a predicate.
172 */
173 public function boolean(): TypeDescriptor
174 {
175 return new TypeDescriptor($this->dialect, 'integer');
176 }
177
178 /**
179 * Resolves supported numeric operations, including signed literal boundaries.
180 *
181 * @param non-empty-list<Expression> $operands
182 */
183 public function arithmetic(string $operator, array $operands, Node $source): TypeDescriptor
184 {
185 $type = (new TypeResolution($this->dialect))->common($operands, $source);
186 if (!in_array(strtolower($type->name), ['smallint', 'integer', 'bigint'], true)) {
187 Tree::unsupported($source, 'arithmetic type');
188 }
189 $type = new TypeDescriptor($this->dialect, 'bigint');
190 return $type;
191 }
192
193 /**
194 * Accepts scalar truth values.
195 */
196 public function predicate(Expression $expression): void
197 {
198 }
199
200 /**
201 * @param list<Expression> $operands
202 * @return list<Expression>
203 */
204 public function coalesce(array $operands, TypeDescriptor $type): array
205 {
206 return $operands;
207 }
208
209 /**
210 * Resolves the output type of a projected literal.
211 */
212 public function project(Expression $expression): Expression
213 {
214 return $expression;
215 }
216}
217