1use std::cmp::Ordering;
7use std::ops::Range;
8
9use vibesql_ast::{
10 Expression, FrameBound, FrameExclude, FrameUnit, NullsOrder, OrderByItem, OrderDirection,
11 UnaryOperator, WindowFrame,
12};
13use vibesql_types::SqlValue;
14
15use vibesql_storage::Row;
16
17use super::partitioning::Partition;
18use super::sorting::{compare_values, compare_values_with_collation};
19
20pub fn validate_frame(frame_spec: &Option<WindowFrame>) -> Result<(), String> {
24 let frame = match frame_spec {
25 Some(f) => f,
26 None => return Ok(()),
27 };
28
29 validate_frame_bound(&frame.start, true)?;
31
32 if let Some(ref end_bound) = frame.end {
34 validate_frame_bound(end_bound, false)?;
35 }
36
37 Ok(())
38}
39
40pub fn validate_range_order_by(
46 frame_spec: &Option<WindowFrame>,
47 order_by: &Option<Vec<OrderByItem>>,
48) -> Result<(), String> {
49 let frame = match frame_spec {
50 Some(f) => f,
51 None => return Ok(()),
52 };
53
54 if !matches!(frame.unit, FrameUnit::Range) {
56 return Ok(());
57 }
58
59 let has_offset_bound =
61 matches!(frame.start, FrameBound::Preceding(_) | FrameBound::Following(_))
62 || frame.end.as_ref().is_some_and(|end| {
63 matches!(end, FrameBound::Preceding(_) | FrameBound::Following(_))
64 });
65
66 if !has_offset_bound {
67 return Ok(());
68 }
69
70 let order_by_count = order_by.as_ref().map_or(0, |items| items.len());
72 if order_by_count != 1 {
73 return Err(
74 "RANGE with offset PRECEDING/FOLLOWING requires one ORDER BY expression".to_string()
75 );
76 }
77
78 Ok(())
79}
80
81fn validate_frame_bound(bound: &FrameBound, is_start: bool) -> Result<(), String> {
83 match bound {
84 FrameBound::UnboundedPreceding
85 | FrameBound::UnboundedFollowing
86 | FrameBound::CurrentRow => Ok(()),
87
88 FrameBound::Preceding(offset_expr) | FrameBound::Following(offset_expr) => {
89 let bound_type = if is_start { "starting" } else { "ending" };
90 match evaluate_offset_expr(offset_expr) {
91 Err(_) => {
92 Err(format!("frame {} offset must be a non-negative integer", bound_type))
94 }
95 Ok(Some(offset)) if offset < 0.0 => {
96 Err(format!("frame {} offset must be a non-negative number", bound_type))
97 }
98 Ok(None) => {
99 Err(format!("frame {} offset must be a non-negative number", bound_type))
101 }
102 Ok(Some(_)) => Ok(()), }
104 }
105 }
106}
107
108fn evaluate_offset_expr(expr: &Expression) -> Result<Option<f64>, String> {
113 match expr {
114 Expression::Literal(SqlValue::Integer(n)) => Ok(Some(*n as f64)),
115 Expression::Literal(SqlValue::Smallint(n)) => Ok(Some(*n as f64)),
116 Expression::Literal(SqlValue::Bigint(n)) => Ok(Some(*n as f64)),
117 Expression::Literal(SqlValue::Unsigned(n)) => Ok(Some(*n as f64)),
118 Expression::Literal(SqlValue::Float(f)) => Ok(Some(*f as f64)),
119 Expression::Literal(SqlValue::Real(f)) => Ok(Some(*f)),
120 Expression::Literal(SqlValue::Double(f)) => Ok(Some(*f)),
121 Expression::Literal(SqlValue::Numeric(f)) => Ok(Some(*f)),
122 Expression::Literal(SqlValue::Null) => {
123 Ok(None)
125 }
126 Expression::Literal(SqlValue::Character(s)) | Expression::Literal(SqlValue::Varchar(s)) => {
127 let s_str = s.as_str();
129 if s_str.is_empty() {
130 Ok(None)
132 } else if let Ok(n) = s_str.parse::<f64>() {
133 Ok(Some(n))
134 } else {
135 Ok(None)
137 }
138 }
139 Expression::Literal(SqlValue::Blob(_)) => {
140 Ok(None)
142 }
143 Expression::UnaryOp { op: UnaryOperator::Minus, expr } => {
144 match evaluate_offset_expr(expr)? {
145 Some(inner) => Ok(Some(-inner)),
146 None => Ok(None),
147 }
148 }
149 Expression::UnaryOp { op: UnaryOperator::Plus, expr } => evaluate_offset_expr(expr),
150 _ => Err("frame offset must be a constant expression".to_string()),
151 }
152}
153
154pub fn calculate_frame<F>(
162 partition: &Partition,
163 current_row_idx: usize,
164 order_by: &Option<Vec<OrderByItem>>,
165 frame_spec: &Option<WindowFrame>,
166 eval_fn: &F,
167) -> Range<usize>
168where
169 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
170{
171 let partition_size = partition.len();
172
173 let frame = match frame_spec {
177 Some(f) => f,
178 None => {
179 let has_order_by = order_by.as_ref().is_some_and(|items| !items.is_empty());
181
182 if has_order_by {
183 return calculate_range_frame(
186 partition,
187 current_row_idx,
188 order_by,
189 &FrameBound::UnboundedPreceding,
190 &Some(FrameBound::CurrentRow),
191 eval_fn,
192 );
193 } else {
194 return 0..partition_size;
196 }
197 }
198 };
199
200 match frame.unit {
202 FrameUnit::Rows => {
203 calculate_rows_frame(partition, current_row_idx, &frame.start, &frame.end)
204 }
205 FrameUnit::Range => calculate_range_frame(
206 partition,
207 current_row_idx,
208 order_by,
209 &frame.start,
210 &frame.end,
211 eval_fn,
212 ),
213 FrameUnit::Groups => calculate_groups_frame(
214 partition,
215 current_row_idx,
216 order_by,
217 &frame.start,
218 &frame.end,
219 eval_fn,
220 ),
221 }
222}
223
224fn calculate_rows_frame(
226 partition: &Partition,
227 current_row_idx: usize,
228 start: &FrameBound,
229 end: &Option<FrameBound>,
230) -> Range<usize> {
231 let partition_size = partition.len();
232
233 let start_idx = calculate_rows_boundary(start, current_row_idx, partition_size, true);
235
236 let end_idx = match end {
238 Some(end_bound) => {
239 calculate_rows_boundary(end_bound, current_row_idx, partition_size, false)
240 }
241 None => current_row_idx + 1, };
243
244 let start = start_idx.min(partition_size);
246 let end = end_idx.min(partition_size).max(start);
247
248 start..end
249}
250
251fn calculate_range_frame<F>(
259 partition: &Partition,
260 current_row_idx: usize,
261 order_by: &Option<Vec<OrderByItem>>,
262 start: &FrameBound,
263 end: &Option<FrameBound>,
264 eval_fn: &F,
265) -> Range<usize>
266where
267 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
268{
269 let partition_size = partition.len();
270
271 if partition_size == 0 || current_row_idx >= partition_size {
272 return 0..0;
273 }
274
275 let order_items = match order_by {
277 Some(items) if !items.is_empty() => items,
278 _ => return 0..partition_size,
279 };
280
281 let current_row = &partition.rows[current_row_idx];
283 let current_value = eval_fn(&order_items[0].expr, current_row).unwrap_or(SqlValue::Null);
284
285 let start_idx = calculate_range_boundary(
287 partition,
288 current_row_idx,
289 order_items,
290 ¤t_value,
291 start,
292 true,
293 eval_fn,
294 );
295
296 let end_idx = match end {
298 Some(end_bound) => calculate_range_boundary(
299 partition,
300 current_row_idx,
301 order_items,
302 ¤t_value,
303 end_bound,
304 false,
305 eval_fn,
306 ),
307 None => {
308 find_last_peer(partition, current_row_idx, order_items, eval_fn) + 1
310 }
311 };
312
313 let start = start_idx.min(partition_size);
315 let end = end_idx.min(partition_size).max(start);
316
317 start..end
318}
319
320fn calculate_range_boundary<F>(
326 partition: &Partition,
327 current_row_idx: usize,
328 order_items: &[OrderByItem],
329 current_value: &SqlValue,
330 bound: &FrameBound,
331 is_start: bool,
332 eval_fn: &F,
333) -> usize
334where
335 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
336{
337 use vibesql_ast::OrderDirection;
338
339 let partition_size = partition.len();
340
341 let is_desc =
343 order_items.first().is_some_and(|item| matches!(item.direction, OrderDirection::Desc));
344
345 match bound {
346 FrameBound::UnboundedPreceding => 0,
347 FrameBound::UnboundedFollowing => partition_size,
348
349 FrameBound::CurrentRow => {
350 if is_start {
351 find_first_peer(partition, current_row_idx, order_items, eval_fn)
353 } else {
354 find_last_peer(partition, current_row_idx, order_items, eval_fn) + 1
356 }
357 }
358
359 FrameBound::Preceding(offset_expr) => {
360 let offset = get_numeric_offset(offset_expr);
361
362 let target_value = if is_desc {
368 add_to_value(current_value, offset)
369 } else {
370 subtract_from_value(current_value, offset)
371 };
372
373 if is_start {
374 find_first_row_at_or_after(partition, order_items, &target_value, eval_fn)
376 } else {
377 find_last_row_at_or_before(partition, order_items, &target_value, eval_fn)
380 .map_or(0, |i| i + 1)
381 }
382 }
383
384 FrameBound::Following(offset_expr) => {
385 let offset = get_numeric_offset(offset_expr);
386
387 let target_value = if is_desc {
390 subtract_from_value(current_value, offset)
391 } else {
392 add_to_value(current_value, offset)
393 };
394
395 if is_start {
396 find_first_row_at_or_after(partition, order_items, &target_value, eval_fn)
397 } else {
398 find_last_row_at_or_before(partition, order_items, &target_value, eval_fn)
399 .map_or(0, |i| i + 1)
400 }
401 }
402 }
403}
404
405fn nulls_sort_first(item: &OrderByItem) -> bool {
410 match item.nulls_order {
411 Some(NullsOrder::First) => true,
412 Some(NullsOrder::Last) => false,
413 None => matches!(item.direction, OrderDirection::Asc),
414 }
415}
416
417fn explicit_collation(expr: &Expression) -> Option<&str> {
420 match expr {
421 Expression::Collate { collation, .. } => Some(collation),
422 _ => None,
423 }
424}
425
426fn compare_keys_window_order(
431 a: &SqlValue,
432 b: &SqlValue,
433 item: &OrderByItem,
434 collation: Option<&str>,
435) -> Ordering {
436 let a_null = matches!(a, SqlValue::Null);
437 let b_null = matches!(b, SqlValue::Null);
438 match (a_null, b_null) {
439 (true, true) => Ordering::Equal,
440 (true, false) => {
441 if nulls_sort_first(item) {
442 Ordering::Less
443 } else {
444 Ordering::Greater
445 }
446 }
447 (false, true) => {
448 if nulls_sort_first(item) {
449 Ordering::Greater
450 } else {
451 Ordering::Less
452 }
453 }
454 (false, false) => {
455 let cmp = compare_values_with_collation(a, b, collation);
456 match item.direction {
457 OrderDirection::Asc => cmp,
458 OrderDirection::Desc => cmp.reverse(),
459 }
460 }
461 }
462}
463
464fn calculate_groups_frame<F>(
472 partition: &Partition,
473 current_row_idx: usize,
474 order_by: &Option<Vec<OrderByItem>>,
475 start: &FrameBound,
476 end: &Option<FrameBound>,
477 eval_fn: &F,
478) -> Range<usize>
479where
480 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
481{
482 let partition_size = partition.len();
483
484 if partition_size == 0 || current_row_idx >= partition_size {
485 return 0..0;
486 }
487
488 let order_items = match order_by {
490 Some(items) if !items.is_empty() => items,
491 _ => return 0..partition_size,
492 };
493
494 let group_boundaries = build_group_boundaries(partition, order_items, eval_fn);
496 let current_group = find_group_for_row(&group_boundaries, current_row_idx);
497
498 let start_idx =
500 calculate_groups_boundary(&group_boundaries, current_group, start, true, partition_size);
501
502 let end_idx = match end {
504 Some(end_bound) => calculate_groups_boundary(
505 &group_boundaries,
506 current_group,
507 end_bound,
508 false,
509 partition_size,
510 ),
511 None => {
512 if current_group + 1 < group_boundaries.len() {
514 group_boundaries[current_group + 1]
515 } else {
516 partition_size
517 }
518 }
519 };
520
521 let start = start_idx.min(partition_size);
523 let end = end_idx.min(partition_size).max(start);
524
525 start..end
526}
527
528fn calculate_groups_boundary(
530 group_boundaries: &[usize],
531 current_group: usize,
532 bound: &FrameBound,
533 is_start: bool,
534 partition_size: usize,
535) -> usize {
536 let num_groups = group_boundaries.len();
537
538 match bound {
539 FrameBound::UnboundedPreceding => 0,
540 FrameBound::UnboundedFollowing => partition_size,
541
542 FrameBound::CurrentRow => {
543 if is_start {
544 group_boundaries[current_group]
545 } else {
546 if current_group + 1 < num_groups {
547 group_boundaries[current_group + 1]
548 } else {
549 partition_size
550 }
551 }
552 }
553
554 FrameBound::Preceding(offset_expr) => {
555 let offset = get_numeric_offset(offset_expr) as usize;
556
557 if offset > current_group {
558 if is_start {
560 0
562 } else {
563 0
565 }
566 } else {
567 let target_group = current_group - offset;
568 if is_start {
569 group_boundaries[target_group]
570 } else {
571 if target_group + 1 < num_groups {
572 group_boundaries[target_group + 1]
573 } else {
574 partition_size
575 }
576 }
577 }
578 }
579
580 FrameBound::Following(offset_expr) => {
581 let offset = get_numeric_offset(offset_expr) as usize;
582 let target_group = current_group + offset;
583
584 if target_group >= num_groups {
585 if is_start {
587 partition_size
589 } else {
590 partition_size
592 }
593 } else {
594 if is_start {
595 group_boundaries[target_group]
596 } else {
597 if target_group + 1 < num_groups {
598 group_boundaries[target_group + 1]
599 } else {
600 partition_size
601 }
602 }
603 }
604 }
605 }
606}
607
608fn build_group_boundaries<F>(
610 partition: &Partition,
611 order_items: &[OrderByItem],
612 eval_fn: &F,
613) -> Vec<usize>
614where
615 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
616{
617 let mut boundaries = vec![0];
618
619 for i in 1..partition.len() {
620 let prev_row = &partition.rows[i - 1];
621 let curr_row = &partition.rows[i];
622
623 let mut is_new_group = false;
625 for item in order_items {
626 let prev_val = eval_fn(&item.expr, prev_row).unwrap_or(SqlValue::Null);
627 let curr_val = eval_fn(&item.expr, curr_row).unwrap_or(SqlValue::Null);
628 if compare_values(&prev_val, &curr_val) != Ordering::Equal {
629 is_new_group = true;
630 break;
631 }
632 }
633
634 if is_new_group {
635 boundaries.push(i);
636 }
637 }
638
639 boundaries
640}
641
642fn find_group_for_row(group_boundaries: &[usize], row_idx: usize) -> usize {
644 for (i, &boundary) in group_boundaries.iter().enumerate().rev() {
645 if row_idx >= boundary {
646 return i;
647 }
648 }
649 0
650}
651
652fn find_first_peer<F>(
654 partition: &Partition,
655 current_row_idx: usize,
656 order_items: &[OrderByItem],
657 eval_fn: &F,
658) -> usize
659where
660 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
661{
662 let current_row = &partition.rows[current_row_idx];
663
664 for i in (0..current_row_idx).rev() {
665 let row = &partition.rows[i];
666 let mut is_peer = true;
667
668 for item in order_items {
669 let curr_val = eval_fn(&item.expr, current_row).unwrap_or(SqlValue::Null);
670 let row_val = eval_fn(&item.expr, row).unwrap_or(SqlValue::Null);
671 if compare_values(&curr_val, &row_val) != Ordering::Equal {
672 is_peer = false;
673 break;
674 }
675 }
676
677 if !is_peer {
678 return i + 1;
679 }
680 }
681
682 0
683}
684
685fn find_last_peer<F>(
687 partition: &Partition,
688 current_row_idx: usize,
689 order_items: &[OrderByItem],
690 eval_fn: &F,
691) -> usize
692where
693 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
694{
695 let current_row = &partition.rows[current_row_idx];
696
697 for i in (current_row_idx + 1)..partition.len() {
698 let row = &partition.rows[i];
699 let mut is_peer = true;
700
701 for item in order_items {
702 let curr_val = eval_fn(&item.expr, current_row).unwrap_or(SqlValue::Null);
703 let row_val = eval_fn(&item.expr, row).unwrap_or(SqlValue::Null);
704 if compare_values(&curr_val, &row_val) != Ordering::Equal {
705 is_peer = false;
706 break;
707 }
708 }
709
710 if !is_peer {
711 return i - 1;
712 }
713 }
714
715 partition.len() - 1
716}
717
718fn find_first_row_at_or_after<F>(
723 partition: &Partition,
724 order_items: &[OrderByItem],
725 target: &SqlValue,
726 eval_fn: &F,
727) -> usize
728where
729 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
730{
731 let item = &order_items[0];
732 let collation = explicit_collation(&item.expr);
733 for (i, row) in partition.rows.iter().enumerate() {
734 let val = eval_fn(&item.expr, row).unwrap_or(SqlValue::Null);
735 if compare_keys_window_order(&val, target, item, collation) != Ordering::Less {
736 return i;
737 }
738 }
739 partition.len()
740}
741
742fn find_last_row_at_or_before<F>(
744 partition: &Partition,
745 order_items: &[OrderByItem],
746 target: &SqlValue,
747 eval_fn: &F,
748) -> Option<usize>
749where
750 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
751{
752 let item = &order_items[0];
753 let collation = explicit_collation(&item.expr);
754 for i in (0..partition.len()).rev() {
755 let val = eval_fn(&item.expr, &partition.rows[i]).unwrap_or(SqlValue::Null);
756 if compare_keys_window_order(&val, target, item, collation) != Ordering::Greater {
757 return Some(i);
758 }
759 }
760 None
761}
762
763fn get_numeric_offset(expr: &Expression) -> f64 {
765 match evaluate_offset_expr(expr) {
766 Ok(Some(n)) => n,
767 _ => 0.0,
768 }
769}
770
771fn subtract_from_value(value: &SqlValue, offset: f64) -> SqlValue {
773 match value {
774 SqlValue::Integer(n) => SqlValue::Real(*n as f64 - offset),
775 SqlValue::Smallint(n) => SqlValue::Real(*n as f64 - offset),
776 SqlValue::Bigint(n) => SqlValue::Real(*n as f64 - offset),
777 SqlValue::Unsigned(n) => SqlValue::Real(*n as f64 - offset),
778 SqlValue::Float(f) => SqlValue::Real(*f as f64 - offset),
779 SqlValue::Real(f) => SqlValue::Real(*f - offset),
780 SqlValue::Double(f) => SqlValue::Real(*f - offset),
781 SqlValue::Numeric(f) => SqlValue::Real(*f - offset),
782 _ => value.clone(), }
784}
785
786fn add_to_value(value: &SqlValue, offset: f64) -> SqlValue {
788 match value {
789 SqlValue::Integer(n) => SqlValue::Real(*n as f64 + offset),
790 SqlValue::Smallint(n) => SqlValue::Real(*n as f64 + offset),
791 SqlValue::Bigint(n) => SqlValue::Real(*n as f64 + offset),
792 SqlValue::Unsigned(n) => SqlValue::Real(*n as f64 + offset),
793 SqlValue::Float(f) => SqlValue::Real(*f as f64 + offset),
794 SqlValue::Real(f) => SqlValue::Real(*f + offset),
795 SqlValue::Double(f) => SqlValue::Real(*f + offset),
796 SqlValue::Numeric(f) => SqlValue::Real(*f + offset),
797 _ => value.clone(), }
799}
800
801fn calculate_rows_boundary(
807 bound: &FrameBound,
808 current_row_idx: usize,
809 partition_size: usize,
810 is_start: bool,
811) -> usize {
812 match bound {
813 FrameBound::UnboundedPreceding => 0,
814
815 FrameBound::UnboundedFollowing => partition_size,
816
817 FrameBound::CurrentRow => {
818 if is_start {
819 current_row_idx
820 } else {
821 current_row_idx + 1 }
823 }
824
825 FrameBound::Preceding(offset_expr) => {
826 let offset = get_numeric_offset(offset_expr) as usize;
827 if is_start {
828 current_row_idx.saturating_sub(offset)
829 } else {
830 if offset > current_row_idx {
834 0
835 } else {
836 current_row_idx - offset + 1
837 }
838 }
839 }
840
841 FrameBound::Following(offset_expr) => {
842 let offset = get_numeric_offset(offset_expr) as usize;
843 let result = current_row_idx + offset;
844
845 if is_start {
846 result.min(partition_size)
847 } else {
848 (result + 1).min(partition_size) }
850 }
851 }
852}
853
854#[derive(Debug, Clone)]
856pub struct FrameResult {
857 pub range: Range<usize>,
859 pub exclude: Option<FrameExclude>,
861 pub current_row_idx: usize,
863}
864
865impl FrameResult {
866 pub fn includes<F>(
870 &self,
871 row_idx: usize,
872 partition: &Partition,
873 order_by: &Option<Vec<OrderByItem>>,
874 eval_fn: &F,
875 ) -> bool
876 where
877 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
878 {
879 if !self.range.contains(&row_idx) {
881 return false;
882 }
883
884 match self.exclude {
886 None | Some(FrameExclude::NoOthers) => true,
887
888 Some(FrameExclude::CurrentRow) => row_idx != self.current_row_idx,
889
890 Some(FrameExclude::Group) => {
891 !is_peer(row_idx, self.current_row_idx, partition, order_by, eval_fn)
893 }
894
895 Some(FrameExclude::Ties) => {
896 row_idx == self.current_row_idx
898 || !is_peer(row_idx, self.current_row_idx, partition, order_by, eval_fn)
899 }
900 }
901 }
902
903 pub fn included_indices<'a, F>(
907 &'a self,
908 partition: &'a Partition,
909 order_by: &'a Option<Vec<OrderByItem>>,
910 eval_fn: &'a F,
911 ) -> impl Iterator<Item = usize> + 'a
912 where
913 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
914 {
915 self.range.clone().filter(move |&idx| self.includes(idx, partition, order_by, eval_fn))
916 }
917}
918
919pub fn calculate_frame_with_exclusion<F>(
923 partition: &Partition,
924 current_row_idx: usize,
925 order_by: &Option<Vec<OrderByItem>>,
926 frame_spec: &Option<WindowFrame>,
927 eval_fn: &F,
928) -> FrameResult
929where
930 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
931{
932 let range = calculate_frame(partition, current_row_idx, order_by, frame_spec, eval_fn);
933 let exclude = frame_spec.as_ref().and_then(|f| f.exclude);
934
935 FrameResult { range, exclude, current_row_idx }
936}
937
938fn is_peer<F>(
943 row_idx_a: usize,
944 row_idx_b: usize,
945 partition: &Partition,
946 order_by: &Option<Vec<OrderByItem>>,
947 eval_fn: &F,
948) -> bool
949where
950 F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
951{
952 if row_idx_a == row_idx_b {
954 return true;
955 }
956
957 let order_items = match order_by {
959 Some(items) if !items.is_empty() => items,
960 _ => return true,
961 };
962
963 if row_idx_a >= partition.len() || row_idx_b >= partition.len() {
965 return false;
966 }
967
968 let row_a = &partition.rows[row_idx_a];
969 let row_b = &partition.rows[row_idx_b];
970
971 for order_item in order_items {
973 let val_a = eval_fn(&order_item.expr, row_a).unwrap_or(SqlValue::Null);
974 let val_b = eval_fn(&order_item.expr, row_b).unwrap_or(SqlValue::Null);
975
976 if compare_values(&val_a, &val_b) != Ordering::Equal {
977 return false;
978 }
979 }
980
981 true
982}