diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index 13196a4a8..75686b90f 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -1548,6 +1548,13 @@ static int parse_string_func_item(parser_t *p, cbm_return_item_t *item) { return 0; } +static void set_unsupported_list_index_error(parser_t *p) { + snprintf(p->error, sizeof(p->error), + "unsupported expression: list indexing/slicing '[...]' is not supported; return " + "labels(n) AS labels directly, or group scalar node labels with RETURN n.label AS " + "label, count(*) AS node_count ORDER BY node_count DESC LIMIT 5"); +} + static int parse_return_item(parser_t *p, cbm_return_item_t *item) { memset(item, 0, sizeof(*item)); int rc = 0; @@ -1583,8 +1590,7 @@ static int parse_return_item(parser_t *p, cbm_return_item_t *item) { "trim, ltrim, rtrim, reverse, labels, type, id, keys, properties)", item->variable ? item->variable : "?"); } else { - snprintf(p->error, sizeof(p->error), - "unsupported expression: list indexing/slicing '[...]' is not supported"); + set_unsupported_list_index_error(p); } safe_str_free(&item->variable); safe_str_free(&item->property); @@ -1880,6 +1886,8 @@ static int parse_post_where(parser_t *p, cbm_query_t *q, // NOLINT(misc-no-recur q->union_next = sub.query; sub.query = NULL; cbm_parse_free(&sub); + /* The recursive parser owns and validates the complete UNION tail. */ + p->pos = p->count - SKIP_ONE; } return 0; } @@ -1943,6 +1951,19 @@ int cbm_parse(const cbm_token_t *tokens, int token_count, // NOLINT(misc-no-recu return CBM_NOT_FOUND; } + if (!check(&p, TOK_EOF)) { + if (check(&p, TOK_LBRACKET)) { + set_unsupported_list_index_error(&p); + out->error = heap_strdup(p.error); + } else { + snprintf(p.error, sizeof(p.error), "unexpected trailing token '%s' at pos %d", + peek(&p)->text, peek(&p)->pos); + out->error = heap_strdup(p.error); + } + cbm_query_free(q); + return CBM_NOT_FOUND; + } + out->query = q; return 0; } diff --git a/tests/test_cypher.c b/tests/test_cypher.c index 62d683726..ee07ecc6c 100644 --- a/tests/test_cypher.c +++ b/tests/test_cypher.c @@ -2769,6 +2769,35 @@ TEST(cypher_issue240_labels_function) { PASS(); } +TEST(cypher_rejects_list_index_after_function_result) { + cbm_query_t *q = NULL; + char *err = NULL; + int rc = cbm_cypher_parse("MATCH (n) RETURN labels(n)[0] AS label, count(*) AS count " + "ORDER BY count DESC LIMIT 5", + &q, &err); + ASSERT(rc != 0); + ASSERT_NULL(q); + ASSERT_NOT_NULL(err); + ASSERT_NOT_NULL(strstr(err, "list indexing")); + ASSERT_NOT_NULL(strstr(err, "labels(n) AS labels")); + ASSERT_NOT_NULL(strstr(err, "n.label AS label")); + ASSERT_NOT_NULL(strstr(err, "count(*) AS node_count")); + free(err); + PASS(); +} + +TEST(cypher_rejects_unconsumed_trailing_tokens) { + cbm_query_t *q = NULL; + char *err = NULL; + int rc = cbm_cypher_parse("MATCH (n) RETURN n.name trailing", &q, &err); + ASSERT(rc != 0); + ASSERT_NULL(q); + ASSERT_NOT_NULL(err); + ASSERT_NOT_NULL(strstr(err, "unexpected trailing token")); + free(err); + PASS(); +} + /* #237: DISTINCT applied before ORDER BY + LIMIT */ TEST(cypher_issue237_distinct_order_limit) { cbm_store_t *s = setup_cypher_store(); @@ -2969,6 +2998,8 @@ SUITE(cypher) { /* Execution */ RUN_TEST(cypher_exec_match_all_functions); RUN_TEST(cypher_issue240_labels_function); + RUN_TEST(cypher_rejects_list_index_after_function_result); + RUN_TEST(cypher_rejects_unconsumed_trailing_tokens); RUN_TEST(cypher_issue237_distinct_order_limit); RUN_TEST(cypher_issue873_distinct_order_limit_dedupes_before_limit); RUN_TEST(cypher_issue873_distinct_limit_dedupes_before_limit);