packages/ztd-query-core/tests/Contract/IdentifierQuoterContractTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Contract;
6
7use PHPUnit\Framework\TestCase;
8use ZtdQuery\Platform\IdentifierQuoter;
9
10/**
11 * Abstract contract test for IdentifierQuoter implementations.
12 *
13 * Enforces contracts defined in quality-standards.md Section 1.8 and properties P-IQ-1 through P-IQ-5.
14 */
15abstract class IdentifierQuoterContractTest extends TestCase
16{
17    /**
18     * Answers the quoter this dialect names things with.
19     *
20     * @return IdentifierQuoter The quoter under test
21     */
22    abstract protected function createQuoter(): IdentifierQuoter;
23
24    /**
25     * Return the expected quote character for this platform.
26     * MySQL: backtick (`), PostgreSQL/SQLite: double quote (").
27     *
28     * @return non-empty-string
29     */
30    abstract protected function quoteCharacter(): string;
31
32    /**
33     * quote() must return a non-empty string (P-IQ-1).
34     */
35    public function testQuoteReturnsNonEmptyString(): void
36    {
37        $quoter = $this->createQuoter();
38
39        $result = $quoter->quote('users');
40
41        self::assertNotEmpty($result);
42    }
43
44    /**
45     * Output must start and end with the platform's quoting character (P-IQ-2).
46     */
47    public function testQuoteWrapsIdentifier(): void
48    {
49        $quoter = $this->createQuoter();
50        $char = $this->quoteCharacter();
51
52        $result = $quoter->quote('table_name');
53
54        self::assertStringStartsWith($char, $result);
55        self::assertStringEndsWith($char, $result);
56    }
57
58    /**
59     * If the identifier contains the quoting character, the output must still be valid (P-IQ-5).
60     */
61    public function testQuoteEscapesQuoteCharacterInIdentifier(): void
62    {
63        $quoter = $this->createQuoter();
64        $char = $this->quoteCharacter();
65
66        $identifier = 'col' . $char . 'name';
67        $result = $quoter->quote($identifier);
68
69        self::assertNotEmpty($result);
70        self::assertStringStartsWith($char, $result);
71        self::assertStringEndsWith($char, $result);
72
73        $simpleQuoted = $char . $identifier . $char;
74        self::assertGreaterThanOrEqual(
75            strlen($simpleQuoted),
76            strlen($result),
77            'Escaped identifier should be at least as long as non-escaped form'
78        );
79    }
80
81    /**
82     * quote() must produce exact expected output for a simple identifier (P-IQ-2).
83     */
84    public function testQuoteProducesExactResult(): void
85    {
86        $quoter = $this->createQuoter();
87        $char = $this->quoteCharacter();
88
89        $result = $quoter->quote('users');
90
91        self::assertSame($char . 'users' . $char, $result);
92    }
93
94    /**
95     * quote() with embedded quote character must double-escape it.
96     */
97    public function testQuoteEscapesEmbeddedQuoteExactly(): void
98    {
99        $quoter = $this->createQuoter();
100        $char = $this->quoteCharacter();
101
102        $result = $quoter->quote('col' . $char . 'name');
103
104        $expected = $char . 'col' . $char . $char . 'name' . $char;
105        self::assertSame($expected, $result);
106    }
107
108    /**
109     * Same input must produce identical output (P-IQ-4).
110     */
111    public function testQuoteIsDeterministic(): void
112    {
113        $quoter = $this->createQuoter();
114
115        $result1 = $quoter->quote('my_table');
116        $result2 = $quoter->quote('my_table');
117
118        self::assertSame($result1, $result2);
119    }
120
121    /**
122     * The original identifier name must be recoverable from the output (P-IQ-3).
123     */
124    public function testQuotedIdentifierContainsOriginalName(): void
125    {
126        $quoter = $this->createQuoter();
127
128        $identifier = 'column_name';
129        $result = $quoter->quote($identifier);
130
131        self::assertStringContainsString($identifier, $result);
132    }
133}
134