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