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

lvyanquan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink-cdc.git


The following commit(s) were added to refs/heads/master by this push:
     new 85b83fc04 [FLINK-40151][runtime] Support missing YAML Transform 
predicates and nullable BOOLEAN logical evaluation (#4474)
85b83fc04 is described below

commit 85b83fc0444c9f6762839c187d17785ffc69bb71
Author: haruki <[email protected]>
AuthorDate: Wed Jul 22 09:03:03 2026 +0800

    [FLINK-40151][runtime] Support missing YAML Transform predicates and 
nullable BOOLEAN logical evaluation (#4474)
---
 docs/content.zh/docs/core-concept/transform.md     |  40 ++++--
 docs/content/docs/core-concept/transform.md        |  38 +++--
 .../src/test/resources/specs/comparison.yaml       |  91 ++++++++++++
 .../src/test/resources/specs/logical.yaml          |  23 ++-
 .../functions/impl/ComparisonFunctions.java        |  11 ++
 .../runtime/functions/impl/LogicalFunctions.java   |  74 ++++++++++
 .../runtime/functions/impl/StringFunctions.java    |  34 +++++
 .../transform/TransformFilterProcessor.java        |   3 +-
 .../flink/cdc/runtime/parser/JaninoCompiler.java   | 159 +++++++++++++++++++--
 .../flink/cdc/runtime/parser/TransformParser.java  | 153 +++++++++++++++++++-
 .../parser/metadata/TransformSqlOperatorTable.java |   7 +
 .../functions/impl/ComparisonFunctionsTest.java    |  78 ++++++++++
 .../functions/impl/StringFunctionsTest.java        |  60 ++++++++
 .../cdc/runtime/parser/JaninoCompilerTest.java     | 103 +++++++++++++
 .../cdc/runtime/parser/TransformParserTest.java    |  84 ++++++++---
 15 files changed, 896 insertions(+), 62 deletions(-)

diff --git a/docs/content.zh/docs/core-concept/transform.md 
b/docs/content.zh/docs/core-concept/transform.md
index 8dc5ceae0..dc405f777 100644
--- a/docs/content.zh/docs/core-concept/transform.md
+++ b/docs/content.zh/docs/core-concept/transform.md
@@ -120,26 +120,38 @@ Flink CDC 使用 [Calcite](https://calcite.apache.org/) 
来解析表达式并且
 | value1 >= value2                     | greaterThanOrEqual(value1, value2)    
       | 如果 value1 大于等于 value2,返回 TRUE;如果 value1 或 value2 为 NULL,返回 FALSE。 |
 | value1 < value2                      | lessThan(value1, value2)              
       | 如果 value1 小于 value2,返回 TRUE;如果 value1 或 value2 为 NULL,返回 FALSE。   |
 | value1 <= value2                     | lessThanOrEqual(value1, value2)       
       | 如果 value1 小于等于 value2,返回 TRUE;如果 value1 或 value2 为 NULL,返回 FALSE。 |
-| value IS NULL                        | null == value                         
       | 如果 value 为 NULL,返回 TRUE。                                      |
-| value IS NOT NULL                    | null != value                         
       | 如果 value 不为 NULL,返回 TRUE。                                     |
+| value IS NULL                        | isNull(value)                         
       | 如果 value 为 NULL,返回 TRUE。                                      |
+| value IS NOT NULL                    | isNotNull(value)                      
       | 如果 value 不为 NULL,返回 TRUE。                                     |
+| value1 IS DISTINCT FROM value2       | isDistinctFrom(value1, value2)        
       | 如果 value1 和 value2 是不同的值,返回 TRUE。NULL 会作为普通值参与比较,且不会返回 NULL。 |
+| value1 IS NOT DISTINCT FROM value2   | isNotDistinctFrom(value1, value2)     
       | 如果 value1 和 value2 不是不同的值,返回 TRUE。NULL 会作为普通值参与比较,且不会返回 NULL。 |
 | value1 BETWEEN value2 AND value3     | betweenAsymmetric(value1, value2, 
value3)    | 如果 value1 大于等于 value2 且小于等于 value3,返回 TRUE。                    |
 | value1 NOT BETWEEN value2 AND value3 | notBetweenAsymmetric(value1, value2, 
value3) | 如果 value1 小于 value2 或大于 value3,返回 TRUE。                        |
-| string1 LIKE string2                 | like(string1, string2)                
       | 如果 string1 匹配模式 string2,返回 TRUE。                              |
-| string1 NOT LIKE string2             | notLike(string1, string2)             
       | 如果 string1 不匹配模式 string2,返回 TRUE。                             |
+| string1 LIKE string2                 | like(string1, string2)                
       | 如果 string1 按 Java 正则表达式和子串匹配语义匹配 string2,返回 TRUE。           |
+| string1 NOT LIKE string2             | notLike(string1, string2)             
       | 如果 string1 按 Java 正则表达式和子串匹配语义不匹配 string2,返回 TRUE。          |
+| string1 LIKE string2 ESCAPE string3  | like(string1, string2, string3)       
       | 如果 string1 匹配 SQL LIKE 模式 string2,返回 TRUE。`%` 匹配任意长度字符串,`_` 
匹配单个字符,string3 用于转义通配符。如果任一参数为 NULL,返回 NULL。 |
+| string1 NOT LIKE string2 ESCAPE string3 | notLike(string1, string2, string3) 
       | 如果 string1 不匹配 SQL LIKE 模式 string2,返回 TRUE。如果任一参数为 NULL,返回 NULL。 |
+| string1 SIMILAR TO string2           | similarTo(string1, string2)           
       | 如果 string1 匹配 SQL SIMILAR TO 模式 string2,返回 TRUE。如果任一参数为 NULL,返回 NULL。 |
+| string1 NOT SIMILAR TO string2       | notSimilarTo(string1, string2)        
       | 如果 string1 不匹配 SQL SIMILAR TO 模式 string2,返回 TRUE。如果任一参数为 NULL,返回 NULL。 
|
+| string1 SIMILAR TO string2 ESCAPE string3 | similarTo(string1, string2, 
string3)    | 如果 string1 使用 string3 作为转义字符匹配 SQL SIMILAR TO 模式 string2,返回 
TRUE。如果任一参数为 NULL,返回 NULL。 |
+| string1 NOT SIMILAR TO string2 ESCAPE string3 | notSimilarTo(string1, 
string2, string3) | 如果 string1 使用 string3 作为转义字符不匹配 SQL SIMILAR TO 模式 
string2,返回 TRUE。如果任一参数为 NULL,返回 NULL。 |
 | value1 IN (value2 [, value3]* )      | in(value1, value2 [, value3]*)        
       | 如果 value1 存在于给定列表 (value2, value3, …) 中,返回 TRUE。              |
 | value1 NOT IN (value2 [, value3]* )  | notIn(value1, value2 [, value3]*)     
       | 如果 value1 不存在于给定列表 (value2, value3, …) 中,返回 TRUE。             |
 
 ## 逻辑函数
 
-| 函数                    | Janino 代码                      | 描述                  
                      |
-|-----------------------|--------------------------------|-------------------------------------------|
-| boolean1 OR boolean2  | boolean1 &#124;&#124; boolean2 | 如果 BOOLEAN1 为 TRUE 
或 BOOLEAN2 为 TRUE,返回 TRUE。 |
-| boolean1 AND boolean2 | boolean1 && boolean2           | 如果 BOOLEAN1 和 
BOOLEAN2 都为 TRUE,返回 TRUE。      |
-| NOT boolean           | !boolean                       | 如果 boolean 为 
FALSE,返回 TRUE;如果 boolean 为 TRUE,返回 FALSE。 |
-| boolean IS FALSE      | false == boolean               | 如果 boolean 为 
FALSE,返回 TRUE;如果 boolean 为 TRUE,返回 FALSE。 |
-| boolean IS NOT FALSE  | true == boolean                | 如果 BOOLEAN 为 
TRUE,返回 TRUE;如果 BOOLEAN 为 FALSE,返回 FALSE。 |
-| boolean IS TRUE       | true == boolean                | 如果 BOOLEAN 为 
TRUE,返回 TRUE;如果 BOOLEAN 为 FALSE,返回 FALSE。 |
-| boolean IS NOT TRUE   | false == boolean               | 如果 boolean 为 
FALSE,返回 TRUE;如果 boolean 为 TRUE,返回 FALSE。 |
+逻辑函数遵循 SQL 的三值逻辑来处理可为 NULL 的 BOOLEAN 值。`AND` 和 `OR` 
会在左操作数已经决定结果时短路,不再计算右操作数。生成的 Janino 代码会根据操作数可空性使用原生运算符、条件表达式或惰性函数调用。
+
+| 函数                     | Janino 代码            | 描述                           
                                      |
+|------------------------|----------------------|--------------------------------------------------------------------|
+| boolean1 OR boolean2   | 短路 OR 表达式  | 如果任一值为 TRUE,返回 TRUE;如果没有 TRUE 且至少一个值为 
NULL,返回 NULL。               |
+| boolean1 AND boolean2  | 短路 AND 表达式 | 如果任一值为 FALSE,返回 FALSE;如果没有 FALSE 
且至少一个值为 NULL,返回 NULL。            |
+| NOT boolean            | not(boolean)           | 如果 boolean 为 FALSE,返回 
TRUE;如果 boolean 为 TRUE,返回 FALSE;如果为 NULL,返回 NULL。 |
+| boolean IS FALSE       | isFalse(boolean)       | 如果 boolean 为 FALSE,返回 
TRUE;如果 boolean 为 TRUE 或 NULL,返回 FALSE。          |
+| boolean IS NOT FALSE   | isNotFalse(boolean)    | 如果 boolean 为 TRUE 或 
NULL,返回 TRUE;如果 boolean 为 FALSE,返回 FALSE。          |
+| boolean IS TRUE        | isTrue(boolean)        | 如果 boolean 为 TRUE,返回 
TRUE;如果 boolean 为 FALSE 或 NULL,返回 FALSE。          |
+| boolean IS NOT TRUE    | isNotTrue(boolean)     | 如果 boolean 为 FALSE 或 
NULL,返回 TRUE;如果 boolean 为 TRUE,返回 FALSE。          |
+| boolean IS UNKNOWN     | isNull(boolean)        | 如果 boolean 为 NULL,返回 TRUE。 
                                         |
+| boolean IS NOT UNKNOWN | isNotNull(boolean)     | 如果 boolean 不为 NULL,返回 
TRUE。                                         |
 
 ## 数学函数
 
@@ -542,4 +554,4 @@ pipeline:
 
|---------------|--------|----------|----------------------------------------------------------------------------------------------------------------------|
 | openai.model  | STRING | 必填       | 
要调用的模型名称,例如:"text-embedding-3-small",可用选项有 
"text-embedding-3-small"、"text-embedding-3-large"、"text-embedding-ada-002"。 |
 | openai.host   | STRING | 必填       | 
要连接的模型服务器地址,例如:`http://langchain4j.dev/demo/openai/v1`。                         
                          |
-| openai.apikey | STRING | 必填       | 模型服务器验证的 API Key,例如:"demo"。              
                                                                    |
\ No newline at end of file
+| openai.apikey | STRING | 必填       | 模型服务器验证的 API Key,例如:"demo"。              
                                                                    |
diff --git a/docs/content/docs/core-concept/transform.md 
b/docs/content/docs/core-concept/transform.md
index 4a6d40b61..eb7629057 100644
--- a/docs/content/docs/core-concept/transform.md
+++ b/docs/content/docs/core-concept/transform.md
@@ -121,26 +121,38 @@ Flink CDC uses [Calcite](https://calcite.apache.org/) to 
parse expressions and [
 | value1 >= value2                     | greaterThanOrEqual(value1, value2)    
       | Returns TRUE if value1 is greater than or equal to value2; returns 
FALSE if value1 or value2 is NULL. |
 | value1 < value2                      | lessThan(value1, value2)              
       | Returns TRUE if value1 is less than value2; returns FALSE if value1 or 
value2 is NULL.                |
 | value1 <= value2                     | lessThanOrEqual(value1, value2)       
       | Returns TRUE if value1 is less than or equal to value2; returns FALSE 
if value1 or value2 is NULL.    |
-| value IS NULL                        | null == value                         
       | Returns TRUE if value is NULL.                                         
                               |
-| value IS NOT NULL                    | null != value                         
       | Returns TRUE if value is not NULL.                                     
                               |
+| value IS NULL                        | isNull(value)                         
       | Returns TRUE if value is NULL.                                         
                               |
+| value IS NOT NULL                    | isNotNull(value)                      
       | Returns TRUE if value is not NULL.                                     
                               |
+| value1 IS DISTINCT FROM value2       | isDistinctFrom(value1, value2)        
       | Returns TRUE if value1 and value2 are distinct. NULL values are 
compared as values and never return NULL. |
+| value1 IS NOT DISTINCT FROM value2   | isNotDistinctFrom(value1, value2)     
       | Returns TRUE if value1 and value2 are not distinct. NULL values are 
compared as values and never return NULL. |
 | value1 BETWEEN value2 AND value3     | betweenAsymmetric(value1, value2, 
value3)    | Returns TRUE if value1 is greater than or equal to value2 and less 
than or equal to value3.           |
 | value1 NOT BETWEEN value2 AND value3 | notBetweenAsymmetric(value1, value2, 
value3) | Returns TRUE if value1 is less than value2 or greater than value3.    
                                |
-| string1 LIKE string2                 | like(string1, string2)                
       | Returns TRUE if string1 matches pattern string2.                       
                               |
-| string1 NOT LIKE string2             | notLike(string1, string2)             
       | Returns TRUE if string1 does not match pattern string2.                
                               |
+| string1 LIKE string2                 | like(string1, string2)                
       | Returns TRUE if string1 matches Java regular expression string2 using 
substring matching.             |
+| string1 NOT LIKE string2             | notLike(string1, string2)             
       | Returns TRUE if string1 does not match Java regular expression string2 
using substring matching.      |
+| string1 LIKE string2 ESCAPE string3  | like(string1, string2, string3)       
       | Returns TRUE if string1 matches SQL LIKE pattern string2. `%` matches 
zero or more characters, `_` matches one character, and string3 escapes 
wildcard characters. Returns NULL if any argument is NULL. |
+| string1 NOT LIKE string2 ESCAPE string3 | notLike(string1, string2, string3) 
       | Returns TRUE if string1 does not match SQL LIKE pattern string2. 
Returns NULL if any argument is NULL. |
+| string1 SIMILAR TO string2           | similarTo(string1, string2)           
       | Returns TRUE if string1 matches SQL SIMILAR TO pattern string2. 
Returns NULL if any argument is NULL. |
+| string1 NOT SIMILAR TO string2       | notSimilarTo(string1, string2)        
       | Returns TRUE if string1 does not match SQL SIMILAR TO pattern string2. 
Returns NULL if any argument is NULL. |
+| string1 SIMILAR TO string2 ESCAPE string3 | similarTo(string1, string2, 
string3)    | Returns TRUE if string1 matches SQL SIMILAR TO pattern string2 
using string3 as the escape character. Returns NULL if any argument is NULL. |
+| string1 NOT SIMILAR TO string2 ESCAPE string3 | notSimilarTo(string1, 
string2, string3) | Returns TRUE if string1 does not match SQL SIMILAR TO 
pattern string2 using string3 as the escape character. Returns NULL if any 
argument is NULL. |
 | value1 IN (value2 [, value3]* )      | in(value1, value2 [, value3]*)        
       | Returns TRUE if value1 exists in the given list (value2, value3, …).   
                               |
 | value1 NOT IN (value2 [, value3]* )  | notIn(value1, value2 [, value3]*)     
       | Returns TRUE if value1 does not exist in the given list (value2, 
value3, …).                          |
 
 ## Logical Functions
 
-| Function              | Janino Code                    | Description         
                                                |
-|-----------------------|--------------------------------|---------------------------------------------------------------------|
-| boolean1 OR boolean2  | boolean1 &#124;&#124; boolean2 | Returns TRUE if 
BOOLEAN1 is TRUE or BOOLEAN2 is TRUE.               |
-| boolean1 AND boolean2 | boolean1 && boolean2           | Returns TRUE if 
BOOLEAN1 and BOOLEAN2 are both TRUE.                |
-| NOT boolean           | !boolean                       | Returns TRUE if 
boolean is FALSE; returns FALSE if boolean is TRUE. |
-| boolean IS FALSE      | false == boolean               | Returns TRUE if 
boolean is FALSE; returns FALSE if boolean is TRUE. |
-| boolean IS NOT FALSE  | true == boolean                | Returns TRUE if 
BOOLEAN is TRUE; returns FALSE if BOOLEAN is FALSE. |
-| boolean IS TRUE       | true == boolean                | Returns TRUE if 
BOOLEAN is TRUE; returns FALSE if BOOLEAN is FALSE. |
-| boolean IS NOT TRUE   | false == boolean               | Returns TRUE if 
boolean is FALSE; returns FALSE if boolean is TRUE. |
+Logical functions follow SQL three-valued logic for nullable BOOLEAN values. 
`AND` and `OR` short-circuit the right operand when the left operand determines 
the result. Generated Janino code may use native operators, conditional 
expressions, or lazy function calls depending on operand nullability.
+
+| Function               | Janino Code           | Description                 
                                                                                
     |
+|------------------------|-----------------------|------------------------------------------------------------------------------------------------------------------|
+| boolean1 OR boolean2   | short-circuit OR expression | Returns TRUE if 
either value is TRUE; returns NULL if neither value is TRUE and at least one 
value is NULL.       |
+| boolean1 AND boolean2  | short-circuit AND expression | Returns FALSE if 
either value is FALSE; returns NULL if neither value is FALSE and at least one 
value is NULL.    |
+| NOT boolean            | not(boolean)           | Returns TRUE if boolean is 
FALSE; returns FALSE if boolean is TRUE; returns NULL if boolean is NULL.       
       |
+| boolean IS FALSE       | isFalse(boolean)       | Returns TRUE if boolean is 
FALSE; returns FALSE if boolean is TRUE or NULL.                                
       |
+| boolean IS NOT FALSE   | isNotFalse(boolean)    | Returns TRUE if boolean is 
TRUE or NULL; returns FALSE if boolean is FALSE.                                
       |
+| boolean IS TRUE        | isTrue(boolean)        | Returns TRUE if boolean is 
TRUE; returns FALSE if boolean is FALSE or NULL.                                
       |
+| boolean IS NOT TRUE    | isNotTrue(boolean)     | Returns TRUE if boolean is 
FALSE or NULL; returns FALSE if boolean is TRUE.                                
       |
+| boolean IS UNKNOWN     | isNull(boolean)        | Returns TRUE if boolean is 
NULL.                                                                           
      |
+| boolean IS NOT UNKNOWN | isNotNull(boolean)     | Returns TRUE if boolean is 
not NULL.                                                                       
      |
 
 ## Arithmetic Functions
 
diff --git a/flink-cdc-composer/src/test/resources/specs/comparison.yaml 
b/flink-cdc-composer/src/test/resources/specs/comparison.yaml
index c022f0780..85d7e80b4 100644
--- a/flink-cdc-composer/src/test/resources/specs/comparison.yaml
+++ b/flink-cdc-composer/src/test/resources/specs/comparison.yaml
@@ -165,6 +165,26 @@
     DataChangeEvent{tableId=foo.bar.baz, before=[-1, 天地玄黄宇宙洪荒, true, true, 
true, false], after=[], op=DELETE, meta=()}
     DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, null, true, 
false, true, false], op=INSERT, meta=()}
     DataChangeEvent{tableId=foo.bar.baz, before=[0, null, true, false, true, 
false], after=[], op=DELETE, meta=()}
+- do: IsUnknown Op
+  projection: id_, bool_, bool_ IS UNKNOWN AS comp_1, bool_ IS NOT UNKNOWN AS 
comp_2, (bool_ AND CAST(NULL AS BOOLEAN)) IS UNKNOWN AS comp_3
+  primary-key: id_
+  expect: |-
+    CreateTableEvent{tableId=foo.bar.baz, schema=columns={`id_` BIGINT NOT 
NULL 'Identifier',`bool_` BOOLEAN 'George' 'false',`comp_1` BOOLEAN,`comp_2` 
BOOLEAN,`comp_3` BOOLEAN}, primaryKeys=id_, options=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[1, true, false, 
true, true], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[1, true, false, true, true], 
after=[-1, false, false, true, false], op=UPDATE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[-1, false, false, true, 
false], after=[], op=DELETE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, null, true, 
false, true], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[0, null, true, false, true], 
after=[], op=DELETE, meta=()}
+- do: Distinct From Op
+  projection: id_, int_ IS DISTINCT FROM 4 AS comp_1, int_ IS NOT DISTINCT 
FROM 4 AS comp_2, CAST(NULL AS INT) IS DISTINCT FROM int_ AS comp_3, CAST(NULL 
AS INT) IS NOT DISTINCT FROM int_ AS comp_4
+  primary-key: id_
+  expect: |-
+    CreateTableEvent{tableId=foo.bar.baz, schema=columns={`id_` BIGINT NOT 
NULL 'Identifier',`comp_1` BOOLEAN,`comp_2` BOOLEAN,`comp_3` BOOLEAN,`comp_4` 
BOOLEAN}, primaryKeys=id_, options=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[1, false, true, 
true, false], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[1, false, true, true, false], 
after=[-1, true, false, true, false], op=UPDATE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[-1, true, false, true, 
false], after=[], op=DELETE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, true, false, 
false, true], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[0, true, false, false, true], 
after=[], op=DELETE, meta=()}
 - do: Between Op
   projection: |-
     id_
@@ -235,6 +255,77 @@
   ignore: FLINK-38905
   projection: id_, char_ NOT LIKE 'A.*' AS comp_1, varchar_ NOT LIKE '.*rro' 
AS comp_2, string_ NOT LIKE 'From [A-Z] to [A-Z] is Lie' AS comp_3
   primary-key: id_
+- do: String Like Escape Op
+  projection: id_, 'A%' LIKE 'A$%' ESCAPE '$' AS comp_1, 'A_' LIKE 'A$_' 
ESCAPE '$' AS comp_2, char_ LIKE 'A$%' ESCAPE '$' AS comp_3
+  primary-key: id_
+  expect: |-
+    CreateTableEvent{tableId=foo.bar.baz, schema=columns={`id_` BIGINT NOT 
NULL 'Identifier',`comp_1` BOOLEAN,`comp_2` BOOLEAN,`comp_3` BOOLEAN}, 
primaryKeys=id_, options=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[1, true, true, 
false], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[1, true, true, false], 
after=[-1, true, true, false], op=UPDATE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[-1, true, true, false], 
after=[], op=DELETE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, true, true, 
null], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[0, true, true, null], 
after=[], op=DELETE, meta=()}
+- do: String Not Like Escape Op
+  projection: |-
+    id_
+    'A%' NOT LIKE 'A$%' ESCAPE '$' AS comp_1
+    'A_' NOT LIKE 'A$_' ESCAPE '$' AS comp_2
+    char_ NOT LIKE 'A%' ESCAPE '$' AS comp_3
+    CAST(NULL AS STRING) NOT LIKE 'A%' ESCAPE '$' AS comp_4
+    char_ NOT LIKE CAST(NULL AS STRING) ESCAPE '$' AS comp_5
+    char_ NOT LIKE 'A%' ESCAPE CAST(NULL AS STRING) AS comp_6
+  primary-key: id_
+  expect: |-
+    CreateTableEvent{tableId=foo.bar.baz, schema=columns={`id_` BIGINT NOT 
NULL 'Identifier',`comp_1` BOOLEAN,`comp_2` BOOLEAN,`comp_3` BOOLEAN,`comp_4` 
BOOLEAN,`comp_5` BOOLEAN,`comp_6` BOOLEAN}, primaryKeys=id_, options=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[1, false, false, 
false, null, null, null], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[1, false, false, false, null, 
null, null], after=[-1, false, false, true, null, null, null], op=UPDATE, 
meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[-1, false, false, true, null, 
null, null], after=[], op=DELETE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, false, false, 
null, null, null, null], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[0, false, false, null, null, 
null, null], after=[], op=DELETE, meta=()}
+- do: String Similar To Escape Op
+  projection: |-
+    id_
+    'A%' SIMILAR TO 'A$%' ESCAPE '$' AS comp_1
+    'A_' SIMILAR TO 'A$_' ESCAPE '$' AS comp_2
+    char_ SIMILAR TO 'A%' ESCAPE '$' AS comp_3
+    CAST(NULL AS STRING) SIMILAR TO 'A%' ESCAPE '$' AS comp_4
+    char_ SIMILAR TO CAST(NULL AS STRING) ESCAPE '$' AS comp_5
+    char_ SIMILAR TO 'A%' ESCAPE CAST(NULL AS STRING) AS comp_6
+  primary-key: id_
+  expect: |-
+    CreateTableEvent{tableId=foo.bar.baz, schema=columns={`id_` BIGINT NOT 
NULL 'Identifier',`comp_1` BOOLEAN,`comp_2` BOOLEAN,`comp_3` BOOLEAN,`comp_4` 
BOOLEAN,`comp_5` BOOLEAN,`comp_6` BOOLEAN}, primaryKeys=id_, options=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[1, true, true, 
true, null, null, null], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[1, true, true, true, null, 
null, null], after=[-1, true, true, false, null, null, null], op=UPDATE, 
meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[-1, true, true, false, null, 
null, null], after=[], op=DELETE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, true, true, 
null, null, null, null], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[0, true, true, null, null, 
null, null], after=[], op=DELETE, meta=()}
+- do: String Not Similar To Escape Op
+  projection: |-
+    id_
+    'A%' NOT SIMILAR TO 'A$%' ESCAPE '$' AS comp_1
+    'A_' NOT SIMILAR TO 'A$_' ESCAPE '$' AS comp_2
+    char_ NOT SIMILAR TO 'A%' ESCAPE '$' AS comp_3
+    CAST(NULL AS STRING) NOT SIMILAR TO 'A%' ESCAPE '$' AS comp_4
+    char_ NOT SIMILAR TO CAST(NULL AS STRING) ESCAPE '$' AS comp_5
+    char_ NOT SIMILAR TO 'A%' ESCAPE CAST(NULL AS STRING) AS comp_6
+  primary-key: id_
+  expect: |-
+    CreateTableEvent{tableId=foo.bar.baz, schema=columns={`id_` BIGINT NOT 
NULL 'Identifier',`comp_1` BOOLEAN,`comp_2` BOOLEAN,`comp_3` BOOLEAN,`comp_4` 
BOOLEAN,`comp_5` BOOLEAN,`comp_6` BOOLEAN}, primaryKeys=id_, options=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[1, false, false, 
false, null, null, null], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[1, false, false, false, null, 
null, null], after=[-1, false, false, true, null, null, null], op=UPDATE, 
meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[-1, false, false, true, null, 
null, null], after=[], op=DELETE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, false, false, 
null, null, null, null], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[0, false, false, null, null, 
null, null], after=[], op=DELETE, meta=()}
+- do: String Similar To Op
+  projection: id_, char_ SIMILAR TO 'A%' AS comp_1, varchar_ NOT SIMILAR TO 
'%rro' AS comp_2, string_ SIMILAR TO 'From (A|B) to (Y|Z) is Lie' AS comp_3, 
char_ NOT SIMILAR TO 'A%' AS comp_4
+  primary-key: id_
+  expect: |-
+    CreateTableEvent{tableId=foo.bar.baz, schema=columns={`id_` BIGINT NOT 
NULL 'Identifier',`comp_1` BOOLEAN,`comp_2` BOOLEAN,`comp_3` BOOLEAN,`comp_4` 
BOOLEAN}, primaryKeys=id_, options=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[1, true, false, 
true, false], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[1, true, false, true, false], 
after=[-1, false, true, false, true], op=UPDATE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[-1, false, true, false, 
true], after=[], op=DELETE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, null, null, 
null, null], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[0, null, null, null, null], 
after=[], op=DELETE, meta=()}
 - do: In Op
   projection: |-
     id_
