1 | <?php |
||||
2 | namespace Graze\Morphism\Parse; |
||||
3 | |||||
4 | use RuntimeException; |
||||
5 | |||||
6 | /** |
||||
7 | * Represents a set of table options - MIN_ROWS, PACK_KEYS, COMMENT, etc. |
||||
8 | */ |
||||
9 | class TableOptions |
||||
10 | { |
||||
11 | /** @var string|null */ |
||||
12 | public $engine = null; |
||||
13 | |||||
14 | /** @var CollationInfo */ |
||||
15 | public $collation = null; |
||||
16 | |||||
17 | /** |
||||
18 | * @var array |
||||
19 | * maps option names to values (string|int|null) |
||||
20 | */ |
||||
21 | public $options = []; |
||||
22 | |||||
23 | /** @var string */ |
||||
24 | private $defaultEngine = null; |
||||
25 | /** @var CollationInfo|null */ |
||||
26 | private $defaultCollation = null; |
||||
27 | /** @var array */ |
||||
28 | private $defaultOptions = [ |
||||
29 | 'AUTO_INCREMENT' => null, |
||||
30 | 'MIN_ROWS' => 0, |
||||
31 | 'MAX_ROWS' => 0, |
||||
32 | 'AVG_ROW_LENGTH' => 0, |
||||
33 | 'PACK_KEYS' => 'DEFAULT', |
||||
34 | 'CHECKSUM' => '0', |
||||
35 | 'DELAY_KEY_WRITE' => '0', |
||||
36 | 'ROW_FORMAT' => 'DEFAULT', |
||||
37 | 'KEY_BLOCK_SIZE' => 0, |
||||
38 | 'COMMENT' => '', |
||||
39 | 'CONNECTION' => '', |
||||
40 | ]; |
||||
41 | |||||
42 | /** |
||||
43 | * Constructor |
||||
44 | * @param CollationInfo $databaseCollation |
||||
45 | */ |
||||
46 | 204 | public function __construct(CollationInfo $databaseCollation) |
|||
47 | { |
||||
48 | 204 | $this->collation = new CollationInfo; |
|||
49 | 204 | $this->defaultCollation = clone $databaseCollation; |
|||
50 | 204 | } |
|||
51 | |||||
52 | /** |
||||
53 | * Set the default storage engine for the table to use in case the |
||||
54 | * ENGINE option is not supplied. |
||||
55 | * |
||||
56 | * @param string $engine e.g. 'InnoDB" |
||||
57 | */ |
||||
58 | 201 | public function setDefaultEngine($engine) |
|||
59 | { |
||||
60 | 201 | $this->defaultEngine = self::normaliseEngine($engine); |
|||
61 | 201 | } |
|||
62 | |||||
63 | /** |
||||
64 | * Parses table options from $stream. |
||||
65 | * @param TokenStream $stream |
||||
66 | */ |
||||
67 | 184 | public function parse(TokenStream $stream) |
|||
68 | { |
||||
69 | 184 | $this->engine = $this->defaultEngine; |
|||
70 | 184 | $this->options = $this->defaultOptions; |
|||
71 | |||||
72 | 184 | while (true) { |
|||
73 | 184 | $mark = $stream->getMark(); |
|||
74 | 184 | $token = $stream->nextToken(); |
|||
75 | 184 | if ($token->type !== Token::IDENTIFIER) { |
|||
76 | 169 | $stream->rewind($mark); |
|||
77 | 169 | break; |
|||
78 | } |
||||
79 | |||||
80 | 102 | if ($token->eq(Token::IDENTIFIER, 'DEFAULT')) { |
|||
81 | 4 | $token = $stream->nextToken(); |
|||
82 | 4 | if (!($token->type === Token::IDENTIFIER && |
|||
83 | 4 | in_array(strtoupper($token->text), ['CHARSET', 'CHARACTER', 'COLLATE'])) |
|||
84 | ) { |
||||
85 | 1 | throw new RuntimeException("Expected CHARSET, CHARACTER SET or COLLATE"); |
|||
86 | } |
||||
87 | } |
||||
88 | |||||
89 | 101 | $this->parseOption($stream, strtoupper($token->text)); |
|||
90 | } |
||||
91 | |||||
92 | 169 | if (!$this->collation->isSpecified()) { |
|||
93 | 161 | $this->collation = clone $this->defaultCollation; |
|||
94 | } |
||||
95 | 169 | } |
|||
96 | |||||
97 | /** |
||||
98 | * @param TokenStream $stream |
||||
99 | * @param string $option |
||||
100 | */ |
||||
101 | 101 | private function parseOption(TokenStream $stream, $option) |
|||
102 | { |
||||
103 | 101 | switch ($option) { |
|||
104 | 101 | case 'ENGINE': |
|||
105 | 92 | case 'COLLATE': |
|||
106 | 86 | case 'CHARSET': |
|||
107 | 22 | $this->parseIdentifier($stream, $option); |
|||
108 | 20 | break; |
|||
109 | |||||
110 | 79 | case 'CHARACTER': |
|||
111 | 2 | $stream->expect(Token::IDENTIFIER, 'SET'); |
|||
112 | 2 | $this->parseIdentifier($stream, 'CHARSET'); |
|||
113 | 2 | break; |
|||
114 | |||||
115 | 77 | case 'AUTO_INCREMENT': |
|||
116 | 72 | case 'AVG_ROW_LENGTH': |
|||
117 | 67 | case 'KEY_BLOCK_SIZE': |
|||
118 | 62 | case 'MAX_ROWS': |
|||
119 | 57 | case 'MIN_ROWS': |
|||
120 | 25 | $this->parseNumber($stream, $option); |
|||
121 | 25 | break; |
|||
122 | |||||
123 | 52 | case 'CHECKSUM': |
|||
124 | 47 | case 'DELAY_KEY_WRITE': |
|||
125 | 10 | $this->parseEnum($stream, $option, ['0', '1']); |
|||
126 | 10 | break; |
|||
127 | |||||
128 | 42 | case 'PACK_KEYS': |
|||
129 | 6 | $this->parseEnum($stream, $option, ['DEFAULT', '0', '1']); |
|||
130 | 6 | break; |
|||
131 | |||||
132 | 36 | case 'DATA': |
|||
133 | 35 | case 'INDEX': |
|||
134 | 2 | $stream->expect(Token::IDENTIFIER, 'DIRECTORY'); |
|||
135 | // fall through // |
||||
136 | 34 | case 'COMMENT': |
|||
137 | 28 | case 'CONNECTION': |
|||
138 | 22 | case 'PASSWORD': |
|||
139 | 15 | $this->parseString($stream, $option); |
|||
140 | 15 | break; |
|||
141 | |||||
142 | 21 | case 'INSERT_METHOD': |
|||
143 | 4 | $this->parseEnum($stream, $option, ['NO', 'FIRST', 'LAST']); |
|||
144 | 3 | throw new RuntimeException("$option is not currently supported by this tool"); |
|||
145 | |||||
146 | 17 | case 'ROW_FORMAT': |
|||
147 | 10 | $this->parseEnum($stream, $option, ['DEFAULT', 'DYNAMIC', 'FIXED', 'COMPRESSED', 'REDUNDANT', 'COMPACT']); |
|||
148 | 9 | break; |
|||
149 | |||||
150 | 7 | case 'PARTITION': |
|||
151 | 6 | case 'STATS_AUTO_RECALC': |
|||
152 | 5 | case 'STATS_PERSISTENT': |
|||
153 | 4 | case 'STATS_SAMPLE_PAGES': |
|||
154 | 3 | case 'TABLESPACE': |
|||
155 | 2 | case 'UNION': |
|||
156 | 6 | throw new RuntimeException("$option is not currently supported by this tool"); |
|||
157 | |||||
158 | default: |
||||
159 | 1 | throw new RuntimeException("Unknown table option: $option"); |
|||
160 | } |
||||
161 | 87 | } |
|||
162 | |||||
163 | /** |
||||
164 | * @param string $engine |
||||
165 | * @return string |
||||
166 | */ |
||||
167 | 202 | private static function normaliseEngine($engine) |
|||
168 | { |
||||
169 | 202 | $engine = strtoupper($engine); |
|||
170 | 202 | switch ($engine) { |
|||
171 | 202 | case 'INNODB': |
|||
172 | 200 | return 'InnoDB'; |
|||
173 | 9 | case 'MYISAM': |
|||
174 | 7 | return 'MyISAM'; |
|||
175 | default: |
||||
176 | 3 | return $engine; |
|||
177 | } |
||||
178 | } |
||||
179 | |||||
180 | /** |
||||
181 | * @param string $option |
||||
182 | * @param string $value |
||||
183 | */ |
||||
184 | 90 | private function setOption($option, $value) |
|||
185 | { |
||||
186 | 90 | switch ($option) { |
|||
187 | 90 | case 'ENGINE': |
|||
188 | 7 | $this->engine = self::normaliseEngine($value); |
|||
189 | 7 | break; |
|||
190 | |||||
191 | 83 | case 'CHARSET': |
|||
192 | 9 | if (strtoupper($value) === 'DEFAULT') { |
|||
193 | 2 | $this->collation = new CollationInfo(); |
|||
194 | } else { |
||||
195 | 8 | $this->collation->setCharset($value); |
|||
196 | } |
||||
197 | 9 | break; |
|||
198 | |||||
199 | 74 | case 'COLLATE': |
|||
200 | 6 | if (strtoupper($value) === 'DEFAULT') { |
|||
201 | 1 | $this->collation = new CollationInfo(); |
|||
202 | } else { |
||||
203 | 5 | $this->collation->setCollation($value); |
|||
204 | } |
||||
205 | 6 | break; |
|||
206 | |||||
207 | default: |
||||
208 | 68 | $this->options[$option] = $value; |
|||
209 | 68 | break; |
|||
210 | } |
||||
211 | 90 | } |
|||
212 | |||||
213 | /** |
||||
214 | * @param TokenStream $stream |
||||
215 | * @param string $option |
||||
216 | */ |
||||
217 | 24 | private function parseIdentifier(TokenStream $stream, $option) |
|||
218 | { |
||||
219 | 24 | $stream->consume([[Token::SYMBOL, '=']]); |
|||
220 | 24 | $token = $stream->nextToken(); |
|||
221 | 24 | if ($token->isEof()) { |
|||
222 | 1 | throw new RuntimeException("Unexpected end-of-file"); |
|||
223 | } |
||||
224 | 23 | if (!in_array($token->type, [Token::IDENTIFIER, Token::STRING])) { |
|||
225 | 1 | throw new RuntimeException("Bad table option value: '$token->text'"); |
|||
226 | } |
||||
227 | 22 | $this->setOption($option, strtolower($token->text)); |
|||
228 | 22 | } |
|||
229 | |||||
230 | /** |
||||
231 | * @param TokenStream $stream |
||||
232 | * @param string $option |
||||
233 | */ |
||||
234 | 25 | private function parseNumber(TokenStream $stream, $option) |
|||
235 | { |
||||
236 | 25 | $stream->consume([[Token::SYMBOL, '=']]); |
|||
237 | 25 | $this->setOption($option, $stream->expectNumber()); |
|||
238 | 25 | } |
|||
239 | |||||
240 | /** |
||||
241 | * @param TokenStream $stream |
||||
242 | * @param string $option |
||||
243 | * @param array $enums |
||||
244 | */ |
||||
245 | 30 | private function parseEnum(TokenStream $stream, $option, array $enums) |
|||
246 | { |
||||
247 | 30 | $stream->consume([[Token::SYMBOL, '=']]); |
|||
248 | 30 | $token = $stream->nextToken(); |
|||
249 | 30 | if (!in_array($token->type, [Token::IDENTIFIER, Token::NUMBER])) { |
|||
250 | 1 | throw new RuntimeException("Bad table option value"); |
|||
251 | } |
||||
252 | 29 | $value = strtoupper($token->text); |
|||
253 | 29 | if (!in_array($value, $enums)) { |
|||
254 | 1 | throw new RuntimeException("Invalid option value, expected " . implode(' | ', $enums)); |
|||
255 | } |
||||
256 | 28 | $this->setOption($option, $value); |
|||
257 | 28 | } |
|||
258 | |||||
259 | /** |
||||
260 | * @param TokenStream $stream |
||||
261 | * @param string $option |
||||
262 | */ |
||||
263 | 15 | private function parseString(TokenStream $stream, $option) |
|||
264 | { |
||||
265 | 15 | $stream->consume([[Token::SYMBOL, '=']]); |
|||
266 | 15 | $this->setOption($option, $stream->expectString()); |
|||
267 | 15 | } |
|||
268 | |||||
269 | /** |
||||
270 | * Returns an SQL fragment to set the options as part of a CREATE TABLE statement. |
||||
271 | * Note that the AUTO_INCREMENT option is explicitly *not* included in the output. |
||||
272 | */ |
||||
273 | 98 | public function toString() |
|||
274 | { |
||||
275 | 98 | $items = []; |
|||
276 | |||||
277 | 98 | $items[] = "ENGINE=" . $this->engine; |
|||
278 | |||||
279 | // (omit AUTO_INCREMENT) |
||||
280 | |||||
281 | 98 | $collation = $this->collation; |
|||
282 | 98 | if ($collation->isSpecified()) { |
|||
283 | 7 | $items[] = "DEFAULT CHARSET=" . $collation->getCharset(); |
|||
284 | 7 | if (!$collation->isDefaultCollation()) { |
|||
285 | 2 | $items[] = "COLLATE=" . $collation->getCollation(); |
|||
286 | } |
||||
287 | } |
||||
288 | |||||
289 | foreach ([ |
||||
290 | 98 | 'MIN_ROWS', |
|||
291 | 'MAX_ROWS', |
||||
292 | 'AVG_ROW_LENGTH', |
||||
293 | 'PACK_KEYS', |
||||
294 | 'CHECKSUM', |
||||
295 | 'DELAY_KEY_WRITE', |
||||
296 | 'ROW_FORMAT', |
||||
297 | 'KEY_BLOCK_SIZE', |
||||
298 | 'COMMENT', |
||||
299 | 'CONNECTION', |
||||
300 | ] as $option) { |
||||
301 | 98 | if ($this->options[$option] !== $this->defaultOptions[$option]) { |
|||
302 | 17 | $value = $this->options[$option]; |
|||
303 | 17 | if (in_array($option, ['COMMENT', 'CONNECTION'])) { |
|||
304 | 4 | $value = Token::escapeString($value); |
|||
305 | } |
||||
306 | 17 | $items[] = "$option=$value"; |
|||
307 | } |
||||
308 | } |
||||
309 | |||||
310 | 98 | return implode(' ', $items); |
|||
311 | } |
||||
312 | |||||
313 | /** |
||||
314 | * Returns an SQL fragment to transform these table options into those |
||||
315 | * specified by $that as part of an ALTER TABLE statement. |
||||
316 | * |
||||
317 | * The empty string is returned if nothing needs to be done. |
||||
318 | * |
||||
319 | * $flags | |
||||
320 | * :----------------| |
||||
321 | * 'alterEngine' | (bool) include 'ALTER TABLE ... ENGINE=' [default: true] |
||||
322 | * |
||||
323 | * @param TableOptions $that |
||||
324 | * @param array $flags |
||||
325 | * @return string |
||||
326 | */ |
||||
327 | 71 | public function diff(TableOptions $that, array $flags = []) |
|||
328 | { |
||||
329 | $flags += [ |
||||
330 | 71 | 'alterEngine' => true, |
|||
331 | ]; |
||||
332 | |||||
333 | 71 | $alters = []; |
|||
334 | 71 | if ($flags['alterEngine']) { |
|||
335 | 69 | if (strcasecmp($this->engine, $that->engine) !== 0) { |
|||
0 ignored issues
–
show
Bug
introduced
by
![]() It seems like
$that->engine can also be of type null ; however, parameter $string2 of strcasecmp() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
336 | 4 | $alters[] = "ENGINE=" . $that->engine; |
|||
337 | } |
||||
338 | } |
||||
339 | |||||
340 | 71 | $thisCollation = $this->collation->isSpecified() |
|||
341 | 3 | ? $this->collation->getCollation() |
|||
342 | 71 | : null; |
|||
343 | 71 | $thatCollation = $that->collation->isSpecified() |
|||
344 | 4 | ? $that->collation->getCollation() |
|||
345 | 71 | : null; |
|||
346 | 71 | if ($thisCollation !== $thatCollation) { |
|||
347 | // TODO - what if !$that->collation->isSpecified() |
||||
348 | 6 | if (!is_null($thatCollation)) { |
|||
349 | 4 | $alters[] = "DEFAULT CHARSET=" . $that->collation->getCharset(); |
|||
350 | 4 | if (!$that->collation->isDefaultCollation()) { |
|||
351 | 2 | $alters[] = "COLLATE=" . $thatCollation; |
|||
352 | } |
||||
353 | } |
||||
354 | } |
||||
355 | |||||
356 | foreach ([ |
||||
357 | 71 | 'MIN_ROWS', |
|||
358 | 'MAX_ROWS', |
||||
359 | 'AVG_ROW_LENGTH', |
||||
360 | 'PACK_KEYS', |
||||
361 | 'CHECKSUM', |
||||
362 | 'DELAY_KEY_WRITE', |
||||
363 | |||||
364 | // The storage engine may pick a different row format when |
||||
365 | // ROW_FORMAT=DEFAULT (or no ROW_FORMAT)/ is specified, depending |
||||
366 | // on whether any variable length columns are present. Since we |
||||
367 | // don't (currently) explicitly specify ROW_FORMAT in any of our |
||||
368 | // tables, I'm choosing to ignore it for the time being... |
||||
369 | // 'ROW_FORMAT', |
||||
370 | 'KEY_BLOCK_SIZE', |
||||
371 | 'COMMENT', |
||||
372 | 'CONNECTION', |
||||
373 | ] as $option) { |
||||
374 | 71 | $thisValue = $this->options[$option]; |
|||
375 | 71 | $thatValue = $that->options[$option]; |
|||
376 | 71 | if (in_array($option, ['COMMENT', 'CONNECTION'])) { |
|||
377 | 71 | $thisValue = Token::escapeString($thisValue); |
|||
378 | 71 | $thatValue = Token::escapeString($thatValue); |
|||
379 | } |
||||
380 | |||||
381 | 71 | if ($thisValue !== $thatValue) { |
|||
382 | 27 | $alters[] = "$option=$thatValue"; |
|||
383 | } |
||||
384 | } |
||||
385 | |||||
386 | 71 | return implode(' ', $alters); |
|||
387 | } |
||||
388 | } |
||||
389 |