packages/ztd-query-mysql/tests/Unit/Rewrite/MySqlQueryGuardTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Rewrite;
6
7use PhpMyAdmin\SqlParser\Parser;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use RuntimeException;
12use ZtdQuery\Platform\MySql\Rewrite\MySqlQueryGuard;
13use ZtdQuery\Platform\MySql\Sql\MySqlParser;
14use ZtdQuery\Rewrite\QueryKind;
15
16#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Diagnostic\DiagnosticKeywords::class)]
17#[UsesClass(\ZtdQuery\Platform\MySql\Sql\OptionalInsertIntoNormalizer::class)]
18#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Classification\CteStatementKind::class)]
19#[CoversClass(MySqlQueryGuard::class)]
20#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Diagnostic\MySqlReadOnlyDiagnosticStatement::class)]
21#[UsesClass(MySqlParser::class)]
22#[UsesClass(\ZtdQuery\Platform\MySql\Sql\MySqlLexerProfile::class)]
23class MySqlQueryGuardTest extends TestCase
24{
25 public function testClassifiesReadStatements(): void
26 {
27 $guard = new MySqlQueryGuard(new MySqlParser());
28 self::assertSame(QueryKind::READ, $guard->classify('SELECT * FROM users'));
29 self::assertSame(QueryKind::READ, $guard->classify('WITH cte AS (SELECT 1) SELECT * FROM users'));
30 }
31
32 public function testClassifiesSingleSelectExpressionsSplitByThirdPartyParser(): void
33 {
34 $guard = new MySqlQueryGuard(new MySqlParser());
35
36 self::assertSame(QueryKind::READ, $guard->classify('SELECT 1 EXCEPT SELECT 2'));
37 self::assertSame(QueryKind::READ, $guard->classify('SELECT 1 INTERSECT SELECT 2'));
38 self::assertSame(QueryKind::READ, $guard->classify('SELECT 1 WHERE CASE WHEN EXISTS(SELECT 1) THEN TRUE ELSE FALSE END'));
39 self::assertNull($guard->classify('SELECT 1; SELECT 2'));
40 }
41
42 public function testClassifiesInsertWithCompoundSelectAsOneWrite(): void
43 {
44 $guard = new MySqlQueryGuard(new MySqlParser());
45
46 self::assertSame(
47 QueryKind::WRITE_SIMULATED,
48 $guard->classify('INSERT INTO combined (id) SELECT id FROM archive UNION ALL SELECT id FROM current'),
49 );
50 }
51
52 public function testClassifiesWriteStatements(): void
53 {
54 $guard = new MySqlQueryGuard(new MySqlParser());
55 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('UPDATE users SET name = "A" WHERE id = 1'));
56 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('DELETE FROM users WHERE id = 1'));
57 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('INSERT INTO users (id, name) VALUES (1, "A")'));
58 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('WITH cte AS (SELECT 1) UPDATE users SET name = "B" WHERE id = 1'));
59 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('WITH `tmp31862` AS( SELECT x862873 ) UPDATE tmp743270 SET tmp526670 = y32462'));
60 }
61
62 public function testReturnsNullForUnsupportedStatements(): void
63 {
64 $guard = new MySqlQueryGuard(new MySqlParser());
65 self::assertNull($guard->classify('DROP DATABASE test'));
66 self::assertNull($guard->classify('CREATE DATABASE test'));
67 self::assertNull($guard->classify('SELECT 1; SELECT 2'));
68 }
69
70 public function testClassifiesReadOnlyDiagnosticsAsRead(): void
71 {
72 $guard = new MySqlQueryGuard(new MySqlParser());
73
74 self::assertSame(QueryKind::READ, $guard->classify('EXPLAIN SELECT * FROM users'));
75 self::assertSame(QueryKind::READ, $guard->classify('EXPLAIN UPDATE users SET active = FALSE'));
76 self::assertSame(QueryKind::READ, $guard->classify('DESCRIBE users'));
77 self::assertSame(QueryKind::READ, $guard->classify('SHOW CREATE TABLE users'));
78 self::assertNull($guard->classify('EXPLAIN ANALYZE UPDATE users SET active = FALSE'));
79 }
80
81 public function testClassifiesDdlStatements(): void
82 {
83 $guard = new MySqlQueryGuard(new MySqlParser());
84 self::assertSame(QueryKind::DDL_SIMULATED, $guard->classify('DROP TABLE users'));
85 self::assertSame(QueryKind::DDL_SIMULATED, $guard->classify('CREATE TABLE demo (id INT)'));
86 self::assertSame(QueryKind::DDL_SIMULATED, $guard->classify('ALTER TABLE users ADD COLUMN email VARCHAR(255)'));
87 self::assertSame(QueryKind::DDL_SIMULATED, $guard->classify('ALTER TABLE users DROP COLUMN email'));
88 self::assertSame(QueryKind::DDL_SIMULATED, $guard->classify('ALTER TABLE users MODIFY COLUMN name VARCHAR(500)'));
89 self::assertSame(QueryKind::DDL_SIMULATED, $guard->classify('ALTER TABLE users CHANGE COLUMN name full_name VARCHAR(255)'));
90 self::assertSame(QueryKind::DDL_SIMULATED, $guard->classify('ALTER TABLE users RENAME TO members'));
91 }
92
93 public function testAssertAllowedThrowsOnForbidden(): void
94 {
95 $guard = new MySqlQueryGuard(new MySqlParser());
96
97 self::expectException(RuntimeException::class);
98 self::expectExceptionMessage('ZTD Write Protection');
99
100 $guard->assertAllowed('DROP DATABASE test');
101 }
102
103 public function testAssertAllowedAcceptsParsedStatement(): void
104 {
105 $guard = new MySqlQueryGuard(new MySqlParser());
106 $parser = new Parser('SELECT 1');
107 $statement = $parser->statements[0];
108
109 $guard->assertAllowed($statement);
110
111 self::assertSame(QueryKind::READ, $guard->classify('SELECT 1'));
112 }
113
114 public function testClassifiesSelectIntoAsNull(): void
115 {
116 $guard = new MySqlQueryGuard(new MySqlParser());
117 self::assertNull($guard->classify('SELECT * INTO OUTFILE "/tmp/data" FROM users'));
118 }
119
120 public function testClassifiesTruncateAsWriteSimulated(): void
121 {
122 $guard = new MySqlQueryGuard(new MySqlParser());
123 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('TRUNCATE TABLE users'));
124 }
125
126 public function testClassifiesReplaceAsWriteSimulated(): void
127 {
128 $guard = new MySqlQueryGuard(new MySqlParser());
129 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify("REPLACE INTO users (id, name) VALUES (1, 'Alice')"));
130 }
131
132 public function testClassifiesLoadDataAsWriteSimulated(): void
133 {
134 $guard = new MySqlQueryGuard(new MySqlParser());
135
136 self::assertSame(
137 QueryKind::WRITE_SIMULATED,
138 $guard->classify("LOAD DATA LOCAL INFILE '/tmp/users.tsv' INTO TABLE users"),
139 );
140 }
141
142 public function testClassifiesEmptySqlAsNull(): void
143 {
144 $guard = new MySqlQueryGuard(new MySqlParser());
145 self::assertNull($guard->classify(''));
146 }
147
148 public function testClassifiesCreateIndexAsNull(): void
149 {
150 $guard = new MySqlQueryGuard(new MySqlParser());
151 self::assertNull($guard->classify('CREATE INDEX idx_name ON users (name)'));
152 }
153
154 public function testClassifiesDropIndexAsNull(): void
155 {
156 $guard = new MySqlQueryGuard(new MySqlParser());
157 self::assertNull($guard->classify('DROP INDEX idx_name ON users'));
158 }
159
160 public function testAssertAllowedDoesNotThrowForWrite(): void
161 {
162 $guard = new MySqlQueryGuard(new MySqlParser());
163 $guard->assertAllowed('INSERT INTO users (id) VALUES (1)');
164 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('INSERT INTO users (id) VALUES (1)'));
165 }
166
167 public function testAssertAllowedDoesNotThrowForDdl(): void
168 {
169 $guard = new MySqlQueryGuard(new MySqlParser());
170 $guard->assertAllowed('CREATE TABLE t (id INT)');
171 self::assertSame(QueryKind::DDL_SIMULATED, $guard->classify('CREATE TABLE t (id INT)'));
172 }
173
174 public function testClassifyWithStatementNoCteParser(): void
175 {
176 $guard = new MySqlQueryGuard(new MySqlParser());
177 self::assertSame(QueryKind::READ, $guard->classify('WITH cte AS (SELECT 1) SELECT * FROM cte'));
178 }
179
180 public function testClassifyWithInsertFallback(): void
181 {
182 $guard = new MySqlQueryGuard(new MySqlParser());
183 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('WITH cte AS (SELECT 1) INSERT INTO users SELECT * FROM cte'));
184 }
185
186 public function testClassifyWithDeleteFallback(): void
187 {
188 $guard = new MySqlQueryGuard(new MySqlParser());
189 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('WITH cte AS (SELECT 1) DELETE FROM users WHERE id IN (SELECT * FROM cte)'));
190 }
191
192 public function testClassifyWithSelectFallback(): void
193 {
194 $guard = new MySqlQueryGuard(new MySqlParser());
195 self::assertSame(QueryKind::READ, $guard->classify('WITH cte AS (SELECT 1) SELECT * FROM cte'));
196 }
197
198 public function testClassifyWithQuotedStringsFallback(): void
199 {
200 $guard = new MySqlQueryGuard(new MySqlParser());
201 $result = $guard->classify("WITH cte AS (SELECT 'SELECT') SELECT * FROM cte");
202 self::assertSame(QueryKind::READ, $result);
203 }
204
205 public function testClassifyMultipleStatementsReturnsNull(): void
206 {
207 $guard = new MySqlQueryGuard(new MySqlParser());
208 self::assertNull($guard->classify('SELECT 1; SELECT 2'));
209 }
210
211 public function testClassifyAlterWithoutTableReturnsNull(): void
212 {
213 $guard = new MySqlQueryGuard(new MySqlParser());
214 self::assertNull($guard->classify('ALTER DATABASE test DEFAULT CHARACTER SET utf8'));
215 }
216
217 public function testAssertAllowedThrowsForSelectInto(): void
218 {
219 $guard = new MySqlQueryGuard(new MySqlParser());
220 self::expectException(RuntimeException::class);
221 $guard->assertAllowed('SELECT * INTO OUTFILE "/tmp/data" FROM users');
222 }
223
224 public function testClassifyWithEscapedBackslashInStringFallback(): void
225 {
226 $guard = new MySqlQueryGuard(new MySqlParser());
227 $result = $guard->classify("WITH cte AS (SELECT 'it\\'s') SELECT * FROM cte");
228 self::assertSame(QueryKind::READ, $result);
229 }
230
231 public function testClassifyWithDoubleQuotedStringFallback(): void
232 {
233 $guard = new MySqlQueryGuard(new MySqlParser());
234 $result = $guard->classify('WITH cte AS (SELECT "DELETE") SELECT * FROM cte');
235 self::assertSame(QueryKind::READ, $result);
236 }
237
238 public function testClassifyWithBacktickQuotedIdentifierFallback(): void
239 {
240 $guard = new MySqlQueryGuard(new MySqlParser());
241 $result = $guard->classify('WITH cte AS (SELECT `UPDATE` FROM t) SELECT * FROM cte');
242 self::assertSame(QueryKind::READ, $result);
243 }
244
245 public function testClassifyWithNestedParensFallback(): void
246 {
247 $guard = new MySqlQueryGuard(new MySqlParser());
248 $result = $guard->classify('WITH cte AS (SELECT (1+2)) SELECT * FROM cte');
249 self::assertSame(QueryKind::READ, $result);
250 }
251
252 public function testClassifyWithFallbackNonAlphaAfterKeyword(): void
253 {
254 $guard = new MySqlQueryGuard(new MySqlParser());
255 $result = $guard->classify('WITH cte AS (SELECT 1) DELETE FROM users WHERE id = 1');
256 self::assertSame(QueryKind::WRITE_SIMULATED, $result);
257 }
258
259 public function testClassifyWithNullCteParserReturnsRead(): void
260 {
261 $guard = new MySqlQueryGuard(new MySqlParser());
262 $result = $guard->classify('WITH cte AS (SELECT 1) GRANT ALL ON users TO admin');
263 self::assertSame(QueryKind::READ, $result);
264 }
265
266 public function testClassifyDropViewReturnsNull(): void
267 {
268 $guard = new MySqlQueryGuard(new MySqlParser());
269 self::assertNull($guard->classify('DROP VIEW v'));
270 }
271
272 public function testClassifyCreateViewReturnsNull(): void
273 {
274 $guard = new MySqlQueryGuard(new MySqlParser());
275 self::assertNull($guard->classify('CREATE VIEW v AS SELECT 1'));
276 }
277
278 public function testClassifyStatementWithCreateNonTableReturnsNull(): void
279 {
280 $guard = new MySqlQueryGuard(new MySqlParser());
281 self::assertNull($guard->classify('CREATE INDEX idx ON t(c)'));
282 }
283
284 public function testClassifyStatementWithAlterNonTableReturnsNull(): void
285 {
286 $guard = new MySqlQueryGuard(new MySqlParser());
287 self::assertNull($guard->classify('ALTER DATABASE test CHARACTER SET utf8'));
288 }
289
290 public function testClassifyStatementDropNonTableReturnsNull(): void
291 {
292 $guard = new MySqlQueryGuard(new MySqlParser());
293 self::assertNull($guard->classify('DROP INDEX idx ON t'));
294 }
295
296 public function testClassifyWithFallbackDeleteInWith(): void
297 {
298 $guard = new MySqlQueryGuard(new MySqlParser());
299 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('WITH cte AS (SELECT 1) DELETE FROM users WHERE id = 1'));
300 }
301
302 public function testClassifyWithFallbackInsertInWith(): void
303 {
304 $guard = new MySqlQueryGuard(new MySqlParser());
305 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('WITH cte AS (SELECT 1) INSERT INTO users (id) VALUES (1)'));
306 }
307
308 public function testClassifyWithFallbackReplaceInWithReturnsNull(): void
309 {
310 $guard = new MySqlQueryGuard(new MySqlParser());
311 self::assertNull($guard->classify('WITH cte AS (SELECT 1) REPLACE INTO users (id) VALUES (1)'));
312 }
313
314 public function testClassifyWithFallbackTruncateInWithReturnsNull(): void
315 {
316 $guard = new MySqlQueryGuard(new MySqlParser());
317 self::assertNull($guard->classify('WITH cte AS (SELECT 1) TRUNCATE TABLE users'));
318 }
319
320 public function testClassifyWithFallbackCreateInWithReturnsNull(): void
321 {
322 $guard = new MySqlQueryGuard(new MySqlParser());
323 self::assertNull($guard->classify('WITH cte AS (SELECT 1) CREATE TABLE t (id INT)'));
324 }
325
326 public function testClassifyWithFallbackDropInWithReturnsNull(): void
327 {
328 $guard = new MySqlQueryGuard(new MySqlParser());
329 self::assertNull($guard->classify('WITH cte AS (SELECT 1) DROP TABLE users'));
330 }
331
332 public function testClassifyWithFallbackAlterInWithReturnsNull(): void
333 {
334 $guard = new MySqlQueryGuard(new MySqlParser());
335 self::assertNull($guard->classify('WITH cte AS (SELECT 1) ALTER TABLE users ADD COLUMN x INT'));
336 }
337
338 public function testClassifyWithFallbackQuotedStringContainingKeyword(): void
339 {
340 $guard = new MySqlQueryGuard(new MySqlParser());
341 self::assertSame(QueryKind::READ, $guard->classify("WITH cte AS (SELECT 'DELETE FROM x') SELECT * FROM users"));
342 }
343
344 public function testClassifyWithFallbackBacktickQuotedIdentifier(): void
345 {
346 $guard = new MySqlQueryGuard(new MySqlParser());
347 self::assertSame(QueryKind::READ, $guard->classify('WITH `DELETE` AS (SELECT 1) SELECT * FROM `DELETE`'));
348 }
349
350 public function testClassifyWithFallbackDoubleQuotedString(): void
351 {
352 $guard = new MySqlQueryGuard(new MySqlParser());
353 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('WITH cte AS (SELECT "test") UPDATE t SET a = 1'));
354 }
355
356 public function testClassifyWithFallbackEscapedQuoteInString(): void
357 {
358 $guard = new MySqlQueryGuard(new MySqlParser());
359 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify("WITH cte AS (SELECT 'it\\'s') UPDATE t SET a = 1"));
360 }
361
362 public function testClassifyWithFallbackLowercaseKeywords(): void
363 {
364 $guard = new MySqlQueryGuard(new MySqlParser());
365 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('with cte as (select 1) update t set a = 1'));
366 }
367
368 public function testClassifyStatementSelectIntoReturnsNull(): void
369 {
370 $guard = new MySqlQueryGuard(new MySqlParser());
371 self::assertNull($guard->classify('SELECT * INTO OUTFILE "/tmp/data" FROM users'));
372 }
373
374 public function testClassifyCreateNonTableDoesNotFallThroughToAlter(): void
375 {
376 $guard = new MySqlQueryGuard(new MySqlParser());
377 $result = $guard->classify('CREATE INDEX idx ON t(c)');
378 self::assertNull($result);
379 }
380
381 public function testClassifyDropNonTableDoesNotFallThroughToAlter(): void
382 {
383 $guard = new MySqlQueryGuard(new MySqlParser());
384 $result = $guard->classify('DROP INDEX idx ON t');
385 self::assertNull($result);
386 }
387
388 public function testClassifyAlterNonTableDoesNotFallThroughToWith(): void
389 {
390 $guard = new MySqlQueryGuard(new MySqlParser());
391 $result = $guard->classify('ALTER DATABASE test CHARACTER SET utf8');
392 self::assertNull($result);
393 }
394
395 public function testClassifyTruncateIsWriteSimulated(): void
396 {
397 $guard = new MySqlQueryGuard(new MySqlParser());
398 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify('TRUNCATE TABLE users'));
399 }
400
401 public function testClassifyReplaceIsWriteSimulated(): void
402 {
403 $guard = new MySqlQueryGuard(new MySqlParser());
404 self::assertSame(QueryKind::WRITE_SIMULATED, $guard->classify("REPLACE INTO users (id, name) VALUES (1, 'Alice')"));
405 }
406
407 public function testAssertAllowedThrowsForUnsupportedSql(): void
408 {
409 $guard = new MySqlQueryGuard(new MySqlParser());
410 self::expectException(RuntimeException::class);
411 $guard->assertAllowed('DROP DATABASE test');
412 }
413
414 public function testAssertAllowedDoesNotThrowForSelect(): void
415 {
416 $guard = new MySqlQueryGuard(new MySqlParser());
417 $guard->assertAllowed('SELECT 1');
418 self::assertSame(QueryKind::READ, $guard->classify('SELECT 1'));
419 }
420
421 public function testAssertAllowedWithParsedStatement(): void
422 {
423 $guard = new MySqlQueryGuard(new MySqlParser());
424 $parser = new Parser('SELECT * FROM users');
425 $guard->assertAllowed($parser->statements[0]);
426 self::assertSame(QueryKind::READ, $guard->classify('SELECT * FROM users'));
427 }
428
429 public function testClassifyStatementReturnsReadForParsedSelectDirectly(): void
430 {
431 $guard = new MySqlQueryGuard(new MySqlParser());
432 $mySqlParser = new MySqlParser();
433 $stmts = $mySqlParser->parse('SELECT 1');
434 self::assertSame(QueryKind::READ, $guard->classifyStatement($stmts[0]));
435 }
436
437 public function testClassifyStatementReturnsNullForCreateNonTableDirectly(): void
438 {
439 $guard = new MySqlQueryGuard(new MySqlParser());
440 $parser = new Parser('CREATE INDEX idx ON t(c)');
441 $result = $guard->classifyStatement($parser->statements[0]);
442 self::assertNull($result);
443 }
444
445 public function testClassifyStatementReturnsNullForDropNonTableDirectly(): void
446 {
447 $guard = new MySqlQueryGuard(new MySqlParser());
448 $parser = new Parser('DROP INDEX idx ON t');
449 $result = $guard->classifyStatement($parser->statements[0]);
450 self::assertNull($result);
451 }
452
453 public function testClassifyStatementReturnsNullForAlterNonTableDirectly(): void
454 {
455 $guard = new MySqlQueryGuard(new MySqlParser());
456 $parser = new Parser('ALTER DATABASE test CHARACTER SET utf8');
457 $result = $guard->classifyStatement($parser->statements[0]);
458 self::assertNull($result);
459 }
460
461 public function testClassifyWithFallbackNestedSubquerySkipsKeywordsInParens(): void
462 {
463 $guard = new MySqlQueryGuard(new MySqlParser());
464 self::assertSame(
465 QueryKind::READ,
466 $guard->classify('WITH cte AS (SELECT 1, (SELECT 2)) SELECT * FROM cte')
467 );
468 }
469
470 public function testClassifyWithFallbackCaseSensitiveKeywordHandling(): void
471 {
472 $guard = new MySqlQueryGuard(new MySqlParser());
473 self::assertSame(
474 QueryKind::WRITE_SIMULATED,
475 $guard->classify('with cte as (select 1) update t set a = 1')
476 );
477 }
478
479 public function testClassifyWithFallbackSingleQuoteWithBackslashEscape(): void
480 {
481 $guard = new MySqlQueryGuard(new MySqlParser());
482 self::assertSame(
483 QueryKind::WRITE_SIMULATED,
484 $guard->classify("WITH cte AS (SELECT 'it\\'s a UPDATE') DELETE FROM t WHERE id = 1")
485 );
486 }
487
488 public function testClassifyWithFallbackBacktickNotEscapedByBackslash(): void
489 {
490 $guard = new MySqlQueryGuard(new MySqlParser());
491 self::assertSame(
492 QueryKind::READ,
493 $guard->classify('WITH `DELETE` AS (SELECT 1) SELECT * FROM `DELETE`')
494 );
495 }
496
497 public function testClassifyWithFallbackReplaceFallsToWriteSimulated(): void
498 {
499 $guard = new MySqlQueryGuard(new MySqlParser());
500 self::assertSame(
501 QueryKind::WRITE_SIMULATED,
502 $guard->classify('WITH `t` AS( SELECT 1 ) REPLACE INTO users (id) VALUES (1)')
503 );
504 }
505
506 public function testClassifyWithFallbackTruncateFallsToWriteSimulated(): void
507 {
508 $guard = new MySqlQueryGuard(new MySqlParser());
509 self::assertSame(
510 QueryKind::WRITE_SIMULATED,
511 $guard->classify('WITH `t` AS( SELECT 1 ) TRUNCATE TABLE users')
512 );
513 }
514
515 public function testClassifyWithFallbackCreateFallsToDdlSimulated(): void
516 {
517 $guard = new MySqlQueryGuard(new MySqlParser());
518 self::assertSame(
519 QueryKind::DDL_SIMULATED,
520 $guard->classify('WITH `t` AS( SELECT 1 ) CREATE TABLE t2 (id INT)')
521 );
522 }
523
524 public function testClassifyWithFallbackDropFallsToDdlSimulated(): void
525 {
526 $guard = new MySqlQueryGuard(new MySqlParser());
527 self::assertSame(
528 QueryKind::DDL_SIMULATED,
529 $guard->classify('WITH `t` AS( SELECT 1 ) DROP TABLE users')
530 );
531 }
532
533 public function testClassifyWithFallbackAlterFallsToDdlSimulated(): void
534 {
535 $guard = new MySqlQueryGuard(new MySqlParser());
536 self::assertSame(
537 QueryKind::DDL_SIMULATED,
538 $guard->classify('WITH `t` AS( SELECT 1 ) ALTER TABLE users ADD COLUMN x INT')
539 );
540 }
541
542 public function testClassifyWithFallbackSelectAfterCteFallsToRead(): void
543 {
544 $guard = new MySqlQueryGuard(new MySqlParser());
545 self::assertSame(
546 QueryKind::READ,
547 $guard->classify('WITH `t` AS( SELECT 1 ) SELECT * FROM users')
548 );
549 }
550
551 public function testClassifyWithFallbackUnrecognizedKeywordFallsThrough(): void
552 {
553 $guard = new MySqlQueryGuard(new MySqlParser());
554 self::assertSame(
555 QueryKind::READ,
556 $guard->classify('WITH `t` AS( SELECT 1 ) GRANT ALL ON t TO admin')
557 );
558 }
559
560 public function testSelectClassifiesAsRead(): void
561 {
562 $kind = (new MySqlQueryGuard(new MySqlParser()))->classify('SELECT id, name FROM users WHERE id = 1');
563 self::assertSame(QueryKind::READ, $kind);
564 }
565
566 public function testInsertClassifiesAsWriteSimulated(): void
567 {
568 $kind = (new MySqlQueryGuard(new MySqlParser()))->classify("INSERT INTO users (id, name) VALUES (1, 'Alice')");
569 self::assertSame(QueryKind::WRITE_SIMULATED, $kind);
570 }
571
572 public function testUpdateClassifiesAsWriteSimulated(): void
573 {
574 $kind = (new MySqlQueryGuard(new MySqlParser()))->classify("UPDATE users SET name = 'Bob' WHERE id = 1");
575 self::assertSame(QueryKind::WRITE_SIMULATED, $kind);
576 }
577
578 public function testDeleteClassifiesAsWriteSimulated(): void
579 {
580 $kind = (new MySqlQueryGuard(new MySqlParser()))->classify('DELETE FROM users WHERE id = 1');
581 self::assertSame(QueryKind::WRITE_SIMULATED, $kind);
582 }
583
584 public function testCreateTableClassifiesAsDdlSimulated(): void
585 {
586 $kind = (new MySqlQueryGuard(new MySqlParser()))->classify('CREATE TABLE orders (id INT PRIMARY KEY, amount DECIMAL(10,2))');
587 self::assertSame(QueryKind::DDL_SIMULATED, $kind);
588 }
589
590 public function testDropTableClassifiesAsDdlSimulated(): void
591 {
592 $kind = (new MySqlQueryGuard(new MySqlParser()))->classify('DROP TABLE orders');
593 self::assertSame(QueryKind::DDL_SIMULATED, $kind);
594 }
595
596 public function testClassifyReturnsNullForGarbageInput(): void
597 {
598 self::assertNull((new MySqlQueryGuard(new MySqlParser()))->classify('NOT VALID SQL %%% @@@'));
599 }
600}
601