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