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
//! v7.17.0 Phase 3.P0-48 — set-returning sources (`generate_series`,
//! `unnest`) routing through the aggregate executor.
//!
//! Phase 3.2 (`0ffd766`) wired `FROM generate_series(...)` and
//! `FROM unnest(...)` as scan sources but the executor short-
//! circuited straight to the projection / ORDER BY / LIMIT
//! pipeline, never calling `aggregate::run`. So
//! `SELECT COUNT(*) FROM generate_series(1, 10)` either errored
//! at projection time (COUNT(*) isn't a per-row eval shape) or
//! silently returned the wrong row count — a Tier-A silent
//! divergence from PG. The same gap hit `unnest`.
//!
//! P0-48 adds the standard "aggregate dispatch happens before
//! projection" branch to both set-returning executors so the
//! customer's metrics queries (`COUNT`, `SUM`, `MIN`, `MAX`,
//! `AVG`, `string_agg`, …) all land cleanly.
use spg_engine::{Engine, QueryResult};
use spg_storage::Value;
fn rows(r: QueryResult) -> Vec<Vec<Value<'static>>> {
match r {
QueryResult::Rows { rows, .. } => rows.into_iter().map(|r| r.values).collect(),
_ => panic!("expected rows"),
}
}
#[test]
fn count_star_over_generate_series() {
let mut e = Engine::new();
let r = rows(
e.execute("SELECT COUNT(*) FROM generate_series(1, 100)")
.unwrap(),
);
assert_eq!(r.len(), 1);
assert_eq!(r[0][0], Value::BigInt(100));
}
#[test]
fn sum_over_generate_series_with_column_alias() {
let mut e = Engine::new();
let r = rows(
e.execute("SELECT SUM(g) FROM generate_series(1, 10) AS g")
.unwrap(),
);
assert_eq!(r.len(), 1);
// 1 + 2 + ... + 10 = 55. v7.38 (read01, T-gs) — generate_series(int4) yields
// int4 elements (matching PG), and sum(int4) widens to BIGINT, so PG and SPG
// agree on bigint 55.
assert_eq!(r[0][0], Value::BigInt(55));
}
#[test]
fn min_max_over_generate_series() {
let mut e = Engine::new();
let r = rows(
e.execute("SELECT MIN(g), MAX(g) FROM generate_series(5, 12) AS g")
.unwrap(),
);
assert_eq!(r.len(), 1);
// generate_series(int4) → int4 elements, so MIN/MAX stay int4 (matching PG).
assert_eq!(r[0][0], Value::Int(5));
assert_eq!(r[0][1], Value::Int(12));
}
#[test]
fn count_with_where_filter_over_generate_series() {
let mut e = Engine::new();
let r = rows(
e.execute("SELECT COUNT(*) FROM generate_series(1, 100) AS g WHERE g > 50")
.unwrap(),
);
assert_eq!(r.len(), 1);
assert_eq!(r[0][0], Value::BigInt(50));
}
#[test]
fn count_with_predicate_over_unnest() {
// Phase 5 unnest sources expose a TEXT column. Aggregate
// routing must work through WHERE-filtered subsets of the
// unnest output.
let mut e = Engine::new();
let r = rows(
e.execute("SELECT COUNT(*) FROM unnest(ARRAY['a','b','b','c','c','c']) AS u WHERE u = 'c'")
.unwrap(),
);
assert_eq!(r.len(), 1);
assert_eq!(r[0][0], Value::BigInt(3));
}
#[test]
fn count_star_over_unnest_keeps_duplicates() {
let mut e = Engine::new();
let r = rows(
e.execute("SELECT COUNT(*) FROM unnest(ARRAY['a','b','b','c','c','c']) AS u")
.unwrap(),
);
assert_eq!(r.len(), 1);
assert_eq!(r[0][0], Value::BigInt(6));
}
#[test]
fn string_agg_over_unnest() {
let mut e = Engine::new();
let r = rows(
e.execute("SELECT string_agg(u, ',') FROM unnest(ARRAY['x','y','z']) AS u")
.unwrap(),
);
assert_eq!(r.len(), 1);
assert_eq!(r[0][0], Value::text("x,y,z"));
}
#[test]
fn projection_path_still_works_no_aggregate() {
// Regression: existing non-aggregate paths (just projection +
// ORDER BY / LIMIT) must continue to work.
let mut e = Engine::new();
let r = rows(
e.execute("SELECT g FROM generate_series(1, 3) AS g ORDER BY g DESC")
.unwrap(),
);
assert_eq!(r.len(), 3);
// generate_series(int4) → int4 elements (matching PG).
assert_eq!(r[0][0], Value::Int(3));
assert_eq!(r[1][0], Value::Int(2));
assert_eq!(r[2][0], Value::Int(1));
}
#[test]
fn group_by_with_generate_series_via_mod_fn() {
// GROUP BY on a derived value from generate_series.
// 1..=10 grouped by `mod(g, 2)` → 5 odd, 5 even. (SPG uses
// the `mod(a, b)` function form; `%` isn't a lexer token.)
let mut e = Engine::new();
let mut r = rows(
e.execute(
"SELECT mod(g, 2) AS parity, COUNT(*) FROM generate_series(1, 10) AS g \
GROUP BY mod(g, 2) ORDER BY parity",
)
.unwrap(),
);
r.sort_by_key(|row| match row[0] {
Value::BigInt(n) => n,
Value::Int(n) => n as i64,
_ => 0,
});
assert_eq!(r.len(), 2);
// 0 (even): 5
// 1 (odd): 5
assert_eq!(r[0][1], Value::BigInt(5));
assert_eq!(r[1][1], Value::BigInt(5));
}
/// r997 — a set-returning SELECT item must not be deferred past the sort.
///
/// v7.37.x added a deferral: on `GROUP BY g ORDER BY <agg> LIMIT k` the
/// per-item projection is skipped for every group and run afterwards on
/// the top-k survivors, which on the mailrs shape turns 40 000 evaluations
/// into 100. The completion evaluates each item scalarly, and the branch
/// that expands a set-returning item into one row per element is the one
/// the deferral skips — so a qualifying query came back as
/// `function unnest(integer[]) does not exist`, the exact error round 621
/// had fixed, reintroduced for the shapes that qualify.
///
/// Differential against live PG18.4 showed the same query answering
/// correctly without LIMIT, with LIMIT >= the group count, and with a
/// HAVING — the three cases where the deferral was already off — which is
/// what identified the deferral rather than the expansion.
///
/// The row COUNT is what this pins. Which rows survive a LIMIT that cuts
/// inside a group is not pinned, because the ORDER BY here ties across a
/// group's expanded rows and neither engine promises an order there.
#[test]
fn a_set_returning_item_survives_order_by_with_limit() {
let mut e = Engine::new();
e.execute("CREATE TABLE srf_defer (g INT, v INT)").unwrap();
for i in 1..=40 {
e.execute(&format!("INSERT INTO srf_defer VALUES ({}, {i})", i % 20))
.unwrap();
}
// 20 groups, two elements per group, LIMIT below the group count: the
// shape that qualifies to defer.
let r = e
.execute(
"SELECT unnest(ARRAY[1,2]), count(*) FROM srf_defer \
GROUP BY g ORDER BY count(*) DESC, g LIMIT 5",
)
.expect("a set-returning item with ORDER BY + LIMIT must not error");
assert_eq!(rows(r).len(), 5, "LIMIT 5 over expanded rows returns 5");
// The cases the deferral never covered, unchanged: two rows per group
// across all twenty groups.
let r = e
.execute(
"SELECT unnest(ARRAY[1,2]), count(*) FROM srf_defer \
GROUP BY g ORDER BY count(*) DESC, g",
)
.expect("no LIMIT");
assert_eq!(rows(r).len(), 40, "20 groups x 2 elements");
let r = e
.execute(
"SELECT unnest(ARRAY[1,2]), count(*) FROM srf_defer \
GROUP BY g ORDER BY count(*) DESC, g LIMIT 100",
)
.expect("LIMIT above the group count");
assert_eq!(
rows(r).len(),
40,
"a LIMIT that cannot bite changes nothing"
);
// And the deferral still applies when nothing is set-returning: this
// one is here so a fix that simply turned the optimisation off would
// not pass unnoticed — it pins the answer, and the perf gate pins the
// speed.
let r = e
.execute("SELECT g, count(*) FROM srf_defer GROUP BY g ORDER BY count(*) DESC, g LIMIT 5")
.expect("no SRF");
assert_eq!(rows(r).len(), 5);
}
/// r999 — `SELECT DISTINCT` deduplicates over a GROUP BY query.
///
/// It never had. Every other path does: the scan paths, the window path
/// and the set operations all call `dedup_rows`, and the aggregate path
/// simply returned one row per group. `SELECT DISTINCT count(*) FROM t
/// GROUP BY g` came back with 200 rows where PG18.4 returns 1, all of
/// them the same value — not an error, not a missing column, 199 extra
/// rows in a query anyone might write.
///
/// The top-K sink in that same function says it outright — "no DISTINCT
/// (would need post-dedup, can't truncate during sort)" — so the sink
/// correctly declined to truncate, and the post-dedup it named was never
/// written.
///
/// Found while validating an unrelated change, and confirmed against the
/// previous binary before being attributed: the behaviour is identical on
/// round 997, so it predates that work.
#[test]
fn select_distinct_deduplicates_over_a_group_by() {
let mut e = Engine::new();
e.execute("CREATE TABLE dg (g INT, v INT)").unwrap();
for i in 1..=200 {
e.execute(&format!("INSERT INTO dg VALUES ({}, {i})", i % 20))
.unwrap();
}
// Twenty groups, every count the same: one distinct row.
let r = e
.execute("SELECT DISTINCT count(*) FROM dg GROUP BY g")
.unwrap();
assert_eq!(rows(r).len(), 1, "twenty identical counts are one row");
let r = e
.execute("SELECT DISTINCT count(*) FROM dg GROUP BY g HAVING count(*) > 0")
.unwrap();
assert_eq!(rows(r).len(), 1, "a HAVING does not change the dedup");
// Partial: g % 4 collapses twenty groups onto four values.
let r = e
.execute("SELECT DISTINCT g % 4 FROM dg GROUP BY g ORDER BY 1")
.unwrap();
assert_eq!(rows(r).len(), 4, "collapses onto four");
// And rows that genuinely differ are all kept — the control that
// separates "deduplicates" from "drops rows".
let r = e
.execute("SELECT DISTINCT count(*), g FROM dg GROUP BY g")
.unwrap();
assert_eq!(rows(r).len(), 20, "distinct rows survive");
// Without DISTINCT nothing is removed.
let r = e.execute("SELECT count(*) FROM dg GROUP BY g").unwrap();
assert_eq!(rows(r).len(), 20, "no DISTINCT, no dedup");
}
/// r1000 — an aggregate ORDER BY may name a set-returning output column.
///
/// `SELECT unnest(ARRAY[2,1]) AS u, count(*) FROM t GROUP BY g ORDER BY 1`
/// answered `column "u" does not exist`, and spelled `ORDER BY u` it
/// answered `function unnest(integer[]) does not exist` instead — two
/// spellings of one thing, both refused, both answered by PG18.4.
///
/// Round 80 had already decided the hard part: a positional key over a
/// set-returning item resolves to the item's output NAME rather than its
/// expression, because the expression is the whole set and evaluates once
/// per group, which silently sorted nothing. What was missing was the
/// other half on each side — the aggregate sort evaluated that name
/// against the synthetic group schema, which carries `__agg_N` and
/// `__grp_K` and no output aliases; and the alias spelling still
/// substituted the expression, so it failed differently for the same
/// reason.
///
/// Now a key that names an output column and nothing in the synthetic
/// schema is read from the projected row, where expansion has already put
/// the per-row value. Synthetic names keep precedence, so keys that
/// resolved before resolve the same way.
#[test]
fn an_aggregate_order_by_can_name_a_set_returning_output_column() {
let mut e = Engine::new();
e.execute("CREATE TABLE og (g INT, v INT)").unwrap();
for i in 1..=20 {
e.execute(&format!("INSERT INTO og VALUES ({}, {i})", i % 5))
.unwrap();
}
let first = |r: QueryResult| -> Vec<i32> {
rows(r)
.into_iter()
.map(|vals| match vals[0] {
Value::Int(n) => n,
ref other => panic!("int expected, got {other:?}"),
})
.collect()
};
// ARRAY[2,1] expands to 2 then 1 per group, so a sort that works puts
// every 1 before every 2 — and a sort that silently does nothing
// leaves them alternating.
let r = e
.execute("SELECT unnest(ARRAY[2,1]) AS u, count(*) FROM og GROUP BY g ORDER BY 1")
.expect("ORDER BY a positional set-returning key");
let got = first(r);
assert_eq!(got.len(), 10, "5 groups x 2 elements");
assert!(got.windows(2).all(|w| w[0] <= w[1]), "sorted, got {got:?}");
// The same query by alias must behave identically; it used to fail
// differently, which is how one bug looked like two.
let r = e
.execute("SELECT unnest(ARRAY[2,1]) AS u, count(*) FROM og GROUP BY g ORDER BY u")
.expect("ORDER BY the alias of a set-returning item");
assert_eq!(first(r), got, "the alias spelling matches the ordinal one");
// And a key that resolves in the synthetic schema still does: this is
// the control for "output names take over too much".
let r = e
.execute("SELECT count(*) AS c, g FROM og GROUP BY g ORDER BY c, g")
.expect("ORDER BY an aggregate alias");
assert_eq!(rows(r).len(), 5);
}