@@ -755,6 +755,7 @@ static void expr_free(cbm_expr_t *e) {
755755 safe_str_free (& cur -> cond .property );
756756 safe_str_free (& cur -> cond .op );
757757 safe_str_free (& cur -> cond .value );
758+ safe_str_free (& cur -> cond .coalesce_default );
758759 for (int i = 0 ; i < cur -> cond .in_value_count ; i ++ ) {
759760 safe_str_free (& cur -> cond .in_values [i ]);
760761 }
@@ -1007,6 +1008,61 @@ static cbm_expr_t *parse_exists_predicate(parser_t *p, bool negated) {
10071008 return expr_leaf (c );
10081009}
10091010
1011+ static bool cyp_ci_eq (const char * a , const char * b );
1012+
1013+ /* Parse the operator + value tail shared by every condition subject
1014+ * (var[.prop] and coalesce(var.prop, literal)): IS [NOT] NULL, IN [...],
1015+ * or a comparison operator with a literal value. Consumes/frees *c. */
1016+ static cbm_expr_t * parse_condition_op (parser_t * p , cbm_condition_t * c ) {
1017+ /* IS NULL / IS NOT NULL */
1018+ if (check (p , TOK_IS )) {
1019+ advance (p );
1020+ if (match (p , TOK_NOT )) {
1021+ c -> op = heap_strdup ("IS NOT NULL" );
1022+ expect (p , TOK_NULL_KW );
1023+ } else {
1024+ expect (p , TOK_NULL_KW );
1025+ c -> op = heap_strdup ("IS NULL" );
1026+ }
1027+ return expr_leaf (* c );
1028+ }
1029+
1030+ /* IN [...] */
1031+ if (check (p , TOK_IN )) {
1032+ return parse_in_list (p , c );
1033+ }
1034+
1035+ /* Standard operators */
1036+ c -> op = parse_comparison_op (p );
1037+ if (!c -> op ) {
1038+ snprintf (p -> error , sizeof (p -> error ), "unexpected operator at pos %d" , peek (p )-> pos );
1039+ safe_str_free (& c -> variable );
1040+ safe_str_free (& c -> property );
1041+ safe_str_free (& c -> coalesce_default );
1042+ return NULL ;
1043+ }
1044+
1045+ /* Value */
1046+ if (check (p , TOK_STRING ) || check (p , TOK_NUMBER )) {
1047+ c -> value = heap_strdup (advance (p )-> text );
1048+ } else if (check (p , TOK_TRUE )) {
1049+ advance (p );
1050+ c -> value = heap_strdup ("true" );
1051+ } else if (check (p , TOK_FALSE )) {
1052+ advance (p );
1053+ c -> value = heap_strdup ("false" );
1054+ } else {
1055+ snprintf (p -> error , sizeof (p -> error ), "expected value at pos %d" , peek (p )-> pos );
1056+ safe_str_free (& c -> variable );
1057+ safe_str_free (& c -> property );
1058+ safe_str_free (& c -> op );
1059+ safe_str_free (& c -> coalesce_default );
1060+ return NULL ;
1061+ }
1062+
1063+ return expr_leaf (* c );
1064+ }
1065+
10101066static cbm_expr_t * parse_condition_expr (parser_t * p ) {
10111067 /* Check for NOT prefix at condition level (e.g. NOT n.name CONTAINS "x") */
10121068 bool negated = match (p , TOK_NOT );
@@ -1016,6 +1072,40 @@ static cbm_expr_t *parse_condition_expr(parser_t *p) {
10161072 return parse_exists_predicate (p , negated );
10171073 }
10181074
1075+ /* coalesce(var.prop, <literal>) as a null-safe condition subject (#874).
1076+ * The default literal substitutes a missing/empty property at eval time;
1077+ * any comparison operator may follow, exactly as with a bare var.prop. */
1078+ if (check (p , TOK_IDENT ) && cyp_ci_eq (peek (p )-> text , "coalesce" ) &&
1079+ p -> pos + SKIP_ONE < p -> count && p -> tokens [p -> pos + SKIP_ONE ].type == TOK_LPAREN ) {
1080+ advance (p ); /* coalesce */
1081+ advance (p ); /* ( */
1082+ const cbm_token_t * cvar = expect (p , TOK_IDENT );
1083+ if (!cvar || !expect (p , TOK_DOT )) {
1084+ return NULL ;
1085+ }
1086+ const cbm_token_t * cprop = expect (p , TOK_IDENT );
1087+ if (!cprop || !expect (p , TOK_COMMA )) {
1088+ return NULL ;
1089+ }
1090+ if (!check (p , TOK_STRING ) && !check (p , TOK_NUMBER )) {
1091+ return NULL ; /* supported form: coalesce(var.prop, literal) */
1092+ }
1093+ const cbm_token_t * cdef = peek (p );
1094+ cbm_condition_t cc = {0 };
1095+ cc .negated = negated ;
1096+ cc .variable = heap_strdup (cvar -> text );
1097+ cc .property = heap_strdup (cprop -> text );
1098+ cc .coalesce_default = heap_strdup (cdef -> text );
1099+ advance (p );
1100+ if (!expect (p , TOK_RPAREN )) {
1101+ safe_str_free (& cc .variable );
1102+ safe_str_free (& cc .property );
1103+ safe_str_free (& cc .coalesce_default );
1104+ return NULL ;
1105+ }
1106+ return parse_condition_op (p , & cc );
1107+ }
1108+
10191109 const cbm_token_t * var = expect (p , TOK_IDENT );
10201110 if (!var ) {
10211111 return NULL ;
@@ -1051,51 +1141,7 @@ static cbm_expr_t *parse_condition_expr(parser_t *p) {
10511141 c .property = NULL ;
10521142 }
10531143
1054- /* IS NULL / IS NOT NULL */
1055- if (check (p , TOK_IS )) {
1056- advance (p );
1057- if (match (p , TOK_NOT )) {
1058- c .op = heap_strdup ("IS NOT NULL" );
1059- expect (p , TOK_NULL_KW );
1060- } else {
1061- expect (p , TOK_NULL_KW );
1062- c .op = heap_strdup ("IS NULL" );
1063- }
1064- return expr_leaf (c );
1065- }
1066-
1067- /* IN [...] */
1068- if (check (p , TOK_IN )) {
1069- return parse_in_list (p , & c );
1070- }
1071-
1072- /* Standard operators */
1073- c .op = parse_comparison_op (p );
1074- if (!c .op ) {
1075- snprintf (p -> error , sizeof (p -> error ), "unexpected operator at pos %d" , peek (p )-> pos );
1076- safe_str_free (& c .variable );
1077- safe_str_free (& c .property );
1078- return NULL ;
1079- }
1080-
1081- /* Value */
1082- if (check (p , TOK_STRING ) || check (p , TOK_NUMBER )) {
1083- c .value = heap_strdup (advance (p )-> text );
1084- } else if (check (p , TOK_TRUE )) {
1085- advance (p );
1086- c .value = heap_strdup ("true" );
1087- } else if (check (p , TOK_FALSE )) {
1088- advance (p );
1089- c .value = heap_strdup ("false" );
1090- } else {
1091- snprintf (p -> error , sizeof (p -> error ), "expected value at pos %d" , peek (p )-> pos );
1092- safe_str_free (& c .variable );
1093- safe_str_free (& c .property );
1094- safe_str_free (& c .op );
1095- return NULL ;
1096- }
1097-
1098- return expr_leaf (c );
1144+ return parse_condition_op (p , & c );
10991145}
11001146
11011147/* Atom: ( expr ) | condition */
@@ -2444,6 +2490,11 @@ static bool eval_condition(const cbm_condition_t *c, binding_t *b) {
24442490 }
24452491
24462492 const char * actual = resolve_condition_value (c , b );
2493+ /* coalesce(var.prop, literal) (#874): a missing/empty property value
2494+ * falls back to the literal default before the operator runs. */
2495+ if (c -> coalesce_default && (!actual || actual [0 ] == '\0' )) {
2496+ actual = c -> coalesce_default ;
2497+ }
24472498 if (!actual ) {
24482499 return true;
24492500 }
0 commit comments