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

panxiaolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new a0b9e3784f9 [Bug](function) fix wrong result of window funnel v2 + 
deduplication/fixed mode (#62043)
a0b9e3784f9 is described below

commit a0b9e3784f982deeaf43fd9da03bdbb598da3e57
Author: Pxl <[email protected]>
AuthorDate: Tue Apr 7 15:39:14 2026 +0800

    [Bug](function) fix wrong result of window funnel v2 + deduplication/fixed 
mode (#62043)
    
    FIXED mode
    修复前的问题:旧代码从前往后只扫描一遍,只维护一个链。遇到新的条件1就重启一个新链,丢弃旧链。这样可能错过从后面某个条件1开始能匹配更长链的情况。
    
    FIXED mode 修复后的逻辑:
    
     1. 找到所有满足条件1的行作为候选起点
     2. 对每个起点,逐行往后检查:
     - 下一行必须匹配当前期望的下一个条件(条件链严格递增)
     - 如果下一行匹配了某个条件但不是期望的那个 → 链断裂,放弃这个起点
     - 不匹配任何条件的行会被跳过(这是
      4.1 的新行为,V1 会在此断裂)
     3. 取所有起点中最长的链作为结果
    
    DEDUP mode 修复前的问题:同样是单次扫描,一个链。无法正确处理"后面的起点能匹配更长链"的情况。
    
    DEDUP mode 修复后的逻辑:
    
     1. 找到所有满足条件1的行作为候选起点
     2. 对每个起点,按顺序搜索下一个期望的条件:
     - 找到匹配条件 K 的行后,检查中间经过的行是否有任何已匹配条件的重复出现
     - 有重复 → 链断裂(这就是 deduplication 的含义:同一条件出现两次就打断链)
     - 无重复 → 接受匹配,继续搜索下一个条件
     3. 取所有起点中最长的链作为结果
    
    两种模式的共同改变:旧代码只尝试一个起点(扫描一遍),新代码尝试每个可能的起点(多次扫描),取最优结果。这和 V1 的做法一致。
    
    This pull request refactors and improves the implementation of the
    window funnel aggregate function in `window_funnel_v2`, focusing on the
    DEDUPLICATION and FIXED modes. The changes enhance correctness by more
    closely matching the intended semantics, improve efficiency with
    multi-pass algorithms, and add comprehensive test coverage for edge
    cases and complex scenarios.
    
    ### Algorithmic and Semantic Improvements
    
    * Rewrote the DEDUPLICATION mode (`_get_deduplication`) to use a
    multi-pass, gap-based duplicate detection algorithm, accurately matching
    V1 semantics by checking for duplicate events in the "gap" between
    matched levels and pruning impossible chains early.
    * Refactored the FIXED mode (`_get_fixed`) to a multi-pass, row-based
    approach that aligns with documented V2 semantics, including the correct
    handling of irrelevant events and level jumps, and added pruning for
    efficiency.
    
    ### Code Simplification and Maintenance
    
    * Removed legacy and now-unnecessary helper functions (`_promote_level`,
    `_eliminate_chain`, and related timestamp tracking), streamlining the
    codebase and reducing complexity.
    
[[1]](diffhunk://#diff-1a1c09dde1a5d97a9723ffebb33ddb27344131ac2031c986ce6866bf248c5971L396-R599)
    
[[2]](diffhunk://#diff-1a1c09dde1a5d97a9723ffebb33ddb27344131ac2031c986ce6866bf248c5971L532-L551)
    * Added new helper methods (`_next_row_start`, `_row_start`,
    `_row_contains_level`) to clarify row and event handling, making the
    logic more readable and maintainable.
    
    ### Test Coverage
    
    * Added a comprehensive set of regression tests for DEDUPLICATION and
    FIXED modes, covering multi-event rows, duplicate detection, level
    jumps, window boundaries, irrelevant event skipping, and various edge
    cases, ensuring correctness and robustness of the new algorithms.
---
 .../aggregate_function_window_funnel_v2.h          | 266 +++++---
 .../exprs/aggregate/vec_window_funnel_v2_test.cpp  | 743 +++++++++++++++++++++
 .../data/query_p0/aggregate/window_funnel_v2.out   |  56 ++
 .../query_p0/aggregate/window_funnel_v2.groovy     | 412 ++++++++++++
 4 files changed, 1377 insertions(+), 100 deletions(-)

diff --git a/be/src/exprs/aggregate/aggregate_function_window_funnel_v2.h 
b/be/src/exprs/aggregate/aggregate_function_window_funnel_v2.h
index 8038c0bef47..421fe701dae 100644
--- a/be/src/exprs/aggregate/aggregate_function_window_funnel_v2.h
+++ b/be/src/exprs/aggregate/aggregate_function_window_funnel_v2.h
@@ -393,9 +393,7 @@ private:
                     if (matched) {
                         events_timestamp[event_idx] = 
{events_timestamp[event_idx - 1].first_ts,
                                                        evt.timestamp, i};
-                        if (event_idx > curr_level) {
-                            curr_level = event_idx;
-                        }
+                        curr_level = std::max(event_idx, curr_level);
                         if (event_idx + 1 == event_count) {
                             return event_count;
                         }
@@ -403,113 +401,201 @@ private:
                 }
             }
 
-            if (curr_level + 1 > max_level) {
-                max_level = curr_level + 1;
-            }
+            max_level = std::max(curr_level + 1, max_level);
         }
         return max_level;
     }
 
-    /// DEDUPLICATION mode: if a previously matched event level appears again,
-    /// the current chain is terminated and max_level is updated.
-    /// This preserves V1 semantics where duplicate events break the chain.
+    /// DEDUPLICATION mode (multi-pass, matching V1 semantics):
+    ///
+    /// V1 tries each E0-matching row as a chain starting point and searches 
for
+    /// conditions one at a time. Before accepting a match for condition K, V1 
checks
+    /// the "gap" (rows between the previous match and the current match) for 
any
+    /// re-occurrence of already-matched conditions 0..K-1. If found, the 
chain breaks.
+    ///
+    /// A single-pass approach cannot correctly implement this because V2's 
greedy
+    /// multi-condition-per-row advancement creates premature higher-level 
matches
+    /// that then conflict with later events. The multi-pass approach iterates 
over
+    /// each event-0 occurrence, building chains one level at a time with 
gap-based
+    /// duplicate detection.
     int _get_deduplication() const {
-        std::vector<TimestampPair> events_timestamp(event_count);
-        int max_level = -1;
-        int curr_level = -1;
+        int max_level = 0;
+        const size_t list_size = events_list.size();
 
-        for (size_t i = 0; i < events_list.size(); ++i) {
-            const auto& evt = events_list[i];
-            int event_idx = get_event_idx(evt.event_idx) - 1;
+        for (size_t start = 0; start < list_size; ++start) {
+            // Remaining-events pruning: if remaining events can't beat 
max_level, stop.
+            if (static_cast<int>(list_size - start) <= max_level) {
+                break;
+            }
 
-            if (event_idx == 0) {
-                // Duplicate of event 0: terminate current chain first
-                if (events_timestamp[0].has_value()) {
-                    if (curr_level > max_level) {
-                        max_level = curr_level;
+            int start_event = get_event_idx(events_list[start].event_idx) - 1;
+            if (start_event != 0) {
+                continue;
+            }
+
+            // Build a chain from this event-0
+            UInt64 first_ts = events_list[start].timestamp;
+            int curr_level = 0;
+            size_t last_advance_idx = start;
+
+            for (size_t i = start + 1; i < list_size; ++i) {
+                int event_idx = get_event_idx(events_list[i].event_idx) - 1;
+
+                // Only search for the next expected level (matches V1's 
sequential scan)
+                if (event_idx != curr_level + 1) {
+                    continue;
+                }
+
+                // Must be from a different row than the last chain event
+                if (_is_same_row(last_advance_idx, i)) {
+                    continue;
+                }
+
+                // Window check
+                if (!_within_window(first_ts, events_list[i].timestamp)) {
+                    break;
+                }
+
+                // Gap-based dedup check (V1 semantics):
+                // Check if any event at a level below the target 
(0..event_idx-1)
+                // fired in the gap between the last chain event's row and the
+                // current event's row.
+                size_t gap_start = _next_row_start(last_advance_idx + 1);
+                size_t gap_end = _row_start(i);
+
+                bool dup = false;
+                if (gap_start < gap_end) {
+                    for (size_t j = gap_start; j < gap_end; ++j) {
+                        int j_level = get_event_idx(events_list[j].event_idx) 
- 1;
+                        if (j_level < event_idx) {
+                            dup = true;
+                            break;
+                        }
                     }
-                    _eliminate_chain(curr_level, events_timestamp);
                 }
-                // Start a new chain
-                events_timestamp[0] = {evt.timestamp, evt.timestamp, i};
-                curr_level = 0;
-            } else if (events_timestamp[event_idx].has_value()) {
-                // Duplicate event detected: this level was already matched
-                if (curr_level > max_level) {
-                    max_level = curr_level;
+
+                if (dup) {
+                    break;
                 }
-                // Eliminate current chain
-                _eliminate_chain(curr_level, events_timestamp);
-            } else if (events_timestamp[event_idx - 1].has_value() &&
-                       !_is_same_row(events_timestamp[event_idx - 
1].last_list_idx, i)) {
-                // Must be from a DIFFERENT row than the predecessor level
-                if (_promote_level(events_timestamp, evt.timestamp, i, 
event_idx, curr_level,
-                                   false)) {
+
+                // Accept this match and advance the chain
+                curr_level = event_idx;
+                last_advance_idx = i;
+
+                if (curr_level + 1 == event_count) {
                     return event_count;
                 }
             }
-        }
 
-        if (curr_level > max_level) {
-            return curr_level + 1;
+            max_level = std::max(curr_level + 1, max_level);
         }
-        return max_level + 1;
+        return max_level;
     }
 
-    /// FIXED mode (StarRocks-style semantics): if a matched event appears 
whose
-    /// predecessor level has NOT been matched, the chain is broken (event 
level jumped).
-    /// Note: V2 semantics differ from V1. V1 checks physical row adjacency;
-    /// V2 checks event level continuity (unmatched rows don't break the 
chain).
+    /// FIXED mode (multi-pass, row-based).
+    ///
+    /// Row-by-row semantics similar to V1: after matching c_K at a row, the 
NEXT
+    /// event-producing row must match c_{K+1} or the chain breaks.
+    ///
+    /// Difference from V1: rows matching no condition ("truly irrelevant") are
+    /// absent from events_list and implicitly skipped. V1 treats them as
+    /// non-matching and breaks the chain. This is the documented 4.1 behavior
+    /// change: irrelevant events no longer break the chain.
+    ///
+    /// Like V1, tries every c1 event as a potential chain starting point and
+    /// returns the maximum level reached across all attempts.
+    ///
+    /// Within each row, events are stored in descending condition-index order:
+    /// [c_max(cont=0), ..., c_min(cont=N)]. The first (non-continuation) event
+    /// carries the row's highest matched condition index. c1 (index 1 after 
add,
+    /// 0 after -1) is always the last event of its row.
     int _get_fixed() const {
-        std::vector<TimestampPair> events_timestamp(event_count);
         int max_level = -1;
-        int curr_level = -1;
-        bool first_event = false;
 
-        for (size_t i = 0; i < events_list.size(); ++i) {
-            const auto& evt = events_list[i];
-            int event_idx = get_event_idx(evt.event_idx) - 1;
+        for (size_t start_i = 0; start_i < events_list.size(); ++start_i) {
+            // Only start from c1 events (condition 0 in 0-based)
+            if (get_event_idx(events_list[start_i].event_idx) != 1) continue;
 
-            if (event_idx == 0) {
-                // Save current chain before starting a new one
-                if (events_timestamp[0].has_value()) {
-                    if (curr_level > max_level) {
-                        max_level = curr_level;
-                    }
-                    _eliminate_chain(curr_level, events_timestamp);
-                }
-                events_timestamp[0] = {evt.timestamp, evt.timestamp, i};
-                curr_level = 0;
-                first_event = true;
-            } else if (first_event && !events_timestamp[event_idx - 
1].has_value()) {
-                // Event level jumped: predecessor was not matched
-                if (curr_level >= 0) {
-                    if (curr_level > max_level) {
-                        max_level = curr_level;
-                    }
-                    _eliminate_chain(curr_level, events_timestamp);
+            UInt64 chain_start_ts = events_list[start_i].timestamp;
+            int curr_level = 0;
+
+            // c1 is always the last event of its row, so start_i+1 is
+            // the first event of the next row (or end of list).
+            size_t i = start_i + 1;
+
+            while (i < events_list.size() && curr_level + 1 < event_count) {
+                // Skip continuation events to reach the first event of the 
next row.
+                i = _next_row_start(i);
+                if (i >= events_list.size()) break;
+
+                // Window check against the row's timestamp.
+                if (!_within_window(chain_start_ts, events_list[i].timestamp)) 
break;
+
+                int expected = curr_level + 1;
+                int highest = get_event_idx(events_list[i].event_idx) - 1;
+
+                if (highest < expected) {
+                    // Row's highest matched condition is below expected → 
break.
+                    break;
                 }
-            } else if (events_timestamp[event_idx - 1].has_value() &&
-                       !_is_same_row(events_timestamp[event_idx - 
1].last_list_idx, i)) {
-                // Must be from a DIFFERENT row than the predecessor level
-                if (_promote_level(events_timestamp, evt.timestamp, i, 
event_idx, curr_level,
-                                   false)) {
-                    return event_count;
+
+                if (!_row_contains_level(i, expected)) {
+                    // Row matches higher conditions but not the expected one
+                    // (level jump) → break.
+                    break;
                 }
+
+                curr_level = expected;
+
+                // Advance past this row to the start of the next one.
+                i = _next_row_start(i + 1);
+            }
+
+            if (curr_level > max_level) {
+                max_level = curr_level;
+                if (max_level + 1 == event_count) return event_count;
             }
-        }
 
-        if (curr_level > max_level) {
-            return curr_level + 1;
+            // Prune: remaining events cannot beat current best.
+            if (static_cast<int>(events_list.size() - start_i) <= max_level) 
break;
         }
+
         return max_level + 1;
     }
 
-    /// Clear the current event chain back to the beginning.
-    static void _eliminate_chain(int& curr_level, std::vector<TimestampPair>& 
events_timestamp) {
-        for (; curr_level >= 0; --curr_level) {
-            events_timestamp[curr_level].reset();
+    /// Advance past continuation events starting at `i`, returning the index 
of the
+    /// next non-continuation event (i.e., the first event of the next row), or
+    /// events_list.size() if no more rows remain.
+    size_t _next_row_start(size_t i) const {
+        while (i < events_list.size() && 
is_continuation(events_list[i].event_idx)) {
+            ++i;
+        }
+        return i;
+    }
+
+    /// Walk backward from `i` to find the first event of the row containing 
`i`.
+    /// The first event of a row has no continuation flag set.
+    size_t _row_start(size_t i) const {
+        while (i > 0 && is_continuation(events_list[i].event_idx)) {
+            --i;
         }
+        return i;
+    }
+
+    /// Check if the row whose first event is at `row_first` contains an event
+    /// matching the given 0-based `target_level`. The first event of a row 
has the
+    /// highest matched condition index; subsequent continuation events have 
lower indices.
+    bool _row_contains_level(size_t row_first, int target_level) const {
+        if (get_event_idx(events_list[row_first].event_idx) - 1 == 
target_level) {
+            return true;
+        }
+        for (size_t j = row_first + 1;
+             j < events_list.size() && 
is_continuation(events_list[j].event_idx); ++j) {
+            if (get_event_idx(events_list[j].event_idx) - 1 == target_level) {
+                return true;
+            }
+        }
+        return false;
     }
 
     /// Check if the event at `current_idx` in events_list is from the same 
original row
@@ -529,26 +615,6 @@ private:
         }
         return true;
     }
-
-    /// Try to promote the chain to the next level.
-    /// Returns true if we've matched all events (early termination).
-    bool _promote_level(std::vector<TimestampPair>& events_timestamp, UInt64 
ts, size_t list_idx,
-                        int event_idx, int& curr_level, bool increase_mode) 
const {
-        bool matched = _within_window(events_timestamp[event_idx - 
1].first_ts, ts);
-        if (increase_mode) {
-            matched = matched && events_timestamp[event_idx - 1].last_ts < ts;
-        }
-        if (matched) {
-            events_timestamp[event_idx] = {events_timestamp[event_idx - 
1].first_ts, ts, list_idx};
-            if (event_idx > curr_level) {
-                curr_level = event_idx;
-            }
-            if (event_idx + 1 == event_count) {
-                return true;
-            }
-        }
-        return false;
-    }
 };
 
 class AggregateFunctionWindowFunnelV2
diff --git a/be/test/exprs/aggregate/vec_window_funnel_v2_test.cpp 
b/be/test/exprs/aggregate/vec_window_funnel_v2_test.cpp
index 6e82d430513..cbf6d0f9bf8 100644
--- a/be/test/exprs/aggregate/vec_window_funnel_v2_test.cpp
+++ b/be/test/exprs/aggregate/vec_window_funnel_v2_test.cpp
@@ -1164,4 +1164,747 @@ TEST_F(VWindowFunnelV2Test, 
testIncreaseModeOldChainBetter) {
     }
 }
 
+// Test DEDUPLICATION mode: same-row multi-event matching should NOT break the 
chain.
+// This is the exact scenario from the bug report where V1 returns 3 but V2 
returned 2.
+//
+// 3 conditions, window=1 day (86400s), DEDUPLICATION mode
+//   Row 0 (t=10:00): event1=T, event2=T, event3=F  → matches E0+E1 on same row
+//   Row 1 (t=11:00): event1=T, event2=T, event3=F  → matches E0+E1 on same row
+//   Row 2 (t=12:00): event1=T, event2=F, event3=T  → matches E0+E2 on same row
+//
+// V2 stores events in reverse order per row: [E1, E0] for each multi-match 
row.
+// After sort, events_list for row0: (t=10:00, E1-cont), (t=10:00, E0)
+// Bug: when processing E0 from row0 after E1 was already used to advance the 
chain,
+// old V2 would break the chain. With the fix, E0 is recognized as same-row 
and skipped.
+//
+// Expected chain: E0@row0 → E1@row1 → E2@row2 = 3
+TEST_F(VWindowFunnelV2Test, testDeduplicationSameRowMultiEvent) {
+    AggregateFunctionSimpleFactory factory = 
AggregateFunctionSimpleFactory::instance();
+    DataTypes data_types_3 = {
+            std::make_shared<DataTypeInt64>(),      
std::make_shared<DataTypeString>(),
+            std::make_shared<DataTypeDateTimeV2>(), 
std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeUInt8>(),      
std::make_shared<DataTypeUInt8>()};
+    auto agg_func_3 = factory.get("window_funnel_v2", data_types_3, nullptr, 
false,
+                                  BeExecVersionManager::get_newest_version());
+    ASSERT_NE(agg_func_3, nullptr);
+
+    const int NUM_ROWS = 3;
+    auto column_mode = ColumnString::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_mode->insert(Field::create_field<TYPE_STRING>("deduplication"));
+    }
+    auto column_timestamp = ColumnDateTimeV2::create();
+    VecDateTimeValue tv0, tv1, tv2;
+    tv0.unchecked_set_time(2022, 3, 12, 10, 0, 0);
+    tv1.unchecked_set_time(2022, 3, 12, 11, 0, 0);
+    tv2.unchecked_set_time(2022, 3, 12, 12, 0, 0);
+    auto dtv2_0 = tv0.to_datetime_v2();
+    auto dtv2_1 = tv1.to_datetime_v2();
+    auto dtv2_2 = tv2.to_datetime_v2();
+    column_timestamp->insert_data((char*)&dtv2_0, 0);
+    column_timestamp->insert_data((char*)&dtv2_1, 0);
+    column_timestamp->insert_data((char*)&dtv2_2, 0);
+
+    // Row 0: event1=T, event2=T, event3=F  (matches E0 and E1)
+    // Row 1: event1=T, event2=T, event3=F  (matches E0 and E1)
+    // Row 2: event1=T, event2=F, event3=T  (matches E0 and E2)
+    auto column_event1 = ColumnUInt8::create();
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+
+    auto column_event2 = ColumnUInt8::create();
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(0));
+
+    auto column_event3 = ColumnUInt8::create();
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(1));
+
+    auto column_window = ColumnInt64::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_window->insert(Field::create_field<TYPE_BIGINT>(86400));
+    }
+
+    std::unique_ptr<char[]> memory(new char[agg_func_3->size_of_data()]);
+    AggregateDataPtr place = memory.get();
+    agg_func_3->create(place);
+    const IColumn* column[6] = {column_window.get(), column_mode.get(),   
column_timestamp.get(),
+                                column_event1.get(), column_event2.get(), 
column_event3.get()};
+    for (int i = 0; i < NUM_ROWS; i++) {
+        agg_func_3->add(place, column, i, arena);
+    }
+
+    ColumnInt32 column_result;
+    agg_func_3->insert_result_into(place, column_result);
+    // Chain: E0@row0 -> E1@row1 -> E2@row2 = 3
+    // Before fix: V2 returned 2 because same-row E0 broke the chain after E1 
was used.
+    EXPECT_EQ(column_result.get_data()[0], 3);
+    agg_func_3->destroy(place);
+}
+
+// Test DEDUPLICATION mode: a true duplicate on a DIFFERENT row should still 
break the chain.
+// This ensures the fix doesn't over-suppress chain breaks.
+//
+// 3 conditions, window=86400s, DEDUPLICATION mode
+//   Row 0 (t=10:00): event1=T only
+//   Row 1 (t=11:00): event2=T only
+//   Row 2 (t=12:00): event1=T only  ← true duplicate E0 from different row → 
breaks chain
+//   Row 3 (t=13:00): event3=T only
+//
+// Expected: 2 (chain E0@row0 → E1@row1, then E0@row2 breaks it; new chain 
E0@row2 alone = 1)
+TEST_F(VWindowFunnelV2Test, testDeduplicationTrueDuplicateStillBreaks) {
+    AggregateFunctionSimpleFactory factory = 
AggregateFunctionSimpleFactory::instance();
+    DataTypes data_types_3 = {
+            std::make_shared<DataTypeInt64>(),      
std::make_shared<DataTypeString>(),
+            std::make_shared<DataTypeDateTimeV2>(), 
std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeUInt8>(),      
std::make_shared<DataTypeUInt8>()};
+    auto agg_func_3 = factory.get("window_funnel_v2", data_types_3, nullptr, 
false,
+                                  BeExecVersionManager::get_newest_version());
+    ASSERT_NE(agg_func_3, nullptr);
+
+    const int NUM_ROWS = 4;
+    auto column_mode = ColumnString::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_mode->insert(Field::create_field<TYPE_STRING>("deduplication"));
+    }
+    auto column_timestamp = ColumnDateTimeV2::create();
+    VecDateTimeValue tv0, tv1, tv2, tv3;
+    tv0.unchecked_set_time(2022, 3, 12, 10, 0, 0);
+    tv1.unchecked_set_time(2022, 3, 12, 11, 0, 0);
+    tv2.unchecked_set_time(2022, 3, 12, 12, 0, 0);
+    tv3.unchecked_set_time(2022, 3, 12, 13, 0, 0);
+    auto dtv2_0 = tv0.to_datetime_v2();
+    auto dtv2_1 = tv1.to_datetime_v2();
+    auto dtv2_2 = tv2.to_datetime_v2();
+    auto dtv2_3 = tv3.to_datetime_v2();
+    column_timestamp->insert_data((char*)&dtv2_0, 0);
+    column_timestamp->insert_data((char*)&dtv2_1, 0);
+    column_timestamp->insert_data((char*)&dtv2_2, 0);
+    column_timestamp->insert_data((char*)&dtv2_3, 0);
+
+    // Row 0: event1=T only
+    // Row 1: event2=T only
+    // Row 2: event1=T only (true duplicate from different row)
+    // Row 3: event3=T only
+    auto column_event1 = ColumnUInt8::create();
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(0));
+
+    auto column_event2 = ColumnUInt8::create();
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(0));
+
+    auto column_event3 = ColumnUInt8::create();
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(1));
+
+    auto column_window = ColumnInt64::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_window->insert(Field::create_field<TYPE_BIGINT>(86400));
+    }
+
+    std::unique_ptr<char[]> memory(new char[agg_func_3->size_of_data()]);
+    AggregateDataPtr place = memory.get();
+    agg_func_3->create(place);
+    const IColumn* column[6] = {column_window.get(), column_mode.get(),   
column_timestamp.get(),
+                                column_event1.get(), column_event2.get(), 
column_event3.get()};
+    for (int i = 0; i < NUM_ROWS; i++) {
+        agg_func_3->add(place, column, i, arena);
+    }
+
+    ColumnInt32 column_result;
+    agg_func_3->insert_result_into(place, column_result);
+    // Chain: E0@row0 -> E1@row1 (level=2), then E0@row2 is a TRUE duplicate 
from a
+    // different row → breaks chain. New chain: E0@row2, no E1 → level=1.
+    // max(2, 1) = 2
+    EXPECT_EQ(column_result.get_data()[0], 2);
+    agg_func_3->destroy(place);
+}
+
+// Dedup mode: same-row E0 skip when chain is advanced by same-row events.
+// When a row matches all 3 conditions and its E2 advances the chain, E0 from
+// the same row should be skipped (not restart the chain) because the row 
already
+// contributed to chain advancement.
+TEST_F(VWindowFunnelV2Test, testDeduplicationE0PendingRestart) {
+    AggregateFunctionSimpleFactory factory = 
AggregateFunctionSimpleFactory::instance();
+    DataTypes data_types_3 = {
+            std::make_shared<DataTypeInt64>(),      
std::make_shared<DataTypeString>(),
+            std::make_shared<DataTypeDateTimeV2>(), 
std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeUInt8>(),      
std::make_shared<DataTypeUInt8>()};
+    auto agg_func_3 = factory.get("window_funnel_v2", data_types_3, nullptr, 
false,
+                                  BeExecVersionManager::get_newest_version());
+    ASSERT_NE(agg_func_3, nullptr);
+
+    // Scenario: 3 conditions, c1/c2/c3.
+    // Row 0 (t=10:00): c1=T only            → E0@R0, chain starts level=0
+    // Row 1 (t=11:00): c2=T only            → E1@R1, chain advances level=1
+    // Row 2 (t=12:00): c1=T, c2=T, c3=T    → E2(cont=0), E1(cont=1), 
E0(cont=1)
+    //   E2: predecessor E1 matched, not same row → promote level=2
+    //   E1(cont=1): duplicate, same row as E2@R2 → skip
+    //   E0(cont=1): same row as chain (E2@R2 at level 2) → skip
+    // Row 3 (t=13:00): c3=T only            → E2: duplicate level=2, 
different row → terminates chain
+    // Result: max(2+1, 0+1) = 3
+    const int NUM_ROWS = 4;
+    auto column_mode = ColumnString::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_mode->insert(Field::create_field<TYPE_STRING>("deduplication"));
+    }
+    auto column_timestamp = ColumnDateTimeV2::create();
+    VecDateTimeValue tv0, tv1, tv2, tv3;
+    tv0.unchecked_set_time(2022, 3, 12, 10, 0, 0);
+    tv1.unchecked_set_time(2022, 3, 12, 11, 0, 0);
+    tv2.unchecked_set_time(2022, 3, 12, 12, 0, 0);
+    tv3.unchecked_set_time(2022, 3, 12, 13, 0, 0);
+    auto dtv2_0 = tv0.to_datetime_v2();
+    auto dtv2_1 = tv1.to_datetime_v2();
+    auto dtv2_2 = tv2.to_datetime_v2();
+    auto dtv2_3 = tv3.to_datetime_v2();
+    column_timestamp->insert_data((char*)&dtv2_0, 0);
+    column_timestamp->insert_data((char*)&dtv2_1, 0);
+    column_timestamp->insert_data((char*)&dtv2_2, 0);
+    column_timestamp->insert_data((char*)&dtv2_3, 0);
+
+    // Row 0: c1=T, c2=F, c3=F
+    // Row 1: c1=F, c2=T, c3=F
+    // Row 2: c1=T, c2=T, c3=T
+    // Row 3: c1=F, c2=F, c3=T
+    auto column_event1 = ColumnUInt8::create();
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(0));
+
+    auto column_event2 = ColumnUInt8::create();
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(0));
+
+    auto column_event3 = ColumnUInt8::create();
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(1));
+
+    auto column_window = ColumnInt64::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_window->insert(Field::create_field<TYPE_BIGINT>(86400));
+    }
+
+    std::unique_ptr<char[]> memory(new char[agg_func_3->size_of_data()]);
+    AggregateDataPtr place = memory.get();
+    agg_func_3->create(place);
+    const IColumn* column[6] = {column_window.get(), column_mode.get(),   
column_timestamp.get(),
+                                column_event1.get(), column_event2.get(), 
column_event3.get()};
+    for (int i = 0; i < NUM_ROWS; i++) {
+        agg_func_3->add(place, column, i, arena);
+    }
+
+    ColumnInt32 column_result;
+    agg_func_3->insert_result_into(place, column_result);
+    // Chain reaches level 2 (3 steps matched: E0→E1→E2) before E2@R3 
duplicate terminates it.
+    EXPECT_EQ(column_result.get_data()[0], 3);
+    agg_func_3->destroy(place);
+}
+
+// Dedup mode: E0 restart after chain terminated by same-row duplicate.
+// When a row matches c1+c2, and c2 is a duplicate that terminates the chain,
+// the E0 from the same row should start a new chain because _eliminate_chain()
+// clears events_timestamp, so _is_same_row_as_chain won't trigger.
+TEST_F(VWindowFunnelV2Test, testDeduplicationE0PendingRestartApplied) {
+    AggregateFunctionSimpleFactory factory = 
AggregateFunctionSimpleFactory::instance();
+    DataTypes data_types_3 = {
+            std::make_shared<DataTypeInt64>(),      
std::make_shared<DataTypeString>(),
+            std::make_shared<DataTypeDateTimeV2>(), 
std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeUInt8>(),      
std::make_shared<DataTypeUInt8>()};
+    auto agg_func_3 = factory.get("window_funnel_v2", data_types_3, nullptr, 
false,
+                                  BeExecVersionManager::get_newest_version());
+    ASSERT_NE(agg_func_3, nullptr);
+
+    // Scenario: Row matches c1+c2 where c2 is a duplicate → chain terminated 
by duplicate.
+    // The pending E0 from same row should restart.
+    // Row 0 (t=10:00): c1=T only → chain level=0
+    // Row 1 (t=11:00): c2=T only → chain level=1
+    // Row 2 (t=12:00): c1=T, c2=T → E1(cont=0), E0(cont=1)
+    //   E1(cont=0): duplicate! R2 ≠ R0, R2 ≠ R1 → NOT same row as chain → 
terminate.
+    //   E0(cont=1): _is_same_row_as_chain → but chain just eliminated! 
events_timestamp[0] cleared.
+    //     Actually after eliminate, events_timestamp[0] has no value, so the 
_is_same_row_as_chain
+    //     check at E0 won't enter the if branch. E0 directly starts new chain.
+    // Row 3 (t=13:00): c2=T only → chain level=1
+    // Row 4 (t=14:00): c3=T only → chain level=2
+    // Result: max(1+1, 2+1) = 3
+    const int NUM_ROWS = 5;
+    auto column_mode = ColumnString::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_mode->insert(Field::create_field<TYPE_STRING>("deduplication"));
+    }
+    auto column_timestamp = ColumnDateTimeV2::create();
+    VecDateTimeValue tv0, tv1, tv2, tv3, tv4;
+    tv0.unchecked_set_time(2022, 3, 12, 10, 0, 0);
+    tv1.unchecked_set_time(2022, 3, 12, 11, 0, 0);
+    tv2.unchecked_set_time(2022, 3, 12, 12, 0, 0);
+    tv3.unchecked_set_time(2022, 3, 12, 13, 0, 0);
+    tv4.unchecked_set_time(2022, 3, 12, 14, 0, 0);
+    auto dtv2_0 = tv0.to_datetime_v2();
+    auto dtv2_1 = tv1.to_datetime_v2();
+    auto dtv2_2 = tv2.to_datetime_v2();
+    auto dtv2_3 = tv3.to_datetime_v2();
+    auto dtv2_4 = tv4.to_datetime_v2();
+    column_timestamp->insert_data((char*)&dtv2_0, 0);
+    column_timestamp->insert_data((char*)&dtv2_1, 0);
+    column_timestamp->insert_data((char*)&dtv2_2, 0);
+    column_timestamp->insert_data((char*)&dtv2_3, 0);
+    column_timestamp->insert_data((char*)&dtv2_4, 0);
+
+    // Row 0: c1=T, c2=F, c3=F
+    // Row 1: c1=F, c2=T, c3=F
+    // Row 2: c1=T, c2=T, c3=F
+    // Row 3: c1=F, c2=T, c3=F
+    // Row 4: c1=F, c2=F, c3=T
+    auto column_event1 = ColumnUInt8::create();
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(0));
+
+    auto column_event2 = ColumnUInt8::create();
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(0));
+
+    auto column_event3 = ColumnUInt8::create();
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(1));
+
+    auto column_window = ColumnInt64::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_window->insert(Field::create_field<TYPE_BIGINT>(86400));
+    }
+
+    std::unique_ptr<char[]> memory(new char[agg_func_3->size_of_data()]);
+    AggregateDataPtr place = memory.get();
+    agg_func_3->create(place);
+    const IColumn* column[6] = {column_window.get(), column_mode.get(),   
column_timestamp.get(),
+                                column_event1.get(), column_event2.get(), 
column_event3.get()};
+    for (int i = 0; i < NUM_ROWS; i++) {
+        agg_func_3->add(place, column, i, arena);
+    }
+
+    ColumnInt32 column_result;
+    agg_func_3->insert_result_into(place, column_result);
+    // First chain: E0@R0→E1@R1 (level=2). E1@R2 duplicate terminates.
+    // E0@R2 restarts. New chain: E0@R2→E1@R3→E2@R4 (level=3).
+    // max(2, 3) = 3.
+    EXPECT_EQ(column_result.get_data()[0], 3);
+    agg_func_3->destroy(place);
+}
+
+// Fixed mode: same-row multi-condition level jump fix.
+// When a row matches both a high-index condition (c3) and a low-index 
condition (c2),
+// the high-index event was processed first and triggered a spurious level 
jump before
+// the low-index event could advance the chain.
+TEST_F(VWindowFunnelV2Test, testFixedSameRowMultiCondLevelJump) {
+    // Use 3 conditions: c1=event1, c2=event2, c3=event3
+    AggregateFunctionSimpleFactory factory = 
AggregateFunctionSimpleFactory::instance();
+    DataTypes data_types_3 = {
+            std::make_shared<DataTypeInt64>(),      
std::make_shared<DataTypeString>(),
+            std::make_shared<DataTypeDateTimeV2>(), 
std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeUInt8>(),      
std::make_shared<DataTypeUInt8>()};
+    auto agg_func_3 = factory.get("window_funnel_v2", data_types_3, nullptr, 
false,
+                                  BeExecVersionManager::get_newest_version());
+    ASSERT_NE(agg_func_3, nullptr);
+
+    const int NUM_ROWS = 2;
+    auto column_mode = ColumnString::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_mode->insert(Field::create_field<TYPE_STRING>("fixed"));
+    }
+    auto column_timestamp = ColumnDateTimeV2::create();
+    VecDateTimeValue tv0, tv1;
+    tv0.unchecked_set_time(2020, 1, 1, 0, 0, 0);
+    tv1.unchecked_set_time(2020, 1, 2, 0, 0, 0);
+    auto dtv2_0 = tv0.to_datetime_v2();
+    auto dtv2_1 = tv1.to_datetime_v2();
+    column_timestamp->insert_data((char*)&dtv2_0, 0);
+    column_timestamp->insert_data((char*)&dtv2_1, 0);
+
+    // Row 0: c1=T, c2=T, c3=T (all match — starts chain via c1)
+    // Row 1: c1=F, c2=T, c3=T (c3 would trigger level jump before c2 can 
advance)
+    auto column_event1 = ColumnUInt8::create();
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event1->insert(Field::create_field<TYPE_BOOLEAN>(0));
+
+    auto column_event2 = ColumnUInt8::create();
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+
+    auto column_event3 = ColumnUInt8::create();
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_event3->insert(Field::create_field<TYPE_BOOLEAN>(1));
+
+    auto column_window = ColumnInt64::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_window->insert(Field::create_field<TYPE_BIGINT>(86400));
+    }
+
+    std::unique_ptr<char[]> memory(new char[agg_func_3->size_of_data()]);
+    AggregateDataPtr place = memory.get();
+    agg_func_3->create(place);
+    const IColumn* column[6] = {column_window.get(), column_mode.get(),   
column_timestamp.get(),
+                                column_event1.get(), column_event2.get(), 
column_event3.get()};
+    for (int i = 0; i < NUM_ROWS; i++) {
+        agg_func_3->add(place, column, i, arena);
+    }
+
+    ColumnInt32 column_result;
+    agg_func_3->insert_result_into(place, column_result);
+    // Before fix: c3 from row 1 triggered level jump (c2 predecessor not 
matched) → result 1.
+    // After fix: same-row look-ahead finds c2 can advance → skip level jump → 
result 2.
+    EXPECT_EQ(column_result.get_data()[0], 2);
+    agg_func_3->destroy(place);
+}
+
+// Fixed mode: same-row duplicate should NOT suppress a level-jump break.
+// Counterexample from review: E0@r0, E1@r1, (E3,E1)@r2, E2@r3, E3@r4.
+// E1@r2 is a duplicate of an already-matched level (curr_level=2, sr_idx=1 ≤ 
curr_level),
+// so it must not be treated as an advancement. The chain should break at 
E3@r2.
+TEST_F(VWindowFunnelV2Test, testFixedSameRowDuplicateDoesNotSuppressJump) {
+    AggregateFunctionSimpleFactory factory = 
AggregateFunctionSimpleFactory::instance();
+    // 4 conditions: window, mode, timestamp, c1, c2, c3, c4
+    DataTypes data_types_4 = {
+            std::make_shared<DataTypeInt64>(),      
std::make_shared<DataTypeString>(),
+            std::make_shared<DataTypeDateTimeV2>(), 
std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeUInt8>(),      
std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeUInt8>()};
+    auto agg_func = factory.get("window_funnel_v2", data_types_4, nullptr, 
false,
+                                BeExecVersionManager::get_newest_version());
+    ASSERT_NE(agg_func, nullptr);
+
+    const int NUM_ROWS = 5;
+    auto column_mode = ColumnString::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_mode->insert(Field::create_field<TYPE_STRING>("fixed"));
+    }
+
+    // Timestamps: r0=t0, r1=t1, r2=t2, r3=t3, r4=t4 (all within window)
+    auto column_timestamp = ColumnDateTimeV2::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        VecDateTimeValue tv;
+        tv.unchecked_set_time(2020, 1, 1 + i, 0, 0, 0);
+        auto dtv2 = tv.to_datetime_v2();
+        column_timestamp->insert_data((char*)&dtv2, 0);
+    }
+
+    // r0: c1=T, c2=F, c3=F, c4=F → E0
+    // r1: c1=F, c2=T, c3=F, c4=F → E1
+    // r2: c1=F, c2=T, c3=F, c4=T → E1(dup)+E3(jump)
+    // r3: c1=F, c2=F, c3=T, c4=F → E2
+    // r4: c1=F, c2=F, c3=F, c4=T → E3
+    auto column_c1 = ColumnUInt8::create();
+    for (int v : {1, 0, 0, 0, 0}) 
column_c1->insert(Field::create_field<TYPE_BOOLEAN>(v));
+
+    auto column_c2 = ColumnUInt8::create();
+    for (int v : {0, 1, 1, 0, 0}) 
column_c2->insert(Field::create_field<TYPE_BOOLEAN>(v));
+
+    auto column_c3 = ColumnUInt8::create();
+    for (int v : {0, 0, 0, 1, 0}) 
column_c3->insert(Field::create_field<TYPE_BOOLEAN>(v));
+
+    auto column_c4 = ColumnUInt8::create();
+    for (int v : {0, 0, 1, 0, 1}) 
column_c4->insert(Field::create_field<TYPE_BOOLEAN>(v));
+
+    auto column_window = ColumnInt64::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_window->insert(Field::create_field<TYPE_BIGINT>(86400 * 10));
+    }
+
+    std::unique_ptr<char[]> memory(new char[agg_func->size_of_data()]);
+    AggregateDataPtr place = memory.get();
+    agg_func->create(place);
+    const IColumn* column[7] = {column_window.get(), column_mode.get(), 
column_timestamp.get(),
+                                column_c1.get(),     column_c2.get(),   
column_c3.get(),
+                                column_c4.get()};
+    for (int i = 0; i < NUM_ROWS; i++) {
+        agg_func->add(place, column, i, arena);
+    }
+
+    ColumnInt32 column_result;
+    agg_func->insert_result_into(place, column_result);
+    // Chain: E0@r0 → E1@r1 → E3@r2 triggers level jump. E1@r2 is a duplicate
+    // (sr_idx=1 ≤ curr_level=2), so it must NOT suppress the break. Result = 
2.
+    EXPECT_EQ(column_result.get_data()[0], 2);
+    agg_func->destroy(place);
+}
+
+// Fixed mode: when a row matches multiple conditions including E0 (c1),
+// the E0 continuation should NOT restart the chain if a same-row event
+// already advanced it. This emulates V1's row-based semantics where each
+// row is tested against exactly one expected condition.
+TEST_F(VWindowFunnelV2Test, testFixedContinuationE0DoesNotRestartChain) {
+    AggregateFunctionSimpleFactory factory = 
AggregateFunctionSimpleFactory::instance();
+    DataTypes data_types_3 = {
+            std::make_shared<DataTypeInt64>(),      
std::make_shared<DataTypeString>(),
+            std::make_shared<DataTypeDateTimeV2>(), 
std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeUInt8>(),      
std::make_shared<DataTypeUInt8>()};
+    auto agg_func_3 = factory.get("window_funnel_v2", data_types_3, nullptr, 
false,
+                                  BeExecVersionManager::get_newest_version());
+    ASSERT_NE(agg_func_3, nullptr);
+
+    // 3 rows, 3 conditions (c1, c2, c3):
+    //   r0: c1=T, c2=F, c3=F   -> E0 starts chain
+    //   r1: c1=T, c2=T, c3=F   -> c2 advances chain, c1(cont) should be 
skipped
+    //   r2: c1=F, c2=F, c3=T   -> c3 advances chain to level 2
+    // Expected result: 3 (c1@r0 -> c2@r1 -> c3@r2)
+    // Without E0-skip fix, c1@r1(cont) would restart chain, and result would 
be 2.
+    const int NUM_ROWS = 3;
+    auto column_mode = ColumnString::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_mode->insert(Field::create_field<TYPE_STRING>("fixed"));
+    }
+    auto column_timestamp = ColumnDateTimeV2::create();
+    VecDateTimeValue tv;
+    for (int i = 0; i < NUM_ROWS; i++) {
+        tv.unchecked_set_time(2020, 1, i + 1, 0, 0, 0);
+        auto dtv2 = tv.to_datetime_v2();
+        column_timestamp->insert_data((char*)&dtv2, 0);
+    }
+
+    auto column_window = ColumnInt64::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_window->insert(Field::create_field<TYPE_BIGINT>(86400 * 10));
+    }
+
+    // c1: r0=T, r1=T, r2=F
+    auto column_c1 = ColumnUInt8::create();
+    column_c1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_c1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_c1->insert(Field::create_field<TYPE_BOOLEAN>(0));
+
+    // c2: r0=F, r1=T, r2=F
+    auto column_c2 = ColumnUInt8::create();
+    column_c2->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_c2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_c2->insert(Field::create_field<TYPE_BOOLEAN>(0));
+
+    // c3: r0=F, r1=F, r2=T
+    auto column_c3 = ColumnUInt8::create();
+    column_c3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_c3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_c3->insert(Field::create_field<TYPE_BOOLEAN>(1));
+
+    std::unique_ptr<char[]> memory(new char[agg_func_3->size_of_data()]);
+    AggregateDataPtr place = memory.get();
+    agg_func_3->create(place);
+    const IColumn* columns[6] = {column_window.get(), column_mode.get(), 
column_timestamp.get(),
+                                 column_c1.get(),     column_c2.get(),   
column_c3.get()};
+    for (int i = 0; i < NUM_ROWS; i++) {
+        agg_func_3->add(place, columns, i, arena);
+    }
+
+    ColumnInt32 column_result;
+    agg_func_3->insert_result_into(place, column_result);
+    EXPECT_EQ(column_result.get_data()[0], 3);
+    agg_func_3->destroy(place);
+}
+
+// Dedup mode multi-pass: V1 reaches level 4 by starting from a later E0 and 
skipping
+// over rows that match higher-level conditions. The old single-pass V2 broke 
at level 3
+// because it greedily promoted from multi-condition rows, creating premature 
duplicates.
+// 7 conditions, 15 valid rows (after null filtering). Window ~35 years 
(effectively unlimited).
+// V1 chain: c1@R5(pk=8) → c2@R9(pk=6) → c3@R10(pk=12) → c4@R12(pk=5), then c1 
dup at R13 → level=4.
+TEST_F(VWindowFunnelV2Test, testDeduplicationMultiPassGapCheck) {
+    AggregateFunctionSimpleFactory factory = 
AggregateFunctionSimpleFactory::instance();
+    // 7 conditions
+    DataTypes data_types_7 = {
+            std::make_shared<DataTypeInt64>(),      
std::make_shared<DataTypeString>(),
+            std::make_shared<DataTypeDateTimeV2>(), 
std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeUInt8>(),      
std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeUInt8>(),      
std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeUInt8>(),      
std::make_shared<DataTypeUInt8>()};
+    auto agg_func = factory.get("window_funnel_v2", data_types_7, nullptr, 
false,
+                                BeExecVersionManager::get_newest_version());
+    ASSERT_NE(agg_func, nullptr);
+
+    // 15 rows sorted by timestamp (null rows excluded at SQL level).
+    // Conditions: c1=val<4, c2=val<=2, c3=val!=6, c4=val<=4, 
c5=date='2023-12-13',
+    //             c6=val<=5, c7=val<=8
+    struct RowData {
+        int year, month, day;
+        int val;
+        bool date_is_2023_12_13;
+    };
+    // clang-format off
+    RowData rows[] = {
+        {2000, 3, 19, 3, false},  // pk=10
+        {2000, 4,  3, 8, false},  // pk=9
+        {2001,10, 12, 3, false},  // pk=2
+        {2002,11, 15, 8, false},  // pk=0
+        {2003, 9, 10, 0, false},  // pk=19
+        {2006, 6,  6, 0, false},  // pk=8  ← V1 best E0 start
+        {2008, 7,  3, 8, false},  // pk=3
+        {2009, 3,  3, 9, false},  // pk=11
+        {2009, 5, 16, 8, true},   // pk=16
+        {2009,11,  3, 0, true},   // pk=6  ← all 7 conditions match
+        {2011, 6, 25, 7, true},   // pk=12
+        {2014, 5, 16, 6, false},  // pk=14
+        {2014,11,  2, 0, false},  // pk=5
+        {2016, 1,  8, 3, false},  // pk=13
+        {2017, 1,  3, 8, true},   // pk=17
+    };
+    // clang-format on
+    const int NUM_ROWS = 15;
+
+    auto column_mode = ColumnString::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_mode->insert(Field::create_field<TYPE_STRING>("deduplication"));
+    }
+
+    auto column_timestamp = ColumnDateTimeV2::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        VecDateTimeValue tv;
+        tv.unchecked_set_time(rows[i].year, rows[i].month, rows[i].day, 0, 0, 
0);
+        auto dtv2 = tv.to_datetime_v2();
+        column_timestamp->insert_data((char*)&dtv2, 0);
+    }
+
+    auto column_window = ColumnInt64::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_window->insert(Field::create_field<TYPE_BIGINT>(1119906808));
+    }
+
+    // c1: val < 4
+    auto col_c1 = ColumnUInt8::create();
+    for (int i = 0; i < NUM_ROWS; i++)
+        col_c1->insert(Field::create_field<TYPE_BOOLEAN>(rows[i].val < 4 ? 1 : 
0));
+    // c2: val <= 2
+    auto col_c2 = ColumnUInt8::create();
+    for (int i = 0; i < NUM_ROWS; i++)
+        col_c2->insert(Field::create_field<TYPE_BOOLEAN>(rows[i].val <= 2 ? 1 
: 0));
+    // c3: val != 6
+    auto col_c3 = ColumnUInt8::create();
+    for (int i = 0; i < NUM_ROWS; i++)
+        col_c3->insert(Field::create_field<TYPE_BOOLEAN>(rows[i].val != 6 ? 1 
: 0));
+    // c4: val <= 4
+    auto col_c4 = ColumnUInt8::create();
+    for (int i = 0; i < NUM_ROWS; i++)
+        col_c4->insert(Field::create_field<TYPE_BOOLEAN>(rows[i].val <= 4 ? 1 
: 0));
+    // c5: date = '2023-12-13'
+    auto col_c5 = ColumnUInt8::create();
+    for (int i = 0; i < NUM_ROWS; i++)
+        
col_c5->insert(Field::create_field<TYPE_BOOLEAN>(rows[i].date_is_2023_12_13 ? 1 
: 0));
+    // c6: val <= 5
+    auto col_c6 = ColumnUInt8::create();
+    for (int i = 0; i < NUM_ROWS; i++)
+        col_c6->insert(Field::create_field<TYPE_BOOLEAN>(rows[i].val <= 5 ? 1 
: 0));
+    // c7: val <= 8
+    auto col_c7 = ColumnUInt8::create();
+    for (int i = 0; i < NUM_ROWS; i++)
+        col_c7->insert(Field::create_field<TYPE_BOOLEAN>(rows[i].val <= 8 ? 1 
: 0));
+
+    std::unique_ptr<char[]> memory(new char[agg_func->size_of_data()]);
+    AggregateDataPtr place = memory.get();
+    agg_func->create(place);
+    const IColumn* column[10] = {column_window.get(), column_mode.get(), 
column_timestamp.get(),
+                                 col_c1.get(),        col_c2.get(),      
col_c3.get(),
+                                 col_c4.get(),        col_c5.get(),      
col_c6.get(),
+                                 col_c7.get()};
+    for (int i = 0; i < NUM_ROWS; i++) {
+        agg_func->add(place, column, i, arena);
+    }
+
+    ColumnInt32 column_result;
+    agg_func->insert_result_into(place, column_result);
+    // V1 = 4: c1@R5(pk=8) → c2@R9(pk=6) → c3@R10(pk=12) → c4@R12(pk=5)
+    // c1 dup at R13(pk=13) breaks the chain before c5 can be matched.
+    EXPECT_EQ(column_result.get_data()[0], 4);
+    agg_func->destroy(place);
+}
+
+// FIXED mode "relevant-but-wrong row" break: when the next row after a matched
+// condition matches some condition but NOT the expected next one, the chain 
must
+// break. V1 checks consecutive rows, so a c2-only row after c1→c2 breaks when
+// expecting c3. Before the fix, V2 silently re-promoted c2 and kept searching.
+TEST_F(VWindowFunnelV2Test, testFixedRelevantButWrongRowBreaksChain) {
+    AggregateFunctionSimpleFactory factory = 
AggregateFunctionSimpleFactory::instance();
+    DataTypes data_types_3 = {
+            std::make_shared<DataTypeInt64>(),      
std::make_shared<DataTypeString>(),
+            std::make_shared<DataTypeDateTimeV2>(), 
std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeUInt8>(),      
std::make_shared<DataTypeUInt8>()};
+    auto agg_func_3 = factory.get("window_funnel_v2", data_types_3, nullptr, 
false,
+                                  BeExecVersionManager::get_newest_version());
+    ASSERT_NE(agg_func_3, nullptr);
+
+    // 4 rows, 3 conditions (c1, c2, c3):
+    //   r0: c1=T, c2=F, c3=F  → E0, chain level 0
+    //   r1: c1=F, c2=T, c3=F  → c2 advances chain to level 1
+    //   r2: c1=F, c2=T, c3=F  → c2 re-matches level 1 (expected c3). Chain 
BREAKS.
+    //   r3: c1=F, c2=F, c3=T  → c3 could advance but chain is already broken
+    // Expected: 2 (c1@r0 → c2@r1, break at r2)
+    const int NUM_ROWS = 4;
+    auto column_mode = ColumnString::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_mode->insert(Field::create_field<TYPE_STRING>("fixed"));
+    }
+    auto column_timestamp = ColumnDateTimeV2::create();
+    VecDateTimeValue tv;
+    for (int i = 0; i < NUM_ROWS; i++) {
+        tv.unchecked_set_time(2020, 1, i + 1, 0, 0, 0);
+        auto dtv2 = tv.to_datetime_v2();
+        column_timestamp->insert_data((char*)&dtv2, 0);
+    }
+    auto column_window = ColumnInt64::create();
+    for (int i = 0; i < NUM_ROWS; i++) {
+        column_window->insert(Field::create_field<TYPE_BIGINT>(86400 * 10));
+    }
+
+    // c1: r0=T, r1=F, r2=F, r3=F
+    auto column_c1 = ColumnUInt8::create();
+    column_c1->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_c1->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_c1->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_c1->insert(Field::create_field<TYPE_BOOLEAN>(0));
+
+    // c2: r0=F, r1=T, r2=T, r3=F
+    auto column_c2 = ColumnUInt8::create();
+    column_c2->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_c2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_c2->insert(Field::create_field<TYPE_BOOLEAN>(1));
+    column_c2->insert(Field::create_field<TYPE_BOOLEAN>(0));
+
+    // c3: r0=F, r1=F, r2=F, r3=T
+    auto column_c3 = ColumnUInt8::create();
+    column_c3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_c3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_c3->insert(Field::create_field<TYPE_BOOLEAN>(0));
+    column_c3->insert(Field::create_field<TYPE_BOOLEAN>(1));
+
+    std::unique_ptr<char[]> memory(new char[agg_func_3->size_of_data()]);
+    AggregateDataPtr place = memory.get();
+    agg_func_3->create(place);
+    const IColumn* columns[6] = {column_window.get(), column_mode.get(), 
column_timestamp.get(),
+                                 column_c1.get(),     column_c2.get(),   
column_c3.get()};
+    for (int i = 0; i < NUM_ROWS; i++) {
+        agg_func_3->add(place, columns, i, arena);
+    }
+
+    ColumnInt32 column_result;
+    agg_func_3->insert_result_into(place, column_result);
+    EXPECT_EQ(column_result.get_data()[0], 2);
+    agg_func_3->destroy(place);
+}
+
 } // namespace doris
