packages/ztd-query-postgres/tests/Unit/Rewrite/Returning/PgSqlReturningProjectionParserTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Rewrite\Returning;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use ZtdQuery\Exception\UnsupportedSqlException;
12use ZtdQuery\Platform\Postgres\Rewrite\Returning\PgSqlReturningProjectionParser;
13
14#[CoversClass(PgSqlReturningProjectionParser::class)]
15#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::class)]
16#[CoversClass(\ZtdQuery\Platform\Postgres\Sql\Returning\ProjectionItem::class)]
17final class PgSqlReturningProjectionParserTest extends TestCase
18{
19    public function testParsesPostgresQualifiedQuotedAliasesAndWildcard(): void
20    {
21        $projection = (new PgSqlReturningProjectionParser())->parse(
22            'UPDATE users SET name = \'x\' RETURNING users.id AS original_id, *, "name" AS display_name',
23        );
24        self::assertNotNull($projection);
25        self::assertSame([
26            ['source' => 'id', 'output' => 'original_id'],
27            ['source' => null, 'output' => null],
28            ['source' => 'name', 'output' => 'display_name'],
29        ], $projection->items());
30
31        self::assertSame([
32            ['original_id' => 1, 'id' => 1, 'name' => 'Alice', 'display_name' => 'Alice'],
33        ], $projection->project([['id' => 1, 'name' => 'Alice']]));
34    }
35
36    public function testUnquotesEscapedPostgresIdentifiersAndStatementTerminator(): void
37    {
38        $projection = (new PgSqlReturningProjectionParser())->parse(
39            'INSERT INTO users VALUES (1) RETURNING "source""name" AS "output""name";',
40        );
41        self::assertNotNull($projection);
42
43        self::assertSame([['output"name' => 'value']], $projection->project([['source"name' => 'value']]));
44    }
45
46    public function testReturnsNullWithoutPostgresReturningClause(): void
47    {
48        self::assertNull((new PgSqlReturningProjectionParser())->parse('INSERT INTO users VALUES (1)'));
49    }
50
51    #[DataProvider('providerInvalidPostgresReturningProjection')]
52    public function testRejectsMalformedPostgresProjection(string $projection): void
53    {
54        $this->expectException(UnsupportedSqlException::class);
55
56        (new PgSqlReturningProjectionParser())->parse('INSERT INTO users VALUES (1) RETURNING ' . $projection);
57    }
58
59    /**
60     * @return array<string, array{string}>
61     */
62    public static function providerInvalidPostgresReturningProjection(): array
63    {
64        return [
65            'empty' => [''],
66            'expression' => ['id + 1'],
67            'missing alias' => ['id AS'],
68            'invalid alias token' => ['id AS +'],
69            'tokens after alias' => ['id AS alias extra'],
70            'wildcard alias' => ['* AS all_columns'],
71            'leading dot' => ['.id'],
72            'trailing dot' => ['users.'],
73            'double dot' => ['users..id'],
74            'non-dot separator' => ['users + id'],
75            'leading wildcard path' => ['*.id'],
76            'non-trailing wildcard' => ['users.*.id'],
77            'non-wildcard symbol path' => ['users.+'],
78            'empty quoted identifier' => ['""'],
79            'backtick identifier' => ['`id`'],
80            'number token' => ['12'],
81            'string token' => ["'id'"],
82            'unterminated double quote' => ['"id'],
83        ];
84    }
85}
86