packages/sql-semantics/tests/Unit/Statement/StatementTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Statement;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Medium;
9use PHPUnit\Framework\Attributes\PreserveGlobalState;
10use PHPUnit\Framework\Attributes\RunInSeparateProcess;
11use PHPUnit\Framework\Attributes\UsesClass;
12use PHPUnit\Framework\TestCase;
13
14#[CoversClass(\SqlSemantics\Statement\Statement::class)]
15#[UsesClass(\SqlSemantics\Core\Analysis\Analyzer::class)]
16#[UsesClass(\SqlSemantics\Core\Analysis\ValueReader::class)]
17#[UsesClass(\SqlSemantics\Core\AnalysisException::class)]
18#[UsesClass(\SqlSemantics\Facade\Semantics::class)]
19#[UsesClass(\SqlSemantics\Statement\Element::class)]
20#[UsesClass(\SqlSemantics\Statement\Writer::class)]
21#[UsesClass(\SqlSemantics\Statement\Assertion::class)]
22#[UsesClass(\SqlSemantics\Statement\ImmutableGraph::class)]
23#[UsesClass(\SqlSemantics\Statement\Comments::class)]
24#[UsesClass(\SqlSemantics\Core\Analysis\TriviaReader::class)]
25#[UsesClass(\SqlSemantics\Core\Analysis\SourceComments::class)]
26#[UsesClass(\SqlSemantics\Core\Ast\DialectParser::class)]
27#[UsesClass(\SqlSemantics\Platform\MySql\Platform::class)]
28#[UsesClass(\SqlSemantics\Platform\PostgreSql\Platform::class)]
29#[UsesClass(\SqlSemantics\Platform\Sqlite\Platform::class)]
30#[Medium]
31final class StatementTest extends TestCase
32{
33    #[RunInSeparateProcess]
34    #[PreserveGlobalState(false)]
35    public function testWithCommandConstructsAndUpdatesWithoutLoadingTheSqlParser(): void
36    {
37        self::assertFalse(class_exists(\SqlParser\Parser\LrParser::class, false));
38        $command = new \SqlSemantics\Statement\Model\Sqlite\Value\CmdWithCommitEndTransOpt_ccca6149(
39            'COMMIT',
40            new \SqlSemantics\Statement\Model\Sqlite\Value\TransOptWith_6ac05548(),
41        );
42        $statement = new \SqlSemantics\Statement\Statement($command);
43        $updated = $statement->withCommand($command->withTransOpt(new \SqlSemantics\Statement\Model\Sqlite\Value\TransOptWithTransaction_ea573324()));
44        self::assertSame('COMMIT TRANSACTION', $updated->toString());
45        self::assertSame('COMMIT', $statement->toString());
46        self::assertFalse(class_exists(\SqlParser\Parser\LrParser::class, false));
47    }
48
49    public function testWithCommandSharesUnchangedValuesAndPreservesTheOriginal(): void
50    {
51        $transaction = new \SqlSemantics\Statement\Model\Sqlite\Value\TransOptWithTransaction_ea573324();
52        $command = new \SqlSemantics\Statement\Model\Sqlite\Value\CmdWithCommitEndTransOpt_ccca6149('COMMIT', $transaction);
53        $original = new \SqlSemantics\Statement\Statement($command);
54        $updated = $original->withCommand($command->withCommitEnd('END'));
55        self::assertSame('COMMIT TRANSACTION', $original->toString());
56        self::assertSame('END TRANSACTION', $updated->toString());
57        self::assertNotSame($original, $updated);
58        self::assertSame($transaction, $command->withCommitEnd('END')->transOpt);
59    }
60
61    public function testWithCommandReplacesWhereUsingStructuredValues(): void
62    {
63        $original = (new \SqlSemantics\Facade\Semantics(\SqlSemantics\Platform\Sqlite\Dialect::Sqlite))->analyze('SELECT foo FROM items');
64        $command = $original->command;
65        self::assertInstanceOf(\SqlSemantics\Statement\Model\Sqlite\Value\EcmdWithCmdxSemi_b7577a8f::class, $command);
66        $select = $command->cmdx;
67        self::assertInstanceOf(\SqlSemantics\Statement\Model\Sqlite\Value\OneselectWithSelectDistinctSelcollistFromWhereOptGroupbyOptHavingOptOrderbyOptLimitOpt_218e0475::class, $select);
68        $where = new \SqlSemantics\Statement\Model\Sqlite\Value\WhereOptWithWhereExpr_93445e09(
69            new \SqlSemantics\Statement\Model\Sqlite\Value\ExprWithExprEqNeExpr_49d16f16(
70                new \SqlSemantics\Statement\Model\Sqlite\Value\ExprWithIdj_e1794d68('foo'),
71                '=',
72                new \SqlSemantics\Statement\Model\Sqlite\Value\TermWithInteger_298801b2('1'),
73            ),
74        );
75        $changedSelect = $select->withWhere($where);
76        $updated = $original->withCommand($command->withCmdx($changedSelect));
77        self::assertSame('SELECT foo FROM items WHERE foo = 1', $updated->toString());
78        self::assertSame('SELECT foo FROM items', $original->toString());
79        self::assertSame($select->from, $changedSelect->from);
80        self::assertSame($select->projections, $changedSelect->projections);
81        self::assertSame('SELECT foo FROM items', $original->withCommand($command->withCmdx($changedSelect->withWhere($select->where)))->toString());
82    }
83
84    public function testToStringUsesIndependentlyConstructedData(): void
85    {
86        $transaction = new \SqlSemantics\Statement\Model\Sqlite\Value\TransOptWithTransaction_ea573324();
87        $commit = new \SqlSemantics\Statement\Statement(new \SqlSemantics\Statement\Model\Sqlite\Value\CmdWithCommitEndTransOpt_ccca6149('COMMIT', $transaction));
88        $end = new \SqlSemantics\Statement\Statement(new \SqlSemantics\Statement\Model\Sqlite\Value\CmdWithCommitEndTransOpt_ccca6149('END', $transaction));
89        self::assertSame('COMMIT TRANSACTION', $commit->toString());
90        self::assertSame('END TRANSACTION', $end->toString());
91        self::assertSame('COMMIT TRANSACTION', $commit->toString());
92    }
93
94    public function testWithCommandKeepsTheCommentsAroundTheCommand(): void
95    {
96        $original = (new \SqlSemantics\Facade\Semantics(\SqlSemantics\Platform\PostgreSql\Dialect::PostgreSql))->analyze("/*+ SeqScan(items) */ SELECT foo FROM items /* traceparent='00-abc' */");
97        $replacement = (new \SqlSemantics\Facade\Semantics(\SqlSemantics\Platform\PostgreSql\Dialect::PostgreSql))->analyze('SELECT bar FROM items')->command;
98        $updated = $original->withCommand($replacement);
99        self::assertSame(['/*+ SeqScan(items) */'], $original->comments->before(\SqlSemantics\Statement\Statement::BEFORE));
100        self::assertSame(["/* traceparent='00-abc' */"], $original->comments->before(\SqlSemantics\Statement\Statement::AFTER));
101        self::assertSame("/*+ SeqScan(items) */ SELECT bar FROM items /* traceparent='00-abc' */", $updated->toString());
102        self::assertSame("/*+ SeqScan(items) */ SELECT foo FROM items /* traceparent='00-abc' */", $original->toString());
103    }
104
105    public function testWithCommentsReplacesTheCommentsAroundTheSameCommand(): void
106    {
107        $original = new \SqlSemantics\Statement\Statement(new \SqlSemantics\Statement\Model\Sqlite\Value\CmdWithCommitEndTransOpt_ccca6149('COMMIT', new \SqlSemantics\Statement\Model\Sqlite\Value\TransOptWith_6ac05548()));
108        $updated = $original->withComments(new \SqlSemantics\Statement\Comments([\SqlSemantics\Statement\Statement::BEFORE => ['-- before'], \SqlSemantics\Statement\Statement::AFTER => ['/* after */']]));
109        self::assertSame("-- before\nCOMMIT /* after */", $updated->toString());
110        self::assertSame('COMMIT', $original->toString());
111        self::assertSame($original->command, $updated->command);
112    }
113
114    public function testToStringKeepsAnOptimizerHintAfterTheStatementKeyword(): void
115    {
116        $statement = (new \SqlSemantics\Facade\Semantics(\SqlSemantics\Platform\MySql\Dialect::MySql))->analyze('SELECT /*+ SET_VAR(sort_buffer_size=16M) */ id FROM users');
117        self::assertSame('SELECT /*+ SET_VAR(sort_buffer_size=16M) */ id FROM users', $statement->toString());
118        self::assertSame([], $statement->comments->positions());
119    }
120
121    public function testToStringKeepsExecutableCommentDelimitersAroundTheSqlTheyEnclose(): void
122    {
123        $statement = (new \SqlSemantics\Facade\Semantics(\SqlSemantics\Platform\MySql\Dialect::MySql, 'mysql-8.4.7'))->analyze('SELECT /*!50700 1, */ 2, /*!99999 3, */ 4 # done');
124        self::assertSame('SELECT /*!50700 1 , */ 2 , /*!99999 3, */ 4 # done', $statement->toString());
125    }
126
127    public function testToStringSeparatesAPrefixOperatorFromANegatedOperand(): void
128    {
129        $statement = (new \SqlSemantics\Facade\Semantics(\SqlSemantics\Platform\PostgreSql\Dialect::PostgreSql))->analyze('SELECT @ -1, @ x');
130        self::assertSame('SELECT @ - 1 , @ x', $statement->toString());
131    }
132
133    public function testToStringAttachesVariableMarkersToTheirNames(): void
134    {
135        $statement = (new \SqlSemantics\Facade\Semantics(\SqlSemantics\Platform\MySql\Dialect::MySql))->analyze("SELECT @a, @@global.max_connections, @`b`, @'c'");
136        self::assertSame("SELECT @a , @@GLOBAL .max_connections , @`b` , @'c'", $statement->toString());
137    }
138}
139