1use std::ffi::{CStr, CString};
18use std::os::raw::{c_char, c_int, c_void};
19use std::rc::Rc;
20
21use rindle::{Condition, Dir, Lit, Op, OrderPart, SimpleCondition, ValuePosition};
22use rindle_planner::{ConnectionCostModel, CostModelCost, PlannerConstraint};
23use rusqlite::{ffi, Connection};
24
25use crate::query_builder::ident;
26use crate::stat_fanout::SQLiteStatFanout;
27
28pub const DEFAULT_FANOUT: f64 = 3.0;
31
32pub struct SqliteCostModel {
37 conn: Rc<Connection>,
38 fanout: Rc<SQLiteStatFanout>,
39}
40
41impl SqliteCostModel {
42 pub fn new(conn: Rc<Connection>) -> Self {
43 let fanout = Rc::new(SQLiteStatFanout::new(conn.clone(), DEFAULT_FANOUT));
44 Self { conn, fanout }
45 }
46}
47
48impl ConnectionCostModel for SqliteCostModel {
49 fn estimate(
50 &self,
51 table: &str,
52 sort: &[OrderPart],
53 filters: Option<&Condition>,
54 constraint: Option<&PlannerConstraint>,
55 ) -> CostModelCost {
56 let no_sub_filters = filters.and_then(remove_correlated_subqueries);
59
60 let sql = build_cost_sql(table, sort, no_sub_filters.as_ref(), constraint);
61 let loops = unsafe { read_scanstatus_loops(&self.conn, &sql) };
62 assert!(
63 !loops.is_empty(),
64 "scanstatus returned no loops for cost query: {sql}"
65 );
66 let (rows, startup_cost) = estimate_cost(&loops);
67
68 let fanout = self.fanout.clone();
69 let table: Box<str> = Box::from(table);
70 CostModelCost {
71 rows,
72 startup_cost,
73 fanout: Rc::new(move |columns| fanout.get_fanout(&table, columns)),
74 }
75 }
76}
77
78pub(crate) struct ScanLoop {
84 pub select_id: i32,
85 pub parent_id: i32,
86 pub est: f64,
87 pub explain: String,
88}
89
90pub(crate) unsafe fn read_scanstatus_loops(conn: &Connection, sql: &str) -> Vec<ScanLoop> {
95 try_read_scanstatus_loops(conn, sql)
96 .unwrap_or_else(|| panic!("cost-model prepare failed for: {sql}"))
97}
98
99pub(crate) unsafe fn try_read_scanstatus_loops(
103 conn: &Connection,
104 sql: &str,
105) -> Option<Vec<ScanLoop>> {
106 let db = conn.handle();
107 let csql = CString::new(sql).ok()?;
108 let mut stmt: *mut ffi::sqlite3_stmt = std::ptr::null_mut();
109 let rc = ffi::sqlite3_prepare_v2(db, csql.as_ptr(), -1, &mut stmt, std::ptr::null_mut());
110 if rc != ffi::SQLITE_OK {
111 return None;
112 }
113
114 let flags = ffi::SQLITE_SCANSTAT_COMPLEX as c_int;
115 let mut out = Vec::new();
116 let mut idx: c_int = 0;
117 loop {
118 let mut select_id: c_int = 0;
119 let rc = ffi::sqlite3_stmt_scanstatus_v2(
120 stmt,
121 idx,
122 ffi::SQLITE_SCANSTAT_SELECTID as c_int,
123 flags,
124 &mut select_id as *mut c_int as *mut c_void,
125 );
126 if rc != 0 {
127 break; }
129 let mut parent_id: c_int = 0;
130 ffi::sqlite3_stmt_scanstatus_v2(
131 stmt,
132 idx,
133 ffi::SQLITE_SCANSTAT_PARENTID as c_int,
134 flags,
135 &mut parent_id as *mut c_int as *mut c_void,
136 );
137 let mut est: f64 = 0.0;
138 ffi::sqlite3_stmt_scanstatus_v2(
139 stmt,
140 idx,
141 ffi::SQLITE_SCANSTAT_EST as c_int,
142 flags,
143 &mut est as *mut f64 as *mut c_void,
144 );
145 let mut explain_ptr: *const c_char = std::ptr::null();
146 ffi::sqlite3_stmt_scanstatus_v2(
147 stmt,
148 idx,
149 ffi::SQLITE_SCANSTAT_EXPLAIN as c_int,
150 flags,
151 &mut explain_ptr as *mut *const c_char as *mut c_void,
152 );
153 let explain = if explain_ptr.is_null() {
154 String::new()
155 } else {
156 CStr::from_ptr(explain_ptr).to_string_lossy().into_owned()
157 };
158
159 out.push(ScanLoop {
160 select_id,
161 parent_id,
162 est,
163 explain,
164 });
165 idx += 1;
166 }
167 ffi::sqlite3_finalize(stmt);
168 Some(out)
169}
170
171#[cfg(feature = "scan-stats")]
178pub fn explain_plan(conn: &Connection, sql: &str) -> Option<String> {
179 let loops = unsafe { try_read_scanstatus_loops(conn, sql)? };
180 let text: Vec<String> = loops
181 .into_iter()
182 .map(|l| l.explain)
183 .filter(|e| !e.trim().is_empty())
184 .collect();
185 (!text.is_empty()).then(|| text.join("; "))
186}
187
188fn estimate_cost(loops: &[ScanLoop]) -> (f64, f64) {
195 let mut top: Vec<&ScanLoop> = loops.iter().filter(|l| l.parent_id == 0).collect();
196 top.sort_by_key(|l| l.select_id);
197
198 let mut total_rows = 0.0;
199 let mut total_cost = 0.0;
200 let mut first = true;
201 for op in top {
202 if first {
203 total_rows = op.est;
204 first = false;
205 } else if op.explain.contains("ORDER BY") {
206 total_cost += btree_cost(total_rows);
207 }
208 }
209 (total_rows, total_cost)
210}
211
212pub fn btree_cost(rows: f64) -> f64 {
216 (rows * rows.log2()) / 10.0
217}
218
219fn remove_correlated_subqueries(condition: &Condition) -> Option<Condition> {
226 match condition {
227 Condition::CorrelatedSubquery(_) => None,
228 Condition::Simple(_) => Some(condition.clone()),
229 Condition::And { conditions } => {
230 let kept: Vec<Condition> = conditions
231 .iter()
232 .filter_map(remove_correlated_subqueries)
233 .collect();
234 collapse(kept, true)
235 }
236 Condition::Or { conditions } => {
237 let kept: Vec<Condition> = conditions
238 .iter()
239 .filter_map(remove_correlated_subqueries)
240 .collect();
241 collapse(kept, false)
242 }
243 }
244}
245
246fn collapse(mut kept: Vec<Condition>, is_and: bool) -> Option<Condition> {
247 match kept.len() {
248 0 => None,
249 1 => kept.pop(),
250 _ => Some(if is_and {
251 Condition::And { conditions: kept }
252 } else {
253 Condition::Or { conditions: kept }
254 }),
255 }
256}
257
258fn build_cost_sql(
263 table: &str,
264 sort: &[OrderPart],
265 filters: Option<&Condition>,
266 constraint: Option<&PlannerConstraint>,
267) -> String {
268 let mut sql = format!("SELECT * FROM {}", ident(table));
269
270 let mut wheres: Vec<String> = Vec::new();
271 if let Some(c) = constraint {
272 for col in c {
273 wheres.push(format!("{} = ?", ident(col)));
274 }
275 }
276 if let Some(f) = filters {
277 wheres.push(condition_to_sql(f));
278 }
279 if !wheres.is_empty() {
280 sql.push_str(" WHERE ");
281 sql.push_str(&wheres.join(" AND "));
282 }
283
284 if !sort.is_empty() {
285 sql.push_str(" ORDER BY ");
286 let parts: Vec<String> = sort
287 .iter()
288 .map(|p| {
289 let dir = match p.dir() {
290 Dir::Asc => "ASC",
291 Dir::Desc => "DESC",
292 };
293 format!("{} {}", ident(p.field()), dir)
294 })
295 .collect();
296 sql.push_str(&parts.join(", "));
297 }
298
299 sql
300}
301
302fn condition_to_sql(condition: &Condition) -> String {
303 match condition {
304 Condition::Simple(s) => simple_to_sql(s),
305 Condition::And { conditions } => group(conditions, "AND"),
306 Condition::Or { conditions } => group(conditions, "OR"),
307 Condition::CorrelatedSubquery(_) => unreachable!("subqueries stripped for cost SQL"),
309 }
310}
311
312fn group(conditions: &[Condition], op: &str) -> String {
313 let inner: Vec<String> = conditions.iter().map(condition_to_sql).collect();
314 format!("({})", inner.join(&format!(" {op} ")))
315}
316
317fn simple_to_sql(s: &SimpleCondition) -> String {
318 let left = value_position_to_sql(&s.left);
319 match s.op {
320 Op::In | Op::NotIn => {
321 let op = if s.op == Op::In { "IN" } else { "NOT IN" };
322 let elems = match &s.right {
323 ValuePosition::Literal {
324 value: Lit::Array(items),
325 } => items.iter().map(inline_lit).collect::<Vec<_>>().join(", "),
326 other => value_position_to_sql(other),
327 };
328 format!("{left} {op} ({elems})")
329 }
330 _ => format!(
331 "{left} {} {}",
332 op_to_sql(s.op),
333 value_position_to_sql(&s.right)
334 ),
335 }
336}
337
338fn value_position_to_sql(v: &ValuePosition) -> String {
339 match v {
340 ValuePosition::Column { name } => ident(name),
341 ValuePosition::Literal { value } => inline_lit(value),
342 }
343}
344
345fn op_to_sql(op: Op) -> &'static str {
346 match op {
347 Op::Eq => "=",
348 Op::Ne => "!=",
349 Op::Lt => "<",
350 Op::Le => "<=",
351 Op::Gt => ">",
352 Op::Ge => ">=",
353 Op::Is => "IS",
354 Op::IsNot => "IS NOT",
355 Op::Like | Op::ILike => "LIKE",
358 Op::NotLike | Op::NotILike => "NOT LIKE",
359 Op::In => "IN",
360 Op::NotIn => "NOT IN",
361 }
362}
363
364fn inline_lit(lit: &Lit) -> String {
366 match lit {
367 Lit::Null => "NULL".to_string(),
368 Lit::Bool(b) => if *b { "1" } else { "0" }.to_string(),
369 Lit::Int(i) => i.to_string(),
370 Lit::Number(n) => format!("{n}"),
371 Lit::Str(s) => format!("'{}'", s.replace('\'', "''")),
372 Lit::Array(items) => {
373 let inner: Vec<String> = items.iter().map(json_lit).collect();
376 format!("'[{}]'", inner.join(","))
377 }
378 }
379}
380
381fn json_lit(lit: &Lit) -> String {
382 match lit {
383 Lit::Null => "null".to_string(),
384 Lit::Bool(b) => b.to_string(),
385 Lit::Int(i) => i.to_string(),
386 Lit::Number(n) => format!("{n}"),
387 Lit::Str(s) => format!("{s:?}"),
388 Lit::Array(items) => {
389 let inner: Vec<String> = items.iter().map(json_lit).collect();
390 format!("[{}]", inner.join(","))
391 }
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 fn cset(items: &[&str]) -> PlannerConstraint {
400 items.iter().map(|s| Box::from(*s)).collect()
401 }
402
403 #[test]
404 fn build_cost_sql_shapes() {
405 assert_eq!(
407 build_cost_sql("users", &[], None, None),
408 r#"SELECT * FROM "users""#
409 );
410
411 assert_eq!(
413 build_cost_sql("posts", &[], None, Some(&cset(&["userId"]))),
414 r#"SELECT * FROM "posts" WHERE "userId" = ?"#
415 );
416
417 let sort = vec![OrderPart(Box::from("created"), Dir::Desc)];
419 assert_eq!(
420 build_cost_sql("posts", &sort, None, Some(&cset(&["userId"]))),
421 r#"SELECT * FROM "posts" WHERE "userId" = ? ORDER BY "created" DESC"#
422 );
423 }
424
425 #[test]
426 fn filter_literals_inline() {
427 let filter = Condition::Simple(SimpleCondition {
428 op: Op::Eq,
429 left: ValuePosition::Column {
430 name: Box::from("active"),
431 },
432 right: ValuePosition::Literal {
433 value: Lit::Bool(true),
434 },
435 });
436 assert_eq!(
437 build_cost_sql("users", &[], Some(&filter), None),
438 r#"SELECT * FROM "users" WHERE "active" = 1"#
439 );
440 }
441
442 #[test]
443 fn remove_correlated_subqueries_collapses() {
444 let csq = Condition::CorrelatedSubquery(rindle::CorrelatedSubqueryCondition {
446 related: rindle::CorrelatedSubquery {
447 correlation: rindle::Correlation {
448 parent_field: vec![Box::from("id")],
449 child_field: vec![Box::from("userId")],
450 },
451 subquery: Box::new(rindle::Ast {
452 table: Box::from("posts"),
453 ..Default::default()
454 }),
455 system: None,
456 },
457 op: rindle::ExistsOp::Exists,
458 flip: None,
459 scalar: None,
460 plan_id: None,
461 });
462 assert!(remove_correlated_subqueries(&csq).is_none());
463
464 let simple = Condition::Simple(SimpleCondition {
466 op: Op::Gt,
467 left: ValuePosition::Column {
468 name: Box::from("n"),
469 },
470 right: ValuePosition::Literal {
471 value: Lit::Number(3.0),
472 },
473 });
474 let and = Condition::And {
475 conditions: vec![simple.clone(), csq],
476 };
477 assert_eq!(remove_correlated_subqueries(&and), Some(simple));
478 }
479
480 #[test]
481 fn btree_cost_formula() {
482 assert_eq!(btree_cost(1024.0), (1024.0 * 10.0) / 10.0); assert_eq!(btree_cost(1.0), 0.0); }
486}