From 419de9efba5e8ee3a5336fb9b23d4a0f577d342f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Alejandro=20Montoya=20Corte=CC=81s?= Date: Wed, 5 Feb 2025 13:06:05 -0500 Subject: [PATCH] Printer for the plan, ie: EXPLAIN * Improve output of plan * Fix print of alias and the output of joins * Allow the test utils of the plan to be used elsewhere * Use the arrow only for nodes, make timing optional * Apply new clippy hints and remove old test logic --- Cargo.lock | 19 + Cargo.toml | 1 + crates/core/Cargo.toml | 1 + crates/core/src/sql/ast.rs | 2 +- crates/expr/src/check.rs | 8 +- crates/expr/src/lib.rs | 2 + crates/expr/src/statement.rs | 8 +- crates/physical-plan/Cargo.toml | 2 + crates/physical-plan/src/compile.rs | 32 +- crates/physical-plan/src/dml.rs | 4 + crates/physical-plan/src/lib.rs | 42 ++ crates/physical-plan/src/plan.rs | 774 +++++++++++++--------------- crates/physical-plan/src/printer.rs | 757 +++++++++++++++++++++++++++ 13 files changed, 1230 insertions(+), 422 deletions(-) create mode 100644 crates/physical-plan/src/printer.rs diff --git a/Cargo.lock b/Cargo.lock index fb3a7ed4558..9ef1cfe2dfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1396,6 +1396,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" @@ -1582,6 +1588,16 @@ dependencies = [ "serde", ] +[[package]] +name = "expect-test" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63af43ff4431e848fb47472a920f14fa71c24de13255a5692e93d4e90302acb0" +dependencies = [ + "dissimilar", + "once_cell", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -4883,6 +4899,7 @@ dependencies = [ "enum-as-inner", "enum-map", "env_logger", + "expect-test", "faststr", "flate2", "fs2", @@ -5122,6 +5139,8 @@ version = "1.0.0-rc4" dependencies = [ "anyhow", "derive_more", + "expect-test", + "itertools 0.12.1", "pretty_assertions", "spacetimedb-expr", "spacetimedb-lib", diff --git a/Cargo.toml b/Cargo.toml index 0c65194d39a..f84f29268f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,6 +155,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/core/Cargo.toml b/crates/core/Cargo.toml index ab8d76e2874..2f6e36eaca8 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -50,6 +50,7 @@ derive_more.workspace = true dirs.workspace = true enum-as-inner.workspace = true enum-map.workspace = true +expect-test.workspace = true flate2.workspace = true fs2.workspace = true futures.workspace = true diff --git a/crates/core/src/sql/ast.rs b/crates/core/src/sql/ast.rs index 4fd4fa4ab32..e365d43f8b8 100644 --- a/crates/core/src/sql/ast.rs +++ b/crates/core/src/sql/ast.rs @@ -977,7 +977,7 @@ pub(crate) fn compile_to_ast( ) -> Result, DBError> { // NOTE: The following ensures compliance with the 1.0 sql api. // Come 1.0, it will have replaced the current compilation stack. - compile_sql_stmt(sql_text, &SchemaViewer::new(tx, auth))?; + compile_sql_stmt(sql_text, &SchemaViewer::new(tx, auth), false)?; let dialect = PostgreSqlDialect {}; let ast = Parser::parse_sql(&dialect, sql_text).map_err(|error| DBError::SqlParser { diff --git a/crates/expr/src/check.rs b/crates/expr/src/check.rs index c1471121746..60e1bc3de3c 100644 --- a/crates/expr/src/check.rs +++ b/crates/expr/src/check.rs @@ -165,11 +165,17 @@ pub fn type_subscription(ast: SqlSelect, tx: &impl SchemaView) -> TypingResult

(sql: &'a str, tx: &impl SchemaView) -> TypingResult> { +pub fn compile_sql_sub<'a>(sql: &'a str, tx: &impl SchemaView, with_timings: bool) -> TypingResult> { + let planning_time = if with_timings { + Some(std::time::Instant::now()) + } else { + None + }; Ok(StatementCtx { statement: Statement::Select(ProjectList::Name(parse_and_type_sub(sql, tx)?)), sql, source: StatementSource::Subscription, + planning_time: planning_time.map(|t| t.elapsed()), }) } diff --git a/crates/expr/src/lib.rs b/crates/expr/src/lib.rs index 25ae6696297..2706e10ea77 100644 --- a/crates/expr/src/lib.rs +++ b/crates/expr/src/lib.rs @@ -305,6 +305,7 @@ pub(crate) fn parse(value: &str, ty: &AlgebraicType) -> anyhow::Result { pub statement: Statement, pub sql: &'a str, pub source: StatementSource, + pub planning_time: Option, } diff --git a/crates/expr/src/statement.rs b/crates/expr/src/statement.rs index 90e3f61ecdd..1957b5a8012 100644 --- a/crates/expr/src/statement.rs +++ b/crates/expr/src/statement.rs @@ -419,12 +419,18 @@ pub fn parse_and_type_sql(sql: &str, tx: &impl SchemaView) -> TypingResult(sql: &'a str, tx: &impl SchemaView) -> TypingResult> { +pub fn compile_sql_stmt<'a>(sql: &'a str, tx: &impl SchemaView, with_timings: bool) -> TypingResult> { + let planning_time = if with_timings { + Some(std::time::Instant::now()) + } else { + None + }; let statement = parse_and_type_sql(sql, tx)?; Ok(StatementCtx { statement, sql, source: StatementSource::Query, + planning_time: planning_time.map(|t| t.elapsed()), }) } diff --git a/crates/physical-plan/Cargo.toml b/crates/physical-plan/Cargo.toml index fb9f62a4a98..68ebb8c6ad6 100644 --- a/crates/physical-plan/Cargo.toml +++ b/crates/physical-plan/Cargo.toml @@ -9,6 +9,8 @@ description = "The physical query plan for the SpacetimeDB query engine" [dependencies] anyhow.workspace = true derive_more.workspace = true +expect-test.workspace = true +itertools.workspace = true spacetimedb-lib.workspace = true spacetimedb-primitives.workspace = true spacetimedb-schema.workspace = true diff --git a/crates/physical-plan/src/compile.rs b/crates/physical-plan/src/compile.rs index bcc211739e5..2d00d72f4b7 100644 --- a/crates/physical-plan/src/compile.rs +++ b/crates/physical-plan/src/compile.rs @@ -5,8 +5,10 @@ use std::collections::HashMap; use crate::dml::{DeletePlan, MutationPlan, UpdatePlan}; use crate::plan::{HashJoin, Label, PhysicalExpr, PhysicalPlan, ProjectListPlan, ProjectPlan, Semi, TupleField}; +use crate::{PhysicalCtx, PlanCtx}; use spacetimedb_expr::expr::{Expr, FieldProject, LeftDeepJoin, ProjectList, ProjectName, RelExpr, Relvar}; -use spacetimedb_expr::statement::DML; +use spacetimedb_expr::statement::{Statement, DML}; +use spacetimedb_expr::StatementCtx; pub trait VarLabel { fn label(&mut self, name: &str) -> Label; @@ -121,6 +123,12 @@ struct NamesToIds { map: HashMap, } +impl NamesToIds { + fn into_map(self) -> HashMap { + self.map + } +} + impl VarLabel for NamesToIds { fn label(&mut self, name: &str) -> Label { if let Some(id) = self.map.get(name) { @@ -143,7 +151,11 @@ pub fn compile_select(project: ProjectName) -> ProjectPlan { /// Note, this utility is applicable to a generic selections. /// In particular, it supports explicit column projections. pub fn compile_select_list(project: ProjectList) -> ProjectListPlan { - compile_project_list(&mut NamesToIds::default(), project) + compile_select_list_raw(&mut NamesToIds::default(), project) +} + +pub fn compile_select_list_raw(var: &mut impl VarLabel, project: ProjectList) -> ProjectListPlan { + compile_project_list(var, project) } /// Converts a logical DML statement into a physical plan, @@ -155,3 +167,19 @@ pub fn compile_dml_plan(stmt: DML) -> MutationPlan { DML::Update(update) => MutationPlan::Update(UpdatePlan::compile(update)), } } + +pub fn compile(ast: StatementCtx<'_>) -> PhysicalCtx<'_> { + let mut vars = NamesToIds::default(); + let plan = match ast.statement { + Statement::Select(project) => PlanCtx::ProjectList(compile_select_list_raw(&mut vars, project)), + Statement::DML(stmt) => PlanCtx::DML(compile_dml_plan(stmt)), + }; + + PhysicalCtx { + plan, + sql: ast.sql, + vars: vars.into_map(), + source: ast.source, + planning_time: None, + } +} diff --git a/crates/physical-plan/src/dml.rs b/crates/physical-plan/src/dml.rs index 2aa7a5bd636..1cfce7be8d2 100644 --- a/crates/physical-plan/src/dml.rs +++ b/crates/physical-plan/src/dml.rs @@ -12,6 +12,7 @@ use spacetimedb_schema::schema::TableSchema; use crate::{compile::compile_select, plan::ProjectPlan}; /// A plan for mutating a table in the database +#[derive(Debug)] pub enum MutationPlan { Insert(InsertPlan), Delete(DeletePlan), @@ -30,6 +31,7 @@ impl MutationPlan { } /// A plan for inserting rows into a table +#[derive(Debug)] pub struct InsertPlan { pub table: Arc, pub rows: Vec, @@ -44,6 +46,7 @@ impl From for InsertPlan { } /// A plan for deleting rows from a table +#[derive(Debug)] pub struct DeletePlan { pub table: Arc, pub filter: ProjectPlan, @@ -77,6 +80,7 @@ impl DeletePlan { } /// A plan for updating rows in a table +#[derive(Debug)] pub struct UpdatePlan { pub table: Arc, pub columns: Vec<(ColId, AlgebraicValue)>, diff --git a/crates/physical-plan/src/lib.rs b/crates/physical-plan/src/lib.rs index c847069d006..16ec8239d98 100644 --- a/crates/physical-plan/src/lib.rs +++ b/crates/physical-plan/src/lib.rs @@ -1,4 +1,46 @@ +use crate::dml::MutationPlan; +use crate::plan::ProjectListPlan; +use anyhow::Result; +use spacetimedb_expr::StatementSource; +use std::collections::HashMap; + pub mod compile; pub mod dml; pub mod plan; +pub mod printer; pub mod rules; + +#[derive(Debug)] +pub enum PlanCtx { + ProjectList(ProjectListPlan), + DML(MutationPlan), +} + +impl PlanCtx { + pub(crate) fn optimize(self) -> Result { + Ok(match self { + Self::ProjectList(plan) => Self::ProjectList(plan.optimize()?), + Self::DML(plan) => Self::DML(plan.optimize()?), + }) + } +} + +/// A physical context for the result of a query compilation. +#[derive(Debug)] +pub struct PhysicalCtx<'a> { + pub plan: PlanCtx, + pub sql: &'a str, + // A map from table names to their labels + pub vars: HashMap, + pub source: StatementSource, + pub planning_time: Option, +} + +impl PhysicalCtx<'_> { + pub fn optimize(self) -> Result { + Ok(Self { + plan: self.plan.optimize()?, + ..self + }) + } +} diff --git a/crates/physical-plan/src/plan.rs b/crates/physical-plan/src/plan.rs index 7facd305b87..c37ca173977 100644 --- a/crates/physical-plan/src/plan.rs +++ b/crates/physical-plan/src/plan.rs @@ -6,7 +6,6 @@ use std::{ use anyhow::Result; use derive_more::From; -use spacetimedb_expr::StatementSource; use spacetimedb_lib::{query::Delta, sats::size_of::SizeOf, AlgebraicValue, ProductValue}; use spacetimedb_primitives::{ColId, ColSet, IndexId}; use spacetimedb_schema::schema::{IndexSchema, TableSchema}; @@ -985,39 +984,87 @@ impl PhysicalExpr { } } -/// A physical context for the result of a query compilation. -pub struct PhysicalCtx<'a> { - pub plan: ProjectListPlan, - pub sql: &'a str, - pub source: StatementSource, +pub mod tests_utils { + use crate::compile::compile; + use crate::printer::{Explain, ExplainOptions}; + use crate::PhysicalCtx; + use expect_test::Expect; + use spacetimedb_expr::check::{compile_sql_sub, SchemaView}; + use spacetimedb_expr::statement::compile_sql_stmt; + + fn sub<'a>(db: &'a impl SchemaView, sql: &'a str) -> PhysicalCtx<'a> { + let plan = compile_sql_sub(sql, db, true).unwrap(); + compile(plan) + } + + fn query<'a>(db: &'a impl SchemaView, sql: &'a str) -> PhysicalCtx<'a> { + let plan = compile_sql_stmt(sql, db, true).unwrap(); + compile(plan) + } + + fn check(plan: PhysicalCtx, options: ExplainOptions, expect: Expect) { + let plan = if options.optimize { + plan.optimize().unwrap() + } else { + plan + }; + + let explain = Explain::new(&plan).with_options(options); + + let explain = explain.build(); + expect.assert_eq(&explain.to_string()); + } + + pub fn check_sub(db: &impl SchemaView, options: ExplainOptions, sql: &str, expect: Expect) { + let plan = sub(db, sql); + check(plan, options, expect); + } + + pub fn check_query(db: &impl SchemaView, options: ExplainOptions, sql: &str, expect: Expect) { + let plan = query(db, sql); + check(plan, options, expect); + } } #[cfg(test)] mod tests { - use std::sync::Arc; + use super::*; - use pretty_assertions::assert_eq; - use spacetimedb_expr::check::{parse_and_type_sub, SchemaView}; + use crate::printer::ExplainOptions; + use expect_test::{expect, Expect}; + use spacetimedb_expr::check::SchemaView; use spacetimedb_lib::{ db::auth::{StAccess, StTableType}, - AlgebraicType, AlgebraicValue, + AlgebraicType, }; use spacetimedb_primitives::{ColId, ColList, ColSet, TableId}; use spacetimedb_schema::{ def::{BTreeAlgorithm, ConstraintData, IndexAlgorithm, UniqueConstraintData}, schema::{ColumnSchema, ConstraintSchema, IndexSchema, TableSchema}, }; - use spacetimedb_sql_parser::ast::BinOp; - - use crate::{ - compile::compile_select, - plan::{HashJoin, IxJoin, IxScan, PhysicalPlan, Sarg, Semi, TupleField}, - }; - - use super::{PhysicalExpr, ProjectPlan}; struct SchemaViewer { schemas: Vec>, + options: ExplainOptions, + } + + impl SchemaViewer { + fn new(schemas: Vec>) -> Self { + Self { + schemas, + options: ExplainOptions::default(), + } + } + + fn with_options(mut self, options: ExplainOptions) -> Self { + self.options = options; + self + } + + fn optimize(mut self, optimize: bool) -> Self { + self.options = self.options.optimize(optimize); + self + } } impl SchemaView for SchemaViewer { @@ -1086,6 +1133,14 @@ mod tests { ) } + fn check_sub(db: &SchemaViewer, sql: &str, expect: Expect) { + tests_utils::check_sub(db, db.options, sql, expect); + } + + fn check_query(db: &SchemaViewer, sql: &str, expect: Expect) { + tests_utils::check_query(db, db.options, sql, expect); + } + /// No rewrites applied to a simple table scan #[test] fn table_scan_noop() { @@ -1100,30 +1155,21 @@ mod tests { Some(0), )); - let db = SchemaViewer { - schemas: vec![t.clone()], - }; - - let sql = "select * from t"; - - let lp = parse_and_type_sub(sql, &db).unwrap(); - let pp = compile_select(lp).optimize().unwrap(); - - match pp { - ProjectPlan::None(PhysicalPlan::TableScan(schema, _, _)) => { - assert_eq!(schema.table_id, t_id); - } - proj => panic!("unexpected project: {:#?}", proj), - }; + let db = SchemaViewer::new(vec![t.clone()]); + check_sub( + &db, + "select * from t", + expect![[r#" +Seq Scan on t + Output: t.id, t.x"#]], + ); } /// No rewrites applied to a table scan + filter #[test] fn filter_noop() { - let t_id = TableId(1); - let t = Arc::new(schema( - t_id, + TableId(1), "t", &[("id", AlgebraicType::U64), ("x", AlgebraicType::U64)], &[&[0]], @@ -1131,38 +1177,25 @@ mod tests { Some(0), )); - let db = SchemaViewer { - schemas: vec![t.clone()], - }; - - let sql = "select * from t where x = 5"; - - let lp = parse_and_type_sub(sql, &db).unwrap(); - let pp = compile_select(lp).optimize().unwrap(); - - match pp { - ProjectPlan::None(PhysicalPlan::Filter(input, PhysicalExpr::BinOp(BinOp::Eq, field, value))) => { - assert!(matches!(*field, PhysicalExpr::Field(TupleField { field_pos: 1, .. }))); - assert!(matches!(*value, PhysicalExpr::Value(AlgebraicValue::U64(5)))); + let db = SchemaViewer::new(vec![t]); - match *input { - PhysicalPlan::TableScan(schema, _, _) => { - assert_eq!(schema.table_id, t_id); - } - plan => panic!("unexpected plan: {:#?}", plan), - } - } - proj => panic!("unexpected project: {:#?}", proj), - }; + check_sub( + &db, + "select * from t where x = 5", + expect![[r#" +Seq Scan on t + Filter: (t.x = U64(5)) + Output: t.id, t.x"#]], + ); } /// 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: /// @@ -1224,132 +1257,41 @@ 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()]).optimize(true); - let sql = " + check_sub( + &db, + " select b.* from u join l as p on u.entity_id = p.entity_id join l as q on p.chunk = q.chunk join b on q.entity_id = b.entity_id - where u.identity = 5 - "; - let lp = parse_and_type_sub(sql, &db).unwrap(); - let pp = compile_select(lp).optimize().unwrap(); - - // Plan: - // rx - // / \ - // rx b - // / \ - // rx l - // / \ - // ix(u) l - let plan = match pp { - ProjectPlan::None(plan) => plan, - proj => panic!("unexpected project: {:#?}", proj), - }; - - // Plan: - // rx - // / \ - // rx b - // / \ - // rx l - // / \ - // ix(u) l - let plan = match plan { - PhysicalPlan::IxJoin( - IxJoin { - lhs, - rhs, - rhs_field: ColId(0), - unique: true, - lhs_field: TupleField { field_pos: 0, .. }, - .. - }, - Semi::Rhs, - ) => { - assert_eq!(rhs.table_id, b_id); - *lhs - } - plan => panic!("unexpected plan: {:#?}", plan), - }; - - // Plan: - // rx - // / \ - // rx l - // / \ - // ix(u) l - let plan = match plan { - PhysicalPlan::IxJoin( - IxJoin { - lhs, - rhs, - rhs_field: ColId(1), - unique: false, - lhs_field: TupleField { field_pos: 1, .. }, - .. - }, - Semi::Rhs, - ) => { - assert_eq!(rhs.table_id, l_id); - *lhs - } - plan => panic!("unexpected plan: {:#?}", plan), - }; - - // Plan: - // rx - // / \ - // ix(u) l - let plan = match plan { - PhysicalPlan::IxJoin( - IxJoin { - lhs, - rhs, - rhs_field: ColId(0), - unique: true, - lhs_field: TupleField { field_pos: 1, .. }, - .. - }, - Semi::Rhs, - ) => { - assert_eq!(rhs.table_id, l_id); - *lhs - } - plan => panic!("unexpected plan: {:#?}", plan), - }; - - // Plan: ix(u) - match plan { - PhysicalPlan::IxScan( - IxScan { - schema, - prefix, - arg: Sarg::Eq(ColId(0), AlgebraicValue::U64(5)), - .. - }, - _, - ) => { - assert!(prefix.is_empty()); - assert_eq!(schema.table_id, u_id); - } - plan => panic!("unexpected plan: {:#?}", plan), - } + where u.identity = 5", + expect![[r#" +Index Join: Rhs on b + -> Index Join: Rhs on q + -> Index Join: Rhs on p + -> Index Scan using Index id 0 on u + Index Cond: (u.identity = U64(5)) + Inner Unique: true + Join Cond: (u.entity_id = p.entity_id) + Inner Unique: false + Join Cond: (p.chunk = q.chunk) + Inner Unique: true + Join Cond: (q.entity_id = b.entity_id) + Output: b.entity_id, b.misc"#]], + ); } /// 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: /// @@ -1415,166 +1357,39 @@ 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()]).optimize(false); - let sql = " + check_sub( + &db, + " select p.* from m join m as n on m.manager = n.manager join w as u on n.employee = u.employee join w as v on u.project = v.project join p on p.id = v.project - where 5 = m.employee and 5 = v.employee - "; - let lp = parse_and_type_sub(sql, &db).unwrap(); - let pp = compile_select(lp).optimize().unwrap(); - - // Plan: - // rx - // / \ - // rj p - // / \ - // rx ix(w) - // / \ - // rx w - // / \ - // ix(m) m - let plan = match pp { - ProjectPlan::None(plan) => plan, - proj => panic!("unexpected project: {:#?}", proj), - }; - - // Plan: - // rx - // / \ - // rj p - // / \ - // rx ix(w) - // / \ - // rx w - // / \ - // ix(m) m - let plan = match plan { - PhysicalPlan::IxJoin( - IxJoin { - lhs, - rhs, - rhs_field: ColId(0), - unique: true, - lhs_field: TupleField { field_pos: 1, .. }, - .. - }, - Semi::Rhs, - ) => { - assert_eq!(rhs.table_id, p_id); - *lhs - } - plan => panic!("unexpected plan: {:#?}", plan), - }; - - // Plan: - // rj - // / \ - // rx ix(w) - // / \ - // rx w - // / \ - // ix(m) m - let (rhs, lhs) = match plan { - PhysicalPlan::HashJoin( - HashJoin { - lhs, - rhs, - lhs_field: TupleField { field_pos: 1, .. }, - rhs_field: TupleField { field_pos: 1, .. }, - unique: true, - }, - Semi::Rhs, - ) => (*rhs, *lhs), - plan => panic!("unexpected plan: {:#?}", plan), - }; - - // Plan: ix(w) - match rhs { - PhysicalPlan::IxScan( - IxScan { - schema, - prefix, - arg: Sarg::Eq(ColId(0), AlgebraicValue::U64(5)), - .. - }, - _, - ) => { - assert!(prefix.is_empty()); - assert_eq!(schema.table_id, w_id); - } - plan => panic!("unexpected plan: {:#?}", plan), - } - - // Plan: - // rx - // / \ - // rx w - // / \ - // ix(m) m - let plan = match lhs { - PhysicalPlan::IxJoin( - IxJoin { - lhs, - rhs, - rhs_field: ColId(0), - unique: false, - lhs_field: TupleField { field_pos: 0, .. }, - .. - }, - Semi::Rhs, - ) => { - assert_eq!(rhs.table_id, w_id); - *lhs - } - plan => panic!("unexpected plan: {:#?}", plan), - }; - - // Plan: - // rx - // / \ - // ix(m) m - let plan = match plan { - PhysicalPlan::IxJoin( - IxJoin { - lhs, - rhs, - rhs_field: ColId(1), - unique: false, - lhs_field: TupleField { field_pos: 1, .. }, - .. - }, - Semi::Rhs, - ) => { - assert_eq!(rhs.table_id, m_id); - *lhs - } - plan => panic!("unexpected plan: {:#?}", plan), - }; - - // Plan: ix(m) - match plan { - PhysicalPlan::IxScan( - IxScan { - schema, - prefix, - arg: Sarg::Eq(ColId(0), AlgebraicValue::U64(5)), - .. - }, - _, - ) => { - assert!(prefix.is_empty()); - assert_eq!(schema.table_id, m_id); - } - plan => panic!("unexpected plan: {:#?}", plan), - } + where 5 = m.employee and 5 = v.employee", + expect![[r#" +Hash Join + -> Hash Join + -> Hash Join + -> Hash Join + -> Seq Scan on m + -> Seq Scan on n + Inner Unique: false + Join Cond: (m.manager = n.manager) + -> Seq Scan on u + Inner Unique: false + Join Cond: (n.employee = u.employee) + -> Seq Scan on v + Inner Unique: false + Join Cond: (u.project = v.project) + -> Seq Scan on p + Inner Unique: false + Join Cond: (p.id = v.project) + Filter: (m.employee = U64(5) AND v.employee = U64(5)) + Output: p.id, p.name"#]], + ); } /// Test single and multi-column index selections @@ -1596,100 +1411,225 @@ mod tests { None, )); - let db = SchemaViewer { - schemas: vec![t.clone()], - }; + let db = SchemaViewer::new(vec![t.clone()]).optimize(true); - let sql = "select * from t where x = 3 and y = 4 and z = 5"; - let lp = parse_and_type_sub(sql, &db).unwrap(); - let pp = compile_select(lp).optimize().unwrap(); + check_sub( + &db, + "select * from t where x = 3 and y = 4 and z = 5", + expect![ + r#" +Index Scan using Index id 2 on t + Index Cond: (t.z = U8(5), t.x = U8(3), t.y = U8(4)) + Output: t.w, t.x, t.y, t.z"# + ], + ); + } - // Select index on (x, y, z) - match pp { - ProjectPlan::None(PhysicalPlan::IxScan( - IxScan { - schema, prefix, arg, .. - }, - _, - )) => { - assert_eq!(schema.table_id, t_id); - assert_eq!(arg, Sarg::Eq(ColId(3), AlgebraicValue::U8(5))); - assert_eq!( - prefix, - vec![(ColId(1), AlgebraicValue::U8(3)), (ColId(2), AlgebraicValue::U8(4))] - ); - } - proj => panic!("unexpected plan: {:#?}", proj), - }; + fn data() -> SchemaViewer { + let m_id = TableId(1); + let w_id = TableId(2); + let p_id = TableId(3); - let sql = "select * from t where x = 3 and y = 4"; - let lp = parse_and_type_sub(sql, &db).unwrap(); - let pp = compile_select(lp).optimize().unwrap(); + let m = Arc::new(schema( + m_id, + "m", + &[("employee", AlgebraicType::U64), ("manager", AlgebraicType::U64)], + &[&[0], &[1]], + &[&[0]], + Some(0), + )); - // Select index on x - let plan = match pp { - ProjectPlan::None(PhysicalPlan::Filter(input, PhysicalExpr::BinOp(BinOp::Eq, field, value))) => { - assert!(matches!(*field, PhysicalExpr::Field(TupleField { field_pos: 2, .. }))); - assert!(matches!(*value, PhysicalExpr::Value(AlgebraicValue::U8(4)))); - *input - } - proj => panic!("unexpected plan: {:#?}", proj), - }; + let w = Arc::new(schema( + w_id, + "w", + &[("employee", AlgebraicType::U64), ("project", AlgebraicType::U64)], + &[&[0], &[1], &[0, 1]], + &[&[0, 1]], + None, + )); - match plan { - PhysicalPlan::IxScan( - IxScan { - schema, prefix, arg, .. - }, - _, - ) => { - assert_eq!(schema.table_id, t_id); - assert_eq!(arg, Sarg::Eq(ColId(1), AlgebraicValue::U8(3))); - assert!(prefix.is_empty()); - } - plan => panic!("unexpected plan: {:#?}", plan), - }; + let p = Arc::new(schema( + p_id, + "p", + &[("id", AlgebraicType::U64), ("name", AlgebraicType::String)], + &[&[0]], + &[&[0]], + Some(0), + )); - let sql = "select * from t where w = 5 and x = 4"; - let lp = parse_and_type_sub(sql, &db).unwrap(); - let pp = compile_select(lp).optimize().unwrap(); + SchemaViewer::new(vec![m.clone(), w.clone(), p.clone()]).with_options(ExplainOptions::default().optimize(false)) + } - // Select index on x - let plan = match pp { - ProjectPlan::None(PhysicalPlan::Filter(input, PhysicalExpr::BinOp(BinOp::Eq, field, value))) => { - assert!(matches!(*field, PhysicalExpr::Field(TupleField { field_pos: 0, .. }))); - assert!(matches!(*value, PhysicalExpr::Value(AlgebraicValue::U8(5)))); - *input - } - proj => panic!("unexpected plan: {:#?}", proj), - }; + #[test] + fn plan_metadata() { + let db = data().with_options(ExplainOptions::new().with_schema().with_source().optimize(true)); + 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 on m + Index Cond: (m.employee = U64(1)) + -> Seq Scan on p:2 + Output: m.employee, m.manager +------- +Schema: + +Label: m, TableId:1 + Columns: employee, manager + Indexes: Index id 0: (m.employee), Index id 1: (m.manager) + Constraints: Constraint id 0: Unique(m.employee) +Label: p, TableId:3 + Columns: id, name + Indexes: Index id 0: (p.id) + Constraints: Constraint id 0: Unique(p.id)"# + ], + ); + } - match plan { - PhysicalPlan::IxScan( - IxScan { - schema, prefix, arg, .. - }, - _, - ) => { - assert_eq!(schema.table_id, t_id); - assert_eq!(arg, Sarg::Eq(ColId(1), AlgebraicValue::U8(4))); - assert!(prefix.is_empty()); - } - plan => panic!("unexpected plan: {:#?}", plan), - }; + #[test] + fn table_scan() { + let db = data(); + check_sub( + &db, + "SELECT * FROM p", + expect![ + r#" + Seq Scan on p + Output: p.id, p.name"# + ], + ); + } - let sql = "select * from t where y = 1"; - let lp = parse_and_type_sub(sql, &db).unwrap(); - let pp = compile_select(lp).optimize().unwrap(); + #[test] + fn table_alias() { + let db = data(); + check_sub( + &db, + "SELECT * FROM p as b", + expect![ + r#" + Seq Scan on b + Output: b.id, b.name"# + ], + ); + check_sub( + &db, + "select p.* + from w + join m as p", + expect![ + r#" + Nested Loop + -> Seq Scan on w + -> Seq Scan on p + Output: p.employee, p.manager"# + ], + ); + } - // Do not select index on (y, z) - match pp { - ProjectPlan::None(PhysicalPlan::Filter(input, PhysicalExpr::BinOp(BinOp::Eq, field, value))) => { - assert!(matches!(*input, PhysicalPlan::TableScan(..))); - assert!(matches!(*field, PhysicalExpr::Field(TupleField { field_pos: 2, .. }))); - assert!(matches!(*value, PhysicalExpr::Value(AlgebraicValue::U8(1)))); - } - proj => panic!("unexpected plan: {:#?}", proj), - }; + #[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![[r#" + Seq Scan on p + Filter: (p.id > U64(1)) + Output: p.id, p.name"#]], + ); + } + + #[test] + fn index_scan_filter() { + let db = data().optimize(true); + + check_sub( + &db, + "SELECT m.* FROM m WHERE employee = 1", + expect![[r#" +Index Scan using Index id 0 on m + Index Cond: (m.employee = U64(1)) + Output: m.employee, m.manager"#]], + ); + } + + #[test] + fn cross_join() { + let db = data(); + + check_sub( + &db, + "SELECT p.* FROM m JOIN p", + expect![[r#" + Nested Loop + -> Seq Scan on m + -> Seq Scan on p + Output: p.id, p.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![[r#" + Hash Join + -> Seq Scan on m + -> Seq Scan on p + Inner Unique: false + Join Cond: (m.employee = p.id) + Filter: (m.employee = U64(1)) + Output: p.id, p.name"#]], + ); + } + + #[test] + fn semi_join() { + let db = data().optimize(true); + + check_sub( + &db, + "SELECT p.* FROM m JOIN p ON m.employee = p.id", + expect![[r#" +Index Join: Rhs on p + -> Seq Scan on m + Inner Unique: true + Join Cond: (m.employee = p.id) + Output: p.id, p.name"#]], + ); } } diff --git a/crates/physical-plan/src/printer.rs b/crates/physical-plan/src/printer.rs new file mode 100644 index 00000000000..2275f424700 --- /dev/null +++ b/crates/physical-plan/src/printer.rs @@ -0,0 +1,757 @@ +use crate::plan::{IxScan, Label, PhysicalExpr, PhysicalPlan, ProjectListPlan, ProjectPlan, Sarg, Semi, TupleField}; +use crate::{PhysicalCtx, PlanCtx}; +use itertools::Itertools; +use spacetimedb_expr::StatementSource; +use spacetimedb_lib::AlgebraicValue; +use spacetimedb_primitives::{ColId, ConstraintId, IndexId}; +use spacetimedb_schema::def::ConstraintData; +use spacetimedb_schema::schema::{IndexSchema, TableSchema}; +use spacetimedb_sql_parser::ast::BinOp; +use std::collections::{BTreeMap, HashMap}; +use std::fmt; +use std::ops::Bound; + +fn range_to_op(lower: &Bound, upper: &Bound) -> BinOp { + match (lower, upper) { + (Bound::Included(_), Bound::Included(_)) => BinOp::Lte, + (Bound::Included(_), Bound::Excluded(_)) => BinOp::Lt, + (Bound::Excluded(_), Bound::Included(_)) => BinOp::Gt, + (Bound::Excluded(_), Bound::Excluded(_)) => BinOp::Gte, + (Bound::Unbounded, Bound::Included(_)) => BinOp::Lte, + (Bound::Unbounded, Bound::Excluded(_)) => BinOp::Lt, + (Bound::Included(_), Bound::Unbounded) => BinOp::Gte, + (Bound::Excluded(_), Bound::Unbounded) => BinOp::Gt, + (Bound::Unbounded, Bound::Unbounded) => BinOp::Eq, + } +} + +/// The options for the printer +/// +/// By default: +/// +/// * `show_source: false` +/// * `show_schema: false` +/// * `show_timings: false` +/// +/// * `optimize: true` +#[derive(Debug, Copy, Clone)] +pub struct ExplainOptions { + pub show_source: bool, + pub show_schema: bool, + pub show_timings: bool, + pub optimize: bool, +} + +impl ExplainOptions { + pub fn new() -> Self { + Self { + show_source: false, + show_schema: false, + show_timings: false, + optimize: true, + } + } + + 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 + } + + pub fn optimize(mut self, optimize: bool) -> Self { + self.optimize = optimize; + self + } +} + +impl Default for ExplainOptions { + fn default() -> Self { + Self::new() + } +} + +/// The name or alias of the `table` with his schema +struct Schema<'a> { + /// The table schema + table: &'a TableSchema, + /// The table name *OR* alias + name: &'a str, +} + +/// A map of labels to table schema and name (that is potentially an alias) +struct Labels<'a> { + // To keep the output consistent between runs... + labels: BTreeMap>, +} + +impl<'a> Labels<'a> { + pub fn new() -> Self { + Self { + labels: Default::default(), + } + } + + /// Insert a new label with the [`TableSchema`] and `name` + fn insert(&mut self, idx: Label, table: &'a TableSchema, name: &'a str) { + self.labels.entry(idx.0).or_insert(Schema { table, name }); + } + + /// Get the table schema by [`Label`] + fn table_by_label(&self, label: &Label) -> Option<&Schema<'a>> { + self.labels.get(&label.0) + } + + fn _field(&self, label: &Label, col: usize) -> Option> { + if let Some(schema) = self.table_by_label(label) { + if let Some(field) = schema.table.get_column(col) { + return Some(Field { + table: schema.name, + field: field.col_name.as_ref(), + }); + } + } + None + } + + /// Get the field by [`Label`] of the `table` and the column index + fn label(&self, label: &Label, col: ColId) -> Option> { + self._field(label, col.idx()) + } + + /// Get the field by [`TupleField`] + fn field(&self, field: &TupleField) -> Option> { + self._field(&field.label, field.field_pos) + } +} + +/// A pretty printer for physical expressions +struct PrintExpr<'a> { + expr: &'a PhysicalExpr, + labels: &'a Labels<'a>, +} + +/// A pretty printer for sargable expressions +struct PrintSarg<'a> { + expr: &'a Sarg, + label: Label, + labels: &'a Labels<'a>, + prefix: &'a [(ColId, AlgebraicValue)], +} + +/// A pretty printer for objects that could have a empty name +pub enum PrintName<'a> { + Named { object: &'a str, name: &'a str }, + Id { object: &'a str, id: usize }, +} + +impl<'a> PrintName<'a> { + fn new(object: &'a str, id: usize, name: &'a str) -> Self { + if name.is_empty() { + Self::Id { object, id } + } else { + Self::Named { object, name } + } + } + + fn index(index_id: IndexId, index_name: &'a str) -> Self { + Self::new("Index", index_id.idx(), index_name) + } + + fn constraint(constraint_id: ConstraintId, constraint_name: &'a str) -> Self { + Self::new("Constraint", constraint_id.idx(), constraint_name) + } +} + +/// A pretty printer for indexes +pub struct PrintIndex<'a> { + name: PrintName<'a>, + cols: Vec>, +} + +impl<'a> PrintIndex<'a> { + pub fn new(idx: &'a IndexSchema, table: &'a TableSchema) -> Self { + let cols = idx + .index_algorithm + .columns() + .iter() + .map(|x| Field { + table: table.table_name.as_ref(), + field: table.get_column(x.idx()).unwrap().col_name.as_ref(), + }) + .collect_vec(); + + Self { + name: PrintName::index(idx.index_id, &idx.index_name), + cols, + } + } +} + +/// A formated line of output +pub enum Line<'a> { + TableScan { + table: &'a str, + label: usize, + ident: u16, + }, + Filter { + expr: &'a PhysicalExpr, + ident: u16, + }, + FilterIxScan { + idx: &'a IxScan, + label: Label, + ident: u16, + }, + IxScan { + table_name: &'a str, + index: PrintName<'a>, + ident: u16, + }, + IxJoin { + semi: &'a Semi, + rhs: String, + ident: u16, + }, + HashJoin { + semi: &'a Semi, + ident: u16, + }, + NlJoin { + ident: u16, + }, + JoinExpr { + unique: bool, + lhs: Field<'a>, + rhs: Field<'a>, + ident: u16, + }, +} + +impl Line<'_> { + 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 + } +} + +/// A `field` in a `table` +#[derive(Debug, Clone)] +pub struct Field<'a> { + table: &'a str, + field: &'a str, +} + +/// The output of the plan, aka the projected columns +enum Output<'a> { + Unknown, + Star(Vec>), + Fields(Vec>), +} + +impl<'a> Output<'a> { + fn tuples(fields: &[TupleField], lines: &Lines<'a>) -> Vec> { + fields.iter().map(|field| lines.labels.field(field).unwrap()).collect() + } + + fn fields(schema: &Schema<'a>) -> Vec> { + schema + .table + .columns() + .iter() + .map(|x| Field { + table: schema.name, + field: &x.col_name, + }) + .collect() + } +} + +/// A list of lines to print +struct Lines<'a> { + lines: Vec>, + labels: Labels<'a>, + output: Output<'a>, + /// A map of label to table name or alias + vars: HashMap, +} + +impl<'a> Lines<'a> { + pub fn new(vars: HashMap) -> Self { + Self { + lines: Vec::new(), + labels: Labels::new(), + output: Output::Unknown, + vars, + } + } + + pub fn add(&mut self, line: Line<'a>) { + self.lines.push(line); + } + + /// Resolve the label to the [`TableSchema`], and add it to the list of labels + pub fn add_table(&mut self, label: Label, table: &'a TableSchema) { + let name = self.vars.get(&label.0).copied().unwrap_or(table.table_name.as_ref()); + self.labels.insert(label, table, name); + } +} + +fn eval_expr<'a>(lines: &mut Lines<'a>, expr: &'a PhysicalExpr, ident: u16) { + lines.add(Line::Filter { expr, ident }); +} + +/// Determine the output of a join using the direction of the [`Semi`] join +fn output_join<'a>(lines: &Lines<'a>, semi: &'a Semi, label_lhs: Label, label_rhs: Label) -> Output<'a> { + match semi { + Semi::Lhs => { + let schema = lines.labels.table_by_label(&label_lhs).unwrap(); + Output::Star(Output::fields(schema)) + } + Semi::Rhs => { + let schema = lines.labels.table_by_label(&label_rhs).unwrap(); + Output::Star(Output::fields(schema)) + } + Semi::All => { + let schema = lines.labels.table_by_label(&label_lhs).unwrap(); + let lhs = Output::fields(schema); + let schema = lines.labels.table_by_label(&label_rhs).unwrap(); + let rhs = Output::fields(schema); + + Output::Star(lhs.iter().chain(rhs.iter()).cloned().collect()) + } + } +} + +fn eval_plan<'a>(lines: &mut Lines<'a>, plan: &'a PhysicalPlan, ident: u16) { + match plan { + PhysicalPlan::TableScan(schema, label, _delta) => { + lines.add_table(*label, schema); + + let schema = lines.labels.table_by_label(label).unwrap(); + + lines.output = Output::Star(Output::fields(schema)); + + lines.add(Line::TableScan { + table: schema.name, + label: label.0, + ident, + }); + } + PhysicalPlan::IxScan(idx, label) => { + lines.add_table(*label, &idx.schema); + let schema = lines.labels.table_by_label(label).unwrap(); + lines.output = Output::Star(Output::fields(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: PrintName::index(idx.index_id, &index.index_name), + ident, + }); + + lines.add(Line::FilterIxScan { + idx, + label: *label, + ident: ident + 4, + }); + } + PhysicalPlan::IxJoin(idx, semi) => { + lines.add_table(idx.rhs_label, &idx.rhs); + //let lhs = lines.labels.table_by_label(&idx.lhs_field.label).unwrap(); + let rhs = lines.labels.table_by_label(&idx.rhs_label).unwrap(); + lines.add(Line::IxJoin { + semi, + ident, + rhs: rhs.name.to_string(), + }); + + eval_plan(lines, &idx.lhs, ident + 2); + + lines.output = output_join(lines, semi, idx.lhs_field.label, idx.rhs_label); + + let lhs = lines.labels.field(&idx.lhs_field).unwrap(); + let rhs = lines.labels.label(&idx.rhs_label, idx.rhs_field).unwrap(); + + lines.add(Line::JoinExpr { + unique: idx.unique, + lhs, + rhs, + ident: ident + 2, + }); + } + PhysicalPlan::HashJoin(idx, semi) => { + lines.add(Line::HashJoin { semi, ident }); + + eval_plan(lines, &idx.lhs, ident + 2); + eval_plan(lines, &idx.rhs, ident + 2); + + lines.output = output_join(lines, semi, idx.lhs_field.label, idx.rhs_field.label); + + let lhs = lines.labels.field(&idx.lhs_field).unwrap(); + let rhs = lines.labels.field(&idx.rhs_field).unwrap(); + + lines.add(Line::JoinExpr { + unique: idx.unique, + lhs, + rhs, + ident: ident + 2, + }); + } + PhysicalPlan::NLJoin(lhs, rhs) => { + lines.add(Line::NlJoin { ident }); + + eval_plan(lines, lhs, ident + 2); + eval_plan(lines, rhs, ident + 2); + } + PhysicalPlan::Filter(plan, filter) => { + eval_plan(lines, plan, ident); + eval_expr(lines, filter, ident + 2); + } + } +} + +/// A pretty printer for physical plans +/// +/// The printer will format the plan in a human-readable format, suitable for the `EXPLAIN` command. +/// +/// It also supports: +/// +/// - Showing the source SQL statement +/// - Showing the schema of the tables +/// - Showing the planning time +pub struct Explain<'a> { + ctx: &'a PhysicalCtx<'a>, + lines: Vec>, + labels: Labels<'a>, + output: Output<'a>, + options: ExplainOptions, +} + +impl<'a> Explain<'a> { + pub fn new(ctx: &'a PhysicalCtx<'a>) -> Self { + Self { + ctx, + lines: Vec::new(), + labels: Labels::new(), + output: Output::Unknown, + options: ExplainOptions::new(), + } + } + + /// Set the options for the printer + pub fn with_options(mut self, options: ExplainOptions) -> Self { + self.options = options; + self + } + + /// Evaluate the plan and build the lines to print + fn lines(&self) -> Lines<'a> { + let mut lines = Lines::new(self.ctx.vars.iter().map(|(x, y)| (*y, x.as_str())).collect()); + + match &self.ctx.plan { + PlanCtx::ProjectList(plan) => match plan { + ProjectListPlan::Name(ProjectPlan::None(plan)) => { + eval_plan(&mut lines, plan, 0); + } + ProjectListPlan::Name(ProjectPlan::Name(plan, label, _count)) => { + eval_plan(&mut lines, plan, 0); + let schema = lines.labels.table_by_label(label).unwrap(); + lines.output = Output::Star(Output::fields(schema)); + } + ProjectListPlan::List(plan, fields) => { + eval_plan(&mut lines, plan, 0); + lines.output = Output::Fields(Output::tuples(fields, &lines)); + } + }, + PlanCtx::DML(_plan) => { + todo!() + } + } + + lines + } + + /// Build the `Explain` output + pub fn build(self) -> Self { + let lines = self.lines(); + Self { + lines: lines.lines, + labels: lines.labels, + output: lines.output, + ..self + } + } +} + +impl fmt::Display for PrintExpr<'_> { + 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).unwrap(); + write!(f, "{col}") + } + } + } +} + +impl fmt::Display for PrintSarg<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.expr { + Sarg::Eq(lhs, rhs) => { + let col = self.labels.label(&self.label, *lhs).unwrap(); + write!(f, "{col} = {:?}", rhs)?; + for (col, val) in self.prefix { + let col = self.labels.label(&self.label, *col).unwrap(); + write!(f, ", {col} = {:?}", val)?; + } + Ok(()) + } + Sarg::Range(col, lower, upper) => { + let col = self.labels.label(&self.label, *col).unwrap(); + let op = range_to_op(lower, upper); + write!(f, "{col} {:?} {op}{:?}", lower, upper) + } + } + } +} + +impl fmt::Display for Field<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}", self.table, self.field) + } +} + +impl fmt::Display for PrintName<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PrintName::Named { object, name } => write!(f, "{} {}", object, name), + PrintName::Id { object, id } => write!(f, "{} id {}", object, id), + } + } +} + +impl fmt::Display for PrintIndex<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: ", self.name)?; + write!(f, "({})", self.cols.iter().join(", ")) + } +} + +impl fmt::Display for Explain<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let ctx = self.ctx; + + if self.options.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(); + let arrow = if ident > 0 { "-> " } else { "" }; + + match line { + Line::TableScan { table, label, ident: _ } => { + if self.options.show_schema { + write!(f, "{:ident$}{arrow}Seq Scan on {table}:{label}", "")?; + } else { + write!(f, "{:ident$}{arrow}Seq Scan on {table}", "")?; + } + } + Line::IxScan { + table_name, + index, + ident: _, + } => { + write!(f, "{:ident$}{arrow}Index Scan using {index} on {table_name}", "")?; + } + Line::Filter { expr, ident: _ } => { + write!(f, "{:ident$}Filter: ", "")?; + write!( + f, + "({})", + PrintExpr { + expr, + labels: &self.labels, + } + )?; + } + Line::FilterIxScan { idx, label, ident: _ } => { + write!(f, "{:ident$}Index Cond: ", "")?; + write!( + f, + "({})", + PrintSarg { + expr: &idx.arg, + prefix: &idx.prefix, + labels: &self.labels, + label: *label, + }, + )?; + } + + Line::IxJoin { semi, rhs, ident: _ } => { + write!(f, "{:ident$}{arrow}Index Join: {semi:?} on {rhs}", "")?; + } + Line::HashJoin { semi, ident: _ } => match semi { + Semi::All => { + write!(f, "{:ident$}{arrow}Hash Join", "")?; + } + semi => { + write!(f, "{:ident$}{arrow}Hash Join: {semi:?}", "")?; + } + }, + Line::NlJoin { ident: _ } => { + write!(f, "{:ident$}{arrow}Nested Loop", "")?; + } + Line::JoinExpr { + unique, + lhs, + rhs, + ident: _, + } => { + writeln!(f, "{:ident$}Inner Unique: {unique}", "")?; + write!(f, "{:ident$}Join Cond: ({} = {})", "", lhs, rhs)?; + } + } + writeln!(f)?; + } + + let columns = match &self.output { + Output::Unknown => None, + Output::Star(fields) => { + let columns = fields.iter().map(|x| format!("{}", x)).join(", "); + Some(columns) + } + Output::Fields(fields) => { + let columns = fields.iter().map(|x| format!("{}", x)).join(", "); + Some(columns) + } + }; + let end = if self.options.show_timings || self.options.show_schema { + "\n" + } else { + "" + }; + if let Some(columns) = columns { + write!(f, " Output: {columns}{end}")?; + } else { + write!(f, " Output: ?{end}")?; + } + + if self.options.show_timings { + let end = if self.options.show_schema { "\n" } else { "" }; + write!(f, "Planning Time: {:?}{end}", ctx.planning_time)?; + } + + if self.options.show_schema { + writeln!(f, "-------")?; + writeln!(f, "Schema:")?; + writeln!(f)?; + for (pos, (_label, schema)) in self.labels.labels.iter().enumerate() { + writeln!(f, "Label: {}, TableId:{}", schema.name, schema.table.table_id)?; + let columns = schema.table.columns().iter().map(|x| &x.col_name).join(", "); + writeln!(f, " Columns: {columns}")?; + + writeln!( + f, + " Indexes: {}", + schema + .table + .indexes + .iter() + .map(|x| PrintIndex::new(x, schema.table)) + .join(", ") + )?; + + write!( + f, + " Constraints: {}", + schema + .table + .constraints + .iter() + .map(|x| { + match &x.data { + ConstraintData::Unique(idx) => format!( + "{}: Unique({})", + PrintName::constraint(x.constraint_id, &x.constraint_name), + idx.columns + .iter() + .map(|x| { + Field { + table: schema.name, + field: &schema.table.columns()[x.idx()].col_name, + } + }) + .join(", ") + ), + _ => "".to_string(), + } + }) + .join(", ") + )?; + + if pos < self.labels.labels.len() - 1 { + writeln!(f)?; + } + } + } + + Ok(()) + } +}