1use crate::sqlselect::{Expr, JoinKind};
58use anyhow::Result;
59use serde_json::Value;
60use std::collections::HashMap;
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum JoinExec {
70 Auto,
71 NestedLoop,
72 Hash,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct JoinChoice {
78 pub kind: JoinKind,
79 pub table: String,
80 pub strategy: Strategy,
81 pub keys: usize,
84 pub left_rows: usize,
85 pub right_rows: usize,
86 pub out_rows: usize,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum Strategy {
91 NestedLoop,
92 Hash,
93}
94
95impl std::fmt::Display for Strategy {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 f.write_str(match self {
98 Strategy::NestedLoop => "Nested Loop",
99 Strategy::Hash => "Hash Join",
100 })
101 }
102}
103
104pub const AUTO_HASH_MIN_PAIRS: usize = 64;
113
114#[derive(Debug, Clone, PartialEq, Eq, Hash)]
120pub enum HKey {
121 Num(u64),
123 Text(String),
125}
126
127fn canon(f: f64) -> u64 {
130 let f = if f == 0.0 { 0.0 } else { f };
131 f.to_bits()
132}
133
134pub fn hkey(v: &Value) -> Option<HKey> {
152 match v {
153 Value::Null => None,
154 Value::Number(n) => Some(match n.as_f64() {
155 Some(f) => HKey::Num(canon(f)),
156 None => HKey::Text(n.to_string()),
159 }),
160 Value::String(s) => match s.parse::<f64>() {
161 Ok(f) => Some(HKey::Num(canon(f))),
162 Err(_) => Some(HKey::Text(s.clone())),
163 },
164 Value::Bool(b) => Some(HKey::Text(if *b { "t" } else { "f" }.to_string())),
167 other => Some(HKey::Text(other.to_string())),
171 }
172}
173
174const PURE_FUNCS: &[&str] = &[
189 "lower",
190 "upper",
191 "length",
192 "char_length",
193 "character_length",
194 "coalesce",
195 "nullif",
196 "int2",
197 "int4",
198 "int8",
199 "text",
200 "quote_ident",
201 "format_type",
202 "array_to_string",
203 "current_schema",
204 "current_database",
205 "current_catalog",
206 "current_user",
207 "session_user",
208 "user",
209 "version",
210 "pg_get_userbyid",
211 "pg_table_is_visible",
212 "pg_type_is_visible",
213 "pg_function_is_visible",
214 "pg_encoding_to_char",
215 "pg_get_expr",
216 "pg_get_indexdef",
217 "pg_get_constraintdef",
218];
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222enum Side {
223 Left,
225 Right,
227 Const,
230 Unusable,
234}
235
236fn side_of(e: &Expr, left: &[String], right: &str) -> Side {
237 let mut saw_left = false;
238 let mut saw_right = false;
239 let mut usable = true;
240 walk(e, left, right, &mut saw_left, &mut saw_right, &mut usable);
241 if !usable || (saw_left && saw_right) {
242 return Side::Unusable;
243 }
244 match (saw_left, saw_right) {
245 (true, false) => Side::Left,
246 (false, true) => Side::Right,
247 (false, false) => Side::Const,
248 (true, true) => unreachable!("handled above"),
249 }
250}
251
252fn walk(
253 e: &Expr,
254 left: &[String],
255 right: &str,
256 saw_left: &mut bool,
257 saw_right: &mut bool,
258 usable: &mut bool,
259) {
260 match e {
261 Expr::Column { qual, .. } => match qual {
262 Some(q) => {
263 if q.eq_ignore_ascii_case(right) {
264 *saw_right = true;
265 } else if left.iter().any(|b| b.eq_ignore_ascii_case(q)) {
266 *saw_left = true;
267 } else {
268 *usable = false;
271 }
272 }
273 None => *usable = false,
278 },
279 Expr::Literal(_) => {}
280 Expr::Star | Expr::QualifiedStar(_) => *usable = false,
281 Expr::Func { name, args } => {
282 if !PURE_FUNCS.iter().any(|f| f.eq_ignore_ascii_case(name)) {
283 *usable = false;
284 }
285 for a in args {
286 walk(a, left, right, saw_left, saw_right, usable);
287 }
288 }
289 Expr::Agg { .. } => *usable = false,
291 Expr::Case { operand, whens, else_ } => {
292 if let Some(o) = operand {
293 walk(o, left, right, saw_left, saw_right, usable);
294 }
295 for (w, t) in whens {
296 walk(w, left, right, saw_left, saw_right, usable);
297 walk(t, left, right, saw_left, saw_right, usable);
298 }
299 if let Some(x) = else_ {
300 walk(x, left, right, saw_left, saw_right, usable);
301 }
302 }
303 Expr::Binary { left: l, right: r, .. } => {
304 walk(l, left, right, saw_left, saw_right, usable);
305 walk(r, left, right, saw_left, saw_right, usable);
306 }
307 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
308 walk(expr, left, right, saw_left, saw_right, usable);
309 }
310 Expr::InList { expr, list, .. } => {
311 walk(expr, left, right, saw_left, saw_right, usable);
312 for i in list {
313 walk(i, left, right, saw_left, saw_right, usable);
314 }
315 }
316 Expr::Index { expr, index } => {
317 walk(expr, left, right, saw_left, saw_right, usable);
318 walk(index, left, right, saw_left, saw_right, usable);
319 }
320 Expr::ArrayLit(items) => {
321 for i in items {
322 walk(i, left, right, saw_left, saw_right, usable);
323 }
324 }
325 Expr::Subquery(_) | Expr::Exists { .. } | Expr::ArrayQuery(_) | Expr::InSubquery { .. } => {
328 *usable = false
329 }
330 Expr::Quantified { left: l, right: r, .. } => {
331 walk(l, left, right, saw_left, saw_right, usable);
332 walk(r, left, right, saw_left, saw_right, usable);
333 }
334 }
335}
336
337fn conjuncts<'a>(e: &'a Expr, out: &mut Vec<&'a Expr>) {
343 match e {
344 Expr::Binary { op, left, right } if op == "AND" => {
345 conjuncts(left, out);
346 conjuncts(right, out);
347 }
348 other => out.push(other),
349 }
350}
351
352pub fn hash_keys(on: Option<&Expr>, left: &[String], right: &str) -> Vec<(Expr, Expr)> {
359 let Some(on) = on else { return vec![] };
360 let mut parts = vec![];
361 conjuncts(on, &mut parts);
362 let mut keys = vec![];
363 for p in parts {
364 let Expr::Binary { op, left: l, right: r } = p else { continue };
365 if op != "=" {
368 continue;
369 }
370 match (side_of(l, left, right), side_of(r, left, right)) {
371 (Side::Left, Side::Right) => keys.push(((**l).clone(), (**r).clone())),
372 (Side::Right, Side::Left) => keys.push(((**r).clone(), (**l).clone())),
373 _ => {}
374 }
375 }
376 keys
377}
378
379pub fn choose(exec: JoinExec, keys: usize, left_rows: usize, right_rows: usize) -> Strategy {
381 if keys == 0 {
382 return Strategy::NestedLoop;
385 }
386 match exec {
387 JoinExec::NestedLoop => Strategy::NestedLoop,
388 JoinExec::Hash => Strategy::Hash,
389 JoinExec::Auto => {
390 if left_rows.saturating_mul(right_rows) > AUTO_HASH_MIN_PAIRS {
391 Strategy::Hash
392 } else {
393 Strategy::NestedLoop
394 }
395 }
396 }
397}
398
399pub struct HashSide {
405 buckets: HashMap<Vec<HKey>, Vec<usize>>,
406 pub null_keyed: Vec<usize>,
410}
411
412impl HashSide {
413 pub fn build(
416 n: usize,
417 mut key_of: impl FnMut(usize) -> Result<Option<Vec<HKey>>>,
418 ) -> Result<Self> {
419 let mut buckets: HashMap<Vec<HKey>, Vec<usize>> = HashMap::new();
420 let mut null_keyed = vec![];
421 for i in 0..n {
422 match key_of(i)? {
423 Some(k) => buckets.entry(k).or_default().push(i),
426 None => null_keyed.push(i),
427 }
428 }
429 Ok(Self { buckets, null_keyed })
430 }
431
432 pub fn probe(&self, key: &[HKey]) -> &[usize] {
434 self.buckets.get(key).map(|v| v.as_slice()).unwrap_or(&[])
435 }
436
437 pub fn distinct_keys(&self) -> usize {
438 self.buckets.len()
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use crate::sqlselect::parse;
446 use serde_json::json;
447
448 fn corpus() -> Vec<Value> {
453 vec![
454 Value::Null,
455 json!(0),
456 json!(-0.0),
457 json!(0.0),
458 json!(1),
459 json!(1.0),
460 json!(-1),
461 json!(1000),
462 json!(0.1),
463 json!(9007199254740993i64),
464 json!(9007199254740992i64),
465 json!("0"),
466 json!("1"),
467 json!("1.0"),
468 json!("1.00"),
469 json!("01"),
470 json!("1e3"),
471 json!(" 1"),
472 json!("1abc"),
473 json!(""),
474 json!("t"),
475 json!("f"),
476 json!("true"),
477 json!("nan"),
478 json!("inf"),
479 json!("-0"),
480 json!("abc"),
481 json!("ABC"),
482 json!(true),
483 json!(false),
484 json!([1, 2]),
485 json!("[1,2]"),
486 json!({"a": 1}),
487 json!(r#"{"a":1}"#),
488 ]
489 }
490
491 fn equals(a: &Value, b: &Value) -> bool {
494 let sel = parse("SELECT l.v = r.v AS eq FROM l JOIN r ON 1 = 1").expect("parses");
495 let (la, lb) = (a.clone(), b.clone());
496 let resolve = move |t: &str| -> Result<Option<Box<dyn crate::sqlselect::Relation>>> {
497 Ok(Some(crate::sqlselect::from_vec(match t {
498 "l" => vec![json!({"v": la})],
499 _ => vec![json!({"v": lb})],
500 })))
501 };
502 let (_, rows) = crate::sqlselect::execute(&sel, &resolve).expect("runs");
503 rows.first().and_then(|r| r.get("eq")).and_then(|v| v.as_bool()) == Some(true)
504 }
505
506 #[test]
507 fn equality_implies_same_bucket() {
508 let c = corpus();
509 let mut equal_pairs = 0;
510 for a in &c {
511 for b in &c {
512 if !equals(a, b) {
513 continue;
514 }
515 equal_pairs += 1;
516 let (ka, kb) = (hkey(a), hkey(b));
517 assert!(
518 ka.is_some() && kb.is_some(),
519 "{a:?} = {b:?} is TRUE but a key is unhashable"
520 );
521 assert_eq!(
522 ka, kb,
523 "{a:?} = {b:?} is TRUE but they bucket apart — the hash \
524 join would LOSE this match"
525 );
526 }
527 }
528 assert!(equal_pairs > 40, "corpus proved too little: {equal_pairs} equal pairs");
531 }
532
533 #[test]
534 fn null_never_hashes() {
535 assert_eq!(hkey(&Value::Null), None);
536 for v in corpus() {
538 assert!(!equals(&Value::Null, &v));
539 assert!(!equals(&v, &Value::Null));
540 }
541 }
542
543 #[test]
544 fn the_non_transitive_case_is_real_and_survives() {
545 assert!(equals(&json!(1), &json!("1")));
547 assert!(equals(&json!(1), &json!("1.0")));
548 assert!(!equals(&json!("1"), &json!("1.0")));
549 assert_eq!(hkey(&json!(1)), hkey(&json!("1")));
552 assert_eq!(hkey(&json!(1)), hkey(&json!("1.0")));
553 assert_eq!(hkey(&json!("1")), hkey(&json!("1.0")));
554 }
555
556 #[test]
557 fn signed_zero_shares_a_bucket() {
558 assert_eq!(hkey(&json!(0.0)), hkey(&json!(-0.0)));
559 assert_eq!(hkey(&json!(0)), hkey(&json!(-0.0)));
560 }
561
562 #[test]
563 fn bool_and_its_text_share_a_bucket() {
564 assert!(equals(&json!(true), &json!("t")));
565 assert_eq!(hkey(&json!(true)), hkey(&json!("t")));
566 assert_eq!(hkey(&json!(false)), hkey(&json!("f")));
567 }
568
569 #[test]
570 fn composite_and_its_json_text_share_a_bucket() {
571 assert_eq!(hkey(&json!([1, 2])), hkey(&json!("[1,2]")));
572 }
573
574 fn keys_for(sql: &str) -> Vec<(Expr, Expr)> {
577 let s = parse(sql).expect("parses");
578 let left = vec![s.from.as_ref().unwrap().binding()];
579 let j = &s.joins[0];
580 hash_keys(j.on.as_ref(), &left, &j.table.binding())
581 }
582
583 #[test]
584 fn simple_equijoin_yields_one_key() {
585 assert_eq!(keys_for("SELECT 1 FROM a JOIN b ON a.x = b.y").len(), 1);
586 }
587
588 #[test]
589 fn key_pairs_are_normalised_left_then_right() {
590 let k = keys_for("SELECT 1 FROM a JOIN b ON b.y = a.x");
593 assert_eq!(k.len(), 1);
594 assert_eq!(k[0].0, Expr::Column { qual: Some("a".into()), name: "x".into() });
595 assert_eq!(k[0].1, Expr::Column { qual: Some("b".into()), name: "y".into() });
596 }
597
598 #[test]
599 fn multiple_equality_conjuncts_all_become_keys() {
600 assert_eq!(keys_for("SELECT 1 FROM a JOIN b ON a.x = b.x AND a.y = b.y").len(), 2);
601 }
602
603 #[test]
604 fn non_equality_conjuncts_are_left_to_the_evaluator() {
605 assert_eq!(keys_for("SELECT 1 FROM a JOIN b ON a.x = b.x AND a.n > b.n").len(), 1);
608 }
609
610 #[test]
611 fn or_is_never_split() {
612 assert!(keys_for("SELECT 1 FROM a JOIN b ON a.x = b.x OR a.y = b.y").is_empty());
613 }
614
615 #[test]
616 fn a_constant_side_is_not_a_key() {
617 assert!(keys_for("SELECT 1 FROM a JOIN b ON a.x = 5").is_empty());
618 assert!(keys_for("SELECT 1 FROM a JOIN b ON 1 = 1").is_empty());
619 }
620
621 #[test]
622 fn same_side_equality_is_not_a_key() {
623 assert!(keys_for("SELECT 1 FROM a JOIN b ON a.x = a.y").is_empty());
624 }
625
626 #[test]
627 fn a_bare_column_is_refused() {
628 assert!(keys_for("SELECT 1 FROM a JOIN b ON x = b.y").is_empty());
631 assert!(keys_for("SELECT 1 FROM a JOIN b ON a.x = y").is_empty());
632 }
633
634 #[test]
635 fn an_expression_key_is_allowed_when_it_reads_one_side() {
636 assert_eq!(keys_for("SELECT 1 FROM a JOIN b ON lower(a.x) = lower(b.y)").len(), 1);
637 assert_eq!(keys_for("SELECT 1 FROM a JOIN b ON a.x = b.y::text").len(), 1);
638 }
639
640 #[test]
641 fn a_key_spanning_both_sides_is_refused() {
642 assert!(keys_for("SELECT 1 FROM a JOIN b ON coalesce(a.x, b.y) = b.z").is_empty());
643 }
644
645 #[test]
646 fn an_unknown_function_is_refused() {
647 let s = parse("SELECT 1 FROM a JOIN b ON a.x = b.y").expect("parses");
649 let left = vec!["a".to_string()];
650 let on = Expr::Binary {
651 op: "=".into(),
652 left: Box::new(Expr::Column { qual: Some("a".into()), name: "x".into() }),
653 right: Box::new(Expr::Func {
654 name: "random".into(),
655 args: vec![Expr::Column { qual: Some("b".into()), name: "y".into() }],
656 }),
657 };
658 assert!(hash_keys(Some(&on), &left, &s.joins[0].table.binding()).is_empty());
659 }
660
661 #[test]
662 fn cross_join_has_no_keys() {
663 assert!(keys_for("SELECT 1 FROM a CROSS JOIN b").is_empty());
664 }
665
666 #[test]
667 fn a_second_join_may_key_off_either_earlier_relation() {
668 let s = parse("SELECT 1 FROM a JOIN b ON a.x = b.x JOIN c ON b.y = c.y").expect("parses");
669 let left = vec!["a".to_string(), "b".to_string()];
670 let j = &s.joins[1];
671 assert_eq!(hash_keys(j.on.as_ref(), &left, &j.table.binding()).len(), 1);
672 }
673
674 #[test]
677 fn no_keys_forces_the_nested_loop_even_when_hash_is_requested() {
678 assert_eq!(choose(JoinExec::Hash, 0, 1000, 1000), Strategy::NestedLoop);
679 }
680
681 #[test]
682 fn auto_stays_on_the_reference_path_for_small_inputs() {
683 assert_eq!(choose(JoinExec::Auto, 1, 4, 4), Strategy::NestedLoop);
684 assert_eq!(choose(JoinExec::Auto, 1, 8, 8), Strategy::NestedLoop);
685 assert_eq!(choose(JoinExec::Auto, 1, 8, 9), Strategy::Hash);
686 }
687
688 #[test]
689 fn forcing_is_honoured_so_differential_tests_mean_something() {
690 assert_eq!(choose(JoinExec::NestedLoop, 2, 10_000, 10_000), Strategy::NestedLoop);
691 assert_eq!(choose(JoinExec::Hash, 2, 1, 1), Strategy::Hash);
692 }
693
694 #[test]
697 fn build_preserves_ascending_row_order_within_a_bucket() {
698 let vals = vec![json!("a"), json!("b"), json!("a"), json!("a")];
699 let side = HashSide::build(vals.len(), |i| Ok(hkey(&vals[i]).map(|k| vec![k])))
700 .expect("builds");
701 let k = vec![hkey(&json!("a")).unwrap()];
702 assert_eq!(side.probe(&k), &[0, 2, 3]);
703 assert_eq!(side.distinct_keys(), 2);
704 }
705
706 #[test]
707 fn null_keyed_rows_are_set_aside_not_dropped() {
708 let vals = vec![json!("a"), Value::Null, json!("b")];
709 let side = HashSide::build(vals.len(), |i| Ok(hkey(&vals[i]).map(|k| vec![k])))
710 .expect("builds");
711 assert_eq!(side.null_keyed, vec![1]);
712 assert!(side.probe(&[HKey::Text("zzz".into())]).is_empty());
713 assert_eq!(side.distinct_keys(), 2);
715 }
716
717 #[test]
718 fn a_compound_key_matches_only_on_every_column() {
719 let rows = vec![(json!(1), json!("x")), (json!(1), json!("y"))];
720 let side = HashSide::build(rows.len(), |i| {
721 Ok(match (hkey(&rows[i].0), hkey(&rows[i].1)) {
722 (Some(a), Some(b)) => Some(vec![a, b]),
723 _ => None,
724 })
725 })
726 .expect("builds");
727 let want = vec![hkey(&json!(1)).unwrap(), hkey(&json!("x")).unwrap()];
728 assert_eq!(side.probe(&want), &[0]);
729 }
730}