Skip to content

Commit b0aead2

Browse files
authored
Merge pull request DeusData#991 from harshitaajoshi/fix/cypher-coalesce-where-874
fix(cypher): support coalesce() and other multi-arg scalar functions in WHERE (DeusData#874)
2 parents e2d41d9 + 57cd3f9 commit b0aead2

3 files changed

Lines changed: 266 additions & 65 deletions

File tree

‎src/cypher/cypher.c‎

Lines changed: 132 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -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 */
840847
static 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. */
843881
static 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. */
10161052
static 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 */
23882447
static 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);

‎src/cypher/cypher.h‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,13 @@ typedef struct {
181181
int rel_count;
182182
} cbm_pattern_t;
183183

184+
/* One argument to a multi-argument scalar function (coalesce, substring, ...). */
185+
typedef struct {
186+
const char *variable; /* variable reference (NULL if a literal) */
187+
const char *property; /* property of the variable (NULL if whole var / literal) */
188+
const char *literal; /* literal string/number text (NULL if a variable ref) */
189+
} cbm_func_arg_t;
190+
184191
/* WHERE condition */
185192
typedef struct {
186193
const char *variable;
@@ -198,6 +205,11 @@ typedef struct {
198205
* anchor, `value` the edge type (NULL = any), `exists_dir` the direction
199206
* (0 = outbound, 1 = inbound, 2 = any). */
200207
int exists_dir;
208+
/* Multi-arg scalar function on the LHS, e.g. coalesce(f.depth, 0) >= 2
209+
* (#874). NULL func = plain variable/property LHS. */
210+
const char *func;
211+
cbm_func_arg_t *args;
212+
int arg_count;
201213
} cbm_condition_t;
202214

203215
/* Expression tree for WHERE clause */
@@ -237,13 +249,6 @@ typedef struct {
237249
const char *else_val; /* NULL if no ELSE */
238250
} cbm_case_expr_t;
239251

240-
/* One argument to a multi-argument scalar function (coalesce, substring, ...). */
241-
typedef struct {
242-
const char *variable; /* variable reference (NULL if a literal) */
243-
const char *property; /* property of the variable (NULL if whole var / literal) */
244-
const char *literal; /* literal string/number text (NULL if a variable ref) */
245-
} cbm_func_arg_t;
246-
247252
/* RETURN item */
248253
typedef struct {
249254
const char *variable;

0 commit comments

Comments
 (0)