Add validation for constraint usage length returned by best_index

Additional changes:
- Update IndexInfo documentation to clarify that constraint_usages must
  have exact 1:1 correspondence with input ConstraintInfo array. The code
  translating constraints into VFilter arguments heavily relies on this.
- Fix best_index implementation in test extension to comply with new
  validation requirements by returning usage entry for each constraint
This commit is contained in:
Piotr Rzysko
2025-07-30 20:28:24 +02:00
parent 7045d44fdc
commit c6f398122d
3 changed files with 49 additions and 27 deletions

View File

@@ -14,7 +14,7 @@ use crate::{
insn::{CmpInsFlags, IdxInsertFlags, Insn},
BranchOffset, CursorID,
},
Result,
LimboError, Result,
};
use super::{
@@ -483,6 +483,14 @@ pub fn open_loop(
// maybe encode more info on t_ctx? we need: [col_idx, is_descending]
let index_info = vtab.best_index(&converted_constraints, &[]);
if index_info.constraint_usages.len() != converted_constraints.len() {
return Err(LimboError::ExtensionError(format!(
"Constraint usage count mismatch (expected {}, got {})",
converted_constraints.len(),
index_info.constraint_usages.len()
)));
}
// Determine the number of VFilter arguments (constraints with an argv_index).
let args_needed = index_info
.constraint_usages

View File

@@ -222,6 +222,8 @@ pub struct IndexInfo {
/// Estimated number of rows that the query will return
pub estimated_rows: u32,
/// List of constraints that can be used to optimize the query.
/// Each `ConstraintInfo` passed to `best_index` must have a corresponding entry in `constraint_usages`.
/// The length and order are important—they must exactly match the input `ConstraintInfo` array.
pub constraint_usages: Vec<ConstraintUsage>,
}
impl Default for IndexInfo {

View File

@@ -168,43 +168,55 @@ impl VTable for KVStoreTable {
}
fn best_index(constraints: &[ConstraintInfo], _order_by: &[OrderByInfo]) -> IndexInfo {
let mut constraint_usages = Vec::with_capacity(constraints.len());
let mut idx_num = -1;
let mut idx_str = None;
let mut estimated_cost = 1000.0;
let mut estimated_rows = u32::MAX;
// Look for: key = ?
for constraint in constraints.iter() {
if constraint.usable
&& constraint.op == ConstraintOp::Eq
&& constraint.column_index == 1
&& idx_num == -1
// Only use the first usable key = ? constraint
{
// this extension wouldn't support order by but for testing purposes,
// we will consume it if we find an ASC order by clause on the value column
let mut consumed = false;
if let Some(order) = _order_by.first() {
if order.column_index == 2 && !order.desc {
consumed = true;
}
}
constraint_usages.push(ConstraintUsage {
omit: true,
argv_index: Some(1),
});
idx_num = 1;
idx_str = Some("key_eq".to_string());
estimated_cost = 10.0;
estimated_rows = 4;
log::debug!("xBestIndex: constraint found for 'key = ?'");
return IndexInfo {
idx_num: 1,
idx_str: Some("key_eq".to_string()),
order_by_consumed: consumed,
estimated_cost: 10.0,
estimated_rows: 4,
constraint_usages: vec![ConstraintUsage {
omit: true,
argv_index: Some(1),
}],
};
} else {
constraint_usages.push(ConstraintUsage {
omit: false,
argv_index: None,
});
}
}
// fallback: full scan
log::debug!("No usable constraints found, using full scan");
// this extension wouldn't support order by but for testing purposes,
// we will consume it if we find an ASC order by clause on the value column
let order_by_consumed = idx_num == 1
&& _order_by
.first()
.is_some_and(|order| order.column_index == 2 && !order.desc);
if idx_num == -1 {
log::debug!("No usable constraints found, using full scan");
}
IndexInfo {
idx_num: -1,
idx_str: None,
order_by_consumed: false,
estimated_cost: 1000.0,
..Default::default()
idx_num,
idx_str,
order_by_consumed,
estimated_cost,
estimated_rows,
constraint_usages,
}
}