@@ -760,6 +760,13 @@ static void expr_free(cbm_expr_t *e) {
760760 safe_str_free (& cur -> cond .in_values [i ]);
761761 }
762762 free (cur -> cond .in_values );
763+ safe_str_free (& cur -> cond .func );
764+ for (int i = 0 ; i < cur -> cond .arg_count ; i ++ ) {
765+ safe_str_free (& cur -> cond .args [i ].variable );
766+ safe_str_free (& cur -> cond .args [i ].property );
767+ safe_str_free (& cur -> cond .args [i ].literal );
768+ }
769+ free (cur -> cond .args );
763770 }
764771 if (cur -> right ) {
765772 if (top < EXPR_FREE_STACK ) {
@@ -838,6 +845,37 @@ static const char *unsupported_clause_error(cbm_token_type_t type) {
838845
839846/* Forward declarations for recursive descent */
840847static cbm_expr_t * parse_or_expr (parser_t * p );
848+ /* Multi-arg scalar function support, shared with the RETURN-item parser (#874) */
849+ static bool is_multiarg_func_call (parser_t * p );
850+ static int parse_multiarg_func_item (parser_t * p , cbm_return_item_t * item );
851+
852+ /* Free a multi-arg function argument array. */
853+ static void func_args_free (cbm_func_arg_t * args , int count ) {
854+ for (int i = 0 ; i < count ; i ++ ) {
855+ safe_str_free (& args [i ].variable );
856+ safe_str_free (& args [i ].property );
857+ safe_str_free (& args [i ].literal );
858+ }
859+ free (args );
860+ }
861+
862+ /* Free the fields of a partially-parsed multi-arg function item. */
863+ static void func_item_fields_free (cbm_return_item_t * item ) {
864+ safe_str_free (& item -> variable );
865+ safe_str_free (& item -> property );
866+ safe_str_free (& item -> func );
867+ func_args_free (item -> args , item -> arg_count );
868+ item -> args = NULL ;
869+ item -> arg_count = 0 ;
870+ }
871+
872+ /* Free the function-call fields of a WHERE condition (#874). */
873+ static void cond_func_fields_free (cbm_condition_t * c ) {
874+ safe_str_free (& c -> func );
875+ func_args_free (c -> args , c -> arg_count );
876+ c -> args = NULL ;
877+ c -> arg_count = 0 ;
878+ }
841879
842880/* Parse IN [val, val, ...] list. Returns expr_leaf or NULL on error. */
843881static cbm_expr_t * parse_in_list (parser_t * p , cbm_condition_t * c ) {
@@ -1008,11 +1046,9 @@ static cbm_expr_t *parse_exists_predicate(parser_t *p, bool negated) {
10081046 return expr_leaf (c );
10091047}
10101048
1011- static bool cyp_ci_eq (const char * a , const char * b );
1012-
10131049/* 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 . */
1050+ * (var[.prop] and multi-arg functions like coalesce(... )): IS [NOT] NULL,
1051+ * IN [...], or a comparison operator with a literal value. */
10161052static cbm_expr_t * parse_condition_op (parser_t * p , cbm_condition_t * c ) {
10171053 /* IS NULL / IS NOT NULL */
10181054 if (check (p , TOK_IS )) {
@@ -1029,13 +1065,18 @@ static cbm_expr_t *parse_condition_op(parser_t *p, cbm_condition_t *c) {
10291065
10301066 /* IN [...] */
10311067 if (check (p , TOK_IN )) {
1032- return parse_in_list (p , c );
1068+ cbm_expr_t * e = parse_in_list (p , c );
1069+ if (!e ) {
1070+ cond_func_fields_free (c );
1071+ }
1072+ return e ;
10331073 }
10341074
10351075 /* Standard operators */
10361076 c -> op = parse_comparison_op (p );
10371077 if (!c -> op ) {
10381078 snprintf (p -> error , sizeof (p -> error ), "unexpected operator at pos %d" , peek (p )-> pos );
1079+ cond_func_fields_free (c );
10391080 safe_str_free (& c -> variable );
10401081 safe_str_free (& c -> property );
10411082 safe_str_free (& c -> coalesce_default );
@@ -1053,6 +1094,7 @@ static cbm_expr_t *parse_condition_op(parser_t *p, cbm_condition_t *c) {
10531094 c -> value = heap_strdup ("false" );
10541095 } else {
10551096 snprintf (p -> error , sizeof (p -> error ), "expected value at pos %d" , peek (p )-> pos );
1097+ cond_func_fields_free (c );
10561098 safe_str_free (& c -> variable );
10571099 safe_str_free (& c -> property );
10581100 safe_str_free (& c -> op );
@@ -1063,82 +1105,94 @@ static cbm_expr_t *parse_condition_op(parser_t *p, cbm_condition_t *c) {
10631105 return expr_leaf (* c );
10641106}
10651107
1066- static cbm_expr_t * parse_condition_expr (parser_t * p ) {
1067- /* Check for NOT prefix at condition level (e.g. NOT n.name CONTAINS "x") */
1068- bool negated = match (p , TOK_NOT );
1069-
1070- /* EXISTS { pattern } predicate (anchored single-hop existence). */
1071- if (check (p , TOK_EXISTS )) {
1072- return parse_exists_predicate (p , negated );
1108+ /* parse_condition_lhs result: the label-test form is a complete condition. */
1109+ enum { COND_LHS_COMPLETE = 1 };
1110+
1111+ /* Parse the left-hand side of a WHERE condition into c.
1112+ * Returns CBM_NOT_FOUND on error, 0 when an operator/value should follow, and
1113+ * COND_LHS_COMPLETE when the condition is already complete (label test). */
1114+ static int parse_condition_lhs (parser_t * p , cbm_condition_t * c ) {
1115+ if (is_multiarg_func_call (p )) {
1116+ /* Multi-arg scalar function LHS: coalesce(f.depth, 0) >= 2 (#874).
1117+ * Reuse the RETURN-item parser, then move ownership into the condition. */
1118+ cbm_return_item_t fitem ;
1119+ memset (& fitem , 0 , sizeof (fitem ));
1120+ if (parse_multiarg_func_item (p , & fitem ) < 0 ) {
1121+ func_item_fields_free (& fitem );
1122+ return CBM_NOT_FOUND ;
1123+ }
1124+ c -> variable = fitem .variable ;
1125+ c -> property = fitem .property ;
1126+ c -> func = fitem .func ;
1127+ c -> args = fitem .args ;
1128+ c -> arg_count = fitem .arg_count ;
1129+ return 0 ;
10731130 }
10741131
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 );
1132+ if (check (p , TOK_IDENT ) && p -> pos + SKIP_ONE < p -> count &&
1133+ p -> tokens [p -> pos + SKIP_ONE ].type == TOK_LPAREN ) {
1134+ /* Unrecognised function call in WHERE — fail loudly with the supported
1135+ * set instead of the misleading "unexpected operator" (#874). */
1136+ snprintf (p -> error , sizeof (p -> error ),
1137+ "unsupported function '%s' in WHERE (supported: coalesce, substring, replace, "
1138+ "left, right)" ,
1139+ peek (p )-> text );
1140+ return CBM_NOT_FOUND ;
11071141 }
11081142
11091143 const cbm_token_t * var = expect (p , TOK_IDENT );
11101144 if (!var ) {
1111- return NULL ;
1145+ return CBM_NOT_FOUND ;
11121146 }
11131147
1114- cbm_condition_t c = {0 };
1115- c .negated = negated ;
1116-
11171148 /* Label test: WHERE n:Label (openCypher, #241). Modelled as a leaf with
11181149 * op="HAS_LABEL" and value=Label, evaluated against the bound node's label. */
11191150 if (check (p , TOK_COLON )) {
11201151 advance (p );
11211152 const cbm_token_t * lbl = expect (p , TOK_IDENT );
11221153 if (!lbl ) {
1123- return NULL ;
1154+ return CBM_NOT_FOUND ;
11241155 }
1125- c . variable = heap_strdup (var -> text );
1126- c . op = heap_strdup ("HAS_LABEL" );
1127- c . value = heap_strdup (lbl -> text );
1128- return expr_leaf ( c ) ;
1156+ c -> variable = heap_strdup (var -> text );
1157+ c -> op = heap_strdup ("HAS_LABEL" );
1158+ c -> value = heap_strdup (lbl -> text );
1159+ return COND_LHS_COMPLETE ;
11291160 }
11301161
11311162 if (match (p , TOK_DOT )) {
11321163 const cbm_token_t * prop = expect (p , TOK_IDENT );
11331164 if (!prop ) {
1134- return NULL ;
1165+ return CBM_NOT_FOUND ;
11351166 }
1136- c . variable = heap_strdup (var -> text );
1137- c . property = heap_strdup (prop -> text );
1167+ c -> variable = heap_strdup (var -> text );
1168+ c -> property = heap_strdup (prop -> text );
11381169 } else {
11391170 /* No dot: bare alias (e.g. post-WITH variable like "cnt") */
1140- c .variable = heap_strdup (var -> text );
1141- c .property = NULL ;
1171+ c -> variable = heap_strdup (var -> text );
1172+ c -> property = NULL ;
1173+ }
1174+ return 0 ;
1175+ }
1176+
1177+ static cbm_expr_t * parse_condition_expr (parser_t * p ) {
1178+ /* Check for NOT prefix at condition level (e.g. NOT n.name CONTAINS "x") */
1179+ bool negated = match (p , TOK_NOT );
1180+
1181+ /* EXISTS { pattern } predicate (anchored single-hop existence). */
1182+ if (check (p , TOK_EXISTS )) {
1183+ return parse_exists_predicate (p , negated );
1184+ }
1185+
1186+ cbm_condition_t c = {0 };
1187+ c .negated = negated ;
1188+
1189+ int lhs_rc = parse_condition_lhs (p , & c );
1190+ if (lhs_rc < 0 ) {
1191+ return NULL ;
1192+ }
1193+ if (lhs_rc > 0 ) {
1194+ /* HAS_LABEL leaf — complete condition, no operator follows */
1195+ return expr_leaf (c );
11421196 }
11431197
11441198 return parse_condition_op (p , & c );
@@ -1995,6 +2049,8 @@ static void free_where(cbm_where_clause_t *w) {
19952049 safe_str_free (& w -> conditions [i ].in_values [j ]);
19962050 }
19972051 free (w -> conditions [i ].in_values );
2052+ safe_str_free (& w -> conditions [i ].func );
2053+ func_args_free (w -> conditions [i ].args , w -> conditions [i ].arg_count );
19982054 }
19992055 free (w -> conditions );
20002056 safe_str_free (& w -> op );
@@ -2384,8 +2440,26 @@ static void binding_set(binding_t *b, const char *var, const cbm_node_t *node) {
23842440 b -> var_count ++ ;
23852441}
23862442
2443+ static const char * eval_multiarg_func (binding_t * b , const cbm_return_item_t * item , char * buf ,
2444+ size_t bufsz );
2445+
23872446/* Resolve the actual property value for a condition from a binding */
23882447static const char * resolve_condition_value (const cbm_condition_t * c , binding_t * b ) {
2448+ /* Multi-arg scalar function LHS: coalesce(f.depth, 0) >= 2 (#874).
2449+ * Evaluated through the same code path as RETURN projections. The value is
2450+ * consumed by eval_condition before any other condition is resolved, so a
2451+ * single thread-local buffer per call is safe. */
2452+ if (c -> func ) {
2453+ static _Thread_local char func_buf [CBM_SZ_512 ];
2454+ cbm_return_item_t item = {0 };
2455+ item .variable = c -> variable ;
2456+ item .property = c -> property ;
2457+ item .func = c -> func ;
2458+ item .args = c -> args ;
2459+ item .arg_count = c -> arg_count ;
2460+ return eval_multiarg_func (b , & item , func_buf , sizeof (func_buf ));
2461+ }
2462+
23892463 cbm_edge_t * e = binding_get_edge (b , c -> variable );
23902464 if (e ) {
23912465 return edge_prop (e , c -> property );
0 commit comments