@@ -1655,7 +1655,6 @@ static int parse_return_item(parser_t *p, cbm_return_item_t *item) {
16551655 return 0 ;
16561656}
16571657
1658- /* Parse ORDER BY field into r->order_by and r->order_dir */
16591658/* Parse aggregate function call for ORDER BY */
16601659static void parse_order_by_agg (parser_t * p , char * buf , size_t buf_sz ) {
16611660 const char * fn = agg_func_name (peek (p )-> type );
@@ -1696,18 +1695,33 @@ static char *parse_order_by_expr(parser_t *p, char *buf, size_t buf_sz) {
16961695 return buf ;
16971696}
16981697
1699- static void parse_order_by_clause (parser_t * p , cbm_return_clause_t * r ) {
1698+ /* Parse the full comma-separated ORDER BY key list (#1334). Consuming only the
1699+ * first key left ", key2 ... LIMIT n" unparsed, which silently dropped the
1700+ * LIMIT and flooded the caller with the whole result set. Returns 0 on
1701+ * success, CBM_NOT_FOUND when the key list exceeds the modeled maximum. */
1702+ static int parse_order_by_clause (parser_t * p , cbm_return_clause_t * r ) {
17001703 expect (p , TOK_BY );
1701- char order_buf [CBM_SZ_256 ];
1702- parse_order_by_expr (p , order_buf , sizeof (order_buf ));
1703- r -> order_by = heap_strdup (order_buf );
1704- if (match (p , TOK_ASC )) {
1705- r -> order_dir = heap_strdup ("ASC" );
1706- } else if (match (p , TOK_DESC )) {
1707- r -> order_dir = heap_strdup ("DESC" );
1708- }
1704+ do {
1705+ if (r -> order_key_count >= CBM_CYPHER_ORDER_KEYS_MAX ) {
1706+ return CBM_NOT_FOUND ;
1707+ }
1708+ char order_buf [CBM_SZ_256 ];
1709+ parse_order_by_expr (p , order_buf , sizeof (order_buf ));
1710+ bool desc = false;
1711+ if (match (p , TOK_ASC )) {
1712+ desc = false;
1713+ } else if (match (p , TOK_DESC )) {
1714+ desc = true;
1715+ }
1716+ r -> order_keys [r -> order_key_count ] = heap_strdup (order_buf );
1717+ r -> order_descs [r -> order_key_count ] = desc ;
1718+ r -> order_key_count ++ ;
1719+ } while (match (p , TOK_COMMA ));
1720+ return 0 ;
17091721}
17101722
1723+ static void free_return_clause (cbm_return_clause_t * r );
1724+
17111725/* Parse RETURN/WITH clause (shared logic) */
17121726static int parse_return_or_with (parser_t * p , cbm_return_clause_t * * out , bool is_with ) {
17131727 cbm_token_type_t tok = (int )is_with ? TOK_WITH : TOK_RETURN ;
@@ -1741,8 +1755,7 @@ static int parse_return_or_with(parser_t *p, cbm_return_clause_t **out, bool is_
17411755
17421756 cbm_return_item_t item = {0 };
17431757 if (parse_return_item (p , & item ) < 0 ) {
1744- free (r -> items );
1745- free (r );
1758+ free_return_clause (r );
17461759 return CBM_NOT_FOUND ;
17471760 }
17481761
@@ -1759,15 +1772,17 @@ static int parse_return_or_with(parser_t *p, cbm_return_clause_t **out, bool is_
17591772 * parsed item count to that width so an over-wide RETURN is rejected here
17601773 * instead of writing past those arrays downstream. */
17611774 if (r -> count > CBM_SZ_32 ) {
1762- free (r -> items );
1763- free (r );
1775+ free_return_clause (r );
17641776 return CBM_NOT_FOUND ;
17651777 }
17661778
17671779tail :
17681780 /* Optional ORDER BY */
17691781 if (match (p , TOK_ORDER )) {
1770- parse_order_by_clause (p , r );
1782+ if (parse_order_by_clause (p , r ) < 0 ) {
1783+ free_return_clause (r );
1784+ return CBM_NOT_FOUND ;
1785+ }
17711786 }
17721787
17731788 /* Optional SKIP */
@@ -2100,8 +2115,9 @@ static void free_return_clause(cbm_return_clause_t *r) {
21002115 free (r -> items [i ].args );
21012116 }
21022117 free (r -> items );
2103- safe_str_free (& r -> order_by );
2104- safe_str_free (& r -> order_dir );
2118+ for (int k = 0 ; k < r -> order_key_count ; k ++ ) {
2119+ safe_str_free (& r -> order_keys [k ]);
2120+ }
21052121 free (r );
21062122}
21072123
@@ -3308,16 +3324,17 @@ static void expand_pattern_rels(cbm_store_t *store, cbm_pattern_t *pat, binding_
33083324
33093325/* ── Result postprocessing helpers ─────────────────────────────── */
33103326
3311- /* Find the column index for ORDER BY, checking both column names and aliases.
3312- * Returns -1 if not found. */
3313- static int rb_find_order_column (const result_builder_t * rb , const cbm_return_clause_t * ret ) {
3327+ /* Find the column index for one ORDER BY key, checking both column names and
3328+ * aliases. Returns -1 if not found. */
3329+ static int rb_find_order_column (const result_builder_t * rb , const cbm_return_clause_t * ret ,
3330+ const char * key ) {
33143331 for (int ci = 0 ; ci < rb -> col_count ; ci ++ ) {
3315- if (strcmp (rb -> columns [ci ], ret -> order_by ) == 0 ) {
3332+ if (strcmp (rb -> columns [ci ], key ) == 0 ) {
33163333 return ci ;
33173334 }
33183335 }
33193336 for (int ci = 0 ; ci < ret -> count ; ci ++ ) {
3320- if (ret -> items [ci ].alias && strcmp (ret -> items [ci ].alias , ret -> order_by ) == 0 ) {
3337+ if (ret -> items [ci ].alias && strcmp (ret -> items [ci ].alias , key ) == 0 ) {
33213338 return ci ;
33223339 }
33233340 }
@@ -3345,26 +3362,44 @@ static bool rb_is_numeric_column(const result_builder_t *rb, int col) {
33453362}
33463363
33473364static void rb_apply_order_by (result_builder_t * rb , const cbm_return_clause_t * ret ) {
3348- if (! ret -> order_by ) {
3365+ if (ret -> order_key_count == 0 ) {
33493366 return ;
33503367 }
3351- int order_col = rb_find_order_column (rb , ret );
3352- if (order_col < 0 ) {
3368+ /* Resolve every key up front; an unresolvable key is skipped, matching the
3369+ * single-key forgiveness (sorting on what can be resolved beats dropping
3370+ * the whole ORDER BY). */
3371+ int cols [CBM_CYPHER_ORDER_KEYS_MAX ];
3372+ bool numeric [CBM_CYPHER_ORDER_KEYS_MAX ];
3373+ bool descs [CBM_CYPHER_ORDER_KEYS_MAX ];
3374+ int keys = 0 ;
3375+ for (int k = 0 ; k < ret -> order_key_count ; k ++ ) {
3376+ int order_col = rb_find_order_column (rb , ret , ret -> order_keys [k ]);
3377+ if (order_col < 0 ) {
3378+ continue ;
3379+ }
3380+ cols [keys ] = order_col ;
3381+ numeric [keys ] = rb_is_numeric_column (rb , order_col );
3382+ descs [keys ] = ret -> order_descs [k ];
3383+ keys ++ ;
3384+ }
3385+ if (keys == 0 ) {
33533386 return ;
33543387 }
3355-
3356- bool desc = ret -> order_dir && strcmp (ret -> order_dir , "DESC" ) == 0 ;
3357- bool numeric = rb_is_numeric_column (rb , order_col );
33583388 for (int i = 0 ; i < rb -> row_count - SKIP_ONE ; i ++ ) {
33593389 for (int j = 0 ; j < rb -> row_count - i - SKIP_ONE ; j ++ ) {
3360- int cmp ;
3361- if (numeric ) {
3362- cmp = (int )strtol (rb -> rows [j ][order_col ], NULL , CBM_DECIMAL_BASE ) -
3363- (int )strtol (rb -> rows [j + SKIP_ONE ][order_col ], NULL , CBM_DECIMAL_BASE );
3364- } else {
3365- cmp = strcmp (rb -> rows [j ][order_col ], rb -> rows [j + SKIP_ONE ][order_col ]);
3390+ int cmp = 0 ;
3391+ for (int k = 0 ; k < keys && cmp == 0 ; k ++ ) {
3392+ if (numeric [k ]) {
3393+ cmp = (int )strtol (rb -> rows [j ][cols [k ]], NULL , CBM_DECIMAL_BASE ) -
3394+ (int )strtol (rb -> rows [j + SKIP_ONE ][cols [k ]], NULL , CBM_DECIMAL_BASE );
3395+ } else {
3396+ cmp = strcmp (rb -> rows [j ][cols [k ]], rb -> rows [j + SKIP_ONE ][cols [k ]]);
3397+ }
3398+ if (descs [k ]) {
3399+ cmp = - cmp ;
3400+ }
33663401 }
3367- if (desc ? cmp < 0 : cmp > 0 ) {
3402+ if (cmp > 0 ) {
33683403 const char * * tmp = rb -> rows [j ];
33693404 rb -> rows [j ] = rb -> rows [j + SKIP_ONE ];
33703405 rb -> rows [j + SKIP_ONE ] = tmp ;
@@ -3674,18 +3709,26 @@ static void distinct_list_add(char ***list, int *count, const char *val) {
36743709 (* list )[idx ] = heap_strdup (val );
36753710}
36763711
3677- /* Sort bindings by a virtual variable using bubble sort */
3678- static void sort_bindings (binding_t * vbindings , int count , const char * key , bool desc ) {
3712+ /* Sort bindings by the ORDER BY key list (virtual variables) using bubble
3713+ * sort; later keys break ties, direction is per key (#1334). */
3714+ static void sort_bindings (binding_t * vbindings , int count , const cbm_return_clause_t * wc ) {
36793715 for (int i = 0 ; i < count - SKIP_ONE ; i ++ ) {
36803716 for (int j = 0 ; j < count - i - SKIP_ONE ; j ++ ) {
3681- const char * va = binding_get_virtual (& vbindings [j ], key , NULL );
3682- const char * vb2 = binding_get_virtual (& vbindings [j + SKIP_ONE ], key , NULL );
3683- char * ea = NULL ;
3684- char * eb = NULL ;
3685- double da = strtod (va , & ea );
3686- double db = strtod (vb2 , & eb );
3687- int cmp = (ea != va && eb != vb2 ) ? ((da > db ) - (da < db )) : strcmp (va , vb2 );
3688- if (desc ? cmp < 0 : cmp > 0 ) {
3717+ int cmp = 0 ;
3718+ for (int k = 0 ; k < wc -> order_key_count && cmp == 0 ; k ++ ) {
3719+ const char * va = binding_get_virtual (& vbindings [j ], wc -> order_keys [k ], NULL );
3720+ const char * vb2 =
3721+ binding_get_virtual (& vbindings [j + SKIP_ONE ], wc -> order_keys [k ], NULL );
3722+ char * ea = NULL ;
3723+ char * eb = NULL ;
3724+ double da = strtod (va , & ea );
3725+ double db = strtod (vb2 , & eb );
3726+ cmp = (ea != va && eb != vb2 ) ? ((da > db ) - (da < db )) : strcmp (va , vb2 );
3727+ if (wc -> order_descs [k ]) {
3728+ cmp = - cmp ;
3729+ }
3730+ }
3731+ if (cmp > 0 ) {
36893732 binding_t tmp = vbindings [j ];
36903733 vbindings [j ] = vbindings [j + SKIP_ONE ];
36913734 vbindings [j + SKIP_ONE ] = tmp ;
@@ -3718,9 +3761,8 @@ static void bindings_skip_limit(binding_t *vbindings, int *count, int skip, int
37183761
37193762/* Sort, skip, and limit binding array in-place */
37203763static void with_sort_skip_limit (const cbm_return_clause_t * wc , binding_t * vbindings , int * vcount ) {
3721- if (wc -> order_by ) {
3722- bool wdesc = wc -> order_dir && strcmp (wc -> order_dir , "DESC" ) == 0 ;
3723- sort_bindings (vbindings , * vcount , wc -> order_by , wdesc );
3764+ if (wc -> order_key_count > 0 ) {
3765+ sort_bindings (vbindings , * vcount , wc );
37243766 }
37253767 bindings_skip_limit (vbindings , vcount , wc -> skip , wc -> limit );
37263768}
@@ -4390,7 +4432,7 @@ static void build_return_columns(result_builder_t *rb, cbm_return_clause_t *ret)
43904432static void execute_return_simple (cbm_return_clause_t * ret , binding_t * bindings , int bind_count ,
43914433 int max_rows , result_builder_t * rb ) {
43924434 int proj_cap = max_rows ;
4393- if (ret -> limit > 0 && !ret -> distinct && ! ret -> order_by && ret -> skip <= 0 ) {
4435+ if (ret -> limit > 0 && !ret -> distinct && ret -> order_key_count == 0 && ret -> skip <= 0 ) {
43944436 proj_cap = ret -> limit ;
43954437 }
43964438 for (int bi = 0 ; bi < bind_count && rb -> row_count < proj_cap ; bi ++ ) {
0 commit comments