packages/ztd-query-core/src/Shadow/Mutation/Upsert/UpsertComparison.php
1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Shadow\Mutation\Upsert;
6
7use ZtdQuery\Exception\UnsupportedSqlException;
8use ZtdQuery\Schema\TableDefinition;
9
10/**
11 * Compares two values the way a database does it.
12 *
13 * Numbers are compared as numbers and text as text; comparing one against the
14 * other is refused rather than guessed at, because which of the two a database
15 * would coerce depends on the column types and the collation, neither of which
16 * is known here. Null is unknown, so every comparison against it is unknown.
17 *
18 * @phpstan-import-type RowValue from TableDefinition
19 */
20final class UpsertComparison
21{
22 /**
23 * @param UpsertNumber $numbers Reads a value as the number it stands for
24 */
25 public function __construct(private readonly UpsertNumber $numbers = new UpsertNumber())
26 {
27 }
28
29 /**
30 * Answers how two values order against each other.
31 *
32 * @param mixed $left Left operand
33 * @param mixed $right Right operand
34 *
35 * @return int|null Negative, zero or positive, or null when either is unknown
36 *
37 * @throws UnsupportedSqlException When one side is a number and the other is not
38 */
39 public function of(mixed $left, mixed $right): ?int
40 {
41 if ($left === null || $right === null) {
42 return null;
43 }
44 if ($this->numbers->isNumeric($left)) {
45 if (!$this->numbers->isNumeric($right)) {
46 throw new UnsupportedSqlException(
47 'incomparable UPSERT operands',
48 'Unsupported UPSERT expression',
49 );
50 }
51
52 return $this->numbers->of($left) <=> $this->numbers->of($right);
53 }
54 if ($this->numbers->isNumeric($right)) {
55 throw new UnsupportedSqlException('incomparable UPSERT operands', 'Unsupported UPSERT expression');
56 }
57
58 return $this->text($left) <=> $this->text($right);
59 }
60
61 /**
62 * Reads a value as the text it stands for.
63 *
64 * A boolean is the one non-text value that still has a text form here,
65 * because that is the form a database compares it in.
66 *
67 * @param mixed $value Value to read
68 *
69 * @return string The text it stands for
70 *
71 * @throws UnsupportedSqlException When the value has no text form
72 */
73 public function text(mixed $value): string
74 {
75 if (is_string($value)) {
76 return $value;
77 }
78 if (is_bool($value)) {
79 return $value ? '1' : '';
80 }
81
82 throw new UnsupportedSqlException('incomparable UPSERT operands', 'Unsupported UPSERT expression');
83 }
84
85 /**
86 * Reports whether two values are the same.
87 *
88 * @param mixed $left Left operand
89 * @param mixed $right Right operand
90 *
91 * @return bool|null Whether they are, or null when either is unknown
92 *
93 * @throws UnsupportedSqlException When one side is a number and the other is not
94 */
95 public function equal(mixed $left, mixed $right): ?bool
96 {
97 $order = $this->of($left, $right);
98
99 return $order === null ? null : $order === 0;
100 }
101
102 /**
103 * Reports whether two values differ.
104 *
105 * @param mixed $left Left operand
106 * @param mixed $right Right operand
107 *
108 * @return bool|null Whether they do, or null when either is unknown
109 *
110 * @throws UnsupportedSqlException When one side is a number and the other is not
111 */
112 public function notEqual(mixed $left, mixed $right): ?bool
113 {
114 $order = $this->of($left, $right);
115
116 return $order === null ? null : $order !== 0;
117 }
118
119 /**
120 * Reports whether the left value orders before the right.
121 *
122 * @param mixed $left Left operand
123 * @param mixed $right Right operand
124 *
125 * @return bool|null Whether it does, or null when either is unknown
126 *
127 * @throws UnsupportedSqlException When one side is a number and the other is not
128 */
129 public function less(mixed $left, mixed $right): ?bool
130 {
131 $order = $this->of($left, $right);
132
133 return $order === null ? null : $order < 0;
134 }
135
136 /**
137 * Reports whether the left value orders before the right, or with it.
138 *
139 * @param mixed $left Left operand
140 * @param mixed $right Right operand
141 *
142 * @return bool|null Whether it does, or null when either is unknown
143 *
144 * @throws UnsupportedSqlException When one side is a number and the other is not
145 */
146 public function lessOrEqual(mixed $left, mixed $right): ?bool
147 {
148 $order = $this->of($left, $right);
149
150 return $order === null ? null : $order <= 0;
151 }
152
153 /**
154 * Reports whether the left value orders after the right.
155 *
156 * @param mixed $left Left operand
157 * @param mixed $right Right operand
158 *
159 * @return bool|null Whether it does, or null when either is unknown
160 *
161 * @throws UnsupportedSqlException When one side is a number and the other is not
162 */
163 public function greater(mixed $left, mixed $right): ?bool
164 {
165 $order = $this->of($left, $right);
166
167 return $order === null ? null : $order > 0;
168 }
169
170 /**
171 * Reports whether the left value orders after the right, or with it.
172 *
173 * @param mixed $left Left operand
174 * @param mixed $right Right operand
175 *
176 * @return bool|null Whether it does, or null when either is unknown
177 *
178 * @throws UnsupportedSqlException When one side is a number and the other is not
179 */
180 public function greaterOrEqual(mixed $left, mixed $right): ?bool
181 {
182 $order = $this->of($left, $right);
183
184 return $order === null ? null : $order >= 0;
185 }
186}
187