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