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