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