packages/ztd-query-sqlite/tests/Unit/Sql/Lexing/LiteralMaskerTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Sql\Lexing;
6
7use Generator;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\DataProvider;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use ZtdQuery\Platform\Sqlite\Sql\Lexing\LiteralMasker;
13
14#[CoversClass(LiteralMasker::class)]
15#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\QuotedSpan::class)]
16#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\SqliteLexicalMasker::class)]
17final class LiteralMaskerTest extends TestCase
18{
19 public function testStripCommentsStripLineComment(): void
20 {
21 $parser = new LiteralMasker();
22 $result = $parser->stripComments("SELECT 1 -- comment\nFROM t");
23 self::assertStringContainsString('SELECT 1', $result);
24 self::assertStringContainsString('FROM t', $result);
25 self::assertStringNotContainsString('comment', $result);
26 }
27
28 public function testStripCommentsStripBlockComment(): void
29 {
30 $parser = new LiteralMasker();
31 self::assertSame('SELECT 1 FROM t', $parser->stripComments('SELECT 1 /* comment */ FROM t'));
32 }
33
34 public function testStripCommentsStripBlockCommentPreservesLexicalBoundaryWithoutSurroundingSpaces(): void
35 {
36 $parser = new LiteralMasker();
37 self::assertSame('SELECT FROM', $parser->stripComments('SELECT/* comment */FROM'));
38 }
39
40 public function testStripCommentsTrimsOuterWhitespace(): void
41 {
42 $parser = new LiteralMasker();
43 self::assertSame('SELECT', $parser->stripComments(" \n/* comment */ SELECT \t"));
44 }
45
46 public function testStripCommentsPreservesQuotedCommentMarkers(): void
47 {
48 $parser = new LiteralMasker();
49 $sql = "SELECT '/* value */', \"-- identifier\", `# identifier`, [/* identifier */] FROM users";
50 self::assertSame($sql, $parser->stripComments($sql));
51 }
52
53 public function testMaskStringLiteralsStringLiteralMaskPreservesOffsetsAndQuotedIdentifiers(): void
54 {
55 $parser = new LiteralMasker();
56 $sql = "SELECT 'FROM hidden', \"quoted\", `backtick`, [bracket] FROM items";
57 $masked = $parser->maskStringLiterals($sql);
58 self::assertSame(strlen($sql), strlen($masked));
59 self::assertSame(strpos($sql, 'FROM items'), strpos($masked, 'FROM items'));
60 self::assertStringContainsString('"quoted"', $masked);
61 self::assertStringContainsString('`backtick`', $masked);
62 self::assertStringContainsString('[bracket]', $masked);
63 }
64
65 #[DataProvider('providerStringLiteralMasks')]
66 public function testMaskStringLiteralsUsesSqliteQuoteBoundaries(string $sql, string $expected): void
67 {
68 $parser = new LiteralMasker();
69 $masked = $parser->maskStringLiterals($sql);
70 self::assertSame($expected, $masked);
71 self::assertSame(strlen($sql), strlen($masked));
72 }
73
74 public function testStripCommentsStripHashComment(): void
75 {
76 $parser = new LiteralMasker();
77 $result = $parser->stripComments("SELECT 1 # comment\nFROM t");
78 self::assertStringNotContainsString('#', $result);
79 self::assertStringContainsString('SELECT 1', $result);
80 }
81
82 public function testStripCommentsPreservesContent(): void
83 {
84 $parser = new LiteralMasker();
85 self::assertSame('SELECT 1', $parser->stripComments('SELECT 1'));
86 }
87
88 public function testStripCommentsStripMultipleComments(): void
89 {
90 $parser = new LiteralMasker();
91 $result = $parser->stripComments("/* c1 */ SELECT /* c2 */ 1 -- c3\n");
92 self::assertStringContainsString('SELECT', $result);
93 self::assertStringContainsString('1', $result);
94 }
95
96 public function testStripCommentsRemovesBlockComment(): void
97 {
98 $parser = new LiteralMasker();
99 $result = $parser->stripComments('SELECT /* comment */ 1');
100 self::assertStringNotContainsString('/*', $result);
101 self::assertStringNotContainsString('*/', $result);
102 }
103
104 public function testStripCommentsRemovesLineComment(): void
105 {
106 $parser = new LiteralMasker();
107 $result = $parser->stripComments("SELECT 1 -- line comment\nFROM t");
108 self::assertStringNotContainsString('--', $result);
109 }
110
111 public function testStripCommentsRemovesHashComment(): void
112 {
113 $parser = new LiteralMasker();
114 $result = $parser->stripComments("SELECT 1 # hash comment\nFROM t");
115 self::assertStringNotContainsString('#', $result);
116 }
117
118 /**
119 * @return Generator<string, array{string, string}>
120 */
121 public static function providerStringLiteralMasks(): Generator
122 {
123 yield 'empty query' => ['', ''];
124 yield 'query without strings' => ['SELECT value FROM items', 'SELECT value FROM items'];
125 yield 'empty string' => ["''", ' '];
126 yield 'single quoted string' => ["SELECT 'FROM hidden' FROM items", 'SELECT ' . str_repeat(' ', 13) . ' FROM items'];
127 yield 'doubled single quote' => ["'value''FROM' FROM items", str_repeat(' ', 13) . ' FROM items'];
128 yield 'doubled quote before closing quote' => ["'a''' FROM items", str_repeat(' ', 5) . ' FROM items'];
129 yield 'unterminated string' => ["'FROM hidden", str_repeat(' ', 12)];
130 yield 'multiple strings' => ["'FROM' || 'JOIN' FROM items", str_repeat(' ', 6) . ' || ' . str_repeat(' ', 6) . ' FROM items'];
131 yield 'newline in string' => ["'FROM\nJOIN' FROM items", str_repeat(' ', 11) . ' FROM items'];
132 yield 'double quoted identifier' => ['"FROM" FROM items', '"FROM" FROM items'];
133 yield 'backtick identifier' => ['`FROM` FROM items', '`FROM` FROM items'];
134 yield 'bracket identifier' => ['[FROM] FROM items', '[FROM] FROM items'];
135 yield 'backslash does not escape quote' => ["'closed \\' FROM items", str_repeat(' ', 10) . ' FROM items'];
136 }
137}
138