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