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
# Tier 10 — aggregate and NULL edge semantics.
#
# Added 2026-08-02, seeded with P14. Tier 7 covers GROUP BY / HAVING over
# non-empty groups; what it never asked is what an aggregate does when there is
# nothing to aggregate. See SQL_PARITY.md.
# --- The baselines that work, so P14 is isolated to the empty ungrouped case ---
[[]]
= "count_nonempty"
= "international_sales.csv"
= "SELECT COUNT(*) AS n FROM international_sales WHERE region = 'Europe'"
[[]]
= "grouped_aggregate_empty"
= "international_sales.csv"
= "SELECT region, COUNT(*) AS n FROM international_sales WHERE region = 'Nowhere' GROUP BY region"
# AGREEs on zero rows, correctly: a GROUPED aggregate over no rows has no groups
# to report. This is what makes P14 specifically about the UNGROUPED form.
# --- P14: an ungrouped aggregate over an empty set returns no row at all ---
[[]]
= "count_star_empty"
= "international_sales.csv"
= "SELECT COUNT(*) AS n FROM international_sales WHERE region = 'Nowhere'"
= "DIFFER"
# P14. Returns 0 rows; standard SQL returns exactly one row containing 0.
[[]]
= "sum_empty"
= "international_sales.csv"
= "SELECT SUM(amount) AS total FROM international_sales WHERE region = 'Nowhere'"
= "DIFFER"
# P14, the NULL-returning half: one row containing NULL, not zero rows. COUNT
# and SUM differ in the value they must produce, so both are pinned.
[[]]
= "min_max_empty"
= "international_sales.csv"
= "SELECT MIN(amount) AS lo, MAX(amount) AS hi FROM international_sales WHERE region = 'Nowhere'"
= "DIFFER"
# P14, multi-aggregate form — the single output row must carry a NULL per
# aggregate, so a fix that emits one row must fill every column.
# ---------------------------------------------------------------------------
# NULL semantics. Uses null_edges.csv — see the fixture notes in 08_ordering.toml.
# ---------------------------------------------------------------------------
# --- Baselines: the NULL handling that is already correct ---
[[]]
= "agg_ignores_nulls"
= "null_edges.csv"
= "SELECT COUNT(*) AS rows_n, COUNT(score) AS score_n, SUM(score) AS s, AVG(score) AS a FROM null_edges"
# COUNT(*) counts rows, COUNT(col) skips NULLs, SUM/AVG skip NULLs. All correct.
[[]]
= "agg_all_null_group"
= "null_edges.csv"
= "SELECT COUNT(*) AS n, COUNT(score) AS nn, SUM(score) AS s FROM null_edges WHERE team = 'delta'"
# Every score in team 'delta' is NULL: COUNT(*)=2, COUNT(score)=0, SUM=NULL.
# Distinct from P14 — here there ARE rows, they just aggregate to nothing.
[[]]
= "group_by_null_key"
= "null_edges.csv"
= "SELECT team, COUNT(*) AS n FROM null_edges GROUP BY team"
# NULL forms its own group, correctly. Deliberately has NO ORDER BY so the
# harness compares as a multiset — adding `ORDER BY team` would drag P17's NULL
# placement in and make this case about ordering instead of grouping.
[[]]
= "distinct_with_nulls"
= "null_edges.csv"
= "SELECT DISTINCT score FROM null_edges"
[[]]
= "count_distinct_with_nulls"
= "null_edges.csv"
= "SELECT COUNT(DISTINCT score) AS d FROM null_edges"
[[]]
= "where_is_null"
= "null_edges.csv"
= "SELECT id FROM null_edges WHERE score IS NULL ORDER BY id"
[[]]
= "where_not_equal_excludes_null"
= "null_edges.csv"
= "SELECT id FROM null_edges WHERE score <> 50 ORDER BY id"
# Correct: `<>` excludes NULL rows. Contrast with NOT IN below, which does not —
# the inconsistency is what makes P19 a bug rather than a design choice.
[[]]
= "where_in_with_null_col"
= "null_edges.csv"
= "SELECT id FROM null_edges WHERE score IN (50, 70) ORDER BY id"
[[]]
= "join_on_null_key"
= "null_edges.csv"
= "SELECT a.id AS aid, b.id AS bid FROM null_edges a JOIN null_edges b ON a.partner_id = b.id ORDER BY a.id"
# NULL never equals NULL in a join key, so the 5 NULL partner_id rows drop out.
[[]]
= "null_arithmetic"
= "null_edges.csv"
= "SELECT id, score + 1 AS plus FROM null_edges ORDER BY id"
# NULL + 1 = NULL. Correct — which makes P20 below the odd one out.
[[]]
= "coalesce_null"
= "null_edges.csv"
= "SELECT id, COALESCE(score, -1) AS c FROM null_edges ORDER BY id"
# --- P18: `= NULL` matches instead of yielding UNKNOWN ---
[[]]
= "where_equals_null"
= "null_edges.csv"
= "SELECT id FROM null_edges WHERE score = NULL"
= "DIFFER"
# P18. We return the four NULL-score rows — `= NULL` is being treated as
# `IS NULL`. Under SQL three-valued logic `x = NULL` is UNKNOWN for every row
# including NULL ones, so the correct answer is zero rows. `IS NULL` (above)
# is the only way to match a NULL and it already works.
# --- P19: NOT IN does not exclude NULLs ---
[[]]
= "where_not_in_excludes_null"
= "null_edges.csv"
= "SELECT id FROM null_edges WHERE score NOT IN (50, 70) ORDER BY id"
= "DIFFER"
# P19. We return 8 rows, including the NULL-score rows; DuckDB returns 4.
# `NULL NOT IN (50, 70)` is UNKNOWN, not TRUE, so those rows must not pass.
# Note `where_not_equal_excludes_null` above gets the equivalent case right,
# so this is an inconsistency inside our own NULL handling.
# --- P20: `||` treats NULL as an empty string ---
[[]]
= "null_concat"
= "null_edges.csv"
= "SELECT id, team || '-' || label AS c FROM null_edges ORDER BY id"
= "DIFFER"
# P20. Row 2 (label IS NULL) gives us 'alpha-' where DuckDB gives NULL.
# Concatenating NULL yields NULL in standard SQL. Oracle takes our view, so
# this one is arguably a coercion-first design choice rather than a bug — it
# needs a decision recorded, not an automatic fix.