From d34889f03e3526c80bbec2c54ed9cb40355f94e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Alejandro=20Montoya=20Corte=CC=81s?= Date: Thu, 19 Dec 2024 15:35:21 -0500 Subject: [PATCH] Printer for the plan, ie: EXPLAIN --- Cargo.lock | 18 + Cargo.toml | 1 + crates/expr/src/check.rs | 2 + crates/expr/src/lib.rs | 2 + crates/expr/src/statement.rs | 2 + crates/physical-plan/Cargo.toml | 4 + crates/physical-plan/src/compile.rs | 1 + crates/physical-plan/src/lib.rs | 1 + crates/physical-plan/src/plan.rs | 323 +++++++++++++++-- crates/physical-plan/src/printer.rs | 529 ++++++++++++++++++++++++++++ 10 files changed, 851 insertions(+), 32 deletions(-) create mode 100644 crates/physical-plan/src/printer.rs diff --git a/Cargo.lock b/Cargo.lock index 655f2fe8e04..7ee7f85469a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1383,6 +1383,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "dissimilar" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" + [[package]] name = "duct" version = "0.13.7" @@ -1569,6 +1575,16 @@ dependencies = [ "serde", ] +[[package]] +name = "expect-test" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0be0a561335815e06dab7c62e50353134c796e7a6155402a64bcff66b6a5e0" +dependencies = [ + "dissimilar", + "once_cell", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -5090,6 +5106,8 @@ name = "spacetimedb-physical-plan" version = "1.0.0-rc2" dependencies = [ "derive_more", + "expect-test", + "itertools 0.12.1", "spacetimedb-expr", "spacetimedb-lib", "spacetimedb-primitives", diff --git a/Cargo.toml b/Cargo.toml index 7f700269cf2..e29ec6e196f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,6 +151,7 @@ enum-as-inner = "0.6" enum-map = "2.6.3" env_logger = "0.10" ethnum = { version = "1.5.0", features = ["serde"] } +expect-test = "1.5.0" flate2 = "1.0.24" fs-err = "2.9.0" fs2 = "0.4.3" diff --git a/crates/expr/src/check.rs b/crates/expr/src/check.rs index 5fa2cb0d7d8..a579decc6e6 100644 --- a/crates/expr/src/check.rs +++ b/crates/expr/src/check.rs @@ -144,11 +144,13 @@ pub fn parse_and_type_sub(sql: &str, tx: &impl SchemaView) -> TypingResult(sql: &'a str, tx: &impl SchemaView) -> TypingResult> { + let planning_time = std::time::Instant::now(); let expr = parse_and_type_sub(sql, tx)?; Ok(StatementCtx { statement: Statement::Select(expr), sql, source: StatementSource::Subscription, + planning_time: planning_time.elapsed(), }) } diff --git a/crates/expr/src/lib.rs b/crates/expr/src/lib.rs index a763780e28a..4ad82dbe8ee 100644 --- a/crates/expr/src/lib.rs +++ b/crates/expr/src/lib.rs @@ -173,6 +173,7 @@ pub(crate) fn parse(value: String, ty: &AlgebraicType) -> Result { pub statement: Statement, pub sql: &'a str, pub source: StatementSource, + pub planning_time: std::time::Duration, } diff --git a/crates/expr/src/statement.rs b/crates/expr/src/statement.rs index b9fe8ca8069..3080fb806e2 100644 --- a/crates/expr/src/statement.rs +++ b/crates/expr/src/statement.rs @@ -279,11 +279,13 @@ fn parse_and_type_sql(sql: &str, tx: &impl SchemaView) -> TypingResult(sql: &'a str, tx: &impl SchemaView) -> TypingResult> { + let planning_time = std::time::Instant::now(); let statement = parse_and_type_sql(sql, tx)?; Ok(StatementCtx { statement, sql, source: StatementSource::Query, + planning_time: planning_time.elapsed(), }) } diff --git a/crates/physical-plan/Cargo.toml b/crates/physical-plan/Cargo.toml index 247d06a11ca..f627edd9b21 100644 --- a/crates/physical-plan/Cargo.toml +++ b/crates/physical-plan/Cargo.toml @@ -8,8 +8,12 @@ description = "The physical query plan for the SpacetimeDB query engine" [dependencies] derive_more.workspace = true +itertools.workspace = true spacetimedb-lib.workspace = true spacetimedb-primitives.workspace = true spacetimedb-schema.workspace = true spacetimedb-expr.workspace = true spacetimedb-sql-parser.workspace = true + +[dev-dependencies] +expect-test.workspace = true \ No newline at end of file diff --git a/crates/physical-plan/src/compile.rs b/crates/physical-plan/src/compile.rs index 160ada0ed8c..d537c51539c 100644 --- a/crates/physical-plan/src/compile.rs +++ b/crates/physical-plan/src/compile.rs @@ -125,5 +125,6 @@ pub fn compile(ast: StatementCtx<'_>) -> PhysicalCtx<'_> { plan, sql: ast.sql, source: ast.source, + planning_time: ast.planning_time, } } diff --git a/crates/physical-plan/src/lib.rs b/crates/physical-plan/src/lib.rs index b79989e66e4..3caea2293e1 100644 --- a/crates/physical-plan/src/lib.rs +++ b/crates/physical-plan/src/lib.rs @@ -1,2 +1,3 @@ pub mod compile; pub mod plan; +pub mod printer; diff --git a/crates/physical-plan/src/plan.rs b/crates/physical-plan/src/plan.rs index 99e10b1e914..bcb04d46618 100644 --- a/crates/physical-plan/src/plan.rs +++ b/crates/physical-plan/src/plan.rs @@ -572,7 +572,7 @@ pub struct IxScan { #[derive(Debug, PartialEq, Eq)] pub enum Sarg { Eq(ColId, AlgebraicValue), - Range(ColId, Bound, Bound), + Range(BinOp, ColId, Bound, Bound), } /// A hash join is potentially a bushy join. @@ -699,6 +699,16 @@ pub struct PhysicalCtx<'a> { pub plan: PhysicalProject, pub sql: &'a str, pub source: StatementSource, + pub planning_time: std::time::Duration, +} + +impl<'a> PhysicalCtx<'a> { + pub fn optimize(self) -> Self { + Self { + plan: self.plan.optimize(), + ..self + } + } } pub trait RewriteRule { @@ -1231,9 +1241,15 @@ impl RewriteRule for UniqueHashJoinRule { #[cfg(test)] mod tests { - use std::sync::Arc; - + use crate::plan::PhysicalCtx; + use crate::printer::Explain; + use crate::{ + compile::compile, + plan::{HashJoin, IxJoin, IxScan, PhysicalPlan, PhysicalProject, ProjectField, Sarg, Semi}, + }; + use expect_test::{expect, Expect}; use spacetimedb_expr::check::{compile_sql_sub, SchemaView}; + use spacetimedb_expr::statement::compile_sql_stmt; use spacetimedb_lib::{ db::auth::{StAccess, StTableType}, AlgebraicType, AlgebraicValue, @@ -1244,16 +1260,45 @@ mod tests { schema::{ColumnSchema, ConstraintSchema, IndexSchema, TableSchema}, }; use spacetimedb_sql_parser::ast::BinOp; - - use crate::{ - compile::compile, - plan::{HashJoin, IxJoin, IxScan, PhysicalPlan, PhysicalProject, ProjectField, Sarg, Semi}, - }; + use std::sync::Arc; use super::PhysicalExpr; struct SchemaViewer { schemas: Vec>, + optimize: bool, + show_source: bool, + show_schema: bool, + show_timings: bool, + } + + impl SchemaViewer { + fn new(schemas: Vec>) -> Self { + Self { + schemas, + optimize: false, + show_source: false, + show_schema: false, + show_timings: false, + } + } + + fn optimize(mut self) -> Self { + self.optimize = true; + self + } + fn show_source(mut self) -> Self { + self.show_source = true; + self + } + fn show_schema(mut self) -> Self { + self.show_schema = true; + self + } + fn show_timings(mut self) -> Self { + self.show_timings = true; + self + } } impl SchemaView for SchemaViewer { @@ -1332,10 +1377,7 @@ mod tests { Some(0), )); - let db = SchemaViewer { - schemas: vec![t.clone()], - }; - + let db = SchemaViewer::new(vec![t.clone()]); let sql = "select * from t"; let lp = compile_sql_sub(sql, &db).unwrap(); @@ -1363,9 +1405,7 @@ mod tests { Some(0), )); - let db = SchemaViewer { - schemas: vec![t.clone()], - }; + let db = SchemaViewer::new(vec![t.clone()]); let sql = "select * from t where x = 5"; @@ -1390,11 +1430,11 @@ mod tests { /// Given the following operator notation: /// - /// x: join - /// p: project - /// s: select - /// ix: index scan - /// rx: right index semijoin + /// x: join + /// p: project + /// s: select + /// ix: index scan + /// rx: right index semijoin /// /// This test takes the following logical plan: /// @@ -1456,9 +1496,7 @@ mod tests { Some(0), )); - let db = SchemaViewer { - schemas: vec![u.clone(), l.clone(), b.clone()], - }; + let db = SchemaViewer::new(vec![u.clone(), l.clone(), b.clone()]); let sql = " select b.* @@ -1576,12 +1614,12 @@ mod tests { /// Given the following operator notation: /// - /// x: join - /// p: project - /// s: select - /// ix: index scan - /// rx: right index semijoin - /// rj: right hash semijoin + /// x: join + /// p: project + /// s: select + /// ix: index scan + /// rx: right index semijoin + /// rj: right hash semijoin /// /// This test takes the following logical plan: /// @@ -1647,9 +1685,7 @@ mod tests { Some(0), )); - let db = SchemaViewer { - schemas: vec![m.clone(), w.clone(), p.clone()], - }; + let db = SchemaViewer::new(vec![m.clone(), w.clone(), p.clone()]); let sql = " select p.* @@ -1808,4 +1844,227 @@ mod tests { plan => panic!("unexpected plan: {:#?}", plan), } } + + fn data() -> SchemaViewer { + let m_id = TableId(1); + let w_id = TableId(2); + let p_id = TableId(3); + + let m = Arc::new(schema( + m_id, + "m", + &[("employee", AlgebraicType::U64), ("manager", AlgebraicType::U64)], + &[&[0], &[1]], + &[&[0]], + Some(0), + )); + + let w = Arc::new(schema( + w_id, + "w", + &[("employee", AlgebraicType::U64), ("project", AlgebraicType::U64)], + &[&[0], &[1], &[0, 1]], + &[&[0, 1]], + None, + )); + + let p = Arc::new(schema( + p_id, + "p", + &[("id", AlgebraicType::U64), ("name", AlgebraicType::String)], + &[&[0]], + &[&[0]], + Some(0), + )); + + SchemaViewer::new(vec![m.clone(), w.clone(), p.clone()]) + } + + fn compile_sub<'a>(db: &'a SchemaViewer, sql: &'a str) -> PhysicalCtx<'a> { + let plan = compile_sql_sub(sql, db).unwrap(); + compile(plan) + } + + fn compile_query<'a>(db: &'a SchemaViewer, sql: &'a str) -> PhysicalCtx<'a> { + let plan = compile_sql_stmt(sql, db).unwrap(); + compile(plan) + } + + fn check(db: &SchemaViewer, plan: PhysicalCtx, expect: Expect) { + let plan = if db.optimize { plan.optimize() } else { plan }; + + let explain = Explain::new(&plan); + let explain = if db.show_source { explain.with_source() } else { explain }; + let explain = if db.show_schema { explain.with_schema() } else { explain }; + let explain = if db.show_timings { + explain.with_timings() + } else { + explain + }; + + let explain = explain.build(); + expect.assert_eq(&explain.to_string()); + } + fn check_sub(db: &SchemaViewer, sql: &str, expect: Expect) { + let plan = compile_sub(db, sql); + check(db, plan, expect); + } + + fn check_query(db: &SchemaViewer, sql: &str, expect: Expect) { + let plan = compile_query(db, sql); + check(db, plan, expect); + } + + #[test] + fn plan_metadata() { + let db = data().show_schema().show_source().optimize(); + check_query( + &db, + "SELECT m.* FROM m CROSS JOIN p WHERE m.employee = 1", + expect![ + r#" + Query: SELECT m.* FROM m CROSS JOIN p WHERE m.employee = 1 + Nested Loop + -> Index Scan using Index id 0: (employee) on m + -> Index Cond: (m.employee = U64(1)) + -> Seq Scan on p:2 + Output: id, name + ------- + Schema: + + Label 1: m + Columns: employee, manager + Indexes: Unique(m.employee) + Label 2: p + Columns: id, name + Indexes: Unique(p.id) + "# + ], + ); + } + + #[test] + fn table_scan() { + let db = data(); + check_sub( + &db, + "SELECT * FROM p", + expect![ + r#" + Seq Scan on p + Output: id, name + "# + ], + ); + } + + #[test] + fn table_project() { + let db = data(); + check_query( + &db, + "SELECT id FROM p", + expect![ + r#" + Seq Scan on p + Output: p.id + "# + ], + ); + + check_query( + &db, + "SELECT p.id,m.employee FROM m CROSS JOIN p", + expect![ + r#" + Nested Loop + -> Seq Scan on m + -> Seq Scan on p + Output: p.id, m.employee + "# + ], + ); + } + + #[test] + fn table_scan_filter() { + let db = data(); + + check_sub( + &db, + "SELECT * FROM p WHERE id = 1", + expect![[" + Seq Scan on p + Filter: (p.id = U64(1)) + Output: id, name + "]], + ); + } + + #[test] + fn index_scan_filter() { + let db = data().optimize(); + + check_sub( + &db, + "SELECT m.* FROM m WHERE employee = 1", + expect![[" + Index Scan using Index id 0[employee] on m + Index Cond: (m.employee = U64(1)) + Output: employee, manager + "]], + ); + } + + #[test] + fn cross_join() { + let db = data(); + + check_sub( + &db, + "SELECT p.* FROM m JOIN p", + expect![[" + Nested Loop + -> Seq Scan on m + -> Seq Scan on p + Output: id, name + "]], + ); + } + + #[test] + fn hash_join() { + let db = data(); + + check_sub( + &db, + "SELECT p.* FROM m JOIN p ON m.employee = p.id where m.employee = 1", + expect![[" + Hash Join: All + -> Seq Scan on m + -> Seq Scan on p + Inner Unique: false + Hash Cond: (m.employee = p.id) + Filter: (m.employee = U64(1)) + Output: id, name + "]], + ); + } + + #[test] + fn semi_join() { + let db = data().optimize(); + + check_sub( + &db, + "SELECT p.* FROM m JOIN p ON m.employee = p.id", + expect![[" + Index Join: Rhs + -> Seq Scan on m + Inner Unique: true + Index Cond: (m.employee = p.id) + Output: employee, manager + "]], + ); + } } diff --git a/crates/physical-plan/src/printer.rs b/crates/physical-plan/src/printer.rs new file mode 100644 index 00000000000..1dc8fbd38df --- /dev/null +++ b/crates/physical-plan/src/printer.rs @@ -0,0 +1,529 @@ +use std::collections::BTreeMap; +use std::fmt; + +use itertools::Itertools; + +use crate::plan::{IxScan, Label, PhysicalCtx, PhysicalExpr, PhysicalPlan, PhysicalProject, Sarg, Semi}; +use spacetimedb_expr::StatementSource; +use spacetimedb_primitives::IndexId; +use spacetimedb_schema::def::ConstraintData; +use spacetimedb_schema::schema::{ColumnSchema, IndexSchema, TableSchema}; + +struct Labels<'a> { + // To keep the output consistent between runs... + labels: BTreeMap, +} + +impl<'a> Labels<'a> { + fn insert(&mut self, idx: usize, schema: &'a TableSchema) { + self.labels.insert(idx, schema); + } + + fn field(&self, label: &Label, pos: usize) -> Option> { + if let Some(table) = self.labels.get(&label.0) { + if let Some(field) = table.get_column(pos) { + return Some(FieldExpr { table, field }); + } + }; + None + } +} + +impl<'a> Labels<'a> { + pub fn new() -> Self { + Self { + labels: Default::default(), + } + } +} + +struct PrintExpr<'a> { + expr: &'a PhysicalExpr, + labels: &'a Labels<'a>, +} + +struct PrintSarg<'a> { + expr: &'a Sarg, + label: Label, + labels: &'a Labels<'a>, +} + +pub enum Index<'a> { + Named(&'a str, Vec<&'a ColumnSchema>), + Id(IndexId, Vec<&'a ColumnSchema>), +} + +impl<'a> Index<'a> { + pub fn new(idx: &'a IndexSchema, table: &'a TableSchema) -> Self { + let cols = idx + .index_algorithm + .columns() + .iter() + .map(|x| table.get_column(x.idx()).unwrap()) + .collect_vec(); + + if idx.index_name.is_empty() { + Self::Id(idx.index_id, cols) + } else { + Self::Named(&idx.index_name, cols) + } + } +} + +pub enum JoinKind { + IxJoin, + HashJoin, + NlJoin, +} +/// A formated line of output +pub enum Line<'a> { + TableScan { + table: &'a str, + label: Label, + ident: u16, + }, + Filter { + expr: &'a PhysicalExpr, + ident: u16, + }, + FilterIxScan { + idx: &'a IxScan, + label: Label, + ident: u16, + }, + IxScan { + table_name: &'a str, + index: Index<'a>, + ident: u16, + }, + IxJoin { + semi: &'a Semi, + ident: u16, + }, + HashJoin { + semi: &'a Semi, + ident: u16, + }, + NlJoin { + ident: u16, + }, + JoinExpr { + kind: JoinKind, + unique: bool, + lhs: FieldExpr<'a>, + rhs: FieldExpr<'a>, + ident: u16, + }, +} + +impl<'a> Line<'a> { + pub fn ident(&self) -> usize { + let ident = match self { + Line::TableScan { ident, .. } => *ident, + Line::Filter { ident, .. } => *ident, + Line::FilterIxScan { ident, .. } => *ident, + Line::IxScan { ident, .. } => *ident, + Line::IxJoin { ident, .. } => *ident, + Line::HashJoin { ident, .. } => *ident, + Line::NlJoin { ident, .. } => *ident, + Line::JoinExpr { ident, .. } => *ident, + }; + ident as usize + } +} + +pub struct FieldExpr<'a> { + table: &'a TableSchema, + field: &'a ColumnSchema, +} + +enum Output<'a> { + Unknown, + Star(&'a TableSchema), + Fields(Vec>), +} + +struct Lines<'a> { + lines: Vec>, + labels: Labels<'a>, + output: Output<'a>, +} + +impl<'a> Lines<'a> { + pub fn new() -> Self { + Self { + lines: Vec::new(), + labels: Labels::new(), + output: Output::Unknown, + } + } + + pub fn add(&mut self, line: Line<'a>) { + self.lines.push(line); + } + + pub fn add_label(&mut self, label: Label, name: &'a TableSchema) { + self.labels.insert(label.0, name); + } +} + +fn eval_expr<'a>(lines: &mut Lines<'a>, expr: &'a PhysicalExpr, ident: u16) { + lines.add(Line::Filter { expr, ident }); +} + +fn eval_plan<'a>(lines: &mut Lines<'a>, plan: &'a PhysicalPlan, ident: u16) { + match plan { + PhysicalPlan::TableScan(schema, label) => { + lines.output = Output::Star(schema); + lines.add_label(*label, schema); + lines.add(Line::TableScan { + table: &schema.table_name, + label: *label, + ident, + }); + } + PhysicalPlan::IxScan(idx, label) => { + lines.output = Output::Star(&idx.schema); + lines.add_label(*label, &idx.schema); + let index = idx.schema.indexes.iter().find(|x| x.index_id == idx.index_id).unwrap(); + lines.add(Line::IxScan { + table_name: &idx.schema.table_name, + index: Index::new(index, &idx.schema), + ident, + }); + lines.add(Line::FilterIxScan { + idx, + label: *label, + ident: ident + 2, + }); + } + PhysicalPlan::IxJoin(idx, semi) => { + lines.add_label(idx.rhs_label, &idx.rhs); + lines.add(Line::IxJoin { semi, ident }); + eval_plan(lines, &idx.lhs, ident + 4); + + let lhs = lines + .labels + .field(&idx.lhs_probe_expr.var, idx.lhs_probe_expr.pos) + .unwrap(); + let rhs = lines.labels.field(&idx.rhs_label, idx.rhs_field.idx()).unwrap(); + lines.add(Line::JoinExpr { + kind: JoinKind::IxJoin, + unique: idx.unique, + lhs, + rhs, + ident: ident + 2, + }); + } + PhysicalPlan::HashJoin(idx, semi) => { + lines.add(Line::HashJoin { semi, ident }); + eval_plan(lines, &idx.lhs, ident + 4); + eval_plan(lines, &idx.rhs, ident + 4); + let lhs = lines.labels.field(&idx.lhs_field.var, idx.lhs_field.pos).unwrap(); + let rhs = lines.labels.field(&idx.rhs_field.var, idx.rhs_field.pos).unwrap(); + lines.add(Line::JoinExpr { + kind: JoinKind::HashJoin, + unique: idx.unique, + lhs, + rhs, + ident: ident + 2, + }); + } + PhysicalPlan::NLJoin(lhs, rhs) => { + lines.add(Line::NlJoin { ident }); + eval_plan(lines, lhs, ident + 4); + eval_plan(lines, rhs, ident + 4); + } + PhysicalPlan::Filter(plan, filter) => { + eval_plan(lines, plan, ident); + eval_expr(lines, filter, ident + 2); + } + } +} + +pub struct Explain<'a> { + ctx: &'a PhysicalCtx<'a>, + lines: Vec>, + labels: Labels<'a>, + output: Output<'a>, + show_source: bool, + show_schema: bool, + show_timings: bool, +} + +impl<'a> Explain<'a> { + pub fn new(ctx: &'a PhysicalCtx<'a>) -> Self { + Self { + ctx, + lines: Vec::new(), + labels: Labels::new(), + output: Output::Unknown, + show_source: false, + show_schema: false, + show_timings: false, + } + } + + pub fn with_source(mut self) -> Self { + self.show_source = true; + self + } + + pub fn with_schema(mut self) -> Self { + self.show_schema = true; + self + } + + pub fn with_timings(mut self) -> Self { + self.show_timings = true; + self + } + + fn lines(&self) -> Lines<'a> { + let mut lines = Lines::new(); + match &self.ctx.plan { + PhysicalProject::None(plan) => { + eval_plan(&mut lines, plan, 0); + } + PhysicalProject::Relvar(plan, _) => { + eval_plan(&mut lines, plan, 0); + } + PhysicalProject::Fields(plan, fields) => { + eval_plan(&mut lines, plan, 0); + lines.output = Output::Fields( + fields + .iter() + .map(|(_, expr)| { + let field = lines.labels.field(&expr.var, expr.pos).unwrap(); + field + }) + .collect(), + ); + } + } + lines + } + + pub fn build(self) -> Self { + let lines = self.lines(); + Self { + lines: lines.lines, + labels: lines.labels, + output: lines.output, + ..self + } + } +} + +impl<'a> fmt::Display for PrintExpr<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.expr { + PhysicalExpr::LogOp(op, expr) => { + write!( + f, + "{}", + expr.iter() + .map(|expr| PrintExpr { + expr, + labels: self.labels + }) + .join(&format!(" {} ", op)) + ) + } + PhysicalExpr::BinOp(op, lhs, rhs) => { + write!( + f, + "{} {} {}", + PrintExpr { + expr: lhs, + labels: self.labels + }, + op, + PrintExpr { + expr: rhs, + labels: self.labels + } + ) + } + PhysicalExpr::Value(val) => { + write!(f, "{:?}", val) + } + PhysicalExpr::Field(field) => { + let col = self.labels.field(&field.var, field.pos).unwrap(); + write!(f, "{col}") + } + } + } +} + +impl<'a> fmt::Display for PrintSarg<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.expr { + Sarg::Eq(lhs, rhs) => { + let col = self.labels.field(&self.label, lhs.idx()).unwrap(); + write!(f, "{col} = {:?}", rhs) + } + Sarg::Range(op, col, lower, upper) => { + let col = self.labels.field(&self.label, col.idx()).unwrap(); + write!(f, "{col} {:?} {op}{:?}", lower, upper) + } + } + } +} + +impl<'a> fmt::Display for FieldExpr<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}", self.table.table_name, self.field.col_name) + } +} +impl<'a> fmt::Display for Index<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let cols = match self { + Index::Named(name, cols) => { + write!(f, "Index {name}: ")?; + cols + } + Index::Id(id, cols) => { + write!(f, "Index id {id}: ")?; + cols + } + }; + write!(f, "({})", cols.iter().map(|x| &x.col_name).join(", ")) + } +} + +impl<'a> fmt::Display for Explain<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let ctx = self.ctx; + + if self.show_source { + match ctx.source { + StatementSource::Subscription => write!(f, "Subscription: {}", ctx.sql)?, + StatementSource::Query => write!(f, "Query: {}", ctx.sql)?, + } + + writeln!(f)?; + } + + for line in &self.lines { + let ident = line.ident(); + // If the ident is bigger than 2 we need to generate `->` for each level + let (ident, arrow) = if ident > 2 { (ident - 2, "-> ") } else { (ident, "") }; + write!(f, "{:ident$}{arrow}", "")?; + match line { + Line::TableScan { table, label, ident: _ } => { + write!(f, "Seq Scan on {}:{}", table, label.0)?; + } + Line::IxScan { + table_name, + index, + ident: _, + } => { + write!(f, "Index Scan using {index} on {table_name}")?; + } + Line::Filter { expr, ident: _ } => { + write!( + f, + "Filter: ({})", + PrintExpr { + expr, + labels: &self.labels, + }, + )?; + } + Line::FilterIxScan { idx, label, ident: _ } => { + write!( + f, + "Index Cond: ({})", + PrintSarg { + expr: &idx.arg, + labels: &self.labels, + label: *label, + }, + )?; + } + + Line::IxJoin { semi, ident: _ } => { + write!(f, "Index Join: {semi:?}")?; + } + Line::HashJoin { semi, ident: _ } => { + write!(f, "Hash Join: {semi:?}")?; + } + Line::NlJoin { ident: _ } => { + write!(f, "Nested Loop")?; + } + Line::JoinExpr { + kind, + unique, + lhs, + rhs, + ident: _, + } => { + let kind = match kind { + JoinKind::IxJoin => "Index Cond", + JoinKind::HashJoin => "Hash Cond", + JoinKind::NlJoin => "Loop Cond", + }; + writeln!(f, "Inner Unique: {unique}")?; + write!(f, "{:ident$}{arrow}{kind}: ({} = {})", "", lhs, rhs)?; + } + } + writeln!(f)?; + } + + match &self.output { + Output::Unknown => { + writeln!(f, " Output: ?")?; + } + Output::Star(t) => { + let columns = t.columns().iter().map(|x| &x.col_name).join(", "); + writeln!(f, " Output: {columns}")?; + } + Output::Fields(fields) => { + let columns = fields.iter().map(|x| format!("{}", x)).join(", "); + writeln!(f, " Output: {columns}")?; + } + } + if self.show_schema { + writeln!(f, "-------")?; + writeln!(f, "Schema:")?; + writeln!(f)?; + for (label, schema) in &self.labels.labels { + writeln!(f, "Label {}: {}", label, schema.table_name)?; + let columns = schema.columns().iter().map(|x| &x.col_name).join(", "); + writeln!(f, " Columns: {columns}")?; + + writeln!( + f, + " Indexes: {}", + schema + .constraints + .iter() + .map(|x| { + match &x.data { + ConstraintData::Unique(idx) => format!( + "Unique({})", + idx.columns + .iter() + .map(|x| { + FieldExpr { + table: schema, + field: &schema.columns()[x.idx()], + } + }) + .join(", ") + ), + _ => "".to_string(), + } + }) + .join(", ") + )?; + } + } + + if self.show_timings { + write!(f, "Planning Time: {:?}", ctx.planning_time)?; + } + Ok(()) + } +}