diff --git a/flink-cdc-composer/src/test/resources/specs/logical.yaml 
b/flink-cdc-composer/src/test/resources/specs/logical.yaml
index 4f09aa823..d5f80818c 100644
--- a/flink-cdc-composer/src/test/resources/specs/logical.yaml
+++ b/flink-cdc-composer/src/test/resources/specs/logical.yaml
@@ -45,4 +45,25 @@
     bool_ IS FALSE AS comp_8
     bool_ IS NOT FALSE AS comp_9
   primary-key: id_
-  expect-error: 'java.lang.RuntimeException: Failed to evaluate projection 
expression ``TB`.`bool_` OR FALSE` for column `comp_2` in table `foo.bar.baz`.'
+  expect: |-
+    CreateTableEvent{tableId=foo.bar.baz, schema=columns={`id_` BIGINT NOT 
NULL 'Identifier',`bool_` BOOLEAN 'George' 'false',`comp_1` BOOLEAN,`comp_2` 
BOOLEAN,`comp_3` BOOLEAN,`comp_4` BOOLEAN,`comp_5` BOOLEAN,`comp_6` 
BOOLEAN,`comp_7` BOOLEAN,`comp_8` BOOLEAN,`comp_9` BOOLEAN}, primaryKeys=id_, 
options=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[1, true, true, 
true, true, false, false, true, false, false, true], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[1, true, true, true, true, 
false, false, true, false, false, true], after=[-1, false, true, false, false, 
false, true, false, true, true, false], op=UPDATE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[-1, false, true, false, 
false, false, true, false, true, true, false], after=[], op=DELETE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, null, true, 
null, null, false, null, false, true, false, true], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[0, null, true, null, null, 
false, null, false, true, false, true], after=[], op=DELETE, meta=()}
+- do: Logical Operators Short-Circuit
+  projection: |-
+    id_
+    TRUE OR 1 / CAST(0 AS INT) > 0 AS comp_1
+    FALSE AND 1 / CAST(0 AS INT) > 0 AS comp_2
+    CAST(TRUE AS BOOLEAN) OR 1 / CAST(0 AS INT) > 0 AS comp_3
+    CAST(FALSE AS BOOLEAN) AND 1 / CAST(0 AS INT) > 0 AS comp_4
+  primary-key: id_
+  expect: |-
+    CreateTableEvent{tableId=foo.bar.baz, schema=columns={`id_` BIGINT NOT 
NULL 'Identifier',`comp_1` BOOLEAN,`comp_2` BOOLEAN,`comp_3` BOOLEAN,`comp_4` 
BOOLEAN}, primaryKeys=id_, options=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[1, true, false, 
true, false], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[1, true, false, true, false], 
after=[-1, true, false, true, false], op=UPDATE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[-1, true, false, true, 
false], after=[], op=DELETE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, true, false, 
true, false], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[0, true, false, true, false], 
after=[], op=DELETE, meta=()}
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/ComparisonFunctions.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/ComparisonFunctions.java
index 41605ca8b..e318e45e1 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/ComparisonFunctions.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/ComparisonFunctions.java
@@ -27,6 +27,17 @@ public class ComparisonFunctions {
         return (object1 != null && object2 != null) && object1.equals(object2);
     }
 
