@@ -11562,52 +11562,53 @@ impl Connection {
1156211562 // component of every secondary index entry, so `x=? AND rowid>?` bounds the
1156311563 // `(x, rowid)` range `[eq…, lo] .. [eq…, hi]` (SQLite renders it the same way).
1156411564 // Superset-safe — `run_core` re-applies the full `WHERE`.
11565- if next_pos == idx_cols.len() {
11566- if let Some(ipk) = meta.ipk {
11567- let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
11568- alloc::collections::BTreeMap::new();
11569- collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
11570- if let Some(b) = ranges.get(&ipk) {
11571- let mut colls = full_colls[..next_pos].to_vec();
11572- colls.push(crate::value::Collation::default());
11573- let mut lo_key = key.clone();
11574- let lo_inc = match b.lower.as_ref() {
11575- Some((v, inc)) => {
11576- lo_key.push(v.clone());
11577- *inc
11578- }
11579- None => true,
11580- };
11581- let mut hi_key = key.clone();
11582- let hi_inc = match b.upper.as_ref() {
11583- Some((v, inc)) => {
11584- hi_key.push(v.clone());
11585- *inc
11586- }
11587- None => true,
11588- };
11589- let rowids = crate::btree::index_range_rowids(
11590- self.backend.source(),
11591- root,
11592- Some((lo_key.as_slice(), lo_inc)),
11593- Some((hi_key.as_slice(), hi_inc)),
11594- &colls,
11595- )?;
11596- let encoding = self.backend.source().header().text_encoding;
11597- let mut cur = TableCursor::new(self.backend.source(), meta.root);
11598- let mut out = Vec::new();
11599- for rid in rowids {
11600- if cur.seek(rid)? {
11601- let values =
11602- self.decode_full_row(meta, rid, &cur.payload()?, encoding)?;
11603- out.push(InputRow {
11604- values,
11605- rowid: Some(rid),
11606- });
11607- }
11565+ if next_pos == idx_cols.len() && meta.ipk.is_some() {
11566+ let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
11567+ alloc::collections::BTreeMap::new();
11568+ collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
11569+ let rowid_bound = meta
11570+ .ipk
11571+ .and_then(|ipk| ranges.remove(&ipk))
11572+ .or_else(|| rowid_alias_range(where_expr, meta, params));
11573+ if let Some(b) = rowid_bound {
11574+ let mut colls = full_colls[..next_pos].to_vec();
11575+ colls.push(crate::value::Collation::default());
11576+ let mut lo_key = key.clone();
11577+ let lo_inc = match b.lower.as_ref() {
11578+ Some((v, inc)) => {
11579+ lo_key.push(v.clone());
11580+ *inc
11581+ }
11582+ None => true,
11583+ };
11584+ let mut hi_key = key.clone();
11585+ let hi_inc = match b.upper.as_ref() {
11586+ Some((v, inc)) => {
11587+ hi_key.push(v.clone());
11588+ *inc
11589+ }
11590+ None => true,
11591+ };
11592+ let rowids = crate::btree::index_range_rowids(
11593+ self.backend.source(),
11594+ root,
11595+ Some((lo_key.as_slice(), lo_inc)),
11596+ Some((hi_key.as_slice(), hi_inc)),
11597+ &colls,
11598+ )?;
11599+ let encoding = self.backend.source().header().text_encoding;
11600+ let mut cur = TableCursor::new(self.backend.source(), meta.root);
11601+ let mut out = Vec::new();
11602+ for rid in rowids {
11603+ if cur.seek(rid)? {
11604+ let values = self.decode_full_row(meta, rid, &cur.payload()?, encoding)?;
11605+ out.push(InputRow {
11606+ values,
11607+ rowid: Some(rid),
11608+ });
1160811609 }
11609- return Ok(Some(out));
1161011610 }
11611+ return Ok(Some(out));
1161111612 }
1161211613 }
1161311614
@@ -14815,18 +14816,20 @@ impl Connection {
1481514816 conds.push(alloc::format!("{name}<?"));
1481614817 }
1481714818 }
14818- } else if matched.len() == idx_cols.len() {
14819- if let Some(ipk) = meta.ipk {
14820- let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
14821- alloc::collections::BTreeMap::new();
14822- collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
14823- if let Some(b) = ranges.get(&ipk) {
14824- if b.lower.is_some() {
14825- conds.push(String::from("rowid>?"));
14826- }
14827- if b.upper.is_some() {
14828- conds.push(String::from("rowid<?"));
14829- }
14819+ } else if matched.len() == idx_cols.len() && meta.ipk.is_some() {
14820+ let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
14821+ alloc::collections::BTreeMap::new();
14822+ collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
14823+ let rowid_bound = meta
14824+ .ipk
14825+ .and_then(|ipk| ranges.remove(&ipk))
14826+ .or_else(|| rowid_alias_range(where_expr, meta, params));
14827+ if let Some(b) = rowid_bound {
14828+ if b.lower.is_some() {
14829+ conds.push(String::from("rowid>?"));
14830+ }
14831+ if b.upper.is_some() {
14832+ conds.push(String::from("rowid<?"));
1483014833 }
1483114834 }
1483214835 }
@@ -30407,6 +30410,70 @@ fn flip_cmp(op: BinaryOp) -> BinaryOp {
3040730410 }
3040830411}
3040930412
30413+ /// A range on the table's rowid expressed through a `rowid`/`_rowid_`/`oid` alias
30414+ /// (`… AND rowid>?`) — the column-name range collector resolves the INTEGER PRIMARY
30415+ /// KEY by its declared name, so the bare-alias spelling needs this separate walk.
30416+ /// Returns the folded `RangeBound`, or `None` when no such bound is present. The
30417+ /// alias must not be shadowed by a real column of that name.
30418+ fn rowid_alias_range(e: &Expr, meta: &TableMeta, params: &Params) -> Option<RangeBound> {
30419+ fn is_rowid(x: &Expr, meta: &TableMeta) -> bool {
30420+ matches!(x, Expr::Column { column, .. }
30421+ if is_rowid_alias(column)
30422+ && !meta.columns.iter().any(|c| c.name.eq_ignore_ascii_case(column)))
30423+ }
30424+ fn walk(e: &Expr, meta: &TableMeta, params: &Params, out: &mut RangeBound, found: &mut bool) {
30425+ match e {
30426+ Expr::Binary {
30427+ op: BinaryOp::And,
30428+ left,
30429+ right,
30430+ } => {
30431+ walk(left, meta, params, out, found);
30432+ walk(right, meta, params, out, found);
30433+ }
30434+ Expr::Paren(inner) => walk(inner, meta, params, out, found),
30435+ Expr::Binary { op, left, right }
30436+ if matches!(
30437+ op,
30438+ BinaryOp::Lt | BinaryOp::LtEq | BinaryOp::Gt | BinaryOp::GtEq
30439+ ) =>
30440+ {
30441+ if is_rowid(left, meta) {
30442+ if let Some(v) = const_value(right, params) {
30443+ apply_bound(out, *op, v);
30444+ *found = true;
30445+ }
30446+ } else if is_rowid(right, meta) {
30447+ if let Some(v) = const_value(left, params) {
30448+ apply_bound(out, flip_cmp(*op), v);
30449+ *found = true;
30450+ }
30451+ }
30452+ }
30453+ Expr::Between {
30454+ expr,
30455+ low,
30456+ high,
30457+ negated: false,
30458+ } if is_rowid(expr, meta) => {
30459+ if let Some(v) = const_value(low, params) {
30460+ apply_bound(out, BinaryOp::GtEq, v);
30461+ *found = true;
30462+ }
30463+ if let Some(v) = const_value(high, params) {
30464+ apply_bound(out, BinaryOp::LtEq, v);
30465+ *found = true;
30466+ }
30467+ }
30468+ _ => {}
30469+ }
30470+ }
30471+ let mut b = RangeBound::default();
30472+ let mut found = false;
30473+ walk(e, meta, params, &mut b, &mut found);
30474+ found.then_some(b)
30475+ }
30476+
3041030477/// Collect per-column range bounds (`<`/`<=`/`>`/`>=`/`BETWEEN`) from the
3041130478/// top-level `AND` conjuncts of `WHERE`, keyed by column index. Drives an index
3041230479/// range scan; non-range and non-constant terms are ignored (the full `WHERE` is
0 commit comments