Skip to content

Commit fc1cf82

Browse files
committed
Fix 8 SQL conformance bugs found via PostgreSQL regression test analysis
- SELECT without FROM: synthesize single-row dummy table - Nested CASE in ELSE: recursive evaluation in vectorized path - Empty window frame returns NULL (not 0) for SUM/COUNT/AVG - GROUP BY no longer drops groups where COUNT(col)=0 (all-NULL) - ORDER BY aggregate not in SELECT: inject into agg list, strip from results - CASE with aggregate condition: post-aggregate expression evaluation - Add 6 new PostgreSQL regression sqllogictest files
1 parent 03a7667 commit fc1cf82

File tree

11 files changed

+930
-49
lines changed

11 files changed

+930
-49
lines changed

src/stratum/query.clj

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,7 @@
893893
:agg [[:sum :revenue]]
894894
:group [:region]
895895
:result :columns})"
896-
[{:keys [from join where select agg group having order limit offset result distinct window _union _set-op _having-only-keys]
896+
[{:keys [from join where select agg group having order limit offset result distinct window _union _set-op _having-only-keys _order-only-keys]
897897
:as query}]
898898
;; Handle set operations (UNION/INTERSECT/EXCEPT)
899899
(if-let [set-op (or _set-op (when _union {:op :union :queries (:queries _union) :all? (:all? _union)}))]
@@ -966,6 +966,9 @@
966966
(mapv #(apply dissoc % _having-only-keys) results)
967967
results)
968968
results (if (seq order) (post/apply-order results order limit offset) results)
969+
results (if (seq _order-only-keys)
970+
(mapv #(apply dissoc % _order-only-keys) results)
971+
results)
969972
results (if (or limit offset) (post/apply-limit-offset results limit offset) results)]
970973
results))
971974
;; Normal path
@@ -1591,6 +1594,9 @@
15911594
(mapv #(apply dissoc % _having-only-keys) results)
15921595
results)
15931596
results (if (seq order) (post/apply-order results order limit offset) results)
1597+
results (if (seq _order-only-keys)
1598+
(mapv #(apply dissoc % _order-only-keys) results)
1599+
results)
15941600
results (if (or limit offset) (post/apply-limit-offset results limit offset) results)]
15951601
results))))))))))
15961602

src/stratum/query/expression.clj

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -220,29 +220,73 @@
220220

221221
:case
222222
(let [branches (:branches expr)
223-
;; Build dictionary from all distinct string branch values
224-
all-strings (into [] (comp (map :val) (filter string?)) branches)
225-
dict (into-array String (distinct all-strings))
226-
dict-map (into {} (map-indexed (fn [i s] [s (long i)])) dict)
227-
codes (long-array length Long/MIN_VALUE)
228-
assigned (long-array length)
223+
;; Collect string literals and nested expression results
224+
;; First pass: evaluate nested expressions to get their dicts
225+
col-arrays (into {} (map (fn [[k v]] [k (:data v)])) columns)
229226
cache (java.util.HashMap.)
230-
;; Extract raw arrays for eval-case-pred-mask
231-
col-arrays (into {} (map (fn [[k v]] [k (:data v)])) columns)]
232-
(doseq [branch branches]
233-
(let [code (long (get dict-map (:val branch) Long/MIN_VALUE))]
227+
;; Evaluate nested expression branches and collect all strings
228+
branch-results (mapv (fn [branch]
229+
(let [v (:val branch)]
230+
(if (string? v)
231+
{:string v}
232+
;; Nested expression (e.g. nested CASE) — evaluate recursively
233+
{:nested (eval-string-expr v columns length)})))
234+
branches)
235+
;; Build unified dictionary from all string literals + nested dicts
236+
all-dict-entries (java.util.ArrayList.)
237+
_ (doseq [br branch-results]
238+
(if (:string br)
239+
(.add all-dict-entries (:string br))
240+
(let [^"[Ljava.lang.String;" d (:dict (:nested br))]
241+
(dotimes [j (alength d)]
242+
(.add all-dict-entries (aget d j))))))
243+
distinct-strings (vec (distinct all-dict-entries))
244+
dict (into-array String distinct-strings)
245+
dict-map (into {} (map-indexed (fn [i s] [s (long i)])) distinct-strings)
246+
codes (long-array length Long/MIN_VALUE)
247+
assigned (long-array length)]
248+
;; Process each branch
249+
(dotimes [bi (count branches)]
250+
(let [branch (nth branches bi)
251+
br-result (nth branch-results bi)]
234252
(if (= :else (:op branch))
235253
;; ELSE: assign to all unassigned rows
236-
(dotimes [i length]
237-
(when (zero? (aget assigned i))
238-
(aset codes i code)
239-
(aset assigned i 1)))
254+
(if (:string br-result)
255+
(let [code (long (get dict-map (:string br-result) Long/MIN_VALUE))]
256+
(dotimes [i length]
257+
(when (zero? (aget assigned i))
258+
(aset codes i code)
259+
(aset assigned i 1))))
260+
;; Nested expression result for ELSE
261+
(let [nested (:nested br-result)
262+
^longs nested-codes (:data nested)
263+
^"[Ljava.lang.String;" nested-dict (:dict nested)]
264+
(dotimes [i length]
265+
(when (zero? (aget assigned i))
266+
(let [nc (aget nested-codes i)]
267+
(if (= nc Long/MIN_VALUE)
268+
(aset codes i Long/MIN_VALUE)
269+
(aset codes i (long (get dict-map (aget nested-dict (int nc)) Long/MIN_VALUE)))))
270+
(aset assigned i 1)))))
240271
;; Conditional: evaluate predicate mask, assign matching unassigned rows
241272
(let [mask (eval-case-pred-mask (:pred branch) col-arrays length cache)]
242-
(dotimes [i length]
243-
(when (and (zero? (aget assigned i)) (== 1 (aget ^longs mask i)))
244-
(aset codes i code)
245-
(aset assigned i 1)))))))
273+
(if (:string br-result)
274+
(let [code (long (get dict-map (:string br-result) Long/MIN_VALUE))]
275+
(dotimes [i length]
276+
(when (and (zero? (aget assigned i)) (== 1 (aget ^longs mask i)))
277+
(aset codes i code)
278+
(aset assigned i 1))))
279+
;; Nested expression result for WHEN
280+
(let [nested (:nested br-result)
281+
^longs nested-codes (:data nested)
282+
^"[Ljava.lang.String;" nested-dict (:dict nested)]
283+
(dotimes [i length]
284+
(when (and (zero? (aget assigned i)) (== 1 (aget ^longs mask i)))
285+
(let [nc (aget nested-codes i)]
286+
(if (= nc Long/MIN_VALUE)
287+
(aset codes i Long/MIN_VALUE)
288+
(aset codes i (long (get dict-map (aget nested-dict (int nc)) Long/MIN_VALUE)))))
289+
(aset assigned i 1)))))))))
246290
{:type :int64 :data codes :dict dict :dict-type :string}))))
247291

