packages/ztd-query-pdo-adapter/tests/Integration/PostgreSql/DomainTypeTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Integration\PostgreSql;
6
7use Container\PostgreSql16Container;
8use PDO;
9use PDOException;
10use PDOStatement;
11use PHPUnit\Framework\Attributes\CoversNothing;
12use PHPUnit\Framework\Attributes\Large;
13use PHPUnit\Framework\TestCase;
14use ZtdQuery\Adapter\Pdo\ZtdPdo;
15
16/**
17 * @requires extension pdo_pgsql
18 * @group integration
19 * @group postgres
20 */
21#[CoversNothing]
22#[Large]
23final class DomainTypeTest extends TestCase
24{
25    public function testDomainConstraintsAndDmlRemainNativeInTheShadowStore(): void
26    {
27        $containerInstance = \Testcontainers\Testcontainers::run(PostgreSql16Container::class);
28        /** @var PDO $rawPdo */
29        $rawPdo = new PDO(
30            sprintf('pgsql:host=%s;port=%d;dbname=test', str_replace('localhost', '127.0.0.1', $containerInstance->getHost()), $containerInstance->getMappedPort(5432)),
31            'test',
32            'test',
33            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC],
34        );
35
36        $schemaName = 'ztd_' . bin2hex(random_bytes(8));
37        $rawPdo->exec(sprintf('CREATE SCHEMA "%s"', $schemaName));
38        $rawPdo->exec(sprintf('SET search_path TO "%s"', $schemaName));
39
40        $suffix = bin2hex(random_bytes(8));
41        $positiveDomain = 'positive_int_' . $suffix;
42        $percentageDomain = 'percentage_' . $suffix;
43        $table = 'contacts_' . $suffix;
44
45        try {
46            $rawPdo->exec("CREATE DOMAIN {$positiveDomain} AS INTEGER CHECK (VALUE > 0)");
47            $rawPdo->exec(
48                "CREATE DOMAIN {$percentageDomain} AS NUMERIC(5,2) CHECK (VALUE >= 0 AND VALUE <= 100)",
49            );
50            $rawPdo->exec(
51                "CREATE TABLE {$table} (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, "
52                . "age {$positiveDomain} NOT NULL, satisfaction {$percentageDomain} NOT NULL)",
53            );
54            $ztdPdo = ZtdPdo::fromPdo($rawPdo);
55
56            $insert = $ztdPdo->prepare(
57                "INSERT INTO {$table} (age, satisfaction) VALUES (\$1, \$2) RETURNING id, age, satisfaction",
58            );
59            self::assertInstanceOf(PDOStatement::class, $insert);
60            self::assertTrue($insert->execute([30, '85.50']));
61            self::assertSame(
62                ['id' => 1, 'age' => 30, 'satisfaction' => '85.50'],
63                $insert->fetch(PDO::FETCH_ASSOC),
64            );
65
66            self::assertSame(1, $ztdPdo->exec("UPDATE {$table} SET satisfaction = 99.99 WHERE id = 1"));
67            $updated = $ztdPdo->query("SELECT satisfaction::text FROM {$table} WHERE id = 1");
68            self::assertNotFalse($updated);
69            self::assertSame('99.99', $updated->fetchColumn());
70
71            self::assertSame(1, $ztdPdo->exec("DELETE FROM {$table} WHERE satisfaction > 90"));
72            $remaining = $ztdPdo->query("SELECT COUNT(*) FROM {$table}");
73            self::assertNotFalse($remaining);
74            self::assertSame(0, (int) $remaining->fetchColumn());
75
76            try {
77                $ztdPdo->exec("INSERT INTO {$table} (age, satisfaction) VALUES (-5, 101)");
78                self::fail('The domain CHECK constraint must reject invalid shadow data.');
79            } catch (PDOException $exception) {
80                self::assertStringContainsString('violates check constraint', $exception->getMessage());
81            }
82
83            $physical = $rawPdo->query("SELECT COUNT(*) FROM {$table}");
84            self::assertNotFalse($physical);
85            self::assertSame(0, (int) $physical->fetchColumn());
86        } finally {
87            $rawPdo->exec(sprintf('DROP SCHEMA IF EXISTS "%s" CASCADE', $schemaName));
88        }
89    }
90}
91