1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
//! v7.39 (round 597) — the right-hand array of an ANY/ALL was rebuilt for
//! every row.
//!
//! The ledger's top entry after round 596 was a CTE self-join at 40.8x, and
//! it was not real: PG answers that query with `column reference "n" is
//! ambiguous`, so the 0.30 ms was an error being raised. SPG raises the same
//! error, just after 12 ms of work — two error paths being compared, not two
//! answers. The ledger now says so. The other three sweep entries were
//! checked the same way and do hold: PG really scans with the filter, really
//! runs the Recursive Union, really runs the ProjectSet.
//!
//! That left `= ANY (ARRAY[…])`, and its cost grew with the array:
//!
//! id = ANY (ARRAY[1]) 56.00 -> 1.84 ms PG 5.64
//! id = ANY (ARRAY[1..10]) 268.09 -> 1.93 PG 8.33
//! id = ANY (ARRAY[1..20]) 493.83 -> 1.97 PG 7.79
//! id <> ALL (ARRAY[1..5]) 149.19 -> 3.31 PG 9.46
//! s = ANY (ARRAY['row1','row2',…]) 147.31 -> 1.95 PG 8.76
//! id = ANY ('{1..10}'::INT[]) 286.60 -> 44.53 PG 7.21
//! id > ANY (ARRAY[1,2,3]) 103.24 -> 17.21 PG 7.60
//!
//! The tell was the equivalent spelling: `id IN (1..10)` was already 2.3 ms
//! and beating PG, because an IN list becomes a membership set at compile
//! time. `ARRAY[…]` was an ordinary expression, evaluated per row — 500k
//! reconstructions of the same ten-element array, then a linear walk.
//!
//! Two steps, and the second is why the numbers stop tracking the array's
//! length. A constant right-hand array is now built once whatever the
//! operator, which is what the last two rows above take. And `x = ANY (…)`
//! is `x IN (…)`, `x <> ALL (…)` is `x NOT IN (…)` — down to the
//! three-valued treatment of a NULL element — so those take the same
//! membership set the IN list builds, and five of the seven shapes now beat
//! PG outright.
//!
//! What the pins are for. The equivalence has to hold where SQL is at its
//! least obvious: a NULL element makes a non-match NULL rather than false, a
//! NULL left-hand side makes everything NULL, and an EMPTY array is decided
//! by emptiness alone — false for ANY, true for ALL — before the left-hand
//! side is even looked at. All 20 shapes here were checked against live PG18
//! and matched.
use spg_engine::{Engine, QueryResult};
fn vals(e: &mut Engine, sql: &str) -> Vec<String> {
match e.execute(sql).unwrap_or_else(|err| panic!("{sql}: {err}")) {
QueryResult::Rows { rows, .. } => rows
.iter()
.map(|r| {
r.values
.iter()
.map(spg_engine::eval::value_to_text)
.collect::<Vec<_>>()
.join("|")
})
.collect(),
other => panic!("{sql}: {other:?}"),
}
}
fn seed() -> Engine {
let mut e = Engine::new();
e.execute("CREATE TABLE an (id INT, b BIGINT, n NUMERIC(10,2), s TEXT)")
.unwrap();
e.execute(
"INSERT INTO an VALUES (1,1,1.00,'a'),(2,2,2.50,'b'),(3,NULL,NULL,NULL),\
(4,4,4.00,'d'),(5,5,5.00,'e')",
)
.unwrap();
e
}
/// The two shapes that become a membership set, and the three-valued cases
/// that decide whether they may.
#[test]
fn round597_any_all_three_valued_logic() {
let mut e = seed();
assert_eq!(
vals(
&mut e,
"SELECT id, id = ANY (ARRAY[1,3,5]) FROM an ORDER BY id"
),
vec!["1|true", "2|false", "3|true", "4|false", "5|true"]
);
assert_eq!(
vals(
&mut e,
"SELECT id, id = ANY (ARRAY[1,NULL,5]) FROM an ORDER BY id"
),
vec!["1|true", "2|NULL", "3|NULL", "4|NULL", "5|true"],
"a NULL element makes a non-match NULL, not false"
);
assert_eq!(
vals(
&mut e,
"SELECT id, id <> ALL (ARRAY[1,3]) FROM an ORDER BY id"
),
vec!["1|false", "2|true", "3|false", "4|true", "5|true"]
);
assert_eq!(
vals(
&mut e,
"SELECT id, id <> ALL (ARRAY[1,NULL]) FROM an ORDER BY id"
),
vec!["1|false", "2|NULL", "3|NULL", "4|NULL", "5|NULL"],
"NOT IN semantics: only a positive match escapes the NULL"
);
assert_eq!(
vals(
&mut e,
"SELECT id, b = ANY (ARRAY[1,2]), b <> ALL (ARRAY[1,2]) FROM an ORDER BY id"
),
vec![
"1|true|false",
"2|true|false",
"3|NULL|NULL",
"4|false|true",
"5|false|true",
],
"a NULL left-hand side"
);
assert_eq!(
vals(
&mut e,
"SELECT id, id = ANY (ARRAY[]::INT[]), id <> ALL (ARRAY[]::INT[]) FROM an ORDER BY id"
),
vec![
"1|false|true",
"2|false|true",
"3|false|true",
"4|false|true",
"5|false|true",
],
"an empty array is decided by emptiness alone"
);
// The equivalence itself, asked of the engine.
assert_eq!(
vals(
&mut e,
"SELECT count(*) FROM an WHERE (id = ANY (ARRAY[1,NULL,5])) \
IS NOT DISTINCT FROM (id IN (1,NULL,5))"
),
vec!["5"],
"every row agrees with the IN spelling, NULLs and all"
);
}
/// Element types that are not the column's own, and the spellings that do
/// not carry their elements in the tree.
#[test]
fn round597_element_types_and_spellings() {
let mut e = seed();
assert_eq!(
vals(
&mut e,
"SELECT id, s = ANY (ARRAY['a','d']) FROM an ORDER BY id"
),
vec!["1|true", "2|false", "3|NULL", "4|true", "5|false"]
);
assert_eq!(
vals(
&mut e,
"SELECT id, id = ANY (ARRAY[1.00, 4.00]), b = ANY (ARRAY[1,4]) FROM an ORDER BY id"
),
vec![
"1|true|true",
"2|false|false",
"3|false|NULL",
"4|true|true",
"5|false|false",
],
"NUMERIC elements against an INT column, and INT against BIGINT"
);
assert_eq!(
vals(
&mut e,
"SELECT id FROM an WHERE id = ANY (ARRAY[2::BIGINT, 4::BIGINT]) ORDER BY id"
),
vec!["2", "4"]
);
assert_eq!(
vals(
&mut e,
"SELECT id, id = ANY ('{1,3}'::INT[]) FROM an ORDER BY id"
),
vec!["1|true", "2|false", "3|true", "4|false", "5|false"],
"the elements live in a string here, so this keeps the folded-array path"
);
assert_eq!(
vals(
&mut e,
"SELECT id, id = ANY (ARRAY[2,2,2]) FROM an ORDER BY id"
),
vec!["1|false", "2|true", "3|false", "4|false", "5|false"],
"duplicates collapse into the set without changing the answer"
);
assert_eq!(
vals(
&mut e,
"SELECT id, id = ANY (ARRAY[3]), id <> ALL (ARRAY[3]) FROM an ORDER BY id"
),
vec![
"1|false|true",
"2|false|true",
"3|true|false",
"4|false|true",
"5|false|true",
]
);
}
/// Operators other than `=`/`<>` keep the comparison but still build the
/// array once.
#[test]
fn round597_other_operators() {
let mut e = seed();
assert_eq!(
vals(
&mut e,
"SELECT id, id > ANY (ARRAY[1,4]), id < ALL (ARRAY[4,5]), id >= ALL (ARRAY[1,2]) \
FROM an ORDER BY id"
),
vec![
"1|false|true|false",
"2|true|true|true",
"3|true|true|true",
"4|true|false|true",
"5|true|false|true",
]
);
}
/// A right-hand side that genuinely varies per row cannot be built once,
/// and must keep answering the same.
#[test]
fn round597_non_constant_arrays_keep_the_interpreter() {
let mut e = seed();
assert_eq!(
vals(
&mut e,
"SELECT id FROM an WHERE id = ANY (ARRAY[id, 1]) ORDER BY id"
),
vec!["1", "2", "3", "4", "5"],
"the array names the row's own column"
);
assert_eq!(
vals(
&mut e,
"SELECT count(*) FROM an WHERE id = ANY (ARRAY[b, b+1])"
),
vec!["4"]
);
}
/// In a filter, negated, and beside other predicates — and at a size where
/// the per-row rebuild was the whole cost.
#[test]
fn round597_filters_and_scale() {
let mut e = seed();
assert_eq!(
vals(
&mut e,
"SELECT id FROM an WHERE id = ANY (ARRAY[2,4]) ORDER BY id"
),
vec!["2", "4"]
);
assert_eq!(
vals(
&mut e,
"SELECT id FROM an WHERE NOT (id = ANY (ARRAY[2,4])) ORDER BY id"
),
vec!["1", "3", "5"]
);
assert_eq!(
vals(
&mut e,
"SELECT id FROM an WHERE id <> ALL (ARRAY[2,4]) ORDER BY id"
),
vec!["1", "3", "5"]
);
assert_eq!(
vals(
&mut e,
"SELECT id FROM an WHERE id = ANY (ARRAY[1,2,3]) AND s IS NOT NULL ORDER BY id"
),
vec!["1", "2"]
);
let mut e2 = Engine::new();
e2.execute("CREATE TABLE big (id INT)").unwrap();
e2.execute("INSERT INTO big SELECT gg FROM generate_series(1, 20000) gg")
.unwrap();
// The set spelling and the IN spelling must agree at scale.
assert_eq!(
vals(
&mut e2,
"SELECT count(*) FROM big WHERE id = ANY (ARRAY[1,2,3,4,5,6,7,8,9,10])"
),
vals(
&mut e2,
"SELECT count(*) FROM big WHERE id IN (1,2,3,4,5,6,7,8,9,10)"
)
);
assert_eq!(
vals(
&mut e2,
"SELECT count(*) FROM big WHERE id <> ALL (ARRAY[1,2,3,4,5])"
),
vec!["19995"]
);
}