Skip to content

Commit 3d9a6cf

Browse files
metehanulusoyclaude
andcommitted
fix(cypher): reject a non-numeric SKIP/LIMIT operand instead of dropping it
expect() fills p->error and returns NULL when the token after SKIP or LIMIT is not a number, but both call sites only tested `if (num)` and then fell through to `*out = r; return 0;`. r->limit kept the -1 "unset" initialiser, which execution reads as "no LIMIT": rb_apply_skip_limit(rb, ret->skip, ret->limit >= 0 ? ret->limit : max_rows); So `RETURN n.name LIMIT $limit` ran unbounded and answered isError:false with the whole result set. `SKIP $offset LIMIT 10` lost both clauses at once, because the orphaned operand also blocked the following match(p, TOK_LIMIT). Propagate the failure the way the ORDER BY branch directly above already does. This is the failure mode #1334 banned, reached by a different route. Fixes #1994 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: metehanulusoy <ulusoy.metehan03@gmail.com>
1 parent 5fbab7b commit 3d9a6cf

2 files changed

Lines changed: 64 additions & 6 deletions

File tree

‎src/cypher/cypher.c‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1817,20 +1817,27 @@ static int parse_return_or_with(parser_t *p, cbm_return_clause_t **out, bool is_
18171817
}
18181818
}
18191819

1820-
/* Optional SKIP */
1820+
/* Optional SKIP. A non-numeric operand is a loud parse error: expect() has
1821+
* already filled p->error, and dropping the clause instead would leave
1822+
* r->skip at its "none" default while the query still reports success. */
18211823
if (match(p, TOK_SKIP)) {
18221824
const cbm_token_t *num = expect(p, TOK_NUMBER);
1823-
if (num) {
1824-
r->skip = (int)strtol(num->text, NULL, CBM_DECIMAL_BASE);
1825+
if (!num) {
1826+
free_return_clause(r);
1827+
return CBM_NOT_FOUND;
18251828
}
1829+
r->skip = (int)strtol(num->text, NULL, CBM_DECIMAL_BASE);
18261830
}
18271831

1828-
/* Optional LIMIT */
1832+
/* Optional LIMIT. Same rule: a dropped LIMIT is the failure mode #1334
1833+
* banned - the caller would silently receive the whole result set. */
18291834
if (match(p, TOK_LIMIT)) {
18301835
const cbm_token_t *num = expect(p, TOK_NUMBER);
1831-
if (num) {
1832-
r->limit = (int)strtol(num->text, NULL, CBM_DECIMAL_BASE);
1836+
if (!num) {
1837+
free_return_clause(r);
1838+
return CBM_NOT_FOUND;
18331839
}
1840+
r->limit = (int)strtol(num->text, NULL, CBM_DECIMAL_BASE);
18341841
}
18351842

18361843
*out = r;

‎tests/test_cypher.c‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,53 @@ TEST(cypher_parse_order_by_over_cap_rejected_issue1334) {
420420
PASS();
421421
}
422422

423+
/* #1994: a non-numeric SKIP/LIMIT operand must be a loud parse error too. The old
424+
* failure mode was the same one #1334 banned by a different route: expect()
425+
* returned NULL, the clause was dropped, and the query still reported success
426+
* with limit left at its -1 "no LIMIT" sentinel - so query_graph answered a
427+
* bounded query with the entire result set. Cypher parameters ($limit) are the
428+
* everyday trigger: they are what a Neo4j-shaped client writes by default. */
429+
TEST(cypher_parse_nonnumeric_limit_rejected_issue1994) {
430+
cbm_query_t *q = NULL;
431+
char *err = NULL;
432+
int rc = cbm_cypher_parse("MATCH (f:Function) RETURN f.name LIMIT $limit", &q, &err);
433+
ASSERT(rc != 0);
434+
free(err);
435+
PASS();
436+
}
437+
438+
TEST(cypher_parse_nonnumeric_skip_rejected_issue1994) {
439+
cbm_query_t *q = NULL;
440+
char *err = NULL;
441+
/* The orphaned operand also swallowed the LIMIT that followed it. */
442+
int rc = cbm_cypher_parse("MATCH (f:Function) RETURN f.name SKIP $offset LIMIT 10", &q, &err);
443+
ASSERT(rc != 0);
444+
free(err);
445+
PASS();
446+
}
447+
448+
TEST(cypher_parse_word_limit_operand_rejected_issue1994) {
449+
cbm_query_t *q = NULL;
450+
char *err = NULL;
451+
int rc = cbm_cypher_parse("MATCH (f:Function) RETURN f.name LIMIT abc", &q, &err);
452+
ASSERT(rc != 0);
453+
free(err);
454+
PASS();
455+
}
456+
457+
/* Control: a well-formed SKIP/LIMIT still parses and still carries its values. */
458+
TEST(cypher_parse_numeric_skip_limit_still_accepted) {
459+
cbm_query_t *q = NULL;
460+
char *err = NULL;
461+
int rc = cbm_cypher_parse("MATCH (f:Function) RETURN f.name SKIP 2 LIMIT 10", &q, &err);
462+
ASSERT_EQ(rc, 0);
463+
ASSERT_EQ(q->ret->skip, 2);
464+
ASSERT_EQ(q->ret->limit, 10);
465+
466+
cbm_query_free(q);
467+
PASS();
468+
}
469+
423470
TEST(cypher_parse_return_distinct) {
424471
cbm_query_t *q = NULL;
425472
char *err = NULL;
@@ -4116,6 +4163,10 @@ SUITE(cypher) {
41164163
RUN_TEST(cypher_parse_return_order_limit);
41174164
RUN_TEST(cypher_parse_multikey_order_by_issue1334);
41184165
RUN_TEST(cypher_parse_order_by_over_cap_rejected_issue1334);
4166+
RUN_TEST(cypher_parse_nonnumeric_limit_rejected_issue1994);
4167+
RUN_TEST(cypher_parse_nonnumeric_skip_rejected_issue1994);
4168+
RUN_TEST(cypher_parse_word_limit_operand_rejected_issue1994);
4169+
RUN_TEST(cypher_parse_numeric_skip_limit_still_accepted);
41194170
RUN_TEST(cypher_parse_return_distinct);
41204171
RUN_TEST(cypher_parse_inline_props);
41214172
RUN_TEST(cypher_parse_error);

0 commit comments

Comments
 (0)