+    public static boolean isDistinctFrom(Object object1, Object object2) {
+        if (object1 == null || object2 == null) {
+            return object1 != object2;
+        }
+        return !object1.equals(object2);
+    }
+
+    public static boolean isNotDistinctFrom(Object object1, Object object2) {
+        return !isDistinctFrom(object1, object2);
+    }
+
     private static int universalCompares(Object lhs, Object rhs) {
         Class<?> leftClass = lhs.getClass();
         Class<?> rightClass = rhs.getClass();
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/LogicalFunctions.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/LogicalFunctions.java
index 80ae5ccf2..07366bd34 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/LogicalFunctions.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/LogicalFunctions.java
@@ -17,9 +17,83 @@
 
 package org.apache.flink.cdc.runtime.functions.impl;
 
+import java.util.function.Supplier;
+
 /** Logical built-in functions. */
 public class LogicalFunctions {
 
+    public static Boolean and(Boolean left, Boolean right) {
+        if (Boolean.FALSE.equals(left) || Boolean.FALSE.equals(right)) {
+            return false;
+        }
+        if (left == null || right == null) {
+            return null;
+        }
+        return true;
+    }
+
+    public static Boolean or(Boolean left, Boolean right) {
+        if (Boolean.TRUE.equals(left) || Boolean.TRUE.equals(right)) {
+            return true;
+        }
+        if (left == null || right == null) {
+            return null;
+        }
+        return false;
+    }
+
+    public static Boolean and(Boolean left, Supplier<Boolean> rightSupplier) {
+        if (Boolean.FALSE.equals(left)) {
+            return false;
+        }
+        Boolean right = rightSupplier.get();
+        return and(left, right);
+    }
+
+    public static Boolean or(Boolean left, Supplier<Boolean> rightSupplier) {
+        if (Boolean.TRUE.equals(left)) {
+            return true;
+        }
+        Boolean right = rightSupplier.get();
+        return or(left, right);
+    }
+
+    public static Boolean not(Boolean value) {
+        return value == null ? null : !value;
+    }
+
+    public static boolean isNull(Object value) {
+        return value == null;
+    }
+
+    public static boolean isNotNull(Object value) {
+        return value != null;
+    }
+
+    public static boolean isTrue(Boolean value) {
+        return Boolean.TRUE.equals(value);
+    }
+
+    public static boolean isNotTrue(Boolean value) {
+        return !Boolean.TRUE.equals(value);
+    }
+
+    public static boolean isFalse(Boolean value) {
+        return Boolean.FALSE.equals(value);
+    }
+
+    public static boolean isNotFalse(Boolean value) {
+        return !Boolean.FALSE.equals(value);
+    }
+
+    public static boolean isUnknown(Boolean value) {
+        return value == null;
+    }
+
+    public static boolean isNotUnknown(Boolean value) {
+        return value != null;
+    }
+
     public static Object coalesce(Object... objects) {
         for (Object item : objects) {
             if (item != null) {
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/StringFunctions.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/StringFunctions.java
index 39194de2d..7e29f63e7 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/StringFunctions.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/StringFunctions.java
@@ -20,6 +20,7 @@ package org.apache.flink.cdc.runtime.functions.impl;
 import org.apache.flink.cdc.common.types.variant.BinaryVariantInternalBuilder;
 import org.apache.flink.cdc.common.types.variant.Variant;
 
+import org.apache.calcite.runtime.SqlFunctions;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -69,10 +70,43 @@ public class StringFunctions {
         return Pattern.compile(regex).matcher(str).find();
     }
 
+    public static Boolean like(String str, String pattern, String escape) {
+        if (str == null || pattern == null || escape == null) {
+            return null;
+        }
+        return SqlFunctions.like(str, pattern, escape);
+    }
+
     public static boolean notLike(String str, String regex) {
         return !like(str, regex);
     }
 
+    public static Boolean notLike(String str, String pattern, String escape) {
+        return LogicalFunctions.not(like(str, pattern, escape));
+    }
+
+    public static Boolean similarTo(String str, String pattern) {
+        if (str == null || pattern == null) {
+            return null;
+        }
+        return SqlFunctions.similar(str, pattern);
+    }
+
+    public static Boolean similarTo(String str, String pattern, String escape) 
{
+        if (str == null || pattern == null || escape == null) {
+            return null;
+        }
+        return SqlFunctions.similar(str, pattern, escape);
+    }
+
+    public static Boolean notSimilarTo(String str, String pattern) {
+        return LogicalFunctions.not(similarTo(str, pattern));
+    }
+
+    public static Boolean notSimilarTo(String str, String pattern, String 
escape) {
+        return LogicalFunctions.not(similarTo(str, pattern, escape));
+    }
+
     public static String substr(String str, int beginIndex) {
         return substring(str, beginIndex);
     }
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformFilterProcessor.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformFilterProcessor.java
index 77dc5cfe6..56ce510c1 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformFilterProcessor.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformFilterProcessor.java
@@ -115,7 +115,8 @@ public class TransformFilterProcessor {
         }
 
         try {
-            return (Boolean) 
expressionEvaluator.evaluate(generateParams(preRow, postRow, context));
+            return Boolean.TRUE.equals(
+                    expressionEvaluator.evaluate(generateParams(preRow, 
postRow, context)));
         } catch (InvocationTargetException e) {
             throw new RuntimeException(
                     String.format(
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/JaninoCompiler.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/JaninoCompiler.java
index 14c483d70..909c628fd 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/JaninoCompiler.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/JaninoCompiler.java
@@ -28,6 +28,7 @@ import org.apache.flink.cdc.common.types.DataTypeRoot;
 import org.apache.flink.cdc.common.utils.Preconditions;
 import org.apache.flink.cdc.common.utils.StringUtils;
 import 
org.apache.flink.cdc.runtime.operators.transform.UserDefinedFunctionDescriptor;
+import org.apache.flink.cdc.runtime.parser.metadata.MetadataColumns;
 
 import org.apache.calcite.sql.SqlBasicCall;
 import org.apache.calcite.sql.SqlBasicTypeNameSpec;
@@ -230,7 +231,8 @@ public class JaninoCompiler {
             sqlCaseRvalueTemp =
                     new Java.ConditionalExpression(
                             Location.NOWHERE,
-                            whenAtoms.get(i),
+                            generateFunctionOperation(
+                                    "isTrue", new Java.Rvalue[] 
{whenAtoms.get(i)}),
                             thenAtoms.get(i),
                             sqlCaseRvalueTemp);
         }
@@ -258,30 +260,41 @@ public class JaninoCompiler {
             Context context, SqlBasicCall sqlBasicCall, Java.Rvalue[] atoms) {
         switch (sqlBasicCall.getKind()) {
             case AND:
-                return generateBinaryOperation(context, sqlBasicCall, atoms, 
"&&");
+                return generateLogicalBinaryOperation(context, sqlBasicCall, 
atoms, true);
             case OR:
-                return generateBinaryOperation(context, sqlBasicCall, atoms, 
"||");
+                return generateLogicalBinaryOperation(context, sqlBasicCall, 
atoms, false);
             case NOT:
-                return generateUnaryOperation(context, "!", atoms[0]);
+                return generateFunctionOperation("not", atoms);
             case EQUALS:
                 return generateEqualsOperation(context, sqlBasicCall, atoms);
             case NOT_EQUALS:
                 return generateUnaryOperation(
                         context, "!", generateEqualsOperation(context, 
sqlBasicCall, atoms));
+            case IS_DISTINCT_FROM:
+            case IS_NOT_DISTINCT_FROM:
+                return generateOtherFunctionOperation(context, sqlBasicCall, 
atoms);
             case IS_NULL:
-                return generateUnaryOperation(context, "null == ", atoms[0]);
+                return generateFunctionOperation("isNull", atoms);
             case IS_NOT_NULL:
-                return generateUnaryOperation(context, "null != ", atoms[0]);
+                return generateFunctionOperation("isNotNull", atoms);
             case IS_FALSE:
+                return generateFunctionOperation("isFalse", atoms);
             case IS_NOT_TRUE:
-                return generateUnaryOperation(context, "false == ", atoms[0]);
+                return generateFunctionOperation("isNotTrue", atoms);
             case IS_TRUE:
+                return generateFunctionOperation("isTrue", atoms);
             case IS_NOT_FALSE:
-                return generateUnaryOperation(context, "true == ", atoms[0]);
+                return generateFunctionOperation("isNotFalse", atoms);
+            case IS_UNKNOWN:
+                if (sqlBasicCall.getOperator().getName().equalsIgnoreCase("IS 
NOT UNKNOWN")) {
+                    return generateFunctionOperation("isNotUnknown", atoms);
+                }
+                return generateFunctionOperation("isUnknown", atoms);
             case BETWEEN:
             case IN:
             case NOT_IN:
             case LIKE:
+            case SIMILAR:
             case CEIL:
             case FLOOR:
             case TRIM:
@@ -322,6 +335,131 @@ public class JaninoCompiler {
         return new Java.UnaryOperation(Location.NOWHERE, operator, atom);
     }
 
+    private static Java.Rvalue generateFunctionOperation(String functionName, 
Java.Rvalue[] atoms) {
+        return new Java.MethodInvocation(Location.NOWHERE, null, functionName, 
atoms);
+    }
+
+    private static Java.Rvalue generateLazyBinaryFunctionOperation(
+            Context context, SqlBasicCall sqlBasicCall, String functionName, 
Java.Rvalue[] atoms) {
+        if (atoms.length != 2) {
+            throw new ParseException("Unrecognized expression: " + 
sqlBasicCall.toString());
+        }
+        Java.Rvalue rightOperandSupplier =
+                new Java.AmbiguousName(
+                        Location.NOWHERE,
+                        new String[] {
+                            "new java.util.function.Supplier<Boolean>() { 
public Boolean get() { return "
+                                    + atoms[1]
+                                    + "; } }"
+                        });
+        return generateFunctionOperation(
+                functionName, new Java.Rvalue[] {atoms[0], 
rightOperandSupplier});
+    }
+
+    private static Java.Rvalue generateLogicalBinaryOperation(
+            Context context, SqlBasicCall sqlBasicCall, Java.Rvalue[] atoms, 
boolean isAnd) {
+        if (atoms.length != 2) {
+            throw new ParseException("Unrecognized expression: " + 
sqlBasicCall.toString());
+        }
+        boolean leftNullable = isExpressionNullable(context, 
sqlBasicCall.getOperandList().get(0));
+        boolean rightNullable = isExpressionNullable(context, 
sqlBasicCall.getOperandList().get(1));
+        if (!leftNullable && !rightNullable) {
+            return generateBinaryOperation(context, sqlBasicCall, atoms, isAnd 
? "&&" : "||");
+        }
+        if (!leftNullable) {
+            return generateLeftNonNullableLogicalOperation(atoms, isAnd);
+        }
+        // TODO: This nullable-left path still relies on JIT escape analysis 
to optimize
+        // per-evaluation Supplier allocation. Introduce statement-level 
codegen
+        // (for example, GeneratedExpression/ScriptEvaluator) to avoid the 
potential cost.
+        return generateLazyBinaryFunctionOperation(
+                context, sqlBasicCall, isAnd ? "and" : "or", atoms);
+    }
+
+    private static Java.Rvalue generateLeftNonNullableLogicalOperation(
+            Java.Rvalue[] atoms, boolean isAnd) {
+        if (isAnd) {
+            return new Java.ConditionalExpression(
+                    Location.NOWHERE,
+                    atoms[0],
+                    atoms[1],
+                    new Java.AmbiguousName(Location.NOWHERE, new String[] 
{"Boolean.FALSE"}));
+        }
+        return new Java.ConditionalExpression(
+                Location.NOWHERE,
+                atoms[0],
+                new Java.AmbiguousName(Location.NOWHERE, new String[] 
{"Boolean.TRUE"}),
+                atoms[1]);
+    }
+
+    private static boolean isExpressionNullable(Context context, SqlNode 
sqlNode) {
+        if (sqlNode instanceof SqlIdentifier) {
+            return isIdentifierNullable(context, (SqlIdentifier) sqlNode);
+        }
+        if (sqlNode instanceof SqlLiteral) {
+            return ((SqlLiteral) sqlNode).getValue() == null;
+        }
+        if (sqlNode instanceof SqlBasicCall) {
+            return isBasicCallNullable(context, (SqlBasicCall) sqlNode);
+        }
+        return true;
+    }
+
+    private static boolean isIdentifierNullable(Context context, SqlIdentifier 
sqlIdentifier) {
+        String columnName = sqlIdentifier.names.get(sqlIdentifier.names.size() 
- 1);
+        for (Column column : context.columns) {
+            if (column.getName().equals(columnName)) {
+                return column.getType().isNullable();
+            }
+        }
+        for (SupportedMetadataColumn metadataColumn : 
context.supportedMetadataColumns) {
+            if (metadataColumn.getName().equals(columnName)) {
+                return metadataColumn.getType().isNullable();
+            }
+        }
+        return MetadataColumns.METADATA_COLUMNS.stream()
+                .filter(column -> column.f0.equals(columnName))
+                .findFirst()
+                .map(column -> column.f1.isNullable())
+                .orElse(true);
+    }
+
+    private static boolean isBasicCallNullable(Context context, SqlBasicCall 
sqlBasicCall) {
+        switch (sqlBasicCall.getKind()) {
+            case AND:
+            case OR:
+                return sqlBasicCall.getOperandList().stream()
+                        .anyMatch(operand -> isExpressionNullable(context, 
operand));
+            case NOT:
+                return isExpressionNullable(context, 
sqlBasicCall.getOperandList().get(0));
+            case IS_NULL:
+            case IS_NOT_NULL:
+            case IS_FALSE:
+            case IS_NOT_TRUE:
+            case IS_TRUE:
+            case IS_NOT_FALSE:
+            case IS_UNKNOWN:
+            case IS_DISTINCT_FROM:
+            case IS_NOT_DISTINCT_FROM:
+            case EQUALS:
+            case NOT_EQUALS:
+            case LESS_THAN:
+            case GREATER_THAN:
+            case LESS_THAN_OR_EQUAL:
+            case GREATER_THAN_OR_EQUAL:
+            case BETWEEN:
+            case IN:
+            case NOT_IN:
+                return false;
+            case LIKE:
+            case SIMILAR:
+                return sqlBasicCall.getOperandList().stream()
+                        .anyMatch(operand -> isExpressionNullable(context, 
operand));
+            default:
+                return true;
+        }
+    }
+
     private static final Map<String, String> decimalArithmeticHandlers =
             Map.of(
                     "+", "plus",
@@ -517,7 +655,10 @@ public class JaninoCompiler {
         if (operationName.equals("IF")) {
             if (atoms.length == 3) {
                 return new Java.ConditionalExpression(
-                        Location.NOWHERE, atoms[0], atoms[1], atoms[2]);
+                        Location.NOWHERE,
+                        generateFunctionOperation("isTrue", new Java.Rvalue[] 
{atoms[0]}),
+                        atoms[1],
+                        atoms[2]);
             } else {
                 throw new ParseException("Unrecognized expression: " + 
sqlBasicCall);
             }
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/TransformParser.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/TransformParser.java
index 5ee715301..4912ef5a5 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/TransformParser.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/TransformParser.java
@@ -62,6 +62,7 @@ import org.apache.calcite.sql.type.InferTypes;
 import org.apache.calcite.sql.type.OperandTypes;
 import org.apache.calcite.sql.type.SqlReturnTypeInference;
 import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.util.SqlOperatorTables;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
 import org.apache.calcite.sql.validate.SqlValidator;
@@ -285,13 +286,15 @@ public class TransformParser {
         }
 
         expandWildcard(sqlSelect, columns);
-        RelNode relNode = sqlToRel(columns, sqlSelect, udfDescriptors, 
supportedMetadataColumns);
-        RelDataType[] relDataTypes =
-                relNode.getRowType().getFieldList().stream()
-                        .map(RelDataTypeField::getType)
-                        .toArray(RelDataType[]::new);
         Map<String, Column> originalColumnMap =
                 columns.stream().collect(Collectors.toMap(Column::getName, 
column -> column));
+        RelDataType[] relDataTypes =
+                deduceProjectionRelDataTypes(
+                        columns,
+                        originalColumnMap,
+                        sqlSelect,
+                        udfDescriptors,
+                        supportedMetadataColumns);
         List<ProjectionColumn> projectionColumns = new ArrayList<>();
         Map<String, Integer> addedProjectionColumnNames = new HashMap<>();
 
@@ -387,6 +390,146 @@ public class TransformParser {
         return projectionColumns;
     }
 
+    private static RelDataType[] deduceProjectionRelDataTypes(
+            List<Column> columns,
+            Map<String, Column> originalColumnMap,
+            SqlSelect sqlSelect,
+            List<UserDefinedFunctionDescriptor> udfDescriptors,
+            SupportedMetadataColumn[] supportedMetadataColumns) {
+        try {
+            RelNode relNode =
+                    sqlToRel(columns, sqlSelect, udfDescriptors, 
supportedMetadataColumns);
+            return relNode.getRowType().getFieldList().stream()
+                    .map(RelDataTypeField::getType)
+                    .toArray(RelDataType[]::new);
+        } catch (RuntimeException e) {
+            try {
+                // Keep Calcite as the primary type inference path. This 
fallback only covers
+                // transform predicates that Janino can evaluate but Calcite 
may fail to convert
+                // while building projection columns.
+                SqlTypeFactoryImpl typeFactory = new 
SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT);
+                List<RelDataType> relDataTypes = new ArrayList<>();
+                for (SqlNode sqlNode : sqlSelect.getSelectList()) {
+                    relDataTypes.add(
+                            deduceProjectionRelDataType(
+                                    typeFactory,
+                                    columns,
+                                    originalColumnMap,
+                                    unwrapAsExpression(sqlNode),
+                                    udfDescriptors,
+                                    supportedMetadataColumns));
+                }
+                return relDataTypes.toArray(new RelDataType[0]);
+            } catch (RuntimeException fallbackException) {
+                e.addSuppressed(fallbackException);
+                throw e;
+            }
+        }
+    }
+
+    private static SqlNode unwrapAsExpression(SqlNode sqlNode) {
+        if (sqlNode instanceof SqlBasicCall) {
+            SqlBasicCall sqlBasicCall = (SqlBasicCall) sqlNode;
+            if (SqlKind.AS.equals(sqlBasicCall.getOperator().kind)
+                    && !sqlBasicCall.getOperandList().isEmpty()) {
+                return sqlBasicCall.getOperandList().get(0);
+            }
+        }
+        return sqlNode;
+    }
+
+    private static RelDataType deduceProjectionRelDataType(
+            RelDataTypeFactory typeFactory,
+            List<Column> columns,
+            Map<String, Column> originalColumnMap,
+            SqlNode exprNode,
+            List<UserDefinedFunctionDescriptor> udfDescriptors,
+            SupportedMetadataColumn[] supportedMetadataColumns) {
+        if (exprNode instanceof SqlIdentifier) {
+            String columnName =
+                    ((SqlIdentifier) exprNode)
+                            .names.get(((SqlIdentifier) exprNode).names.size() 
- 1);
+            return toRelDataType(
+                    typeFactory,
+                    findIdentifierDataType(
+                            originalColumnMap, columnName, 
supportedMetadataColumns));
+        }
+        if (requiresBooleanTypeFallback(exprNode)) {
+            validateReferencedColumns(originalColumnMap, exprNode, 
supportedMetadataColumns);
+            return typeFactory.createTypeWithNullability(
+                    typeFactory.createSqlType(SqlTypeName.BOOLEAN), true);
+        }
+        return toRelDataType(
+                typeFactory,
+                deduceSubExpressionType(
+                        columns, exprNode, udfDescriptors, 
supportedMetadataColumns));
+    }
+
+    private static DataType findIdentifierDataType(
+            Map<String, Column> originalColumnMap,
+            String columnName,
+            SupportedMetadataColumn[] supportedMetadataColumns) {
+        if (originalColumnMap.containsKey(columnName)) {
+            return originalColumnMap.get(columnName).getType();
+        }
+        for (SupportedMetadataColumn metadataColumn : 
supportedMetadataColumns) {
+            if (metadataColumn.getName().equals(columnName)) {
+                return metadataColumn.getType();
+            }
+        }
+        return METADATA_COLUMNS.stream()
+                .filter(column -> column.f0.equals(columnName))
+                .findFirst()
+                .map(column -> column.f1)
+                .orElseThrow(
+                        () ->
+                                new IllegalArgumentException(
+                                        "Referenced column "
+                                                + columnName
+                                                + " is not present in original 
table."));
+    }
+
+    private static RelDataType toRelDataType(RelDataTypeFactory typeFactory, 
DataType dataType) {
+        return CalciteDataTypeConverter.convertCalciteRelDataType(
+                        typeFactory,
+                        
Collections.singletonList(Column.physicalColumn("__tmp", dataType)))
+                .getFieldList()
+                .get(0)
+                .getType();
+    }
+
+    private static void validateReferencedColumns(
+            Map<String, Column> originalColumnMap,
+            SqlNode exprNode,
+            SupportedMetadataColumn[] supportedMetadataColumns) {
+        for (String columnName : parseColumnNameList(exprNode)) {
+            findIdentifierDataType(originalColumnMap, columnName, 
supportedMetadataColumns);
+        }
+    }
+
+    private static boolean requiresBooleanTypeFallback(SqlNode sqlNode) {
+        if (!(sqlNode instanceof SqlBasicCall)) {
+            return false;
+        }
+        SqlBasicCall sqlBasicCall = (SqlBasicCall) sqlNode;
+        switch (sqlBasicCall.getKind()) {
+            case AND:
+            case OR:
+            case NOT:
+                return sqlBasicCall.getOperandList().stream()
+                        
.anyMatch(TransformParser::requiresBooleanTypeFallback);
+            case IS_DISTINCT_FROM:
+            case IS_NOT_DISTINCT_FROM:
+            case IS_UNKNOWN:
+            case SIMILAR:
+                return true;
+            case LIKE:
+                return sqlBasicCall.getOperandList().size() == 3;
+            default:
+                return false;
+        }
+    }
+
     /**
      * Create a projection column from a simple identifier node (could be an 
upstream physical
      * column or a metadata column).
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/metadata/TransformSqlOperatorTable.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/metadata/TransformSqlOperatorTable.java
index eef0755c1..7a94e0586 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/metadata/TransformSqlOperatorTable.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/metadata/TransformSqlOperatorTable.java
@@ -89,6 +89,9 @@ public class TransformSqlOperatorTable extends 
ReflectiveSqlOperatorTable {
     // --------------------
     public static final SqlBinaryOperator EQUALS = SqlStdOperatorTable.EQUALS;
     public static final SqlBinaryOperator NOT_EQUALS = 
SqlStdOperatorTable.NOT_EQUALS;
+    public static final SqlBinaryOperator IS_DISTINCT_FROM = 
SqlStdOperatorTable.IS_DISTINCT_FROM;
+    public static final SqlBinaryOperator IS_NOT_DISTINCT_FROM =
+            SqlStdOperatorTable.IS_NOT_DISTINCT_FROM;
     public static final SqlBinaryOperator GREATER_THAN = 
SqlStdOperatorTable.GREATER_THAN;
     public static final SqlBinaryOperator GREATER_THAN_OR_EQUAL =
             SqlStdOperatorTable.GREATER_THAN_OR_EQUAL;
@@ -98,12 +101,16 @@ public class TransformSqlOperatorTable extends 
ReflectiveSqlOperatorTable {
 
     public static final SqlPostfixOperator IS_NULL = 
SqlStdOperatorTable.IS_NULL;
     public static final SqlPostfixOperator IS_NOT_NULL = 
SqlStdOperatorTable.IS_NOT_NULL;
+    public static final SqlPostfixOperator IS_UNKNOWN = 
SqlStdOperatorTable.IS_UNKNOWN;
+    public static final SqlPostfixOperator IS_NOT_UNKNOWN = 
SqlStdOperatorTable.IS_NOT_UNKNOWN;
 
     public static final SqlBetweenOperator BETWEEN = 
SqlStdOperatorTable.BETWEEN;
     public static final SqlBetweenOperator NOT_BETWEEN = 
SqlStdOperatorTable.NOT_BETWEEN;
 
     public static final SqlSpecialOperator LIKE = SqlStdOperatorTable.LIKE;
     public static final SqlSpecialOperator NOT_LIKE = 
SqlStdOperatorTable.NOT_LIKE;
+    public static final SqlSpecialOperator SIMILAR_TO = 
SqlStdOperatorTable.SIMILAR_TO;
+    public static final SqlSpecialOperator NOT_SIMILAR_TO = 
SqlStdOperatorTable.NOT_SIMILAR_TO;
 
     public static final SqlBinaryOperator IN = SqlStdOperatorTable.IN;
     public static final SqlBinaryOperator NOT_IN = SqlStdOperatorTable.NOT_IN;
diff --git 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/ComparisonFunctionsTest.java
 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/ComparisonFunctionsTest.java
new file mode 100644
index 000000000..a7a1c3f39
--- /dev/null
+++ 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/ComparisonFunctionsTest.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.cdc.runtime.functions.impl;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Unit tests for {@link ComparisonFunctions}. */
+class ComparisonFunctionsTest {
+
+    @Test
+    void testLegacyNullComparisonBehavior() {
+        assertThat(ComparisonFunctions.valueEquals(null, 1)).isFalse();
+        assertThat(ComparisonFunctions.valueEquals(1, null)).isFalse();
+        assertThat(ComparisonFunctions.lessThan(null, 1)).isFalse();
+        assertThat(ComparisonFunctions.lessThanOrEqual(1, null)).isFalse();
+        assertThat(ComparisonFunctions.greaterThan(null, 1)).isFalse();
+        assertThat(ComparisonFunctions.greaterThanOrEqual(1, null)).isFalse();
+    }
+
+    @Test
+    void testThreeValuedLogicalOperators() {
+        assertThat(LogicalFunctions.and(true, (Boolean) null)).isNull();
+        assertThat(LogicalFunctions.and(false, (Boolean) null)).isFalse();
+        assertThat(LogicalFunctions.or(false, (Boolean) null)).isNull();
+        assertThat(LogicalFunctions.or(true, (Boolean) null)).isTrue();
+        assertThat(LogicalFunctions.not(null)).isNull();
+    }
+
+    @Test
+    void testLogicalOperatorsShortCircuitRightOperand() {
+        assertThat(LogicalFunctions.and(false, () -> 
failIfEvaluated())).isFalse();
+        assertThat(LogicalFunctions.or(true, () -> 
failIfEvaluated())).isTrue();
+    }
+
+    @Test
+    void testLegacyBetweenNullBehavior() {
+        assertThat(ComparisonFunctions.betweenAsymmetric((Integer) null, 1, 
3)).isFalse();
+        assertThat(ComparisonFunctions.notBetweenAsymmetric((Integer) null, 1, 
3)).isTrue();
+    }
+
+    @Test
+    void testLegacyInNullBehavior() {
+        assertThat(ComparisonFunctions.in(1, 1, null)).isTrue();
+        assertThat(ComparisonFunctions.in(1, 2, null)).isFalse();
+        assertThat(ComparisonFunctions.in(null, 1, 2)).isFalse();
+        assertThat(ComparisonFunctions.notIn(1, 2, null)).isTrue();
+    }
+
+    @Test
+    void testDistinctFromNeverReturnsUnknown() {
+        assertThat(ComparisonFunctions.isDistinctFrom(null, null)).isFalse();
+        assertThat(ComparisonFunctions.isDistinctFrom(null, 1)).isTrue();
+        assertThat(ComparisonFunctions.isDistinctFrom(1, 1)).isFalse();
+        assertThat(ComparisonFunctions.isNotDistinctFrom(null, null)).isTrue();
+        assertThat(ComparisonFunctions.isNotDistinctFrom(null, 1)).isFalse();
+    }
+
+    private static Boolean failIfEvaluated() {
+        throw new AssertionError("Right operand should not be evaluated.");
+    }
+}
diff --git 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/StringFunctionsTest.java
 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/StringFunctionsTest.java
new file mode 100644
index 000000000..420b6713f
--- /dev/null
+++ 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/StringFunctionsTest.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.cdc.runtime.functions.impl;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Unit tests for {@link StringFunctions}. */
+class StringFunctionsTest {
+
+    @Test
+    void testLegacyLikeUsesJavaRegex() {
+        assertThat(StringFunctions.like("Alice", "A.*")).isTrue();
+        assertThat(StringFunctions.like("xabcy", "abc")).isTrue();
+        assertThat(StringFunctions.like("Alice", "A%")).isFalse();
+        assertThat(StringFunctions.notLike("Alice", "A.*")).isFalse();
+    }
+
+    @Test
+    void testLikeEscape() {
+        assertThat(StringFunctions.like("A%", "A$%", "$")).isTrue();
+        assertThat(StringFunctions.like("A_", "A$_", "$")).isTrue();
+        assertThat(StringFunctions.like("Alice", "A$%", "$")).isFalse();
+    }
+
+    @Test
+    void testLikeEscapeNullReturnsUnknown() {
+        assertThat(StringFunctions.like("Alice", "A%", null)).isNull();
+    }
+
+    @Test
+    void testSimilarTo() {
+        assertThat(StringFunctions.similarTo("Alice", "(A|B)%")).isTrue();
+        assertThat(StringFunctions.similarTo("Carol", "(A|B)%")).isFalse();
+        assertThat(StringFunctions.notSimilarTo("Alice", "(A|B)%")).isFalse();
+    }
+
+    @Test
+    void testSimilarToNullReturnsUnknown() {
+        assertThat(StringFunctions.similarTo(null, "(A|B)%")).isNull();
+        assertThat(StringFunctions.similarTo("Alice", null)).isNull();
+        assertThat(StringFunctions.notSimilarTo(null, "(A|B)%")).isNull();
+    }
+}
diff --git 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/JaninoCompilerTest.java
 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/JaninoCompilerTest.java
index 413b0dd6f..8298d384a 100644
--- 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/JaninoCompilerTest.java
+++ 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/JaninoCompilerTest.java
@@ -18,6 +18,9 @@
 package org.apache.flink.cdc.runtime.parser;
 
 import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.cdc.common.schema.Column;
+import org.apache.flink.cdc.common.source.SupportedMetadataColumn;
+import org.apache.flink.cdc.common.types.DataTypes;
 import org.apache.flink.cdc.common.types.variant.BinaryVariantInternalBuilder;
 import org.apache.flink.cdc.common.types.variant.Variant;
 import org.apache.flink.cdc.common.types.variant.VariantTypeException;
@@ -39,6 +42,7 @@ import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Stream;
 
 /** Unit tests for the {@link JaninoCompiler}. */
@@ -214,6 +218,75 @@ class JaninoCompilerTest {
                 
.isEqualTo(BinaryVariantInternalBuilder.parseJson(duplicatedNameJsonStr, true));
     }
 
+    @Test
+    void testTranslatedLogicalExpressionCompilesWithJanino() throws 
InvocationTargetException {
+        List<Column> columns =
+                List.of(
+                        Column.physicalColumn("id", DataTypes.INT()),
+                        Column.physicalColumn("uid", DataTypes.INT()));
+        Map<String, String> columnNameMap = Map.of("id", "$0", "uid", "$1");
+
+        ExpressionEvaluator andEvaluator =
+                compileTranslatedFilterExpression("id = 1 and 1 / uid > 0", 
columns, columnNameMap);
+        Assertions.assertThat(andEvaluator.evaluate(new Object[] {2, 
0})).isEqualTo(false);
+
+        ExpressionEvaluator orEvaluator =
+                compileTranslatedFilterExpression("id = 1 or 1 / uid > 0", 
columns, columnNameMap);
+        Assertions.assertThat(orEvaluator.evaluate(new Object[] {1, 
0})).isEqualTo(true);
+
+        List<Column> booleanColumns =
+                List.of(
+                        Column.physicalColumn("left_bool", 
DataTypes.BOOLEAN().notNull()),
+                        Column.physicalColumn("right_bool", 
DataTypes.BOOLEAN().notNull()),
+                        Column.physicalColumn("divisor", DataTypes.INT()));
+        Map<String, String> booleanColumnNameMap =
+                Map.of("left_bool", "$0", "right_bool", "$1", "divisor", "$2");
+        List<String> booleanColumnNames = List.of("$0", "$1", "$2");
+        List<Class<?>> booleanColumnTypes = List.of(Boolean.class, 
Boolean.class, Integer.class);
+
+        ExpressionEvaluator nativeAndEvaluator =
+                compileTranslatedFilterExpression(
+                        "left_bool and right_bool",
+                        booleanColumns,
+                        booleanColumnNameMap,
+                        booleanColumnNames,
+                        booleanColumnTypes);
+        Assertions.assertThat(nativeAndEvaluator.evaluate(new Object[] {true, 
true, 0}))
+                .isEqualTo(true);
+
+        ExpressionEvaluator conditionalAndEvaluator =
+                compileTranslatedFilterExpression(
+                        "left_bool and 1 / divisor > 0",
+                        booleanColumns,
+                        booleanColumnNameMap,
+                        booleanColumnNames,
+                        booleanColumnTypes);
+        Assertions.assertThat(conditionalAndEvaluator.evaluate(new Object[] 
{false, true, 0}))
+                .isEqualTo(false);
+
+        ExpressionEvaluator conditionalOrEvaluator =
+                compileTranslatedFilterExpression(
+                        "left_bool or 1 / divisor > 0",
+                        booleanColumns,
+                        booleanColumnNameMap,
+                        booleanColumnNames,
+                        booleanColumnTypes);
+        Assertions.assertThat(conditionalOrEvaluator.evaluate(new Object[] 
{true, false, 0}))
+                .isEqualTo(true);
+
+        List<Column> nullableBooleanColumns =
+                List.of(Column.physicalColumn("nullable_bool", 
DataTypes.BOOLEAN()));
+        Map<String, String> nullableBooleanColumnNameMap = 
Map.of("nullable_bool", "$0");
+        ExpressionEvaluator nullableOrEvaluator =
+                compileTranslatedFilterExpression(
+                        "nullable_bool or false",
+                        nullableBooleanColumns,
+                        nullableBooleanColumnNameMap,
+                        List.of("$0"),
+                        List.of(Boolean.class));
+        Assertions.assertThat(nullableOrEvaluator.evaluate(new Object[] 
{null})).isNull();
+    }
+
     @Test
     void testLargeNumericLiterals() {
         // Test parsing integer literals
@@ -293,4 +366,34 @@ class JaninoCompilerTest {
                             }
                         });
     }
+
+    private static ExpressionEvaluator compileTranslatedFilterExpression(
+            String expression, List<Column> columns, Map<String, String> 
columnNameMap) {
+        return compileTranslatedFilterExpression(
+                expression,
+                columns,
+                columnNameMap,
+                List.of("$0", "$1"),
+                List.of(Integer.class, Integer.class));
+    }
+
+    private static ExpressionEvaluator compileTranslatedFilterExpression(
+            String expression,
+            List<Column> columns,
+            Map<String, String> columnNameMap,
+            List<String> columnNames,
+            List<Class<?>> columnTypes) {
+        String janinoExpression =
+                TransformParser.translateFilterExpressionToJaninoExpression(
+                        expression,
+                        columns,
+                        Collections.emptyList(),
+                        new SupportedMetadataColumn[0],
+                        columnNameMap);
+        return JaninoCompiler.compileExpression(
+                JaninoCompiler.loadSystemFunction(janinoExpression),
+                columnNames,
+                columnTypes,
+                Boolean.class);
+    }
 }
diff --git 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/TransformParserTest.java
 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/TransformParserTest.java
index a522f4f34..9943ed069 100644
--- 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/TransformParserTest.java
+++ 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/TransformParserTest.java
@@ -172,21 +172,25 @@ class TransformParserTest {
 
     @Test
     void testTranslateFilterToJaninoExpression() {
-        testFilterExpression("id is not null", "null != id");
-        testFilterExpression("id is null", "null == id");
+        testFilterExpression("id is not null", "isNotNull(id)");
+        testFilterExpression("id is null", "isNull(id)");
         testFilterExpression("id = 1 and uid = 2", "valueEquals(id, 1) && 
valueEquals(uid, 2)");
         testFilterExpression("id = 1 or id = 2", "valueEquals(id, 1) || 
valueEquals(id, 2)");
-        testFilterExpression("not (id = 1)", "!valueEquals(id, 1)");
+        testFilterExpression("not (id = 1)", "not(valueEquals(id, 1))");
         testFilterExpression("id = '1'", "valueEquals(id, \"1\")");
         testFilterExpression("id <> '1'", "!valueEquals(id, \"1\")");
+        testFilterExpression("id is distinct from '1'", "isDistinctFrom(id, 
\"1\")");
+        testFilterExpression("id is not distinct from '1'", 
"isNotDistinctFrom(id, \"1\")");
         testFilterExpression("d between d1 and d2", "betweenAsymmetric(d, d1, 
d2)");
         testFilterExpression("d not between d1 and d2", 
"notBetweenAsymmetric(d, d1, d2)");
         testFilterExpression("d in (d1, d2)", "in(d, d1, d2)");
         testFilterExpression("d not in (d1, d2)", "notIn(d, d1, d2)");
-        testFilterExpression("id is false", "false == id");
-        testFilterExpression("id is not false", "true == id");
-        testFilterExpression("id is true", "true == id");
-        testFilterExpression("id is not true", "false == id");
+        testFilterExpression("id is false", "isFalse(id)");
+        testFilterExpression("id is not false", "isNotFalse(id)");
+        testFilterExpression("id is true", "isTrue(id)");
+        testFilterExpression("id is not true", "isNotTrue(id)");
+        testFilterExpression("id is unknown", "isNull(id)");
+        testFilterExpression("id is not unknown", "isNotNull(id)");
         testFilterExpression("a || b", "concat(a, b)");
         testFilterExpression("CHAR_LENGTH(id)", "charLength(id)");
         testFilterExpression("trim(id)", "trim(\"BOTH\", \" \", id)");
@@ -198,6 +202,9 @@ class TransformParserTest {
         testFilterExpression("SUBSTR(a,1)", "substr(a, 1)");
         testFilterExpression("id like '^[a-zA-Z]'", "like(id, \"^[a-zA-Z]\")");
         testFilterExpression("id not like '^[a-zA-Z]'", "notLike(id, 
\"^[a-zA-Z]\")");
+        testFilterExpression("id like 'A$%' escape '$'", "like(id, \"A$%\", 
\"$\")");
+        testFilterExpression("id similar to '(A|B)%'", "similarTo(id, 
\"(A|B)%\")");
+        testFilterExpression("id not similar to '(A|B)%'", "notSimilarTo(id, 
\"(A|B)%\")");
         testFilterExpression("abs(2)", "abs(2)");
         testFilterExpression("ceil(2)", "ceil(2)");
         testFilterExpression("ceiling(2)", "ceil(2)");
@@ -329,7 +336,7 @@ class TransformParserTest {
                 "TIMESTAMPADD(YEAR, 1, dt)", "timestampadd(\"YEAR\", 1, dt, 
__time_zone__)");
         testFilterExpression(
                 "timestampadd(year, 1, dt)", "timestampadd(\"YEAR\", 1, dt, 
__time_zone__)");
-        testFilterExpression("IF(a>b,a,b)", "greaterThan(a, b) ? a : b");
+        testFilterExpression("IF(a>b,a,b)", "isTrue(greaterThan(a, b)) ? a : 
b");
         testFilterExpression("NULLIF(a,b)", "nullif(a, b)");
         testFilterExpression("COALESCE(a,b,c)", "coalesce(a, b, c)");
         testFilterExpression("id + 2", "id + 2");
@@ -348,19 +355,19 @@ class TransformParserTest {
         testFilterExpression("upper(lower(id))", "upper(lower(id))");
         testFilterExpression(
                 "abs(uniq_id) > 10 and id is not null",
-                "greaterThan(abs(uniq_id), 10) && null != id");
+                "greaterThan(abs(uniq_id), 10) && isNotNull(id)");
         testFilterExpression(
                 "case id when 1 then 'a' when 2 then 'b' else 'c' end",
-                "(valueEquals(id, 1) ? \"a\" : valueEquals(id, 2) ? \"b\" : 
\"c\")");
+                "(isTrue(valueEquals(id, 1)) ? \"a\" : isTrue(valueEquals(id, 
2)) ? \"b\" : \"c\")");
         testFilterExpression(
                 "case when id = 1 then 'a' when id = 2 then 'b' else 'c' end",
-                "(valueEquals(id, 1) ? \"a\" : valueEquals(id, 2) ? \"b\" : 
\"c\")");
+                "(isTrue(valueEquals(id, 1)) ? \"a\" : isTrue(valueEquals(id, 
2)) ? \"b\" : \"c\")");
         testFilterExpression(
                 "case id when 1 then 'a' when 2 then 'b' else 'c' end",
-                "(valueEquals(id, 1) ? \"a\" : valueEquals(id, 2) ? \"b\" : 
\"c\")");
+                "(isTrue(valueEquals(id, 1)) ? \"a\" : isTrue(valueEquals(id, 
2)) ? \"b\" : \"c\")");
         testFilterExpression(
                 "case when id = 1 then 'a' when id = 2 then 'b' else 'c' end",
-                "(valueEquals(id, 1) ? \"a\" : valueEquals(id, 2) ? \"b\" : 
\"c\")");
+                "(isTrue(valueEquals(id, 1)) ? \"a\" : isTrue(valueEquals(id, 
2)) ? \"b\" : \"c\")");
         testFilterExpression("cast(id||'0' as int)", "castToInteger(concat(id, 
\"0\"))");
         testFilterExpression("cast(1 as string)", "castToString(1)");
         testFilterExpression("cast(1 as boolean)", "castToBoolean(1)");
@@ -391,6 +398,38 @@ class TransformParserTest {
         testFilterExpression("try_parse_json(jsonStr)", 
"tryParseJson(jsonStr)");
     }
 
+    @Test
+    void testTranslateLogicalFilterToJaninoExpressionByNullability() {
+        List<Column> columns =
+                List.of(
+                        Column.physicalColumn("left_bool", 
DataTypes.BOOLEAN().notNull()),
+                        Column.physicalColumn("right_bool", 
DataTypes.BOOLEAN().notNull()),
+                        Column.physicalColumn("nullable_bool", 
DataTypes.BOOLEAN()));
+
+        testFilterExpressionWithColumns(
+                "left_bool and right_bool", "left_bool && right_bool", 
columns);
+        testFilterExpressionWithColumns(
+                "left_bool or right_bool", "left_bool || right_bool", columns);
+        testFilterExpressionWithColumns(
+                "left_bool and nullable_bool",
+                "left_bool ? nullable_bool : Boolean.FALSE",
+                columns);
+        testFilterExpressionWithColumns(
+                "left_bool or nullable_bool", "left_bool ? Boolean.TRUE : 
nullable_bool", columns);
+        testFilterExpressionWithColumns(
+                "nullable_bool and right_bool",
+                lazyLogicalFunction("and", "nullable_bool", "right_bool"),
+                columns);
+        testFilterExpressionWithColumns(
+                "nullable_bool or right_bool",
+                lazyLogicalFunction("or", "nullable_bool", "right_bool"),
+                columns);
+        testFilterExpressionWithColumns(
+                "nullable_bool or false",
+                lazyLogicalFunction("or", "nullable_bool", "false"),
+                columns);
+    }
+
     @Test
     public void testTranslateItemAccessToJaninoExpression() {
         // Test collection access functions (ARRAY, MAP) with proper column 
schema
@@ -596,12 +635,12 @@ class TransformParserTest {
                         "ProjectionColumn{column=`id` INT 'id', 
expression='id', scriptExpression='$0', originalColumnNames=[id], 
columnNameMap={id=$0}}",
                         "ProjectionColumn{column=`name2` STRING, 
expression='UPPER(`TB`.`name`)', scriptExpression='upper($0)', 
originalColumnNames=[name], columnNameMap={name=$0}}",
                         "ProjectionColumn{column=`sex2` STRING, 
expression='UPPER(`TB`.`sex`)', scriptExpression='upper($0)', 
originalColumnNames=[sex], columnNameMap={sex=$0}}",
-                        "ProjectionColumn{column=`address2` BINARY(50), 
expression='CASE WHEN `TB`.`address` IS NOT NULL THEN `TB`.`address` ELSE 
`TB`.`address` END', scriptExpression='(null != $0 ? $0 : $0)', 
originalColumnNames=[address, address, address], columnNameMap={address=$0}}",
-                        "ProjectionColumn{column=`phone2` VARBINARY(50), 
expression='CASE WHEN `TB`.`phone` IS NOT NULL THEN `TB`.`phone` ELSE 
`TB`.`phone` END', scriptExpression='(null != $0 ? $0 : $0)', 
originalColumnNames=[phone, phone, phone], columnNameMap={phone=$0}}",
-                        "ProjectionColumn{column=`deposit2` DECIMAL(10, 2), 
expression='CASE WHEN `TB`.`deposit` IS NOT NULL THEN `TB`.`deposit` ELSE 
`TB`.`deposit` END', scriptExpression='(null != $0 ? $0 : $0)', 
originalColumnNames=[deposit, deposit, deposit], columnNameMap={deposit=$0}}",
-                        "ProjectionColumn{column=`birthday2` TIMESTAMP(3), 
expression='CASE WHEN `TB`.`birthday` IS NOT NULL THEN `TB`.`birthday` ELSE 
`TB`.`birthday` END', scriptExpression='(null != $0 ? $0 : $0)', 
originalColumnNames=[birthday, birthday, birthday], 
columnNameMap={birthday=$0}}",
-                        "ProjectionColumn{column=`birthday_ltz2` 
TIMESTAMP_LTZ(3), expression='CASE WHEN `TB`.`birthday_ltz` IS NOT NULL THEN 
`TB`.`birthday_ltz` ELSE `TB`.`birthday_ltz` END', scriptExpression='(null != 
$0 ? $0 : $0)', originalColumnNames=[birthday_ltz, birthday_ltz, birthday_ltz], 
columnNameMap={birthday_ltz=$0}}",
-                        "ProjectionColumn{column=`update_time2` TIME(3), 
expression='CASE WHEN `TB`.`update_time` IS NOT NULL THEN `TB`.`update_time` 
ELSE `TB`.`update_time` END', scriptExpression='(null != $0 ? $0 : $0)', 
originalColumnNames=[update_time, update_time, update_time], 
columnNameMap={update_time=$0}}");
+                        "ProjectionColumn{column=`address2` BINARY(50), 
expression='CASE WHEN `TB`.`address` IS NOT NULL THEN `TB`.`address` ELSE 
`TB`.`address` END', scriptExpression='(isTrue(isNotNull($0)) ? $0 : $0)', 
originalColumnNames=[address, address, address], columnNameMap={address=$0}}",
+                        "ProjectionColumn{column=`phone2` VARBINARY(50), 
expression='CASE WHEN `TB`.`phone` IS NOT NULL THEN `TB`.`phone` ELSE 
`TB`.`phone` END', scriptExpression='(isTrue(isNotNull($0)) ? $0 : $0)', 
originalColumnNames=[phone, phone, phone], columnNameMap={phone=$0}}",
+                        "ProjectionColumn{column=`deposit2` DECIMAL(10, 2), 
expression='CASE WHEN `TB`.`deposit` IS NOT NULL THEN `TB`.`deposit` ELSE 
`TB`.`deposit` END', scriptExpression='(isTrue(isNotNull($0)) ? $0 : $0)', 
originalColumnNames=[deposit, deposit, deposit], columnNameMap={deposit=$0}}",
+                        "ProjectionColumn{column=`birthday2` TIMESTAMP(3), 
expression='CASE WHEN `TB`.`birthday` IS NOT NULL THEN `TB`.`birthday` ELSE 
`TB`.`birthday` END', scriptExpression='(isTrue(isNotNull($0)) ? $0 : $0)', 
originalColumnNames=[birthday, birthday, birthday], 
columnNameMap={birthday=$0}}",
+                        "ProjectionColumn{column=`birthday_ltz2` 
TIMESTAMP_LTZ(3), expression='CASE WHEN `TB`.`birthday_ltz` IS NOT NULL THEN 
`TB`.`birthday_ltz` ELSE `TB`.`birthday_ltz` END', 
scriptExpression='(isTrue(isNotNull($0)) ? $0 : $0)', 
originalColumnNames=[birthday_ltz, birthday_ltz, birthday_ltz], 
columnNameMap={birthday_ltz=$0}}",
+                        "ProjectionColumn{column=`update_time2` TIME(3), 
expression='CASE WHEN `TB`.`update_time` IS NOT NULL THEN `TB`.`update_time` 
ELSE `TB`.`update_time` END', scriptExpression='(isTrue(isNotNull($0)) ? $0 : 
$0)', originalColumnNames=[update_time, update_time, update_time], 
columnNameMap={update_time=$0}}");
         Assertions.assertThat(result).hasToString("[" + String.join(", ", 
expected) + "]");
     }
 
@@ -887,6 +926,13 @@ class TransformParserTest {
         Assertions.assertThat(janinoExpression).isEqualTo(expressionExpect);
     }
 
+    private static String lazyLogicalFunction(
+            String functionName, String leftOperand, String rightOperand) {
+        return String.format(
+                "%s(%s, new java.util.function.Supplier<Boolean>() { public 
Boolean get() { return %s; } })",
+                functionName, leftOperand, rightOperand);
+    }
+
     private void testFilterExpressionWithUdf(String expression, String 
expressionExpect) {
         testFilterExpressionWithUdf(
                 expression, expressionExpect, DUMMY_COLUMNS, 
Collections.emptyMap());

Reply via email to