nocfbot-0018-JIT-support-for-PREV-NEXT.txt

text/plain

Filename: nocfbot-0018-JIT-support-for-PREV-NEXT.txt
Type: text/plain
Part: 17
Message: Re: Row pattern recognition
From efb99428cfdb41363d49d4b7ca199f9212ba5a6e Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Thu, 2 Apr 2026 10:54:30 +0900
Subject: [PATCH 18/40] Add JIT compilation support for RPR PREV/NEXT
 navigation

---
 src/backend/jit/llvm/llvmjit_expr.c | 72 +++++++++++++++++++++--------
 src/test/regress/expected/rpr.out   | 31 +++++++++++++
 src/test/regress/sql/rpr.sql        | 27 +++++++++++
 3 files changed, 111 insertions(+), 19 deletions(-)

diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index d158e37e7b5..4901b2a7ff4 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -127,6 +127,9 @@ llvm_compile_expr(ExprState *state)
 	LLVMValueRef v_aggvalues;
 	LLVMValueRef v_aggnulls;
 
+	/* RPR navigation: when true, EEOP_OUTER_VAR reloads from econtext */
+	bool		has_rpr_nav;
+
 	instr_time	starttime;
 	instr_time	deform_starttime;
 	instr_time	endtime;
@@ -300,19 +303,16 @@ llvm_compile_expr(ExprState *state)
 	 * RPR navigation opcodes (PREV/NEXT) swap ecxt_outertuple to a different
 	 * row mid-expression.  The JIT code loads v_outervalues and v_outernulls
 	 * once in the entry block and reuses them for all EEOP_OUTER_VAR steps.
-	 * After a slot swap, these pointers become stale because the new slot has
-	 * its own tts_values/tts_isnull arrays.  Fall back to the interpreter for
-	 * these expressions.
+	 * After a slot swap, these cached pointers become stale because the new
+	 * slot has its own tts_values/tts_isnull arrays.
 	 *
-	 * XXX To JIT-compile these expressions properly, the NAV_SET and
-	 * NAV_RESTORE handlers would need to reload the tts_values and tts_isnull
-	 * pointers from the new slot.  However, LLVM uses SSA (Static Single
-	 * Assignment) form where each value is defined exactly once.  When
-	 * different basic blocks produce different values for the same pointer,
-	 * LLVM requires PHI nodes at the merge point to select the correct one.
-	 * Without that plumbing, OUTER_VAR steps after a slot swap would read
-	 * from the wrong pointer.
+	 * When RPR navigation opcodes are present, EEOP_OUTER_VAR reloads the
+	 * slot pointer from econtext->ecxt_outertuple on every access instead of
+	 * using the cached entry-block values.  This avoids the SSA/PHI
+	 * complexity while keeping the rest of the expression JIT-compiled.
+	 * Expressions without RPR navigation use the cached values as before.
 	 */
+	has_rpr_nav = false;
 	if (parent && IsA(parent, WindowAggState) &&
 		((WindowAgg *) parent->plan)->rpPattern != NULL)
 	{
@@ -323,9 +323,8 @@ llvm_compile_expr(ExprState *state)
 			if (opcode == EEOP_RPR_NAV_SET ||
 				opcode == EEOP_RPR_NAV_RESTORE)
 			{
-				LLVMDeleteFunction(eval_fn);
-				LLVMDisposeBuilder(b);
-				return false;
+				has_rpr_nav = true;
+				break;
 			}
 		}
 	}
@@ -492,8 +491,37 @@ llvm_compile_expr(ExprState *state)
 					}
 					else if (opcode == EEOP_OUTER_VAR)
 					{
-						v_values = v_outervalues;
-						v_nulls = v_outernulls;
+						if (has_rpr_nav)
+						{
+							/*
+							 * RPR navigation swaps ecxt_outertuple
+							 * mid-expression.  Reload slot pointer from
+							 * econtext on every access so we read from the
+							 * current (possibly swapped) slot.
+							 */
+							LLVMValueRef v_tmpslot;
+
+							v_tmpslot = l_load_struct_gep(b,
+														  StructExprContext,
+														  v_econtext,
+														  FIELDNO_EXPRCONTEXT_OUTERTUPLE,
+														  "v_outerslot_reload");
+							v_values = l_load_struct_gep(b,
+														 StructTupleTableSlot,
+														 v_tmpslot,
+														 FIELDNO_TUPLETABLESLOT_VALUES,
+														 "v_outervalues_reload");
+							v_nulls = l_load_struct_gep(b,
+														StructTupleTableSlot,
+														v_tmpslot,
+														FIELDNO_TUPLETABLESLOT_ISNULL,
+														"v_outernulls_reload");
+						}
+						else
+						{
+							v_values = v_outervalues;
+							v_nulls = v_outernulls;
+						}
 					}
 					else if (opcode == EEOP_SCAN_VAR)
 					{
@@ -2467,10 +2495,16 @@ llvm_compile_expr(ExprState *state)
 				break;
 
 			case EEOP_RPR_NAV_SET:
+				build_EvalXFunc(b, mod, "ExecEvalRPRNavSet",
+								v_state, op, v_econtext);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
 			case EEOP_RPR_NAV_RESTORE:
-				/* unreachable: filtered out by the pre-scan above */
-				Assert(false);
-				return false;
+				build_EvalXFunc(b, mod, "ExecEvalRPRNavRestore",
+								v_state, op, v_econtext);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
 
 			case EEOP_AGG_STRICT_DESERIALIZE:
 			case EEOP_AGG_DESERIALIZE:
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index d586e17e0a1..de6ce4fba8a 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -2153,6 +2153,37 @@ SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
            0 |      99998 |     99999
 (1 row)
 
+-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
+-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
+-- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit_above_cost = 0;
+WITH data AS (
+ SELECT i, abs(50000 - i) AS price
+ FROM generate_series(1, 100000) i
+),
+result AS (
+ SELECT i, price,
+        count(*) OVER w AS match_len,
+        first_value(price) OVER w AS match_first
+ FROM data
+ WINDOW w AS (
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  INITIAL
+  PATTERN (DOWN+ UP+)
+  DEFINE
+   DOWN AS price < PREV(price),
+   UP AS price > PREV(price)
+ )
+)
+SELECT count(*) AS matched_rows, max(match_len) AS longest_match
+FROM result WHERE match_len > 0;
+ matched_rows | longest_match 
+--------------+---------------
+            1 |         99999
+(1 row)
+
+RESET jit_above_cost;
 --
 -- IGNORE NULLS
 --
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 504476a2b02..b3bbc8254c4 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1084,6 +1084,33 @@ result AS (
 -- Should match: A (33333 rows) + B (33333 rows) + C (33333 rows) = 99999 rows
 SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
 
+-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
+-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
+-- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit_above_cost = 0;
+WITH data AS (
+ SELECT i, abs(50000 - i) AS price
+ FROM generate_series(1, 100000) i
+),
+result AS (
+ SELECT i, price,
+        count(*) OVER w AS match_len,
+        first_value(price) OVER w AS match_first
+ FROM data
+ WINDOW w AS (
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  INITIAL
+  PATTERN (DOWN+ UP+)
+  DEFINE
+   DOWN AS price < PREV(price),
+   UP AS price > PREV(price)
+ )
+)
+SELECT count(*) AS matched_rows, max(match_len) AS longest_match
+FROM result WHERE match_len > 0;
+RESET jit_above_cost;
+
 --
 -- IGNORE NULLS
 --
-- 
2.50.1 (Apple Git-155)