packages/sql-catalog/src/Core/Analysis/QueryRecord.php

1<?php
2
3declare(strict_types=1);
4
5namespace SqlCatalog\Core\Analysis;
6
7use SqlCatalog\Core\Catalog\CallSite;
8use SqlCatalog\Core\Evaluation\Domain;
9use SqlCatalog\Core\Sql\StatementKind;
10use SqlCatalog\Core\Text\TextPattern;
11
12/**
13 * One statement found at a call site, while values may still be bound to it.
14 *
15 * A prepared statement is recorded when it is prepared and completed when it is
16 * executed, so the record stays open between the two.
17 *
18 * @visibility root
19 */
20final class QueryRecord
21{
22    /**
23     * @var array<int, Domain>
24     */
25    private array $positional = [];
26
27    /**
28     * @var array<string, Domain>
29     */
30    private array $named = [];
31
32    private bool $bound = false;
33
34    private bool $truncated;
35
36    /**
37     * @param CallSite $site Where the statement is issued
38     * @param string $siteKey What tells this call apart from every other, including one on the same line
39     * @param TextPattern $pattern The statement text as far as it resolved
40     * @param StatementKind|null $kind The kind the call implies, when it implies one
41     * @param bool $combined Whether the statement came from pairing parts that vary independently
42     * @param list<string> $through The path taken to this reading, from the body the walk started in, outermost first
43     * @param bool $truncated Whether a bound cut the search short of every way the statement can be
44     */
45    public function __construct(
46        public readonly CallSite $site,
47        public readonly string $siteKey,
48        public readonly TextPattern $pattern,
49        public readonly ?StatementKind $kind = null,
50        public bool $combined = false,
51        public readonly array $through = [],
52        bool $truncated = false,
53    ) {
54        $this->truncated = $truncated;
55    }
56
57    /**
58     * Whether a bound cut the search short of every way the statement can be.
59     */
60    public function isTruncated(): bool
61    {
62        return $this->truncated;
63    }
64
65    /**
66     * Binds values given as one array, by position and by name.
67     *
68     * Named keys are written both with and without their leading colon, so the
69     * key is normalized to the name the statement itself uses.
70     *
71     * @param array<int, Domain> $positional
72     * @param array<string, Domain> $named
73     */
74    public function bind(array $positional, array $named): void
75    {
76        $this->positional = $positional;
77        foreach ($named as $key => $value) {
78            $this->named[ltrim($key, ':')] = $value;
79        }
80        $this->bound = true;
81    }
82
83    /**
84     * Binds one value, named or positional depending on how it is keyed.
85     */
86    public function bindOne(string|int|null $key, Domain $value): void
87    {
88        $this->bound = true;
89        if (is_int($key)) {
90            $this->positional[$key - 1] = $value;
91
92            return;
93        }
94        if ($key === null) {
95            $this->positional[] = $value;
96
97            return;
98        }
99        $this->named[ltrim($key, ':')] = $value;
100    }
101
102    /**
103     * Takes on everything another reading of the same statement bound.
104     *
105     * The same call is reached both on its own and through the callers that
106     * lead to it. Each reading is one way the statement can be bound, so the
107     * record keeps the union of them rather than whichever was seen last.
108     */
109    public function absorb(self $other): void
110    {
111        foreach ($other->positional as $position => $value) {
112            $held = $this->positional[$position] ?? null;
113            $this->positional[$position] = $held === null ? $value : $held->union($value);
114        }
115        foreach ($other->named as $name => $value) {
116            $held = $this->named[$name] ?? null;
117            $this->named[$name] = $held === null ? $value : $held->union($value);
118        }
119        $this->bound = $this->bound || $other->bound;
120        $this->truncated = $this->truncated || $other->truncated;
121        $this->combined = $this->combined || $other->combined;
122    }
123
124    /**
125     * The values bound by position, keyed by position counting from zero, in order.
126     *
127     * A position nothing was bound to is absent rather than closed up, so a
128     * value bound to the second placeholder alone stays with the second
129     * placeholder.
130     *
131     * @return array<int, Domain>
132     */
133    public function positional(): array
134    {
135        ksort($this->positional);
136
137        return $this->positional;
138    }
139
140    /**
141     * The values bound by name.
142     *
143     * @return array<string, Domain>
144     */
145    public function named(): array
146    {
147        return $this->named;
148    }
149
150    /**
151     * Whether anything was ever bound to the statement.
152     */
153    public function isBound(): bool
154    {
155        return $this->bound;
156    }
157}
158