packages/sql-semantics/src/Statement/Statement.php
1<?php
2
3declare(strict_types=1);
4
5namespace SqlSemantics\Statement;
6
7/**
8 * A complete SQL command whose values are independent of parsing.
9 *
10 * Fixed syntax is defined by the concrete value classes. Arguments are named
11 * fields, finite options are enums, and forwarding grammar rules are removed.
12 * No source string, parser node, token, or construction map is retained.
13 * Comments written before the command or after it belong to the statement,
14 * so they survive when the command is replaced.
15 *
16 * @visibility public
17 * @example Reconstructing a statement from its values
18 * $statement = (new \SqlSemantics\Facade\Semantics(\SqlSemantics\Platform\Sqlite\Dialect::Sqlite))->analyze('SELECT 1');
19 * str_contains($statement->toString(), 'SELECT') // => true
20 * @example Keeping a trailing comment while replacing the command
21 * $statement = (new \SqlSemantics\Facade\Semantics(\SqlSemantics\Platform\Sqlite\Dialect::Sqlite))->analyze('SELECT 1 -- audited');
22 * $statement->withCommand($statement->command)->toString() // => "SELECT 1 -- audited"
23 */
24final class Statement
25{
26 use Assertion;
27
28 /**
29 * The comment position before the command.
30 */
31 public const BEFORE = 0;
32
33 /**
34 * The comment position after the command.
35 */
36 public const AFTER = 1;
37
38 /**
39 * Supplies the complete command value and the comments written around it.
40 *
41 * @param Comments $comments Comments at position BEFORE precede the command; those at AFTER follow it
42 */
43 public function __construct(public readonly Command $command, public readonly Comments $comments = new Comments())
44 {
45 $this->assertImmutableValueGraph($command);
46 $this->assert(array_diff($comments->positions(), [self::BEFORE, self::AFTER]) === [], 'Statement comments are written before or after the command.');
47 }
48
49 /**
50 * Returns a statement containing the replacement command, preserving this statement.
51 */
52 public function withCommand(Command $command): self
53 {
54 return new self($command, $this->comments);
55 }
56
57 /**
58 * Returns a statement with other comments around the same command.
59 */
60 public function withComments(Comments $comments): self
61 {
62 return new self($this->command, $comments);
63 }
64
65 /**
66 * Reconstructs SQL solely from the command's fields, options, and comments.
67 */
68 public function toString(): string
69 {
70 $writer = new Writer();
71 $writer->comments($this->comments, self::BEFORE);
72 $this->command->write($writer);
73 $writer->comments($this->comments, self::AFTER);
74
75 return $writer->toString();
76 }
77}
78