From 1189b7a288a4afae6969ac2487f058cb494e54c6 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 13 Apr 2025 15:18:15 +0300 Subject: [PATCH] codegen: add support for descending indexes --- core/translate/main_loop.rs | 79 ++++++-- core/translate/optimizer.rs | 375 +++++++++++++++++++++++++++--------- core/translate/plan.rs | 46 ++--- core/vdbe/execute.rs | 30 +-- 4 files changed, 377 insertions(+), 153 deletions(-) diff --git a/core/translate/main_loop.rs b/core/translate/main_loop.rs index 1b709e0d3..76057c53b 100644 --- a/core/translate/main_loop.rs +++ b/core/translate/main_loop.rs @@ -845,27 +845,37 @@ fn emit_seek( is_index: bool, ) -> Result<()> { let Some(seek) = seek_def.seek.as_ref() else { - assert!(seek_def.iter_dir == IterationDirection::Backwards, "A SeekDef without a seek operation should only be used in backwards iteration direction"); - program.emit_insn(Insn::Last { - cursor_id: seek_cursor_id, - pc_if_empty: loop_end, - }); + // If there is no seek key, we start from the first or last row of the index, + // depending on the iteration direction. + match seek_def.iter_dir { + IterationDirection::Forwards => { + program.emit_insn(Insn::Rewind { + cursor_id: seek_cursor_id, + pc_if_empty: loop_end, + }); + } + IterationDirection::Backwards => { + program.emit_insn(Insn::Last { + cursor_id: seek_cursor_id, + pc_if_empty: loop_end, + }); + } + } return Ok(()); }; // We allocated registers for the full index key, but our seek key might not use the full index key. - // Later on for the termination condition we will overwrite the NULL registers. // See [crate::translate::optimizer::build_seek_def] for more details about in which cases we do and don't use the full index key. for i in 0..seek_def.key.len() { let reg = start_reg + i; if i >= seek.len { - if seek_def.null_pad_unset_cols() { + if seek.null_pad { program.emit_insn(Insn::Null { dest: reg, dest_end: None, }); } } else { - let expr = &seek_def.key[i]; + let expr = &seek_def.key[i].0; translate_expr(program, Some(tables), &expr, reg, &t_ctx.resolver)?; // If the seek key column is not verifiably non-NULL, we need check whether it is NULL, // and if so, jump to the loop end. @@ -879,7 +889,7 @@ fn emit_seek( } } } - let num_regs = if seek_def.null_pad_unset_cols() { + let num_regs = if seek.null_pad { seek_def.key.len() } else { seek.len @@ -943,19 +953,46 @@ fn emit_seek_termination( program.resolve_label(loop_start, program.offset()); return Ok(()); }; - let num_regs = termination.len; - // If the seek termination was preceded by a seek (which happens in most cases), - // we can re-use the registers that were allocated for the full index key. - let start_idx = seek_def.seek.as_ref().map_or(0, |seek| seek.len); - for i in start_idx..termination.len { + + // How many non-NULL values were used for seeking. + let seek_len = seek_def.seek.as_ref().map_or(0, |seek| seek.len); + + // How many values will be used for the termination condition. + let num_regs = if termination.null_pad { + seek_def.key.len() + } else { + termination.len + }; + for i in 0..seek_def.key.len() { let reg = start_reg + i; - translate_expr( - program, - Some(tables), - &seek_def.key[i], - reg, - &t_ctx.resolver, - )?; + let is_last = i == seek_def.key.len() - 1; + + // For all index key values apart from the last one, we are guaranteed to use the same values + // as were used for the seek, so we don't need to emit them again. + if i < seek_len && !is_last { + continue; + } + // For the last index key value, we need to emit a NULL if the termination condition is NULL-padded. + // See [SeekKey::null_pad] and [crate::translate::optimizer::build_seek_def] for why this is the case. + if i >= termination.len && !termination.null_pad { + continue; + } + if is_last && termination.null_pad { + program.emit_insn(Insn::Null { + dest: reg, + dest_end: None, + }); + // if the seek key is shorter than the termination key, we need to translate the remaining suffix of the termination key. + // if not, we just reuse what was emitted for the seek. + } else if seek_len < termination.len { + translate_expr( + program, + Some(tables), + &seek_def.key[i].0, + reg, + &t_ctx.resolver, + )?; + } } program.resolve_label(loop_start, program.offset()); let mut rowid_reg = None; diff --git a/core/translate/optimizer.rs b/core/translate/optimizer.rs index c4bf12810..80875da91 100644 --- a/core/translate/optimizer.rs +++ b/core/translate/optimizer.rs @@ -779,6 +779,7 @@ pub fn try_extract_index_search_from_where_clause( pub struct IndexConstraint { position_in_where_clause: (usize, BinaryExprSide), operator: ast::Operator, + index_column_sort_order: SortOrder, } /// Helper enum for [IndexConstraint] to indicate which side of a binary comparison expression is being compared to the index column. @@ -898,6 +899,7 @@ fn find_index_constraints( out_constraints.push(IndexConstraint { operator: *operator, position_in_where_clause: (position_in_where_clause, BinaryExprSide::Rhs), + index_column_sort_order: index.columns[position_in_index].order, }); found = true; break; @@ -907,6 +909,7 @@ fn find_index_constraints( out_constraints.push(IndexConstraint { operator: opposite_cmp_op(*operator), // swap the operator since e.g. if condition is 5 >= x, we want to use x <= 5 position_in_where_clause: (position_in_where_clause, BinaryExprSide::Lhs), + index_column_sort_order: index.columns[position_in_index].order, }); found = true; break; @@ -963,7 +966,7 @@ pub fn build_seek_def_from_index_constraints( } else { *rhs }; - key.push(cmp_expr); + key.push((cmp_expr, constraint.index_column_sort_order)); } // We know all but potentially the last term is an equality, so we can use the operator of the last term @@ -995,46 +998,80 @@ pub fn build_seek_def_from_index_constraints( /// 2. In contrast, having (x=10 AND y>20) forms a valid index key GT(x:10, y:20) because after the seek, we can simply terminate as soon as x > 10, /// i.e. use GT(x:10, y:20) as the [SeekKey] and GT(x:10) as the [TerminationKey]. /// +/// The preceding examples are for an ascending index. The logic is similar for descending indexes, but an important distinction is that +/// since a descending index is laid out in reverse order, the comparison operators are reversed, e.g. LT becomes GT, LE becomes GE, etc. +/// So when you see e.g. a SeekOp::GT below for a descending index, it actually means that we are seeking the first row where the index key is LESS than the seek key. +/// fn build_seek_def( op: ast::Operator, iter_dir: IterationDirection, - key: Vec, + key: Vec<(ast::Expr, SortOrder)>, ) -> Result { let key_len = key.len(); + let sort_order_of_last_key = key.last().unwrap().1; + + // For the commented examples below, keep in mind that since a descending index is laid out in reverse order, the comparison operators are reversed, e.g. LT becomes GT, LE becomes GE, etc. + // Also keep in mind that index keys are compared based on the number of columns given, so for example: + // - if key is GT(x:10), then (x=10, y=usize::MAX) is not GT because only X is compared. (x=11, y=) is GT. + // - if key is GT(x:10, y:20), then (x=10, y=21) is GT because both X and Y are compared. + // - if key is GT(x:10, y:NULL), then (x=10, y=0) is GT because NULL is always LT in index key comparisons. Ok(match (iter_dir, op) { // Forwards, EQ: // Example: (x=10 AND y=20) - // Seek key: GE(x:10, y:20) - // Termination key: GT(x:10, y:20) + // Seek key: start from the first GE(x:10, y:20) + // Termination key: end at the first GT(x:10, y:20) + // Ascending vs descending doesn't matter because all the comparisons are equalities. (IterationDirection::Forwards, ast::Operator::Equals) => SeekDef { key, iter_dir, seek: Some(SeekKey { len: key_len, + null_pad: false, op: SeekOp::GE, }), termination: Some(TerminationKey { len: key_len, + null_pad: false, op: SeekOp::GT, }), }, // Forwards, GT: - // Example: (x=10 AND y>20) - // Seek key: GT(x:10, y:20) - // Termination key: GT(x:10) + // Ascending index example: (x=10 AND y>20) + // Seek key: start from the first GT(x:10, y:20), e.g. (x=10, y=21) + // Termination key: end at the first GT(x:10), e.g. (x=11, y=0) + // + // Descending index example: (x=10 AND y>20) + // Seek key: start from the first LE(x:10), e.g. (x=10, y=usize::MAX), so reversed -> GE(x:10) + // Termination key: end at the first LE(x:10, y:20), e.g. (x=10, y=20) so reversed -> GE(x:10, y:20) (IterationDirection::Forwards, ast::Operator::Greater) => { - let termination_key_len = key_len - 1; + let (seek_key_len, termination_key_len, seek_op, termination_op) = + if sort_order_of_last_key == SortOrder::Asc { + (key_len, key_len - 1, SeekOp::GT, SeekOp::GT) + } else { + ( + key_len - 1, + key_len, + SeekOp::LE.reverse(), + SeekOp::LE.reverse(), + ) + }; SeekDef { key, iter_dir, - seek: Some(SeekKey { - len: key_len, - op: SeekOp::GT, - }), + seek: if seek_key_len > 0 { + Some(SeekKey { + len: seek_key_len, + op: seek_op, + null_pad: false, + }) + } else { + None + }, termination: if termination_key_len > 0 { Some(TerminationKey { len: termination_key_len, - op: SeekOp::GT, + op: termination_op, + null_pad: false, }) } else { None @@ -1042,22 +1079,42 @@ fn build_seek_def( } } // Forwards, GE: - // Example: (x=10 AND y>=20) - // Seek key: GE(x:10, y:20) - // Termination key: GT(x:10) + // Ascending index example: (x=10 AND y>=20) + // Seek key: start from the first GE(x:10, y:20), e.g. (x=10, y=20) + // Termination key: end at the first GT(x:10), e.g. (x=11, y=0) + // + // Descending index example: (x=10 AND y>=20) + // Seek key: start from the first LE(x:10), e.g. (x=10, y=usize::MAX), so reversed -> GE(x:10) + // Termination key: end at the first LT(x:10, y:20), e.g. (x=10, y=19), so reversed -> GT(x:10, y:20) (IterationDirection::Forwards, ast::Operator::GreaterEquals) => { - let termination_key_len = key_len - 1; + let (seek_key_len, termination_key_len, seek_op, termination_op) = + if sort_order_of_last_key == SortOrder::Asc { + (key_len, key_len - 1, SeekOp::GE, SeekOp::GT) + } else { + ( + key_len - 1, + key_len, + SeekOp::LE.reverse(), + SeekOp::LT.reverse(), + ) + }; SeekDef { key, iter_dir, - seek: Some(SeekKey { - len: key_len, - op: SeekOp::GE, - }), + seek: if seek_key_len > 0 { + Some(SeekKey { + len: seek_key_len, + op: seek_op, + null_pad: false, + }) + } else { + None + }, termination: if termination_key_len > 0 { Some(TerminationKey { len: termination_key_len, - op: SeekOp::GT, + op: termination_op, + null_pad: false, }) } else { None @@ -1065,70 +1122,142 @@ fn build_seek_def( } } // Forwards, LT: - // Example: (x=10 AND y<20) - // Seek key: GT(x:10, y: NULL) // NULL is always LT, indicating we only care about x - // Termination key: GE(x:10, y:20) - (IterationDirection::Forwards, ast::Operator::Less) => SeekDef { - key, - iter_dir, - seek: Some(SeekKey { - len: key_len - 1, - op: SeekOp::GT, - }), - termination: Some(TerminationKey { - len: key_len, - op: SeekOp::GE, - }), - }, + // Ascending index example: (x=10 AND y<20) + // Seek key: start from the first GT(x:10, y: NULL), e.g. (x=10, y=0) + // Termination key: end at the first GE(x:10, y:20), e.g. (x=10, y=20) + // + // Descending index example: (x=10 AND y<20) + // Seek key: start from the first LT(x:10, y:20), e.g. (x=10, y=19), so reversed -> GT(x:10, y:20) + // Termination key: end at the first LT(x:10), e.g. (x=9, y=usize::MAX), so reversed -> GE(x:10, NULL); i.e. GE the smallest possible (x=10, y) combination (NULL is always LT) + (IterationDirection::Forwards, ast::Operator::Less) => { + let (seek_key_len, termination_key_len, seek_op, termination_op) = + if sort_order_of_last_key == SortOrder::Asc { + (key_len - 1, key_len, SeekOp::GT, SeekOp::GE) + } else { + (key_len, key_len - 1, SeekOp::GT, SeekOp::GE) + }; + SeekDef { + key, + iter_dir, + seek: if seek_key_len > 0 { + Some(SeekKey { + len: seek_key_len, + op: seek_op, + null_pad: sort_order_of_last_key == SortOrder::Asc, + }) + } else { + None + }, + termination: if termination_key_len > 0 { + Some(TerminationKey { + len: termination_key_len, + op: termination_op, + null_pad: sort_order_of_last_key == SortOrder::Desc, + }) + } else { + None + }, + } + } // Forwards, LE: - // Example: (x=10 AND y<=20) - // Seek key: GE(x:10, y:NULL) // NULL is always LT, indicating we only care about x - // Termination key: GT(x:10, y:20) - (IterationDirection::Forwards, ast::Operator::LessEquals) => SeekDef { - key, - iter_dir, - seek: Some(SeekKey { - len: key_len - 1, - op: SeekOp::GE, - }), - termination: Some(TerminationKey { - len: key_len, - op: SeekOp::GT, - }), - }, + // Ascending index example: (x=10 AND y<=20) + // Seek key: start from the first GE(x:10, y:NULL), e.g. (x=10, y=0) + // Termination key: end at the first GT(x:10, y:20), e.g. (x=10, y=21) + // + // Descending index example: (x=10 AND y<=20) + // Seek key: start from the first LE(x:10, y:20), e.g. (x=10, y=20) so reversed -> GE(x:10, y:20) + // Termination key: end at the first LT(x:10), e.g. (x=9, y=usize::MAX), so reversed -> GE(x:10, NULL); i.e. GE the smallest possible (x=10, y) combination (NULL is always LT) + (IterationDirection::Forwards, ast::Operator::LessEquals) => { + let (seek_key_len, termination_key_len, seek_op, termination_op) = + if sort_order_of_last_key == SortOrder::Asc { + (key_len - 1, key_len, SeekOp::GT, SeekOp::GT) + } else { + ( + key_len, + key_len - 1, + SeekOp::LE.reverse(), + SeekOp::LE.reverse(), + ) + }; + SeekDef { + key, + iter_dir, + seek: if seek_key_len > 0 { + Some(SeekKey { + len: seek_key_len, + op: seek_op, + null_pad: sort_order_of_last_key == SortOrder::Asc, + }) + } else { + None + }, + termination: if termination_key_len > 0 { + Some(TerminationKey { + len: termination_key_len, + op: termination_op, + null_pad: sort_order_of_last_key == SortOrder::Desc, + }) + } else { + None + }, + } + } // Backwards, EQ: // Example: (x=10 AND y=20) - // Seek key: LE(x:10, y:20) - // Termination key: LT(x:10, y:20) + // Seek key: start from the last LE(x:10, y:20) + // Termination key: end at the first LT(x:10, y:20) + // Ascending vs descending doesn't matter because all the comparisons are equalities. (IterationDirection::Backwards, ast::Operator::Equals) => SeekDef { key, iter_dir, seek: Some(SeekKey { len: key_len, op: SeekOp::LE, + null_pad: false, }), termination: Some(TerminationKey { len: key_len, op: SeekOp::LT, + null_pad: false, }), }, // Backwards, LT: - // Example: (x=10 AND y<20) - // Seek key: LT(x:10, y:20) - // Termination key: LT(x:10) + // Ascending index example: (x=10 AND y<20) + // Seek key: start from the last LT(x:10, y:20), e.g. (x=10, y=19) + // Termination key: end at the first LE(x:10, NULL), e.g. (x=9, y=usize::MAX) + // + // Descending index example: (x=10 AND y<20) + // Seek key: start from the last GT(x:10, y:NULL), e.g. (x=10, y=0) so reversed -> LT(x:10, NULL) + // Termination key: end at the first GE(x:10, y:20), e.g. (x=10, y=20) so reversed -> LE(x:10, y:20) (IterationDirection::Backwards, ast::Operator::Less) => { - let termination_key_len = key_len - 1; + let (seek_key_len, termination_key_len, seek_op, termination_op) = + if sort_order_of_last_key == SortOrder::Asc { + (key_len, key_len - 1, SeekOp::LT, SeekOp::LE) + } else { + ( + key_len - 1, + key_len, + SeekOp::GT.reverse(), + SeekOp::GE.reverse(), + ) + }; SeekDef { key, iter_dir, - seek: Some(SeekKey { - len: key_len, - op: SeekOp::LT, - }), + seek: if seek_key_len > 0 { + Some(SeekKey { + len: seek_key_len, + op: seek_op, + null_pad: sort_order_of_last_key == SortOrder::Desc, + }) + } else { + None + }, termination: if termination_key_len > 0 { Some(TerminationKey { len: termination_key_len, - op: SeekOp::LT, + op: termination_op, + null_pad: sort_order_of_last_key == SortOrder::Asc, }) } else { None @@ -1136,22 +1265,42 @@ fn build_seek_def( } } // Backwards, LE: - // Example: (x=10 AND y<=20) - // Seek key: LE(x:10, y:20) - // Termination key: LT(x:10) + // Ascending index example: (x=10 AND y<=20) + // Seek key: start from the last LE(x:10, y:20), e.g. (x=10, y=20) + // Termination key: end at the first LT(x:10, NULL), e.g. (x=9, y=usize::MAX) + // + // Descending index example: (x=10 AND y<=20) + // Seek key: start from the last GT(x:10, NULL), e.g. (x=10, y=0) so reversed -> LT(x:10, NULL) + // Termination key: end at the first GT(x:10, y:20), e.g. (x=10, y=21) so reversed -> LT(x:10, y:20) (IterationDirection::Backwards, ast::Operator::LessEquals) => { - let termination_key_len = key_len - 1; + let (seek_key_len, termination_key_len, seek_op, termination_op) = + if sort_order_of_last_key == SortOrder::Asc { + (key_len, key_len - 1, SeekOp::LE, SeekOp::LE) + } else { + ( + key_len - 1, + key_len, + SeekOp::GT.reverse(), + SeekOp::GT.reverse(), + ) + }; SeekDef { key, iter_dir, - seek: Some(SeekKey { - len: key_len, - op: SeekOp::LE, - }), + seek: if seek_key_len > 0 { + Some(SeekKey { + len: seek_key_len, + op: seek_op, + null_pad: sort_order_of_last_key == SortOrder::Desc, + }) + } else { + None + }, termination: if termination_key_len > 0 { Some(TerminationKey { len: termination_key_len, - op: SeekOp::LT, + op: termination_op, + null_pad: sort_order_of_last_key == SortOrder::Asc, }) } else { None @@ -1159,49 +1308,89 @@ fn build_seek_def( } } // Backwards, GT: - // Example: (x=10 AND y>20) - // Seek key: LE(x:10) // try to find the last row where x = 10, not considering y at all. - // Termination key: LE(x:10, y:20) + // Ascending index example: (x=10 AND y>20) + // Seek key: start from the last LE(x:10), e.g. (x=10, y=usize::MAX) + // Termination key: end at the first LE(x:10, y:20), e.g. (x=10, y=20) + // + // Descending index example: (x=10 AND y>20) + // Seek key: start from the last GT(x:10, y:20), e.g. (x=10, y=21) so reversed -> LT(x:10, y:20) + // Termination key: end at the first GT(x:10), e.g. (x=11, y=0) so reversed -> LT(x:10) (IterationDirection::Backwards, ast::Operator::Greater) => { - let seek_key_len = key_len - 1; + let (seek_key_len, termination_key_len, seek_op, termination_op) = + if sort_order_of_last_key == SortOrder::Asc { + (key_len - 1, key_len, SeekOp::LE, SeekOp::LE) + } else { + ( + key_len, + key_len - 1, + SeekOp::GT.reverse(), + SeekOp::GT.reverse(), + ) + }; SeekDef { key, iter_dir, seek: if seek_key_len > 0 { Some(SeekKey { len: seek_key_len, - op: SeekOp::LE, + op: seek_op, + null_pad: false, + }) + } else { + None + }, + termination: if termination_key_len > 0 { + Some(TerminationKey { + len: termination_key_len, + op: termination_op, + null_pad: false, }) } else { None }, - termination: Some(TerminationKey { - len: key_len, - op: SeekOp::LE, - }), } } // Backwards, GE: - // Example: (x=10 AND y>=20) - // Seek key: LE(x:10) // try to find the last row where x = 10, not considering y at all. - // Termination key: LT(x:10, y:20) + // Ascending index example: (x=10 AND y>=20) + // Seek key: start from the last LE(x:10), e.g. (x=10, y=usize::MAX) + // Termination key: end at the first LT(x:10, y:20), e.g. (x=10, y=19) + // + // Descending index example: (x=10 AND y>=20) + // Seek key: start from the last GE(x:10, y:20), e.g. (x=10, y=20) so reversed -> LE(x:10, y:20) + // Termination key: end at the first GT(x:10), e.g. (x=11, y=0) so reversed -> LT(x:10) (IterationDirection::Backwards, ast::Operator::GreaterEquals) => { - let seek_key_len = key_len - 1; + let (seek_key_len, termination_key_len, seek_op, termination_op) = + if sort_order_of_last_key == SortOrder::Asc { + (key_len - 1, key_len, SeekOp::LE, SeekOp::LT) + } else { + ( + key_len, + key_len - 1, + SeekOp::GE.reverse(), + SeekOp::GT.reverse(), + ) + }; SeekDef { key, iter_dir, seek: if seek_key_len > 0 { Some(SeekKey { len: seek_key_len, - op: SeekOp::LE, + op: seek_op, + null_pad: false, + }) + } else { + None + }, + termination: if termination_key_len > 0 { + Some(TerminationKey { + len: termination_key_len, + op: termination_op, + null_pad: false, }) } else { None }, - termination: Some(TerminationKey { - len: key_len, - op: SeekOp::LT, - }), } } (_, op) => { @@ -1252,7 +1441,8 @@ pub fn try_extract_rowid_search_expression( | ast::Operator::Less | ast::Operator::LessEquals => { let rhs_owned = rhs.take_ownership(); - let seek_def = build_seek_def(*operator, iter_dir, vec![rhs_owned])?; + let seek_def = + build_seek_def(*operator, iter_dir, vec![(rhs_owned, SortOrder::Asc)])?; return Ok(Some(Search::Seek { index: None, seek_def, @@ -1280,7 +1470,8 @@ pub fn try_extract_rowid_search_expression( | ast::Operator::LessEquals => { let lhs_owned = lhs.take_ownership(); let op = opposite_cmp_op(*operator); - let seek_def = build_seek_def(op, iter_dir, vec![lhs_owned])?; + let seek_def = + build_seek_def(op, iter_dir, vec![(lhs_owned, SortOrder::Asc)])?; return Ok(Some(Search::Seek { index: None, seek_def, diff --git a/core/translate/plan.rs b/core/translate/plan.rs index 038dd90ee..bb581ab13 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -1,5 +1,5 @@ use core::fmt; -use limbo_sqlite3_parser::ast; +use limbo_sqlite3_parser::ast::{self, SortOrder}; use std::{ cmp::Ordering, fmt::{Display, Formatter}, @@ -391,10 +391,10 @@ impl TableReference { pub struct SeekDef { /// The key to use when seeking and when terminating the scan that follows the seek. /// For example, given: - /// - CREATE INDEX i ON t (x, y) + /// - CREATE INDEX i ON t (x, y desc) /// - SELECT * FROM t WHERE x = 1 AND y >= 30 - /// The key is [1, 30] - pub key: Vec, + /// The key is [(1, ASC), (30, DESC)] + pub key: Vec<(ast::Expr, SortOrder)>, /// The condition to use when seeking. See [SeekKey] for more details. pub seek: Option, /// The condition to use when terminating the scan that follows the seek. See [TerminationKey] for more details. @@ -403,35 +403,22 @@ pub struct SeekDef { pub iter_dir: IterationDirection, } -impl SeekDef { - /// Whether we should null pad unset columns when seeking. - /// This is only done for forward seeks. - /// The reason it is done is that sometimes our full index key is not used in seeking. - /// See [SeekKey] for more details. - /// - /// For example, given: - /// - CREATE INDEX i ON t (x, y) - /// - SELECT * FROM t WHERE x = 1 AND y < 30 - /// We want to seek to the first row where x = 1, and then iterate forwards. - /// In this case, the seek key is GT(1, NULL) since '30' cannot be used to seek (since we want y < 30), - /// and any value of y will be greater than NULL. - /// - /// In backwards iteration direction, we do not null pad because we want to seek to the last row that matches the seek key. - /// For example, given: - /// - CREATE INDEX i ON t (x, y) - /// - SELECT * FROM t WHERE x = 1 AND y > 30 ORDER BY y - /// We want to seek to the last row where x = 1, and then iterate backwards. - /// In this case, the seek key is just LE(1) so any row with x = 1 will be a match. - pub fn null_pad_unset_cols(&self) -> bool { - self.iter_dir == IterationDirection::Forwards - } -} - /// A condition to use when seeking. #[derive(Debug, Clone)] pub struct SeekKey { /// How many columns from [SeekDef::key] are used in seeking. pub len: usize, + /// Whether to NULL pad the last column of the seek key to match the length of [SeekDef::key]. + /// The reason it is done is that sometimes our full index key is not used in seeking, + /// but we want to find the lowest value that matches the non-null prefix of the key. + /// For example, given: + /// - CREATE INDEX i ON t (x, y) + /// - SELECT * FROM t WHERE x = 1 AND y < 30 + /// We want to seek to the first row where x = 1, and then iterate forwards. + /// In this case, the seek key is GT(1, NULL) since NULL is always LT in index key comparisons. + /// We can't use just GT(1) because in index key comparisons, only the given number of columns are compared, + /// so this means any index keys with (x=1) will compare equal, e.g. (x=1, y=usize::MAX) will compare equal to the seek key (x:1) + pub null_pad: bool, /// The comparison operator to use when seeking. pub op: SeekOp, } @@ -441,6 +428,9 @@ pub struct SeekKey { pub struct TerminationKey { /// How many columns from [SeekDef::key] are used in terminating the scan that follows the seek. pub len: usize, + /// Whether to NULL pad the last column of the termination key to match the length of [SeekDef::key]. + /// See [SeekKey::null_pad]. + pub null_pad: bool, /// The comparison operator to use when terminating the scan that follows the seek. pub op: SeekOp, } diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index 263ce491a..d00ee6129 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -12,6 +12,7 @@ use crate::{ }, printf::exec_printf, }, + types::compare_immutable, }; use std::{borrow::BorrowMut, rc::Rc, sync::Arc}; @@ -2053,9 +2054,11 @@ pub fn op_idx_ge( let record_from_regs = make_record(&state.registers, start_reg, num_regs); let pc = if let Some(ref idx_record) = *cursor.record() { // Compare against the same number of values - let ord = idx_record.get_values()[..record_from_regs.len()] - .partial_cmp(&record_from_regs.get_values()[..]) - .unwrap(); + let idx_values = idx_record.get_values(); + let idx_values = &idx_values[..record_from_regs.len()]; + let record_values = record_from_regs.get_values(); + let record_values = &record_values[..idx_values.len()]; + let ord = compare_immutable(&idx_values, &record_values, cursor.index_key_sort_order); if ord.is_ge() { target_pc.to_offset_int() } else { @@ -2111,9 +2114,10 @@ pub fn op_idx_le( let record_from_regs = make_record(&state.registers, start_reg, num_regs); let pc = if let Some(ref idx_record) = *cursor.record() { // Compare against the same number of values - let ord = idx_record.get_values()[..record_from_regs.len()] - .partial_cmp(&record_from_regs.get_values()[..]) - .unwrap(); + let idx_values = idx_record.get_values(); + let idx_values = &idx_values[..record_from_regs.len()]; + let record_values = record_from_regs.get_values(); + let ord = compare_immutable(&idx_values, &record_values, cursor.index_key_sort_order); if ord.is_le() { target_pc.to_offset_int() } else { @@ -2151,9 +2155,10 @@ pub fn op_idx_gt( let record_from_regs = make_record(&state.registers, start_reg, num_regs); let pc = if let Some(ref idx_record) = *cursor.record() { // Compare against the same number of values - let ord = idx_record.get_values()[..record_from_regs.len()] - .partial_cmp(&record_from_regs.get_values()[..]) - .unwrap(); + let idx_values = idx_record.get_values(); + let idx_values = &idx_values[..record_from_regs.len()]; + let record_values = record_from_regs.get_values(); + let ord = compare_immutable(&idx_values, &record_values, cursor.index_key_sort_order); if ord.is_gt() { target_pc.to_offset_int() } else { @@ -2191,9 +2196,10 @@ pub fn op_idx_lt( let record_from_regs = make_record(&state.registers, start_reg, num_regs); let pc = if let Some(ref idx_record) = *cursor.record() { // Compare against the same number of values - let ord = idx_record.get_values()[..record_from_regs.len()] - .partial_cmp(&record_from_regs.get_values()[..]) - .unwrap(); + let idx_values = idx_record.get_values(); + let idx_values = &idx_values[..record_from_regs.len()]; + let record_values = record_from_regs.get_values(); + let ord = compare_immutable(&idx_values, &record_values, cursor.index_key_sort_order); if ord.is_lt() { target_pc.to_offset_int() } else {