This is an automated email from the ASF dual-hosted git repository.

mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new dc4f7f3700 [CALCITE-7683] SessionizationEnumerator produces wrong 
results for SESSION table function
dc4f7f3700 is described below

commit dc4f7f37005d7b267fb578bf8e518160bc21aada
Author: Mihai Budiu <[email protected]>
AuthorDate: Fri Jul 31 10:51:16 2026 -0700

    [CALCITE-7683] SessionizationEnumerator produces wrong results for SESSION 
table function
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../calcite/adapter/enumerable/EnumUtils.java      | 34 ++++++++++------
 .../org/apache/calcite/runtime/SortedMultiMap.java |  4 ++
 core/src/test/resources/sql/stream.iq              | 47 ++++++++++++++++++++++
 3 files changed, 73 insertions(+), 12 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java
index d1c395c226..9cd7ba802b 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java
@@ -55,7 +55,6 @@
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexProgramBuilder;
 import org.apache.calcite.runtime.PairList;
-import org.apache.calcite.runtime.SortedMultiMap;
 import org.apache.calcite.runtime.SqlFunctions;
 import org.apache.calcite.runtime.Utilities;
 import org.apache.calcite.sql.SqlCollation;
@@ -87,7 +86,9 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.NavigableMap;
 import java.util.TimeZone;
+import java.util.TreeMap;
 import java.util.function.Function;
 
 import static 
