Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wSm2r-0005TV-00 for pgsql-hackers@arkaria.postgresql.org; Fri, 29 May 2026 01:29:53 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wSm1o-0010LW-1M for pgsql-hackers@arkaria.postgresql.org; Fri, 29 May 2026 01:28:48 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wSm1n-0010LN-2h for pgsql-hackers@lists.postgresql.org; Fri, 29 May 2026 01:28:48 +0000 Received: from mail-ed1-x535.google.com ([2a00:1450:4864:20::535]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wSm1j-000000002y5-3Rev for pgsql-hackers@postgresql.org; Fri, 29 May 2026 01:28:46 +0000 Received: by mail-ed1-x535.google.com with SMTP id 4fb4d7f45d1cf-68be16c61d1so690204a12.2 for ; Thu, 28 May 2026 18:28:44 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1780018121; cv=none; d=google.com; s=arc-20240605; b=PEWA+FCtJllpB2jEMygWMO4fc+IgH2Jq4VWKODm55NVUy3/Vw8yJTeocjoucFnkQwn kpstd7SzkG6k0JLXCvm9GVvb7wklOZhFsWtEJOBFBxFwe65tpDRj5TDTKW4AYc1vdUEf rwa0+JV3+ITfYqChCIrDB1USQK8pAX7qf1tANW8sVSuDcy6nxvJ9a+b+ZGFYygvTMAaY kEMulFEA5AT1k6z1e7eKzH3+w1NLpa9GhQDdP5wU0OG5EfrZ0srJZUYGDZ25DxfbR4Og 7M/D9Z9yFLkDuWbKd9mCytewBMTzXgfaJAdz4o7EE9GNxek1HV7/YCtkK9/i71w6iIXw kFWQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=cc:to:subject:message-id:date:from:reply-to:in-reply-to:references :mime-version:dkim-signature; bh=3L6nVGu2crBSTNrJ9KOhDaW7jY7j/7b6v5Q/rWt9f2k=; fh=uEfhrTCHQSgW8e1CCtx1cUKCYhxOvl7msrGE7lcDCIc=; b=dA9ouPZ35BV/AEMNqWwdQJzb0rJ351YlsHCbLewhF+re+zAQQ6au7B2ZXjWjdWa06F X5i0azK/MQOpAsgglmhT0JACieRQGyIJom9TtLr8OITzW1Ukx+LQFdzz5HzX1LgPyp45 uiP0tEng+WB0iKjxGAdvliwOaAmxcSFGoBVywPZwFxqRsDhQQgrwKtd0Vq3dsXWlOu+f xV30rLqgWLngXyR9XQh27pZOKZ/v1btVLpo0kK0ykr5r5Z680ShJu9OxQ1P31RzfVtga 2hnHoBJMXzSgDUAk0OAX/15u/D+ufq6+XxB7ukkGzfveXNrpfNaLC7+lDKaJ2AYsuqsj f1VQ==; darn=postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1780018121; x=1780622921; darn=postgresql.org; h=cc:to:subject:message-id:date:from:reply-to:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=3L6nVGu2crBSTNrJ9KOhDaW7jY7j/7b6v5Q/rWt9f2k=; b=rkm+9sW8stfwlTtRsmnNPwki8VcAWVdzwJm+itbcoutgtNqDtSPdPb4X4WMN/ML4jA N6HPBt6HZotAYUQw5wwC0QKKDJVDNTxmmxDnkHGtgsalEDr+Q2dZxVprzJ5ozr240fRI 1Uga4DHdxe5i/qh0MaHdLtE0B0SIKxLoLyKynEPoyTpUrWzvPHw87Glm13C/pJHApcWV Gen13Q+bC9pPgQOsgNczW6da6vDOD9r2flKFDoo+x1HEvSX4B7WMUN8y16mICIk6FSVJ EYBCWYmsl/zsVWa5kEvwXG+8xyt+03N8kL5rBG2SBX3rRDYMxI2nnutSh7p2RTjrAQ2m bh8w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1780018121; x=1780622921; h=cc:to:subject:message-id:date:from:reply-to:in-reply-to:references :mime-version:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=3L6nVGu2crBSTNrJ9KOhDaW7jY7j/7b6v5Q/rWt9f2k=; b=ZPNJqhK58oXWOQpzZQrLJwq2UKQ90F4Cq7eZNEdKM0PjkFt+xI8adi3Vp0WzYvukpC E9+5VGXrzoCFb1VV6GjQot3p12+eEaZLU9FyGmbgeYCtL3pfnXmZLc4p3aeKoOrWCjdO 8X4DOiq+k2jXpQn8fMhl3yHtuoFyxNL84XbETD0Eoa/MA+HBFBYScwoxPPz+TpTKkFTR YpfvTY7jPmnyTxLcl9JL/YOzN3AaKrkN2xXQ8wsDonEqmHlvxtTQOYMBDpG3k6WeeCjF 1c0RHAg/d14Xaj9oHKV4S/HjkZbawV2BqBTTZy26S47D+XmRiRCKW43GfKvLx+vjcPFf B5Mg== X-Forwarded-Encrypted: i=1; AFNElJ++TWu9CC2I3kbOHAEY3ljekFMMoTwewJTBQlCjjH2j4xK3fko6ib7fdQy+ug+CxVDPSu0UtyMVxhr+JiGm@postgresql.org X-Gm-Message-State: AOJu0YyZGVIl/cBE3jSzdsGNGO+jQPCAV3rzocYa9P3RI7/AmOjwDD2x QM9ir+j9H11B9oCnxbDLPayWItvP62kxO5Rfp9ZktvSi8v5pNazUZNIhe2/b4+TZdvC8rr5sHSo A2slN+16Dldqro70mHSSRZuWlunEo54Y= X-Gm-Gg: Acq92OHEtOCu3eaDi3SvTfDyd/c+J5GiPfEYfyuhomcnGQZHrlwqQRgYsEECbN5o7ji 2gx3gg/0/CYBw1TFZzPVXDNjlePRLkfoQQGUEEcmdXszyaGwqFLJ8QBFCIanuc9T1RoR4hmaoxj 6I2zNbQIAol2h9HoVZisNLFhriGD+RCwgCh8z65TVmbWysscd3/lPtqB5eyrMt2N8eHSiDxTvsW nhbgQu786yg+0Yb/V+oY83raPxJS6rDSzDufMVBF5arF9w535BfPDpnRG0RoLkN1XbY7QSaB6xZ wPV8Ztp/WHCtJ+Op+ppkWPyc+0jZNxGbnzNkKs8mxR7x06P0TQ== X-Received: by 2002:a17:907:2d87:b0:bdc:aca4:8d3f with SMTP id a640c23a62f3a-be9c94399b0mr19193166b.13.1780018121256; Thu, 28 May 2026 18:28:41 -0700 (PDT) MIME-Version: 1.0 References: <20260502.140304.670813149418899420.ishii@postgresql.org> <20260505.090124.365339750969814137.ishii@postgresql.org> <20260517.190023.159085681032648582.ishii@postgresql.org> In-Reply-To: Reply-To: assam258@gmail.com From: Henson Choi Date: Fri, 29 May 2026 10:28:29 +0900 X-Gm-Features: AVHnY4IRuW4krjJc0_AADhRubTsHipzACOxCSduDsBO2-7CQqeqW48d8B66gxzQ Message-ID: Subject: Re: Row pattern recognition To: jian he Cc: Tatsuo Ishii , zsolt.parragi@percona.com, sjjang112233@gmail.com, vik@postgresfriends.org, er@xs4all.nl, jacob.champion@enterprisedb.com, david.g.johnston@gmail.com, peter@eisentraut.org, li.evan.chao@gmail.com, pgsql-hackers@postgresql.org Content-Type: multipart/alternative; boundary="000000000000cfb4fd0652eac355" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --000000000000cfb4fd0652eac355 Content-Type: text/plain; charset="UTF-8" Hi Jian, Thanks for the careful read of execRPR.c and the NFA -- several of these landed on real gaps in our comments/docs. Going through your points inline below, with a summary of decisions at the end. > In src/include/nodes/execnodes.h, we're adding quite a few fields to > WindowAggState that are only used for RPR queries. > Should we consolidate these fields behind a single pointer (named > RPRContext) to keep the WindowAggState size smaller for non-RPR > queries? The size win is real -- it's roughly 450-500 bytes per WindowAggState that every non-RPR window query carries today. But it takes a wide code change to get there: it reshapes every RPR access path, a sizable (if mostly mechanical) diff across nodeWindowAgg.c, execRPR.c, and the explain side. Tatsuo, as co-author -- do you want this in v48? If you do, I'll prepare the RPRContext consolidation as an incremental patch for you to fold into v48. It isn't blocking either way. > In function get_reduced_frame_status, I made the following changes: > [conditionA..D probe elided] > I re-ran the regression tests and noticed it's entirely possible for > multiple conditions to be true at once. Because of this, all the IF > blocks in get_reduced_frame_status (after the initial one) are > order-dependent. If I move one IF statement above another, > get_reduced_frame_status may return a different result. > Is this the expected behavior? It seems like a potential logic flaw. You're right that the conditions aren't mutually exclusive and that reordering the branches changes the result -- but that's the cascade-with-early-return idiom, standard in C and used throughout the backend, not a logic flaw. Each branch is the minimal test given the negations the earlier returns have already established. heapam_visibility.c is the canonical example: the HeapTupleSatisfies* family cascades through ordered early returns, with the later XMAX branches correct only because the XMIN-committed invariant above holds. The order is correct, not just idiomatic: update_reduced_frame() only ever records (matched, length) as (false, 1), (true, 0), or (true, >=1), and A->B->C->D is the order that classifies exactly those three cases -- reorder it and one of them gets misclassified. I'll add short "by here, ..." comments between the branches to make the invariants visible, but keep the structure. If you feel strongly it should be restructured, I'd defer to Tatsuo on that. > "boundary chk" should be "boundary check". > column "match_start dep.", I don't understand the meaning of "direct" > and "boundary check". > "count-dominance" seems like an invented word, but I could not find > the explanation. > "match_start-dependent" occurred many times, it would also deserve an > explanation. We can also rename it as match_start_dependent. All fair. v48 fixes the typo and adds a terminology block to README.rpr defining count-dominance (the VIII-3 cover condition, named there), match_start_dependent (renamed to the underscore spelling, to match the defineMatchStartDependent identifier), and the "direct" vs "boundary check" navigation distinction. > Attached is a minor refactoring of ExecRPRProcessRow, no need for > boolean hasLimitedFrame. > ``if (hasLimitedFrame && winstate->endOffsetValue != 0)`` seems wrong > to me, endOffsetValue is a Datum, and you directly compare it with 0. > see also calculate_frame_offsets. Good eye on the Datum comparison, and I agree your version reads more clearly -- gating on the frame-option flag and going through DatumGetInt64() rather than comparing a Datum to 0, plus the two new Asserts. (The existing code is correct, since hasLimitedFrame carries the limited/unbounded distinction separately, but the refactor is the clearer shape.) I'll take it. One thing to watch when dropping hasLimitedFrame, though: a zero offset is still a valid limited frame -- ROWS BETWEEN CURRENT ROW AND CURRENT ROW, and ... AND 0 FOLLOWING -- so the new check must not treat offset 0 as unbounded, otherwise the boundary check is skipped and a quantified pattern (A+) silently absorbs the whole partition. The limited/unbounded distinction lives in the frame-option flag, not the offset value, so the refactor has to keep that flag in the picture. We don't currently cover the offset-0 quantified case -- the two zero-offset frame tests in rpr_base both use a plain PATTERN (A) -- so I'll add an A+ regression test for it. > [nfa_evaluate_row's window_gettupleslot returning false] > indicates that it's not possible for (currentPos > ctxFrameEnd). > therefore [if (currentPos >= ctxFrameEnd) ...] > should change to ``if (currentPos == ctxFrameEnd)``. > With that, the comment wording "exceeded" seems wrong too. Agreed. `>` is unreachable by the loop invariant -- currentPos advances by exactly one per row, and once a context is finalized the states == NULL guard skips it -- so >= and == behave identically here, and == states the intent better. The `>=` was a defensive guard against an overshoot that can't actually happen; v48 tightens it to ==, changes the comment from "exceeded" to "reached", and moves that defense into an Assert(currentPos <= ctxFrameEnd) so a future change that breaks the invariant trips immediately instead of silently. > Since I couldn't generate a coverage report, I am using elog(INFO) to > test reachability. Rerun the regress tests, you will find out that the > ELSE IF branch below is not reached by current regress tests. > else if (targetCtx->states == NULL) > { > /* Context already completed - skip to result registration */ > goto register_result; > elog(INFO, "XXXX reached"); > } The elog in your snippet sits *after* the goto, so it can never print even though the branch runs. Moving it above the goto shows it firing across the suite. Easy mistake; I've put a probe right after a goto/return more times than I'd like to admit. The branch is correct as written: under SKIP TO NEXT ROW an overlapping context can finish (and be preserved by cleanup, since matchEndRow >= matchStartRow) before the next update_reduced_frame() call lands on its start row. So no new test is needed -- I'll just add a comment above it explaining why the head context can already be complete there. Summary of decisions, in the order above: RPRContext consolidation Tatsuo's call -- non-blocking for v48 get_reduced_frame_status order Keep -- cascade idiom; add comments README terminology + typo Accept -- definitions block + rename ExecRPRProcessRow refactor Accept -- but fix frameOffset currentPos >= -> == Accept -- plus comment + Assert states == NULL branch coverage Already reached (155x) -- add comment I'll post an incremental patch shortly with the accepted changes and the comment additions; the RPRContext question is for Tatsuo. Thanks again, Henson --000000000000cfb4fd0652eac355 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Hi Jian,

Thanks for the careful read of execRPR.c a= nd the NFA -- several of these
landed on real gaps in our comments/docs.= Going through your points
inline below, with a summary of decisions at = the end.


> In src/include/nodes/execnodes.h, we're adding= quite a few fields to
> WindowAggState that are only used for RPR qu= eries.
> Should we consolidate these fields behind a single pointer (= named
> RPRContext) to keep the WindowAggState size smaller for non-R= PR
> queries?

The size win is real -- it's roughly 450-500= bytes per WindowAggState
that every non-RPR window query carries today.= But it takes a wide
code change to get there: it reshapes every RPR acc= ess path, a
sizable (if mostly mechanical) diff across nodeWindowAgg.c,<= br>execRPR.c, and the explain side.

Tatsuo, as co-author -- do you w= ant this in v48? If you do, I'll
prepare the RPRContext consolidatio= n as an incremental patch for you to
fold into v48. It isn't blockin= g either way.


> In function get_reduced_frame_status, I made = the following changes:
> [conditionA..D probe elided]
> I re-ra= n the regression tests and noticed it's entirely possible for
> m= ultiple conditions to be true at once. Because of this, all the IF
> = blocks in get_reduced_frame_status (after the initial one) are
> orde= r-dependent. If I move one IF statement above another,
> get_reduced_= frame_status may return a different result.
> Is this the expected be= havior? It seems like a potential logic flaw.

You're right that = the conditions aren't mutually exclusive and that
reordering the bra= nches changes the result -- but that's the
cascade-with-early-return= idiom, standard in C and used throughout the
backend, not a logic flaw.= Each branch is the minimal test given the
negations the earlier returns= have already established.

heapam_visibility.c is the canonical exam= ple: the HeapTupleSatisfies*
family cascades through ordered early retur= ns, with the later XMAX
branches correct only because the XMIN-committed= invariant above holds.

The order is correct, not just idiomatic: up= date_reduced_frame() only
ever records (matched, length) as (false, 1), = (true, 0), or (true, >=3D1),
and A->B->C->D is the order tha= t classifies exactly those three cases --
reorder it and one of them get= s misclassified.

I'll add short "by here, ..." comment= s between the branches to make the
invariants visible, but keep the stru= cture. If you feel strongly it
should be restructured, I'd defer to = Tatsuo on that.


> "boundary chk" should be "bo= undary check".
> column "match_start dep.", I don'= t understand the meaning of "direct"
> and "boundary c= heck".
> "count-dominance" seems like an invented word= , but I could not find
> the explanation.
> "match_start-d= ependent" occurred many times, it would also deserve an
> explan= ation. We can also rename it as match_start_dependent.

All fair. v48= fixes the typo and adds a terminology block to README.rpr
defining coun= t-dominance (the VIII-3 cover condition, named there),
match_start_depen= dent (renamed to the underscore spelling, to match the
defineMatchStartD= ependent identifier), and the "direct" vs "boundary
check= " navigation distinction.


> Attached is a minor refactor= ing of ExecRPRProcessRow, no need for
> boolean hasLimitedFrame.
&= gt; ``if (hasLimitedFrame && winstate->endOffsetValue !=3D 0)`` = seems wrong
> to me, endOffsetValue is a Datum, and you directly comp= are it with 0.
> see also calculate_frame_offsets.

Good eye on= the Datum comparison, and I agree your version reads more
clearly -- ga= ting on the frame-option flag and going through
DatumGetInt64() rather t= han comparing a Datum to 0, plus the two new
Asserts. (The existing code= is correct, since hasLimitedFrame carries
the limited/unbounded distinc= tion separately, but the refactor is the
clearer shape.) I'll take i= t.

One thing to watch when dropping hasLimitedFrame, though: a zero = offset
is still a valid limited frame -- ROWS BETWEEN CURRENT ROW AND CU= RRENT
ROW, and ... AND 0 FOLLOWING -- so the new check must not treat of= fset 0
as unbounded, otherwise the boundary check is skipped and a quant= ified
pattern (A+) silently absorbs the whole partition. The limited/unb= ounded
distinction lives in the frame-option flag, not the offset value,= so the
refactor has to keep that flag in the picture.

We don'= ;t currently cover the offset-0 quantified case -- the two
zero-offset f= rame tests in rpr_base both use a plain PATTERN (A) -- so
I'll add a= n A+ regression test for it.


> [nfa_evaluate_row's window= _gettupleslot returning false]
> indicates that it's not possible= for (currentPos > ctxFrameEnd).
> therefore [if (currentPos >= =3D ctxFrameEnd) ...]
> should change to ``if (currentPos =3D=3D ctxF= rameEnd)``.
> With that, the comment wording "exceeded" see= ms wrong too.

Agreed. `>` is unreachable by the loop invariant --= currentPos advances
by exactly one per row, and once a context is final= ized the
states =3D=3D NULL guard skips it -- so >=3D and =3D=3D beha= ve identically here,
and =3D=3D states the intent better. The `>=3D` = was a defensive guard against
an overshoot that can't actually happe= n; v48 tightens it to =3D=3D, changes
the comment from "exceeded&qu= ot; to "reached", and moves that defense into an
Assert(curren= tPos <=3D ctxFrameEnd) so a future change that breaks the
invariant t= rips immediately instead of silently.


> Since I couldn't = generate a coverage report, I am using elog(INFO) to
> test reachabil= ity. Rerun the regress tests, you will find out that the
> ELSE IF br= anch below is not reached by current regress tests.
> =C2=A0 =C2=A0 e= lse if (targetCtx->states =3D=3D NULL)
> =C2=A0 =C2=A0 {
> = =C2=A0 =C2=A0 =C2=A0 =C2=A0 /* Context already completed - skip to result r= egistration */
> =C2=A0 =C2=A0 =C2=A0 =C2=A0 goto register_result;> =C2=A0 =C2=A0 =C2=A0 =C2=A0 elog(INFO, "XXXX reached");
= > =C2=A0 =C2=A0 }

The elog in your snippet sits *after* the goto,= so it can never print
even though the branch runs. Moving it above the = goto shows it firing
across the suite. Easy mistake; I've put a prob= e right after a
goto/return more times than I'd like to admit.
The branch is correct as written: under SKIP TO NEXT ROW an overlappingcontext can finish (and be preserved by cleanup, since
matchEndRow >= ;=3D matchStartRow) before the next update_reduced_frame()
call lands on= its start row. So no new test is needed -- I'll just add a
comment = above it explaining why the head context can already be complete
there.<= br>

Summary of decisions, in the order above:


=C2=A0 RPRContext consolidation =C2=A0 =C2=A0 =C2=A0 =C2=A0 = Tatsuo's call -- non-blocking for v48
=C2=A0 get_reduced_frame_statu= s order =C2=A0 Keep -- cascade idiom; add comments
=C2=A0 README termino= logy + typo =C2=A0 =C2=A0 =C2=A0 =C2=A0Accept -- definitions block + rename=
=C2=A0 ExecRPRProcessRow refactor =C2=A0 =C2=A0 =C2=A0 Accept -- but fi= x frameOffset
=C2=A0 currentPos >=3D -> =3D=3D =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Accept -- plus comment + Assert
=C2=A0 st= ates =3D=3D NULL branch coverage =C2=A0 Already reached (155x) -- add comme= nt



I'll post an incremental patch shortly with the ac= cepted changes and the
comment additions; the RPRContext question is for= Tatsuo.

Thanks again,
Henson
--000000000000cfb4fd0652eac355--