From 9ebddb718a8bf5056bc9d6d2d22f6b9b95194353 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sat, 27 Jun 2026 02:20:47 +0200 Subject: [PATCH] fix(cypher): LIMIT 0 returns 0 rows instead of all rows The return-clause `limit` was zero-initialized (calloc) and every guard tested `limit > 0`, so an explicit `LIMIT 0` was indistinguishable from "no LIMIT clause" and returned ALL rows. Standard Cypher treats `LIMIT N` as an upper bound, so `LIMIT 0` must return zero rows. Default `limit` to the sentinel -1 ("no limit"); the parser still sets 0 for an explicit `LIMIT 0`. Change the truncation guards (rb_apply_skip_limit, bindings_skip_limit) and the apply call to `limit >= 0`, so limit 0 truncates to 0 while -1 returns all. The proj_cap optimization stays `> 0` (LIMIT 0 falls through to full projection, then the final truncation yields 0). Reproduced by repro_new_cypher_limit_zero (was RED): `RETURN ... LIMIT 0` must yield row_count == 0. Signed-off-by: Martin Vogel --- src/cypher/cypher.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index 11cbcf4d1..be3df4573 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -1615,6 +1615,10 @@ static int parse_return_or_with(parser_t *p, cbm_return_clause_t **out, bool is_ } cbm_return_clause_t *r = calloc(CBM_ALLOC_ONE, sizeof(cbm_return_clause_t)); + /* -1 = no LIMIT clause (return all). An explicit `LIMIT 0` parses to 0 below + * and must return 0 rows — distinguishing the two requires a sentinel, since + * calloc zeroes limit and `limit > 0` would treat LIMIT 0 as "no limit". */ + r->limit = -1; int cap = CYP_INIT_CAP8; r->items = malloc(cap * sizeof(cbm_return_item_t)); @@ -3092,7 +3096,7 @@ static void rb_apply_skip_limit(result_builder_t *rb, int skip_n, int limit) { rb->row_count = 0; } /* Limit */ - if (limit > 0 && rb->row_count > limit) { + if (limit >= 0 && rb->row_count > limit) { for (int i = limit; i < rb->row_count; i++) { for (int c = 0; c < rb->col_count; c++) { safe_str_free(&rb->rows[i][c]); @@ -3406,7 +3410,7 @@ static void bindings_skip_limit(binding_t *vbindings, int *count, int skip, int } *count = 0; } - if (limit > 0 && *count > limit) { + if (limit >= 0 && *count > limit) { for (int i = limit; i < *count; i++) { binding_free(&vbindings[i]); } @@ -4246,7 +4250,7 @@ static void execute_return_clause(cbm_query_t *q, cbm_return_clause_t *ret, bind } rb_apply_order_by(rb, ret); - rb_apply_skip_limit(rb, ret->skip, ret->limit > 0 ? ret->limit : max_rows); + rb_apply_skip_limit(rb, ret->skip, ret->limit >= 0 ? ret->limit : max_rows); if (ret->distinct) { rb_apply_distinct(rb); }