org.apache.calcite.config.CalciteSystemProperty.JOIN_SELECTOR_COMPACT_CODE_THRESHOLD;
@@ -1163,15 +1164,22 @@ private static class SessionizationEnumerator 
implements Enumerator<@Nullable Ob
     }
 
     @Override public @Nullable Object[] current() {
-      if (!initialized) {
-        initialize();
-        initialized = true;
-      }
       return list.removeFirst();
     }
 
     @Override public boolean moveNext() {
-      return initialized ? !list.isEmpty() : inputEnumerator.moveNext();
+      // Sessionization needs to see all the input, so the first call consumes
+      // it entirely.  Initializing here rather than in current() lets this
+      // method report that there is nothing to return, which happens when
+      // every row was discarded for having a NULL timestamp.
+      if (!initialized) {
+        initialized = true;
+        if (!inputEnumerator.moveNext()) {
+          return false;
+        }
+        initialize();
+      }
+      return !list.isEmpty();
     }
 
     @Override public void reset() {
@@ -1196,24 +1204,26 @@ private void initialize() {
         elements.add(inputEnumerator.current());
       }
 
-      Map<@Nullable Object, SortedMultiMap<Pair<Long, Long>, @Nullable 
Object[]>> sessionKeyMap =
-          new HashMap<>();
+      // The windows of each key are kept sorted by start time; the merge
+      // below only compares a window with the one that precedes it.
+      Map<@Nullable Object, NavigableMap<Pair<Long, Long>, List<@Nullable 
Object[]>>>
+          sessionKeyMap = new HashMap<>();
       for (@Nullable Object[] element : elements) {
         // A key column index of -1 means that there is no key; every element
         // then maps to the same (null) key, forming one session timeline.
         Object key = indexOfKeyColumn < 0 ? null : element[indexOfKeyColumn];
-        SortedMultiMap<Pair<Long, Long>, @Nullable Object[]> session =
-            sessionKeyMap.computeIfAbsent(key, k -> new SortedMultiMap<>());
         Object watermark =
             requireNonNull(element[indexOfWatermarkedColumn],
                 "element[indexOfWatermarkedColumn]");
+        NavigableMap<Pair<Long, Long>, List<@Nullable Object[]>> session =
+            sessionKeyMap.computeIfAbsent(key, k -> new TreeMap<>());
         Pair<Long, Long> initWindow =
             computeInitWindow(SqlFunctions.toLong(watermark), gap);
-        session.putMulti(initWindow, element);
+        session.computeIfAbsent(initWindow, k -> new 
ArrayList<>()).add(element);
       }
 
       // merge per key session windows if there is any overlap between windows.
-      for (Map.Entry<@Nullable Object, SortedMultiMap<Pair<Long, Long>, 
@Nullable Object[]>>
+      for (Map.Entry<@Nullable Object, NavigableMap<Pair<Long, Long>, 
List<@Nullable Object[]>>>
           perKeyEntry : sessionKeyMap.entrySet()) {
         Map<Pair<Long, Long>, List<@Nullable Object[]>> finalWindowElementsMap 
= new HashMap<>();
         Pair<Long, Long> currentWindow = null;
diff --git a/core/src/main/java/org/apache/calcite/runtime/SortedMultiMap.java 
b/core/src/main/java/org/apache/calcite/runtime/SortedMultiMap.java
index 7b2448c5c9..958d3849f9 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SortedMultiMap.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SortedMultiMap.java
@@ -28,6 +28,10 @@
  * Map that allows you to partition values into lists according to a common
  * key, and then convert those lists into an iterator of sorted arrays.
  *
+ * <p>Only the values are sorted, by {@link #arrays(Comparator)}; the keys are
+ * not, because this map extends {@link HashMap}. Use a {@link 
java.util.TreeMap}
+ * if you need to visit the keys in order.
+ *
  * @param <K> Key type
  * @param <V> Value type
  */
diff --git a/core/src/test/resources/sql/stream.iq 
b/core/src/test/resources/sql/stream.iq
index b9df0692cc..4328a63cca 100644
--- a/core/src/test/resources/sql/stream.iq
+++ b/core/src/test/resources/sql/stream.iq
@@ -327,3 +327,50 @@ SELECT * FROM TABLE(
 (5 rows)
 
 !ok
+
+# Test case for [CALCITE-7683] SessionizationEnumerator produces wrong results
+# for SESSION table function.
+# Here 10:05 and 10:30 are 25 minutes apart and 10:40 and 11:30 are 50 minutes
+# apart, both more than the 15 minute gap, so the rows form three sessions.
+SELECT * FROM TABLE(
+  SESSION(
+    (SELECT * FROM (VALUES
+      (TIMESTAMP '2020-01-01 10:00:00', 'a'),
+      (TIMESTAMP '2020-01-01 10:05:00', 'a'),
+      (TIMESTAMP '2020-01-01 10:30:00', 'a'),
+      (TIMESTAMP '2020-01-01 10:40:00', 'a'),
+      (TIMESTAMP '2020-01-01 11:30:00', 'a')) AS T(TS, UID)),
+    DESCRIPTOR(TS), DESCRIPTOR(UID), INTERVAL '15' MINUTE))
+ORDER BY TS;
++---------------------+-----+---------------------+---------------------+
+| TS                  | UID | window_start        | window_end          |
++---------------------+-----+---------------------+---------------------+
+| 2020-01-01 10:00:00 | a   | 2020-01-01 10:00:00 | 2020-01-01 10:20:00 |
+| 2020-01-01 10:05:00 | a   | 2020-01-01 10:00:00 | 2020-01-01 10:20:00 |
+| 2020-01-01 10:30:00 | a   | 2020-01-01 10:30:00 | 2020-01-01 10:55:00 |
+| 2020-01-01 10:40:00 | a   | 2020-01-01 10:30:00 | 2020-01-01 10:55:00 |
+| 2020-01-01 11:30:00 | a   | 2020-01-01 11:30:00 | 2020-01-01 11:45:00 |
++---------------------+-----+---------------------+---------------------+
+(5 rows)
+
+!ok
+
+# Test case for [CALCITE-7683] SessionizationEnumerator produces wrong results
+# for SESSION table function.
+SELECT * FROM TABLE(
+  SESSION(
+    (SELECT * FROM (VALUES
+      (TIMESTAMP '2020-01-01 10:00:00', 'a'),
+      (TIMESTAMP '2020-01-01 10:05:00', 'a'),
+      (TIMESTAMP '2020-01-01 10:30:00', 'a')) AS T(TS, UID)),
+    DESCRIPTOR(TS), DESCRIPTOR(UID), INTERVAL '15' MINUTE));
++---------------------+-----+---------------------+---------------------+
+| TS                  | UID | window_start        | window_end          |
++---------------------+-----+---------------------+---------------------+
+| 2020-01-01 10:00:00 | a   | 2020-01-01 10:00:00 | 2020-01-01 10:20:00 |
+| 2020-01-01 10:05:00 | a   | 2020-01-01 10:00:00 | 2020-01-01 10:20:00 |
+| 2020-01-01 10:30:00 | a   | 2020-01-01 10:30:00 | 2020-01-01 10:45:00 |
++---------------------+-----+---------------------+---------------------+
+(3 rows)
+
+!ok

Reply via email to