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