packages/ztd-query-sqlite/tests/Unit/Sql/Value/SqliteValueRendererTest.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\Sqlite\Sql\Value\SqliteValueRenderer;
13use ZtdQuery\Schema\ColumnDeclaration;
14use ZtdQuery\Schema\ColumnTypeFamily;
15
16#[CoversClass(SqliteValueRenderer::class)]
17#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Value\CastTypeMapper::class)]
18#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Value\ValueExpressionRenderer::class)]
19#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Value\ValueLiteralRenderer::class)]
20#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Value\SqliteCastRenderer::class)]
21final class SqliteValueRendererTest extends TestCase
22{
23    public function testRenderValueIntegerStringUsesNumericExpression(): void
24    {
25        $renderer = new SqliteValueRenderer();
26
27        self::assertSame(
28            "CAST('42' AS INTEGER)",
29            $renderer->renderValue('42', new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER')),
30        );
31    }
32
33    public function testFloatUsesRoundTripRepresentation(): void
34    {
35        $renderer = new SqliteValueRenderer();
36
37        self::assertSame(
38            "CAST('2.718281828459045' AS REAL)",
39            $renderer->renderValue(2.718281828459045, new ColumnDeclaration(ColumnTypeFamily::FLOAT, 'REAL')),
40        );
41    }
42
43    public function testBinaryUsesLosslessHexLiteral(): void
44    {
45        $renderer = new SqliteValueRenderer();
46
47        self::assertSame(
48            "CAST(X'0001ff' AS BLOB)",
49            $renderer->renderValue("\x00\x01\xFF", new ColumnDeclaration(ColumnTypeFamily::BINARY, 'BLOB')),
50        );
51    }
52
53    public function testNullRetainsDeclaredType(): void
54    {
55        $renderer = new SqliteValueRenderer();
56
57        self::assertSame('NULL', $renderer->renderValue(null, new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')));
58    }
59
60    public function testTextRemainsText(): void
61    {
62        $renderer = new SqliteValueRenderer();
63
64        self::assertSame("CAST('O''Reilly' AS TEXT)", $renderer->renderValue("O'Reilly"));
65    }
66
67    public function testInferredScalarTypesUseNativeLiterals(): void
68    {
69        $renderer = new SqliteValueRenderer();
70
71        self::assertSame('1', $renderer->renderValue(true));
72        self::assertSame('2.718281828459045', $renderer->renderValue(2.718281828459045));
73        self::assertSame('CAST(42 AS INTEGER)', $renderer->renderValue(42));
74    }
75
76    public function testDeclaredBooleanAndIntegerUseTypedText(): void
77    {
78        $renderer = new SqliteValueRenderer();
79
80        self::assertSame(
81            "CAST('1' AS INTEGER)",
82            $renderer->renderValue(true, new ColumnDeclaration(ColumnTypeFamily::BOOLEAN, 'BOOLEAN')),
83        );
84        self::assertSame(
85            "CAST('0' AS INTEGER)",
86            $renderer->renderValue(false, new ColumnDeclaration(ColumnTypeFamily::BOOLEAN, 'BOOLEAN')),
87        );
88        self::assertSame(
89            "CAST('42' AS INTEGER)",
90            $renderer->renderValue(42, new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER')),
91        );
92    }
93
94    public function testInferredAndDeclaredStringableRemainDistinct(): void
95    {
96        $value = new class () implements Stringable {
97            public function __toString(): string
98            {
99                return 'CURRENT_TIMESTAMP';
100            }
101        };
102        $renderer = new SqliteValueRenderer();
103
104        self::assertSame('CURRENT_TIMESTAMP', $renderer->renderValue($value));
105        self::assertSame(
106            "CAST('CURRENT_TIMESTAMP' AS TEXT)",
107            $renderer->renderValue($value, new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')),
108        );
109    }
110
111    public function testUntypedArrayIsRejected(): void
112    {
113        $this->expectException(RuntimeException::class);
114
115        (new SqliteValueRenderer())->renderValue(['value']);
116    }
117
118    public function testDeclaredNonScalarUsesSerializedRepresentation(): void
119    {
120        self::assertSame(
121            "CAST('a:1:{i:0;s:5:\"value\";}' AS TEXT)",
122            (new SqliteValueRenderer())->renderValue(['value'], new ColumnDeclaration(ColumnTypeFamily::JSON, 'JSON')),
123        );
124    }
125
126    public function testBinaryStreamIsReadWithoutChangingItsPosition(): void
127    {
128        $stream = fopen('php://memory', 'r+');
129        self::assertIsResource($stream);
130        fwrite($stream, "\x00\x01\xFF");
131        fseek($stream, 1);
132
133        self::assertSame(
134            "CAST(X'0001ff' AS BLOB)",
135            (new SqliteValueRenderer())->renderValue($stream, new ColumnDeclaration(ColumnTypeFamily::BINARY, 'BLOB')),
136        );
137        self::assertSame(1, ftell($stream));
138        fclose($stream);
139    }
140
141    public function testBinaryRejectsNonStringableNonResource(): void
142    {
143        $this->expectException(RuntimeException::class);
144
145        (new SqliteValueRenderer())->renderValue(
146            ['value'],
147            new ColumnDeclaration(ColumnTypeFamily::BINARY, 'BLOB'),
148        );
149    }
150}
151