packages/sql-catalog/tests/Unit/Core/Sql/PlaceholderScannerTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Core\Sql;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlCatalog\Core\Sql\PlaceholderRef;
11use SqlCatalog\Core\Sql\PlaceholderScanner;
12use SqlCatalog\Core\Sql\SqlLexer;
13use SqlCatalog\Core\Sql\SqlToken;
14use SqlCatalog\Core\Text\LiteralText;
15use SqlCatalog\Core\Text\Origin;
16use SqlCatalog\Core\Text\TextHole;
17use SqlCatalog\Core\Text\TextPattern;
18use SqlCatalog\Core\Type\TypeShape;
19
20#[CoversClass(PlaceholderScanner::class)]
21#[UsesClass(PlaceholderRef::class)]
22#[UsesClass(SqlLexer::class)]
23#[UsesClass(SqlToken::class)]
24#[UsesClass(LiteralText::class)]
25#[UsesClass(TextHole::class)]
26#[UsesClass(TextPattern::class)]
27#[UsesClass(TypeShape::class)]
28final class PlaceholderScannerTest extends TestCase
29{
30 public function testScanFindsPositionalParametersInOrder(): void
31 {
32 $found = (new PlaceholderScanner())->scan(TextPattern::fromText('INSERT INTO t VALUES (?, ?)'));
33 self::assertCount(2, $found);
34 self::assertSame([0, 1], array_map(static fn (PlaceholderRef $ref): int => $ref->position, $found));
35 }
36
37 public function testScanFindsNamedParameters(): void
38 {
39 $found = (new PlaceholderScanner())->scan(TextPattern::fromText('UPDATE t SET a = :a WHERE id = :id'));
40 self::assertSame(['a', 'id'], array_map(static fn (PlaceholderRef $ref): ?string => $ref->name, $found));
41 }
42
43 public function testScanIgnoresQuestionMarksInsideStrings(): void
44 {
45 $found = (new PlaceholderScanner())->scan(TextPattern::fromText("SELECT '?' FROM t WHERE a = ?"));
46 self::assertCount(1, $found);
47 }
48
49 public function testScanIgnoresPostgresCasts(): void
50 {
51 self::assertCount(0, (new PlaceholderScanner())->scan(TextPattern::fromText('SELECT a::text FROM t')));
52 }
53
54 public function testScanReadsThroughAGapWithoutBreakingTheStringAroundIt(): void
55 {
56 $pattern = TextPattern::fromText("SELECT * FROM t WHERE a = '")
57 ->concat(TextPattern::fromHole(new TextHole(Origin::External, TypeShape::unknown())))
58 ->concat(TextPattern::fromText("' AND b = ?"));
59 self::assertCount(1, (new PlaceholderScanner())->scan($pattern));
60 }
61
62 public function testNameOfReadsEveryParameterSyntax(): void
63 {
64 $scanner = new PlaceholderScanner();
65 self::assertNull($scanner->nameOf('?'));
66 self::assertSame('id', $scanner->nameOf(':id'));
67 self::assertSame('1', $scanner->nameOf('$1'));
68 self::assertSame('2', $scanner->nameOf('?2'));
69 }
70}
71