packages/ztd-query-postgres/tests/Unit/Sql/Statement/ConflictClauseTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Sql\Statement;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\TestCase;
9
10#[CoversClass(\ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause::class)]
11#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Platform\Postgres\Sql\Conflict\ColumnSet::class)]
12#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Platform\Postgres\Sql\Lexing\IdentifierDecoder::class)]
13#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Platform\Postgres\Sql\Statement\TargetTableParser::class)]
14#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Platform\Postgres\Sql\Dml\Update\UpdateClauseParser::class)]
15#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Platform\Postgres\Sql\Dml\Delete\DeleteClauseParser::class)]
16#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Platform\Postgres\Sql\Dml\Insert\InsertClauseParser::class)]
17#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Platform\Postgres\Sql\Conflict\PgSqlConflictTarget::class)]
18#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::class)]
19#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Platform\Postgres\Sql\PostgreSqlLexicalMasker::class)]
20#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Platform\Postgres\Sql\Lexing\CommentSpan::class)]
21#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Platform\Postgres\Sql\Lexing\QuotedSpan::class)]
22final class ConflictClauseTest extends TestCase
23{
24    public function testExtractOnConflictTarget(): void
25    {
26        $sql = 'INSERT INTO users VALUES (1) ON CONFLICT (id) DO UPDATE SET name = excluded.name WHERE users.active';
27        $stream = \ZtdQuery\Sql\SqlTokenStream::tokenize($sql, \ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::create());
28        $tokens = $stream->significantTokens();
29
30        self::assertEquals(new \ZtdQuery\Platform\Postgres\Sql\Conflict\PgSqlConflictTarget(specified: true, columns: ['id'], predicate: null, constraint: null), (new \ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause())->extractOnConflictTarget($sql));
31    }
32
33    public function testExtractOnConflictUpdateColumns(): void
34    {
35        $sql = 'INSERT INTO users VALUES (1) ON CONFLICT (id) DO UPDATE SET name = excluded.name WHERE users.active';
36        $stream = \ZtdQuery\Sql\SqlTokenStream::tokenize($sql, \ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::create());
37        $tokens = $stream->significantTokens();
38
39        self::assertSame(['columns' => ['name'], 'values' => ['name' => 'excluded.name']], (new \ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause())->extractOnConflictUpdateColumns($sql));
40    }
41
42    public function testExtractOnConflictUpdateWhere(): void
43    {
44        $sql = 'INSERT INTO users VALUES (1) ON CONFLICT (id) DO UPDATE SET name = excluded.name WHERE users.active';
45        $stream = \ZtdQuery\Sql\SqlTokenStream::tokenize($sql, \ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::create());
46        $tokens = $stream->significantTokens();
47
48        self::assertSame('users.active', (new \ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause())->extractOnConflictUpdateWhere($sql));
49    }
50
51    public function testFindOnConflictTargetStart(): void
52    {
53        $sql = 'INSERT INTO users VALUES (1) ON CONFLICT (id) DO UPDATE SET name = excluded.name WHERE users.active';
54        $stream = \ZtdQuery\Sql\SqlTokenStream::tokenize($sql, \ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::create());
55        $tokens = $stream->significantTokens();
56
57        self::assertSame(9, \ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause::findOnConflictTargetStart($tokens));
58    }
59
60    public function testFindTopLevelSymbol(): void
61    {
62        $sql = 'INSERT INTO users VALUES (1) ON CONFLICT (id) DO UPDATE SET name = excluded.name WHERE users.active';
63        $stream = \ZtdQuery\Sql\SqlTokenStream::tokenize($sql, \ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::create());
64        $tokens = $stream->significantTokens();
65
66        self::assertSame(11, \ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause::findTopLevelSymbol($tokens, 10, ')'));
67    }
68
69    public function testFindTopLevelKeyword(): void
70    {
71        $sql = 'INSERT INTO users VALUES (1) ON CONFLICT (id) DO UPDATE SET name = excluded.name WHERE users.active';
72        $stream = \ZtdQuery\Sql\SqlTokenStream::tokenize($sql, \ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::create());
73        $tokens = $stream->significantTokens();
74
75        self::assertSame(12, \ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause::findTopLevelKeyword($tokens, 0, 'DO'));
76    }
77    public function testNamedConstraintReadsTheArbiterName(): void
78    {
79        $sql = 'ON CONSTRAINT users_pkey DO NOTHING';
80        $tokens = \ZtdQuery\Sql\SqlTokenStream::tokenize($sql, \ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::create())->significantTokens();
81        $target = (new \ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause())->namedConstraint($tokens, 0);
82        self::assertNotNull($target);
83        self::assertSame('users_pkey', $target->constraint);
84        self::assertTrue($target->specified);
85    }
86
87    public function testIndexColumnsRejectsExpressionsAndPreservesNames(): void
88    {
89        $clause = new \ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause();
90        self::assertSame(['id', 'Email'], $clause->indexColumns('id, "Email"'));
91        self::assertNull($clause->indexColumns('lower(email)'));
92    }
93
94    public function testIndexTargetCarriesThePartialPredicate(): void
95    {
96        $sql = '(id) WHERE active DO UPDATE SET id = excluded.id';
97        $tokens = \ZtdQuery\Sql\SqlTokenStream::tokenize($sql, \ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::create())->significantTokens();
98        $target = (new \ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause())->indexTarget($sql, $tokens, 2, ['id']);
99        self::assertNotNull($target);
100        self::assertSame(['id'], $target->columns);
101        self::assertSame('active', $target->predicate);
102    }
103
104    public function testHasOnConflictIgnoresCommentedClauses(): void
105    {
106        $parser = new \ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause();
107        self::assertSame(true, $parser->hasOnConflict('INSERT INTO users VALUES (1) ON CONFLICT DO NOTHING'));
108        self::assertSame(false, $parser->hasOnConflict('INSERT INTO users VALUES (1) /* ON CONFLICT DO NOTHING */'));
109    }
110}
111