@@ -4894,6 +4894,164 @@ static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *projec
48944894
48954895/* ── Main entry point ─────────────────────────────────────────── */
48964896
4897+ /* ── Is every name a RETURN or WITH item uses actually in scope? ──
4898+ *
4899+ * An unbound variable resolves to NULL and renders as "" on purpose: an
4900+ * OPTIONAL MATCH target that found no row has to project a blank rather than
4901+ * drop the row. The cost of that convention is that a name the query NEVER
4902+ * carried through looks exactly the same on screen. A typo, or a variable a
4903+ * WITH dropped, then reads as "the graph holds no such data" instead of "your
4904+ * query named something that is not there" (#1919). Same failure shape as
4905+ * #373, and the same answer: say so out loud.
4906+ *
4907+ * The check runs on the parsed query, not on the run-time bindings, and that
4908+ * is the whole trick. It asks whether the query DECLARED the name — a
4909+ * different question from whether a row happened to bind it. So the OPTIONAL
4910+ * MATCH convention above is untouched: a target that did not match is still
4911+ * declared, still legal, and still projects "".
4912+ */
4913+
4914+ /* Cap for one query's declared names. CYP_MAX_VARS is the binding cap for
4915+ * nodes alone; a pattern also names edges, so this is the roomier of the two.
4916+ * A query that overruns it skips the check rather than guessing — a wrong
4917+ * refusal costs the caller a working query, which is worse than the silence
4918+ * this guard removes. */
4919+ enum { CYP_SCOPE_MAX_NAMES = 32 };
4920+
4921+ static bool scope_holds (const char * const * names , int count , const char * want ) {
4922+ for (int i = 0 ; i < count ; i ++ ) {
4923+ if (names [i ] && strcmp (names [i ], want ) == 0 ) {
4924+ return true;
4925+ }
4926+ }
4927+ return false;
4928+ }
4929+
4930+ /* Every name the query's patterns declare, plus an UNWIND alias.
4931+ * Answers -1 when there are more names than the cap holds. */
4932+ static int collect_declared_names (const cbm_query_t * q , const char * * out , int cap ) {
4933+ int n = 0 ;
4934+ for (int pi = 0 ; pi < q -> pattern_count ; pi ++ ) {
4935+ const cbm_pattern_t * pat = & q -> patterns [pi ];
4936+ for (int ni = 0 ; ni < pat -> node_count ; ni ++ ) {
4937+ const char * var = pat -> nodes [ni ].variable ;
4938+ if (var && !scope_holds (out , n , var )) {
4939+ if (n >= cap ) {
4940+ return -1 ;
4941+ }
4942+ out [n ++ ] = var ;
4943+ }
4944+ }
4945+ for (int ri = 0 ; ri < pat -> rel_count ; ri ++ ) {
4946+ const char * var = pat -> rels [ri ].variable ;
4947+ if (var && !scope_holds (out , n , var )) {
4948+ if (n >= cap ) {
4949+ return -1 ;
4950+ }
4951+ out [n ++ ] = var ;
4952+ }
4953+ }
4954+ }
4955+ if (q -> unwind_alias && !scope_holds (out , n , q -> unwind_alias )) {
4956+ if (n >= cap ) {
4957+ return -1 ;
4958+ }
4959+ out [n ++ ] = q -> unwind_alias ;
4960+ }
4961+ return n ;
4962+ }
4963+
4964+ /* What a WITH leaves behind: its alias where it made one, and the plain
4965+ * variable where it carried one through whole. `WITH f.name AS caller` leaves
4966+ * `caller` and nothing else — `f` is gone, which is the case #1919 is about. */
4967+ static int collect_with_names (const cbm_return_clause_t * wc , const char * * out , int cap ) {
4968+ int n = 0 ;
4969+ for (int i = 0 ; i < wc -> count ; i ++ ) {
4970+ const cbm_return_item_t * item = & wc -> items [i ];
4971+ const char * name = NULL ;
4972+ if (item -> alias ) {
4973+ name = item -> alias ;
4974+ } else if (item -> variable && !item -> property && !item -> func ) {
4975+ name = item -> variable ;
4976+ }
4977+ if (name && !scope_holds (out , n , name )) {
4978+ if (n >= cap ) {
4979+ return -1 ;
4980+ }
4981+ out [n ++ ] = name ;
4982+ }
4983+ }
4984+ return n ;
4985+ }
4986+
4987+ /* Build the message. It names the variable and the clause, because an error
4988+ * that does not say WHICH name is wrong sends the reader back to guessing. */
4989+ static char * scope_error (const char * var , const char * clause , const char * why ) {
4990+ char buf [CBM_SZ_256 ];
4991+ snprintf (buf , sizeof (buf ), "variable '%s' is not in scope for %s — %s" , var , clause , why );
4992+ return heap_strdup (buf );
4993+ }
4994+
4995+ /* The variable this item really references, or NULL when it references none.
4996+ * Two items carry a placeholder in that field rather than a name the query
4997+ * declared: `count(*)` stores "*", and a CASE expression stores "CASE". Both
4998+ * would read as an unknown variable, so neither is checkable here. */
4999+ static const char * scope_checkable_var (const cbm_return_item_t * item ) {
5000+ if (item -> kase || !item -> variable ) {
5001+ return NULL ;
5002+ }
5003+ if (strcmp (item -> variable , "*" ) == 0 ) {
5004+ return NULL ;
5005+ }
5006+ return item -> variable ;
5007+ }
5008+
5009+ /* Answers NULL when the query is fine, or a heap message naming the first
5010+ * variable that is not in scope. Checks one query; the caller walks a UNION. */
5011+ static char * check_projection_scope (const cbm_query_t * q ) {
5012+ const char * declared [CYP_SCOPE_MAX_NAMES ];
5013+ int declared_n = collect_declared_names (q , declared , CYP_SCOPE_MAX_NAMES );
5014+ if (declared_n < 0 ) {
5015+ return NULL ; /* too many names to model — stay quiet rather than guess */
5016+ }
5017+
5018+ /* A WITH still reads the pattern variables. */
5019+ if (q -> with_clause && !q -> with_clause -> star ) {
5020+ for (int i = 0 ; i < q -> with_clause -> count ; i ++ ) {
5021+ const char * var = scope_checkable_var (& q -> with_clause -> items [i ]);
5022+ if (var && !scope_holds (declared , declared_n , var )) {
5023+ return scope_error (var , "WITH" , "no pattern in this query names it" );
5024+ }
5025+ }
5026+ }
5027+
5028+ if (!q -> ret || q -> ret -> star ) {
5029+ return NULL ;
5030+ }
5031+
5032+ /* A RETURN after a WITH reads only what the WITH left behind. */
5033+ const char * after_with [CYP_SCOPE_MAX_NAMES ];
5034+ const char * const * scope = declared ;
5035+ int scope_n = declared_n ;
5036+ if (q -> with_clause ) {
5037+ scope_n = collect_with_names (q -> with_clause , after_with , CYP_SCOPE_MAX_NAMES );
5038+ if (scope_n < 0 ) {
5039+ return NULL ;
5040+ }
5041+ scope = after_with ;
5042+ }
5043+
5044+ for (int i = 0 ; i < q -> ret -> count ; i ++ ) {
5045+ const char * var = scope_checkable_var (& q -> ret -> items [i ]);
5046+ if (var && !scope_holds (scope , scope_n , var )) {
5047+ return scope_error (var , "RETURN" ,
5048+ q -> with_clause ? "the WITH clause did not carry it through"
5049+ : "no pattern in this query names it" );
5050+ }
5051+ }
5052+ return NULL ;
5053+ }
5054+
48975055int cbm_cypher_execute (cbm_store_t * store , const char * query , const char * project , int max_rows ,
48985056 cbm_cypher_result_t * out ) {
48995057 memset (out , 0 , sizeof (* out ));
@@ -4911,6 +5069,15 @@ int cbm_cypher_execute(cbm_store_t *store, const char *query, const char *projec
49115069 return CBM_NOT_FOUND ;
49125070 }
49135071
5072+ for (const cbm_query_t * sq = q ; sq ; sq = sq -> union_next ) {
5073+ char * scope_err = check_projection_scope (sq );
5074+ if (scope_err ) {
5075+ cbm_query_free (q );
5076+ out -> error = scope_err ;
5077+ return CBM_NOT_FOUND ;
5078+ }
5079+ }
5080+
49145081 result_builder_t rb = {0 };
49155082 if (execute_single (store , q , project , max_rows , & rb ) < 0 ) {
49165083 rb_free (& rb );
0 commit comments