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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
//! v7.38.19 — a full sort whose ORDER BY column the projection already
//! carries builds no sort key and reads the projected cell instead.
//!
//! The key it skips is a COPY: on 400,000 rows of 192-character text
//! that was 400,000 allocations, 400,000 frees and 77 MB moved, and the
//! copy exists only because the source row is gone by the time the sort
//! runs. When the column is IN the output, it is not gone.
//!
//! The danger is that the two paths answer differently. A sort KEY
//! encodes what the column means; a VALUE is just what it holds, and for
//! several types those are not the same order:
//!
//! * a user ENUM stores its label as text and orders by DECLARATION
//! position — `('high','mid')` is the declared order and the sorted
//! order, but as text `high < mid`;
//! * an array orders element-wise, not by its rendered form;
//! * a collated column orders by the collator, not by bytes.
//!
//! Each of those was a real failure before `value_order_is_key_order`
//! existed: eleven tests across four files went red on the first draft,
//! and `enum_member_ordering_still_holds_through_a_projection` reported
//! `["high", "mid"]` where the answer is `["mid", "high"]`. The tests
//! below hold that guard from the other side — remove any clause of it
//! and one of them returns a wrongly ordered result, not an error.
use spg_engine::{Engine, QueryResult};
fn col0(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| match &r.values[0] {
spg_storage::Value::Text(t) => t.to_string(),
spg_storage::Value::Null => "<NULL>".into(),
other => format!("{other:?}"),
})
.collect(),
other => panic!("expected rows from {sql}, got {other:?}"),
}
}
/// The shape the change exists for.
#[test]
fn text_sorts_the_same_with_no_key_built() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id int, s text)").unwrap();
for (i, s) in ["pear", "Apple", "fig", "apple", "Pear", "_under"]
.iter()
.enumerate()
{
e.execute(&format!("INSERT INTO t VALUES ({i}, '{s}')"))
.unwrap();
}
// PostgreSQL 18.4, C collation: byte order, so capitals lead.
assert_eq!(
col0(&mut e, "SELECT s FROM t ORDER BY s"),
["Apple", "Pear", "_under", "apple", "fig", "pear"]
);
assert_eq!(
col0(&mut e, "SELECT s FROM t ORDER BY s DESC"),
["pear", "fig", "apple", "_under", "Pear", "Apple"]
);
}
/// NULL placement is the comparator's, not the fast arm's: the inlined
/// pair is two non-NULL strings and nothing else, so a NULL still walks
/// the shared comparator and lands where PG puts it.
#[test]
fn nulls_keep_their_place_without_a_key() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id int, s text)").unwrap();
e.execute("INSERT INTO t VALUES (1, 'b'), (2, NULL), (3, 'a')")
.unwrap();
// PG18.4: ASC puts NULLs last, DESC puts them first.
assert_eq!(
col0(&mut e, "SELECT s FROM t ORDER BY s"),
["a", "b", "<NULL>"]
);
assert_eq!(
col0(&mut e, "SELECT s FROM t ORDER BY s DESC"),
["<NULL>", "b", "a"]
);
assert_eq!(
col0(&mut e, "SELECT s FROM t ORDER BY s NULLS FIRST"),
["<NULL>", "a", "b"]
);
}
/// A user ENUM orders by DECLARATION position. Its label is text, so a
/// value-level comparison would order it alphabetically — the exact
/// disagreement `value_order_is_key_order` exists to refuse.
#[test]
fn an_enum_still_orders_by_declaration_through_a_projection() {
let mut e = Engine::new();
e.execute("CREATE TYPE prio AS ENUM ('urgent', 'mid', 'low')")
.unwrap();
e.execute("CREATE TABLE t (id int, p prio)").unwrap();
e.execute("INSERT INTO t VALUES (1, 'low'), (2, 'urgent'), (3, 'mid')")
.unwrap();
// Declared order, not 'low' < 'mid' < 'urgent'.
assert_eq!(
col0(&mut e, "SELECT p FROM t ORDER BY p"),
["urgent", "mid", "low"]
);
}
/// An array orders element-wise. Rendered, `{2}` would precede `{10}`.
#[test]
fn an_array_still_orders_element_wise_through_a_projection() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id int, a int[])").unwrap();
e.execute("INSERT INTO t VALUES (1, '{10}'), (2, '{2}'), (3, '{1,9}')")
.unwrap();
assert_eq!(
col0(&mut e, "SELECT a FROM t ORDER BY a"),
[
"IntArray([Some(1), Some(9)])",
"IntArray([Some(2)])",
"IntArray([Some(10)])"
]
);
}
/// The projection must BE the column, not merely be named for it. An
/// output item that renames or transforms holds a different value, and
/// sorting by the output would sort by the wrong one.
#[test]
fn an_output_item_that_only_shares_a_name_is_refused() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id int, s text)").unwrap();
e.execute("INSERT INTO t VALUES (1, 'bb'), (2, 'a'), (3, 'ccc')")
.unwrap();
// `s` in the ORDER BY is the OUTPUT item here, which is the length.
assert_eq!(
col0(&mut e, "SELECT length(s) AS s FROM t ORDER BY s"),
["Int(1)", "Int(2)", "Int(3)"]
);
}
/// v7.38.20 — a key that leaves long runs of ties is sorted run by run,
/// and a run that is NOT all-equal still gets sorted.
///
/// The shortcut is that a run whose values are all equal is already in
/// its stable order, which costs n-1 comparisons to prove instead of
/// n log n to re-establish. It is only sound when the run really is
/// uniform, so the interesting case is the one that is not: eight
/// leading bytes shared, and the ninth deciding.
#[test]
fn a_tied_run_that_is_not_uniform_is_still_ordered() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id int, s text)").unwrap();
// Every value shares the first eight bytes, so the prefix key ties
// across all of them and the run covers the whole table.
// Big enough that the sampler runs at all: `key_discriminates`
// answers `true` for anything under eight sampled keys, so a
// four-row fixture never reaches the path this test is for. The
// first draft WAS four rows, and its negative control did not bite.
for i in 0..400i32 {
let tail = ["d", "b", "a", "c"][(i % 4) as usize];
e.execute(&format!("INSERT INTO t VALUES ({i}, 'SHAREDPX{tail}')"))
.unwrap();
}
let got = col0(&mut e, "SELECT s FROM t ORDER BY s");
assert_eq!(got.len(), 400);
assert_eq!(got[0], "SHAREDPXa");
assert_eq!(got[399], "SHAREDPXd");
assert!(got.windows(2).all(|w| w[0] <= w[1]), "not ordered");
let desc = col0(&mut e, "SELECT s FROM t ORDER BY s DESC");
assert_eq!(desc[0], "SHAREDPXd");
assert_eq!(desc[399], "SHAREDPXa");
assert!(desc.windows(2).all(|w| w[0] >= w[1]), "not ordered");
}
/// And a run that IS uniform keeps its input order, which is what
/// stability means. The `id` column rides along to make the order
/// visible.
#[test]
fn a_uniform_run_keeps_its_input_order() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id int, s text)").unwrap();
for i in 0..400i32 {
e.execute(&format!("INSERT INTO t VALUES ({i}, 'SAMEVALUE')"))
.unwrap();
}
// Every row ties; a stable sort must return them as they went in.
match e.execute("SELECT s, id FROM t ORDER BY s").unwrap() {
QueryResult::Rows { rows, .. } => {
let ids: Vec<String> = rows.iter().map(|r| format!("{:?}", r.values[1])).collect();
assert_eq!(
ids,
(0..400).map(|i| format!("Int({i})")).collect::<Vec<_>>()
);
}
other => panic!("expected rows, got {other:?}"),
}
}
/// v7.38.20 — a SECOND sort key no longer sends the whole sort to the
/// general path.
///
/// The inline-integer-key sort used to bail on `k.len() != 1`, saying
/// the tie rate was not knowable. The runs know it: sorting on the
/// first key leaves runs of equal first keys, and only those need the
/// later ones. These fixtures are the two shapes that matters between —
/// a first key that decides everything, and one that decides nothing.
#[test]
fn a_second_key_orders_within_the_first() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (a int, b int, pad text)")
.unwrap();
// `a` repeats in blocks of ten, so every run is ten long and `b`
// decides inside it. `b` descends on input so a working second key
// has to move rows.
for i in 0..400i32 {
e.execute(&format!(
"INSERT INTO t VALUES ({}, {}, 'p{i}')",
i / 10,
400 - i
))
.unwrap();
}
// `pad` is what the projection carries, NOT the sort columns —
// which is what keeps this on the key-based path in `orderby`
// rather than the output-reading one above. A first draft selected
// `a, b` and its negative control did not bite, because it was
// exercising a different sort entirely.
// NEITHER sort column is projected, which is what keeps this on the
// key-based path rather than the output-reading one. `pad` carries
// the input index so the order is still checkable.
let got = col0(&mut e, "SELECT pad FROM t ORDER BY a, b");
assert_eq!(got.len(), 400);
// Row i has a = i/10 and b = 400-i, so inside each block of ten the
// ascending `b` is the DESCENDING input index: the first block must
// come back p9, p8, … p0.
assert_eq!(
&got[..10],
&["p9", "p8", "p7", "p6", "p5", "p4", "p3", "p2", "p1", "p0"]
);
assert_eq!(got[10], "p19");
}
/// And when the first key decides everything — a permutation, every run
/// of length one — the second key must not disturb it.
#[test]
fn a_first_key_that_decides_needs_no_second_pass() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (a int, b int, pad text)")
.unwrap();
for i in 0..400i64 {
let a = (i * 7919) % 400;
e.execute(&format!("INSERT INTO t VALUES ({a}, {}, 'p{i}')", 400 - i))
.unwrap();
}
match e.execute("SELECT a, pad FROM t ORDER BY a, b").unwrap() {
QueryResult::Rows { rows, .. } => {
let got: Vec<i32> = rows
.iter()
.map(|r| match &r.values[0] {
spg_storage::Value::Int(a) => *a,
other => panic!("expected int, got {other:?}"),
})
.collect();
assert_eq!(got, (0..400).collect::<Vec<i32>>());
}
other => panic!("expected rows, got {other:?}"),
}
}
/// v7.38.20 — a top-N row that loses the boundary on its first eight
/// bytes is turned away before a sort key is built for it.
///
/// The danger is a row turned away that should have been kept, and it
/// is silent: the answer is simply missing a row. These fixtures put
/// the winners at the END of the scan, after the boundary has settled
/// on much larger values, so a gate that rejects too eagerly loses
/// them.
#[test]
fn the_top_n_boundary_never_turns_away_a_winner() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id int, s text)").unwrap();
// 2,000 rows of `zz…`, then ten of `aa…` at the very end.
for i in 0..2000i32 {
e.execute(&format!("INSERT INTO t VALUES ({i}, 'zz{i:06}')"))
.unwrap();
}
for i in 0..10i32 {
e.execute(&format!("INSERT INTO t VALUES ({}, 'aa{i:06}')", 9000 + i))
.unwrap();
}
let got = col0(&mut e, "SELECT s FROM t ORDER BY s LIMIT 10");
assert_eq!(
got,
(0..10)
.map(|i| format!("aa{i:06}"))
.collect::<Vec<String>>(),
"the ten smallest arrived last and must still be the answer"
);
}
/// A tie on the first eight bytes decides nothing, so those rows must
/// take the ordinary path rather than being rejected.
#[test]
fn a_tie_on_the_prefix_is_not_a_rejection() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id int, s text)").unwrap();
// Every value shares eight leading bytes; only the tail orders them,
// and the smallest tails arrive last.
for i in 0..2000i32 {
e.execute(&format!(
"INSERT INTO t VALUES ({i}, 'SHAREDPX{:06}')",
9999 - i
))
.unwrap();
}
let got = col0(&mut e, "SELECT s FROM t ORDER BY s LIMIT 3");
assert_eq!(got, ["SHAREDPX008000", "SHAREDPX008001", "SHAREDPX008002"]);
}
/// v7.38.21 — the same gate under a DECLARED collation.
///
/// v7.38.20 turned it off whenever a collation was in play, which was
/// the safe answer and cost the collated leg the entire win: once `C`
/// got faster, the release panel that runs the same binary under a
/// collation against itself under `C` read this shape at 4.16x, and a
/// cost-class difference is what that panel exists to refuse.
///
/// Every value here is `[0-9a-z]`, which is the class `en_US` orders by
/// byte — so the gate may read the bytes, and the answer must be the
/// one the collation gives.
#[test]
fn a_declared_collation_still_gets_the_boundary_gate() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id int, s text COLLATE \"en_US.utf8\")")
.unwrap();
for i in 0..2000i32 {
e.execute(&format!("INSERT INTO t VALUES ({i}, 'zz{i:06}')"))
.unwrap();
}
for i in 0..10i32 {
e.execute(&format!("INSERT INTO t VALUES ({}, 'aa{i:06}')", 9000 + i))
.unwrap();
}
let got = col0(&mut e, "SELECT s FROM t ORDER BY s LIMIT 10");
assert_eq!(
got,
(0..10)
.map(|i| format!("aa{i:06}"))
.collect::<Vec<String>>(),
"the ten smallest arrived last and must still be the answer"
);
}
/// And it is OFF for a row whose own bytes do not answer the collation,
/// even when the boundary's do.
///
/// This is the hazard the row-side check exists for. The boundary
/// settles on `zz…`, which is `[0-9a-z]` and orders by byte; the rows
/// that must win are `ápple…`, which does not. PostgreSQL 18.4 answers
/// `'ápple' < 'zz000000'` true under `en_US.utf8` and false under `C`,
/// so byte order would reject exactly the ten rows that belong in the
/// answer, and the answer would simply be short of them.
///
/// Two thousand rows and `LIMIT 10` because that is the shape this gate
/// is on. Written first at six hundred rows with `LIMIT 1`, it passed
/// with the gate sabotaged two independent ways — which is what a test
/// that never reaches the code looks like.
#[test]
fn the_gate_declines_a_row_whose_bytes_do_not_answer_the_collation() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id int, s text COLLATE \"en_US.utf8\")")
.unwrap();
for i in 0..2000i32 {
e.execute(&format!("INSERT INTO t VALUES ({i}, 'zz{i:06}')"))
.unwrap();
}
for i in 0..10i32 {
e.execute(&format!(
"INSERT INTO t VALUES ({}, 'ápple{i:06}')",
9000 + i
))
.unwrap();
}
assert_eq!(
col0(&mut e, "SELECT s FROM t ORDER BY s LIMIT 10"),
(0..10)
.map(|i| format!("ápple{i:06}"))
.collect::<Vec<String>>(),
"en_US puts á next to a; its bytes put it after z"
);
}