1use crate::encoding::encode_composite_key;
4use crate::parser::{BinOp, Expr};
5use crate::types::{IndexDef, IndexKey, IndexKind, InvertedKind, TableSchema, Value};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
10enum CanonicalExpr {
11 Literal(String),
12 Column(String),
13 Function {
14 name: String,
15 args: Vec<CanonicalExpr>,
16 },
17 BinaryOp {
18 op: BinOp,
19 operands: Vec<CanonicalExpr>,
20 },
21 UnaryOp {
22 op: crate::parser::UnaryOp,
23 operand: Box<CanonicalExpr>,
24 },
25 Cast {
26 expr: Box<CanonicalExpr>,
27 data_type: crate::types::DataType,
28 },
29 Other(String),
30}
31
32fn canonicalize(expr: &Expr) -> CanonicalExpr {
33 match expr {
34 Expr::Literal(v) => CanonicalExpr::Literal(format!("{v:?}")),
35 Expr::Column(name) => CanonicalExpr::Column(name.to_ascii_lowercase()),
36 Expr::QualifiedColumn { column, .. } => CanonicalExpr::Column(column.to_ascii_lowercase()),
37 Expr::Function { name, args, .. } => {
38 let canon_args: Vec<CanonicalExpr> = args.iter().map(canonicalize).collect();
39 CanonicalExpr::Function {
40 name: name.to_ascii_lowercase(),
41 args: canon_args,
42 }
43 }
44 Expr::BinaryOp { left, op, right } => {
45 let mut operands = vec![canonicalize(left), canonicalize(right)];
46 if is_commutative(*op) {
47 operands.sort_by_key(|e| format!("{e:?}"));
48 }
49 CanonicalExpr::BinaryOp { op: *op, operands }
50 }
51 Expr::UnaryOp { op, expr: inner } => CanonicalExpr::UnaryOp {
52 op: *op,
53 operand: Box::new(canonicalize(inner)),
54 },
55 Expr::Cast {
56 expr: inner,
57 data_type,
58 } => CanonicalExpr::Cast {
59 expr: Box::new(canonicalize(inner)),
60 data_type: *data_type,
61 },
62 Expr::Collate { expr: inner, .. } => canonicalize(inner),
63 other => CanonicalExpr::Other(format!("{other:?}")),
64 }
65}
66
67fn is_commutative(op: BinOp) -> bool {
68 matches!(op, BinOp::Add | BinOp::Mul | BinOp::And | BinOp::Or)
69}
70
71#[derive(Debug, Clone)]
72pub enum ScanPlan {
73 SeqScan,
74 PkLookup {
75 pk_values: Vec<Value>,
76 },
77 PkRangeScan {
78 start_key: Vec<u8>,
79 range_conds: Vec<(BinOp, Value)>,
80 num_pk_cols: usize,
81 },
82 IndexScan {
83 index_name: String,
84 idx_table: Vec<u8>,
85 prefix: Vec<u8>,
86 num_prefix_cols: usize,
87 range_conds: Vec<(BinOp, Value)>,
88 is_unique: bool,
89 index_columns: Vec<u16>,
90 },
91 InvertedScan {
92 kind: InvertedKind,
93 idx_table: Vec<u8>,
94 column_idx: u16,
95 probe_entries: Vec<Vec<u8>>,
96 recheck_expr: Expr,
97 recheck_needed: bool,
98 },
99}
100
101struct SimplePredicate {
102 col_idx: usize,
103 op: BinOp,
104 value: Value,
105}
106
107fn flatten_and(expr: &Expr) -> Vec<&Expr> {
108 match expr {
109 Expr::BinaryOp {
110 left,
111 op: BinOp::And,
112 right,
113 } => {
114 let mut v = flatten_and(left);
115 v.extend(flatten_and(right));
116 v
117 }
118 _ => vec![expr],
119 }
120}
121
122fn is_comparison(op: BinOp) -> bool {
123 matches!(
124 op,
125 BinOp::Eq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
126 )
127}
128
129fn is_range_op(op: BinOp) -> bool {
130 matches!(op, BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq)
131}
132
133fn flip_op(op: BinOp) -> BinOp {
134 match op {
135 BinOp::Lt => BinOp::Gt,
136 BinOp::LtEq => BinOp::GtEq,
137 BinOp::Gt => BinOp::Lt,
138 BinOp::GtEq => BinOp::LtEq,
139 other => other,
140 }
141}
142
143fn resolve_column_name(expr: &Expr) -> Option<&str> {
144 match expr {
145 Expr::Column(name) => Some(name.as_str()),
146 Expr::QualifiedColumn { column, .. } => Some(column.as_str()),
147 _ => None,
148 }
149}
150
151fn resolve_literal(expr: &Expr) -> Option<Value> {
152 match expr {
153 Expr::Literal(v) => Some(v.clone()),
154 Expr::Parameter(n) => crate::eval::resolve_scoped_param(*n).ok(),
155 Expr::Function { .. } | Expr::Cast { .. } => {
156 let col_map = crate::eval::ColumnMap::new(&[]);
157 let ctx = crate::eval::EvalCtx::new(&col_map, &[]);
158 crate::eval::eval_expr(expr, &ctx).ok()
159 }
160 _ => None,
161 }
162}
163
164fn extract_simple_predicate(expr: &Expr, schema: &TableSchema) -> Option<SimplePredicate> {
165 match expr {
166 Expr::BinaryOp { left, op, right } if is_comparison(*op) => {
167 if let (Some(name), Some(val)) = (resolve_column_name(left), resolve_literal(right)) {
168 let col_idx = schema.column_index(name)?;
169 return Some(SimplePredicate {
170 col_idx,
171 op: *op,
172 value: val,
173 });
174 }
175 if let (Some(val), Some(name)) = (resolve_literal(left), resolve_column_name(right)) {
176 let col_idx = schema.column_index(name)?;
177 return Some(SimplePredicate {
178 col_idx,
179 op: flip_op(*op),
180 value: val,
181 });
182 }
183 None
184 }
185 _ => None,
186 }
187}
188
189fn flatten_between(expr: &Expr, schema: &TableSchema, out: &mut Vec<SimplePredicate>) {
191 match expr {
192 Expr::Between {
193 expr: col_expr,
194 low,
195 high,
196 negated: false,
197 } => {
198 if let (Some(name), Some(lo), Some(hi)) = (
199 resolve_column_name(col_expr),
200 resolve_literal(low),
201 resolve_literal(high),
202 ) {
203 if let Some(col_idx) = schema.column_index(name) {
204 out.push(SimplePredicate {
205 col_idx,
206 op: BinOp::GtEq,
207 value: lo,
208 });
209 out.push(SimplePredicate {
210 col_idx,
211 op: BinOp::LtEq,
212 value: hi,
213 });
214 }
215 }
216 }
217 Expr::BinaryOp {
218 left,
219 op: BinOp::And,
220 right,
221 } => {
222 flatten_between(left, schema, out);
223 flatten_between(right, schema, out);
224 }
225 _ => {}
226 }
227}
228
229pub fn plan_select(schema: &TableSchema, where_clause: &Option<Expr>) -> ScanPlan {
230 plan_select_inner(schema, where_clause, false)
231}
232
233pub fn plan_select_inverted(schema: &TableSchema, where_clause: &Option<Expr>) -> ScanPlan {
234 plan_select_inner(schema, where_clause, true)
235}
236
237fn plan_select_inner(
238 schema: &TableSchema,
239 where_clause: &Option<Expr>,
240 allow_inverted: bool,
241) -> ScanPlan {
242 let where_expr = match where_clause {
243 Some(e) => e,
244 None => return ScanPlan::SeqScan,
245 };
246
247 let predicates = flatten_and(where_expr);
248 let simple: Vec<Option<SimplePredicate>> = predicates
249 .iter()
250 .map(|p| extract_simple_predicate(p, schema))
251 .collect();
252
253 if let Some(plan) = try_pk_lookup(schema, &simple) {
254 return plan;
255 }
256
257 let mut range_preds: Vec<SimplePredicate> = simple
258 .iter()
259 .filter_map(|p| {
260 let p = p.as_ref()?;
261 if is_range_op(p.op) {
262 Some(SimplePredicate {
263 col_idx: p.col_idx,
264 op: p.op,
265 value: p.value.clone(),
266 })
267 } else {
268 None
269 }
270 })
271 .collect();
272 flatten_between(where_expr, schema, &mut range_preds);
273
274 if let Some(plan) = try_pk_range_scan(schema, &range_preds) {
275 return plan;
276 }
277
278 if allow_inverted {
279 if let Some(plan) = try_inverted_scan(schema, where_expr) {
280 return plan;
281 }
282 }
283
284 if let Some(plan) = try_best_index(schema, where_expr, &simple) {
285 return plan;
286 }
287
288 ScanPlan::SeqScan
289}
290
291fn try_inverted_scan(schema: &TableSchema, where_expr: &Expr) -> Option<ScanPlan> {
292 use crate::parser::BinOp as B;
293 let (col_idx, rhs_val, op) = match where_expr {
294 Expr::BinaryOp {
295 left,
296 op: B::JsonContains,
297 right,
298 } => {
299 let name = resolve_column_name(left)?;
300 let col_idx = schema.column_index(name)? as u16;
301 let rhs = resolve_literal(right)?;
302 (col_idx, rhs, B::JsonContains)
303 }
304 Expr::BinaryOp {
305 left,
306 op: B::JsonPathMatch,
307 right,
308 } => {
309 let name = resolve_column_name(left)?;
310 let col_idx = schema.column_index(name)? as u16;
311 let rhs = resolve_literal(right)?;
312 (col_idx, rhs, B::JsonPathMatch)
313 }
314 _ => return None,
315 };
316 let idx = schema.indices.iter().find(|i| {
317 matches!(i.kind, IndexKind::Inverted(_))
318 && i.column_positions_iter()
319 .next()
320 .is_some_and(|c| c == col_idx)
321 && i.predicate_expr.is_none()
322 })?;
323 let kind = match idx.kind {
324 IndexKind::Inverted(k) => k,
325 _ => return None,
326 };
327 match (kind, op) {
328 (InvertedKind::Gin(_), B::JsonContains) => {}
329 (InvertedKind::Fts { .. }, B::JsonPathMatch) => {}
330 _ => return None,
331 }
332 let probe_entries = extract_inverted_probe(&rhs_val, kind)?;
333 if probe_entries.is_empty() {
334 return None;
335 }
336 let recheck_needed = inverted_recheck_needed(kind, &rhs_val);
337 let idx_table = TableSchema::index_table_name(&schema.name, &idx.name);
338 Some(ScanPlan::InvertedScan {
339 kind,
340 idx_table,
341 column_idx: col_idx,
342 probe_entries,
343 recheck_expr: where_expr.clone(),
344 recheck_needed,
345 })
346}
347
348fn inverted_recheck_needed(kind: InvertedKind, rhs: &Value) -> bool {
349 match kind {
350 InvertedKind::Gin(_) => true,
351 InvertedKind::Fts { .. } => match rhs {
352 Value::TsQuery(bytes) => match crate::fts::TsQueryAst::decode(bytes) {
353 Ok(ast) => !fts_ast_exact_for_index(&ast),
354 Err(_) => true,
355 },
356 _ => true,
357 },
358 InvertedKind::Ann { .. } => true,
359 }
360}
361
362fn fts_ast_exact_for_index(ast: &crate::fts::TsQueryAst) -> bool {
363 use crate::fts::TsQueryAst;
364 match ast {
365 TsQueryAst::Lexeme {
366 prefix: false,
367 weight_mask: 0,
368 ..
369 } => true,
370 TsQueryAst::Lexeme { .. } => false,
371 TsQueryAst::And(l, r) => fts_ast_exact_for_index(l) && fts_ast_exact_for_index(r),
372 _ => false,
373 }
374}
375
376pub(crate) fn fts_ast_is_pure_phrase(ast: &crate::fts::TsQueryAst) -> bool {
377 use crate::fts::TsQueryAst;
378 match ast {
379 TsQueryAst::Lexeme {
380 prefix: false,
381 weight_mask: 0,
382 ..
383 } => true,
384 TsQueryAst::Phrase { left, right, .. } => {
385 fts_ast_is_pure_phrase(left) && fts_ast_is_pure_phrase(right)
386 }
387 _ => false,
388 }
389}
390
391fn extract_inverted_probe(rhs: &Value, kind: InvertedKind) -> Option<Vec<Vec<u8>>> {
392 use crate::types::GinOpsClass;
393 match kind {
394 InvertedKind::Gin(ops) => {
395 let entries = crate::json::extract_gin_entries(rhs, ops).ok()?;
396 let filtered: Vec<Vec<u8>> = match ops {
397 GinOpsClass::JsonbOps => entries
398 .into_iter()
399 .filter(|e| !matches!(e.first(), Some(&0x01)))
400 .collect(),
401 GinOpsClass::JsonbPathOps => entries,
402 };
403 Some(filtered)
404 }
405 InvertedKind::Fts { .. } => match rhs {
406 Value::TsQuery(bytes) => {
407 let ast = crate::fts::TsQueryAst::decode(bytes).ok()?;
408 let required = fts_required_lexemes(&ast)?;
409 if required.is_empty() {
410 None
411 } else {
412 Some(required)
413 }
414 }
415 _ => None,
416 },
417 InvertedKind::Ann { .. } => None,
418 }
419}
420
421fn fts_required_lexemes(ast: &crate::fts::TsQueryAst) -> Option<Vec<Vec<u8>>> {
422 let mut out: std::collections::BTreeSet<Vec<u8>> = std::collections::BTreeSet::new();
423 let ok = collect_required(ast, &mut out);
424 if !ok || out.is_empty() {
425 None
426 } else {
427 Some(out.into_iter().collect())
428 }
429}
430
431fn collect_required(
432 ast: &crate::fts::TsQueryAst,
433 out: &mut std::collections::BTreeSet<Vec<u8>>,
434) -> bool {
435 use crate::fts::TsQueryAst;
436 match ast {
437 TsQueryAst::Lexeme { prefix, .. } if *prefix => false,
438 TsQueryAst::Lexeme { lexeme, .. } => {
439 out.insert(lexeme.clone());
440 true
441 }
442 TsQueryAst::And(l, r) => {
443 let lo = collect_required(l, out);
444 let ro = collect_required(r, out);
445 lo || ro
446 }
447 TsQueryAst::Or(..) => false,
448 TsQueryAst::Not(_) => false,
449 TsQueryAst::Phrase { left, right, .. } => {
450 let lo = collect_required(left, out);
451 let ro = collect_required(right, out);
452 lo && ro
453 }
454 }
455}
456
457fn try_pk_range_scan(schema: &TableSchema, range_preds: &[SimplePredicate]) -> Option<ScanPlan> {
458 if schema.primary_key_columns.len() != 1 {
459 return None; }
461 let pk_col = schema.primary_key_columns[0] as usize;
462 let conds: Vec<(BinOp, Value)> = range_preds
463 .iter()
464 .filter(|p| p.col_idx == pk_col)
465 .map(|p| (p.op, p.value.clone()))
466 .collect();
467 if conds.is_empty() {
468 return None;
469 }
470 let start_key = conds
471 .iter()
472 .filter(|(op, _)| matches!(op, BinOp::GtEq | BinOp::Gt))
473 .map(|(_, v)| encode_composite_key(std::slice::from_ref(v)))
474 .min_by(|a, b| a.cmp(b))
475 .unwrap_or_default();
476 Some(ScanPlan::PkRangeScan {
477 start_key,
478 range_conds: conds,
479 num_pk_cols: 1,
480 })
481}
482
483fn try_pk_lookup(schema: &TableSchema, predicates: &[Option<SimplePredicate>]) -> Option<ScanPlan> {
484 let pk_cols = &schema.primary_key_columns;
485 if pk_cols.is_empty() {
487 return None;
488 }
489 let mut pk_values: Vec<Option<Value>> = vec![None; pk_cols.len()];
490
491 for pred in predicates.iter().flatten() {
492 if pred.op == BinOp::Eq {
493 if let Some(pk_pos) = pk_cols.iter().position(|&c| c == pred.col_idx as u16) {
494 pk_values[pk_pos] = Some(pred.value.clone());
495 }
496 }
497 }
498
499 if pk_values.iter().all(|v| v.is_some()) {
500 let values: Vec<Value> = pk_values.into_iter().map(|v| v.unwrap()).collect();
501 Some(ScanPlan::PkLookup { pk_values: values })
502 } else {
503 None
504 }
505}
506
507#[derive(PartialEq, Eq, PartialOrd, Ord)]
508struct IndexScore {
509 num_equality: usize,
510 has_range: bool,
511 is_unique: bool,
512}
513
514fn try_best_index(
515 schema: &TableSchema,
516 where_expr: &Expr,
517 predicates: &[Option<SimplePredicate>],
518) -> Option<ScanPlan> {
519 let mut best_score: Option<IndexScore> = None;
520 let mut best_plan: Option<ScanPlan> = None;
521
522 let conjuncts = flatten_and(where_expr);
523 for idx in &schema.indices {
524 if !partial_predicate_implied(idx, where_expr, &conjuncts) {
525 continue;
526 }
527 if let Some((score, plan)) = try_index_scan(schema, idx, predicates) {
528 if best_score.is_none() || score > *best_score.as_ref().unwrap() {
529 best_score = Some(score);
530 best_plan = Some(plan);
531 }
532 }
533 if !idx.is_pure_column_index() {
534 if let Some((score, plan)) = try_expr_index_scan(schema, idx, &conjuncts) {
535 if best_score.is_none() || score > *best_score.as_ref().unwrap() {
536 best_score = Some(score);
537 best_plan = Some(plan);
538 }
539 }
540 }
541 }
542
543 best_plan
544}
545
546fn try_expr_index_scan(
547 schema: &TableSchema,
548 idx: &IndexDef,
549 conjuncts: &[&Expr],
550) -> Option<(IndexScore, ScanPlan)> {
551 let first_key = idx.keys.first()?;
553 let key_expr = match first_key {
554 IndexKey::Expr { expr, .. } => expr,
555 IndexKey::Column { .. } => return None,
556 };
557 let canonical_key = canonicalize(key_expr);
558
559 let mut matched: Option<Value> = None;
560 for conj in conjuncts {
561 if let Expr::BinaryOp {
562 left,
563 op: BinOp::Eq,
564 right,
565 } = conj
566 {
567 let (expr_side, value_side) = match (left.as_ref(), right.as_ref()) {
568 (Expr::Literal(v), other) | (other, Expr::Literal(v)) => (other, v.clone()),
569 _ => continue,
570 };
571 if canonicalize(expr_side) == canonical_key {
572 matched = Some(value_side);
573 break;
574 }
575 }
576 }
577
578 let value = matched?;
579 let score = IndexScore {
580 num_equality: 1,
581 has_range: false,
582 is_unique: idx.unique,
583 };
584 let prefix = encode_composite_key(&[value]);
585 let idx_table = TableSchema::index_table_name(&schema.name, &idx.name);
586 Some((
587 score,
588 ScanPlan::IndexScan {
589 index_name: idx.name.clone(),
590 idx_table,
591 prefix,
592 num_prefix_cols: 1,
593 range_conds: vec![],
594 is_unique: idx.unique,
595 index_columns: vec![],
596 },
597 ))
598}
599
600fn partial_predicate_implied(idx: &IndexDef, where_expr: &Expr, conjuncts: &[&Expr]) -> bool {
601 let Some(pred) = idx.predicate_expr.as_ref() else {
602 return true;
603 };
604 if expr_structurally_eq(pred, where_expr) {
605 return true;
606 }
607 if conjuncts.iter().any(|c| expr_structurally_eq(pred, c)) {
608 return true;
609 }
610 if let Expr::IsNotNull(target) = pred {
611 if let Expr::Column(col) = target.as_ref() {
612 return conjuncts.iter().any(|c| conjunct_proves_not_null(c, col));
613 }
614 }
615 false
616}
617
618fn expr_structurally_eq(a: &Expr, b: &Expr) -> bool {
619 format!("{a:?}") == format!("{b:?}")
620}
621
622fn conjunct_proves_not_null(expr: &Expr, col: &str) -> bool {
623 let mentions = |e: &Expr| matches!(e, Expr::Column(n) if n.eq_ignore_ascii_case(col));
624 match expr {
625 Expr::BinaryOp {
626 left,
627 op: BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq,
628 right,
629 } => mentions(left) || mentions(right),
630 Expr::IsNotNull(inner) => mentions(inner),
631 _ => false,
632 }
633}
634
635fn try_index_scan(
636 schema: &TableSchema,
637 idx: &IndexDef,
638 predicates: &[Option<SimplePredicate>],
639) -> Option<(IndexScore, ScanPlan)> {
640 let mut used = Vec::new();
641 let mut equality_values: Vec<Value> = Vec::new();
642 let mut range_conds: Vec<(BinOp, Value)> = Vec::new();
643
644 if !idx.is_pure_column_index() {
646 return None;
647 }
648 let idx_columns = idx.columns_vec();
649 for &col_idx in &idx_columns {
650 let mut found_eq = false;
651 for (i, pred) in predicates.iter().enumerate() {
652 if used.contains(&i) {
653 continue;
654 }
655 if let Some(sp) = pred {
656 if sp.col_idx == col_idx as usize && sp.op == BinOp::Eq {
657 equality_values.push(sp.value.clone());
658 used.push(i);
659 found_eq = true;
660 break;
661 }
662 }
663 }
664 if !found_eq {
665 for (i, pred) in predicates.iter().enumerate() {
666 if used.contains(&i) {
667 continue;
668 }
669 if let Some(sp) = pred {
670 if sp.col_idx == col_idx as usize && is_range_op(sp.op) {
671 range_conds.push((sp.op, sp.value.clone()));
672 used.push(i);
673 }
674 }
675 }
676 break;
677 }
678 }
679
680 if equality_values.is_empty() && range_conds.is_empty() {
681 return None;
682 }
683
684 let score = IndexScore {
685 num_equality: equality_values.len(),
686 has_range: !range_conds.is_empty(),
687 is_unique: idx.unique,
688 };
689
690 let prefix = encode_composite_key(&equality_values);
691 let idx_table = TableSchema::index_table_name(&schema.name, &idx.name);
692
693 Some((
694 score,
695 ScanPlan::IndexScan {
696 index_name: idx.name.clone(),
697 idx_table,
698 prefix,
699 num_prefix_cols: equality_values.len(),
700 range_conds,
701 is_unique: idx.unique,
702 index_columns: idx_columns.clone(),
703 },
704 ))
705}
706
707pub fn describe_plan(plan: &ScanPlan, table_schema: &TableSchema) -> String {
708 match plan {
709 ScanPlan::SeqScan => String::new(),
710
711 ScanPlan::PkLookup { pk_values } => {
712 let pk_cols: Vec<&str> = table_schema
713 .primary_key_columns
714 .iter()
715 .map(|&idx| table_schema.columns[idx as usize].name.as_str())
716 .collect();
717 let conditions: Vec<String> = pk_cols
718 .iter()
719 .zip(pk_values.iter())
720 .map(|(col, val)| format!("{col} = {}", format_value(val)))
721 .collect();
722 format!("USING PRIMARY KEY ({})", conditions.join(", "))
723 }
724
725 ScanPlan::PkRangeScan { range_conds, .. } => {
726 let pk_col = &table_schema.columns[table_schema.primary_key_columns[0] as usize].name;
727 let conditions: Vec<String> = range_conds
728 .iter()
729 .map(|(op, val)| format!("{pk_col} {} {}", op_symbol(*op), format_value(val)))
730 .collect();
731 format!("USING PRIMARY KEY RANGE ({})", conditions.join(", "))
732 }
733
734 ScanPlan::IndexScan {
735 index_name,
736 num_prefix_cols,
737 range_conds,
738 index_columns,
739 ..
740 } => {
741 let mut conditions = Vec::new();
742 for &col in index_columns.iter().take(*num_prefix_cols) {
743 let col_idx = col as usize;
744 let col_name = &table_schema.columns[col_idx].name;
745 conditions.push(format!("{col_name} = ?"));
746 }
747 if !range_conds.is_empty() && *num_prefix_cols < index_columns.len() {
748 let col_idx = index_columns[*num_prefix_cols] as usize;
749 let col_name = &table_schema.columns[col_idx].name;
750 for (op, _) in range_conds {
751 conditions.push(format!("{col_name} {} ?", op_symbol(*op)));
752 }
753 }
754 if conditions.is_empty() {
755 format!("USING INDEX {index_name}")
756 } else {
757 format!("USING INDEX {index_name} ({})", conditions.join(", "))
758 }
759 }
760
761 ScanPlan::InvertedScan { .. } => "USING INVERTED INDEX".to_string(),
762 }
763}
764
765fn format_value(val: &Value) -> String {
766 match val {
767 Value::Null => "NULL".into(),
768 Value::Integer(i) => i.to_string(),
769 Value::Real(f) => format!("{f}"),
770 Value::Text(s) => format!("'{s}'"),
771 Value::Blob(_) => "BLOB".into(),
772 Value::Boolean(b) => b.to_string(),
773 Value::Date(d) => format!("DATE '{}'", crate::datetime::format_date(*d)),
774 Value::Time(t) => format!("TIME '{}'", crate::datetime::format_time(*t)),
775 Value::Timestamp(t) => format!("TIMESTAMP '{}'", crate::datetime::format_timestamp(*t)),
776 Value::Interval {
777 months,
778 days,
779 micros,
780 } => format!(
781 "INTERVAL '{}'",
782 crate::datetime::format_interval(*months, *days, *micros)
783 ),
784 Value::Json(s) => format!("JSON '{s}'"),
785 Value::Jsonb(_) => "JSONB '<binary>'".into(),
786 Value::TsVector(_) => "TSVECTOR '<binary>'".into(),
787 Value::TsQuery(_) => "TSQUERY '<binary>'".into(),
788 Value::Array(_) => val.to_string(),
789 Value::Vector(v) => format!("VECTOR({})", v.len()),
790 }
791}
792
793fn op_symbol(op: BinOp) -> &'static str {
794 match op {
795 BinOp::Lt => "<",
796 BinOp::LtEq => "<=",
797 BinOp::Gt => ">",
798 BinOp::GtEq => ">=",
799 BinOp::Eq => "=",
800 BinOp::NotEq => "!=",
801 _ => "?",
802 }
803}
804
805#[cfg(test)]
806#[path = "planner_tests.rs"]
807mod tests;