packages/ztd-query-postgres/tests/Unit/Sql/Conflict/PgSqlConflictTargetTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Sql\Conflict;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\TestCase;
9use ZtdQuery\Exception\UnsupportedSqlException;
10use ZtdQuery\Platform\Postgres\Sql\Conflict\PgSqlConflictTarget;
11use ZtdQuery\Schema\Key\CandidateKeySet;
12use ZtdQuery\Schema\Key\PartialUniqueIndex;
13
14#[CoversClass(PgSqlConflictTarget::class)]
15#[CoversClass(\ZtdQuery\Platform\Postgres\Sql\Conflict\ColumnSet::class)]
16final class PgSqlConflictTargetTest extends TestCase
17{
18 public function testUnspecifiedTargetKeepsEveryRegularCandidateKey(): void
19 {
20 $keys = CandidateKeySet::fromSchema(['id'], ['users_email' => ['email']]);
21
22 $resolved = (new PgSqlConflictTarget(false))->resolve($keys, [], 'INSERT');
23
24 self::assertSame($keys, $resolved['keys']);
25 self::assertNull($resolved['predicate']);
26 }
27
28 public function testRegularUniqueIndexTakesPrecedenceOverPartialIndexInference(): void
29 {
30 $target = new PgSqlConflictTarget(true, ['email'], "status = 'active'");
31 $keys = CandidateKeySet::fromSchema([], ['users_email' => ['email']]);
32 $partial = new PartialUniqueIndex('users_active_email', ['email'], "status = 'active'");
33
34 $resolved = $target->resolve($keys, [$partial->name => $partial], 'INSERT');
35
36 self::assertSame(['users_email' => ['email']], $resolved['keys']->keys());
37 self::assertNull($resolved['predicate']);
38 }
39
40 public function testResolvesPartialIndexWithOrderIndependentColumns(): void
41 {
42 $target = new PgSqlConflictTarget(true, ['EMAIL', 'TENANT_ID'], "status = 'active'");
43 $partial = new PartialUniqueIndex(
44 'users_active_email',
45 ['tenant_id', 'email'],
46 "status = 'active'::text",
47 );
48
49 $resolved = $target->resolve(new CandidateKeySet([]), [$partial->name => $partial], 'INSERT');
50
51 self::assertSame(['users_active_email' => ['tenant_id', 'email']], $resolved['keys']->keys());
52 self::assertSame("status = 'active'", $resolved['predicate']);
53 }
54
55 public function testRejectsPartialIndexWithDifferentColumnArity(): void
56 {
57 $target = new PgSqlConflictTarget(true, ['email', 'tenant_id'], "status = 'active'");
58 $partial = new PartialUniqueIndex('users_active_email', ['email'], "status = 'active'");
59
60 $this->expectException(UnsupportedSqlException::class);
61
62 $target->resolve(new CandidateKeySet([]), [$partial->name => $partial], 'INSERT');
63 }
64
65 public function testResolvesNamedConstraintCaseInsensitively(): void
66 {
67 $keys = CandidateKeySet::fromSchema([], ['Users_Email' => ['email'], 'users_name' => ['name']]);
68
69 $resolved = (new PgSqlConflictTarget(true, constraint: 'users_email'))->resolve($keys, [], 'INSERT');
70
71 self::assertSame(['Users_Email' => ['email']], $resolved['keys']->keys());
72 }
73
74 public function testRejectsAmbiguousPartialIndexes(): void
75 {
76 $target = new PgSqlConflictTarget(true, ['email'], "status = 'active'");
77 $active = new PartialUniqueIndex('users_active_email', ['email'], "status = 'active'");
78 $pending = new PartialUniqueIndex('users_pending_email', ['email'], "status = 'pending'");
79
80 $this->expectException(UnsupportedSqlException::class);
81
82 $target->resolve(new CandidateKeySet([]), [$active->name => $active, $pending->name => $pending], 'INSERT');
83 }
84}
85