packages/ztd-query-sqlite/tests/Unit/Rewrite/Returning/SqliteReturningProjectionParserTest.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\Sqlite\Rewrite\Returning\SqliteReturningProjectionParser;
13
14#[CoversClass(SqliteReturningProjectionParser::class)]
15#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Returning\ReturningItemParser::class)]
16#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\SqliteLexerProfile::class)]
17final class SqliteReturningProjectionParserTest extends TestCase
18{
19    public function testParsesSqliteQualifiedQuotedAliasesAndWildcard(): void
20    {
21        $projection = (new SqliteReturningProjectionParser())->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 testUnquotesEscapedSqliteIdentifiersAndStatementTerminator(): void
37    {
38        $projection = (new SqliteReturningProjectionParser())->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 testReturnsNullWithoutSqliteReturningClause(): void
47    {
48        self::assertNull((new SqliteReturningProjectionParser())->parse('INSERT INTO users VALUES (1)'));
49    }
50
51    #[DataProvider('providerInvalidSqliteReturningProjection')]
52    public function testRejectsMalformedSqliteProjection(string $projection): void
53    {
54        $this->expectException(UnsupportedSqlException::class);
55
56        (new SqliteReturningProjectionParser())->parse('INSERT INTO users VALUES (1) RETURNING ' . $projection);
57    }
58
59    /**
60     * @return array<string, array{string}>
61     */
62    public static function providerInvalidSqliteReturningProjection(): 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            'number token' => ['12'],
80            'string token' => ["'id'"],
81            'unterminated double quote' => ['"id'],
82            'unterminated backtick' => ['`id'],
83        ];
84    }
85}
86