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