packages/ztd-query-postgres/tests/Unit/Sql/Value/PgSqlValueRendererTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Sql\Value;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use RuntimeException;
11use Stringable;
12use ZtdQuery\Platform\Postgres\Sql\Value\PgSqlValueRenderer;
13use ZtdQuery\Schema\ColumnDeclaration;
14use ZtdQuery\Schema\ColumnTypeFamily;
15
16#[CoversClass(PgSqlValueRenderer::class)]
17#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\PgSqlCastRenderer::class)]
18#[CoversClass(\ZtdQuery\Platform\Postgres\Sql\Value\BinaryStream::class)]
19#[CoversClass(\ZtdQuery\Platform\Postgres\Sql\Value\LiteralText::class)]
20#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\CastTypeMapper::class)]
21#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\NativeCastTarget::class)]
22final class PgSqlValueRendererTest extends TestCase
23{
24    public function testRenderValueFalseUsesBooleanLiteral(): void
25    {
26        $renderer = new PgSqlValueRenderer();
27
28        self::assertSame(
29            "CAST('0' AS BOOLEAN)",
30            $renderer->renderValue(false, new ColumnDeclaration(ColumnTypeFamily::BOOLEAN, 'BOOLEAN')),
31        );
32    }
33
34    public function testBigIntRetainsNativeWidth(): void
35    {
36        $renderer = new PgSqlValueRenderer();
37
38        self::assertSame(
39            "CAST('9223372036854775807' AS BIGINT)",
40            $renderer->renderValue(9223372036854775807, new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'BIGINT')),
41        );
42    }
43
44    public function testArrayRetainsNativeArrayType(): void
45    {
46        $renderer = new PgSqlValueRenderer();
47
48        self::assertSame(
49            "CAST('{1,2,3}' AS INT4[])",
50            $renderer->renderValue('{1,2,3}', new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INT4[]')),
51        );
52    }
53
54    public function testBinaryUsesDecodeExpression(): void
55    {
56        $renderer = new PgSqlValueRenderer();
57
58        self::assertSame(
59            "CAST(decode('0001ff', 'hex') AS BYTEA)",
60            $renderer->renderValue("\x00\x01\xFF", new ColumnDeclaration(ColumnTypeFamily::BINARY, 'BYTEA')),
61        );
62    }
63
64    public function testStringQuotesApostrophes(): void
65    {
66        $renderer = new PgSqlValueRenderer();
67
68        self::assertSame("CAST('O''Reilly' AS TEXT)", $renderer->renderValue("O'Reilly"));
69    }
70
71    public function testInferredScalarTypesUseNativeLiterals(): void
72    {
73        $renderer = new PgSqlValueRenderer();
74
75        self::assertSame('TRUE', $renderer->renderValue(true));
76        self::assertSame('2.718281828459045', $renderer->renderValue(2.718281828459045));
77        self::assertSame('CAST(42 AS INTEGER)', $renderer->renderValue(42));
78    }
79
80    public function testDeclaredFloatUsesQuotedRoundTripRepresentation(): void
81    {
82        $renderer = new PgSqlValueRenderer();
83
84        self::assertSame(
85            "CAST('2.718281828459045' AS DOUBLE PRECISION)",
86            $renderer->renderValue(2.718281828459045, new ColumnDeclaration(ColumnTypeFamily::DOUBLE, 'DOUBLE PRECISION')),
87        );
88    }
89
90    public function testInferredAndDeclaredStringableRemainDistinct(): void
91    {
92        $value = new class () implements Stringable {
93            public function __toString(): string
94            {
95                return 'CURRENT_TIMESTAMP';
96            }
97        };
98        $renderer = new PgSqlValueRenderer();
99
100        self::assertSame('CURRENT_TIMESTAMP', $renderer->renderValue($value));
101        self::assertSame(
102            "CAST('CURRENT_TIMESTAMP' AS TEXT)",
103            $renderer->renderValue($value, new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')),
104        );
105    }
106
107    public function testUntypedArrayIsRejected(): void
108    {
109        $this->expectException(RuntimeException::class);
110
111        (new PgSqlValueRenderer())->renderValue(['value']);
112    }
113
114    public function testDeclaredNonScalarUsesSerializedRepresentation(): void
115    {
116        self::assertSame(
117            "CAST('a:1:{i:0;s:5:\"value\";}' AS JSONB)",
118            (new PgSqlValueRenderer())->renderValue(['value'], new ColumnDeclaration(ColumnTypeFamily::JSON, 'JSONB')),
119        );
120    }
121
122    public function testBinaryStreamIsReadWithoutChangingItsPosition(): void
123    {
124        $stream = fopen('php://memory', 'r+');
125        self::assertIsResource($stream);
126        fwrite($stream, "\x00\x01\xFF");
127        fseek($stream, 1);
128
129        self::assertSame(
130            "CAST(decode('0001ff', 'hex') AS BYTEA)",
131            (new PgSqlValueRenderer())->renderValue($stream, new ColumnDeclaration(ColumnTypeFamily::BINARY, 'BYTEA')),
132        );
133        self::assertSame(1, ftell($stream));
134        fclose($stream);
135    }
136
137    public function testBinaryRejectsNonStringableNonResource(): void
138    {
139        $this->expectException(RuntimeException::class);
140
141        (new PgSqlValueRenderer())->renderValue(
142            ['value'],
143            new ColumnDeclaration(ColumnTypeFamily::BINARY, 'BYTEA'),
144        );
145    }
146}
147