diff --git a/regression-test/data/query_p0/aggregate/window_funnel_v2.out 
b/regression-test/data/query_p0/aggregate/window_funnel_v2.out
index 5d91239ad73..83ff120a218 100644
--- a/regression-test/data/query_p0/aggregate/window_funnel_v2.out
+++ b/regression-test/data/query_p0/aggregate/window_funnel_v2.out
@@ -86,3 +86,59 @@
 -- !v2_increase_old_chain_better --
 2
 
+-- !v2_dedup_same_row_multi_event --
+3
+
+-- !v2_dedup_true_dup_breaks --
+2
+
+-- !v2_fixed_same_row_level_jump --
+2
+
+-- !v2_fixed_multi_cond_complex --
+2
+
+-- !v2_fixed_e0_skip --
+3
+
+-- !v2_fixed_edge_single_row_all_match --
+1
+
+-- !v2_fixed_edge_next_row_all --
+2
+
+-- !v2_fixed_edge_e1e2_no_e0 --
+2
+
+-- !v2_fixed_edge_4cond_progressive --
+3
+
+-- !v2_fixed_edge_both_rows_multi --
+2
+
+-- !v2_fixed_edge_full_chain --
+4
+
+-- !v2_fixed_edge_level_jump_break --
+1
+
+-- !v2_fixed_edge_e0_restart_diff_row --
+2
+
+-- !v2_fixed_edge_window_boundary --
+1
+
+-- !v2_fixed_edge_multi_restart_best --
+3
+
+-- !v2_fixed_edge_many_e0_restarts --
+3
+
+-- !v2_fixed_edge_irrelevant_skip --
+3
+
+-- !v2_fixed_edge_relevant_but_wrong --
+2
+
+-- !v2_fixed_edge_relevant_wrong_no_restart --
+2
diff --git a/regression-test/suites/query_p0/aggregate/window_funnel_v2.groovy 
b/regression-test/suites/query_p0/aggregate/window_funnel_v2.groovy
index e8157c23543..10aa142c842 100644
--- a/regression-test/suites/query_p0/aggregate/window_funnel_v2.groovy
+++ b/regression-test/suites/query_p0/aggregate/window_funnel_v2.groovy
@@ -486,5 +486,417 @@ suite("window_funnel_v2") {
         from windowfunnel_v2_test t;
     """
 
+    // ==================== DEDUPLICATION mode: same-row multi-event bug fix 
====================
+    // Regression test for the bug where deduplication mode incorrectly breaks 
the chain
+    // when a single row matches multiple conditions including E0.
+    // V1 returns 3, V2 was returning 2 before the fix.
     sql """ DROP TABLE IF EXISTS windowfunnel_v2_test """
+    sql """
+        CREATE TABLE IF NOT EXISTS windowfunnel_v2_test (
+            xwho varchar(50) NULL COMMENT 'xwho',
+            xwhen datetimev2(3) COMMENT 'xwhen',
+            xwhat int NULL COMMENT 'xwhat'
+        )
+        DUPLICATE KEY(xwho)
+        DISTRIBUTED BY HASH(xwho) BUCKETS 3
+        PROPERTIES (
+        "replication_num" = "1"
+        );
+    """
+    // Case 1: Rows matching both E0+E1 or E0+E2 simultaneously.
+    // Row 0 (t=10:00): xwhat=1 → matches E0 (xwhat=1) and E1 (xwhat!=2)
+    // Row 1 (t=11:00): xwhat=1 → matches E0 (xwhat=1) and E1 (xwhat!=2)
+    // Row 2 (t=12:00): xwhat=3 → matches E0 (xwhat=1 false, but see below), 
E1 (xwhat!=2) and E2 (xwhat=3)
+    // Using conditions: xwhat=1, xwhat!=2, xwhat=3
+    // Row 0: E0=T(1=1), E1=T(1!=2), E2=F(1!=3) → same-row E0+E1
+    // Row 1: E0=T(1=1), E1=T(1!=2), E2=F(1!=3) → same-row E0+E1
+    // Row 2: E0=F(3!=1), E1=T(3!=2), E2=T(3=3) → same-row E1+E2
+    // Expected chain: E0@row0 → E1@row1 → E2@row2 = 3
+    sql "INSERT into windowfunnel_v2_test (xwho, xwhen, xwhat) VALUES('1', 
'2022-03-12 10:00:00.000', 1)"
+    sql "INSERT INTO windowfunnel_v2_test (xwho, xwhen, xwhat) VALUES('1', 
'2022-03-12 11:00:00.000', 1)"
+    sql "INSERT INTO windowfunnel_v2_test (xwho, xwhen, xwhat) VALUES('1', 
'2022-03-12 12:00:00.000', 3)"
+    order_qt_v2_dedup_same_row_multi_event """
+        select
+            window_funnel(
+                86400,
+                'deduplication',
+                t.xwhen,
+                t.xwhat = 1,
+                t.xwhat != 2,
+                t.xwhat = 3
+            ) AS level
+        from windowfunnel_v2_test t;
+    """
+
+    // Case 2: True duplicate from a different row should still break the 
chain.
+    sql """ truncate table windowfunnel_v2_test; """
+    sql "INSERT into windowfunnel_v2_test (xwho, xwhen, xwhat) VALUES('1', 
'2022-03-12 10:00:00.000', 1)"
+    sql "INSERT INTO windowfunnel_v2_test (xwho, xwhen, xwhat) VALUES('1', 
'2022-03-12 11:00:00.000', 2)"
+    sql "INSERT INTO windowfunnel_v2_test (xwho, xwhen, xwhat) VALUES('1', 
'2022-03-12 12:00:00.000', 1)"
+    sql "INSERT INTO windowfunnel_v2_test (xwho, xwhen, xwhat) VALUES('1', 
'2022-03-12 13:00:00.000', 3)"
+    order_qt_v2_dedup_true_dup_breaks """
+        select
+            window_funnel(
+                86400,
+                'deduplication',
+                t.xwhen,
+                t.xwhat = 1,
+                t.xwhat = 2,
+                t.xwhat = 3
+            ) AS level
+        from windowfunnel_v2_test t;
+    """
+
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_test """
+
+    // ==================== FIXED mode: same-row multi-condition level jump 
fix ====================
+    // When a row matches both a high-index condition (e.g., c3) and a 
low-index condition (e.g., c2),
+    // the high-index event was processed first (descending order) and 
triggered a spurious level jump
+    // before the low-index event could advance the chain. This test verifies 
the fix.
+
+    // Minimal reproduction: 3 conditions, 2 rows
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_multi_cond """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_multi_cond (
+            ts datetime,
+            val int
+        ) DUPLICATE KEY(ts)
+        DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_multi_cond VALUES
+            ('2020-01-01 00:00:00', 3),
+            ('2020-01-02 00:00:00', 1);
+    """
+    // Row 1: val=3 -> c1(>2)=T, c2(<5)=T, c3(>0)=T  (starts chain)
+    // Row 2: val=1 -> c1=F, c2(<5)=T, c3(>0)=T
+    // Bug: E2(c3) from row 2 triggered level jump before E1(c2) could advance.
+    // Fix: same-row look-ahead detects E1 can advance, skips level jump. 
Result=2.
+    order_qt_v2_fixed_same_row_level_jump """
+        SELECT window_funnel(86400, 'fixed', ts, val > 2, val < 5, val > 0) AS 
level
+        FROM windowfunnel_v2_fixed_multi_cond;
+    """
+
+    // 4 conditions with overlapping column-based predicates
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_multi_cond """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_multi_cond (
+            col_date date NULL,
+            col_int int NULL,
+            col_datetime datetime NULL,
+            pk int
+        ) DUPLICATE KEY(col_date, col_int)
+        DISTRIBUTED BY HASH(pk) BUCKETS 10
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_multi_cond (pk, col_int, col_date, 
col_datetime) VALUES
+            (0,2,'2011-03-24','2009-09-17 
17:25:39'),(1,null,'2023-12-10','2001-09-19 20:24:22'),
+            (2,1,null,'2003-04-04 02:59:52'),(3,8,'2023-12-16','2004-02-13 
03:59:09'),
+            (4,5,'2023-12-18','2015-09-09 
23:02:51'),(5,1,'2023-12-14','2001-10-21 07:04:03'),
+            (6,3,'2023-12-10','2009-04-07 
10:55:27'),(7,null,'2023-12-14','2018-11-02 09:18:56'),
+            (8,1,'2023-12-11','2015-05-03 
17:34:12'),(9,1,'2023-12-12','2010-12-25 06:21:52'),
+            (10,3,null,'2008-06-02 22:49:15'),(11,0,'2023-12-12','2010-11-13 
22:32:47'),
+            (12,1,'2023-12-16','2019-04-28 
20:36:03'),(13,null,null,'2019-01-01 21:24:32'),
+            (14,3,'2023-12-15','2006-08-17 21:47:52'),(15,3,null,'2015-12-23 
05:58:29'),
+            (16,5,'2013-09-15','2018-08-20 
22:15:56'),(17,5,'2023-12-11','2017-01-17 11:34:56'),
+            (18,7,'2023-12-18','2016-01-16 
13:00:40'),(19,null,'2023-12-13','2003-03-16 01:28:28'),
+            (20,0,'2023-12-16','2006-08-28 
22:48:19'),(21,8,'2013-10-28','2007-01-19 07:52:53'),
+            (22,3,'2023-12-17','2001-06-05 
18:32:05'),(23,2,'2023-12-17','2001-05-15 22:39:45'),
+            (24,null,'2023-12-18','2015-02-06 
18:44:55'),(25,null,'2023-12-11','2004-02-22 19:46:22'),
+            (26,0,'2023-12-17','2001-05-20 
05:25:26'),(27,7,'2023-12-15','2015-05-07 15:56:20'),
+            (28,0,'2023-12-16','2016-02-10 
04:26:23'),(29,0,'2023-12-18','2019-09-04 05:15:31'),
+            (30,1,'2023-12-11','2000-12-08 
10:19:58'),(31,9,'2023-12-14','2004-07-11 03:51:22'),
+            (32,5,'2023-12-16','2013-11-18 16:32:18'),(33,6,null,'2000-11-13 
05:31:42'),
+            (34,7,'2023-12-18','2013-01-02 
22:46:46'),(35,4,'2023-12-12','2008-10-23 09:22:02'),
+            (36,2,'2023-12-16','2006-01-18 
13:41:57'),(37,5,'2023-12-14','2008-05-27 21:09:03'),
+            (38,2,'2023-12-15','2007-01-15 
01:30:56'),(39,1,'2023-12-18','2003-04-10 16:23:13'),
+            (40,null,'2023-12-18','2018-03-25 
20:34:39'),(41,4,'2023-12-15','2007-10-06 00:49:06'),
+            (42,7,'2023-12-11','2015-11-11 
23:53:16'),(43,null,'2023-12-10','2002-05-26 11:17:20'),
+            (44,6,'2023-12-14','2003-04-11 
05:05:55'),(45,null,'2014-08-07','2000-10-02 00:55:06'),
+            (46,2,'2023-12-16','2018-08-26 
22:48:02'),(47,2,'2023-12-11','2004-12-15 08:38:08'),
+            (48,5,'2019-01-19','2000-11-09 
19:21:35'),(49,6,'2023-12-12','2010-11-21 05:54:00');
+    """
+    // c1: col_int>2, c2: col_date<'2023-12-12', c3: col_int=5, c4: 
col_date>'2009-10-14'
+    // Many rows match both c2 and c4 (e.g., col_date='2023-12-11'). In the 
buggy code,
+    // c4's event triggered a level jump before c2 could advance. Expected: 2.
+    order_qt_v2_fixed_multi_cond_complex """
+        SELECT WINDOW_FUNNEL(1736123071, 'fixed', col_datetime,
+            col_int > 2, col_date < '2023-12-12', col_int = 5, col_date > 
'2009-10-14')
+        FROM windowfunnel_v2_fixed_multi_cond;
+    """
+
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_multi_cond """
+
+    // ==================== FIXED mode: continuation E0 should not restart 
chain ====================
+    // When a row matches both the expected next condition (e.g., c2) and c1, 
the c2 event
+    // advances the chain while c1 (stored as continuation) should NOT restart 
a new chain.
+    // This matches V1's row-based semantics.
+
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_e0_skip """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_e0_skip (
+            ts datetime,
+            val int
+        ) DUPLICATE KEY(ts)
+        DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_e0_skip VALUES
+            ('2020-01-01 00:00:00', 1),
+            ('2020-01-02 00:00:00', 3),
+            ('2020-01-03 00:00:00', -1);
+    """
+    // r0: val=1  -> c1(val>0)=T, c2(val>2)=F, c3(val<0)=F. Starts chain.
+    // r1: val=3  -> c1=T, c2=T, c3=F. c2 advances chain; c1(cont) should NOT 
restart.
+    // r2: val=-1 -> c1=F, c2=F, c3=T. c3 advances to level 2.
+    // Expected: 3 (c1@r0 -> c2@r1 -> c3@r2)
+    order_qt_v2_fixed_e0_skip """
+        SELECT window_funnel(864000, 'fixed', ts, val > 0, val > 2, val < 0) 
AS level
+        FROM windowfunnel_v2_fixed_e0_skip;
+    """
+
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_e0_skip """
+
+    // ==================== FIXED mode: comprehensive same-row multi-condition 
edge cases ====================
+
+    // Test: single row matching all conditions → only E0 counts (no next row 
for c2)
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """ INSERT INTO windowfunnel_v2_fixed_edge VALUES ('2020-01-01 
00:00:00', 3) """
+    order_qt_v2_fixed_edge_single_row_all_match """
+        SELECT window_funnel(864000, 'fixed', ts, val>=0, val>=1, val>=2) FROM 
windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: R0:E0, R1:E0+E1+E2 (next row matches all conditions)
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 0), ('2020-01-02 00:00:00', 3);
+    """
+    order_qt_v2_fixed_edge_next_row_all """
+        SELECT window_funnel(864000, 'fixed', ts, val>=0, val>=1, val>=2) FROM 
windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: R0:E0, R1:E1+E2 (not E0), R2:E2 only
+    // c3 at R2 doesn't match → chain stops at level 1
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 1), ('2020-01-02 00:00:00', 3), 
('2020-01-03 00:00:00', 2);
+    """
+    order_qt_v2_fixed_edge_e1e2_no_e0 """
+        SELECT window_funnel(864000, 'fixed', ts, val=1, val>1, val>2) FROM 
windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: 4 conditions progressive (R0:c1, R1:c1+c2+c3, R2:all)
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 0), ('2020-01-02 00:00:00', 2), 
('2020-01-03 00:00:00', 3);
+    """
+    order_qt_v2_fixed_edge_4cond_progressive """
+        SELECT window_funnel(864000, 'fixed', ts, val>=0, val>=1, val>=2, 
val>=3) FROM windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: R0:E0+E1, R1:E0+E1+E2 (both rows match c1+c2)
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 1), ('2020-01-02 00:00:00', 2);
+    """
+    order_qt_v2_fixed_edge_both_rows_multi """
+        SELECT window_funnel(864000, 'fixed', ts, val>=1, val>=1, val>=2) FROM 
windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: 4 rows, 4 conditions, each row adds one more matching condition
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 0), ('2020-01-02 00:00:00', 1),
+            ('2020-01-03 00:00:00', 2), ('2020-01-04 00:00:00', 3);
+    """
+    order_qt_v2_fixed_edge_full_chain """
+        SELECT window_funnel(864000, 'fixed', ts, val>=0, val>=1, val>=2, 
val>=3) FROM windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: R0:E0, R1:only E2 (c2 not matched) → level jump breaks chain
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 1), ('2020-01-02 00:00:00', 5);
+    """
+    order_qt_v2_fixed_edge_level_jump_break """
+        SELECT window_funnel(864000, 'fixed', ts, val=1, val=2, val>2) FROM 
windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: E0 restart from different row should work normally
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 1), ('2020-01-02 00:00:00', 5),
+            ('2020-01-03 00:00:00', 1), ('2020-01-04 00:00:00', 2);
+    """
+    order_qt_v2_fixed_edge_e0_restart_diff_row """
+        SELECT window_funnel(864000, 'fixed', ts, val>0, val=2) FROM 
windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: Window boundary with multi-condition same-row (exceeds 1s window)
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 0), ('2020-01-01 00:00:02', 2);
+    """
+    order_qt_v2_fixed_edge_window_boundary """
+        SELECT window_funnel(1, 'fixed', ts, val>=0, val>=1, val>=2) FROM 
windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: Multiple restarts find best chain
+    // c1@R0→c2@R1 then R2 has c2 again (breaks). c1@R1→c2@R2→c3@R3 succeeds → 
level 3.
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 1), ('2020-01-02 00:00:00', 2),
+            ('2020-01-03 00:00:00', 2), ('2020-01-04 00:00:00', 3);
+    """
+    order_qt_v2_fixed_edge_multi_restart_best """
+        SELECT window_funnel(864000, 'fixed', ts, val>0, val>1, val>2) FROM 
windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: Multiple restarts with many E0-only rows
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 1), ('2020-01-02 00:00:00', 1),
+            ('2020-01-03 00:00:00', 1), ('2020-01-04 00:00:00', 2),
+            ('2020-01-05 00:00:00', 3);
+    """
+    order_qt_v2_fixed_edge_many_e0_restarts """
+        SELECT window_funnel(864000, 'fixed', ts, val>0, val>1, val>2) FROM 
windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: Irrelevant events are skipped in V2 FIXED mode (4.1+ behavior)
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 1), ('2020-01-02 00:00:00', 0),
+            ('2020-01-03 00:00:00', 2), ('2020-01-04 00:00:00', 3);
+    """
+    // R1(val=0) matches NO condition → V2 skips it → E0@R0→E1@R2→E2@R3 = 3
+    order_qt_v2_fixed_edge_irrelevant_skip """
+        SELECT window_funnel(864000, 'fixed', ts, val=1, val=2, val=3) FROM 
windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: "Relevant-but-wrong row" breaks chain in FIXED mode.
+    // R2 matches c2 (relevant) but not c3 (expected) → chain breaks at R2.
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 1), ('2020-01-02 00:00:00', 2),
+            ('2020-01-03 00:00:00', 2), ('2020-01-04 00:00:00', 3);
+    """
+    // c1@R0→c2@R1, R2 has c2 again (val=2, not c3=val=3) → breaks chain.
+    // No other row matches c1(val=1), so no multi-pass recovery. Expected: 2.
+    order_qt_v2_fixed_edge_relevant_but_wrong """
+        SELECT window_funnel(864000, 'fixed', ts, val=1, val=2, val=3) FROM 
windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+
+    // Test: "Relevant-but-wrong row" with no better starting point → breaks 
chain.
+    // Only c1 at R0. R1 has c2, R2 has c2 again (not c3). No later c1 to 
restart.
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
+    sql """
+        CREATE TABLE windowfunnel_v2_fixed_edge (ts datetime, val int)
+        DUPLICATE KEY(ts) DISTRIBUTED BY HASH(ts) BUCKETS 3
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO windowfunnel_v2_fixed_edge VALUES
+            ('2020-01-01 00:00:00', 10), ('2020-01-02 00:00:00', 5),
+            ('2020-01-03 00:00:00', 5), ('2020-01-04 00:00:00', 20);
+    """
+    // c1: val>8 (only 10,20). c2: val<8 (only 5). c3: val>15 (only 20).
+    // c1@R0→c2@R1, R2 has c2 (not c3) → break. c1@R3: no more rows. Best=2.
+    order_qt_v2_fixed_edge_relevant_wrong_no_restart """
+        SELECT window_funnel(864000, 'fixed', ts, val>8, val<8, val>15) FROM 
windowfunnel_v2_fixed_edge;
+    """
+    sql """ DROP TABLE IF EXISTS windowfunnel_v2_fixed_edge """
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to