248292
;; ============================================================================

src/stratum/query/group_by.clj

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3096,13 +3096,8 @@
30963096
(aget accs agg-base))]
30973097
[alias result]))
30983098
aggs))]
3099-
;; Filter out groups with zero valid agg rows (all NaN)
3100-
(let [max-cnt (loop [idx 0 mx 0.0]
3101-
(if (>= idx (count aggs))
3102-
mx
3103-
(recur (inc idx)
3104-
(Math/max mx (aget accs (inc (* idx 2)))))))]
3105-
(when (pos? max-cnt)
3106-
(merge base {:_count (long (aget accs 1))} agg-results))))))
3099+
;; Always emit group — HashMap membership implies existence.
3100+
;; count-non-null aggs legitimately have count=0 (all NULLs).
3101+
(merge base {:_count (long (aget accs 1))} agg-results))))
31073102
entries))))))
31083103

src/stratum/query/window.clj

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@
142142
(let [[n dir] start-bound]
143143
(case dir
144144
:preceding (max ps (- i (long n)))
145-
:following (min (dec pe) (+ i (long n)))))
145+
:following (min pe (+ i (long n)))))
146146
:else ps))
147147
win-end (int (cond
148148
(= end-bound :unbounded-following) pe
@@ -153,7 +153,9 @@
153153
:preceding (max ps (inc (- i (long n))))
154154
:following (min pe (inc (+ i (long n))))))
155155
:else (inc i)))]
156-
(aset result idx (- (aget prefix win-end) (aget prefix win-start))))))
156+
(aset result idx (if (>= win-start win-end)
157+
Double/NaN ;; empty frame → NULL per SQL standard
158+
(- (aget prefix win-end) (aget prefix win-start)))))))
157159
result))
158160

159161
(defn- compute-sliding-window-count
@@ -174,7 +176,7 @@
174176
(= start-bound :current-row) i
175177
(vector? start-bound)
176178
(let [[n dir] start-bound]
177-
(case dir :preceding (max ps (- i (long n))) :following (min (dec pe) (+ i (long n)))))
179+
(case dir :preceding (max ps (- i (long n))) :following (min pe (+ i (long n)))))
178180
:else ps))
179181
win-end (int (cond
180182
(= end-bound :unbounded-following) pe
@@ -183,7 +185,9 @@
183185
(let [[n dir] end-bound]
184186
(case dir :preceding (max ps (inc (- i (long n)))) :following (min pe (inc (+ i (long n))))))
185187
:else (inc i)))]
186-
(aset result idx (double (- win-end win-start)))))
188+
(aset result idx (if (>= win-start win-end)
189+
Double/NaN ;; empty frame → NULL per SQL standard
190+
(double (- win-end win-start))))))
187191
result))
188192

189193
;; ============================================================================
@@ -439,8 +443,10 @@
439443
(:start frame) (:end frame))
440444
result (double-array length)]
441445
(dotimes [i length]
442-
(aset result i (/ (aget ^doubles sums i)
443-
(Math/max 1.0 (aget ^doubles cnts i)))))
446+
(let [c (aget ^doubles cnts i)]
447+
(aset result i (if (Double/isNaN c)
448+
Double/NaN ;; empty frame → NULL
449+
(/ (aget ^doubles sums i) (Math/max 1.0 c))))))
444450
result)
445451
:else
446452
(let [result (double-array length)

0 commit comments

Comments
 (0)