[FLINK-40233][table] Correctly share the parsed JSON across JSON_VALUE/JSON_QUERY calls when the owning call is skipped because of NULL or not taken branch - #28836
Conversation
…E/JSON_QUERY calls when the owning call is skipped because of NULL or not taken branch
gustavodemorais
left a comment
There was a problem hiding this comment.
Hey @snuyanzin, thanks quickly picking this one up. I've added some suggestions
| assertThat(countJsonParse(code)) | ||
| .as("JSON_VALUE + JSON_QUERY on the same input should parse once") | ||
| .isEqualTo(1); | ||
| .isOdd(); |
There was a problem hiding this comment.
Leftover?
| .isOdd(); | |
| .isOne(); |
| } | ||
|
|
||
| @Test | ||
| void testReuseSurvivesCodeSplitting() { |
There was a problem hiding this comment.
Are you sure this is testing code splitting?
extractGeneratedCode reads GeneratedClass.getCode(), which is the original source; splitting is applied in the constructor into the separate splitCode field and only that one is compiled (GeneratedClass.java:55-62, :97). The config only affects the collect(sql) half. I think it needs some tweaks in the test to test splitting
But to be honest is not the most important thing to test in the PR, so I'd not say this is a blocker for me and dropping the test would also be ok
| } | ||
|
|
||
| @Test | ||
| void testFilterAndProjectionShareParse() { |
There was a problem hiding this comment.
All tests use a plain column: could you add a test using a computed column like using TRIM(json_data) as the input to the json functions?
| .as("JSON_VALUE calls on different inputs should parse separately") | ||
| .isEqualTo(2); | ||
| } | ||
|
|
There was a problem hiding this comment.
I think I found one edge case, but it might have an easy solution
Repro
-- rows: (1000,'{"n":1}') (2000,'{"n":2}') (3000,'{"n":3}') (9000,'{"n":9}')
SELECT total FROM events MATCH_RECOGNIZE (
ORDER BY rt
MEASURES SUM(CAST(JSON_VALUE(A.f1, '$.n') AS INT)) AS total
AFTER MATCH SKIP PAST LAST ROW
PATTERN (A+ B)
DEFINE A AS A.f0 < 9000, B AS B.f0 >= 9000
)
-- this branch: [3] (= 1+1+1)
-- master: [6] (= 1+2+3)
The aggregate loops over every row of the match (MatchCodeGenerator.scala), but jsonParsed$0 = null; is a per-record statement, emitted once per processMatch - so rows 2..n reuse row 1's parse. Same cause in batch with table.exec.operator-fusion-codegen.enabled=true: CalcFusionCodegenSpec never emits reusePerRecordCode(), so the reset is dropped entirely.
Master is safe because the assignment was unconditional at the call site. The problem is that correctness now depends on which operator template wraps the expression.
Possible easy solution: A local (addReusableLocalVariable, plus an explicit = null init) would fix both - reuseLocalVariableCode() is emitted in the fusion path and is keyed per method, so it resets inside the MATCH loop too
| // null means not parsed yet - jsonParse never returns null for a non-null input | ||
| ctx.addReusableMember( | ||
| s""" | ||
| |private $typeName $methodName(Object in) { |
There was a problem hiding this comment.
Could we do this?
| |private $typeName $methodName(Object in) { | |
| |private $typeName $methodName(${CodeGenUtils.BINARY_STRING} in) { |
What is the purpose of the change
It might happen that
NULLis cached while it should not for instance in case ofBrief change log
codegen
Verifying this change
Does this pull request potentially affect one of the following parts:
@Public(Evolving): ( no)Documentation
Was generative AI tooling used to co-author this PR?